@zylem/behaviors 0.3.0 → 0.4.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 (169) hide show
  1. package/LICENSE +21 -0
  2. package/dist/{behavior-system-DYbx-x_-.d.ts → behavior-system-DSNek0W9.d.ts} +205 -29
  3. package/dist/{chunk-RER2IDPW.js → chunk-4URF65BR.js} +1 -0
  4. package/dist/chunk-4URF65BR.js.map +1 -0
  5. package/dist/{chunk-YYJP62QD.js → chunk-4V7TIRO2.js} +1 -0
  6. package/dist/chunk-4V7TIRO2.js.map +1 -0
  7. package/dist/{chunk-F7E23E26.js → chunk-4WH2ZN7P.js} +1 -0
  8. package/dist/chunk-4WH2ZN7P.js.map +1 -0
  9. package/dist/{chunk-VPZ32CCD.js → chunk-5SM5SH5O.js} +29 -28
  10. package/dist/chunk-5SM5SH5O.js.map +1 -0
  11. package/dist/{chunk-L76JG73P.js → chunk-5XZWTFGF.js} +7 -4
  12. package/dist/chunk-5XZWTFGF.js.map +1 -0
  13. package/dist/{chunk-HIBJDA5Z.js → chunk-64HKCSXD.js} +21 -17
  14. package/dist/chunk-64HKCSXD.js.map +1 -0
  15. package/dist/{chunk-WSKD7GY3.js → chunk-7JJEIADR.js} +1 -0
  16. package/dist/chunk-7JJEIADR.js.map +1 -0
  17. package/dist/{chunk-F6LFX4V3.js → chunk-BXP4K7RF.js} +3 -2
  18. package/dist/chunk-BXP4K7RF.js.map +1 -0
  19. package/dist/{chunk-2HKY6WF7.js → chunk-EIWQM6U3.js} +1 -0
  20. package/dist/chunk-EIWQM6U3.js.map +1 -0
  21. package/dist/{chunk-YDLO5M7S.js → chunk-EZNCCO4L.js} +1 -0
  22. package/dist/chunk-EZNCCO4L.js.map +1 -0
  23. package/dist/{chunk-T2P4JTQN.js → chunk-FRJDQM23.js} +110 -155
  24. package/dist/chunk-FRJDQM23.js.map +1 -0
  25. package/dist/{chunk-FHDYJ5WP.js → chunk-FTDZAKZW.js} +5 -17
  26. package/dist/chunk-FTDZAKZW.js.map +1 -0
  27. package/dist/{chunk-D53GNZXP.js → chunk-GFARAJV2.js} +6 -7
  28. package/dist/chunk-GFARAJV2.js.map +1 -0
  29. package/dist/{chunk-IQRG5BAZ.js → chunk-GTLA6LOJ.js} +18 -23
  30. package/dist/chunk-GTLA6LOJ.js.map +1 -0
  31. package/dist/{chunk-VO5J64DG.js → chunk-H4TJ7X4O.js} +4 -6
  32. package/dist/chunk-H4TJ7X4O.js.map +1 -0
  33. package/dist/{chunk-6UMC6BB5.js → chunk-HNESASBB.js} +7 -8
  34. package/dist/chunk-HNESASBB.js.map +1 -0
  35. package/dist/{chunk-4DVDFCVY.js → chunk-HZMY3TT4.js} +1 -0
  36. package/dist/chunk-HZMY3TT4.js.map +1 -0
  37. package/dist/{chunk-2HLZBWGD.js → chunk-IZJFUAXB.js} +1 -0
  38. package/dist/chunk-IZJFUAXB.js.map +1 -0
  39. package/dist/{chunk-UFHYR2LV.js → chunk-JXMF4NUI.js} +51 -2
  40. package/dist/chunk-JXMF4NUI.js.map +1 -0
  41. package/dist/{chunk-ZHBD65AZ.js → chunk-KDY5N6J4.js} +1 -0
  42. package/dist/chunk-KDY5N6J4.js.map +1 -0
  43. package/dist/chunk-KFFETUAL.js +40 -0
  44. package/dist/chunk-KFFETUAL.js.map +1 -0
  45. package/dist/chunk-LL4FLHIY.js +12 -0
  46. package/dist/chunk-LL4FLHIY.js.map +1 -0
  47. package/dist/{chunk-BC3IJYS7.js → chunk-MJFMNRMZ.js} +15 -15
  48. package/dist/chunk-MJFMNRMZ.js.map +1 -0
  49. package/dist/{chunk-WHKPWIQO.js → chunk-OHCPV2NT.js} +1 -0
  50. package/dist/chunk-OHCPV2NT.js.map +1 -0
  51. package/dist/{chunk-RQWWUML4.js → chunk-OTLSUYQQ.js} +2 -1
  52. package/dist/chunk-OTLSUYQQ.js.map +1 -0
  53. package/dist/{chunk-E4HTCQPK.js → chunk-PWF757B6.js} +7 -8
  54. package/dist/chunk-PWF757B6.js.map +1 -0
  55. package/dist/{chunk-4EWASZHB.js → chunk-QVCMJ4Q2.js} +1 -0
  56. package/dist/chunk-QVCMJ4Q2.js.map +1 -0
  57. package/dist/{chunk-FOOM42MK.js → chunk-RONH2KDI.js} +12 -13
  58. package/dist/chunk-RONH2KDI.js.map +1 -0
  59. package/dist/chunk-RTRFJVCZ.js +454 -0
  60. package/dist/chunk-RTRFJVCZ.js.map +1 -0
  61. package/dist/{chunk-IEA7F73P.js → chunk-UL6Q5HDP.js} +25 -2
  62. package/dist/chunk-UL6Q5HDP.js.map +1 -0
  63. package/dist/{chunk-K6CWKFHL.js → chunk-UYFOLI2X.js} +1 -0
  64. package/dist/chunk-UYFOLI2X.js.map +1 -0
  65. package/dist/{chunk-RD2YTTXY.js → chunk-V74N7D6Q.js} +9 -8
  66. package/dist/chunk-V74N7D6Q.js.map +1 -0
  67. package/dist/{chunk-ERS6SNA5.js → chunk-WOMHL23C.js} +2 -1
  68. package/dist/chunk-WOMHL23C.js.map +1 -0
  69. package/dist/{chunk-RHW3IDQX.js → chunk-WWVD5DR5.js} +1 -0
  70. package/dist/chunk-WWVD5DR5.js.map +1 -0
  71. package/dist/{chunk-SBACY5SU.js → chunk-WXXXJ22R.js} +14 -15
  72. package/dist/chunk-WXXXJ22R.js.map +1 -0
  73. package/dist/{chunk-6Y3GB76J.js → chunk-XLUJDKCT.js} +9 -8
  74. package/dist/chunk-XLUJDKCT.js.map +1 -0
  75. package/dist/{chunk-7Y6JVZ5H.js → chunk-XP3ASVGJ.js} +14 -10
  76. package/dist/chunk-XP3ASVGJ.js.map +1 -0
  77. package/dist/{chunk-7U4WFBWM.js → chunk-Y7YIAWPW.js} +11 -0
  78. package/dist/chunk-Y7YIAWPW.js.map +1 -0
  79. package/dist/{chunk-7IZ36Y5G.js → chunk-ZA43LPRM.js} +5 -24
  80. package/dist/chunk-ZA43LPRM.js.map +1 -0
  81. package/dist/{chunk-J5XLB24H.js → chunk-ZRQWSURL.js} +3 -2
  82. package/dist/chunk-ZRQWSURL.js.map +1 -0
  83. package/dist/cooldown.d.ts +2 -1
  84. package/dist/cooldown.js +3 -2
  85. package/dist/cooldown.js.map +1 -0
  86. package/dist/coordinators/boundary-ricochet-3d.d.ts +12 -5
  87. package/dist/coordinators/boundary-ricochet-3d.js +4 -2
  88. package/dist/coordinators/boundary-ricochet-3d.js.map +1 -0
  89. package/dist/coordinators/boundary-ricochet.d.ts +9 -7
  90. package/dist/coordinators/boundary-ricochet.js +4 -2
  91. package/dist/coordinators/boundary-ricochet.js.map +1 -0
  92. package/dist/coordinators/first-person-shooter.d.ts +9 -8
  93. package/dist/coordinators/first-person-shooter.js +4 -3
  94. package/dist/coordinators/first-person-shooter.js.map +1 -0
  95. package/dist/coordinators/multidirectional-space-shooter.d.ts +12 -9
  96. package/dist/coordinators/multidirectional-space-shooter.js +5 -4
  97. package/dist/coordinators/multidirectional-space-shooter.js.map +1 -0
  98. package/dist/coordinators/top-down-shooter.d.ts +9 -8
  99. package/dist/coordinators/top-down-shooter.js +5 -4
  100. package/dist/coordinators/top-down-shooter.js.map +1 -0
  101. package/dist/core.d.ts +4 -4
  102. package/dist/core.js +9 -36
  103. package/dist/core.js.map +1 -0
  104. package/dist/destructible-3d.d.ts +2 -1
  105. package/dist/destructible-3d.js +4 -3
  106. package/dist/destructible-3d.js.map +1 -0
  107. package/dist/destructible-prebake.js +2 -1
  108. package/dist/destructible-prebake.js.map +1 -0
  109. package/dist/first-person.d.ts +12 -12
  110. package/dist/first-person.js +12 -11
  111. package/dist/first-person.js.map +1 -0
  112. package/dist/index.d.ts +37 -14
  113. package/dist/index.js +85 -55
  114. package/dist/index.js.map +1 -0
  115. package/dist/jumper-2d.d.ts +7 -6
  116. package/dist/jumper-2d.js +8 -7
  117. package/dist/jumper-2d.js.map +1 -0
  118. package/dist/jumper-3d.d.ts +12 -11
  119. package/dist/jumper-3d.js +9 -8
  120. package/dist/jumper-3d.js.map +1 -0
  121. package/dist/{moveable-BCRpjAQb.d.ts → moveable-BLTHO7Jq.d.ts} +2 -78
  122. package/dist/particle-emitter/presets.js +2 -1
  123. package/dist/particle-emitter/presets.js.map +1 -0
  124. package/dist/particle-emitter.d.ts +1 -1
  125. package/dist/particle-emitter.js +5 -4
  126. package/dist/particle-emitter.js.map +1 -0
  127. package/dist/platformer-2d.d.ts +306 -0
  128. package/dist/platformer-2d.js +23 -0
  129. package/dist/platformer-2d.js.map +1 -0
  130. package/dist/platformer-3d.d.ts +74 -87
  131. package/dist/platformer-3d.js +7 -6
  132. package/dist/platformer-3d.js.map +1 -0
  133. package/dist/ricochet-2d.d.ts +1 -1
  134. package/dist/ricochet-2d.js +5 -4
  135. package/dist/ricochet-2d.js.map +1 -0
  136. package/dist/ricochet-3d.d.ts +1 -1
  137. package/dist/ricochet-3d.js +5 -4
  138. package/dist/ricochet-3d.js.map +1 -0
  139. package/dist/runtime-2d.d.ts +93 -2
  140. package/dist/runtime-2d.js +146 -22
  141. package/dist/runtime-2d.js.map +1 -0
  142. package/dist/screen-visibility.d.ts +2 -1
  143. package/dist/screen-visibility.js +3 -2
  144. package/dist/screen-visibility.js.map +1 -0
  145. package/dist/screen-wrap.d.ts +2 -1
  146. package/dist/screen-wrap.js +5 -4
  147. package/dist/screen-wrap.js.map +1 -0
  148. package/dist/shooter-2d.d.ts +5 -3
  149. package/dist/shooter-2d.js +5 -4
  150. package/dist/shooter-2d.js.map +1 -0
  151. package/dist/shooter-fire-DGripl0z.d.ts +55 -0
  152. package/dist/thruster.d.ts +8 -18
  153. package/dist/thruster.js +9 -8
  154. package/dist/thruster.js.map +1 -0
  155. package/dist/top-down-movement.d.ts +7 -4
  156. package/dist/top-down-movement.js +8 -7
  157. package/dist/top-down-movement.js.map +1 -0
  158. package/dist/{use-behavior-D_82jAL3.d.ts → use-behavior-h9-T5FSv.d.ts} +1 -1
  159. package/dist/world-boundary-2d.d.ts +2 -1
  160. package/dist/world-boundary-2d.js +6 -5
  161. package/dist/world-boundary-2d.js.map +1 -0
  162. package/dist/world-boundary-3d.d.ts +2 -1
  163. package/dist/world-boundary-3d.js +6 -5
  164. package/dist/world-boundary-3d.js.map +1 -0
  165. package/package.json +8 -7
  166. package/dist/chunk-23HQH3GJ.js +0 -34
  167. package/dist/chunk-K4UBXSZL.js +0 -175
  168. package/dist/runtime-pong.d.ts +0 -92
  169. package/dist/runtime-pong.js +0 -37
@@ -1,5 +1,5 @@
1
1
  // src/lib/behaviors/shared/ground-probe-3d.ts
2
- import { Cuboid } from "@dimforge/rapier3d-compat";
2
+ import { Ball, Capsule, Cuboid } from "@dimforge/rapier3d-compat";
3
3
  import {
4
4
  BoxGeometry,
5
5
  Mesh,
@@ -86,6 +86,42 @@ function extractShapeData(shapeType, internal) {
86
86
  }
87
87
 
88
88
  // src/lib/behaviors/shared/ground-probe-3d.ts
89
+ function inspectLiveColliderShape(entity) {
90
+ const shape = entity.collider?.shape;
91
+ if (!shape) return null;
92
+ if (shape instanceof Capsule) {
93
+ return {
94
+ halfBelow: shape.halfHeight + shape.radius,
95
+ halfX: shape.radius,
96
+ halfZ: shape.radius
97
+ };
98
+ }
99
+ if (shape instanceof Cuboid) {
100
+ return {
101
+ halfBelow: shape.halfExtents.y,
102
+ halfX: shape.halfExtents.x,
103
+ halfZ: shape.halfExtents.z
104
+ };
105
+ }
106
+ if (shape instanceof Ball) {
107
+ return {
108
+ halfBelow: shape.radius,
109
+ halfX: shape.radius,
110
+ halfZ: shape.radius
111
+ };
112
+ }
113
+ return null;
114
+ }
115
+ function getLiveColliderCenterOffsetY(entity) {
116
+ const collider = entity.collider;
117
+ const body = entity.body;
118
+ if (!collider || !body) return 0;
119
+ try {
120
+ return collider.translation().y - body.translation().y;
121
+ } catch {
122
+ return 0;
123
+ }
124
+ }
89
125
  var GROUND_SNAP_EPSILON = 1e-3;
90
126
  var CENTER_BOX_HALF = 0.05;
91
127
  var DEFAULT_BOX_THICKNESS = 0.05;
@@ -130,7 +166,6 @@ var GroundProbe3D = class {
130
166
  z: shapePos.z
131
167
  },
132
168
  origin: { ...shapePos },
133
- rayIndex: 0,
134
169
  colliderUuid: hit.collider?._parent?.userData?.uuid
135
170
  };
136
171
  }
@@ -195,6 +230,14 @@ function deriveDefaultBox(entity, mode) {
195
230
  if (mode === "center") {
196
231
  return { x: CENTER_BOX_HALF, y: CENTER_BOX_HALF, z: CENTER_BOX_HALF };
197
232
  }
233
+ const live = inspectLiveColliderShape(entity);
234
+ if (live) {
235
+ return {
236
+ x: Math.max(live.halfX * 0.7, CENTER_BOX_HALF),
237
+ y: DEFAULT_BOX_THICKNESS,
238
+ z: Math.max(live.halfZ * 0.7, CENTER_BOX_HALF)
239
+ };
240
+ }
198
241
  const runtimeColliderDesc = entity?.colliderDesc;
199
242
  if (runtimeColliderDesc) {
200
243
  const inspected = inspectColliderDesc(runtimeColliderDesc);
@@ -226,6 +269,11 @@ function deriveDefaultBox(entity, mode) {
226
269
  return { x: 0.35, y: DEFAULT_BOX_THICKNESS, z: 0.35 };
227
270
  }
228
271
  function getGroundAnchorOffsetY(entity) {
272
+ const live = inspectLiveColliderShape(entity);
273
+ if (live) {
274
+ const centerY2 = getLiveColliderCenterOffsetY(entity);
275
+ return live.halfBelow - centerY2;
276
+ }
229
277
  const runtimeColliderDesc = entity?.colliderDesc;
230
278
  if (runtimeColliderDesc) {
231
279
  const inspected = inspectColliderDesc(runtimeColliderDesc);
@@ -258,3 +306,4 @@ export {
258
306
  getGroundAnchorOffsetY,
259
307
  getGroundSnapTargetY
260
308
  };
309
+ //# sourceMappingURL=chunk-JXMF4NUI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/behaviors/shared/ground-probe-3d.ts","../src/lib/physics/collider-desc-inspect.ts"],"sourcesContent":["import { Ball, Capsule, Cuboid, type Collider, type RigidBody } from '@dimforge/rapier3d-compat';\nimport {\n\tBoxGeometry,\n\tMesh,\n\tMeshBasicMaterial,\n} from 'three';\nimport type { ZylemWorld } from '../../collision/world';\nimport { inspectColliderDesc } from '../../physics/collider-desc-inspect';\n\n/**\n * Probe sizing hint.\n *\n * - `center`: prefer a tiny box that behaves close to a single downward ray\n * (used by `Jumper3DBehavior` for thin/single-point ground tests).\n * - `any`: prefer a box sized from the entity's collider footprint\n * (used by `Platformer3D` so the character \"feels\" the ground under any\n * foot, not just dead-center).\n *\n * The actual box dimensions are still configurable via `options.box`.\n */\nexport type GroundProbeMode = 'center' | 'any';\n\nexport interface GroundProbeEntity {\n\tuuid: string;\n\tbody: RigidBody | null;\n\t/** Live Rapier collider; when present, probe geometry is derived from its shape. */\n\tcollider?: Collider | null;\n}\n\n/**\n * Geometry extracted from a live Rapier collider shape: the half-extent of\n * the shape below its center (`halfBelow`) and the horizontal half-extents\n * of its footprint. First-priority source for probe sizing/anchoring — works\n * for any entity with a live collider, engine metadata or not.\n */\ninterface LiveShapeInfo {\n\thalfBelow: number;\n\thalfX: number;\n\thalfZ: number;\n}\n\nfunction inspectLiveColliderShape(entity: GroundProbeEntity): LiveShapeInfo | null {\n\tconst shape = entity.collider?.shape;\n\tif (!shape) return null;\n\n\tif (shape instanceof Capsule) {\n\t\treturn {\n\t\t\thalfBelow: shape.halfHeight + shape.radius,\n\t\t\thalfX: shape.radius,\n\t\t\thalfZ: shape.radius,\n\t\t};\n\t}\n\tif (shape instanceof Cuboid) {\n\t\treturn {\n\t\t\thalfBelow: shape.halfExtents.y,\n\t\t\thalfX: shape.halfExtents.x,\n\t\t\thalfZ: shape.halfExtents.z,\n\t\t};\n\t}\n\tif (shape instanceof Ball) {\n\t\treturn {\n\t\t\thalfBelow: shape.radius,\n\t\t\thalfX: shape.radius,\n\t\t\thalfZ: shape.radius,\n\t\t};\n\t}\n\treturn null;\n}\n\n/**\n * Vertical offset of the collider center relative to the body origin\n * (0 when either translation is unavailable).\n */\nfunction getLiveColliderCenterOffsetY(entity: GroundProbeEntity): number {\n\tconst collider = entity.collider;\n\tconst body = entity.body;\n\tif (!collider || !body) return 0;\n\ttry {\n\t\treturn collider.translation().y - body.translation().y;\n\t} catch {\n\t\treturn 0;\n\t}\n}\n\nexport interface GroundProbeOptions {\n\t/** Maximum cast distance in world units. */\n\trayLength: number;\n\t/** Selects the default box size when `box` is not explicitly provided. */\n\tmode?: GroundProbeMode;\n\t/** Render a wireframe of the cast box for debugging. Requires `scene`. */\n\tdebug?: boolean;\n\t/** Three.js scene used to host the debug wireframe mesh. */\n\tscene?: any;\n\t/**\n\t * Vertical offset added to the body translation when computing the cast\n\t * origin. Allows callers to shift the probe to e.g. just above the\n\t * capsule's bottom face.\n\t */\n\toriginYOffset?: number;\n\t/**\n\t * Half-extents of the cast cuboid. When omitted the probe derives a\n\t * sensible default from the entity's collider:\n\t * - capsule: `(r * 0.7, 0.05, r * 0.7)` — a thin slab smaller than the\n\t * capsule footprint, so side walls don't register as ground.\n\t * - cuboid: shrink the bottom face by 30% so the probe sits well inside\n\t * the collider footprint.\n\t * - `mode: 'center'`: forces a tiny `(0.05, 0.05, 0.05)` box.\n\t */\n\tbox?: { x: number; y: number; z: number };\n}\n\nexport interface GroundProbeSupportHit {\n\ttoi: number;\n\tpoint: {\n\t\tx: number;\n\t\ty: number;\n\t\tz: number;\n\t};\n\torigin: {\n\t\tx: number;\n\t\ty: number;\n\t\tz: number;\n\t};\n\tcolliderUuid?: string;\n}\n\nexport const GROUND_SNAP_EPSILON = 0.001;\n\n/**\n * Tiny box used when the caller selects `mode: 'center'` — close enough to a\n * point cast that it preserves the old single-ray behavior of `Jumper3DBehavior`.\n */\nconst CENTER_BOX_HALF = 0.05;\n\n/**\n * Default vertical thickness of the cast cuboid. The box is intentionally thin\n * vertically so the impact distance closely matches a downward ray's TOI;\n * existing snap-to-ground math (which assumes `point.y ≈ origin.y - toi`)\n * keeps working with negligible drift.\n */\nconst DEFAULT_BOX_THICKNESS = 0.05;\n\n/**\n * GroundProbe3D — single-cast cuboid probe used by 3D platformer and jumper\n * behaviors to detect the ground beneath an entity.\n *\n * Replaces the previous 5-ray probe: a single shape-cast handles edge cases\n * (corner of a platform, narrow gap) more robustly and produces a single,\n * intuitive debug visualization (a wireframe box).\n */\nexport class GroundProbe3D {\n\tprivate debugMeshes = new Map<string, Mesh>();\n\n\tconstructor(private world: ZylemWorld | null) {}\n\n\tprobeSupport(\n\t\tentity: GroundProbeEntity,\n\t\toptions: GroundProbeOptions,\n\t): GroundProbeSupportHit | null {\n\t\tif (!this.world?.world || !entity.body) return null;\n\n\t\tconst translation = entity.body.translation();\n\t\tconst originYOffset = options.originYOffset ?? 0;\n\t\tconst box = options.box ?? deriveDefaultBox(entity, options.mode ?? 'any');\n\n\t\tconst shapePos = {\n\t\t\tx: translation.x,\n\t\t\ty: translation.y + originYOffset,\n\t\t\tz: translation.z,\n\t\t};\n\t\tconst shapeRot = { x: 0, y: 0, z: 0, w: 1 };\n\t\tconst shapeVel = { x: 0, y: -1, z: 0 };\n\n\t\tconst shape = new Cuboid(box.x, box.y, box.z);\n\t\tconst hit = this.world.world.castShape(\n\t\t\tshapePos,\n\t\t\tshapeRot,\n\t\t\tshapeVel,\n\t\t\tshape,\n\t\t\toptions.rayLength,\n\t\t\ttrue,\n\t\t\tundefined,\n\t\t\tundefined,\n\t\t\tundefined,\n\t\t\tentity.body,\n\t\t);\n\n\t\tlet support: GroundProbeSupportHit | null = null;\n\t\tif (hit) {\n\t\t\tsupport = {\n\t\t\t\ttoi: hit.toi,\n\t\t\t\tpoint: {\n\t\t\t\t\tx: shapePos.x,\n\t\t\t\t\ty: shapePos.y - hit.toi - box.y,\n\t\t\t\t\tz: shapePos.z,\n\t\t\t\t},\n\t\t\t\torigin: { ...shapePos },\n\t\t\t\tcolliderUuid: (hit.collider as any)?._parent?.userData?.uuid,\n\t\t\t};\n\t\t}\n\n\t\tif (options.debug && options.scene) {\n\t\t\tthis.updateDebugMesh(\n\t\t\t\tentity.uuid,\n\t\t\t\tshapePos,\n\t\t\t\tbox,\n\t\t\t\toptions.rayLength,\n\t\t\t\tBoolean(support),\n\t\t\t\toptions.scene,\n\t\t\t);\n\t\t} else {\n\t\t\tthis.disposeDebugMesh(entity.uuid);\n\t\t}\n\n\t\treturn support;\n\t}\n\n\tdetect(entity: GroundProbeEntity, options: GroundProbeOptions): boolean {\n\t\treturn this.probeSupport(entity, options) != null;\n\t}\n\n\tdestroyEntity(uuid: string): void {\n\t\tthis.disposeDebugMesh(uuid);\n\t}\n\n\tdestroy(): void {\n\t\tfor (const uuid of this.debugMeshes.keys()) {\n\t\t\tthis.disposeDebugMesh(uuid);\n\t\t}\n\t\tthis.debugMeshes.clear();\n\t}\n\n\tprivate updateDebugMesh(\n\t\tuuid: string,\n\t\tshapePos: { x: number; y: number; z: number },\n\t\tbox: { x: number; y: number; z: number },\n\t\trayLength: number,\n\t\thasGround: boolean,\n\t\tscene: any,\n\t): void {\n\t\tlet mesh = this.debugMeshes.get(uuid);\n\t\tif (!mesh) {\n\t\t\tconst geometry = new BoxGeometry(2 * box.x, 2 * box.y, 2 * box.z);\n\t\t\tconst material = new MeshBasicMaterial({\n\t\t\t\twireframe: true,\n\t\t\t\tcolor: 0xff0000,\n\t\t\t});\n\t\t\tmesh = new Mesh(geometry, material);\n\t\t\tscene.add(mesh);\n\t\t\tthis.debugMeshes.set(uuid, mesh);\n\t\t}\n\n\t\t// Position the box at the cast midpoint so users can see the swept volume.\n\t\tmesh.position.set(\n\t\t\tshapePos.x,\n\t\t\tshapePos.y - rayLength * 0.5,\n\t\t\tshapePos.z,\n\t\t);\n\t\tmesh.scale.set(1, Math.max(rayLength / Math.max(2 * box.y, 1e-6), 1), 1);\n\t\t(mesh.material as MeshBasicMaterial).color.setHex(\n\t\t\thasGround ? 0x00ff00 : 0xff0000,\n\t\t);\n\t}\n\n\tprivate disposeDebugMesh(uuid: string): void {\n\t\tconst mesh = this.debugMeshes.get(uuid);\n\t\tif (!mesh) return;\n\n\t\tmesh.removeFromParent();\n\t\tmesh.geometry.dispose();\n\t\t(mesh.material as MeshBasicMaterial).dispose();\n\n\t\tthis.debugMeshes.delete(uuid);\n\t}\n}\n\nfunction deriveDefaultBox(\n\tentity: GroundProbeEntity,\n\tmode: GroundProbeMode,\n): { x: number; y: number; z: number } {\n\tif (mode === 'center') {\n\t\treturn { x: CENTER_BOX_HALF, y: CENTER_BOX_HALF, z: CENTER_BOX_HALF };\n\t}\n\n\t// First priority: derive from the live Rapier collider shape. Works for\n\t// any consumer (e.g. the web harness) without engine collider metadata.\n\tconst live = inspectLiveColliderShape(entity);\n\tif (live) {\n\t\treturn {\n\t\t\tx: Math.max(live.halfX * 0.7, CENTER_BOX_HALF),\n\t\t\ty: DEFAULT_BOX_THICKNESS,\n\t\t\tz: Math.max(live.halfZ * 0.7, CENTER_BOX_HALF),\n\t\t};\n\t}\n\n\tconst runtimeColliderDesc = (entity as any)?.colliderDesc;\n\tif (runtimeColliderDesc) {\n\t\tconst inspected = inspectColliderDesc(runtimeColliderDesc);\n\t\tif (inspected.shape === 'capsule' && inspected.dimensions.length >= 2) {\n\t\t\tconst radius = inspected.dimensions[1] ?? 0.5;\n\t\t\tconst half = Math.max(radius * 0.7, CENTER_BOX_HALF);\n\t\t\treturn { x: half, y: DEFAULT_BOX_THICKNESS, z: half };\n\t\t}\n\t\tif (inspected.shape === 'cuboid' && inspected.dimensions.length >= 3) {\n\t\t\tconst halfX = (inspected.dimensions[0] ?? 0.5) * 0.7;\n\t\t\tconst halfZ = (inspected.dimensions[2] ?? 0.5) * 0.7;\n\t\t\treturn {\n\t\t\t\tx: Math.max(halfX, CENTER_BOX_HALF),\n\t\t\t\ty: DEFAULT_BOX_THICKNESS,\n\t\t\t\tz: Math.max(halfZ, CENTER_BOX_HALF),\n\t\t\t};\n\t\t}\n\t}\n\n\tconst collisionSize =\n\t\t(entity as any)?.options?.collision?.size ??\n\t\t(entity as any)?.options?.collisionSize ??\n\t\t(entity as any)?.options?.size;\n\tif (collisionSize) {\n\t\tconst halfX = ((collisionSize.x ?? 1) / 2) * 0.7;\n\t\tconst halfZ = ((collisionSize.z ?? collisionSize.x ?? 1) / 2) * 0.7;\n\t\treturn {\n\t\t\tx: Math.max(halfX, CENTER_BOX_HALF),\n\t\t\ty: DEFAULT_BOX_THICKNESS,\n\t\t\tz: Math.max(halfZ, CENTER_BOX_HALF),\n\t\t};\n\t}\n\n\treturn { x: 0.35, y: DEFAULT_BOX_THICKNESS, z: 0.35 };\n}\n\nexport function getGroundAnchorOffsetY(entity: any): number {\n\t// First priority: live Rapier collider shape (see inspectLiveColliderShape).\n\tconst live = inspectLiveColliderShape(entity);\n\tif (live) {\n\t\tconst centerY = getLiveColliderCenterOffsetY(entity);\n\t\treturn live.halfBelow - centerY;\n\t}\n\n\tconst runtimeColliderDesc = entity?.colliderDesc;\n\tif (runtimeColliderDesc) {\n\t\tconst inspected = inspectColliderDesc(runtimeColliderDesc);\n\t\tconst centerY = inspected.translation?.[1] ?? 0;\n\t\tif (inspected.shape === 'capsule' && inspected.dimensions.length >= 2) {\n\t\t\tconst halfCylinder = inspected.dimensions[0] ?? 0;\n\t\t\tconst radius = inspected.dimensions[1] ?? 0;\n\t\t\treturn halfCylinder + radius - centerY;\n\t\t}\n\t\tif (inspected.shape === 'cuboid' && inspected.dimensions.length >= 2) {\n\t\t\tconst halfHeight = inspected.dimensions[1] ?? 0;\n\t\t\treturn halfHeight - centerY;\n\t\t}\n\t}\n\n\tconst collisionSize =\n\t\tentity?.options?.collision?.size ??\n\t\tentity?.options?.collisionSize ??\n\t\tentity?.options?.size;\n\tconst height = collisionSize?.y ?? 0;\n\tif (height <= 0) {\n\t\treturn 0;\n\t}\n\n\tconst collisionPosition =\n\t\tentity?.options?.collision?.position ??\n\t\tentity?.options?.collisionPosition;\n\tconst centerY = collisionPosition?.y ?? height / 2;\n\treturn (height / 2) - centerY;\n}\n\nexport function getGroundSnapTargetY(\n\tentity: any,\n\tsupport: GroundProbeSupportHit,\n\tepsilon: number = GROUND_SNAP_EPSILON,\n): number {\n\treturn support.point.y + getGroundAnchorOffsetY(entity) + epsilon;\n}\n","import type { ColliderDesc } from '@dimforge/rapier3d-compat';\n\n/**\n * Subset of Rapier shape kinds that game-lib introspects for sizing decisions\n * (e.g. ground probes derive their cast box from the entity collider). Mirrors\n * the public Rapier shape vocabulary, kept as string literals so callers can\n * branch without depending on Rapier's internal shape enum.\n */\nexport type ColliderShapeKind =\n\t| 'ball'\n\t| 'cuboid'\n\t| 'capsule'\n\t| 'cone'\n\t| 'cylinder'\n\t| 'heightfield'\n\t| 'convexHull'\n\t| 'trimesh';\n\n/**\n * Plain, runtime-agnostic snapshot of a {@link ColliderDesc}'s shape and\n * placement. Only contains the fields game-lib needs today; extend as new\n * call sites appear.\n *\n * - `shape` — discriminator for the underlying Rapier shape.\n * - `dimensions` — shape-specific numbers (see {@link inspectColliderDesc}).\n * - `translation` — local offset relative to the parent body, when set.\n * - `vertices` / `indices` — raw geometry payload for `convexHull` and\n * `trimesh` shapes; tagged via `__zylemShapeData` at construction time.\n */\nexport interface ColliderDescInspection {\n\tshape: ColliderShapeKind;\n\tdimensions: number[];\n\ttranslation?: [number, number, number];\n\tvertices?: number[];\n\tindices?: number[];\n}\n\n/**\n * Read the shape kind, dimensions, and local translation off a Rapier\n * {@link ColliderDesc} without going through the WASM-backed accessors that\n * cannot cross worker boundaries.\n *\n * Layout per shape (matches Rapier's primitive constructors):\n * - `ball` → `[radius]`\n * - `cuboid` → `[halfX, halfY, halfZ]`\n * - `capsule` → `[halfHeight, radius]`\n * - `cone` → `[halfHeight, radius]`\n * - `cylinder` → `[halfHeight, radius]`\n * - `heightfield` → flattened heights array\n * - `convexHull` → flattened vertex array (set via the `__zylemShapeData` tag)\n * - `trimesh` → flattened vertex array (indices ignored here)\n */\nexport function inspectColliderDesc(desc: ColliderDesc): ColliderDescInspection {\n\tconst internal = desc as any;\n\tconst customShapeData = internal.__zylemShapeData as\n\t\t| { shape: 'convexHull'; vertices: number[] }\n\t\t| { shape: 'trimesh'; vertices: number[]; indices: number[] }\n\t\t| undefined;\n\n\tif (customShapeData?.shape === 'convexHull') {\n\t\tconst result = finalize('convexHull', [], internal);\n\t\tresult.vertices = [...customShapeData.vertices];\n\t\treturn result;\n\t}\n\tif (customShapeData?.shape === 'trimesh') {\n\t\tconst result = finalize('trimesh', [], internal);\n\t\tresult.vertices = [...customShapeData.vertices];\n\t\tresult.indices = [...customShapeData.indices];\n\t\treturn result;\n\t}\n\n\tconst shapeType = internal.shape?.type ?? internal.shapeType ?? 0;\n\tconst { shape, dimensions } = extractShapeData(shapeType, internal);\n\treturn finalize(shape, dimensions, internal);\n}\n\nfunction finalize(\n\tshape: ColliderShapeKind,\n\tdimensions: number[],\n\tinternal: any,\n): ColliderDescInspection {\n\tconst result: ColliderDescInspection = { shape, dimensions };\n\tconst t = internal.translation;\n\tif (t && (t.x !== 0 || t.y !== 0 || t.z !== 0)) {\n\t\tresult.translation = [t.x, t.y, t.z];\n\t}\n\treturn result;\n}\n\n/**\n * Rapier shape type enum values (from rapier internals):\n * 0 = Ball, 1 = Cuboid, 2 = Capsule, 6 = Cone, 7 = Cylinder, 11 = HeightField\n */\nfunction extractShapeData(\n\tshapeType: number,\n\tinternal: any,\n): { shape: ColliderShapeKind; dimensions: number[] } {\n\tswitch (shapeType) {\n\t\tcase 0:\n\t\t\treturn {\n\t\t\t\tshape: 'ball',\n\t\t\t\tdimensions: [internal.shape?.radius ?? internal.halfExtents?.x ?? 1],\n\t\t\t};\n\t\tcase 1:\n\t\t\treturn {\n\t\t\t\tshape: 'cuboid',\n\t\t\t\tdimensions: [\n\t\t\t\t\tinternal.shape?.halfExtents?.x ?? internal.halfExtents?.x ?? 0.5,\n\t\t\t\t\tinternal.shape?.halfExtents?.y ?? internal.halfExtents?.y ?? 0.5,\n\t\t\t\t\tinternal.shape?.halfExtents?.z ?? internal.halfExtents?.z ?? 0.5,\n\t\t\t\t],\n\t\t\t};\n\t\tcase 2:\n\t\t\treturn {\n\t\t\t\tshape: 'capsule',\n\t\t\t\tdimensions: [\n\t\t\t\t\tinternal.shape?.halfHeight ?? 0.5,\n\t\t\t\t\tinternal.shape?.radius ?? 0.5,\n\t\t\t\t],\n\t\t\t};\n\t\tcase 6:\n\t\t\treturn {\n\t\t\t\tshape: 'cone',\n\t\t\t\tdimensions: [\n\t\t\t\t\tinternal.shape?.halfHeight ?? 1,\n\t\t\t\t\tinternal.shape?.radius ?? 1,\n\t\t\t\t],\n\t\t\t};\n\t\tcase 7:\n\t\t\treturn {\n\t\t\t\tshape: 'cylinder',\n\t\t\t\tdimensions: [\n\t\t\t\t\tinternal.shape?.halfHeight ?? 1,\n\t\t\t\t\tinternal.shape?.radius ?? 1,\n\t\t\t\t],\n\t\t\t};\n\t\tcase 11: {\n\t\t\tconst heights = internal.shape?.heights;\n\t\t\treturn {\n\t\t\t\tshape: 'heightfield',\n\t\t\t\tdimensions: heights ? Array.from(heights) : [],\n\t\t\t};\n\t\t}\n\t\tdefault:\n\t\t\treturn { shape: 'cuboid', dimensions: [0.5, 0.5, 0.5] };\n\t}\n}\n"],"mappings":"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","names":["centerY"]}
@@ -79,3 +79,4 @@ export {
79
79
  wrapPoint2D,
80
80
  getBounds2DNormalFromHits
81
81
  };
82
+ //# sourceMappingURL=chunk-KDY5N6J4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/behaviors/shared/bounds-2d.ts"],"sourcesContent":["export interface Bounds2DPoint {\n\tx: number;\n\ty: number;\n}\n\nexport interface Bounds2DRect {\n\tminX: number;\n\tmaxX: number;\n\tminY: number;\n\tmaxY: number;\n}\n\nexport interface Bounds2DSize {\n\twidth: number;\n\theight: number;\n\tcenterX?: number;\n\tcenterY?: number;\n}\n\nexport interface Bounds2DHits {\n\tleft: boolean;\n\tright: boolean;\n\ttop: boolean;\n\tbottom: boolean;\n}\n\nexport interface Bounds2DWrapResult {\n\tx: number;\n\ty: number;\n\twrapped: boolean;\n}\n\nexport function createBounds2DRect({\n\twidth,\n\theight,\n\tcenterX = 0,\n\tcenterY = 0,\n}: Bounds2DSize): Bounds2DRect {\n\tconst halfWidth = width / 2;\n\tconst halfHeight = height / 2;\n\treturn {\n\t\tminX: centerX - halfWidth,\n\t\tmaxX: centerX + halfWidth,\n\t\tminY: centerY - halfHeight,\n\t\tmaxY: centerY + halfHeight,\n\t};\n}\n\nexport function computeBounds2DHits(\n\tposition: Bounds2DPoint,\n\tbounds: Bounds2DRect,\n): Bounds2DHits {\n\tconst hits: Bounds2DHits = {\n\t\ttop: false,\n\t\tbottom: false,\n\t\tleft: false,\n\t\tright: false,\n\t};\n\n\tif (position.x <= bounds.minX) hits.left = true;\n\telse if (position.x >= bounds.maxX) hits.right = true;\n\n\tif (position.y <= bounds.minY) hits.bottom = true;\n\telse if (position.y >= bounds.maxY) hits.top = true;\n\n\treturn hits;\n}\n\nexport function hasAnyBounds2DHit(hits: Bounds2DHits): boolean {\n\treturn hits.left || hits.right || hits.top || hits.bottom;\n}\n\nexport function constrainMovementToBounds2D(\n\thits: Bounds2DHits,\n\tmoveX: number,\n\tmoveY: number,\n): { moveX: number; moveY: number } {\n\tlet adjustedX = moveX;\n\tlet adjustedY = moveY;\n\n\tif ((hits.left && moveX < 0) || (hits.right && moveX > 0)) {\n\t\tadjustedX = 0;\n\t}\n\n\tif ((hits.bottom && moveY < 0) || (hits.top && moveY > 0)) {\n\t\tadjustedY = 0;\n\t}\n\n\treturn { moveX: adjustedX, moveY: adjustedY };\n}\n\nexport function wrapPoint2D(\n\tposition: Bounds2DPoint,\n\tbounds: Bounds2DRect,\n): Bounds2DWrapResult {\n\tlet x = position.x;\n\tlet y = position.y;\n\tlet wrapped = false;\n\n\tif (position.x < bounds.minX) {\n\t\tx = bounds.maxX - (bounds.minX - position.x);\n\t\twrapped = true;\n\t} else if (position.x > bounds.maxX) {\n\t\tx = bounds.minX + (position.x - bounds.maxX);\n\t\twrapped = true;\n\t}\n\n\tif (position.y < bounds.minY) {\n\t\ty = bounds.maxY - (bounds.minY - position.y);\n\t\twrapped = true;\n\t} else if (position.y > bounds.maxY) {\n\t\ty = bounds.minY + (position.y - bounds.maxY);\n\t\twrapped = true;\n\t}\n\n\treturn { x, y, wrapped };\n}\n\nexport function getBounds2DNormalFromHits(\n\thits: Bounds2DHits,\n): { x: number; y: number } {\n\tlet x = 0;\n\tlet y = 0;\n\n\tif (hits.left) x = 1;\n\tif (hits.right) x = -1;\n\tif (hits.bottom) y = 1;\n\tif (hits.top) y = -1;\n\n\treturn { x, y };\n}\n"],"mappings":";AAgCO,SAAS,mBAAmB;AAAA,EAClC;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,UAAU;AACX,GAA+B;AAC9B,QAAM,YAAY,QAAQ;AAC1B,QAAM,aAAa,SAAS;AAC5B,SAAO;AAAA,IACN,MAAM,UAAU;AAAA,IAChB,MAAM,UAAU;AAAA,IAChB,MAAM,UAAU;AAAA,IAChB,MAAM,UAAU;AAAA,EACjB;AACD;AAEO,SAAS,oBACf,UACA,QACe;AACf,QAAM,OAAqB;AAAA,IAC1B,KAAK;AAAA,IACL,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EACR;AAEA,MAAI,SAAS,KAAK,OAAO,KAAM,MAAK,OAAO;AAAA,WAClC,SAAS,KAAK,OAAO,KAAM,MAAK,QAAQ;AAEjD,MAAI,SAAS,KAAK,OAAO,KAAM,MAAK,SAAS;AAAA,WACpC,SAAS,KAAK,OAAO,KAAM,MAAK,MAAM;AAE/C,SAAO;AACR;AAEO,SAAS,kBAAkB,MAA6B;AAC9D,SAAO,KAAK,QAAQ,KAAK,SAAS,KAAK,OAAO,KAAK;AACpD;AAEO,SAAS,4BACf,MACA,OACA,OACmC;AACnC,MAAI,YAAY;AAChB,MAAI,YAAY;AAEhB,MAAK,KAAK,QAAQ,QAAQ,KAAO,KAAK,SAAS,QAAQ,GAAI;AAC1D,gBAAY;AAAA,EACb;AAEA,MAAK,KAAK,UAAU,QAAQ,KAAO,KAAK,OAAO,QAAQ,GAAI;AAC1D,gBAAY;AAAA,EACb;AAEA,SAAO,EAAE,OAAO,WAAW,OAAO,UAAU;AAC7C;AAEO,SAAS,YACf,UACA,QACqB;AACrB,MAAI,IAAI,SAAS;AACjB,MAAI,IAAI,SAAS;AACjB,MAAI,UAAU;AAEd,MAAI,SAAS,IAAI,OAAO,MAAM;AAC7B,QAAI,OAAO,QAAQ,OAAO,OAAO,SAAS;AAC1C,cAAU;AAAA,EACX,WAAW,SAAS,IAAI,OAAO,MAAM;AACpC,QAAI,OAAO,QAAQ,SAAS,IAAI,OAAO;AACvC,cAAU;AAAA,EACX;AAEA,MAAI,SAAS,IAAI,OAAO,MAAM;AAC7B,QAAI,OAAO,QAAQ,OAAO,OAAO,SAAS;AAC1C,cAAU;AAAA,EACX,WAAW,SAAS,IAAI,OAAO,MAAM;AACpC,QAAI,OAAO,QAAQ,SAAS,IAAI,OAAO;AACvC,cAAU;AAAA,EACX;AAEA,SAAO,EAAE,GAAG,GAAG,QAAQ;AACxB;AAEO,SAAS,0BACf,MAC2B;AAC3B,MAAI,IAAI;AACR,MAAI,IAAI;AAER,MAAI,KAAK,KAAM,KAAI;AACnB,MAAI,KAAK,MAAO,KAAI;AACpB,MAAI,KAAK,OAAQ,KAAI;AACrB,MAAI,KAAK,IAAK,KAAI;AAElB,SAAO,EAAE,GAAG,EAAE;AACf;","names":[]}
@@ -0,0 +1,40 @@
1
+ import {
2
+ getBounds3DNormalFromHits
3
+ } from "./chunk-OHCPV2NT.js";
4
+ import {
5
+ updateBoundaryRicochet
6
+ } from "./chunk-LL4FLHIY.js";
7
+
8
+ // src/lib/coordinators/boundary-ricochet-3d.coordinator.ts
9
+ var BoundaryRicochet3DCoordinator = class {
10
+ constructor(entity, boundary, ricochet) {
11
+ this.entity = entity;
12
+ this.boundary = boundary;
13
+ this.ricochet = ricochet;
14
+ }
15
+ entity;
16
+ boundary;
17
+ ricochet;
18
+ update() {
19
+ return updateBoundaryRicochet({
20
+ getLastHits: () => this.boundary.getLastHits(),
21
+ hasAnyHit: (hits) => hits.left || hits.right || hits.top || hits.bottom || hits.back || hits.front,
22
+ getNormal: (hits) => getBounds3DNormalFromHits(hits),
23
+ snapToClamped: () => {
24
+ const clamped = this.boundary.getLastClampedPosition();
25
+ if (clamped && this.entity.body) {
26
+ this.entity.body.setTranslation(clamped, true);
27
+ }
28
+ },
29
+ applyRicochet: (normal) => this.ricochet.applyRicochet({
30
+ entity: this.entity,
31
+ contact: { normal }
32
+ })
33
+ });
34
+ }
35
+ };
36
+
37
+ export {
38
+ BoundaryRicochet3DCoordinator
39
+ };
40
+ //# sourceMappingURL=chunk-KFFETUAL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/coordinators/boundary-ricochet-3d.coordinator.ts"],"sourcesContent":["import type { MoveableEntity } from '../capabilities/moveable';\nimport type { BaseEntityInterface } from '../types/entity-types';\nimport type { Ricochet3DHandle } from '../behaviors/ricochet-3d/ricochet-3d.descriptor';\nimport type { WorldBoundary3DHandle } from '../behaviors/world-boundary-3d/world-boundary-3d.descriptor';\nimport { getBounds3DNormalFromHits } from '../behaviors/shared/bounds-3d';\nimport { updateBoundaryRicochet } from './shared/boundary-ricochet';\n\n/** Entity shape required by the 3D boundary-ricochet coordinator. */\nexport type BoundaryRicochet3DEntity = BaseEntityInterface & MoveableEntity;\n\n/**\n * Coordinator that bridges WorldBoundary3DBehavior and Ricochet3DBehavior.\n * Snaps the body back to the boundary's clamped position before reflecting,\n * so fast entities can't tunnel outside the box between frames.\n */\nexport class BoundaryRicochet3DCoordinator {\n\tconstructor(\n\t\tprivate entity: BoundaryRicochet3DEntity,\n\t\tprivate boundary: WorldBoundary3DHandle,\n\t\tprivate ricochet: Ricochet3DHandle,\n\t) {}\n\n\tpublic update(): boolean {\n\t\treturn updateBoundaryRicochet({\n\t\t\tgetLastHits: () => this.boundary.getLastHits(),\n\t\t\thasAnyHit: (hits) =>\n\t\t\t\thits.left || hits.right || hits.top || hits.bottom || hits.back || hits.front,\n\t\t\tgetNormal: (hits) => getBounds3DNormalFromHits(hits),\n\t\t\tsnapToClamped: () => {\n\t\t\t\tconst clamped = this.boundary.getLastClampedPosition();\n\t\t\t\tif (clamped && this.entity.body) {\n\t\t\t\t\tthis.entity.body.setTranslation(clamped, true);\n\t\t\t\t}\n\t\t\t},\n\t\t\tapplyRicochet: (normal) =>\n\t\t\t\tthis.ricochet.applyRicochet({\n\t\t\t\t\tentity: this.entity,\n\t\t\t\t\tcontact: { normal },\n\t\t\t\t}),\n\t\t});\n\t}\n}\n"],"mappings":";;;;;;;;AAeO,IAAM,gCAAN,MAAoC;AAAA,EAC1C,YACS,QACA,UACA,UACP;AAHO;AACA;AACA;AAAA,EACN;AAAA,EAHM;AAAA,EACA;AAAA,EACA;AAAA,EAGF,SAAkB;AACxB,WAAO,uBAAuB;AAAA,MAC7B,aAAa,MAAM,KAAK,SAAS,YAAY;AAAA,MAC7C,WAAW,CAAC,SACX,KAAK,QAAQ,KAAK,SAAS,KAAK,OAAO,KAAK,UAAU,KAAK,QAAQ,KAAK;AAAA,MACzE,WAAW,CAAC,SAAS,0BAA0B,IAAI;AAAA,MACnD,eAAe,MAAM;AACpB,cAAM,UAAU,KAAK,SAAS,uBAAuB;AACrD,YAAI,WAAW,KAAK,OAAO,MAAM;AAChC,eAAK,OAAO,KAAK,eAAe,SAAS,IAAI;AAAA,QAC9C;AAAA,MACD;AAAA,MACA,eAAe,CAAC,WACf,KAAK,SAAS,cAAc;AAAA,QAC3B,QAAQ,KAAK;AAAA,QACb,SAAS,EAAE,OAAO;AAAA,MACnB,CAAC;AAAA,IACH,CAAC;AAAA,EACF;AACD;","names":[]}
@@ -0,0 +1,12 @@
1
+ // src/lib/coordinators/shared/boundary-ricochet.ts
2
+ function updateBoundaryRicochet(update) {
3
+ const hits = update.getLastHits();
4
+ if (!hits || !update.hasAnyHit(hits)) return false;
5
+ update.snapToClamped?.();
6
+ return update.applyRicochet(update.getNormal(hits));
7
+ }
8
+
9
+ export {
10
+ updateBoundaryRicochet
11
+ };
12
+ //# sourceMappingURL=chunk-LL4FLHIY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/coordinators/shared/boundary-ricochet.ts"],"sourcesContent":["/**\n * Shared boundary→ricochet glue used by the 2D and 3D boundary-ricochet\n * coordinators. Reads the boundary behavior's last hits, optionally snaps the\n * body back inside the bounds, and applies the ricochet reflection.\n */\nexport interface BoundaryRicochetUpdate<THits, TNormal> {\n\t/** Boundary behavior handle (or any source of last-frame hits). */\n\tgetLastHits(): THits | null;\n\t/** Whether any boundary face was hit this frame. */\n\thasAnyHit(hits: THits): boolean;\n\t/** Convert hits into a reflection normal. */\n\tgetNormal(hits: THits): TNormal;\n\t/** Optional pre-reflection snap (e.g. clamp the body back inside bounds). */\n\tsnapToClamped?(): void;\n\t/** Apply the reflection; returns true when a ricochet occurred. */\n\tapplyRicochet(normal: TNormal): boolean;\n}\n\nexport function updateBoundaryRicochet<THits, TNormal>(\n\tupdate: BoundaryRicochetUpdate<THits, TNormal>,\n): boolean {\n\tconst hits = update.getLastHits();\n\tif (!hits || !update.hasAnyHit(hits)) return false;\n\n\tupdate.snapToClamped?.();\n\treturn update.applyRicochet(update.getNormal(hits));\n}\n"],"mappings":";AAkBO,SAAS,uBACf,QACU;AACV,QAAM,OAAO,OAAO,YAAY;AAChC,MAAI,CAAC,QAAQ,CAAC,OAAO,UAAU,IAAI,EAAG,QAAO;AAE7C,SAAO,gBAAgB;AACvB,SAAO,OAAO,cAAc,OAAO,UAAU,IAAI,CAAC;AACnD;","names":[]}
@@ -1,18 +1,18 @@
1
1
  import {
2
2
  createThrusterInputComponent,
3
3
  createThrusterMovementComponent
4
- } from "./chunk-2HLZBWGD.js";
4
+ } from "./chunk-IZJFUAXB.js";
5
5
  import {
6
6
  getRotationAngle2D,
7
- normalizeDirection2D
8
- } from "./chunk-7U4WFBWM.js";
7
+ normalizeDirection2DInto
8
+ } from "./chunk-Y7YIAWPW.js";
9
9
  import {
10
10
  SyncStateMachine,
11
11
  t
12
- } from "./chunk-RER2IDPW.js";
12
+ } from "./chunk-4URF65BR.js";
13
13
  import {
14
14
  defineBehavior
15
- } from "./chunk-YDLO5M7S.js";
15
+ } from "./chunk-EZNCCO4L.js";
16
16
 
17
17
  // src/lib/behaviors/thruster/thruster-fsm.ts
18
18
  var ThrusterState = /* @__PURE__ */ ((ThrusterState2) => {
@@ -88,8 +88,9 @@ var ThrusterFSM = class {
88
88
  }
89
89
  };
90
90
 
91
- // src/lib/behaviors/thruster/thruster-movement.behavior.ts
92
- var ThrusterMovementBehavior = class {
91
+ // src/lib/behaviors/thruster/thruster.system.ts
92
+ var _thrustDirection = { x: 0, y: 0 };
93
+ var ThrusterSystem = class {
93
94
  constructor(world) {
94
95
  this.world = world;
95
96
  }
@@ -114,13 +115,13 @@ var ThrusterMovementBehavior = class {
114
115
  }
115
116
  const rotationZ = getRotationAngle2D(body.rotation());
116
117
  const currentVel = body.linvel();
117
- const vectorThrust = normalizeDirection2D(input.thrustX, input.thrustY);
118
- if (vectorThrust) {
118
+ const hasVectorThrust = normalizeDirection2DInto(input.thrustX, input.thrustY, _thrustDirection);
119
+ if (hasVectorThrust) {
119
120
  const thrustScale = Math.min(1, Math.hypot(input.thrustX, input.thrustY));
120
121
  const thrustAmount = thruster.linearThrust * thrustScale * 0.1;
121
122
  body.setLinvel({
122
- x: currentVel.x + vectorThrust.x * thrustAmount,
123
- y: currentVel.y + vectorThrust.y * thrustAmount,
123
+ x: currentVel.x + _thrustDirection.x * thrustAmount,
124
+ y: currentVel.y + _thrustDirection.y * thrustAmount,
124
125
  z: currentVel.z
125
126
  }, true);
126
127
  } else if (input.thrust !== 0) {
@@ -157,12 +158,10 @@ var defaultOptions = {
157
158
  var THRUSTER_BEHAVIOR_KEY = /* @__PURE__ */ Symbol.for("zylem:behavior:thruster");
158
159
  var ThrusterBehaviorSystem = class {
159
160
  constructor(world, wasmStage, getBehaviorLinks) {
160
- this.world = world;
161
161
  this.wasmStage = wasmStage;
162
162
  this.getBehaviorLinks = getBehaviorLinks;
163
- this.movementBehavior = new ThrusterMovementBehavior(world);
163
+ this.movementBehavior = new ThrusterSystem(world);
164
164
  }
165
- world;
166
165
  wasmStage;
167
166
  getBehaviorLinks;
168
167
  movementBehavior;
@@ -235,6 +234,7 @@ export {
235
234
  ThrusterState,
236
235
  ThrusterEvent,
237
236
  ThrusterFSM,
238
- ThrusterMovementBehavior,
237
+ ThrusterSystem,
239
238
  ThrusterBehavior
240
239
  };
240
+ //# sourceMappingURL=chunk-MJFMNRMZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/behaviors/thruster/thruster-fsm.ts","../src/lib/behaviors/thruster/thruster.system.ts","../src/lib/behaviors/thruster/thruster.descriptor.ts"],"sourcesContent":["/**\n * ThrusterFSM\n * \n * State machine controller for thruster behavior.\n * FSM does NOT touch physics or ThrusterSystem - it only writes ThrusterInputComponent.\n */\n\nimport { SyncStateMachine, t } from '../../core/utility/sync-state-machine';\nimport type { ThrusterInputComponent } from './components';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// FSM State Model\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport enum ThrusterState {\n\tIdle = 'idle',\n\tActive = 'active',\n\tBoosting = 'boosting',\n\tDisabled = 'disabled',\n\tDocked = 'docked',\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// FSM Events\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport enum ThrusterEvent {\n\tActivate = 'activate',\n\tDeactivate = 'deactivate',\n\tBoost = 'boost',\n\tEndBoost = 'endBoost',\n\tDisable = 'disable',\n\tEnable = 'enable',\n\tDock = 'dock',\n\tUndock = 'undock',\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// FSM Context Object\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface ThrusterFSMContext {\n\tinput: ThrusterInputComponent;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Player Input (raw input from controller/keyboard)\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface PlayerInput {\n\tthrust: number;\n\trotate: number;\n\tthrustX?: number;\n\tthrustY?: number;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// ThrusterFSM Controller\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport class ThrusterFSM {\n\tmachine: SyncStateMachine<ThrusterState, ThrusterEvent, never>;\n\n\tconstructor(private ctx: ThrusterFSMContext) {\n\t\tthis.machine = new SyncStateMachine<ThrusterState, ThrusterEvent, never>(\n\t\t\tThrusterState.Idle,\n\t\t\t[\n\t\t\t\t// Core transitions\n\t\t\t\tt(ThrusterState.Idle, ThrusterEvent.Activate, ThrusterState.Active),\n\t\t\t\tt(ThrusterState.Active, ThrusterEvent.Deactivate, ThrusterState.Idle),\n\t\t\t\tt(ThrusterState.Active, ThrusterEvent.Boost, ThrusterState.Boosting),\n\t\t\t\tt(ThrusterState.Active, ThrusterEvent.Disable, ThrusterState.Disabled),\n\t\t\t\tt(ThrusterState.Active, ThrusterEvent.Dock, ThrusterState.Docked),\n\t\t\t\tt(ThrusterState.Boosting, ThrusterEvent.EndBoost, ThrusterState.Active),\n\t\t\t\tt(ThrusterState.Boosting, ThrusterEvent.Disable, ThrusterState.Disabled),\n\t\t\t\tt(ThrusterState.Disabled, ThrusterEvent.Enable, ThrusterState.Idle),\n\t\t\t\tt(ThrusterState.Docked, ThrusterEvent.Undock, ThrusterState.Idle),\n\t\t\t\t// Self-transitions (no-ops for redundant events)\n\t\t\t\tt(ThrusterState.Idle, ThrusterEvent.Deactivate, ThrusterState.Idle),\n\t\t\t\tt(ThrusterState.Active, ThrusterEvent.Activate, ThrusterState.Active),\n\t\t\t]\n\t\t);\n\t}\n\n\t/**\n\t * Get current state\n\t */\n\tgetState(): ThrusterState {\n\t\treturn this.machine.getState();\n\t}\n\n\t/**\n\t * Dispatch an event to transition state\n\t */\n\tdispatch(event: ThrusterEvent): void {\n\t\tif (this.machine.can(event)) {\n\t\t\tthis.machine.syncDispatch(event);\n\t\t}\n\t}\n\n\t/**\n\t * Update FSM state based on player input.\n\t * Auto-transitions between Idle/Active to report current state.\n\t * Does NOT modify input - just observes and reports.\n\t */\n\tupdate(playerInput: PlayerInput): void {\n\t\tconst state = this.machine.getState();\n\t\tconst hasInput =\n\t\t\tMath.abs(playerInput.thrust) > 0.01\n\t\t\t|| Math.abs(playerInput.rotate) > 0.01\n\t\t\t|| Math.abs(playerInput.thrustX ?? 0) > 0.01\n\t\t\t|| Math.abs(playerInput.thrustY ?? 0) > 0.01;\n\n\t\t// Auto-transition to report state based on input\n\t\tif (hasInput && state === ThrusterState.Idle) {\n\t\t\tthis.dispatch(ThrusterEvent.Activate);\n\t\t} else if (!hasInput && state === ThrusterState.Active) {\n\t\t\tthis.dispatch(ThrusterEvent.Deactivate);\n\t\t}\n\t}\n}\n","/**\n * ThrusterSystem\n * \n * This is the heart of the thruster movement system - a pure, stateless force generator.\n * Works identically for player, AI, and replay.\n */\n\nimport type { ZylemWorld } from '../../collision/world';\nimport type { PhysicsBodyComponent } from '../components';\nimport type { ThrusterMovementComponent, ThrusterInputComponent } from './components';\nimport {\n\tgetRotationAngle2D,\n\tnormalizeDirection2DInto,\n\ttype Direction2D,\n} from '../shared/direction-2d';\n\n// Per-frame scratch; normalizeDirection2DInto writes here instead of\n// allocating a fresh direction object per entity per frame.\nconst _thrustDirection: Direction2D = { x: 0, y: 0 };\n\n/**\n * Entity with thruster components\n */\nexport interface ThrusterEntity {\n\tphysics: PhysicsBodyComponent;\n\tthruster: ThrusterMovementComponent;\n\t$thruster: ThrusterInputComponent;\n}\n\n/**\n * ThrusterSystem - Force generator for thruster-equipped entities\n * \n * Responsibilities:\n * - Query entities with PhysicsBody, ThrusterMovement, and ThrusterInput components\n * - Apply velocities based on thrust input (2D mode)\n * - Apply angular velocity based on rotation input\n */\nexport class ThrusterSystem {\n\tconstructor(private world: ZylemWorld | null) {}\n\n\t/**\n\t * Update a single thruster-enabled entity.\n\t */\n\tupdateEntity(gameEntity: any, _dt: number): void {\n\t\tif (!gameEntity.physics?.body || !gameEntity.thruster || !gameEntity.$thruster) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst e: ThrusterEntity = {\n\t\t\tphysics: gameEntity.physics,\n\t\t\tthruster: gameEntity.thruster,\n\t\t\t$thruster: gameEntity.$thruster,\n\t\t};\n\n\t\tconst body = e.physics.body;\n\t\tconst thruster = e.thruster;\n\t\tconst input = e.$thruster;\n\n\t\tif (thruster.linearDamping != null && typeof body.setLinearDamping === 'function') {\n\t\t\tbody.setLinearDamping(thruster.linearDamping);\n\t\t}\n\n\t\t// Get Z rotation from quaternion (for 2D sprites)\n\t\tconst rotationZ = getRotationAngle2D(body.rotation());\n\t\tconst currentVel = body.linvel();\n\t\tconst hasVectorThrust = normalizeDirection2DInto(input.thrustX, input.thrustY, _thrustDirection);\n\n\t\t// --- Linear thrust ---\n\t\tif (hasVectorThrust) {\n\t\t\tconst thrustScale = Math.min(1, Math.hypot(input.thrustX, input.thrustY));\n\t\t\tconst thrustAmount = thruster.linearThrust * thrustScale * 0.1;\n\t\t\tbody.setLinvel({\n\t\t\t\tx: currentVel.x + _thrustDirection.x * thrustAmount,\n\t\t\t\ty: currentVel.y + _thrustDirection.y * thrustAmount,\n\t\t\t\tz: currentVel.z,\n\t\t\t}, true);\n\t\t} else if (input.thrust !== 0) {\n\t\t\tconst forwardX = Math.sin(-rotationZ);\n\t\t\tconst forwardY = Math.cos(-rotationZ);\n\t\t\tconst thrustAmount = thruster.linearThrust * input.thrust * 0.1;\n\t\t\tbody.setLinvel({\n\t\t\t\tx: currentVel.x + forwardX * thrustAmount,\n\t\t\t\ty: currentVel.y + forwardY * thrustAmount,\n\t\t\t\tz: currentVel.z\n\t\t\t}, true);\n\t\t}\n\n\t\t// --- Angular thrust (Z-axis for 2D) ---\n\t\tif (input.rotate !== 0) {\n\t\t\tbody.setAngvel({ x: 0, y: 0, z: -thruster.angularThrust * input.rotate }, true);\n\t\t} else {\n\t\t\t// Stop rotation when no input\n\t\t\tconst angVel = body.angvel();\n\t\t\tbody.setAngvel({ x: angVel.x, y: angVel.y, z: 0 }, true);\n\t\t}\n\t}\n\n\tupdate(dt: number): void {\n\t\tif (!this.world?.collisionMap) return;\n\t\tfor (const [, entity] of this.world.collisionMap) {\n\t\t\tthis.updateEntity(entity, dt);\n\t\t}\n\t}\n}\n","/**\n * Thruster Behavior Descriptor\n * \n * Type-safe descriptor for the thruster behavior system using the new entity.use() API.\n * This wraps the existing ThrusterSystem and components.\n */\n\nimport { defineBehavior } from '../behavior-descriptor';\nimport type { BehaviorEntityLink, BehaviorSystem } from '../behavior-system';\nimport { ThrusterSystem, ThrusterEntity } from './thruster.system';\nimport { ThrusterFSM } from './thruster-fsm';\nimport {\n\tcreateThrusterInputComponent,\n\tcreateThrusterMovementComponent,\n\ttype ThrusterMovementComponent,\n\ttype ThrusterInputComponent,\n} from './components';\nimport type { WasmStageRuntime } from '../../runtime/wasm-stage-runtime';\n\n/**\n * Thruster behavior options (typed for entity.use() autocomplete)\n */\nexport interface ThrusterBehaviorOptions {\n\t/** Forward thrust force (default: 10) */\n\tlinearThrust: number;\n\t/** Rotation torque (default: 5) */\n\tangularThrust: number;\n\t/** Optional linear damping override */\n\tlinearDamping?: number;\n}\n\nconst defaultOptions: ThrusterBehaviorOptions = {\n\tlinearThrust: 10,\n\tangularThrust: 5,\n\tlinearDamping: undefined,\n};\n\nconst THRUSTER_BEHAVIOR_KEY = Symbol.for('zylem:behavior:thruster');\n\n/**\n * Adapter that wraps ThrusterSystem as a BehaviorSystem.\n * \n * This bridges the entity.use() pattern with the ECS component-based approach:\n * - Reads options from entity behaviorRefs\n * - Initializes ThrusterMovementComponent and ThrusterInputComponent on entities\n * - Creates FSM lazily and attaches to BehaviorRef for access via handle.getFSM()\n * - Delegates physics to ThrusterSystem\n * \n * NOTE: Input is controlled by the user via entity.onUpdate() - set entity.input.thrust/rotate\n */\nclass ThrusterBehaviorSystem implements BehaviorSystem {\n\tprivate movementBehavior: ThrusterSystem;\n\tprivate attachedRuntimeSlots = new Set<number>();\n\n\tconstructor(\n\t\tworld: import('../../collision/world').ZylemWorld | null,\n\t\tprivate wasmStage: WasmStageRuntime | null,\n\t\tprivate getBehaviorLinks?: (key: symbol) => Iterable<BehaviorEntityLink>,\n\t) {\n\t\tthis.movementBehavior = new ThrusterSystem(world);\n\t}\n\n\tupdate(_ecs: unknown, delta: number): void {\n\t\tconst links = this.getBehaviorLinks?.(THRUSTER_BEHAVIOR_KEY);\n\t\tif (!links) return;\n\n\t\tfor (const link of links) {\n\t\t\tconst gameEntity = link.entity as any;\n\t\t\tconst thrusterRef = link.ref as any;\n\n\t\t\tconst options = thrusterRef.options as ThrusterBehaviorOptions;\n\n\t\t\tif (!gameEntity.thruster) {\n\t\t\t\tgameEntity.thruster = createThrusterMovementComponent(\n\t\t\t\t\toptions.linearThrust,\n\t\t\t\t\toptions.angularThrust,\n\t\t\t\t\t{ linearDamping: options.linearDamping },\n\t\t\t\t) as ThrusterMovementComponent;\n\t\t\t}\n\n\t\t\tif (!gameEntity.$thruster) {\n\t\t\t\tgameEntity.$thruster = createThrusterInputComponent() as ThrusterInputComponent;\n\t\t\t}\n\n\t\t\tif (!thrusterRef.fsm && gameEntity.$thruster) {\n\t\t\t\tthrusterRef.fsm = new ThrusterFSM({ input: gameEntity.$thruster });\n\t\t\t}\n\n\t\t\tif (thrusterRef.fsm && gameEntity.$thruster) {\n\t\t\t\tthrusterRef.fsm.update({\n\t\t\t\t\tthrust: gameEntity.$thruster.thrust,\n\t\t\t\t\trotate: gameEntity.$thruster.rotate,\n\t\t\t\t\tthrustX: gameEntity.$thruster.thrustX,\n\t\t\t\t\tthrustY: gameEntity.$thruster.thrustY,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst handle = (gameEntity.runtimeHandle ?? -1) as number;\n\t\t\tif (this.wasmStage && handle >= 0) {\n\t\t\t\tthis.ensureWasmAttached(handle, options);\n\t\t\t\tconst input = gameEntity.$thruster as ThrusterInputComponent | undefined;\n\t\t\t\tif (input) {\n\t\t\t\t\tthis.wasmStage.setThrusterInput(handle, input.thrust, input.rotate);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (!gameEntity.body) continue;\n\t\t\tif (!gameEntity.physics) {\n\t\t\t\tgameEntity.physics = { body: gameEntity.body };\n\t\t\t}\n\t\t\tthis.movementBehavior.updateEntity(gameEntity, delta);\n\t\t}\n\t}\n\n\tprivate ensureWasmAttached(handle: number, options: ThrusterBehaviorOptions): void {\n\t\tif (!this.wasmStage || this.attachedRuntimeSlots.has(handle)) return;\n\t\tthis.wasmStage.attachThruster(handle, {\n\t\t\tmaxSpeed: 50,\n\t\t\tacceleration: options.linearThrust,\n\t\t\tturnRateRadPerSec: options.angularThrust,\n\t\t\tlinearDamping: options.linearDamping ?? 0,\n\t\t});\n\t\tthis.attachedRuntimeSlots.add(handle);\n\t}\n\n\tdestroy(_ecs: unknown): void {\n\t\tthis.attachedRuntimeSlots.clear();\n\t}\n}\n\n/**\n * ThrusterBehavior - typed descriptor for thruster movement.\n * \n * Uses the existing ThrusterSystem under the hood.\n * \n * @example\n * ```typescript\n * import { ThrusterBehavior } from \"@zylem/game-lib/behavior\";\n * \n * const ship = createSprite({ ... });\n * ship.use(ThrusterBehavior, { linearThrust: 15, angularThrust: 8 });\n * ```\n */\nexport const ThrusterBehavior = defineBehavior<ThrusterBehaviorOptions, Record<string, never>, ThrusterEntity>({\n\tname: 'thruster',\n\tdefaultOptions,\n\tsystemFactory: (ctx) =>\n\t\tnew ThrusterBehaviorSystem(ctx.world, ctx.wasmStage ?? null, ctx.getBehaviorLinks),\n});\n"],"mappings":"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","names":["ThrusterState","ThrusterEvent"]}
@@ -83,3 +83,4 @@ export {
83
83
  clampPointToBounds3D,
84
84
  getBounds3DNormalFromHits
85
85
  };
86
+ //# sourceMappingURL=chunk-OHCPV2NT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/behaviors/shared/bounds-3d.ts"],"sourcesContent":["export interface Bounds3DPoint {\n\tx: number;\n\ty: number;\n\tz: number;\n}\n\nexport interface Bounds3DBox {\n\tminX: number;\n\tmaxX: number;\n\tminY: number;\n\tmaxY: number;\n\tminZ: number;\n\tmaxZ: number;\n}\n\nexport interface Bounds3DSize {\n\twidth: number;\n\theight: number;\n\tdepth: number;\n\tcenterX?: number;\n\tcenterY?: number;\n\tcenterZ?: number;\n}\n\nexport interface Bounds3DHits {\n\tleft: boolean;\n\tright: boolean;\n\ttop: boolean;\n\tbottom: boolean;\n\tback: boolean;\n\tfront: boolean;\n}\n\nexport interface Bounds3DPadding {\n\tx: number;\n\ty: number;\n\tz: number;\n}\n\nexport type Bounds3DPaddingInput =\n\t| number\n\t| Partial<Bounds3DPadding>\n\t| undefined;\n\nexport function createBounds3DBox({\n\twidth,\n\theight,\n\tdepth,\n\tcenterX = 0,\n\tcenterY = 0,\n\tcenterZ = 0,\n}: Bounds3DSize): Bounds3DBox {\n\tconst halfWidth = width / 2;\n\tconst halfHeight = height / 2;\n\tconst halfDepth = depth / 2;\n\treturn {\n\t\tminX: centerX - halfWidth,\n\t\tmaxX: centerX + halfWidth,\n\t\tminY: centerY - halfHeight,\n\t\tmaxY: centerY + halfHeight,\n\t\tminZ: centerZ - halfDepth,\n\t\tmaxZ: centerZ + halfDepth,\n\t};\n}\n\nexport function normalizeBounds3DPadding(\n\tpadding?: Bounds3DPaddingInput,\n): Bounds3DPadding {\n\tif (typeof padding === 'number') {\n\t\treturn { x: padding, y: padding, z: padding };\n\t}\n\n\treturn {\n\t\tx: padding?.x ?? 0,\n\t\ty: padding?.y ?? 0,\n\t\tz: padding?.z ?? 0,\n\t};\n}\n\nexport function insetBounds3D(\n\tbounds: Bounds3DBox,\n\tpadding?: Bounds3DPaddingInput,\n): Bounds3DBox {\n\tconst resolved = normalizeBounds3DPadding(padding);\n\treturn {\n\t\tminX: bounds.minX + resolved.x,\n\t\tmaxX: bounds.maxX - resolved.x,\n\t\tminY: bounds.minY + resolved.y,\n\t\tmaxY: bounds.maxY - resolved.y,\n\t\tminZ: bounds.minZ + resolved.z,\n\t\tmaxZ: bounds.maxZ - resolved.z,\n\t};\n}\n\nexport function computeBounds3DHits(\n\tposition: Bounds3DPoint,\n\tbounds: Bounds3DBox,\n): Bounds3DHits {\n\tconst hits: Bounds3DHits = {\n\t\tleft: false,\n\t\tright: false,\n\t\ttop: false,\n\t\tbottom: false,\n\t\tback: false,\n\t\tfront: false,\n\t};\n\n\tif (position.x <= bounds.minX) hits.left = true;\n\telse if (position.x >= bounds.maxX) hits.right = true;\n\n\tif (position.y <= bounds.minY) hits.bottom = true;\n\telse if (position.y >= bounds.maxY) hits.top = true;\n\n\tif (position.z <= bounds.minZ) hits.back = true;\n\telse if (position.z >= bounds.maxZ) hits.front = true;\n\n\treturn hits;\n}\n\nexport function hasAnyBounds3DHit(hits: Bounds3DHits): boolean {\n\treturn hits.left || hits.right || hits.top || hits.bottom || hits.back || hits.front;\n}\n\nexport function constrainMovementToBounds3D(\n\thits: Bounds3DHits,\n\tmoveX: number,\n\tmoveY: number,\n\tmoveZ: number,\n): { moveX: number; moveY: number; moveZ: number } {\n\tlet adjustedX = moveX;\n\tlet adjustedY = moveY;\n\tlet adjustedZ = moveZ;\n\n\tif ((hits.left && moveX < 0) || (hits.right && moveX > 0)) {\n\t\tadjustedX = 0;\n\t}\n\n\tif ((hits.bottom && moveY < 0) || (hits.top && moveY > 0)) {\n\t\tadjustedY = 0;\n\t}\n\n\tif ((hits.back && moveZ < 0) || (hits.front && moveZ > 0)) {\n\t\tadjustedZ = 0;\n\t}\n\n\treturn { moveX: adjustedX, moveY: adjustedY, moveZ: adjustedZ };\n}\n\nexport function clampPointToBounds3D(\n\tposition: Bounds3DPoint,\n\tbounds: Bounds3DBox,\n): Bounds3DPoint {\n\treturn {\n\t\tx: Math.min(bounds.maxX, Math.max(bounds.minX, position.x)),\n\t\ty: Math.min(bounds.maxY, Math.max(bounds.minY, position.y)),\n\t\tz: Math.min(bounds.maxZ, Math.max(bounds.minZ, position.z)),\n\t};\n}\n\nexport function getBounds3DNormalFromHits(\n\thits: Bounds3DHits,\n): { x: number; y: number; z: number } {\n\tlet x = 0;\n\tlet y = 0;\n\tlet z = 0;\n\n\tif (hits.left) x = 1;\n\tif (hits.right) x = -1;\n\tif (hits.bottom) y = 1;\n\tif (hits.top) y = -1;\n\tif (hits.back) z = 1;\n\tif (hits.front) z = -1;\n\n\treturn { x, y, z };\n}\n"],"mappings":";AAiEO,SAAS,yBACf,SACkB;AAClB,MAAI,OAAO,YAAY,UAAU;AAChC,WAAO,EAAE,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ;AAAA,EAC7C;AAEA,SAAO;AAAA,IACN,GAAG,SAAS,KAAK;AAAA,IACjB,GAAG,SAAS,KAAK;AAAA,IACjB,GAAG,SAAS,KAAK;AAAA,EAClB;AACD;AAEO,SAAS,cACf,QACA,SACc;AACd,QAAM,WAAW,yBAAyB,OAAO;AACjD,SAAO;AAAA,IACN,MAAM,OAAO,OAAO,SAAS;AAAA,IAC7B,MAAM,OAAO,OAAO,SAAS;AAAA,IAC7B,MAAM,OAAO,OAAO,SAAS;AAAA,IAC7B,MAAM,OAAO,OAAO,SAAS;AAAA,IAC7B,MAAM,OAAO,OAAO,SAAS;AAAA,IAC7B,MAAM,OAAO,OAAO,SAAS;AAAA,EAC9B;AACD;AAEO,SAAS,oBACf,UACA,QACe;AACf,QAAM,OAAqB;AAAA,IAC1B,MAAM;AAAA,IACN,OAAO;AAAA,IACP,KAAK;AAAA,IACL,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,EACR;AAEA,MAAI,SAAS,KAAK,OAAO,KAAM,MAAK,OAAO;AAAA,WAClC,SAAS,KAAK,OAAO,KAAM,MAAK,QAAQ;AAEjD,MAAI,SAAS,KAAK,OAAO,KAAM,MAAK,SAAS;AAAA,WACpC,SAAS,KAAK,OAAO,KAAM,MAAK,MAAM;AAE/C,MAAI,SAAS,KAAK,OAAO,KAAM,MAAK,OAAO;AAAA,WAClC,SAAS,KAAK,OAAO,KAAM,MAAK,QAAQ;AAEjD,SAAO;AACR;AAEO,SAAS,kBAAkB,MAA6B;AAC9D,SAAO,KAAK,QAAQ,KAAK,SAAS,KAAK,OAAO,KAAK,UAAU,KAAK,QAAQ,KAAK;AAChF;AAEO,SAAS,4BACf,MACA,OACA,OACA,OACkD;AAClD,MAAI,YAAY;AAChB,MAAI,YAAY;AAChB,MAAI,YAAY;AAEhB,MAAK,KAAK,QAAQ,QAAQ,KAAO,KAAK,SAAS,QAAQ,GAAI;AAC1D,gBAAY;AAAA,EACb;AAEA,MAAK,KAAK,UAAU,QAAQ,KAAO,KAAK,OAAO,QAAQ,GAAI;AAC1D,gBAAY;AAAA,EACb;AAEA,MAAK,KAAK,QAAQ,QAAQ,KAAO,KAAK,SAAS,QAAQ,GAAI;AAC1D,gBAAY;AAAA,EACb;AAEA,SAAO,EAAE,OAAO,WAAW,OAAO,WAAW,OAAO,UAAU;AAC/D;AAEO,SAAS,qBACf,UACA,QACgB;AAChB,SAAO;AAAA,IACN,GAAG,KAAK,IAAI,OAAO,MAAM,KAAK,IAAI,OAAO,MAAM,SAAS,CAAC,CAAC;AAAA,IAC1D,GAAG,KAAK,IAAI,OAAO,MAAM,KAAK,IAAI,OAAO,MAAM,SAAS,CAAC,CAAC;AAAA,IAC1D,GAAG,KAAK,IAAI,OAAO,MAAM,KAAK,IAAI,OAAO,MAAM,SAAS,CAAC,CAAC;AAAA,EAC3D;AACD;AAEO,SAAS,0BACf,MACsC;AACtC,MAAI,IAAI;AACR,MAAI,IAAI;AACR,MAAI,IAAI;AAER,MAAI,KAAK,KAAM,KAAI;AACnB,MAAI,KAAK,MAAO,KAAI;AACpB,MAAI,KAAK,OAAQ,KAAI;AACrB,MAAI,KAAK,IAAK,KAAI;AAClB,MAAI,KAAK,KAAM,KAAI;AACnB,MAAI,KAAK,MAAO,KAAI;AAEpB,SAAO,EAAE,GAAG,GAAG,EAAE;AAClB;","names":[]}
@@ -1,4 +1,4 @@
1
- // src/lib/actions/capabilities/velocity-intents.ts
1
+ // src/lib/capabilities/velocity-intents.ts
2
2
  function setVelocityIntent(store, sourceId, vector, options = {}) {
3
3
  const prev = store.velocityChannels[sourceId];
4
4
  const mode = options.mode ?? prev?.mode ?? "replace";
@@ -26,3 +26,4 @@ export {
26
26
  setVelocityIntent,
27
27
  clearVelocityIntent
28
28
  };
29
+ //# sourceMappingURL=chunk-OTLSUYQQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/capabilities/velocity-intents.ts"],"sourcesContent":["import { TransformState, VelocityIntentMode } from './transform-store';\n\nexport interface VelocityIntentOptions {\n\tmode?: VelocityIntentMode;\n\tpriority?: number;\n}\n\nexport interface VelocityIntentVector {\n\tx?: number;\n\ty?: number;\n\tz?: number;\n}\n\nexport function setVelocityIntent(\n\tstore: TransformState,\n\tsourceId: string,\n\tvector: VelocityIntentVector,\n\toptions: VelocityIntentOptions = {},\n): void {\n\tconst prev = store.velocityChannels[sourceId];\n\tconst mode = options.mode ?? prev?.mode ?? 'replace';\n\tconst priority = options.priority ?? prev?.priority ?? 0;\n\tconst isAdditiveMerge = mode === 'add' && prev?.mode === 'add';\n\n\tconst x =\n\t\tvector.x == null\n\t\t\t? prev?.x\n\t\t\t: isAdditiveMerge\n\t\t\t\t? (prev?.x ?? 0) + vector.x\n\t\t\t\t: vector.x;\n\tconst y =\n\t\tvector.y == null\n\t\t\t? prev?.y\n\t\t\t: isAdditiveMerge\n\t\t\t\t? (prev?.y ?? 0) + vector.y\n\t\t\t\t: vector.y;\n\tconst z =\n\t\tvector.z == null\n\t\t\t? prev?.z\n\t\t\t: isAdditiveMerge\n\t\t\t\t? (prev?.z ?? 0) + vector.z\n\t\t\t\t: vector.z;\n\n\tstore.velocityChannels[sourceId] = {\n\t\tx,\n\t\ty,\n\t\tz,\n\t\tmode,\n\t\tpriority,\n\t};\n\tstore.dirty.velocityChannels = true;\n}\n\nexport function clearVelocityIntent(store: TransformState, sourceId: string): void {\n\tif (!(sourceId in store.velocityChannels)) return;\n\tdelete store.velocityChannels[sourceId];\n\tstore.dirty.velocityChannels = true;\n}\n"],"mappings":";AAaO,SAAS,kBACf,OACA,UACA,QACA,UAAiC,CAAC,GAC3B;AACP,QAAM,OAAO,MAAM,iBAAiB,QAAQ;AAC5C,QAAM,OAAO,QAAQ,QAAQ,MAAM,QAAQ;AAC3C,QAAM,WAAW,QAAQ,YAAY,MAAM,YAAY;AACvD,QAAM,kBAAkB,SAAS,SAAS,MAAM,SAAS;AAEzD,QAAM,IACL,OAAO,KAAK,OACT,MAAM,IACN,mBACE,MAAM,KAAK,KAAK,OAAO,IACxB,OAAO;AACZ,QAAM,IACL,OAAO,KAAK,OACT,MAAM,IACN,mBACE,MAAM,KAAK,KAAK,OAAO,IACxB,OAAO;AACZ,QAAM,IACL,OAAO,KAAK,OACT,MAAM,IACN,mBACE,MAAM,KAAK,KAAK,OAAO,IACxB,OAAO;AAEZ,QAAM,iBAAiB,QAAQ,IAAI;AAAA,IAClC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD;AACA,QAAM,MAAM,mBAAmB;AAChC;AAEO,SAAS,oBAAoB,OAAuB,UAAwB;AAClF,MAAI,EAAE,YAAY,MAAM,kBAAmB;AAC3C,SAAO,MAAM,iBAAiB,QAAQ;AACtC,QAAM,MAAM,mBAAmB;AAChC;","names":[]}
@@ -2,17 +2,17 @@ import {
2
2
  computeBounds2DHits,
3
3
  constrainMovementToBounds2D,
4
4
  hasAnyBounds2DHit
5
- } from "./chunk-ZHBD65AZ.js";
5
+ } from "./chunk-KDY5N6J4.js";
6
6
  import {
7
7
  StageBoundaryDim
8
- } from "./chunk-YYJP62QD.js";
8
+ } from "./chunk-4V7TIRO2.js";
9
9
  import {
10
10
  SyncStateMachine,
11
11
  t
12
- } from "./chunk-RER2IDPW.js";
12
+ } from "./chunk-4URF65BR.js";
13
13
  import {
14
14
  defineBehavior
15
- } from "./chunk-YDLO5M7S.js";
15
+ } from "./chunk-EZNCCO4L.js";
16
16
 
17
17
  // src/lib/behaviors/world-boundary-2d/world-boundary-2d-fsm.ts
18
18
  var WorldBoundary2DState = /* @__PURE__ */ ((WorldBoundary2DState2) => {
@@ -130,12 +130,10 @@ function createWorldBoundary2DHandle(ref) {
130
130
  };
131
131
  }
132
132
  var WorldBoundary2DSystem = class {
133
- constructor(world, wasmStage, getBehaviorLinks) {
134
- this.world = world;
133
+ constructor(wasmStage, getBehaviorLinks) {
135
134
  this.wasmStage = wasmStage;
136
135
  this.getBehaviorLinks = getBehaviorLinks;
137
136
  }
138
- world;
139
137
  wasmStage;
140
138
  getBehaviorLinks;
141
139
  attachedRuntimeSlots = /* @__PURE__ */ new Set();
@@ -186,7 +184,7 @@ var WorldBoundary2DSystem = class {
186
184
  var WorldBoundary2DBehavior = defineBehavior({
187
185
  name: "world-boundary-2d",
188
186
  defaultOptions,
189
- systemFactory: (ctx) => new WorldBoundary2DSystem(ctx.world, ctx.wasmStage ?? null, ctx.getBehaviorLinks),
187
+ systemFactory: (ctx) => new WorldBoundary2DSystem(ctx.wasmStage ?? null, ctx.getBehaviorLinks),
190
188
  createHandle: createWorldBoundary2DHandle
191
189
  });
192
190
 
@@ -198,3 +196,4 @@ export {
198
196
  WorldBoundary2DFSM,
199
197
  WorldBoundary2DBehavior
200
198
  };
199
+ //# sourceMappingURL=chunk-PWF757B6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/behaviors/world-boundary-2d/world-boundary-2d-fsm.ts","../src/lib/behaviors/world-boundary-2d/world-boundary-2d.descriptor.ts"],"sourcesContent":["/**\n * WorldBoundary2DFSM\n *\n * Minimal FSM + extended state to track which world boundaries were hit.\n *\n * Notes:\n * - \"Hit boundaries\" is inherently a *set* (can hit left+bottom in one frame),\n * so we store it as extended state (`lastHits`) rather than a single FSM state.\n * - The FSM state is still useful for coarse status like \"inside\" vs \"touching\".\n */\n\nimport { SyncStateMachine, t } from '../../core/utility/sync-state-machine';\nimport {\n\tcomputeBounds2DHits,\n\tconstrainMovementToBounds2D,\n\thasAnyBounds2DHit,\n} from '../shared/bounds-2d';\n\nexport type WorldBoundary2DHit = 'top' | 'bottom' | 'left' | 'right';\nexport type WorldBoundary2DHits = Record<WorldBoundary2DHit, boolean>;\n\nexport interface WorldBoundary2DPosition {\n\tx: number;\n\ty: number;\n}\n\nexport interface WorldBoundary2DBounds {\n\ttop: number;\n\tbottom: number;\n\tleft: number;\n\tright: number;\n}\n\nexport enum WorldBoundary2DState {\n\tInside = 'inside',\n\tTouching = 'touching',\n}\n\nexport enum WorldBoundary2DEvent {\n\tEnterInside = 'enter-inside',\n\tTouchBoundary = 'touch-boundary',\n}\n\n/**\n * Compute which boundaries are being hit for a position and bounds.\n * This matches the semantics of the legacy `boundary2d` behavior:\n * - left hit if x <= left\n * - right hit if x >= right\n * - bottom hit if y <= bottom\n * - top hit if y >= top\n */\nexport function computeWorldBoundary2DHits(\n\tposition: WorldBoundary2DPosition,\n\tbounds: WorldBoundary2DBounds\n): WorldBoundary2DHits {\n\treturn computeBounds2DHits(position, {\n\t\tminX: bounds.left,\n\t\tmaxX: bounds.right,\n\t\tminY: bounds.bottom,\n\t\tmaxY: bounds.top,\n\t});\n}\n\nexport function hasAnyWorldBoundary2DHit(hits: WorldBoundary2DHits): boolean {\n\treturn hasAnyBounds2DHit(hits);\n}\n\n/**\n * FSM wrapper with \"extended state\" (lastHits / lastPosition).\n * Systems should call `update(...)` once per frame.\n */\nexport class WorldBoundary2DFSM {\n\tpublic readonly machine: SyncStateMachine<WorldBoundary2DState, WorldBoundary2DEvent, never>;\n\n\tprivate lastHits: WorldBoundary2DHits = { top: false, bottom: false, left: false, right: false };\n\tprivate lastPosition: WorldBoundary2DPosition | null = null;\n\tprivate lastUpdatedAtMs: number | null = null;\n\n\tconstructor() {\n\t\tthis.machine = new SyncStateMachine<WorldBoundary2DState, WorldBoundary2DEvent, never>(\n\t\t\tWorldBoundary2DState.Inside,\n\t\t\t[\n\t\t\t\tt(WorldBoundary2DState.Inside, WorldBoundary2DEvent.TouchBoundary, WorldBoundary2DState.Touching),\n\t\t\t\tt(WorldBoundary2DState.Touching, WorldBoundary2DEvent.EnterInside, WorldBoundary2DState.Inside),\n\n\t\t\t\t// Self transitions (no-ops)\n\t\t\t\tt(WorldBoundary2DState.Inside, WorldBoundary2DEvent.EnterInside, WorldBoundary2DState.Inside),\n\t\t\t\tt(WorldBoundary2DState.Touching, WorldBoundary2DEvent.TouchBoundary, WorldBoundary2DState.Touching),\n\t\t\t]\n\t\t);\n\t}\n\n\tgetState(): WorldBoundary2DState {\n\t\treturn this.machine.getState();\n\t}\n\n\t/**\n\t * Returns the last computed hits (always available after first update call).\n\t */\n\tgetLastHits(): WorldBoundary2DHits {\n\t\treturn this.lastHits;\n\t}\n\n\t/**\n\t * Returns adjusted movement values based on boundary hits.\n\t * If the entity is touching a boundary and trying to move further into it,\n\t * that axis component is zeroed out.\n\t *\n\t * @param moveX - The desired X movement\n\t * @param moveY - The desired Y movement\n\t * @returns Adjusted { moveX, moveY } with boundary-blocked axes zeroed\n\t */\n\tgetMovement(moveX: number, moveY: number): { moveX: number; moveY: number } {\n\t\treturn constrainMovementToBounds2D(this.lastHits, moveX, moveY);\n\t}\n\n\t/**\n\t * Returns the last position passed to `update`, if any.\n\t */\n\tgetLastPosition(): WorldBoundary2DPosition | null {\n\t\treturn this.lastPosition;\n\t}\n\n\t/**\n\t * Best-effort timestamp (ms) of the last `update(...)` call.\n\t * This is optional metadata; systems can ignore it.\n\t */\n\tgetLastUpdatedAtMs(): number | null {\n\t\treturn this.lastUpdatedAtMs;\n\t}\n\n\t/**\n\t * Update FSM + extended state based on current position and bounds.\n\t * Returns the computed hits for convenience.\n\t */\n\tupdate(position: WorldBoundary2DPosition, bounds: WorldBoundary2DBounds): WorldBoundary2DHits {\n\t\tconst hits = computeWorldBoundary2DHits(position, bounds);\n\n\t\tthis.lastHits = hits;\n\t\tthis.lastPosition = { x: position.x, y: position.y };\n\t\tthis.lastUpdatedAtMs = Date.now();\n\n\t\tif (hasAnyWorldBoundary2DHit(hits)) {\n\t\t\tthis.dispatch(WorldBoundary2DEvent.TouchBoundary);\n\t\t} else {\n\t\t\tthis.dispatch(WorldBoundary2DEvent.EnterInside);\n\t\t}\n\n\t\treturn hits;\n\t}\n\n\tprivate dispatch(event: WorldBoundary2DEvent): void {\n\t\tif (this.machine.can(event)) {\n\t\t\tthis.machine.syncDispatch(event);\n\t\t}\n\t}\n}\n","/**\n * WorldBoundary2DBehavior\n *\n * Tracks which 2D world boundaries an entity is touching.\n * Use `getMovement()` on the behavior handle to adjust movement based on hits.\n *\n * Source of truth: `entity.body` (Rapier rigid body), consistent with other new behaviors.\n */\n\nimport { defineBehavior, type BehaviorRef } from '../behavior-descriptor';\nimport type { BehaviorEntityLink, BehaviorSystem } from '../behavior-system';\nimport {\n\tWorldBoundary2DFSM,\n\ttype WorldBoundary2DBounds,\n\ttype WorldBoundary2DHits,\n} from './world-boundary-2d-fsm';\nimport {\n\tStageBoundaryDim,\n\ttype WasmStageRuntime,\n} from '../../runtime/wasm-stage-runtime';\n\nexport interface WorldBoundary2DOptions {\n\t/**\n\t * World boundaries (in world units).\n\t * - left hit if x <= left\n\t * - right hit if x >= right\n\t * - bottom hit if y <= bottom\n\t * - top hit if y >= top\n\t */\n\tboundaries: WorldBoundary2DBounds;\n}\n\n/**\n * Handle methods provided by WorldBoundary2DBehavior\n */\nexport interface WorldBoundary2DHandle {\n\t/**\n\t * Get the last computed boundary hits.\n\t * Returns null until entity is spawned and FSM is initialized.\n\t */\n\tgetLastHits(): WorldBoundary2DHits | null;\n\n\t/**\n\t * Get adjusted movement values based on boundary hits.\n\t * Zeros out movement into boundaries the entity is touching.\n\t */\n\tgetMovement(moveX: number, moveY: number): { moveX: number; moveY: number };\n}\n\nconst defaultOptions: WorldBoundary2DOptions = {\n\tboundaries: { top: 0, bottom: 0, left: 0, right: 0 },\n};\n\nconst WORLD_BOUNDARY_BEHAVIOR_KEY = Symbol.for(\n\t'zylem:behavior:world-boundary-2d',\n);\n\n/**\n * Creates behavior-specific handle methods for WorldBoundary2DBehavior.\n */\nfunction createWorldBoundary2DHandle(\n\tref: BehaviorRef<WorldBoundary2DOptions>\n): WorldBoundary2DHandle {\n\treturn {\n\t\tgetLastHits: () => {\n\t\t\tconst fsm = ref.fsm as WorldBoundary2DFSM | undefined;\n\t\t\treturn fsm?.getLastHits() ?? null;\n\t\t},\n\t\tgetMovement: (moveX: number, moveY: number) => {\n\t\t\tconst fsm = ref.fsm as WorldBoundary2DFSM | undefined;\n\t\t\treturn fsm?.getMovement(moveX, moveY) ?? { moveX, moveY };\n\t\t},\n\t};\n}\n\n/**\n * WorldBoundary2DSystem\n *\n * Stage-level system that:\n * - finds entities with this behavior attached\n * - computes and tracks boundary hits using the FSM\n *\n * When a {@link WasmStageRuntime} is present in `BehaviorSystemContext` and the\n * entity has been registered with the unified Stage (`runtimeHandle >= 0`), the\n * system uses the Rust `world_boundary` behavior as the source of truth and\n * mirrors its query into the local FSM so handle methods (`getLastHits`,\n * `getMovement`) return the same shape as the legacy code path.\n */\nclass WorldBoundary2DSystem implements BehaviorSystem {\n\tprivate attachedRuntimeSlots = new Set<number>();\n\n\tconstructor(\n\t\tprivate wasmStage: WasmStageRuntime | null,\n\t\tprivate getBehaviorLinks?: (key: symbol) => Iterable<BehaviorEntityLink>,\n\t) {}\n\n\tupdate(_ecs: unknown, _delta: number): void {\n\t\tconst links = this.getBehaviorLinks?.(WORLD_BOUNDARY_BEHAVIOR_KEY);\n\t\tif (!links) return;\n\n\t\tfor (const link of links) {\n\t\t\tconst gameEntity = link.entity as any;\n\t\t\tconst boundaryRef = link.ref as any;\n\t\t\tconst options = boundaryRef.options as WorldBoundary2DOptions;\n\n\t\t\tif (!boundaryRef.fsm) {\n\t\t\t\tboundaryRef.fsm = new WorldBoundary2DFSM();\n\t\t\t}\n\n\t\t\tconst handle = (gameEntity.runtimeHandle ?? -1) as number;\n\t\t\tif (this.wasmStage && handle >= 0) {\n\t\t\t\tthis.ensureWasmAttached(handle, options.boundaries);\n\t\t\t\tconst query = this.wasmStage.queryWorldBoundary(handle);\n\t\t\t\tif (query) {\n\t\t\t\t\tboundaryRef.fsm.update(\n\t\t\t\t\t\t{ x: query.clamped[0], y: query.clamped[1] },\n\t\t\t\t\t\toptions.boundaries,\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!gameEntity.body) continue;\n\t\t\tconst body = gameEntity.body;\n\t\t\tconst pos = body.translation();\n\t\t\tboundaryRef.fsm.update({ x: pos.x, y: pos.y }, options.boundaries);\n\t\t}\n\t}\n\n\tprivate ensureWasmAttached(handle: number, bounds: WorldBoundary2DBounds): void {\n\t\tif (!this.wasmStage || this.attachedRuntimeSlots.has(handle)) return;\n\t\tthis.wasmStage.attachWorldBoundary(handle, StageBoundaryDim.Two, {\n\t\t\ttop: bounds.top,\n\t\t\tbottom: bounds.bottom,\n\t\t\tleft: bounds.left,\n\t\t\tright: bounds.right,\n\t\t\tfront: 0,\n\t\t\tback: 0,\n\t\t});\n\t\tthis.attachedRuntimeSlots.add(handle);\n\t}\n\n\tdestroy(_ecs: unknown): void {\n\t\tthis.attachedRuntimeSlots.clear();\n\t}\n}\n\n/**\n * WorldBoundary2DBehavior\n *\n * @example\n * ```ts\n * import { WorldBoundary2DBehavior } from \"@zylem/game-lib/behavior\";\n *\n * const ship = createSprite({ ... });\n * const boundary = ship.use(WorldBoundary2DBehavior, {\n * boundaries: { left: -10, right: 10, bottom: -7.5, top: 7.5 },\n * });\n *\n * ship.onUpdate(({ me }) => {\n * let moveX = ..., moveY = ...;\n * const hits = boundary.getLastHits(); // Fully typed!\n * ({ moveX, moveY } = boundary.getMovement(moveX, moveY));\n * me.moveXY(moveX, moveY);\n * });\n * ```\n */\nexport const WorldBoundary2DBehavior = defineBehavior({\n\tname: 'world-boundary-2d',\n\tdefaultOptions,\n\tsystemFactory: (ctx) =>\n\t\tnew WorldBoundary2DSystem(ctx.wasmStage ?? null, ctx.getBehaviorLinks),\n\tcreateHandle: createWorldBoundary2DHandle,\n});\n"],"mappings":"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","names":["WorldBoundary2DState","WorldBoundary2DEvent"]}
@@ -29,3 +29,4 @@ export {
29
29
  createFirstPersonInputComponent,
30
30
  createFirstPersonStateComponent
31
31
  };
32
+ //# sourceMappingURL=chunk-QVCMJ4Q2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/behaviors/first-person/components.ts"],"sourcesContent":["/**\n * First Person Controller ECS Components\n *\n * Components for first-person movement and camera control:\n * - Movement configuration (walk/run speed, sensitivity, eye height)\n * - Input intent (WASD movement + mouse look deltas)\n * - Runtime state (yaw, pitch, speed)\n */\n\n// ─────────────────────────────────────────────────────────────────────────────\n// FirstPersonMovementComponent (capability / configuration)\n// Defines movement capabilities and look parameters\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface FirstPersonMovementComponent {\n\t/** Base walking speed (units/sec) */\n\twalkSpeed: number;\n\t/** Sprint/run speed (units/sec) */\n\trunSpeed: number;\n\t/** Look sensitivity multiplier applied to raw input deltas */\n\tlookSensitivity: number;\n\t/** Vertical offset above entity position for eye/camera placement */\n\teyeHeight: number;\n}\n\nexport function createFirstPersonMovementComponent(\n\toptions: Partial<FirstPersonMovementComponent> = {},\n): FirstPersonMovementComponent {\n\treturn {\n\t\twalkSpeed: options.walkSpeed ?? 8,\n\t\trunSpeed: options.runSpeed ?? 16,\n\t\tlookSensitivity: options.lookSensitivity ?? 2,\n\t\teyeHeight: options.eyeHeight ?? 1.7,\n\t};\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// FirstPersonInputComponent (intent)\n// Written by: Player controller (game.onUpdate / entity.onUpdate)\n// Read by: FirstPersonSystem\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface FirstPersonInputComponent {\n\t/** Horizontal movement input (-1 to 1, strafe) */\n\tmoveX: number;\n\t/** Forward/backward movement input (-1 to 1) */\n\tmoveZ: number;\n\t/** Horizontal look delta (mouse/stick, raw value before sensitivity) */\n\tlookX: number;\n\t/** Vertical look delta (mouse/stick, raw value before sensitivity) */\n\tlookY: number;\n\t/** Sprint button held */\n\tsprint: boolean;\n}\n\nexport function createFirstPersonInputComponent(): FirstPersonInputComponent {\n\treturn {\n\t\tmoveX: 0,\n\t\tmoveZ: 0,\n\t\tlookX: 0,\n\t\tlookY: 0,\n\t\tsprint: false,\n\t};\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// FirstPersonStateComponent (runtime state)\n// Tracks the current state of the first-person entity\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface FirstPersonStateComponent {\n\t/** Current yaw in radians */\n\tyaw: number;\n\t/** Current pitch in radians */\n\tpitch: number;\n\t/** Current movement speed being applied */\n\tcurrentSpeed: number;\n}\n\nexport function createFirstPersonStateComponent(): FirstPersonStateComponent {\n\treturn {\n\t\tyaw: 0,\n\t\tpitch: 0,\n\t\tcurrentSpeed: 0,\n\t};\n}\n"],"mappings":";AAyBO,SAAS,mCACf,UAAiD,CAAC,GACnB;AAC/B,SAAO;AAAA,IACN,WAAW,QAAQ,aAAa;AAAA,IAChC,UAAU,QAAQ,YAAY;AAAA,IAC9B,iBAAiB,QAAQ,mBAAmB;AAAA,IAC5C,WAAW,QAAQ,aAAa;AAAA,EACjC;AACD;AAqBO,SAAS,kCAA6D;AAC5E,SAAO;AAAA,IACN,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,IACP,QAAQ;AAAA,EACT;AACD;AAgBO,SAAS,kCAA6D;AAC5E,SAAO;AAAA,IACN,KAAK;AAAA,IACL,OAAO;AAAA,IACP,cAAc;AAAA,EACf;AACD;","names":[]}
@@ -2,24 +2,24 @@ import {
2
2
  createJumpConfig3D,
3
3
  createJumpInput3D,
4
4
  createJumpState3D
5
- } from "./chunk-F7E23E26.js";
5
+ } from "./chunk-4WH2ZN7P.js";
6
6
  import {
7
7
  GroundProbe3D,
8
8
  getGroundAnchorOffsetY,
9
9
  getGroundSnapTargetY
10
- } from "./chunk-UFHYR2LV.js";
10
+ } from "./chunk-JXMF4NUI.js";
11
11
  import {
12
12
  setVelocityIntent
13
- } from "./chunk-RQWWUML4.js";
13
+ } from "./chunk-OTLSUYQQ.js";
14
14
  import {
15
15
  SyncStateMachine,
16
16
  t
17
- } from "./chunk-RER2IDPW.js";
17
+ } from "./chunk-4URF65BR.js";
18
18
  import {
19
19
  defineBehavior
20
- } from "./chunk-YDLO5M7S.js";
20
+ } from "./chunk-EZNCCO4L.js";
21
21
 
22
- // src/lib/behaviors/jumper-3d/jumper-3d.behavior.ts
22
+ // src/lib/behaviors/jumper-3d/jumper-3d.system.ts
23
23
  function jumpVelocityFromHeight(gravity, height) {
24
24
  return Math.sqrt(2 * gravity * height);
25
25
  }
@@ -35,7 +35,7 @@ function normalizeMut(v) {
35
35
  }
36
36
  return len;
37
37
  }
38
- var Jumper3DBehavior = class {
38
+ var Jumper3DSystem = class {
39
39
  /**
40
40
  * Advance the jump system by one frame.
41
41
  *
@@ -276,17 +276,15 @@ function shouldSnapJumper3DToGround(support, options, velocityY) {
276
276
  }
277
277
  var Jumper3DBehaviorSystem = class {
278
278
  constructor(world, scene, wasmStage, getBehaviorLinks) {
279
- this.world = world;
280
279
  this.scene = scene;
281
280
  this.wasmStage = wasmStage;
282
281
  this.getBehaviorLinks = getBehaviorLinks;
283
282
  this.groundProbe = new GroundProbe3D(world);
284
283
  }
285
- world;
286
284
  scene;
287
285
  wasmStage;
288
286
  getBehaviorLinks;
289
- behavior = new Jumper3DBehavior();
287
+ behavior = new Jumper3DSystem();
290
288
  groundProbe;
291
289
  initializedEntities = /* @__PURE__ */ new Set();
292
290
  timeSinceGroundedMs = /* @__PURE__ */ new Map();
@@ -425,7 +423,7 @@ var Jumper3DBehaviorSystem = class {
425
423
  this.attachedRuntimeSlots.clear();
426
424
  }
427
425
  };
428
- var Jumper3D = defineBehavior({
426
+ var Jumper3DBehavior = defineBehavior({
429
427
  name: "jumper-3d",
430
428
  defaultOptions,
431
429
  systemFactory: (ctx) => new Jumper3DBehaviorSystem(
@@ -447,10 +445,11 @@ var Jumper3D = defineBehavior({
447
445
  });
448
446
 
449
447
  export {
450
- Jumper3DBehavior,
448
+ Jumper3DSystem,
451
449
  JumperTickEvent,
452
450
  Jumper3DState,
453
451
  Jumper3DEvent,
454
452
  Jumper3DFSM,
455
- Jumper3D
453
+ Jumper3DBehavior
456
454
  };
455
+ //# sourceMappingURL=chunk-RONH2KDI.js.map