@zylem/behaviors 0.3.1 → 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 (138) hide show
  1. package/dist/{behavior-system-DYbx-x_-.d.ts → behavior-system-DSNek0W9.d.ts} +205 -29
  2. package/dist/{chunk-Y6UJOUE5.js → chunk-4V7TIRO2.js} +1 -1
  3. package/dist/chunk-4V7TIRO2.js.map +1 -0
  4. package/dist/{chunk-DYLH6K7B.js → chunk-4WH2ZN7P.js} +1 -1
  5. package/dist/{chunk-DYLH6K7B.js.map → chunk-4WH2ZN7P.js.map} +1 -1
  6. package/dist/{chunk-ZPSAXX73.js → chunk-5SM5SH5O.js} +26 -26
  7. package/dist/chunk-5SM5SH5O.js.map +1 -0
  8. package/dist/{chunk-STCCNLPC.js → chunk-5XZWTFGF.js} +5 -3
  9. package/dist/chunk-5XZWTFGF.js.map +1 -0
  10. package/dist/{chunk-OYWFPIHF.js → chunk-64HKCSXD.js} +19 -16
  11. package/dist/chunk-64HKCSXD.js.map +1 -0
  12. package/dist/{chunk-JAN34G6S.js → chunk-FRJDQM23.js} +108 -154
  13. package/dist/chunk-FRJDQM23.js.map +1 -0
  14. package/dist/{chunk-RTRVBEIB.js → chunk-FTDZAKZW.js} +5 -18
  15. package/dist/chunk-FTDZAKZW.js.map +1 -0
  16. package/dist/{chunk-XIJNE67Y.js → chunk-GFARAJV2.js} +3 -5
  17. package/dist/{chunk-XIJNE67Y.js.map → chunk-GFARAJV2.js.map} +1 -1
  18. package/dist/{chunk-SS7ELMJ7.js → chunk-GTLA6LOJ.js} +16 -22
  19. package/dist/chunk-GTLA6LOJ.js.map +1 -0
  20. package/dist/{chunk-VAN4WW3F.js → chunk-H4TJ7X4O.js} +4 -7
  21. package/dist/chunk-H4TJ7X4O.js.map +1 -0
  22. package/dist/{chunk-4I6E6DGV.js → chunk-HNESASBB.js} +4 -6
  23. package/dist/{chunk-4I6E6DGV.js.map → chunk-HNESASBB.js.map} +1 -1
  24. package/dist/{chunk-KS72RZHX.js → chunk-IZJFUAXB.js} +1 -1
  25. package/dist/{chunk-KS72RZHX.js.map → chunk-IZJFUAXB.js.map} +1 -1
  26. package/dist/{chunk-SVLNF434.js → chunk-JXMF4NUI.js} +51 -3
  27. package/dist/chunk-JXMF4NUI.js.map +1 -0
  28. package/dist/chunk-KFFETUAL.js +40 -0
  29. package/dist/chunk-KFFETUAL.js.map +1 -0
  30. package/dist/chunk-LL4FLHIY.js +12 -0
  31. package/dist/chunk-LL4FLHIY.js.map +1 -0
  32. package/dist/{chunk-2CLT36VB.js → chunk-MJFMNRMZ.js} +13 -14
  33. package/dist/chunk-MJFMNRMZ.js.map +1 -0
  34. package/dist/{chunk-RTVRUNYR.js → chunk-OTLSUYQQ.js} +2 -2
  35. package/dist/chunk-OTLSUYQQ.js.map +1 -0
  36. package/dist/{chunk-XG2YMCJW.js → chunk-PWF757B6.js} +4 -6
  37. package/dist/{chunk-XG2YMCJW.js.map → chunk-PWF757B6.js.map} +1 -1
  38. package/dist/{chunk-LD5SMIB3.js → chunk-QVCMJ4Q2.js} +1 -1
  39. package/dist/{chunk-LD5SMIB3.js.map → chunk-QVCMJ4Q2.js.map} +1 -1
  40. package/dist/{chunk-F3KRX2V4.js → chunk-RONH2KDI.js} +10 -12
  41. package/dist/chunk-RONH2KDI.js.map +1 -0
  42. package/dist/chunk-RTRFJVCZ.js +454 -0
  43. package/dist/chunk-RTRFJVCZ.js.map +1 -0
  44. package/dist/{chunk-6C6NXNEW.js → chunk-UL6Q5HDP.js} +25 -3
  45. package/dist/chunk-UL6Q5HDP.js.map +1 -0
  46. package/dist/{chunk-KOZUNIIE.js → chunk-V74N7D6Q.js} +7 -7
  47. package/dist/{chunk-KOZUNIIE.js.map → chunk-V74N7D6Q.js.map} +1 -1
  48. package/dist/{chunk-YUO6SKJD.js → chunk-WXXXJ22R.js} +11 -13
  49. package/dist/chunk-WXXXJ22R.js.map +1 -0
  50. package/dist/{chunk-ARJWUNRE.js → chunk-XLUJDKCT.js} +7 -7
  51. package/dist/{chunk-ARJWUNRE.js.map → chunk-XLUJDKCT.js.map} +1 -1
  52. package/dist/{chunk-GCNRXAKH.js → chunk-XP3ASVGJ.js} +13 -10
  53. package/dist/chunk-XP3ASVGJ.js.map +1 -0
  54. package/dist/{chunk-ID5QJYF5.js → chunk-Y7YIAWPW.js} +11 -1
  55. package/dist/chunk-Y7YIAWPW.js.map +1 -0
  56. package/dist/{chunk-FPTWVCPN.js → chunk-ZA43LPRM.js} +4 -24
  57. package/dist/chunk-ZA43LPRM.js.map +1 -0
  58. package/dist/{chunk-K3UNLQ7W.js → chunk-ZRQWSURL.js} +2 -2
  59. package/dist/chunk-ZRQWSURL.js.map +1 -0
  60. package/dist/cooldown.d.ts +2 -1
  61. package/dist/cooldown.js +1 -1
  62. package/dist/coordinators/boundary-ricochet-3d.d.ts +12 -5
  63. package/dist/coordinators/boundary-ricochet-3d.js +2 -1
  64. package/dist/coordinators/boundary-ricochet.d.ts +9 -7
  65. package/dist/coordinators/boundary-ricochet.js +2 -1
  66. package/dist/coordinators/first-person-shooter.d.ts +9 -8
  67. package/dist/coordinators/first-person-shooter.js +3 -3
  68. package/dist/coordinators/multidirectional-space-shooter.d.ts +12 -9
  69. package/dist/coordinators/multidirectional-space-shooter.js +4 -4
  70. package/dist/coordinators/top-down-shooter.d.ts +9 -8
  71. package/dist/coordinators/top-down-shooter.js +3 -3
  72. package/dist/core.d.ts +4 -4
  73. package/dist/core.js +4 -32
  74. package/dist/core.js.map +1 -1
  75. package/dist/destructible-3d.d.ts +2 -1
  76. package/dist/first-person.d.ts +12 -12
  77. package/dist/first-person.js +8 -8
  78. package/dist/index.d.ts +37 -14
  79. package/dist/index.js +73 -44
  80. package/dist/jumper-2d.d.ts +7 -6
  81. package/dist/jumper-2d.js +5 -5
  82. package/dist/jumper-3d.d.ts +12 -11
  83. package/dist/jumper-3d.js +6 -6
  84. package/dist/{moveable-BCRpjAQb.d.ts → moveable-BLTHO7Jq.d.ts} +2 -78
  85. package/dist/particle-emitter.d.ts +1 -1
  86. package/dist/particle-emitter.js +1 -1
  87. package/dist/platformer-2d.d.ts +306 -0
  88. package/dist/platformer-2d.js +23 -0
  89. package/dist/platformer-3d.d.ts +74 -87
  90. package/dist/platformer-3d.js +4 -4
  91. package/dist/ricochet-2d.d.ts +1 -1
  92. package/dist/ricochet-2d.js +2 -2
  93. package/dist/ricochet-3d.d.ts +1 -1
  94. package/dist/ricochet-3d.js +2 -2
  95. package/dist/runtime-2d.d.ts +93 -2
  96. package/dist/runtime-2d.js +145 -22
  97. package/dist/runtime-2d.js.map +1 -1
  98. package/dist/screen-visibility.d.ts +2 -1
  99. package/dist/screen-wrap.d.ts +2 -1
  100. package/dist/screen-wrap.js +1 -1
  101. package/dist/shooter-2d.d.ts +5 -3
  102. package/dist/shooter-2d.js +2 -2
  103. package/dist/shooter-fire-DGripl0z.d.ts +55 -0
  104. package/dist/thruster.d.ts +8 -18
  105. package/dist/thruster.js +6 -6
  106. package/dist/top-down-movement.d.ts +7 -4
  107. package/dist/top-down-movement.js +5 -5
  108. package/dist/{use-behavior-D_82jAL3.d.ts → use-behavior-h9-T5FSv.d.ts} +1 -1
  109. package/dist/world-boundary-2d.d.ts +2 -1
  110. package/dist/world-boundary-2d.js +2 -2
  111. package/dist/world-boundary-3d.d.ts +2 -1
  112. package/dist/world-boundary-3d.js +2 -2
  113. package/package.json +5 -5
  114. package/dist/chunk-2C3Z5GE7.js +0 -176
  115. package/dist/chunk-2C3Z5GE7.js.map +0 -1
  116. package/dist/chunk-2CLT36VB.js.map +0 -1
  117. package/dist/chunk-3PYGRHAL.js +0 -35
  118. package/dist/chunk-3PYGRHAL.js.map +0 -1
  119. package/dist/chunk-6C6NXNEW.js.map +0 -1
  120. package/dist/chunk-F3KRX2V4.js.map +0 -1
  121. package/dist/chunk-FPTWVCPN.js.map +0 -1
  122. package/dist/chunk-GCNRXAKH.js.map +0 -1
  123. package/dist/chunk-ID5QJYF5.js.map +0 -1
  124. package/dist/chunk-JAN34G6S.js.map +0 -1
  125. package/dist/chunk-K3UNLQ7W.js.map +0 -1
  126. package/dist/chunk-OYWFPIHF.js.map +0 -1
  127. package/dist/chunk-RTRVBEIB.js.map +0 -1
  128. package/dist/chunk-RTVRUNYR.js.map +0 -1
  129. package/dist/chunk-SS7ELMJ7.js.map +0 -1
  130. package/dist/chunk-STCCNLPC.js.map +0 -1
  131. package/dist/chunk-SVLNF434.js.map +0 -1
  132. package/dist/chunk-VAN4WW3F.js.map +0 -1
  133. package/dist/chunk-Y6UJOUE5.js.map +0 -1
  134. package/dist/chunk-YUO6SKJD.js.map +0 -1
  135. package/dist/chunk-ZPSAXX73.js.map +0 -1
  136. package/dist/runtime-pong.d.ts +0 -92
  137. package/dist/runtime-pong.js +0 -38
  138. /package/dist/{runtime-pong.js.map → platformer-2d.js.map} +0 -0
@@ -5,17 +5,19 @@ import {
5
5
  } from "./chunk-EIWQM6U3.js";
6
6
  import {
7
7
  angleFromDirection2D,
8
- normalizeDirection2D
9
- } from "./chunk-ID5QJYF5.js";
8
+ normalizeDirection2DInto
9
+ } from "./chunk-Y7YIAWPW.js";
10
10
  import {
11
11
  StageTopDownPlane
12
- } from "./chunk-Y6UJOUE5.js";
12
+ } from "./chunk-4V7TIRO2.js";
13
13
  import {
14
14
  defineBehavior
15
15
  } from "./chunk-EZNCCO4L.js";
16
16
 
17
- // src/lib/behaviors/top-down-movement/top-down-movement.behavior.ts
18
- var TopDownMovementRuntimeBehavior = class {
17
+ // src/lib/behaviors/top-down-movement/top-down-movement.system.ts
18
+ var _moveDirection = { x: 0, y: 0 };
19
+ var _facingDirection = { x: 0, y: 0 };
20
+ var TopDownMovementSystem = class {
19
21
  updateEntity(entity, _delta) {
20
22
  if (!entity.body || !entity.transformStore || !entity.topDownMovement || !entity.$topDownMovement || !entity.topDownMovementState) {
21
23
  return;
@@ -23,16 +25,17 @@ var TopDownMovementRuntimeBehavior = class {
23
25
  const movement = entity.topDownMovement;
24
26
  const input = entity.$topDownMovement;
25
27
  const state = entity.topDownMovementState;
26
- const moveDirection = normalizeDirection2D(input.moveX, input.moveY);
28
+ const isMoving = normalizeDirection2DInto(input.moveX, input.moveY, _moveDirection);
27
29
  const velocity = entity.body.linvel();
28
- state.moving = moveDirection !== null;
29
- entity.transformStore.velocity.x = (moveDirection?.x ?? 0) * movement.moveSpeed;
30
- entity.transformStore.velocity.y = (moveDirection?.y ?? 0) * movement.moveSpeed;
30
+ state.moving = isMoving;
31
+ entity.transformStore.velocity.x = isMoving ? _moveDirection.x * movement.moveSpeed : 0;
32
+ entity.transformStore.velocity.y = isMoving ? _moveDirection.y * movement.moveSpeed : 0;
31
33
  entity.transformStore.velocity.z = velocity.z;
32
- entity.transformStore.dirty.velocity = true;
33
- const facingDirection = normalizeDirection2D(input.faceX, input.faceY);
34
- if (facingDirection) {
35
- state.facingAngle = angleFromDirection2D(facingDirection);
34
+ entity.transformStore.dirty.velocityX = true;
35
+ entity.transformStore.dirty.velocityY = true;
36
+ entity.transformStore.dirty.velocityZ = true;
37
+ if (normalizeDirection2DInto(input.faceX, input.faceY, _facingDirection)) {
38
+ state.facingAngle = angleFromDirection2D(_facingDirection);
36
39
  }
37
40
  entity.setRotationZ?.(state.facingAngle);
38
41
  }
@@ -57,7 +60,7 @@ var TopDownMovementBehaviorSystem = class {
57
60
  }
58
61
  wasmStage;
59
62
  getBehaviorLinks;
60
- behavior = new TopDownMovementRuntimeBehavior();
63
+ behavior = new TopDownMovementSystem();
61
64
  attachedRuntimeSlots = /* @__PURE__ */ new Set();
62
65
  update(_ecs, delta) {
63
66
  const links = this.getBehaviorLinks?.(TOP_DOWN_MOVEMENT_BEHAVIOR_KEY);
@@ -109,7 +112,7 @@ var TopDownMovementBehavior = defineBehavior({
109
112
  });
110
113
 
111
114
  export {
112
- TopDownMovementRuntimeBehavior,
115
+ TopDownMovementSystem,
113
116
  TopDownMovementBehavior
114
117
  };
115
- //# sourceMappingURL=chunk-OYWFPIHF.js.map
118
+ //# sourceMappingURL=chunk-64HKCSXD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/behaviors/top-down-movement/top-down-movement.system.ts","../src/lib/behaviors/top-down-movement/top-down-movement.descriptor.ts"],"sourcesContent":["import {\n\tangleFromDirection2D,\n\tnormalizeDirection2DInto,\n\ttype Direction2D,\n} from '../shared/direction-2d';\nimport type {\n\tTopDownMovementComponent,\n\tTopDownMovementInputComponent,\n\tTopDownMovementStateComponent,\n} from './components';\n\n// Per-frame scratch objects; normalizeDirection2DInto writes here instead of\n// allocating fresh direction objects per entity per frame.\nconst _moveDirection: Direction2D = { x: 0, y: 0 };\nconst _facingDirection: Direction2D = { x: 0, y: 0 };\n\nexport interface TopDownMovementEntity {\n\tbody: {\n\t\tlinvel(): { x: number; y: number; z: number };\n\t};\n\ttransformStore?: {\n\t\tvelocity: { x: number; y: number; z: number };\n\t\tdirty: { velocityX: boolean; velocityY: boolean; velocityZ: boolean };\n\t};\n\tsetRotationZ?: (angle: number) => void;\n\ttopDownMovement: TopDownMovementComponent;\n\t$topDownMovement: TopDownMovementInputComponent;\n\ttopDownMovementState: TopDownMovementStateComponent;\n}\n\nexport class TopDownMovementSystem {\n\tupdateEntity(entity: TopDownMovementEntity, _delta: number): void {\n\t\tif (\n\t\t\t!entity.body\n\t\t\t|| !entity.transformStore\n\t\t\t|| !entity.topDownMovement\n\t\t\t|| !entity.$topDownMovement\n\t\t\t|| !entity.topDownMovementState\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst movement = entity.topDownMovement;\n\t\tconst input = entity.$topDownMovement;\n\t\tconst state = entity.topDownMovementState;\n\t\tconst isMoving = normalizeDirection2DInto(input.moveX, input.moveY, _moveDirection);\n\t\tconst velocity = entity.body.linvel();\n\n\t\tstate.moving = isMoving;\n\t\tentity.transformStore.velocity.x = isMoving ? _moveDirection.x * movement.moveSpeed : 0;\n\t\tentity.transformStore.velocity.y = isMoving ? _moveDirection.y * movement.moveSpeed : 0;\n\t\tentity.transformStore.velocity.z = velocity.z;\n\t\tentity.transformStore.dirty.velocityX = true;\n\t\tentity.transformStore.dirty.velocityY = true;\n\t\tentity.transformStore.dirty.velocityZ = true;\n\n\t\tif (normalizeDirection2DInto(input.faceX, input.faceY, _facingDirection)) {\n\t\t\tstate.facingAngle = angleFromDirection2D(_facingDirection);\n\t\t}\n\n\t\tentity.setRotationZ?.(state.facingAngle);\n\t}\n}\n","import { defineBehavior, type BehaviorRef } from '../behavior-descriptor';\nimport type { BehaviorEntityLink, BehaviorSystem } from '../behavior-system';\nimport {\n\tcreateTopDownMovementComponent,\n\tcreateTopDownMovementInputComponent,\n\tcreateTopDownMovementStateComponent,\n} from './components';\nimport {\n\tTopDownMovementSystem,\n\ttype TopDownMovementEntity,\n} from './top-down-movement.system';\nimport {\n\tStageTopDownPlane,\n\ttype WasmStageRuntime,\n} from '../../runtime/wasm-stage-runtime';\n\nexport interface TopDownMovementBehaviorOptions {\n\tmoveSpeed?: number;\n}\n\nexport interface TopDownMovementHandle {\n\tgetFacingAngle(): number;\n}\n\nconst defaultOptions: TopDownMovementBehaviorOptions = {\n\tmoveSpeed: 8,\n};\n\nconst TOP_DOWN_MOVEMENT_BEHAVIOR_KEY = Symbol.for(\n\t'zylem:behavior:top-down-movement',\n);\n\nfunction createTopDownMovementHandle(\n\tref: BehaviorRef<TopDownMovementBehaviorOptions>,\n): TopDownMovementHandle {\n\treturn {\n\t\tgetFacingAngle: () =>\n\t\t\t(ref as any).__entity?.topDownMovementState?.facingAngle ?? 0,\n\t};\n}\n\nclass TopDownMovementBehaviorSystem implements BehaviorSystem {\n\tprivate behavior = new TopDownMovementSystem();\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?.(TOP_DOWN_MOVEMENT_BEHAVIOR_KEY);\n\t\tif (!links) return;\n\n\t\tfor (const link of links) {\n\t\t\tconst entity = link.entity as any;\n\t\t\tconst ref = link.ref as any;\n\n\t\t\tif (!entity.topDownMovement) {\n\t\t\t\tentity.topDownMovement = createTopDownMovementComponent(ref.options);\n\t\t\t}\n\t\t\tif (!entity.$topDownMovement) {\n\t\t\t\tentity.$topDownMovement = createTopDownMovementInputComponent();\n\t\t\t}\n\t\t\tif (!entity.topDownMovementState) {\n\t\t\t\tentity.topDownMovementState = createTopDownMovementStateComponent();\n\t\t\t}\n\n\t\t\tref.__entity = entity;\n\n\t\t\tconst handle = (entity.runtimeHandle ?? -1) as number;\n\t\t\tif (this.wasmStage && handle >= 0) {\n\t\t\t\tthis.ensureWasmAttached(handle, ref.options as TopDownMovementBehaviorOptions);\n\t\t\t\tconst input = entity.$topDownMovement;\n\t\t\t\tif (input) {\n\t\t\t\t\tthis.wasmStage.setTopDownInput(handle, input.moveX ?? 0, input.moveY ?? input.moveZ ?? 0);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (!entity.body || !entity.transformStore) continue;\n\t\t\tthis.behavior.updateEntity(entity, delta);\n\t\t}\n\t}\n\n\tprivate ensureWasmAttached(handle: number, options: TopDownMovementBehaviorOptions): void {\n\t\tif (!this.wasmStage || this.attachedRuntimeSlots.has(handle)) return;\n\t\tthis.wasmStage.attachTopDown(handle, {\n\t\t\tplane: StageTopDownPlane.Xy,\n\t\t\tspeed: options.moveSpeed ?? defaultOptions.moveSpeed!,\n\t\t\tfaceMovement: true,\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\nexport const TopDownMovementBehavior = defineBehavior<\n\tTopDownMovementBehaviorOptions,\n\tTopDownMovementHandle,\n\tTopDownMovementEntity\n>({\n\tname: 'top-down-movement',\n\tdefaultOptions,\n\tsystemFactory: (ctx) =>\n\t\tnew TopDownMovementBehaviorSystem(ctx.wasmStage ?? null, ctx.getBehaviorLinks),\n\tcreateHandle: createTopDownMovementHandle,\n});\n"],"mappings":"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","names":[]}
@@ -14,13 +14,16 @@ function createPlatformer3DMovementComponent(options = {}) {
14
14
  jumpForce: options.jumpForce ?? 12,
15
15
  maxJumps: options.maxJumps ?? 1,
16
16
  gravity: options.gravity ?? 9.82,
17
- groundRayLength: options.groundRayLength ?? 1,
18
17
  coyoteTime: options.coyoteTime ?? 0.1,
19
18
  jumpBufferTime: options.jumpBufferTime ?? 0.1,
20
19
  jumpCutMultiplier: options.jumpCutMultiplier ?? 0.5,
21
20
  multiJumpWindowTime: options.multiJumpWindowTime ?? 0.15,
22
21
  // 150ms default
23
- debugGroundProbe: options.debugGroundProbe ?? false
22
+ slopeClimbAngle: options.slopeClimbAngle ?? 50,
23
+ slopeSlideAngle: options.slopeSlideAngle ?? 40,
24
+ autostep: options.autostep ?? { maxHeight: 0.3, minWidth: 0.1 },
25
+ snapToGroundDistance: options.snapToGroundDistance ?? 0.3,
26
+ characterOffset: options.characterOffset ?? 0.05
24
27
  };
25
28
  }
26
29
  function createPlatformer3DInputComponent() {
@@ -52,7 +55,7 @@ function createPlatformer3DStateComponent() {
52
55
  timeSinceJump: 0,
53
56
  jumpedThisFrame: false,
54
57
  jumpCutRampTimer: 0,
55
- groundedGraceTimer: 0
58
+ verticalVelocity: 0
56
59
  };
57
60
  }
58
61
 
@@ -183,92 +186,52 @@ var Platformer3DFSM = class {
183
186
  }
184
187
  };
185
188
 
186
- // src/lib/behaviors/platformer-3d/platformer-3d.behavior.ts
189
+ // src/lib/behaviors/platformer-3d/platformer-3d.system.ts
190
+ import { RigidBodyType, QueryFilterFlags } from "@dimforge/rapier3d-compat";
187
191
  var JUMP_CUT_RAMP_DURATION = 0.05;
188
- var GROUNDED_GRACE_WINDOW = 0.06;
189
- var Platformer3DBehavior = class {
192
+ var GROUND_STICK_SPEED = 2;
193
+ var FALL_VELOCITY_THRESHOLD = -0.1;
194
+ var DEG_TO_RAD = Math.PI / 180;
195
+ var _desired = { x: 0, y: 0, z: 0 };
196
+ var Platformer3DSystem = class {
190
197
  world;
191
- scene;
192
- /**
193
- * Tracks entities whose Rapier `gravityScale` has been zeroed so that
194
- * `applyGravity` is the sole writer of vertical velocity. We do this
195
- * lazily on the first update because the body isn't always available at
196
- * construction time.
197
- */
198
- gravityConfiguredUUIDs = /* @__PURE__ */ new Set();
199
- constructor(world, scene) {
198
+ /** Per-entity character controllers, keyed by entity uuid. */
199
+ controllers = /* @__PURE__ */ new Map();
200
+ constructor(world) {
200
201
  this.world = world;
201
- this.scene = scene;
202
- }
203
- /**
204
- * Detect "grounded" using the actual capsule collider's contact pairs.
205
- *
206
- * A contact is treated as ground when its solver point lies below the
207
- * body center (i.e., it sits on the bottom hemisphere/cylinder). We
208
- * deliberately don't filter by normal direction: the contact point's
209
- * position alone is robust against the various normal-flip conventions
210
- * Rapier uses for `flipped` manifolds and against rolling-bottom edges
211
- * on heightmap terrain.
212
- */
213
- detectGroundedFromContacts(entity) {
214
- const collider = entity.collider;
215
- const physicsWorld = this.world?.world;
216
- if (!collider || !physicsWorld?.contactsWith) {
217
- return false;
218
- }
219
- let grounded = false;
220
- physicsWorld.contactsWith(collider, (other) => {
221
- if (grounded) return;
222
- physicsWorld.contactPair(collider, other, (manifold, flipped) => {
223
- if (grounded) return;
224
- const num = manifold.numContacts();
225
- for (let i = 0; i < num; i++) {
226
- const local = flipped ? manifold.localContactPoint2(i) : manifold.localContactPoint1(i);
227
- if (!local) continue;
228
- if (local.y < 0) {
229
- grounded = true;
230
- return;
231
- }
232
- }
233
- });
234
- });
235
- return grounded;
236
202
  }
237
203
  /**
238
- * One-time per-entity physics setup: take over gravity from Rapier's
239
- * world so that `applyGravity` controls the entire airborne fall curve.
240
- * Without this, gravity was effectively doubled (Rapier's world gravity
241
- * + this behavior's explicit gravity were both pulling the body down).
204
+ * One-time per-entity physics setup: convert the body to a
205
+ * position-based kinematic body and create its character controller.
206
+ * Done lazily on the first update because the body isn't always
207
+ * available at construction time.
242
208
  */
243
209
  ensurePhysicsConfigured(entity) {
244
- if (this.gravityConfiguredUUIDs.has(entity.uuid)) return;
245
- if (typeof entity.body?.setGravityScale === "function") {
246
- entity.body.setGravityScale(0, true);
210
+ const existing = this.controllers.get(entity.uuid);
211
+ if (existing) return existing;
212
+ const physicsWorld = this.world?.world;
213
+ if (!physicsWorld || !entity.body) return null;
214
+ if (entity.body.bodyType() !== RigidBodyType.KinematicPositionBased) {
215
+ entity.body.setBodyType(RigidBodyType.KinematicPositionBased, true);
247
216
  }
248
- this.gravityConfiguredUUIDs.add(entity.uuid);
249
- }
250
- /**
251
- * Apply horizontal movement based on input.
252
- *
253
- * Only writes X/Z axes — Y is left untouched so Rapier's contact resolution
254
- * can damp vertical motion correctly while grounded. Jump and gravity are
255
- * the sole writers of Y velocity (see handleJump / applyGravity).
256
- */
257
- applyMovement(entity) {
258
- const input = entity.$platformer;
259
217
  const movement = entity.platformer;
260
- const state = entity.platformerState;
261
- const speed = input.run ? movement.runSpeed : movement.walkSpeed;
262
- state.currentSpeed = speed;
263
- const moveX = input.moveX * speed;
264
- const moveZ = input.moveZ * speed;
265
- entity.transformStore.velocity.x = moveX;
266
- entity.transformStore.velocity.z = moveZ;
267
- entity.transformStore.dirty.velocityX = true;
268
- entity.transformStore.dirty.velocityZ = true;
218
+ const controller = physicsWorld.createCharacterController(movement.characterOffset);
219
+ controller.setUp({ x: 0, y: 1, z: 0 });
220
+ controller.setMaxSlopeClimbAngle(movement.slopeClimbAngle * DEG_TO_RAD);
221
+ controller.setMinSlopeSlideAngle(movement.slopeSlideAngle * DEG_TO_RAD);
222
+ if (movement.autostep) {
223
+ controller.enableAutostep(movement.autostep.maxHeight, movement.autostep.minWidth, true);
224
+ } else {
225
+ controller.disableAutostep();
226
+ }
227
+ controller.enableSnapToGround(movement.snapToGroundDistance);
228
+ controller.setApplyImpulsesToDynamicBodies(true);
229
+ this.controllers.set(entity.uuid, controller);
230
+ return controller;
269
231
  }
270
232
  /**
271
- * Handle jump logic with multi-jump, coyote time, buffering, and jump-cut ramp.
233
+ * Handle jump logic with multi-jump, coyote time, buffering, and jump-cut
234
+ * ramp. All vertical motion is written to `state.verticalVelocity`.
272
235
  */
273
236
  handleJump(entity, delta) {
274
237
  const input = entity.$platformer;
@@ -299,12 +262,10 @@ var Platformer3DBehavior = class {
299
262
  state.jumpCutRampTimer = JUMP_CUT_RAMP_DURATION;
300
263
  }
301
264
  if (state.jumpCutRampTimer > 0) {
302
- const velocity = entity.body.linvel();
303
- if (velocity.y > 0) {
265
+ if (state.verticalVelocity > 0) {
304
266
  const stepT = Math.min(delta / state.jumpCutRampTimer, 1);
305
- const targetY = velocity.y * movement.jumpCutMultiplier;
306
- entity.transformStore.velocity.y = velocity.y + (targetY - velocity.y) * stepT;
307
- entity.transformStore.dirty.velocityY = true;
267
+ const targetY = state.verticalVelocity * movement.jumpCutMultiplier;
268
+ state.verticalVelocity += (targetY - state.verticalVelocity) * stepT;
308
269
  }
309
270
  state.jumpCutRampTimer = Math.max(0, state.jumpCutRampTimer - delta);
310
271
  }
@@ -325,19 +286,17 @@ var Platformer3DBehavior = class {
325
286
  state.falling = false;
326
287
  state.jumpCutApplied = false;
327
288
  state.jumpCutRampTimer = 0;
328
- state.groundedGraceTimer = 0;
329
- entity.transformStore.velocity.y = movement.jumpForce;
330
- entity.transformStore.dirty.velocityY = true;
289
+ state.verticalVelocity = movement.jumpForce;
331
290
  state.jumpedThisFrame = true;
332
291
  }
333
292
  }
334
293
  /**
335
- * Apply gravity when not grounded.
294
+ * Integrate gravity into `state.verticalVelocity`.
336
295
  *
337
- * Skipped on the single frame immediately after a jump impulse is applied
338
- * (tracked via `state.jumpedThisFrame`) so gravity never overwrites a
339
- * just-applied jump velocity. The flag is cleared here regardless, so it
340
- * only suppresses exactly one gravity step — independent of frame rate.
296
+ * While grounded (and not on the jump frame) vertical velocity is held at
297
+ * a small constant downward speed so the controller stays pressed into
298
+ * the ground. Skipped entirely on the frame a jump was applied — the
299
+ * grounded flag is still stale-true then and would clobber the jump.
341
300
  */
342
301
  applyGravity(entity, delta) {
343
302
  const movement = entity.platformer;
@@ -346,58 +305,27 @@ var Platformer3DBehavior = class {
346
305
  state.jumpedThisFrame = false;
347
306
  return;
348
307
  }
349
- if (state.grounded) return;
350
- const currentVel = entity.body.linvel();
351
- const newYVelocity = currentVel.y - movement.gravity * delta;
352
- entity.transformStore.velocity.y = newYVelocity;
353
- entity.transformStore.dirty.velocityY = true;
308
+ if (state.grounded) {
309
+ state.verticalVelocity = -GROUND_STICK_SPEED;
310
+ return;
311
+ }
312
+ state.verticalVelocity -= movement.gravity * delta;
354
313
  }
355
314
  /**
356
- * Update entity state based on physics.
357
- *
358
- * Grounded is determined purely by Rapier's contact pairs on the entity's
359
- * collider. This removes a whole class of slope/heightmap jitter caused
360
- * by the previous shape probe disagreeing with the contact resolver — the
361
- * probe could report grounded while a contact impulse had just nudged the
362
- * body off the ground, producing visible micro-bouncing.
363
- *
364
- * While grounded, vertical velocity is held at 0 each frame. This stops
365
- * residual upward kicks from slope-contact resolution from accumulating
366
- * across frames (which previously caused the character to "ride up"
367
- * slopes and lose contact briefly, then fall back, then re-contact, in a
368
- * tight oscillation).
315
+ * Update entity state from the controller's grounded result.
369
316
  */
370
- updateState(entity, delta) {
317
+ updateState(entity, delta, computedGrounded) {
371
318
  const state = entity.platformerState;
372
319
  const wasGrounded = state.grounded;
373
- const velocity = entity.body.linvel();
374
- const hasContact = this.detectGroundedFromContacts(entity);
375
320
  const isAirborne = state.jumping || state.falling;
376
321
  let isGrounded;
377
322
  if (isAirborne) {
378
323
  const canLand = state.falling && !state.jumping;
379
- isGrounded = hasContact && canLand;
324
+ isGrounded = computedGrounded && canLand;
380
325
  } else {
381
- isGrounded = hasContact;
382
- }
383
- if (!isGrounded && wasGrounded) {
384
- if (state.groundedGraceTimer <= 0) {
385
- state.groundedGraceTimer = GROUNDED_GRACE_WINDOW;
386
- }
387
- }
388
- if (state.groundedGraceTimer > 0) {
389
- if (!isGrounded && wasGrounded && !isAirborne) {
390
- isGrounded = true;
391
- state.groundedGraceTimer = Math.max(0, state.groundedGraceTimer - delta);
392
- } else {
393
- state.groundedGraceTimer = 0;
394
- }
326
+ isGrounded = computedGrounded;
395
327
  }
396
328
  state.grounded = isGrounded;
397
- if (state.grounded) {
398
- entity.transformStore.velocity.y = 0;
399
- entity.transformStore.dirty.velocityY = true;
400
- }
401
329
  if (state.grounded) {
402
330
  state.timeSinceGrounded = 0;
403
331
  state.lastGroundedY = entity.body.translation().y;
@@ -410,9 +338,12 @@ var Platformer3DBehavior = class {
410
338
  state.falling = false;
411
339
  state.jumpCutApplied = false;
412
340
  state.jumpCutRampTimer = 0;
341
+ if (state.verticalVelocity < 0) {
342
+ state.verticalVelocity = 0;
343
+ }
413
344
  }
414
- if (velocity.y < -0.1 && !state.grounded) {
415
- if (state.jumping && velocity.y < 0) {
345
+ if (state.verticalVelocity < FALL_VELOCITY_THRESHOLD && !state.grounded) {
346
+ if (state.jumping) {
416
347
  state.jumping = false;
417
348
  state.falling = true;
418
349
  } else if (!state.jumping) {
@@ -428,15 +359,40 @@ var Platformer3DBehavior = class {
428
359
  if (!entity.platformer || !entity.$platformer || !entity.platformerState) {
429
360
  return;
430
361
  }
431
- const platformerEntity = entity;
432
- this.ensurePhysicsConfigured(platformerEntity);
433
- this.updateState(platformerEntity, delta);
434
- this.applyMovement(platformerEntity);
435
- this.handleJump(platformerEntity, delta);
436
- this.applyGravity(platformerEntity, delta);
362
+ const controller = this.ensurePhysicsConfigured(entity);
363
+ const collider = entity.collider;
364
+ if (!controller || !collider) return;
365
+ const input = entity.$platformer;
366
+ const movement = entity.platformer;
367
+ const state = entity.platformerState;
368
+ this.handleJump(entity, delta);
369
+ this.applyGravity(entity, delta);
370
+ const speed = input.run ? movement.runSpeed : movement.walkSpeed;
371
+ state.currentSpeed = speed;
372
+ _desired.x = input.moveX * speed * delta;
373
+ _desired.y = state.verticalVelocity * delta;
374
+ _desired.z = input.moveZ * speed * delta;
375
+ controller.computeColliderMovement(collider, _desired, QueryFilterFlags.EXCLUDE_SENSORS);
376
+ const corrected = controller.computedMovement();
377
+ if (_desired.y > 1e-9 && corrected.y < _desired.y * 0.5) {
378
+ state.verticalVelocity = 0;
379
+ }
380
+ const translation = entity.body.translation();
381
+ entity.body.setNextKinematicTranslation({
382
+ x: translation.x + corrected.x,
383
+ y: translation.y + corrected.y,
384
+ z: translation.z + corrected.z
385
+ });
386
+ this.updateState(entity, delta, controller.computedGrounded());
437
387
  }
438
388
  destroy() {
439
- this.gravityConfiguredUUIDs.clear();
389
+ const physicsWorld = this.world?.world;
390
+ if (physicsWorld) {
391
+ for (const controller of this.controllers.values()) {
392
+ physicsWorld.removeCharacterController(controller);
393
+ }
394
+ }
395
+ this.controllers.clear();
440
396
  }
441
397
  };
442
398
 
@@ -447,20 +403,19 @@ var defaultOptions = {
447
403
  jumpForce: 12,
448
404
  maxJumps: 1,
449
405
  gravity: 9.82,
450
- groundRayLength: 1,
451
- debugGroundProbe: false
406
+ slopeClimbAngle: 50,
407
+ slopeSlideAngle: 40,
408
+ autostep: { maxHeight: 0.3, minWidth: 0.1 },
409
+ snapToGroundDistance: 0.3,
410
+ characterOffset: 0.05
452
411
  };
453
412
  var PLATFORMER_BEHAVIOR_KEY = /* @__PURE__ */ Symbol.for("zylem:behavior:platformer-3d");
454
413
  var Platformer3DBehaviorSystem = class {
455
- constructor(world, scene, wasmStage, getBehaviorLinks) {
456
- this.world = world;
457
- this.scene = scene;
414
+ constructor(world, wasmStage, getBehaviorLinks) {
458
415
  this.wasmStage = wasmStage;
459
416
  this.getBehaviorLinks = getBehaviorLinks;
460
- this.movementBehavior = new Platformer3DBehavior(world, scene);
417
+ this.movementBehavior = new Platformer3DSystem(world);
461
418
  }
462
- world;
463
- scene;
464
419
  wasmStage;
465
420
  getBehaviorLinks;
466
421
  movementBehavior;
@@ -522,12 +477,11 @@ var Platformer3DBehaviorSystem = class {
522
477
  this.attachedRuntimeSlots.clear();
523
478
  }
524
479
  };
525
- var Platformer3DBehavior2 = defineBehavior({
480
+ var Platformer3DBehavior = defineBehavior({
526
481
  name: "platformer-3d",
527
482
  defaultOptions,
528
483
  systemFactory: (ctx) => new Platformer3DBehaviorSystem(
529
- ctx.world,
530
- ctx.scene,
484
+ ctx.world ?? null,
531
485
  ctx.wasmStage ?? null,
532
486
  ctx.getBehaviorLinks
533
487
  ),
@@ -546,7 +500,7 @@ export {
546
500
  Platformer3DState,
547
501
  Platformer3DEvent,
548
502
  Platformer3DFSM,
549
- Platformer3DBehavior,
550
- Platformer3DBehavior2
503
+ Platformer3DSystem,
504
+ Platformer3DBehavior
551
505
  };
552
- //# sourceMappingURL=chunk-JAN34G6S.js.map
506
+ //# sourceMappingURL=chunk-FRJDQM23.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/behaviors/platformer-3d/components.ts","../src/lib/behaviors/platformer-3d/platformer-3d-fsm.ts","../src/lib/behaviors/platformer-3d/platformer-3d.system.ts","../src/lib/behaviors/platformer-3d/platformer-3d.descriptor.ts"],"sourcesContent":["/**\n * Platformer 3D ECS Components\n * \n * Components for 3D platformer movement system including walking, running,\n * jumping with multi-jump support, and falling/landing states.\n */\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Platformer3DMovementComponent (capability / configuration)\n// Defines the movement capabilities and physics parameters\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface Platformer3DMovementComponent {\n\t/** Base walking speed */\n\twalkSpeed: number;\n\t/** Sprint/run speed */\n\trunSpeed: number;\n\t/** Initial jump force/velocity */\n\tjumpForce: number;\n\t/** Maximum number of jumps (1 = single jump, 2 = double jump, etc.) */\n\tmaxJumps: number;\n\t/** Gravity force (optional override) */\n\tgravity: number;\n\t/** Coyote time in seconds (grace period after leaving ground) */\n\tcoyoteTime: number;\n\t/** Jump buffer time in seconds (queue jump input before landing) */\n\tjumpBufferTime: number;\n\t/** Velocity multiplier when releasing jump button early (0-1) */\n\tjumpCutMultiplier: number;\n\t/** Time in seconds before multi-jump becomes available (0 = after button release) */\n\tmultiJumpWindowTime: number;\n\t/** Maximum slope angle (degrees) the character can walk up */\n\tslopeClimbAngle: number;\n\t/** Slope angle (degrees) past which the character slides down */\n\tslopeSlideAngle: number;\n\t/** Autostep configuration for small ledges, or false to disable */\n\tautostep: { maxHeight: number; minWidth: number } | false;\n\t/** Snap-to-ground distance (keeps the character glued going downhill) */\n\tsnapToGroundDistance: number;\n\t/** Character controller offset (small gap kept between collider and environment) */\n\tcharacterOffset: number;\n}\n\nexport function createPlatformer3DMovementComponent(\n\toptions: Partial<Platformer3DMovementComponent> = {}\n): Platformer3DMovementComponent {\n\treturn {\n\t\twalkSpeed: options.walkSpeed ?? 12,\n\t\trunSpeed: options.runSpeed ?? 24,\n\t\tjumpForce: options.jumpForce ?? 12,\n\t\tmaxJumps: options.maxJumps ?? 1,\n\t\tgravity: options.gravity ?? 9.82,\n\t\tcoyoteTime: options.coyoteTime ?? 0.1,\n\t\tjumpBufferTime: options.jumpBufferTime ?? 0.1,\n\t\tjumpCutMultiplier: options.jumpCutMultiplier ?? 0.5,\n\t\tmultiJumpWindowTime: options.multiJumpWindowTime ?? 0.15, // 150ms default\n\t\tslopeClimbAngle: options.slopeClimbAngle ?? 50,\n\t\tslopeSlideAngle: options.slopeSlideAngle ?? 40,\n\t\tautostep: options.autostep ?? { maxHeight: 0.3, minWidth: 0.1 },\n\t\tsnapToGroundDistance: options.snapToGroundDistance ?? 0.3,\n\t\tcharacterOffset: options.characterOffset ?? 0.05,\n\t};\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Platformer3DInputComponent (intent)\n// Written by: Player controller, AI controller\n// Read by: Platformer3DBehavior\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface Platformer3DInputComponent {\n\t/** Horizontal movement input (-1 to 1) */\n\tmoveX: number;\n\t/** Forward/backward movement input (-1 to 1) */\n\tmoveZ: number;\n\t/** Jump button pressed */\n\tjump: boolean;\n\t/** Run/sprint button held */\n\trun: boolean;\n}\n\nexport function createPlatformer3DInputComponent(): Platformer3DInputComponent {\n\treturn {\n\t\tmoveX: 0,\n\t\tmoveZ: 0,\n\t\tjump: false,\n\t\trun: false,\n\t};\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Platformer3DStateComponent (runtime state)\n// Tracks the current state of the platformer entity\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface Platformer3DStateComponent {\n\t/** Is the entity on the ground */\n\tgrounded: boolean;\n\t/** Is currently in a jump arc */\n\tjumping: boolean;\n\t/** Is falling (not jumping) */\n\tfalling: boolean;\n\t/** Current jump count (for multi-jump) */\n\tjumpCount: number;\n\t/** Y position when jump started */\n\tjumpStartHeight: number;\n\t/** Current movement speed being applied */\n\tcurrentSpeed: number;\n\t/** Last known ground Y position */\n\tlastGroundedY: number;\n\t/** Was jump button pressed last frame (for edge detection) */\n\tjumpPressedLastFrame: boolean;\n\t\n\t// -- New fields --\n\t/** Is currently colliding with ground (from physics collision callback) */\n\tcollisionGrounded: boolean;\n\t/** Timestamp of last ground collision */\n\tgroundedCollisionTime: number;\n\t/** Time in seconds since last grounded */\n\ttimeSinceGrounded: number;\n\t/** Is a jump allowed due to buffer? */\n\tjumpBuffered: boolean;\n\t/** Time remaining in jump buffer */\n\tjumpBufferTimer: number;\n\t/** Is jump button currently held */\n\tjumpHeld: boolean;\n\t/** Has the jump cut been applied for this jump? */\n\tjumpCutApplied: boolean;\n\t/** Has jump button been released since last jump (required for multi-jump) */\n\tjumpReleasedSinceLastJump: boolean;\n\t/** Time since current jump started (for multi-jump window) */\n\ttimeSinceJump: number;\n\t/** True for the single frame on which a jump impulse was applied; prevents gravity from clobbering jump velocity. */\n\tjumpedThisFrame: boolean;\n\t/** Remaining seconds of jump-cut velocity ramp; while > 0, verticalVelocity lerps toward verticalVelocity * jumpCutMultiplier. */\n\tjumpCutRampTimer: number;\n\t/**\n\t * Controller-owned vertical velocity (units/second).\n\t * The body is kinematic, so `linvel()` is meaningless — this field is the\n\t * single source of truth for jump/gravity motion on the Y axis.\n\t */\n\tverticalVelocity: number;\n}\n\nexport function createPlatformer3DStateComponent(): Platformer3DStateComponent {\n\treturn {\n\t\tgrounded: false,\n\t\tjumping: false,\n\t\tfalling: false,\n\t\tjumpCount: 0,\n\t\tjumpStartHeight: 0,\n\t\tcurrentSpeed: 0,\n\t\tlastGroundedY: 0,\n\t\tjumpPressedLastFrame: false,\n\t\tcollisionGrounded: false,\n\t\tgroundedCollisionTime: 0,\n\t\ttimeSinceGrounded: 0,\n\t\tjumpBuffered: false,\n\t\tjumpBufferTimer: 0,\n\t\tjumpHeld: false,\n\t\tjumpCutApplied: false,\n\t\tjumpReleasedSinceLastJump: true,\n\t\ttimeSinceJump: 0,\n\t\tjumpedThisFrame: false,\n\t\tjumpCutRampTimer: 0,\n\t\tverticalVelocity: 0,\n\t};\n}\n","/**\n * Platformer 3D Finite State Machine\n * \n * Manages state transitions for 3D platformer movement:\n * - Idle: Standing still on ground\n * - Walking: Moving on ground (slow)\n * - Running: Moving on ground (fast)\n * - Jumping: Ascending from jump\n * - Falling: Descending (gravity)\n * - Landing: Just touched ground\n */\n\nimport { SyncStateMachine, t } from '../../core/utility/sync-state-machine';\nimport type { Platformer3DInputComponent, Platformer3DStateComponent } from './components';\n\n/**\n * Simplified Collision Context interface\n */\nexport interface PlatformerCollisionContext {\n\tcontact: {\n\t\tnormal: { x: number; y: number; z?: number };\n\t};\n}\n\n/**\n * Platformer states\n */\nexport enum Platformer3DState {\n\tIdle = 'idle',\n\tWalking = 'walking',\n\tRunning = 'running',\n\tJumping = 'jumping',\n\tFalling = 'falling',\n\tLanding = 'landing',\n}\n\n/**\n * Events that trigger state transitions\n */\nexport enum Platformer3DEvent {\n\tWalk = 'walk',\n\tRun = 'run',\n\tJump = 'jump',\n\tFall = 'fall',\n\tLand = 'land',\n\tStop = 'stop',\n}\n\n/**\n * Context for the FSM\n */\nexport interface Platformer3DContext {\n\tinput: Platformer3DInputComponent;\n\tstate: Platformer3DStateComponent;\n}\n\n/**\n * Platformer 3D FSM\n */\nexport class Platformer3DFSM {\n\tmachine: SyncStateMachine<Platformer3DState, Platformer3DEvent, never>;\n\n\tconstructor(private ctx: Platformer3DContext) {\n\t\tthis.machine = new SyncStateMachine<Platformer3DState, Platformer3DEvent, never>(\n\t\t\tPlatformer3DState.Idle,\n\t\t\t[\n\t\t\t\t// Idle transitions\n\t\t\t\tt(Platformer3DState.Idle, Platformer3DEvent.Walk, Platformer3DState.Walking),\n\t\t\t\tt(Platformer3DState.Idle, Platformer3DEvent.Run, Platformer3DState.Running),\n\t\t\t\tt(Platformer3DState.Idle, Platformer3DEvent.Jump, Platformer3DState.Jumping),\n\t\t\t\tt(Platformer3DState.Idle, Platformer3DEvent.Fall, Platformer3DState.Falling),\n\t\t\t\t\n\t\t\t\t// Walking transitions\n\t\t\t\tt(Platformer3DState.Walking, Platformer3DEvent.Run, Platformer3DState.Running),\n\t\t\t\tt(Platformer3DState.Walking, Platformer3DEvent.Jump, Platformer3DState.Jumping),\n\t\t\t\tt(Platformer3DState.Walking, Platformer3DEvent.Stop, Platformer3DState.Idle),\n\t\t\t\tt(Platformer3DState.Walking, Platformer3DEvent.Fall, Platformer3DState.Falling),\n\t\t\t\t\n\t\t\t\t// Running transitions\n\t\t\t\tt(Platformer3DState.Running, Platformer3DEvent.Walk, Platformer3DState.Walking),\n\t\t\t\tt(Platformer3DState.Running, Platformer3DEvent.Jump, Platformer3DState.Jumping),\n\t\t\t\tt(Platformer3DState.Running, Platformer3DEvent.Stop, Platformer3DState.Idle),\n\t\t\t\tt(Platformer3DState.Running, Platformer3DEvent.Fall, Platformer3DState.Falling),\n\t\t\t\t\n\t\t\t\t// Jumping transitions\n\t\t\t\tt(Platformer3DState.Jumping, Platformer3DEvent.Fall, Platformer3DState.Falling),\n\t\t\t\tt(Platformer3DState.Jumping, Platformer3DEvent.Land, Platformer3DState.Landing),\n\t\t\t\tt(Platformer3DState.Jumping, Platformer3DEvent.Jump, Platformer3DState.Jumping), // Multi-jump\n\t\t\t\t\n\t\t\t\t// Falling transitions\n\t\t\t\tt(Platformer3DState.Falling, Platformer3DEvent.Land, Platformer3DState.Landing),\n\t\t\t\t\n\t\t\t\t// Landing transitions\n\t\t\t\tt(Platformer3DState.Landing, Platformer3DEvent.Walk, Platformer3DState.Walking),\n\t\t\t\tt(Platformer3DState.Landing, Platformer3DEvent.Run, Platformer3DState.Running),\n\t\t\t\tt(Platformer3DState.Landing, Platformer3DEvent.Stop, Platformer3DState.Idle),\n\t\t\t\t\n\t\t\t\t// Self-transitions (no-ops)\n\t\t\t\tt(Platformer3DState.Idle, Platformer3DEvent.Stop, Platformer3DState.Idle),\n\t\t\t]\n\t\t);\n\t}\n\n\t/**\n\t * Get the current state\n\t */\n\tgetState(): Platformer3DState {\n\t\treturn this.machine.getState();\n\t}\n\n\t/**\n\t * Dispatch an event to the FSM\n\t */\n\tdispatch(event: Platformer3DEvent): 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 * Check if grounded\n\t */\n\tisGrounded(): boolean {\n\t\tconst state = this.getState();\n\t\treturn state === Platformer3DState.Idle || \n\t\t state === Platformer3DState.Walking || \n\t\t state === Platformer3DState.Running || \n\t\t state === Platformer3DState.Landing;\n\t}\n\n\t/**\n\t * Get current jump count from context\n\t */\n\tgetJumpCount(): number {\n\t\treturn this.ctx.state.jumpCount;\n\t}\n\n\t/**\n\t * Handle collision event to update ground state\n\t */\n\thandleCollision(ctx: PlatformerCollisionContext): void {\n\t\t// Check if collision is ground (normal pointing up)\n\t\t// Y normal > 0.5 means roughly within 60 degrees of up\n\t\tif (ctx.contact.normal.y > 0.5) {\n\t\t\tthis.ctx.state.collisionGrounded = true;\n\t\t\tthis.ctx.state.groundedCollisionTime = performance.now();\n\t\t}\n\t}\n\n\t/**\n\t * Update FSM based on current state\n\t */\n\tupdate(input: Platformer3DInputComponent, state: Platformer3DStateComponent): void {\n\t\t// Update context\n\t\tthis.ctx.input = input;\n\t\tthis.ctx.state = state;\n\n\t\t// Get current FSM state\n\t\tconst currentState = this.getState();\n\n\t\t// Determine movement state\n\t\tconst hasInput = Math.abs(input.moveX) > 0.1 || Math.abs(input.moveZ) > 0.1;\n\t\tconst isRunning = input.run;\n\n\t\t// Handle landing: if we're in falling state and now grounded, land\n\t\tif (currentState === Platformer3DState.Falling && state.grounded) {\n\t\t\tthis.dispatch(Platformer3DEvent.Land);\n\t\t\t// Don't return - allow immediate transition to movement state\n\t\t}\n\n\t\t// Send appropriate events based on state\n\t\tif (state.grounded) {\n\t\t\tif (hasInput) {\n\t\t\t\tif (isRunning) {\n\t\t\t\t\tthis.dispatch(Platformer3DEvent.Run);\n\t\t\t\t} else {\n\t\t\t\t\tthis.dispatch(Platformer3DEvent.Walk);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tthis.dispatch(Platformer3DEvent.Stop);\n\t\t\t}\n\t\t} else {\n\t\t\t// Not grounded - check if we should be falling\n\t\t\t// Transition to falling if: We're in jumping state and now falling, \n\t\t\t// OR we walked off a ledge (idle/walking/running and now falling)\n\t\t\tif (state.falling) {\n\t\t\t\tthis.dispatch(Platformer3DEvent.Fall);\n\t\t\t}\n\t\t}\n\n\t\t// Handle jump input (edge detection handled in behavior)\n\t\tif (input.jump && !state.jumpPressedLastFrame) {\n\t\t\tthis.dispatch(Platformer3DEvent.Jump);\n\t\t}\n\t}\n}\n","/**\n * Platformer 3D System\n *\n * Kinematic character controller (N64-style): the controller owns all motion\n * and physics is only a collision database.\n *\n * - The entity's body is converted to `KinematicPositionBased` on first\n * update; Rapier never applies forces to it.\n * - Each tick, desired displacement (horizontal input + integrated vertical\n * velocity) is run through Rapier's `KinematicCharacterController`\n * (collide-and-slide with autostep, slope limits, and snap-to-ground) and\n * the corrected movement is applied via `setNextKinematicTranslation`.\n * - Grounded comes from `controller.computedGrounded()` — no contact-pair or\n * raycast probing.\n * - Vertical velocity lives in `state.verticalVelocity` (`linvel()` is\n * meaningless on kinematic bodies). Jump buffering, coyote time,\n * multi-jump, and the jump-cut ramp all operate on that field.\n */\n\nimport { RigidBodyType, QueryFilterFlags } from '@dimforge/rapier3d-compat';\nimport type { Collider, KinematicCharacterController, RigidBody } from '@dimforge/rapier3d-compat';\nimport type { ZylemWorld } from '../../collision/world';\nimport type {\n\tPlatformer3DMovementComponent,\n\tPlatformer3DInputComponent,\n\tPlatformer3DStateComponent,\n} from './components';\n\n/**\n * Duration of the jump-cut velocity ramp in seconds.\n * Instead of halving vertical velocity in a single frame (which produces a\n * visible camera pop), we lerp toward the cut velocity across roughly 3\n * physics steps at 60 Hz.\n */\nconst JUMP_CUT_RAMP_DURATION = 0.05;\n\n/**\n * Constant downward speed applied while grounded (units/second). Keeps the\n * controller pressed into the ground so `computedGrounded()` stays engaged on\n * flat ground and going downhill (snap-to-ground handles larger drops).\n */\nconst GROUND_STICK_SPEED = 2;\n\n/** Vertical speed below which the airborne state flips from jumping to falling. */\nconst FALL_VELOCITY_THRESHOLD = -0.1;\n\nconst DEG_TO_RAD = Math.PI / 180;\n\n/**\n * Entity with platformer components\n */\nexport interface Platformer3DEntity {\n\tuuid: string;\n\tbody: RigidBody;\n\tcollider?: Collider | null;\n\tplatformer: Platformer3DMovementComponent;\n\t$platformer: Platformer3DInputComponent;\n\tplatformerState: Platformer3DStateComponent;\n}\n\n/** Scratch vector reused for the desired per-tick displacement. */\nconst _desired = { x: 0, y: 0, z: 0 };\n\n/**\n * Platformer 3D movement system.\n *\n * Core kinematic controller for 3D platformer movement. Requires a live\n * `ZylemWorld` with the raw Rapier `World` exposed on `.world` — without it\n * the system is a no-op (the WASM fast path handles that case upstream).\n */\nexport class Platformer3DSystem {\n\tprivate world: ZylemWorld | null;\n\t/** Per-entity character controllers, keyed by entity uuid. */\n\tprivate controllers = new Map<string, KinematicCharacterController>();\n\n\tconstructor(world: ZylemWorld | null) {\n\t\tthis.world = world;\n\t}\n\n\t/**\n\t * One-time per-entity physics setup: convert the body to a\n\t * position-based kinematic body and create its character controller.\n\t * Done lazily on the first update because the body isn't always\n\t * available at construction time.\n\t */\n\tprivate ensurePhysicsConfigured(entity: Platformer3DEntity): KinematicCharacterController | null {\n\t\tconst existing = this.controllers.get(entity.uuid);\n\t\tif (existing) return existing;\n\n\t\tconst physicsWorld = this.world?.world;\n\t\tif (!physicsWorld || !entity.body) return null;\n\n\t\tif (entity.body.bodyType() !== RigidBodyType.KinematicPositionBased) {\n\t\t\tentity.body.setBodyType(RigidBodyType.KinematicPositionBased, true);\n\t\t}\n\n\t\tconst movement = entity.platformer;\n\t\tconst controller = physicsWorld.createCharacterController(movement.characterOffset);\n\t\tcontroller.setUp({ x: 0, y: 1, z: 0 });\n\t\tcontroller.setMaxSlopeClimbAngle(movement.slopeClimbAngle * DEG_TO_RAD);\n\t\tcontroller.setMinSlopeSlideAngle(movement.slopeSlideAngle * DEG_TO_RAD);\n\t\tif (movement.autostep) {\n\t\t\tcontroller.enableAutostep(movement.autostep.maxHeight, movement.autostep.minWidth, true);\n\t\t} else {\n\t\t\tcontroller.disableAutostep();\n\t\t}\n\t\tcontroller.enableSnapToGround(movement.snapToGroundDistance);\n\t\tcontroller.setApplyImpulsesToDynamicBodies(true);\n\n\t\tthis.controllers.set(entity.uuid, controller);\n\t\treturn controller;\n\t}\n\n\t/**\n\t * Handle jump logic with multi-jump, coyote time, buffering, and jump-cut\n\t * ramp. All vertical motion is written to `state.verticalVelocity`.\n\t */\n\tprivate handleJump(entity: Platformer3DEntity, delta: number): void {\n\t\tconst input = entity.$platformer;\n\t\tconst movement = entity.platformer;\n\t\tconst state = entity.platformerState;\n\n\t\t// Track time since last jump (for multi-jump window)\n\t\tif (state.jumping || state.falling) {\n\t\t\tstate.timeSinceJump += delta;\n\t\t}\n\n\t\t// Track jump button release (required for multi-jump)\n\t\tif (!input.jump && state.jumpHeld) {\n\t\t\tstate.jumpReleasedSinceLastJump = true;\n\t\t}\n\n\t\t// 1. Jump Buffering - queue on new press\n\t\tif (input.jump && !state.jumpPressedLastFrame) {\n\t\t\tstate.jumpBuffered = true;\n\t\t\tstate.jumpBufferTimer = movement.jumpBufferTime;\n\t\t}\n\n\t\tstate.jumpPressedLastFrame = input.jump;\n\t\tstate.jumpHeld = input.jump;\n\n\t\t// Decrement buffer timer\n\t\tif (state.jumpBuffered) {\n\t\t\tstate.jumpBufferTimer -= delta;\n\t\t\tif (state.jumpBufferTimer <= 0) {\n\t\t\t\tstate.jumpBuffered = false;\n\t\t\t}\n\t\t}\n\n\t\t// 2. Variable Jump Height (Jump Cut)\n\t\t// Only trigger a cut if we've been jumping for a bit (prevents cutting multi-jump immediately).\n\t\tconst minTimeBeforeCut = 0.1;\n\t\tconst canApplyCut = state.timeSinceJump >= minTimeBeforeCut;\n\t\tif (!input.jump && state.jumping && !state.jumpCutApplied && canApplyCut) {\n\t\t\tstate.jumpCutApplied = true;\n\t\t\tstate.jumpCutRampTimer = JUMP_CUT_RAMP_DURATION;\n\t\t}\n\n\t\t// 2a. Ramp verticalVelocity toward verticalVelocity * jumpCutMultiplier\n\t\t// across the ramp window so the change isn't a single-frame discontinuity.\n\t\tif (state.jumpCutRampTimer > 0) {\n\t\t\tif (state.verticalVelocity > 0) {\n\t\t\t\tconst stepT = Math.min(delta / state.jumpCutRampTimer, 1);\n\t\t\t\tconst targetY = state.verticalVelocity * movement.jumpCutMultiplier;\n\t\t\t\tstate.verticalVelocity += (targetY - state.verticalVelocity) * stepT;\n\t\t\t}\n\t\t\tstate.jumpCutRampTimer = Math.max(0, state.jumpCutRampTimer - delta);\n\t\t}\n\n\t\t// Execute Jump (if buffered input exists)\n\t\tif (!state.jumpBuffered) return;\n\n\t\t// 3. Jump eligibility check\n\t\tconst inCoyoteWindow = !state.grounded && state.timeSinceGrounded <= movement.coyoteTime;\n\t\tconst isFirstJump = state.grounded || (inCoyoteWindow && state.jumpCount === 0);\n\n\t\t// Multi-jump requirements:\n\t\t// 1. Not grounded\n\t\t// 2. Have jumps remaining\n\t\t// 3. Button was released since last jump (no holding for double jump)\n\t\t// 4. Within the jump window (after multiJumpWindowTime has passed)\n\t\tconst hasJumpsRemaining = state.jumpCount < movement.maxJumps;\n\t\tconst buttonReleased = state.jumpReleasedSinceLastJump;\n\t\tconst inMultiJumpWindow = state.timeSinceJump >= movement.multiJumpWindowTime;\n\t\tconst canMultiJump = !state.grounded && hasJumpsRemaining && buttonReleased && inMultiJumpWindow;\n\n\t\tif (isFirstJump || canMultiJump) {\n\t\t\t// Consume buffered input\n\t\t\tstate.jumpBuffered = false;\n\n\t\t\t// Increment jump count and reset tracking\n\t\t\tstate.jumpCount++;\n\t\t\tstate.jumpReleasedSinceLastJump = false; // Must release again for next jump\n\t\t\tstate.timeSinceJump = 0; // Reset window timer\n\n\t\t\t// Record jump start height\n\t\t\tstate.jumpStartHeight = entity.body.translation().y;\n\n\t\t\t// Apply jump force\n\t\t\tstate.jumping = true;\n\t\t\tstate.falling = false;\n\t\t\tstate.jumpCutApplied = false;\n\t\t\tstate.jumpCutRampTimer = 0;\n\n\t\t\tstate.verticalVelocity = movement.jumpForce;\n\n\t\t\t// Flag this frame so applyGravity skips exactly one grounded\n\t\t\t// clamp/gravity step (the jump velocity is authoritative).\n\t\t\tstate.jumpedThisFrame = true;\n\t\t}\n\t}\n\n\t/**\n\t * Integrate gravity into `state.verticalVelocity`.\n\t *\n\t * While grounded (and not on the jump frame) vertical velocity is held at\n\t * a small constant downward speed so the controller stays pressed into\n\t * the ground. Skipped entirely on the frame a jump was applied — the\n\t * grounded flag is still stale-true then and would clobber the jump.\n\t */\n\tprivate applyGravity(entity: Platformer3DEntity, delta: number): void {\n\t\tconst movement = entity.platformer;\n\t\tconst state = entity.platformerState;\n\n\t\tif (state.jumpedThisFrame) {\n\t\t\tstate.jumpedThisFrame = false;\n\t\t\treturn;\n\t\t}\n\n\t\tif (state.grounded) {\n\t\t\tstate.verticalVelocity = -GROUND_STICK_SPEED;\n\t\t\treturn;\n\t\t}\n\n\t\tstate.verticalVelocity -= movement.gravity * delta;\n\t}\n\n\t/**\n\t * Update entity state from the controller's grounded result.\n\t */\n\tprivate updateState(entity: Platformer3DEntity, delta: number, computedGrounded: boolean): void {\n\t\tconst state = entity.platformerState;\n\t\tconst wasGrounded = state.grounded;\n\n\t\tconst isAirborne = state.jumping || state.falling;\n\t\tlet isGrounded: boolean;\n\t\tif (isAirborne) {\n\t\t\t// Never land while still moving upward in a jump arc.\n\t\t\tconst canLand = state.falling && !state.jumping;\n\t\t\tisGrounded = computedGrounded && canLand;\n\t\t} else {\n\t\t\tisGrounded = computedGrounded;\n\t\t}\n\n\t\tstate.grounded = isGrounded;\n\n\t\tif (state.grounded) {\n\t\t\tstate.timeSinceGrounded = 0;\n\t\t\tstate.lastGroundedY = entity.body.translation().y;\n\t\t} else {\n\t\t\tstate.timeSinceGrounded += delta;\n\t\t}\n\n\t\tif (!wasGrounded && state.grounded) {\n\t\t\tstate.jumpCount = 0;\n\t\t\tstate.jumping = false;\n\t\t\tstate.falling = false;\n\t\t\tstate.jumpCutApplied = false;\n\t\t\tstate.jumpCutRampTimer = 0;\n\t\t\tif (state.verticalVelocity < 0) {\n\t\t\t\tstate.verticalVelocity = 0;\n\t\t\t}\n\t\t}\n\n\t\tif (state.verticalVelocity < FALL_VELOCITY_THRESHOLD && !state.grounded) {\n\t\t\tif (state.jumping) {\n\t\t\t\tstate.jumping = false;\n\t\t\t\tstate.falling = true;\n\t\t\t} else if (!state.jumping) {\n\t\t\t\tstate.falling = true;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Update one platformer entity. Invoked per linked entity by the\n\t * Platformer3DBehaviorSystem adapter.\n\t */\n\tupdateEntity(entity: Platformer3DEntity, delta: number): void {\n\t\tif (!entity.platformer || !entity.$platformer || !entity.platformerState) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst controller = this.ensurePhysicsConfigured(entity);\n\t\tconst collider = entity.collider;\n\t\tif (!controller || !collider) return;\n\n\t\tconst input = entity.$platformer;\n\t\tconst movement = entity.platformer;\n\t\tconst state = entity.platformerState;\n\n\t\t// 1. Jump logic (may set verticalVelocity = jumpForce this frame)\n\t\tthis.handleJump(entity, delta);\n\n\t\t// 2. Gravity / grounded stick on verticalVelocity\n\t\tthis.applyGravity(entity, delta);\n\n\t\t// 3. Desired displacement for this tick\n\t\tconst speed = input.run ? movement.runSpeed : movement.walkSpeed;\n\t\tstate.currentSpeed = speed;\n\t\t_desired.x = input.moveX * speed * delta;\n\t\t_desired.y = state.verticalVelocity * delta;\n\t\t_desired.z = input.moveZ * speed * delta;\n\n\t\t// 4. Collide-and-slide\n\t\tcontroller.computeColliderMovement(collider, _desired, QueryFilterFlags.EXCLUDE_SENSORS);\n\t\tconst corrected = controller.computedMovement();\n\n\t\t// Head bump: upward motion was blocked, so kill the remaining ascent\n\t\t// instead of hanging under the ceiling for the rest of the arc.\n\t\tif (_desired.y > 1e-9 && corrected.y < _desired.y * 0.5) {\n\t\t\tstate.verticalVelocity = 0;\n\t\t}\n\n\t\tconst translation = entity.body.translation();\n\t\tentity.body.setNextKinematicTranslation({\n\t\t\tx: translation.x + corrected.x,\n\t\t\ty: translation.y + corrected.y,\n\t\t\tz: translation.z + corrected.z,\n\t\t});\n\n\t\t// 5. State transitions from the controller's grounded result\n\t\tthis.updateState(entity, delta, controller.computedGrounded());\n\t}\n\n\tdestroy(): void {\n\t\tconst physicsWorld = this.world?.world;\n\t\tif (physicsWorld) {\n\t\t\tfor (const controller of this.controllers.values()) {\n\t\t\t\tphysicsWorld.removeCharacterController(controller);\n\t\t\t}\n\t\t}\n\t\tthis.controllers.clear();\n\t}\n}\n","/**\n * Platformer 3D Behavior Descriptor\n * \n * Type-safe descriptor for the platformer 3D behavior system.\n * Provides entity.use() API for 3D platformer movement.\n */\n\nimport { defineBehavior } from '../behavior-descriptor';\nimport type { BehaviorEntityLink, BehaviorSystem } from '../behavior-system';\nimport { Platformer3DSystem, Platformer3DEntity } from './platformer-3d.system';\nimport type { ZylemWorld } from '../../collision/world';\nimport { Platformer3DFSM, Platformer3DState, PlatformerCollisionContext } from './platformer-3d-fsm';\nimport {\n\tcreatePlatformer3DMovementComponent,\n\tcreatePlatformer3DInputComponent,\n\tcreatePlatformer3DStateComponent,\n} from './components';\nimport type { WasmStageRuntime } from '../../runtime/wasm-stage-runtime';\n\n/**\n * Platformer behavior options (typed for entity.use() autocomplete)\n */\nexport interface Platformer3DBehaviorOptions {\n\t/** Base walking speed (default: 12) */\n\twalkSpeed?: number;\n\t/** Sprint/run speed (default: 24) */\n\trunSpeed?: number;\n\t/** Initial jump force (default: 12) */\n\tjumpForce?: number;\n\t/** Maximum number of jumps (default: 1) */\n\tmaxJumps?: number;\n\t/** Gravity force (default: 9.82) */\n\tgravity?: number;\n\t/** Maximum slope angle in degrees the character can walk up (default: 50) */\n\tslopeClimbAngle?: number;\n\t/** Slope angle in degrees past which the character slides (default: 40) */\n\tslopeSlideAngle?: number;\n\t/** Autostep over small ledges, or false to disable (default: { maxHeight: 0.3, minWidth: 0.1 }) */\n\tautostep?: { maxHeight: number; minWidth: number } | false;\n\t/** Snap-to-ground distance for downhill/step-down stability (default: 0.3) */\n\tsnapToGroundDistance?: number;\n\t/** Character controller collision offset (default: 0.05) */\n\tcharacterOffset?: number;\n}\n\nconst defaultOptions: Platformer3DBehaviorOptions = {\n\twalkSpeed: 12,\n\trunSpeed: 24,\n\tjumpForce: 12,\n\tmaxJumps: 1,\n\tgravity: 9.82,\n\tslopeClimbAngle: 50,\n\tslopeSlideAngle: 40,\n\tautostep: { maxHeight: 0.3, minWidth: 0.1 },\n\tsnapToGroundDistance: 0.3,\n\tcharacterOffset: 0.05,\n};\n\nconst PLATFORMER_BEHAVIOR_KEY = Symbol.for('zylem:behavior:platformer-3d');\n\n/**\n * Adapter that wraps Platformer3DSystem as a BehaviorSystem\n */\nclass Platformer3DBehaviorSystem implements BehaviorSystem {\n\tprivate movementBehavior: Platformer3DSystem;\n\tprivate attachedRuntimeSlots = new Set<number>();\n\n\tconstructor(\n\t\tworld: ZylemWorld | null,\n\t\tprivate wasmStage: WasmStageRuntime | null,\n\t\tprivate getBehaviorLinks?: (key: symbol) => Iterable<BehaviorEntityLink>,\n\t) {\n\t\tthis.movementBehavior = new Platformer3DSystem(world);\n\t}\n\n\tupdate(_ecs: unknown, delta: number): void {\n\t\tconst links = this.getBehaviorLinks?.(PLATFORMER_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 platformerRef = link.ref as any;\n\n\t\t\tconst options = platformerRef.options as Platformer3DBehaviorOptions;\n\n\t\t\tif (!gameEntity.platformer) {\n\t\t\t\tgameEntity.platformer = createPlatformer3DMovementComponent(options);\n\t\t\t}\n\t\t\tif (!gameEntity.$platformer) {\n\t\t\t\tgameEntity.$platformer = createPlatformer3DInputComponent();\n\t\t\t}\n\t\t\tif (!gameEntity.platformerState) {\n\t\t\t\tgameEntity.platformerState = createPlatformer3DStateComponent();\n\t\t\t}\n\n\t\t\tif (!platformerRef.fsm && gameEntity.$platformer && gameEntity.platformerState) {\n\t\t\t\tplatformerRef.fsm = new Platformer3DFSM({\n\t\t\t\t\tinput: gameEntity.$platformer,\n\t\t\t\t\tstate: gameEntity.platformerState,\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (platformerRef.fsm && gameEntity.$platformer && gameEntity.platformerState) {\n\t\t\t\tplatformerRef.fsm.update(gameEntity.$platformer, gameEntity.platformerState);\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.$platformer;\n\t\t\t\tif (input) {\n\t\t\t\t\tthis.wasmStage.setPlatformer3DInputAxes(handle, input.moveX ?? 0, input.moveZ ?? 0);\n\t\t\t\t\tthis.wasmStage.setPlatformer3DInputButtons(handle, !!input.jump, !!input.run);\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\tthis.movementBehavior.updateEntity(gameEntity, delta);\n\t\t}\n\t}\n\n\tprivate ensureWasmAttached(handle: number, options: Platformer3DBehaviorOptions): void {\n\t\tif (!this.wasmStage || this.attachedRuntimeSlots.has(handle)) return;\n\t\tthis.wasmStage.attachPlatformer3D(handle, {\n\t\t\thalfHeight: 0.9,\n\t\t\tradius: 0.4,\n\t\t\twalkSpeed: options.walkSpeed ?? defaultOptions.walkSpeed!,\n\t\t\trunSpeed: options.runSpeed ?? defaultOptions.runSpeed!,\n\t\t\tjumpForce: options.jumpForce ?? defaultOptions.jumpForce!,\n\t\t\tmaxJumps: options.maxJumps ?? defaultOptions.maxJumps!,\n\t\t\tgravity: options.gravity ?? defaultOptions.gravity!,\n\t\t});\n\t\tthis.attachedRuntimeSlots.add(handle);\n\t}\n\n\tdestroy(_ecs: unknown): void {\n\t\tthis.movementBehavior.destroy();\n\t\tthis.attachedRuntimeSlots.clear();\n\t}\n}\n\n/**\n * Platformer3DBehavior - typed descriptor for 3D platformer movement\n * \n * Provides complete 3D platformer physics including:\n * - Walking and running\n * - Jumping with multi-jump support\n * - Gravity and falling\n * - Ground detection\n * \n * @example\n * ```typescript\n * import { Platformer3DBehavior } from \"@zylem/game-lib/behavior\";\n * \n * const player = await playgroundActor('player');\n * const platformer = player.use(Platformer3DBehavior, {\n * walkSpeed: 12,\n * runSpeed: 24,\n * jumpForce: 12,\n * maxJumps: 2, // Double jump!\n * });\n * \n * // In update loop\n * player.onUpdate(({ inputs }) => {\n * player.$platformer.moveX = inputs.p1.axes.Horizontal.value;\n * player.$platformer.moveZ = inputs.p1.axes.Vertical.value;\n * player.$platformer.jump = inputs.p1.buttons.A.held > 0;\n * player.$platformer.run = inputs.p1.shoulders.LTrigger.held > 0;\n * \n * const state = platformer.getState();\n * const grounded = platformer.isGrounded();\n * });\n * ```\n */\nexport const Platformer3DBehavior = defineBehavior<\n\tPlatformer3DBehaviorOptions,\n\t{\n\t\tgetState: () => Platformer3DState;\n\t\tisGrounded: () => boolean;\n\t\tgetJumpCount: () => number;\n\t\tonPlatformCollision: (ctx: PlatformerCollisionContext) => void;\n\t},\n\tPlatformer3DEntity\n>({\n\tname: 'platformer-3d',\n\tdefaultOptions,\n\tsystemFactory: (ctx) =>\n\t\tnew Platformer3DBehaviorSystem(\n\t\t\tctx.world ?? null,\n\t\t\tctx.wasmStage ?? null,\n\t\t\tctx.getBehaviorLinks,\n\t\t),\n\tcreateHandle: (ref) => ({\n\t\tgetState: () => ref.fsm?.getState() ?? Platformer3DState.Idle,\n\t\tisGrounded: () => ref.fsm?.isGrounded() ?? false,\n\t\tgetJumpCount: () => ref.fsm?.getJumpCount() ?? 0,\n\t\tonPlatformCollision: (ctx) => ref.fsm?.handleCollision(ctx),\n\t}),\n});\n"],"mappings":"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","names":["Platformer3DState","Platformer3DEvent"]}
@@ -1,10 +1,11 @@
1
1
  import {
2
+ fireShooter2DIfRequested,
2
3
  getEntityFacingAngle2D,
3
4
  resolveAim2D
4
- } from "./chunk-6C6NXNEW.js";
5
+ } from "./chunk-UL6Q5HDP.js";
5
6
  import {
6
7
  createThrusterInputComponent
7
- } from "./chunk-KS72RZHX.js";
8
+ } from "./chunk-IZJFUAXB.js";
8
9
 
9
10
  // src/lib/coordinators/multidirectional-space-shooter.coordinator.ts
10
11
  var MultidirectionalSpaceShooterCoordinator = class {
@@ -32,25 +33,11 @@ var MultidirectionalSpaceShooterCoordinator = class {
32
33
  if (aim.hasAimInput) {
33
34
  this.entity.setRotationZ(aim.angle);
34
35
  }
35
- if (!input.shootPressed && !input.shootHeld) {
36
- return;
37
- }
38
- const sourcePosition = this.entity.body?.translation();
39
- if (!sourcePosition) {
40
- return;
41
- }
42
- void this.shooter.fire({
43
- source: this.entity,
44
- stage: this.stage,
45
- target: {
46
- x: sourcePosition.x + aim.direction.x,
47
- y: sourcePosition.y + aim.direction.y
48
- }
49
- });
36
+ fireShooter2DIfRequested(this.entity, this.shooter, this.stage, input, aim);
50
37
  }
51
38
  };
52
39
 
53
40
  export {
54
41
  MultidirectionalSpaceShooterCoordinator
55
42
  };
56
- //# sourceMappingURL=chunk-RTRVBEIB.js.map
43
+ //# sourceMappingURL=chunk-FTDZAKZW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/coordinators/multidirectional-space-shooter.coordinator.ts"],"sourcesContent":["import {\n\tcreateThrusterInputComponent,\n\ttype ThrusterInputComponent,\n} from '../behaviors/thruster/components';\nimport type {\n\tShooter2DHandle,\n\tShooter2DSourceEntity,\n\tShooter2DStageLike,\n} from '../behaviors/shooter-2d/shooter-2d.descriptor';\nimport { getEntityFacingAngle2D, resolveAim2D } from './shared/top-down-aim';\nimport {\n\tfireShooter2DIfRequested,\n\ttype Shooter2DAimFireInput,\n} from './shared/shooter-fire';\n\nexport interface MultidirectionalSpaceShooterInput extends Shooter2DAimFireInput {\n\tmoveX: number;\n\tmoveY: number;\n}\n\nexport type MultidirectionalSpaceShooterEntity = Shooter2DSourceEntity & {\n\t$thruster?: ThrusterInputComponent;\n\tsetRotationZ(angle: number): void;\n};\n\n/**\n * Coordinator that fans a single input snapshot out to the thruster behavior\n * (vector thrust), entity rotation (aim), and the Shooter2D behavior (fire\n * requests).\n */\nexport class MultidirectionalSpaceShooterCoordinator {\n\tconstructor(\n\t\tprivate entity: MultidirectionalSpaceShooterEntity,\n\t\tprivate shooter: Shooter2DHandle,\n\t\tprivate stage: Shooter2DStageLike,\n\t) {}\n\n\tpublic update(input: MultidirectionalSpaceShooterInput): void {\n\t\tif (!this.entity.$thruster) {\n\t\t\tthis.entity.$thruster = createThrusterInputComponent();\n\t\t}\n\n\t\tthis.entity.$thruster.thrustX = input.moveX;\n\t\tthis.entity.$thruster.thrustY = input.moveY;\n\t\tthis.entity.$thruster.thrust = 0;\n\t\tthis.entity.$thruster.rotate = 0;\n\n\t\tconst aim = resolveAim2D(\n\t\t\tinput.aimX,\n\t\t\tinput.aimY,\n\t\t\tgetEntityFacingAngle2D(this.entity),\n\t\t);\n\t\tif (aim.hasAimInput) {\n\t\t\tthis.entity.setRotationZ(aim.angle);\n\t\t}\n\n\t\tfireShooter2DIfRequested(this.entity, this.shooter, this.stage, input, aim);\n\t}\n}\n"],"mappings":";;;;;;;;;;AA8BO,IAAM,0CAAN,MAA8C;AAAA,EACpD,YACS,QACA,SACA,OACP;AAHO;AACA;AACA;AAAA,EACN;AAAA,EAHM;AAAA,EACA;AAAA,EACA;AAAA,EAGF,OAAO,OAAgD;AAC7D,QAAI,CAAC,KAAK,OAAO,WAAW;AAC3B,WAAK,OAAO,YAAY,6BAA6B;AAAA,IACtD;AAEA,SAAK,OAAO,UAAU,UAAU,MAAM;AACtC,SAAK,OAAO,UAAU,UAAU,MAAM;AACtC,SAAK,OAAO,UAAU,SAAS;AAC/B,SAAK,OAAO,UAAU,SAAS;AAE/B,UAAM,MAAM;AAAA,MACX,MAAM;AAAA,MACN,MAAM;AAAA,MACN,uBAAuB,KAAK,MAAM;AAAA,IACnC;AACA,QAAI,IAAI,aAAa;AACpB,WAAK,OAAO,aAAa,IAAI,KAAK;AAAA,IACnC;AAEA,6BAAyB,KAAK,QAAQ,KAAK,SAAS,KAAK,OAAO,OAAO,GAAG;AAAA,EAC3E;AACD;","names":[]}