incanto 0.74.0 → 0.76.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 (92) hide show
  1. package/bin/incanto-multiplay.mjs +52 -4
  2. package/dist/2d.d.ts +83 -7
  3. package/dist/2d.js +4 -3
  4. package/dist/3d.d.ts +861 -167
  5. package/dist/3d.js +6 -6
  6. package/dist/{agent8-CHTT4unP.js → agent8-B-bh3n0X.js} +1 -1
  7. package/dist/{audio-player-BOrk4eQx.d.ts → audio-player-BRo2uvG6.d.ts} +23 -2
  8. package/dist/{behavior-J1cmVah0.d.ts → behavior-B_245qRy.d.ts} +15 -1
  9. package/dist/{create-game-ssvGvP7W.js → create-game-BMIPKrQW.js} +84 -8
  10. package/dist/{create-game-BAiA-FjP.js → create-game-Dai5vfVu.js} +80 -11
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-Bzt0GzVp.d.ts} +13 -3
  13. package/dist/editor.js +1600 -1529
  14. package/dist/env.d.ts +1 -1
  15. package/dist/{environment-presets-BkqlWewf.js → environment-presets-e_9YCnGu.js} +2361 -104
  16. package/dist/{gameplay-Bxe1sMVT.js → gameplay-Bfff_beb.js} +10919 -6798
  17. package/dist/gameplay.d.ts +573 -31
  18. package/dist/gameplay.js +3 -2
  19. package/dist/index.d.ts +150 -6
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-BAeWMYfF.d.ts → loader-CbkVdXL8.d.ts} +2 -2
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +2 -2
  24. package/dist/{physics-2d-C-SfvjzH.js → physics-2d-Tuf5LAyq.js} +250 -31
  25. package/dist/{physics-3d-rOzeg890.js → physics-3d-C5MOujfQ.js} +673 -51
  26. package/dist/{picking-BMV34Pjl.js → picking-DIwE48sU.js} +10 -7
  27. package/dist/{save-slots-CEUGeLuB.js → pose-ByFB_J3O.js} +217 -11
  28. package/dist/react.d.ts +2 -2
  29. package/dist/react.js +1 -1
  30. package/dist/{register-DRQyZKGU.js → register-7E3P1gz6.js} +191 -430
  31. package/dist/{register-CjociOtt.js → register-B768Qedj.js} +596 -2
  32. package/dist/{replay-B6BbgGM_.js → replay-DrqQ6dZj.js} +133 -18
  33. package/dist/{replay-wZK5Ok5c.d.ts → replay-W5nCw_cU.d.ts} +21 -2
  34. package/dist/{rng-Bb-IutXB.d.ts → rng-BsXZg3D6.d.ts} +7 -0
  35. package/dist/{split-screen-BxZ5rBe7.js → split-screen-B-dIcQsQ.js} +10 -5
  36. package/dist/{split-screen-D6sx8z3Z.d.ts → split-screen-CiLJf2-C.d.ts} +3 -3
  37. package/dist/{sprite-animation-Bz3DHD_4.js → sprite-animation-SQa5gIu2.js} +8 -6
  38. package/dist/{src-CqtvYtSN.js → src-D823V07k.js} +1 -1
  39. package/dist/{test-CEl0y2Sw.js → test-CVbxnXlv.js} +487 -82
  40. package/dist/test.d.ts +90 -12
  41. package/dist/test.js +2 -2
  42. package/dist/vite.js +34 -5
  43. package/editor/assets/{agent8-BlzturGi.js → agent8-DA-I5qhv.js} +1 -1
  44. package/editor/assets/{debug-BMUi8usj.js → debug-99tEPoGm.js} +1 -1
  45. package/editor/assets/index-Db74LZY-.js +11579 -0
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +1639 -280
  49. package/skills/incanto-3d-character.md +383 -20
  50. package/skills/incanto-audio.md +24 -2
  51. package/skills/incanto-building-2d-games.md +21 -2
  52. package/skills/incanto-building-3d-games.md +38 -3
  53. package/skills/incanto-editor.md +29 -8
  54. package/skills/incanto-environment.md +257 -14
  55. package/skills/incanto-game-feel.md +60 -1
  56. package/skills/incanto-gameplay-behaviors.md +668 -20
  57. package/skills/incanto-hud.md +116 -0
  58. package/skills/incanto-multiplayer.md +141 -2
  59. package/skills/incanto-node-reference.md +398 -16
  60. package/skills/incanto-performance.md +32 -0
  61. package/skills/incanto-physics-and-input.md +241 -13
  62. package/skills/incanto-playtesting.md +2 -2
  63. package/skills/incanto-save-slots.md +61 -2
  64. package/skills/incanto-scene-json-authoring.md +13 -0
  65. package/skills/incanto-verifying-your-game.md +86 -3
  66. package/templates-app/beacon-isle-3d/PROJECT/Status.md +12 -0
  67. package/templates-app/beacon-isle-3d/package.json +1 -1
  68. package/templates-app/beacon-isle-3d/src/game.scene.json +157 -3
  69. package/templates-app/molehill-2d/PROJECT/Status.md +7 -0
  70. package/templates-app/molehill-2d/package.json +1 -1
  71. package/templates-app/molehill-2d/src/game.scene.json +129 -2
  72. package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
  73. package/templates-app/platformer-2d/docs/project-2d-rules.md +3 -2
  74. package/templates-app/platformer-2d/package.json +1 -1
  75. package/templates-app/platformer-2d/src/behaviors.ts +40 -26
  76. package/templates-app/platformer-2d/src/game.scene.json +144 -2
  77. package/templates-app/platformer-2d/verify.ts +18 -1
  78. package/templates-app/star-survivor/PROJECT/Status.md +7 -0
  79. package/templates-app/star-survivor/package.json +1 -1
  80. package/templates-app/star-survivor/src/behaviors.ts +9 -8
  81. package/templates-app/star-survivor/src/game.scene.json +142 -4
  82. package/templates-app/star-survivor/verify.ts +13 -0
  83. package/templates-app/tps-3d/PROJECT/Status.md +6 -0
  84. package/templates-app/tps-3d/package.json +1 -1
  85. package/templates-app/tps-3d/src/game.scene.json +71 -10
  86. package/templates-app/village-quest-3d/PROJECT/Status.md +7 -0
  87. package/templates-app/village-quest-3d/package.json +1 -1
  88. package/templates-app/village-quest-3d/src/behaviors.ts +8 -16
  89. package/templates-app/village-quest-3d/src/grove.scene.json +121 -5
  90. package/templates-app/village-quest-3d/src/village.scene.json +126 -2
  91. package/templates-app/village-quest-3d/verify.ts +52 -2
  92. package/editor/assets/index-CrUCQoaB.js +0 -11046
@@ -1,9 +1,10 @@
1
1
  import { o as __exportAll } from "./json-CfTjpvW8.js";
2
- import { Y as diagnose } from "./save-slots-CEUGeLuB.js";
2
+ import { et as diagnose } from "./pose-ByFB_J3O.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, v as StaticBody2D, y as Node2D } from "./register-DRQyZKGU.js";
4
+ import { _ as Node2D, f as Area2D, g as StaticBody2D, h as RigidBody2D, m as PhysicsBody2D, p as CharacterBody2D, v as validateCollider2D } from "./gameplay-Bfff_beb.js";
5
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
6
6
  import { n as jointContacts, t as withoutRapierInitNoise } from "./quiet-rapier-C6fW4zcW.js";
7
+ import { p as Joint2D } from "./register-7E3P1gz6.js";
7
8
  //#region src/2d/physics/physics-2d.ts
8
9
  var physics_2d_exports = /* @__PURE__ */ __exportAll({
9
10
  Physics2D: () => Physics2D,
@@ -98,13 +99,18 @@ var Physics2D = class {
98
99
  for (const [node, e] of this.entries) {
99
100
  const t = e.body.translation();
100
101
  if (!e.body.isFixed() && !(node instanceof Area2D)) {
101
- const [ox, oy] = parentWorldOffset2D(node);
102
- node.position = [t.x * SCALE - ox, t.y * SCALE - oy];
102
+ const [ox, oy, oa] = parentPose2D(node);
103
+ const dx = t.x * SCALE - ox;
104
+ const dy = t.y * SCALE - oy;
105
+ if (oa !== 0) {
106
+ const a = -oa * DEG;
107
+ node.position = [dx * Math.cos(a) - dy * Math.sin(a), dx * Math.sin(a) + dy * Math.cos(a)];
108
+ } else node.position = [dx, dy];
103
109
  }
104
110
  if (node instanceof RigidBody2D) {
105
111
  if (!node.fixedRotation) {
106
- node.rotation = e.body.rotation() / DEG;
107
- e.lastRot = node.rotation;
112
+ node.rotation = e.body.rotation() / DEG - parentPose2D(node)[2];
113
+ e.lastRot = e.body.rotation() / DEG;
108
114
  }
109
115
  const lv = e.body.linvel();
110
116
  node.linearVelocity = [lv.x * SCALE, lv.y * SCALE];
@@ -121,6 +127,7 @@ var Physics2D = class {
121
127
  const a = this.byColliderHandle.get(h1);
122
128
  const b = this.byColliderHandle.get(h2);
123
129
  if (!a || !b) return;
130
+ if (!a.enabled || !b.enabled) return;
124
131
  const sig = started ? "triggerEnter" : "triggerExit";
125
132
  if (started === (this.overlaps.get(a)?.has(b) ?? false)) return;
126
133
  this.trackOverlap(a, b, started);
@@ -149,6 +156,7 @@ var Physics2D = class {
149
156
  * you can carry you.
150
157
  */
151
158
  canCarry(rider, candidate) {
159
+ if (this.jointed.get(rider)?.has(candidate)) return false;
152
160
  let at = candidate;
153
161
  for (let depth = 0; depth < 8 && at; depth++) {
154
162
  if (at === rider) return false;
@@ -171,6 +179,14 @@ var Physics2D = class {
171
179
  const [gx, gy] = worldPosition2D(e.ground);
172
180
  carryX = gx - (e.groundX ?? gx);
173
181
  carryY = gy - (e.groundY ?? gy);
182
+ const gb = this.entries.get(e.ground);
183
+ if (gb && e.groundRot !== void 0 && e.riderX !== void 0 && e.riderY !== void 0) {
184
+ const d = gb.body.rotation() - e.groundRot;
185
+ const ox = e.riderX - (e.groundX ?? gx);
186
+ const oy = e.riderY - (e.groundY ?? gy);
187
+ carryX += ox * Math.cos(d) - oy * Math.sin(d) - ox;
188
+ carryY += ox * Math.sin(d) + oy * Math.cos(d) - oy;
189
+ }
174
190
  } else e.ground = null;
175
191
  const vyTerm = carryY < -.1 && node.isOnFloor?.() === true && vy > 0 ? 0 : vy * this.lastDt;
176
192
  const desired = {
@@ -186,7 +202,15 @@ var Physics2D = class {
186
202
  }
187
203
  return true;
188
204
  };
189
- const filter = node.collideWithCharacters ? predicate : (c) => !(this.byColliderHandle.get(c.handle) instanceof CharacterBody2D) && predicate(c);
205
+ const noContact = this.jointed.get(node);
206
+ const filter = (c) => {
207
+ const other = this.byColliderHandle.get(c.handle);
208
+ if (!node.collideWithCharacters && other instanceof CharacterBody2D) return false;
209
+ if (other && noContact?.get(other) === false) return false;
210
+ return predicate(c);
211
+ };
212
+ this.kcc.setApplyImpulsesToDynamicBodies(node.pushes > 0);
213
+ if (node.pushes > 0) this.kcc.setCharacterMass(node.pushes);
190
214
  this.kcc.computeColliderMovement(e.collider, desired, this.R.QueryFilterFlags.EXCLUDE_SENSORS, void 0, filter);
191
215
  const corrected = this.kcc.computedMovement();
192
216
  const t = e.body.translation();
@@ -214,6 +238,9 @@ var Physics2D = class {
214
238
  const [gx, gy] = worldPosition2D(hit.node);
215
239
  e.groundX = gx;
216
240
  e.groundY = gy;
241
+ e.groundRot = this.entries.get(hit.node)?.body.rotation();
242
+ e.riderX = footX;
243
+ e.riderY = (t.y + corrected.y) * SCALE + charHalfHeight(node);
217
244
  } else e.ground = null;
218
245
  } else e.ground = null;
219
246
  }
@@ -290,14 +317,28 @@ var Physics2D = class {
290
317
  for (const [joint, impulse] of this.joints) if (!this.jointSet.has(joint)) {
291
318
  this.world.removeImpulseJoint(impulse, true);
292
319
  this.joints.delete(joint);
320
+ const pair = this.jointBodies.get(joint);
321
+ if (pair) this.unlinkJointed(pair[0], pair[1]);
322
+ this.jointBodies.delete(joint);
323
+ this.hingeState.delete(joint);
324
+ this.tetherState.delete(joint);
293
325
  }
294
326
  }
295
327
  for (const joint of this.jointSet) {
296
- if (this.joints.has(joint)) continue;
328
+ if (this.joints.has(joint) || this.refusedJoints.has(joint)) continue;
297
329
  const { a, b } = joint._resolveBodies();
298
330
  const ea = this.entries.get(a);
299
331
  const eb = this.entries.get(b);
300
332
  if (!ea || !eb) continue;
333
+ if (joint.type === "prismatic") {
334
+ let off = Math.abs(ea.body.rotation() - eb.body.rotation()) / DEG;
335
+ off = Math.abs((off + 180) % 360 - 180);
336
+ if (off > .5) {
337
+ this.refusedJoints.add(joint);
338
+ diagnose(this.engine, "error", `[incanto] Joint2D '${joint.getPath()}': its two bodies disagree about the slide axis by ${off.toFixed(1)}° — '${a.name}' and '${b.name}' are authored with different rotations, and Rapier reads the axis in BOTH bodies' local frames. Give both bodies the same rotation. The joint was not made.`);
339
+ continue;
340
+ }
341
+ }
301
342
  const a1 = {
302
343
  x: (joint.anchor[0] ?? 0) / SCALE,
303
344
  y: (joint.anchor[1] ?? 0) / SCALE
@@ -316,10 +357,63 @@ var Physics2D = class {
316
357
  if (joint.type === "fixed") data = R.JointData.fixed(a1, 0, a2, 0);
317
358
  else if (joint.type === "rope") data = R.JointData.rope(length, a1, a2);
318
359
  else if (joint.type === "spring") data = R.JointData.spring(length, joint.stiffness, joint.damping, a1, a2);
319
- else data = R.JointData.revolute(a1, a2);
360
+ else if (joint.type === "prismatic") {
361
+ const ax = joint.axis[0] ?? 0;
362
+ const ay = joint.axis[1] ?? 0;
363
+ const n = Math.hypot(ax, ay) || 1;
364
+ data = R.JointData.prismatic(a1, a2, {
365
+ x: ax / n,
366
+ y: ay / n
367
+ });
368
+ } else data = R.JointData.revolute(a1, a2);
320
369
  const made = this.world.createImpulseJoint(data, ea.body, eb.body, true);
321
- made.setContactsEnabled(jointContacts(joint.type, joint.collide));
370
+ if (joint.type === "revolute" || joint.type === "prismatic") this.applyHinge(joint, made, true);
371
+ const contacts = jointContacts(joint.type, joint.collide);
372
+ made.setContactsEnabled(contacts);
322
373
  this.joints.set(joint, made);
374
+ this.jointBodies.set(joint, [a, b]);
375
+ this.linkJointed(a, b, contacts);
376
+ if (joint.type === "rope" || joint.type === "spring") this.tetherState.set(joint, this.tetherKey(joint));
377
+ }
378
+ for (const [joint, made] of this.joints) {
379
+ if (joint.type !== "rope" && joint.type !== "spring") continue;
380
+ const key = this.tetherKey(joint);
381
+ if (this.tetherState.get(joint) === key) continue;
382
+ this.world.removeImpulseJoint(made, true);
383
+ this.joints.delete(joint);
384
+ const pair = this.jointBodies.get(joint);
385
+ if (pair) this.unlinkJointed(pair[0], pair[1]);
386
+ this.jointBodies.delete(joint);
387
+ this.tetherState.delete(joint);
388
+ }
389
+ for (const [joint, made] of this.joints) {
390
+ if (joint.type !== "revolute" && joint.type !== "prismatic") continue;
391
+ this.applyHinge(joint, made, false);
392
+ const pair = this.jointBodies.get(joint);
393
+ const ea = pair && this.entries.get(pair[0]);
394
+ const eb = pair && this.entries.get(pair[1]);
395
+ if (!ea || !eb) continue;
396
+ if (joint.type === "revolute") {
397
+ let deg = (ea.body.rotation() - eb.body.rotation()) / DEG;
398
+ deg = ((deg + 180) % 360 + 360) % 360 - 180;
399
+ joint.angle = deg;
400
+ } else {
401
+ const ra = ea.body.rotation();
402
+ const rb = eb.body.rotation();
403
+ const ta = ea.body.translation();
404
+ const tb = eb.body.translation();
405
+ const [ax, ay] = [joint.anchor[0] ?? 0, joint.anchor[1] ?? 0];
406
+ const [bx, by] = [joint.targetAnchor[0] ?? 0, joint.targetAnchor[1] ?? 0];
407
+ const wax = ta.x * SCALE + ax * Math.cos(ra) - ay * Math.sin(ra);
408
+ const way = ta.y * SCALE + ax * Math.sin(ra) + ay * Math.cos(ra);
409
+ const wbx = tb.x * SCALE + bx * Math.cos(rb) - by * Math.sin(rb);
410
+ const wby = tb.y * SCALE + bx * Math.sin(rb) + by * Math.cos(rb);
411
+ const [ux, uy] = [joint.axis[0] ?? 0, joint.axis[1] ?? 0];
412
+ const n = Math.hypot(ux, uy) || 1;
413
+ const ex = (ux * Math.cos(ra) - uy * Math.sin(ra)) / n;
414
+ const ey = (ux * Math.sin(ra) + uy * Math.cos(ra)) / n;
415
+ joint.travel = (wax - wbx) * ex + (way - wby) * ey;
416
+ }
323
417
  }
324
418
  }
325
419
  /** Who is currently inside each sensor/body, maintained from the event drain. */
@@ -418,6 +512,11 @@ var Physics2D = class {
418
512
  massOf(node) {
419
513
  return this.entries.get(node)?.body.mass() ?? 1;
420
514
  }
515
+ /** The solver's rotation of a body, in degrees (the node's own unit). */
516
+ rotationOf(node) {
517
+ const e = this.entries.get(node);
518
+ return e ? e.body.rotation() / DEG : 0;
519
+ }
421
520
  /**
422
521
  * Current solver velocity in px/s (fresher than the node prop mid-step).
423
522
  *
@@ -436,10 +535,26 @@ var Physics2D = class {
436
535
  * adapters while only one of them had it.
437
536
  */
438
537
  applyImpulse(node, impulse) {
439
- this.entries.get(node)?.body.applyImpulse({
538
+ const e = this.entries.get(node);
539
+ if (!e) return;
540
+ if (node instanceof RigidBody2D) {
541
+ const vx = node.linearVelocity[0] ?? 0;
542
+ const vy = node.linearVelocity[1] ?? 0;
543
+ if (vx !== e.lastVX || vy !== e.lastVY) e.body.setLinvel({
544
+ x: vx / SCALE,
545
+ y: vy / SCALE
546
+ }, true);
547
+ }
548
+ e.body.applyImpulse({
440
549
  x: impulse[0] / SCALE,
441
550
  y: impulse[1] / SCALE
442
551
  }, true);
552
+ if (node instanceof RigidBody2D) {
553
+ const lv = e.body.linvel();
554
+ node.linearVelocity = [lv.x * SCALE, lv.y * SCALE];
555
+ e.lastVX = node.linearVelocity[0] ?? 0;
556
+ e.lastVY = node.linearVelocity[1] ?? 0;
557
+ }
443
558
  }
444
559
  /**
445
560
  * World-space raycast in PIXELS (y-down); `dir` may be any length (it is
@@ -514,6 +629,71 @@ var Physics2D = class {
514
629
  lastRoot = null;
515
630
  jointSet = /* @__PURE__ */ new Set();
516
631
  joints = /* @__PURE__ */ new Map();
632
+ /** The two bodies each live joint ties, so a departed joint can be unlinked. */
633
+ jointBodies = /* @__PURE__ */ new Map();
634
+ /** What a pin's limits/motor were last pushed as, so a write re-applies. */
635
+ hingeState = /* @__PURE__ */ new Map();
636
+ /** Joints refused at creation (bodies that disagree about the axis) — said once. */
637
+ refusedJoints = /* @__PURE__ */ new Set();
638
+ /** A rope's or spring's numbers as last made, so a write REMAKES the joint. */
639
+ tetherState = /* @__PURE__ */ new Map();
640
+ tetherKey(joint) {
641
+ return `${joint.length}/${joint.stiffness}/${joint.damping}`;
642
+ }
643
+ /**
644
+ * Who is jointed to whom, and whether that pair's contacts are on — see the
645
+ * 3D twin: a joint's own bodies are not the world to the character
646
+ * controller (not its obstacle when the contacts are off, never its ground).
647
+ */
648
+ jointed = /* @__PURE__ */ new Map();
649
+ linkJointed(a, b, contacts) {
650
+ for (const [x, y] of [[a, b], [b, a]]) {
651
+ let m = this.jointed.get(x);
652
+ if (!m) {
653
+ m = /* @__PURE__ */ new Map();
654
+ this.jointed.set(x, m);
655
+ }
656
+ m.set(y, contacts);
657
+ }
658
+ }
659
+ unlinkJointed(a, b) {
660
+ this.jointed.get(a)?.delete(b);
661
+ this.jointed.get(b)?.delete(a);
662
+ }
663
+ /**
664
+ * Push a pin's `limits` and motor to the solver — on creation, and again
665
+ * whenever either changes. Limits and `angle` are this body relative to the
666
+ * target; Rapier measures the other way round, hence the flip.
667
+ */
668
+ applyHinge(joint, made, first) {
669
+ const limits = JSON.stringify(joint.limits);
670
+ const motor = `${joint.motorSpeed}/${joint.motorStrength}`;
671
+ const last = this.hingeState.get(joint);
672
+ const unit = made;
673
+ const unitScale = joint.type === "revolute" ? DEG : 1 / SCALE;
674
+ if (first || last?.limits !== limits) {
675
+ if (joint.limits.length === 2) {
676
+ const lo = (joint.limits[0] ?? 0) * unitScale;
677
+ const hi = (joint.limits[1] ?? 0) * unitScale;
678
+ unit.setLimits(-Math.max(lo, hi), -Math.min(lo, hi));
679
+ } else if (!first) unit.setLimits(-1e9, 1e9);
680
+ }
681
+ if (first || last?.motor !== motor) {
682
+ if (joint.motorSpeed !== 0) unit.configureMotorVelocity(-joint.motorSpeed * unitScale, joint.motorStrength);
683
+ else if (!first) unit.configureMotorVelocity(0, 0);
684
+ }
685
+ this.hingeState.set(joint, {
686
+ limits,
687
+ motor
688
+ });
689
+ if (joint.motorSpeed !== 0) {
690
+ const pair = this.jointBodies.get(joint);
691
+ for (const b of pair ?? []) {
692
+ const e = this.entries.get(b);
693
+ if (e && !e.body.isFixed() && e.body.isSleeping()) e.body.wakeUp();
694
+ }
695
+ }
696
+ }
517
697
  bodySet = /* @__PURE__ */ new Set();
518
698
  /**
519
699
  * Move every body to its node's current transform — outline mode only.
@@ -523,12 +703,12 @@ var Physics2D = class {
523
703
  */
524
704
  poseFromTree() {
525
705
  for (const [node, e] of this.entries) {
526
- const [wx, wy] = worldPosition2D(node);
706
+ const [wx, wy, wa] = worldPose2D(node);
527
707
  e.body.setTranslation({
528
708
  x: wx / SCALE,
529
709
  y: wy / SCALE
530
710
  }, false);
531
- e.body.setRotation(node.rotation * DEG, false);
711
+ e.body.setRotation(wa * DEG, false);
532
712
  }
533
713
  this.world.propagateModifiedBodyPositionsToColliders();
534
714
  }
@@ -600,7 +780,7 @@ var Physics2D = class {
600
780
  let e = this.entries.get(node);
601
781
  const R = this.R;
602
782
  if (!e) {
603
- const [wx, wy] = worldPosition2D(node);
783
+ const [wx, wy, wa] = worldPose2D(node);
604
784
  const x = wx / SCALE;
605
785
  const y = wy / SCALE;
606
786
  let desc;
@@ -608,7 +788,7 @@ var Physics2D = class {
608
788
  if (dyn) desc = R.RigidBodyDesc.dynamic();
609
789
  else if (node instanceof CharacterBody2D || node instanceof Area2D) desc = R.RigidBodyDesc.kinematicPositionBased();
610
790
  else desc = R.RigidBodyDesc.fixed();
611
- desc.setTranslation(x, y).setRotation(node.rotation * DEG);
791
+ desc.setTranslation(x, y).setRotation(wa * DEG);
612
792
  if (node instanceof RigidBody2D) {
613
793
  desc.setGravityScale(node.gravityScale);
614
794
  desc.setLinearDamping(node.linearDamping).setAngularDamping(node.angularDamping);
@@ -629,7 +809,7 @@ var Physics2D = class {
629
809
  collider,
630
810
  lastX: wx,
631
811
  lastY: wy,
632
- lastRot: node.rotation,
812
+ lastRot: wa,
633
813
  lastVX: 0,
634
814
  lastVY: 0,
635
815
  lastAV: dyn ? node.angularVelocity : 0,
@@ -658,7 +838,7 @@ var Physics2D = class {
658
838
  node._enabledChanged = false;
659
839
  if (node.enabled) this.armOverlaps(node, e.collider);
660
840
  }
661
- const [px, py] = worldPosition2D(node);
841
+ const [px, py, pa] = worldPose2D(node);
662
842
  if (px !== e.lastX || py !== e.lastY) {
663
843
  e.body.setTranslation({
664
844
  x: px / SCALE,
@@ -667,9 +847,9 @@ var Physics2D = class {
667
847
  e.lastX = px;
668
848
  e.lastY = py;
669
849
  }
670
- if (node.rotation !== e.lastRot) {
671
- e.body.setRotation(node.rotation * DEG, true);
672
- e.lastRot = node.rotation;
850
+ if (pa !== e.lastRot) {
851
+ e.body.setRotation(pa * DEG, true);
852
+ e.lastRot = pa;
673
853
  }
674
854
  if (node instanceof RigidBody2D) {
675
855
  const vx = node.linearVelocity[0] ?? 0;
@@ -743,23 +923,62 @@ function platformTopPx(node) {
743
923
  const c = node.collider;
744
924
  return wy - (c.shape === "rect" ? (c.size?.[1] ?? 32) / 2 : c.shape === "capsule" ? (c.height ?? 32) / 2 + (c.radius ?? 8) : c.radius ?? 16);
745
925
  }
746
- function worldPosition2D(node) {
926
+ const poseChain2 = [];
927
+ const poseOut = [
928
+ 0,
929
+ 0,
930
+ 0
931
+ ];
932
+ /**
933
+ * The WORLD pose of `node` — position and angle (degrees) composed through
934
+ * every Node2D ancestor, the way the renderer composes them. This used to sum
935
+ * positions only, so a sensor 120 px down +x of a body turned 90° was put at
936
+ * world +x while the renderer drew it at world +y. Scale is not composed.
937
+ */
938
+ function worldPose2D(node) {
939
+ poseChain2.length = 0;
940
+ for (let n = node; n; n = n.parent) if (n instanceof Node2D) poseChain2.push(n);
747
941
  let x = 0;
748
942
  let y = 0;
749
- for (let n = node; n; n = n.parent) if (n instanceof Node2D) {
750
- x += n.position[0] ?? 0;
751
- y += n.position[1] ?? 0;
943
+ let angle = 0;
944
+ for (let i = poseChain2.length - 1; i >= 0; i--) {
945
+ const n = poseChain2[i];
946
+ const lx = n.position[0] ?? 0;
947
+ const ly = n.position[1] ?? 0;
948
+ if (angle !== 0) {
949
+ const a = angle * DEG;
950
+ const c = Math.cos(a);
951
+ const s = Math.sin(a);
952
+ x += lx * c - ly * s;
953
+ y += lx * s + ly * c;
954
+ } else {
955
+ x += lx;
956
+ y += ly;
957
+ }
958
+ angle += n.rotation;
752
959
  }
753
- return [x, y];
960
+ poseOut[0] = x;
961
+ poseOut[1] = y;
962
+ poseOut[2] = angle;
963
+ return poseOut;
754
964
  }
755
- function parentWorldOffset2D(node) {
756
- let x = 0;
757
- let y = 0;
965
+ function worldPosition2D(node) {
966
+ const p = worldPose2D(node);
967
+ return [p[0], p[1]];
968
+ }
969
+ /** The pose of `node`'s PARENT frame: [x, y, angleDeg]. */
970
+ function parentPose2D(node) {
971
+ let parent = null;
758
972
  for (let n = node.parent; n; n = n.parent) if (n instanceof Node2D) {
759
- x += n.position[0] ?? 0;
760
- y += n.position[1] ?? 0;
973
+ parent = n;
974
+ break;
761
975
  }
762
- return [x, y];
976
+ const p = worldPose2D(parent);
977
+ return [
978
+ p[0],
979
+ p[1],
980
+ p[2]
981
+ ];
763
982
  }
764
983
  function parseCollider2D(R, collider, nodeName) {
765
984
  validateCollider2D(collider, nodeName);