q5play 4.1.1 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/package.json +2 -2
  2. package/py.typed +0 -0
  3. package/q5play.d.ts +1387 -163
  4. package/q5play.js +543 -146
  5. package/q5play.pyi +5007 -0
package/q5play.js CHANGED
@@ -12,12 +12,12 @@
12
12
  * |__/ |__/ \______/
13
13
  *
14
14
  * @package q5play
15
- * @version 4.1
15
+ * @version 4.2
16
16
  * @author quinton-ashley
17
17
  * @website https://q5play.org
18
18
  */
19
19
 
20
- let q5play_version = '4.1';
20
+ let q5play_version = '4.2';
21
21
 
22
22
  if (typeof globalThis.Q5 == 'undefined') {
23
23
  console.error('q5play requires q5.js to be loaded first. Visit https://q5js.org to learn more.');
@@ -85,6 +85,7 @@ async function q5playPreSetup(q) {
85
85
  b2World_OverlapShape,
86
86
  b2World_CastRay,
87
87
  b2World_CastRayClosest,
88
+ b2World_CastShape,
88
89
  b2World_SetCustomFilterCallback,
89
90
  b2World_SetPreSolveCallback,
90
91
  b2World_GetGravity,
@@ -395,7 +396,9 @@ async function q5playPreSetup(q) {
395
396
  const scaleFrom = (x, y) => ({ x: x * meterSize, y: y * meterSize });
396
397
 
397
398
  const linearSlop = 0.005,
398
- angularSlop = 0.000582;
399
+ angularSlop = 0.0333;
400
+
401
+ let visualSlop = 0.5;
399
402
 
400
403
  const isSlop = (val) => Math.abs(val) <= linearSlop;
401
404
  const fixRound = (val) => {
@@ -538,6 +541,12 @@ async function q5playPreSetup(q) {
538
541
  this._density = val;
539
542
  b2Shape_SetDensity(this.id, val);
540
543
  }
544
+
545
+ applyWind(strength, angle, drag = 0, lift = 0) {
546
+ const wind = new b2Vec2(strength * $.cos(angle), strength * $.sin(angle));
547
+ b2Shape_ApplyWind(this.id, wind, drag, lift, true);
548
+ wind.delete();
549
+ }
541
550
  };
542
551
 
543
552
  const Sensor = class extends Shape {
@@ -1192,10 +1201,10 @@ async function q5playPreSetup(q) {
1192
1201
  else this.addCollider(...args);
1193
1202
  }
1194
1203
 
1195
- this.prevPos = { x, y };
1204
+ this.prevX = x;
1205
+ this.prevY = y;
1196
1206
  this.prevRotation = 0;
1197
- this._dest = { x, y };
1198
- this._destIdx = 0;
1207
+
1199
1208
  this._debug = false;
1200
1209
 
1201
1210
  if (!group._isAllSpritesGroup) $.allSprites.push(this);
@@ -1884,7 +1893,7 @@ async function q5playPreSetup(q) {
1884
1893
  }
1885
1894
  const speed = this._vel.mag();
1886
1895
  if (speed) {
1887
- this._setVel($.cos(val) * speed, $.sin(val) * speed);
1896
+ this.__setVel($.cos(val) * speed, $.sin(val) * speed);
1888
1897
  this._vel._magCached = false;
1889
1898
  }
1890
1899
  this._vel._direction = val;
@@ -1992,19 +2001,17 @@ async function q5playPreSetup(q) {
1992
2001
  this._opacity = val;
1993
2002
  }
1994
2003
 
1995
- get previousPosition() {
1996
- return this.prevPos;
2004
+ get previousX() {
2005
+ return this.prevX;
1997
2006
  }
1998
- set previousPosition(val) {
1999
- this.prevPos = val;
2007
+
2008
+ get previousY() {
2009
+ return this.prevY;
2000
2010
  }
2001
2011
 
2002
2012
  get previousRotation() {
2003
2013
  return this.prevRotation;
2004
2014
  }
2005
- set previousRotation(val) {
2006
- this.prevRotation = val;
2007
- }
2008
2015
 
2009
2016
  get pixelPerfect() {
2010
2017
  return this._pixelPerfect;
@@ -2028,8 +2035,9 @@ async function q5playPreSetup(q) {
2028
2035
  get rotation() {
2029
2036
  if (!this._physicsEnabled || !usePhysics) return this._rotation || 0;
2030
2037
  let val = b2Body_GetRotation(this.bdID).GetAngle();
2038
+ if ($._angleMode == DEGREES) val = $.degrees(val);
2031
2039
  if (friendlyRounding) val = fixRoundAngular(val);
2032
- return (this._rotation = $._angleMode == DEGREES ? $.degrees(val) : val);
2040
+ return (this._rotation = val);
2033
2041
  }
2034
2042
  set rotation(val) {
2035
2043
  this._rotation = val;
@@ -2053,17 +2061,15 @@ async function q5playPreSetup(q) {
2053
2061
  }
2054
2062
  set rotationLock(val) {
2055
2063
  if (this.watch) this.mod[25] = true;
2064
+ this._rotationLock = val;
2065
+ if (!this._physicsEnabled) return;
2056
2066
 
2057
- // let mass = this.mass;
2058
-
2059
- // TODO: not working, shape is ignored by physics sim after this
2060
- let locks = new b2MotionLocks();
2067
+ const locks = new b2MotionLocks();
2061
2068
  locks.linearX = false;
2062
2069
  locks.linearY = false;
2063
2070
  locks.angularZ = val;
2064
2071
  b2Body_SetMotionLocks(this.bdID, locks);
2065
-
2066
- // this.mass = mass;
2072
+ locks.delete();
2067
2073
  }
2068
2074
 
2069
2075
  get rotationSpeed() {
@@ -2148,14 +2154,14 @@ async function q5playPreSetup(q) {
2148
2154
  return this._vel.mag();
2149
2155
  }
2150
2156
  set speed(val) {
2151
- if (!val) this._setVel(0, 0);
2157
+ if (!val) this.__setVel(0, 0);
2152
2158
  else {
2153
2159
  const mag = this._vel.mag();
2154
2160
  if (mag > 0) {
2155
- this._setVel((this._vel.x / mag) * val, (this._vel.y / mag) * val);
2161
+ this.__setVel((this._vel.x / mag) * val, (this._vel.y / mag) * val);
2156
2162
  } else {
2157
2163
  const dir = this._vel.direction();
2158
- this._setVel($.cos(dir) * val, $.sin(dir) * val);
2164
+ this.__setVel($.cos(dir) * val, $.sin(dir) * val);
2159
2165
  }
2160
2166
  }
2161
2167
  this._vel._mag = val;
@@ -2163,7 +2169,7 @@ async function q5playPreSetup(q) {
2163
2169
  }
2164
2170
 
2165
2171
  setSpeedAndDirection(speed, direction) {
2166
- this._setVel($.cos(direction) * speed, $.sin(direction) * speed);
2172
+ this.__setVel($.cos(direction) * speed, $.sin(direction) * speed);
2167
2173
  this._vel._mag = speed;
2168
2174
  this._vel._direction = direction;
2169
2175
  this._vel._magCached = this._vel._directionCached = true;
@@ -2225,7 +2231,7 @@ async function q5playPreSetup(q) {
2225
2231
  }
2226
2232
 
2227
2233
  get pos() {
2228
- return this._pos;
2234
+ return { x: this._posX, y: this._posY };
2229
2235
  }
2230
2236
  set pos(val) {
2231
2237
  if (val == $.mouse && !$.mouse.isActive) return;
@@ -2404,7 +2410,6 @@ async function q5playPreSetup(q) {
2404
2410
  }
2405
2411
  set vel(val) {
2406
2412
  this._setVel(val[0] ?? val.x, val[1] ?? val.y);
2407
- this._vel._magCached = this._vel._directionCached = false;
2408
2413
  }
2409
2414
 
2410
2415
  _setVel(x, y) {
@@ -2414,6 +2419,16 @@ async function q5playPreSetup(q) {
2414
2419
  this._velX = x;
2415
2420
  this._velY = y;
2416
2421
  this._velSynced = true;
2422
+ this._vel._magCached = this._vel._directionCached = false;
2423
+ }
2424
+
2425
+ __setVel(x, y) {
2426
+ if (this._physicsEnabled) {
2427
+ b2Body_SetLinearVelocity(this.bdID, new b2Vec2(x, y));
2428
+ }
2429
+ this._velX = x;
2430
+ this._velY = y;
2431
+ this._velSynced = true;
2417
2432
  }
2418
2433
 
2419
2434
  get velocity() {
@@ -2442,6 +2457,9 @@ async function q5playPreSetup(q) {
2442
2457
 
2443
2458
  _update() {
2444
2459
  if (this._customUpdate) this._customUpdate();
2460
+ this.prevX = this._posX;
2461
+ this.prevY = this._posY;
2462
+ this.prevRotation = this.rotation;
2445
2463
  if (this.autoUpdate) this.autoUpdate = null;
2446
2464
  }
2447
2465
 
@@ -2463,6 +2481,70 @@ async function q5playPreSetup(q) {
2463
2481
  }
2464
2482
  }
2465
2483
 
2484
+ if (this._destArrivalTime !== undefined && $.world.physicsTime >= this._destArrivalTime) {
2485
+ const x = this._posX,
2486
+ y = this._posY,
2487
+ prevX = this.prevX,
2488
+ prevY = this.prevY,
2489
+ destX = this._destX,
2490
+ destY = this._destY;
2491
+
2492
+ const destReachedX =
2493
+ destX === undefined || (destX >= Math.min(prevX, x) - visualSlop && destX <= Math.max(prevX, x) + visualSlop);
2494
+ const destReachedY =
2495
+ destY === undefined || (destY >= Math.min(prevY, y) - visualSlop && destY <= Math.max(prevY, y) + visualSlop);
2496
+ const destReached = destReachedX && destReachedY;
2497
+
2498
+ if (destReached) {
2499
+ this._setVel(0, 0);
2500
+ this.rotationSpeed = 0;
2501
+ this.pos = [this._destX ?? this._posX, this._destY ?? this._posY];
2502
+ if (this._destRot !== undefined) {
2503
+ this.rotation = this._destRot;
2504
+ }
2505
+ }
2506
+
2507
+ if (this._destResolve) {
2508
+ this._destResolve(destReached);
2509
+ this._destResolve = undefined;
2510
+ }
2511
+
2512
+ this._destX = this._destY = this._destRot = this._destArrivalTime = undefined;
2513
+ }
2514
+
2515
+ if (this._destRotArrivalTime !== undefined && $.world.physicsTime >= this._destRotArrivalTime) {
2516
+ const isDeg = $._angleMode == DEGREES,
2517
+ full = isDeg ? 360 : $.TWO_PI,
2518
+ half = isDeg ? 180 : Math.PI,
2519
+ prevRot = this.prevRotation,
2520
+ currRot = this.rotation,
2521
+ destRot = this._destRot;
2522
+
2523
+ // normalize destRot to [-half, half) to match this.rotation's range
2524
+ let nd = ((destRot % full) + full) % full;
2525
+ if (nd >= half) nd -= full;
2526
+
2527
+ // when the sprite crosses the ±half boundary, prevRot and currRot jump
2528
+ // by ~full°; invert the range check so nd is tested against the arc that
2529
+ // was actually traversed rather than the gap between them
2530
+ const crossed180 = Math.abs(prevRot - currRot) > half;
2531
+ const rotReached = crossed180
2532
+ ? nd >= Math.max(prevRot, currRot) - visualSlop || nd <= Math.min(prevRot, currRot) + visualSlop
2533
+ : nd >= Math.min(prevRot, currRot) - visualSlop && nd <= Math.max(prevRot, currRot) + visualSlop;
2534
+
2535
+ if (rotReached) {
2536
+ this.rotationSpeed = 0;
2537
+ this.rotation = nd;
2538
+ }
2539
+
2540
+ if (this._destRotationResolve) {
2541
+ this._destRotationResolve(rotReached);
2542
+ this._destRotationResolve = undefined;
2543
+ }
2544
+
2545
+ this._destRot = this._destRotArrivalTime = undefined;
2546
+ }
2547
+
2466
2548
  if (!this._physicsEnabled && !this._deleted) return;
2467
2549
 
2468
2550
  this.__step();
@@ -2763,30 +2845,12 @@ async function q5playPreSetup(q) {
2763
2845
  b2Body_ApplyTorque(this.bdID, val, true);
2764
2846
  }
2765
2847
 
2766
- angleTo(x, y) {
2767
- if (typeof x == 'object') {
2768
- y = x.y;
2769
- x = x.x;
2770
- }
2771
- return $.atan2(y - this.y, x - this.x);
2772
- }
2773
-
2774
- rotationToFace(x, y, facing) {
2775
- if (typeof x == 'object') {
2776
- facing = y;
2777
- y = x.y;
2778
- x = x.x;
2779
- }
2780
- // if the sprite is too close to the position, don't rotate
2781
- if (Math.abs(x - this.x) < 0.01 && Math.abs(y - this.y) < 0.01) {
2782
- return 0;
2848
+ applyWind(strength, angle, drag = 0, lift = 0) {
2849
+ const wind = new b2Vec2(strength * $.cos(angle), strength * $.sin(angle));
2850
+ for (let shape of this._shapes) {
2851
+ b2Shape_ApplyWind(shape.id, wind, drag, lift, true);
2783
2852
  }
2784
- return this.angleTo(x, y) + (facing || 0);
2785
- }
2786
-
2787
- angleToFace(x, y, facing) {
2788
- let ang = this.rotationToFace(x, y, facing);
2789
- return minAngleDist(ang, this.rotation);
2853
+ wind.delete();
2790
2854
  }
2791
2855
 
2792
2856
  moveTowards(x, y, tracking) {
@@ -2803,13 +2867,13 @@ async function q5playPreSetup(q) {
2803
2867
 
2804
2868
  let velX, velY;
2805
2869
 
2806
- if (x !== null) {
2870
+ if (x !== null && x !== undefined) {
2807
2871
  let diffX = x - this.x;
2808
2872
  if (!isSlop(diffX)) {
2809
2873
  velX = diffX * tracking;
2810
2874
  } else velX = 0;
2811
2875
  } else velX = this._velX;
2812
- if (y !== null) {
2876
+ if (y !== null && y !== undefined) {
2813
2877
  let diffY = y - this.y;
2814
2878
  if (!isSlop(diffY)) {
2815
2879
  velY = diffY * tracking;
@@ -2817,7 +2881,155 @@ async function q5playPreSetup(q) {
2817
2881
  } else velY = this._velY;
2818
2882
 
2819
2883
  this._setVel(velX, velY);
2820
- this._vel._magCached = this._vel._directionCached = false;
2884
+ }
2885
+
2886
+ moveTo(x, y, speed) {
2887
+ if (x === undefined && (y === undefined || y === null)) return;
2888
+
2889
+ if (x !== null && x !== undefined && typeof x != 'number') {
2890
+ let pos = x;
2891
+ if (pos == $.mouse && !$.mouse.isActive) return;
2892
+ speed = y;
2893
+ y = pos.y;
2894
+ x = pos.x;
2895
+ }
2896
+
2897
+ const moveX = x !== null && x !== undefined;
2898
+ const moveY = y !== null && y !== undefined;
2899
+
2900
+ speed ||= this.speed || 1;
2901
+
2902
+ const dist =
2903
+ moveX && moveY
2904
+ ? Math.hypot(x - this._posX, y - this._posY)
2905
+ : moveX
2906
+ ? Math.abs(x - this._posX)
2907
+ : Math.abs(y - this._posY);
2908
+
2909
+ if (this._destResolve) {
2910
+ this._destResolve(false);
2911
+ this._destResolve = undefined;
2912
+ }
2913
+
2914
+ this._destX = moveX ? x : undefined;
2915
+ this._destY = moveY ? y : undefined;
2916
+ this._destArrivalTime = $.world.physicsTime + dist / (speed * $.world._updateRate);
2917
+
2918
+ if (dist > 0) {
2919
+ if (moveX && moveY) {
2920
+ this.setSpeedAndDirection(speed, $.atan2(y - this.y, x - this.x));
2921
+ } else if (moveX) {
2922
+ this._setVel(x > this._posX ? speed : -speed, this._velY);
2923
+ } else {
2924
+ this._setVel(this._velX, y > this._posY ? speed : -speed);
2925
+ }
2926
+ }
2927
+
2928
+ return {
2929
+ then: (onFulfilled) => {
2930
+ this._destResolve = onFulfilled;
2931
+ }
2932
+ };
2933
+ }
2934
+
2935
+ angleTo(x, y, facing = 0) {
2936
+ if (typeof x == 'object') {
2937
+ facing = y || 0;
2938
+ y = x.y;
2939
+ x = x.x;
2940
+ }
2941
+ // if the sprite is too close to the position, don't rotate
2942
+ if (Math.abs(x - this.x) < 0.01 && Math.abs(y - this.y) < 0.01) {
2943
+ return this.rotation;
2944
+ }
2945
+ return $.atan2(y - this.y, x - this.x) + facing;
2946
+ }
2947
+
2948
+ angleDistTo(x, y, facing = 0) {
2949
+ if (typeof x == 'object') {
2950
+ facing = y || 0;
2951
+ y = x.y;
2952
+ x = x.x;
2953
+ }
2954
+ // if the sprite is too close to the position, don't rotate
2955
+ if (Math.abs(x - this.x) < 0.01 && Math.abs(y - this.y) < 0.01) {
2956
+ return 0;
2957
+ }
2958
+ return minAngleDist($.atan2(y - this.y, x - this.x) + facing, this.rotation);
2959
+ }
2960
+
2961
+ rotateTo(angle, speed) {
2962
+ let args = arguments;
2963
+ let x, y, facing;
2964
+ if (typeof args[0] != 'number') {
2965
+ x = args[0].x;
2966
+ y = args[0].y;
2967
+ speed = args[1];
2968
+ facing = args[2];
2969
+ } else if (arguments.length > 2) {
2970
+ x = args[0];
2971
+ y = args[1];
2972
+ speed = args[2];
2973
+ facing = args[3];
2974
+ }
2975
+
2976
+ if (x !== undefined) angle = this.angleTo(x, y, facing);
2977
+
2978
+ const full = $._angleMode == DEGREES ? 360 : $.TWO_PI;
2979
+ let angleDist = (angle - this.rotation) % full;
2980
+ if (angleDist < 0 && speed > 0) angleDist += full;
2981
+ if (angleDist > 0 && speed < 0) angleDist -= full;
2982
+
2983
+ return this.rotate(angleDist, speed);
2984
+ }
2985
+
2986
+ rotateMinTo(angle, speed, facing) {
2987
+ let args = arguments;
2988
+ let x, y;
2989
+ if (typeof args[0] != 'number') {
2990
+ x = args[0].x;
2991
+ y = args[0].y;
2992
+ speed = args[1];
2993
+ facing = args[2];
2994
+ } else if (args.length > 2) {
2995
+ x = args[0];
2996
+ y = args[1];
2997
+ speed = args[2];
2998
+ facing = args[3];
2999
+ }
3000
+
3001
+ if (x !== undefined) angle = this.angleTo(x, y, facing);
3002
+
3003
+ return this.rotate(minAngleDist(angle, this.rotation), speed);
3004
+ }
3005
+
3006
+ rotate(angleDist, speed) {
3007
+ if (Math.abs(angleDist) <= angularSlop) return;
3008
+
3009
+ speed ||= this.rotationSpeed || 1;
3010
+ speed = Math.abs(speed) * Math.sign(angleDist);
3011
+
3012
+ // cap speed so the sprite doesn't overshoot the destination in one physics step
3013
+ if (Math.abs(speed) > Math.abs(angleDist)) speed = angleDist;
3014
+
3015
+ this._destRotArrivalTime = $.world.physicsTime + Math.abs(angleDist) / (Math.abs(speed) * $.world._updateRate);
3016
+
3017
+ this._destRot = this.rotation + angleDist;
3018
+
3019
+ if (this._destRotationResolve) {
3020
+ this._destRotationResolve(false);
3021
+ this._destRotationResolve = undefined;
3022
+ }
3023
+
3024
+ this.rotationSpeed = speed;
3025
+
3026
+ log(this.rotation, this._destRot, angleDist, speed, this._destRotArrivalTime);
3027
+
3028
+ return {
3029
+ then: (onFulfilled) => {
3030
+ this._destRotationResolve = onFulfilled;
3031
+ }
3032
+ };
2821
3033
  }
2822
3034
 
2823
3035
  rotateTowards(angle, tracking) {
@@ -2835,18 +3047,31 @@ async function q5playPreSetup(q) {
2835
3047
  facing = args[3];
2836
3048
  }
2837
3049
 
2838
- if (x !== undefined) angle = this.angleToFace(x, y, facing);
3050
+ if (x !== undefined) angle = this.angleDistTo(x, y, facing);
2839
3051
  else angle -= this.rotation;
2840
3052
 
2841
3053
  tracking ??= 0.1;
2842
3054
  this.rotationSpeed = angle * tracking;
2843
3055
  }
2844
3056
 
2845
- _setTargetTransform(x, y, rotation) {
2846
- let t = new b2Transform();
2847
- t.p = scaleTo(x, y);
3057
+ transformTowards(x, y, rotation, tracking = 0.1) {
3058
+ if (x === undefined) return;
3059
+
3060
+ if (typeof x != 'number') {
3061
+ let pos = x;
3062
+ if (pos == $.mouse && !$.mouse.isActive) return;
3063
+ tracking = rotation ?? tracking;
3064
+ rotation = y;
3065
+ y = pos.y;
3066
+ x = pos.x;
3067
+ }
3068
+
3069
+ const t = new b2Transform();
3070
+ t.p = scaleTo(x ?? this._posX, y ?? this._posY);
3071
+ rotation ??= this._rotation;
3072
+ if ($._angleMode == DEGREES) rotation = (rotation % 360) * DEGTORAD;
2848
3073
  t.q = b2MakeRot(rotation);
2849
- b2Body_SetTargetTransform(this.bdID, t, $.world._timeStep);
3074
+ b2Body_SetTargetTransform(this.bdID, t, $.world._timeStep / tracking);
2850
3075
  }
2851
3076
 
2852
3077
  delete() {
@@ -4467,6 +4692,16 @@ async function q5playPreSetup(q) {
4467
4692
  }
4468
4693
  }
4469
4694
 
4695
+ applyWind(strength, angle, drag = 0, lift = 0) {
4696
+ const wind = new b2Vec2(strength * $.cos(angle), strength * $.sin(angle));
4697
+ for (let s of this) {
4698
+ for (let shape of s._shapes) {
4699
+ b2Shape_ApplyWind(shape.id, wind, drag, lift, true);
4700
+ }
4701
+ }
4702
+ wind.delete();
4703
+ }
4704
+
4470
4705
  _resetCentroid() {
4471
4706
  let x = 0;
4472
4707
  let y = 0;
@@ -4509,6 +4744,134 @@ async function q5playPreSetup(q) {
4509
4744
  }
4510
4745
  }
4511
4746
 
4747
+ rotateTowards() {
4748
+ for (let s of this) {
4749
+ s.rotateTowards(...arguments);
4750
+ }
4751
+ }
4752
+
4753
+ rotateTo(angle, speed) {
4754
+ const thenables = [];
4755
+ for (let s of this) {
4756
+ thenables.push(s.rotateTo(...arguments));
4757
+ }
4758
+ return {
4759
+ then: (onFulfilled) => {
4760
+ let pending = thenables.length;
4761
+ if (!pending) return onFulfilled(true);
4762
+ let allReached = true;
4763
+ for (let t of thenables) {
4764
+ t.then((reached) => {
4765
+ if (!reached) allReached = false;
4766
+ if (--pending === 0) onFulfilled(allReached);
4767
+ });
4768
+ }
4769
+ }
4770
+ };
4771
+ }
4772
+
4773
+ rotate(angle, speed) {
4774
+ const thenables = [];
4775
+ for (let s of this) {
4776
+ thenables.push(s.rotate(...arguments));
4777
+ }
4778
+ return {
4779
+ then: (onFulfilled) => {
4780
+ let pending = thenables.length;
4781
+ if (!pending) return onFulfilled(true);
4782
+ let allReached = true;
4783
+ for (let t of thenables) {
4784
+ t.then((reached) => {
4785
+ if (!reached) allReached = false;
4786
+ if (--pending === 0) onFulfilled(allReached);
4787
+ });
4788
+ }
4789
+ }
4790
+ };
4791
+ }
4792
+
4793
+ rotateMinTo(angle, speed) {
4794
+ const thenables = [];
4795
+ for (let s of this) {
4796
+ thenables.push(s.rotateMinTo(...arguments));
4797
+ }
4798
+ return {
4799
+ then: (onFulfilled) => {
4800
+ let pending = thenables.length;
4801
+ if (!pending) return onFulfilled(true);
4802
+ let allReached = true;
4803
+ for (let t of thenables) {
4804
+ t.then((reached) => {
4805
+ if (!reached) allReached = false;
4806
+ if (--pending === 0) onFulfilled(allReached);
4807
+ });
4808
+ }
4809
+ }
4810
+ };
4811
+ }
4812
+
4813
+ transformTowards(x, y, rotation, tracking = 0.1) {
4814
+ if (x === undefined) return;
4815
+
4816
+ if (typeof x != 'number') {
4817
+ let pos = x;
4818
+ if (pos == $.mouse && !$.mouse.isActive) return;
4819
+ tracking = rotation ?? tracking;
4820
+ rotation = y;
4821
+ y = pos.y;
4822
+ x = pos.x;
4823
+ }
4824
+
4825
+ this._resetCentroid();
4826
+
4827
+ for (let s of this) {
4828
+ if (s.distCentroid === undefined) {
4829
+ this._resetDistancesFromCentroid();
4830
+ }
4831
+ s.transformTowards(s.distCentroid.x + x, s.distCentroid.y + y, rotation, tracking);
4832
+ }
4833
+ }
4834
+
4835
+ moveTo(x, y, speed) {
4836
+ if (x === undefined && (y === undefined || y === null)) return;
4837
+
4838
+ let nullX = x === null || x === undefined;
4839
+ let nullY = y === null || y === undefined;
4840
+
4841
+ if (!nullX && typeof x != 'number') {
4842
+ let pos = x;
4843
+ if (pos == $.mouse && !$.mouse.isActive) return;
4844
+ speed = y;
4845
+ y = pos.y;
4846
+ x = pos.x;
4847
+ nullX = nullY = false;
4848
+ }
4849
+
4850
+ this._resetCentroid();
4851
+ this._resetDistancesFromCentroid();
4852
+
4853
+ const thenables = [];
4854
+ for (let s of this) {
4855
+ const tx = nullX ? null : s.distCentroid.x + x;
4856
+ const ty = nullY ? null : s.distCentroid.y + y;
4857
+ thenables.push(s.moveTo(tx, ty, speed));
4858
+ }
4859
+
4860
+ return {
4861
+ then: (onFulfilled) => {
4862
+ let pending = thenables.length;
4863
+ if (!pending) return onFulfilled(true);
4864
+ let allReached = true;
4865
+ for (let t of thenables) {
4866
+ t.then((reached) => {
4867
+ if (!reached) allReached = false;
4868
+ if (--pending === 0) onFulfilled(allReached);
4869
+ });
4870
+ }
4871
+ }
4872
+ };
4873
+ }
4874
+
4512
4875
  toString() {
4513
4876
  return 'g' + this.idNum;
4514
4877
  }
@@ -4787,7 +5150,7 @@ async function q5playPreSetup(q) {
4787
5150
  $.Visuals.prototype.addAni = $.Group.prototype.addAni = $.Sprite.prototype.addAni;
4788
5151
  $.Visuals.prototype.addAnis = $.Group.prototype.addAnis = $.Sprite.prototype.addAnis;
4789
5152
 
4790
- class RayInfo {
5153
+ class CastInfo {
4791
5154
  constructor(sprite, px, py, nx, ny, fraction, maxDistance) {
4792
5155
  this.sprite = sprite;
4793
5156
  this._px = px;
@@ -5017,6 +5380,7 @@ async function q5playPreSetup(q) {
5017
5380
  }
5018
5381
  set meterSize(val) {
5019
5382
  meterSize = val;
5383
+ visualSlop = meterSize / 120;
5020
5384
  }
5021
5385
 
5022
5386
  get profile() {
@@ -5132,6 +5496,8 @@ async function q5playPreSetup(q) {
5132
5496
 
5133
5497
  this.physicsTime += timeStep;
5134
5498
 
5499
+ this._sync();
5500
+
5135
5501
  let sprites = Object.values($.q5play.sprites);
5136
5502
  let groups = Object.values($.q5play.groups);
5137
5503
 
@@ -5148,6 +5514,71 @@ async function q5playPreSetup(q) {
5148
5514
  if (this.autoStep) this.autoStep = null;
5149
5515
  }
5150
5516
 
5517
+ _sync() {
5518
+ jointStack = [];
5519
+ shapeStack = [];
5520
+
5521
+ b2World_Draw(wID, drawCmds.GetDebugDraw());
5522
+
5523
+ let cmdPtr = drawCmds.GetCommandsData(),
5524
+ cmdSize = drawCmds.GetCommandsSize(),
5525
+ cmdStride = drawCmds.GetCommandStride(),
5526
+ offset = cmdPtr,
5527
+ renderJointForces = $.q5play.renderJointForces,
5528
+ s;
5529
+
5530
+ for (let i = 0; i < cmdSize; i++, offset += cmdStride) {
5531
+ // workaround that unpacks data from
5532
+ // the shape material's customColor
5533
+ const customColor = Box2D.HEAPU32[(offset + 4) >> 2],
5534
+ uid = customColor & 0xffffff,
5535
+ isSensor = (customColor >>> 25) & 0x1,
5536
+ isFirstShape = (customColor >>> 26) & 0x1;
5537
+
5538
+ s = $.q5play.sprites[uid];
5539
+
5540
+ let type = Box2D.HEAPU8[offset];
5541
+
5542
+ if (type == 7) {
5543
+ continue;
5544
+ }
5545
+
5546
+ let vertexCount = Box2D.HEAPU16[(offset + 8) >> 1];
5547
+
5548
+ let dataLen = 4;
5549
+ if (type == 1) dataLen = 5 + vertexCount * 2;
5550
+ else if (type == 3 || type == 4) dataLen = 5;
5551
+ let data = new Float32Array(Box2D.HEAPU8.buffer, offset + 12, dataLen);
5552
+
5553
+ if (!s) {
5554
+ if (type == 0 && renderJointForces) jointStack.push(data);
5555
+ continue;
5556
+ }
5557
+
5558
+ // always keep position in sync since it has a low performance cost
5559
+ // unless the shape is a chain
5560
+ if (type < 4 || (type == 4 && !s._hasCapsuleChain)) {
5561
+ s._posX = data[0] * meterSize;
5562
+ s._posY = data[1] * meterSize;
5563
+ }
5564
+
5565
+ s._velSynced = false;
5566
+ s._vel._magCached = false;
5567
+
5568
+ if (!s.visible) {
5569
+ continue;
5570
+ }
5571
+
5572
+ if (s._hasImagery || s._userDefinedDraw) {
5573
+ s._rotation = Math.atan2(data[2], data[3]) * RADTODEG;
5574
+ }
5575
+
5576
+ if (s.debug || (!s._hasImagery && !s._userDefinedDraw)) {
5577
+ shapeStack.push({ type, sprite: s, isSensor, isFirstShape, data, vertexCount });
5578
+ }
5579
+ }
5580
+ }
5581
+
5151
5582
  get realTime() {
5152
5583
  return $.millis() / 1000;
5153
5584
  }
@@ -5183,7 +5614,7 @@ async function q5playPreSetup(q) {
5183
5614
  if (shape?.sprite) {
5184
5615
  const s = shape.sprite;
5185
5616
 
5186
- s.ray = new RayInfo(
5617
+ s.cast = new CastInfo(
5187
5618
  s,
5188
5619
  castResult.point.x,
5189
5620
  castResult.point.y,
@@ -5200,7 +5631,54 @@ async function q5playPreSetup(q) {
5200
5631
  });
5201
5632
 
5202
5633
  // sort results by distance from start
5203
- results.sort((a, b) => a.ray.distance - b.ray.distance);
5634
+ results.sort((a, b) => a.cast.distance - b.cast.distance);
5635
+ return results;
5636
+ }
5637
+
5638
+ circleCast(startPos, endPos, radius) {
5639
+ let sprites = this.circleCastAll(startPos, endPos, radius, () => true);
5640
+ return sprites[0];
5641
+ }
5642
+
5643
+ circleCastAll(startPos, endPos, radius, limiter) {
5644
+ const startX = startPos.x ?? startPos[0];
5645
+ const startY = startPos.y ?? startPos[1];
5646
+ const endX = endPos.x ?? endPos[0];
5647
+ const endY = endPos.y ?? endPos[1];
5648
+
5649
+ const maxDistance = Math.sqrt((endX - startX) ** 2 + (endY - startY) ** 2);
5650
+
5651
+ const center = scaleTo(startX, startY);
5652
+ const proxy = b2MakeProxy(center, 1, radius / meterSize);
5653
+ const translation = scaleTo(endX - startX, endY - startY);
5654
+
5655
+ const results = [];
5656
+
5657
+ b2World_CastShape(wID, proxy, translation, NULL_FILTER, (castResult) => {
5658
+ const shape = shapeDict[castResult.shapeId.index1];
5659
+ if (shape?.sprite) {
5660
+ const s = shape.sprite;
5661
+
5662
+ s.cast = new CastInfo(
5663
+ s,
5664
+ castResult.point.x,
5665
+ castResult.point.y,
5666
+ castResult.normal.x,
5667
+ castResult.normal.y,
5668
+ castResult.fraction,
5669
+ maxDistance
5670
+ );
5671
+ results.push(s);
5672
+
5673
+ if (limiter && limiter(s)) return 0; // stop cast
5674
+ }
5675
+ return 1; // continue to collect all hits
5676
+ });
5677
+
5678
+ proxy.delete();
5679
+
5680
+ // sort results by distance from start
5681
+ results.sort((a, b) => a.cast.distance - b.cast.distance);
5204
5682
  return results;
5205
5683
  }
5206
5684
  };
@@ -5228,7 +5706,7 @@ async function q5playPreSetup(q) {
5228
5706
  }
5229
5707
 
5230
5708
  get pos() {
5231
- return this._pos;
5709
+ return { x: this._pos.x, y: this._pos.y };
5232
5710
  }
5233
5711
  set pos(val) {
5234
5712
  this.x = val[0] ?? val.x;
@@ -5497,7 +5975,7 @@ async function q5playPreSetup(q) {
5497
5975
  }
5498
5976
 
5499
5977
  _draw(xA, yA, xB, yB) {
5500
- if (xB) $.line(xA, yA, xB, yB);
5978
+ if (xB || yB) $.line(xA, yA, xB, yB);
5501
5979
  else $.point(xA, yA);
5502
5980
  }
5503
5981
 
@@ -6045,20 +6523,6 @@ async function q5playPreSetup(q) {
6045
6523
  Box2D.b2PrismaticJoint_EnableLimit(this.jID, true);
6046
6524
  }
6047
6525
 
6048
- set limits(val) {
6049
- let min, max;
6050
- if (typeof val == 'number') {
6051
- val /= 2;
6052
- min = -val;
6053
- max = val;
6054
- } else {
6055
- min = val[0];
6056
- max = val[1];
6057
- }
6058
- Box2D.b2PrismaticJoint_SetLimits(this.jID, min / meterSize, max / meterSize);
6059
- Box2D.b2PrismaticJoint_EnableLimit(this.jID, true);
6060
- }
6061
-
6062
6526
  get springEnabled() {
6063
6527
  return Box2D.b2PrismaticJoint_IsSpringEnabled(this.jID);
6064
6528
  }
@@ -6910,7 +7374,7 @@ async function q5playPreSetup(q) {
6910
7374
  }
6911
7375
 
6912
7376
  get pos() {
6913
- return this._pos;
7377
+ return { x: this.x, y: this.y };
6914
7378
  }
6915
7379
  get position() {
6916
7380
  return this._pos;
@@ -7823,71 +8287,6 @@ async function q5playPreSetup(q) {
7823
8287
  jointStack = [],
7824
8288
  shapeStack = [];
7825
8289
 
7826
- $._syncWorld = () => {
7827
- jointStack = [];
7828
- shapeStack = [];
7829
-
7830
- b2World_Draw(wID, drawCmds.GetDebugDraw());
7831
-
7832
- let cmdPtr = drawCmds.GetCommandsData(),
7833
- cmdSize = drawCmds.GetCommandsSize(),
7834
- cmdStride = drawCmds.GetCommandStride(),
7835
- offset = cmdPtr,
7836
- renderJointForces = $.q5play.renderJointForces,
7837
- s;
7838
-
7839
- for (let i = 0; i < cmdSize; i++, offset += cmdStride) {
7840
- // workaround that unpacks data from
7841
- // the shape material's customColor
7842
- const customColor = Box2D.HEAPU32[(offset + 4) >> 2],
7843
- uid = customColor & 0xffffff,
7844
- isSensor = (customColor >>> 25) & 0x1,
7845
- isFirstShape = (customColor >>> 26) & 0x1;
7846
-
7847
- s = $.q5play.sprites[uid];
7848
-
7849
- let type = Box2D.HEAPU8[offset];
7850
-
7851
- if (type == 7) {
7852
- continue;
7853
- }
7854
-
7855
- let vertexCount = Box2D.HEAPU16[(offset + 8) >> 1];
7856
-
7857
- let dataLen = 4;
7858
- if (type == 1) dataLen = 5 + vertexCount * 2;
7859
- else if (type == 3 || type == 4) dataLen = 5;
7860
- let data = new Float32Array(Box2D.HEAPU8.buffer, offset + 12, dataLen);
7861
-
7862
- if (!s) {
7863
- if (type == 0 && renderJointForces) jointStack.push(data);
7864
- continue;
7865
- }
7866
-
7867
- // always keep position in sync since it has a low performance cost
7868
- // unless the shape is a chain
7869
- if (type < 4 || (type == 4 && !s._hasCapsuleChain)) {
7870
- s._posX = data[0] * meterSize;
7871
- s._posY = data[1] * meterSize;
7872
- }
7873
-
7874
- s._velSynced = false;
7875
- s._vel._magCached = false;
7876
-
7877
- if (!s.visible) {
7878
- continue;
7879
- }
7880
-
7881
- if (s._hasImagery || s._userDefinedDraw) {
7882
- s._rotation = Math.atan2(data[2], data[3]) * RADTODEG;
7883
- }
7884
-
7885
- if (s.debug || (!s._hasImagery && !s._userDefinedDraw)) {
7886
- shapeStack.push({ type, sprite: s, isSensor, isFirstShape, data, vertexCount });
7887
- }
7888
- }
7889
- };
7890
-
7891
8290
  const colorMax = $._colorFormat,
7892
8291
  debugGreen = $.color(0, colorMax, 0, colorMax * 0.9),
7893
8292
  debugGreenFill = $.color(0, colorMax, 0, colorMax * 0.1),
@@ -8186,7 +8585,6 @@ function q5playUpdate() {
8186
8585
 
8187
8586
  if ($.world.autoStep && $.world.timeScale > 0) {
8188
8587
  $.world.physicsUpdate();
8189
- $._syncWorld();
8190
8588
  }
8191
8589
  $.world.autoStep ??= true;
8192
8590
 
@@ -8357,8 +8755,7 @@ attractTo -> es:atraerA
8357
8755
  repelFrom -> es:repelerDe
8358
8756
  applyTorque -> es:aplicarTorque
8359
8757
  angleTo -> es:ánguloHacia
8360
- rotationToFace -> es:rotaciónParaMirar
8361
- angleToFace -> es:ánguloParaMirar
8758
+ angleDistTo -> es:distÁnguloHacia
8362
8759
  setSpeedAndDirection -> es:establecerVelocidadYDirección
8363
8760
  scaleBy -> es:escalarPor
8364
8761
  resetMass -> es:reiniciarMasa