@zylem/game-lib 0.6.3 → 0.6.4

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 (74) hide show
  1. package/dist/actions.d.ts +5 -5
  2. package/dist/actions.js +196 -32
  3. package/dist/actions.js.map +1 -1
  4. package/dist/behavior/jumper-2d.d.ts +114 -0
  5. package/dist/behavior/jumper-2d.js +711 -0
  6. package/dist/behavior/jumper-2d.js.map +1 -0
  7. package/dist/behavior/platformer-3d.d.ts +14 -14
  8. package/dist/behavior/platformer-3d.js +347 -104
  9. package/dist/behavior/platformer-3d.js.map +1 -1
  10. package/dist/behavior/ricochet-2d.d.ts +4 -3
  11. package/dist/behavior/ricochet-2d.js +53 -22
  12. package/dist/behavior/ricochet-2d.js.map +1 -1
  13. package/dist/behavior/ricochet-3d.d.ts +117 -0
  14. package/dist/behavior/ricochet-3d.js +443 -0
  15. package/dist/behavior/ricochet-3d.js.map +1 -0
  16. package/dist/behavior/screen-visibility.d.ts +79 -0
  17. package/dist/behavior/screen-visibility.js +358 -0
  18. package/dist/behavior/screen-visibility.js.map +1 -0
  19. package/dist/behavior/screen-wrap.d.ts +4 -3
  20. package/dist/behavior/screen-wrap.js +100 -49
  21. package/dist/behavior/screen-wrap.js.map +1 -1
  22. package/dist/behavior/shooter-2d.d.ts +79 -0
  23. package/dist/behavior/shooter-2d.js +180 -0
  24. package/dist/behavior/shooter-2d.js.map +1 -0
  25. package/dist/behavior/thruster.d.ts +5 -4
  26. package/dist/behavior/thruster.js +133 -75
  27. package/dist/behavior/thruster.js.map +1 -1
  28. package/dist/behavior/top-down-movement.d.ts +56 -0
  29. package/dist/behavior/top-down-movement.js +125 -0
  30. package/dist/behavior/top-down-movement.js.map +1 -0
  31. package/dist/behavior/world-boundary-2d.d.ts +4 -3
  32. package/dist/behavior/world-boundary-2d.js +90 -36
  33. package/dist/behavior/world-boundary-2d.js.map +1 -1
  34. package/dist/behavior/world-boundary-3d.d.ts +76 -0
  35. package/dist/behavior/world-boundary-3d.js +274 -0
  36. package/dist/behavior/world-boundary-3d.js.map +1 -0
  37. package/dist/{behavior-descriptor-BWNWmIjv.d.ts → behavior-descriptor-BXnVR8Ki.d.ts} +22 -5
  38. package/dist/{blueprints-BWGz8fII.d.ts → blueprints-DmbK2dki.d.ts} +2 -2
  39. package/dist/camera-4XO5gbQH.d.ts +905 -0
  40. package/dist/camera.d.ts +1 -1
  41. package/dist/camera.js +876 -289
  42. package/dist/camera.js.map +1 -1
  43. package/dist/{composition-DrzFrbqI.d.ts → composition-BASvMKrW.d.ts} +1 -1
  44. package/dist/{core-DAkskq6Y.d.ts → core-CARRaS55.d.ts} +57 -14
  45. package/dist/core.d.ts +9 -8
  46. package/dist/core.js +4519 -1255
  47. package/dist/core.js.map +1 -1
  48. package/dist/{entities-DC9ce_vx.d.ts → entities-ChFirVL9.d.ts} +22 -28
  49. package/dist/entities.d.ts +4 -4
  50. package/dist/entities.js +1231 -314
  51. package/dist/entities.js.map +1 -1
  52. package/dist/{entity-BpbZqg19.d.ts → entity-vj-HTjzU.d.ts} +80 -11
  53. package/dist/{global-change-Dc8uCKi2.d.ts → global-change-2JvMaz44.d.ts} +1 -1
  54. package/dist/main.d.ts +718 -19
  55. package/dist/main.js +12129 -5959
  56. package/dist/main.js.map +1 -1
  57. package/dist/physics-pose-DCc4oE44.d.ts +25 -0
  58. package/dist/physics-protocol-BDD3P5W2.d.ts +200 -0
  59. package/dist/physics-worker.d.ts +21 -0
  60. package/dist/physics-worker.js +306 -0
  61. package/dist/physics-worker.js.map +1 -0
  62. package/dist/physics.d.ts +205 -0
  63. package/dist/physics.js +577 -0
  64. package/dist/physics.js.map +1 -0
  65. package/dist/{stage-types-BFsm3qsZ.d.ts → stage-types-C19IhuzA.d.ts} +253 -89
  66. package/dist/stage.d.ts +9 -8
  67. package/dist/stage.js +3782 -1041
  68. package/dist/stage.js.map +1 -1
  69. package/dist/sync-state-machine-CZyspBpj.d.ts +16 -0
  70. package/dist/{thruster-DhRaJnoL.d.ts → thruster-23lzoPZd.d.ts} +16 -8
  71. package/dist/world-DfgxoNMt.d.ts +105 -0
  72. package/package.json +25 -1
  73. package/dist/camera-B5e4c78l.d.ts +0 -468
  74. package/dist/world-Be5m1XC1.d.ts +0 -31
@@ -10,8 +10,9 @@ function createPlatformer3DMovementComponent(options = {}) {
10
10
  coyoteTime: options.coyoteTime ?? 0.1,
11
11
  jumpBufferTime: options.jumpBufferTime ?? 0.1,
12
12
  jumpCutMultiplier: options.jumpCutMultiplier ?? 0.5,
13
- multiJumpWindowTime: options.multiJumpWindowTime ?? 0.15
13
+ multiJumpWindowTime: options.multiJumpWindowTime ?? 0.15,
14
14
  // 150ms default
15
+ debugGroundProbe: options.debugGroundProbe ?? false
15
16
  };
16
17
  }
17
18
  function createPlatformer3DInputComponent() {
@@ -44,8 +45,46 @@ function createPlatformer3DStateComponent() {
44
45
  };
45
46
  }
46
47
 
48
+ // src/lib/core/utility/sync-state-machine.ts
49
+ import {
50
+ StateMachine as BaseStateMachine
51
+ } from "typescript-fsm";
52
+ import { t } from "typescript-fsm";
53
+ var SyncStateMachine = class extends BaseStateMachine {
54
+ constructor(init, transitions = [], logger = console) {
55
+ super(init, transitions, logger);
56
+ }
57
+ dispatch(_event, ..._args) {
58
+ throw new Error("SyncStateMachine does not support async dispatch.");
59
+ }
60
+ syncDispatch(event, ...args) {
61
+ const found = this.transitions.some((transition) => {
62
+ if (transition.fromState !== this._current || transition.event !== event) {
63
+ return false;
64
+ }
65
+ const current = this._current;
66
+ this._current = transition.toState;
67
+ if (!transition.cb) {
68
+ return true;
69
+ }
70
+ try {
71
+ transition.cb(...args);
72
+ return true;
73
+ } catch (error) {
74
+ this._current = current;
75
+ this.logger.error("Exception in callback", error);
76
+ throw error;
77
+ }
78
+ });
79
+ if (!found) {
80
+ const errorMessage = this.formatErr(this._current, event);
81
+ this.logger.error(errorMessage);
82
+ }
83
+ return found;
84
+ }
85
+ };
86
+
47
87
  // src/lib/behaviors/platformer-3d/platformer-3d-fsm.ts
48
- import { StateMachine, t } from "typescript-fsm";
49
88
  var Platformer3DState = /* @__PURE__ */ ((Platformer3DState2) => {
50
89
  Platformer3DState2["Idle"] = "idle";
51
90
  Platformer3DState2["Walking"] = "walking";
@@ -67,7 +106,7 @@ var Platformer3DEvent = /* @__PURE__ */ ((Platformer3DEvent2) => {
67
106
  var Platformer3DFSM = class {
68
107
  constructor(ctx) {
69
108
  this.ctx = ctx;
70
- this.machine = new StateMachine(
109
+ this.machine = new SyncStateMachine(
71
110
  "idle" /* Idle */,
72
111
  [
73
112
  // Idle transitions
@@ -113,7 +152,7 @@ var Platformer3DFSM = class {
113
152
  */
114
153
  dispatch(event) {
115
154
  if (this.machine.can(event)) {
116
- this.machine.dispatch(event);
155
+ this.machine.syncDispatch(event);
117
156
  }
118
157
  }
119
158
  /**
@@ -171,97 +210,294 @@ var Platformer3DFSM = class {
171
210
  }
172
211
  };
173
212
 
174
- // src/lib/behaviors/platformer-3d/platformer-3d.behavior.ts
175
- import { Vector3, BufferGeometry, LineBasicMaterial, Line } from "three";
213
+ // src/lib/behaviors/shared/ground-probe-3d.ts
176
214
  import { Ray } from "@dimforge/rapier3d-compat";
177
- var Platformer3DBehavior = class {
178
- world;
179
- scene;
180
- rays = /* @__PURE__ */ new Map();
181
- debugLines = /* @__PURE__ */ new Map();
182
- // Store Line objects for debug visualization
183
- constructor(world, scene) {
215
+ import { BufferGeometry, Line, LineBasicMaterial, Vector3 } from "three";
216
+
217
+ // src/lib/physics/serialize-descriptors.ts
218
+ import { RigidBodyType } from "@dimforge/rapier3d-compat";
219
+ function serializeColliderDesc(desc) {
220
+ const internal = desc;
221
+ const customShapeData = internal.__zylemShapeData;
222
+ if (customShapeData?.shape === "trimesh") {
223
+ const result2 = {
224
+ shape: "trimesh",
225
+ dimensions: [],
226
+ vertices: [...customShapeData.vertices],
227
+ indices: [...customShapeData.indices]
228
+ };
229
+ const translation = internal.translation;
230
+ if (translation && (translation.x !== 0 || translation.y !== 0 || translation.z !== 0)) {
231
+ result2.translation = [translation.x, translation.y, translation.z];
232
+ }
233
+ if (internal.isSensor) {
234
+ result2.sensor = true;
235
+ }
236
+ if (internal.collisionGroups !== void 0 && internal.collisionGroups !== 4294967295) {
237
+ result2.collisionGroups = internal.collisionGroups;
238
+ }
239
+ if (internal.activeCollisionTypes !== void 0) {
240
+ result2.activeCollisionTypes = internal.activeCollisionTypes;
241
+ }
242
+ return result2;
243
+ }
244
+ const shapeType = internal.shape?.type ?? internal.shapeType ?? 0;
245
+ const { shape, dimensions, heightfieldMeta } = extractShapeData(shapeType, internal);
246
+ const result = {
247
+ shape,
248
+ dimensions
249
+ };
250
+ const t2 = internal.translation;
251
+ if (t2 && (t2.x !== 0 || t2.y !== 0 || t2.z !== 0)) {
252
+ result.translation = [t2.x, t2.y, t2.z];
253
+ }
254
+ if (internal.isSensor) {
255
+ result.sensor = true;
256
+ }
257
+ if (internal.collisionGroups !== void 0 && internal.collisionGroups !== 4294967295) {
258
+ result.collisionGroups = internal.collisionGroups;
259
+ }
260
+ if (internal.activeCollisionTypes !== void 0) {
261
+ result.activeCollisionTypes = internal.activeCollisionTypes;
262
+ }
263
+ if (heightfieldMeta) {
264
+ result.heightfieldMeta = heightfieldMeta;
265
+ }
266
+ return result;
267
+ }
268
+ function extractShapeData(shapeType, internal) {
269
+ switch (shapeType) {
270
+ case 0:
271
+ return {
272
+ shape: "ball",
273
+ dimensions: [internal.shape?.radius ?? internal.halfExtents?.x ?? 1]
274
+ };
275
+ case 1:
276
+ return {
277
+ shape: "cuboid",
278
+ dimensions: [
279
+ internal.shape?.halfExtents?.x ?? internal.halfExtents?.x ?? 0.5,
280
+ internal.shape?.halfExtents?.y ?? internal.halfExtents?.y ?? 0.5,
281
+ internal.shape?.halfExtents?.z ?? internal.halfExtents?.z ?? 0.5
282
+ ]
283
+ };
284
+ case 2:
285
+ return {
286
+ shape: "capsule",
287
+ dimensions: [
288
+ internal.shape?.halfHeight ?? 0.5,
289
+ internal.shape?.radius ?? 0.5
290
+ ]
291
+ };
292
+ case 6:
293
+ return {
294
+ shape: "cone",
295
+ dimensions: [
296
+ internal.shape?.halfHeight ?? 1,
297
+ internal.shape?.radius ?? 1
298
+ ]
299
+ };
300
+ case 7:
301
+ return {
302
+ shape: "cylinder",
303
+ dimensions: [
304
+ internal.shape?.halfHeight ?? 1,
305
+ internal.shape?.radius ?? 1
306
+ ]
307
+ };
308
+ case 11: {
309
+ const nrows = internal.shape?.nrows ?? 10;
310
+ const ncols = internal.shape?.ncols ?? 10;
311
+ const heights = internal.shape?.heights;
312
+ return {
313
+ shape: "heightfield",
314
+ dimensions: heights ? Array.from(heights) : [],
315
+ heightfieldMeta: { nrows, ncols }
316
+ };
317
+ }
318
+ default:
319
+ return { shape: "cuboid", dimensions: [0.5, 0.5, 0.5] };
320
+ }
321
+ }
322
+
323
+ // src/lib/behaviors/shared/ground-probe-3d.ts
324
+ var DEFAULT_OFFSETS = [
325
+ { x: 0, z: 0 },
326
+ { x: 0.4, z: 0.4 },
327
+ { x: -0.4, z: 0.4 },
328
+ { x: 0.4, z: -0.4 },
329
+ { x: -0.4, z: -0.4 }
330
+ ];
331
+ var GroundProbe3D = class {
332
+ constructor(world) {
184
333
  this.world = world;
185
- this.scene = scene;
186
334
  }
187
- /**
188
- * Detect if entity is on the ground using raycasting
189
- */
190
- /**
191
- * Detect if entity is on the ground using raycasting (multi-sample: center + 4 corners)
192
- */
193
- detectGround(entity) {
194
- if (!this.world?.world || !entity.body) return false;
335
+ rays = /* @__PURE__ */ new Map();
336
+ debugLines = /* @__PURE__ */ new Map();
337
+ probeSupport(entity, options) {
338
+ if (!this.world?.world || !entity.body) return null;
339
+ const mode = options.mode ?? "any";
340
+ const offsets = mode === "center" ? (options.offsets ?? DEFAULT_OFFSETS).slice(0, 1) : options.offsets ?? DEFAULT_OFFSETS;
195
341
  const translation = entity.body.translation();
196
- const rayLength = entity.platformer.groundRayLength;
197
- const radius = 0.4;
198
- const offsets = [
199
- { x: 0, z: 0 },
200
- { x: radius, z: radius },
201
- { x: -radius, z: radius },
202
- { x: radius, z: -radius },
203
- { x: -radius, z: -radius }
204
- ];
205
- let entityRays = this.rays.get(entity.uuid);
206
- if (!entityRays) {
207
- entityRays = offsets.map(() => new Ray({ x: 0, y: 0, z: 0 }, { x: 0, y: -1, z: 0 }));
208
- this.rays.set(entity.uuid, entityRays);
209
- }
210
- let grounded = false;
211
- offsets.forEach((offset, i) => {
212
- if (grounded) return;
213
- const ray = entityRays[i];
342
+ const rays = this.getOrCreateRays(entity.uuid, offsets.length);
343
+ const originYOffset = options.originYOffset ?? 0;
344
+ let support = null;
345
+ for (let index = 0; index < offsets.length; index++) {
346
+ const offset = offsets[index];
347
+ const ray = rays[index];
214
348
  ray.origin = {
215
349
  x: translation.x + offset.x,
216
- y: translation.y,
350
+ y: translation.y + originYOffset,
217
351
  z: translation.z + offset.z
218
352
  };
219
353
  ray.dir = { x: 0, y: -1, z: 0 };
220
354
  const hit = this.world.world.castRay(
221
355
  ray,
222
- rayLength,
356
+ options.rayLength,
223
357
  true,
224
358
  void 0,
225
359
  void 0,
226
360
  void 0,
227
- void 0,
228
- (collider) => {
229
- const ref = collider._parent?.userData?.uuid;
230
- if (ref === entity.uuid) return false;
231
- grounded = true;
232
- return true;
233
- }
361
+ entity.body
234
362
  );
235
- });
236
- if (this.scene) {
237
- this.updateDebugLines(entity, entityRays, grounded, rayLength);
363
+ if (!hit) continue;
364
+ const nextSupport = {
365
+ toi: hit.toi,
366
+ point: {
367
+ x: ray.origin.x + ray.dir.x * hit.toi,
368
+ y: ray.origin.y + ray.dir.y * hit.toi,
369
+ z: ray.origin.z + ray.dir.z * hit.toi
370
+ },
371
+ origin: {
372
+ x: ray.origin.x,
373
+ y: ray.origin.y,
374
+ z: ray.origin.z
375
+ },
376
+ rayIndex: index,
377
+ colliderUuid: hit.collider?._parent?.userData?.uuid
378
+ };
379
+ if (mode === "center") {
380
+ support = nextSupport;
381
+ break;
382
+ }
383
+ if (!support || nextSupport.toi < support.toi) {
384
+ support = nextSupport;
385
+ }
386
+ }
387
+ if (options.debug && options.scene) {
388
+ this.updateDebugLines(
389
+ entity.uuid,
390
+ rays,
391
+ Boolean(support),
392
+ options.rayLength,
393
+ options.scene
394
+ );
395
+ } else {
396
+ this.disposeDebugLines(entity.uuid);
397
+ }
398
+ return support;
399
+ }
400
+ detect(entity, options) {
401
+ return this.probeSupport(entity, options) != null;
402
+ }
403
+ destroyEntity(uuid) {
404
+ this.rays.delete(uuid);
405
+ this.disposeDebugLines(uuid);
406
+ }
407
+ destroy() {
408
+ this.rays.clear();
409
+ for (const uuid of this.debugLines.keys()) {
410
+ this.disposeDebugLines(uuid);
238
411
  }
239
- return grounded;
412
+ this.debugLines.clear();
240
413
  }
241
- updateDebugLines(entity, rays, hasGround, length) {
242
- let lines = this.debugLines.get(entity.uuid);
414
+ getOrCreateRays(uuid, count) {
415
+ let rays = this.rays.get(uuid);
416
+ if (!rays || rays.length !== count) {
417
+ rays = Array.from(
418
+ { length: count },
419
+ () => new Ray({ x: 0, y: 0, z: 0 }, { x: 0, y: -1, z: 0 })
420
+ );
421
+ this.rays.set(uuid, rays);
422
+ }
423
+ return rays;
424
+ }
425
+ updateDebugLines(uuid, rays, hasGround, length, scene) {
426
+ let lines = this.debugLines.get(uuid);
243
427
  if (!lines) {
244
428
  lines = rays.map(() => {
245
- const geometry = new BufferGeometry().setFromPoints([new Vector3(), new Vector3()]);
429
+ const geometry = new BufferGeometry().setFromPoints([
430
+ new Vector3(),
431
+ new Vector3()
432
+ ]);
246
433
  const material = new LineBasicMaterial({ color: 16711680 });
247
434
  const line = new Line(geometry, material);
248
- this.scene.add(line);
435
+ scene.add(line);
249
436
  return line;
250
437
  });
251
- this.debugLines.set(entity.uuid, lines);
438
+ this.debugLines.set(uuid, lines);
252
439
  }
253
- rays.forEach((ray, i) => {
254
- const line = lines[i];
440
+ rays.forEach((ray, index) => {
441
+ const line = lines[index];
255
442
  const start = new Vector3(ray.origin.x, ray.origin.y, ray.origin.z);
256
443
  const end = new Vector3(
257
444
  ray.origin.x + ray.dir.x * length,
258
445
  ray.origin.y + ray.dir.y * length,
259
446
  ray.origin.z + ray.dir.z * length
260
447
  );
448
+ line.visible = true;
261
449
  line.geometry.setFromPoints([start, end]);
262
- line.material.color.setHex(hasGround ? 65280 : 16711680);
450
+ line.material.color.setHex(
451
+ hasGround ? 65280 : 16711680
452
+ );
263
453
  });
264
454
  }
455
+ disposeDebugLines(uuid) {
456
+ const lines = this.debugLines.get(uuid);
457
+ if (!lines) return;
458
+ for (const line of lines) {
459
+ line.removeFromParent();
460
+ line.geometry.dispose();
461
+ line.material.dispose();
462
+ }
463
+ this.debugLines.delete(uuid);
464
+ }
465
+ };
466
+ function getGroundAnchorOffsetY(entity) {
467
+ const runtimeColliderDesc = entity?.colliderDesc;
468
+ if (runtimeColliderDesc) {
469
+ const serialized = serializeColliderDesc(runtimeColliderDesc);
470
+ const centerY2 = serialized.translation?.[1] ?? 0;
471
+ if (serialized.shape === "capsule" && serialized.dimensions.length >= 2) {
472
+ const halfCylinder = serialized.dimensions[0] ?? 0;
473
+ const radius = serialized.dimensions[1] ?? 0;
474
+ return halfCylinder + radius - centerY2;
475
+ }
476
+ if (serialized.shape === "cuboid" && serialized.dimensions.length >= 2) {
477
+ const halfHeight = serialized.dimensions[1] ?? 0;
478
+ return halfHeight - centerY2;
479
+ }
480
+ }
481
+ const collisionSize = entity?.options?.collision?.size ?? entity?.options?.collisionSize ?? entity?.options?.size;
482
+ const height = collisionSize?.y ?? 0;
483
+ if (height <= 0) {
484
+ return 0;
485
+ }
486
+ const collisionPosition = entity?.options?.collision?.position ?? entity?.options?.collisionPosition;
487
+ const centerY = collisionPosition?.y ?? height / 2;
488
+ return height / 2 - centerY;
489
+ }
490
+
491
+ // src/lib/behaviors/platformer-3d/platformer-3d.behavior.ts
492
+ var Platformer3DBehavior = class {
493
+ world;
494
+ scene;
495
+ groundProbe;
496
+ constructor(world, scene) {
497
+ this.world = world;
498
+ this.scene = scene;
499
+ this.groundProbe = new GroundProbe3D(world);
500
+ }
265
501
  /**
266
502
  * Apply horizontal movement based on input
267
503
  */
@@ -322,22 +558,7 @@ var Platformer3DBehavior = class {
322
558
  const buttonReleased = state.jumpReleasedSinceLastJump;
323
559
  const inMultiJumpWindow = state.timeSinceJump >= movement.multiJumpWindowTime;
324
560
  const canMultiJump = !state.grounded && hasJumpsRemaining && buttonReleased && inMultiJumpWindow;
325
- console.log("[JUMP DEBUG] Attempting jump:", {
326
- grounded: state.grounded,
327
- jumpCount: state.jumpCount,
328
- maxJumps: movement.maxJumps,
329
- isFirstJump,
330
- canMultiJump,
331
- "--- Multi-jump conditions ---": "",
332
- "!grounded": !state.grounded,
333
- hasJumpsRemaining,
334
- buttonReleased,
335
- inMultiJumpWindow,
336
- timeSinceJump: state.timeSinceJump.toFixed(3),
337
- multiJumpWindowTime: movement.multiJumpWindowTime
338
- });
339
561
  if (isFirstJump || canMultiJump) {
340
- console.log("[JUMP DEBUG] \u2705 EXECUTING JUMP #" + (state.jumpCount + 1));
341
562
  state.jumpBuffered = false;
342
563
  state.jumpCount++;
343
564
  state.jumpReleasedSinceLastJump = false;
@@ -348,8 +569,6 @@ var Platformer3DBehavior = class {
348
569
  state.jumpCutApplied = false;
349
570
  entity.transformStore.velocity.y = movement.jumpForce;
350
571
  entity.transformStore.dirty.velocity = true;
351
- } else {
352
- console.log("[JUMP DEBUG] \u274C JUMP BLOCKED - conditions not met");
353
572
  }
354
573
  }
355
574
  /**
@@ -377,12 +596,26 @@ var Platformer3DBehavior = class {
377
596
  let isGrounded = false;
378
597
  const isAirborne = state.jumping || state.falling;
379
598
  if (isAirborne) {
380
- const nearGround = this.detectGround(entity);
599
+ const probeOriginYOffset = 0.05 - getGroundAnchorOffsetY(entity);
600
+ const nearGround = this.groundProbe.detect(entity, {
601
+ rayLength: entity.platformer.groundRayLength,
602
+ mode: "any",
603
+ debug: entity.platformer.debugGroundProbe,
604
+ scene: this.scene,
605
+ originYOffset: probeOriginYOffset
606
+ });
381
607
  const canLand = state.falling && !state.jumping;
382
608
  const hasLanded = Math.abs(velocity.y) < 0.5;
383
609
  isGrounded = nearGround && canLand && hasLanded;
384
610
  } else {
385
- const nearGround = this.detectGround(entity);
611
+ const probeOriginYOffset = 0.05 - getGroundAnchorOffsetY(entity);
612
+ const nearGround = this.groundProbe.detect(entity, {
613
+ rayLength: entity.platformer.groundRayLength,
614
+ mode: "any",
615
+ debug: entity.platformer.debugGroundProbe,
616
+ scene: this.scene,
617
+ originYOffset: probeOriginYOffset
618
+ });
386
619
  const notFallingFast = velocity.y > -2;
387
620
  isGrounded = nearGround && notFallingFast;
388
621
  }
@@ -409,26 +642,32 @@ var Platformer3DBehavior = class {
409
642
  }
410
643
  }
411
644
  /**
412
- * Update all platformer entities
645
+ * Update one platformer entity.
646
+ */
647
+ updateEntity(entity, delta) {
648
+ if (!entity.platformer || !entity.$platformer || !entity.platformerState) {
649
+ return;
650
+ }
651
+ const platformerEntity = entity;
652
+ this.updateState(platformerEntity, delta);
653
+ this.applyMovement(platformerEntity, delta);
654
+ this.handleJump(platformerEntity, delta);
655
+ this.applyGravity(platformerEntity, delta);
656
+ }
657
+ /**
658
+ * Update all platformer entities.
413
659
  */
414
660
  update(delta) {
415
661
  if (!this.world?.collisionMap) return;
416
662
  for (const [, entity] of this.world.collisionMap) {
417
- const platformerEntity = entity;
418
- if (!platformerEntity.platformer || !platformerEntity.$platformer || !platformerEntity.platformerState) {
419
- continue;
420
- }
421
- this.updateState(platformerEntity, delta);
422
- this.applyMovement(platformerEntity, delta);
423
- this.handleJump(platformerEntity, delta);
424
- this.applyGravity(platformerEntity, delta);
663
+ this.updateEntity(entity, delta);
425
664
  }
426
665
  }
427
666
  /**
428
667
  * Cleanup
429
668
  */
430
669
  destroy() {
431
- this.rays.clear();
670
+ this.groundProbe.destroy();
432
671
  }
433
672
  };
434
673
 
@@ -449,25 +688,25 @@ var defaultOptions = {
449
688
  jumpForce: 12,
450
689
  maxJumps: 1,
451
690
  gravity: 9.82,
452
- groundRayLength: 1
691
+ groundRayLength: 1,
692
+ debugGroundProbe: false
453
693
  };
694
+ var PLATFORMER_BEHAVIOR_KEY = /* @__PURE__ */ Symbol.for("zylem:behavior:platformer-3d");
454
695
  var Platformer3DBehaviorSystem = class {
455
- constructor(world, scene) {
696
+ constructor(world, scene, getBehaviorLinks) {
456
697
  this.world = world;
457
698
  this.scene = scene;
699
+ this.getBehaviorLinks = getBehaviorLinks;
458
700
  this.movementBehavior = new Platformer3DBehavior(world, scene);
459
701
  }
460
702
  movementBehavior;
461
- update(ecs, delta) {
462
- if (!this.world?.collisionMap) return;
463
- for (const [, entity] of this.world.collisionMap) {
464
- const gameEntity = entity;
465
- if (typeof gameEntity.getBehaviorRefs !== "function") continue;
466
- const refs = gameEntity.getBehaviorRefs();
467
- const platformerRef = refs.find(
468
- (r) => r.descriptor.key === /* @__PURE__ */ Symbol.for("zylem:behavior:platformer-3d")
469
- );
470
- if (!platformerRef || !gameEntity.body) continue;
703
+ update(_ecs, delta) {
704
+ const links = this.getBehaviorLinks?.(PLATFORMER_BEHAVIOR_KEY);
705
+ if (!links) return;
706
+ for (const link of links) {
707
+ const gameEntity = link.entity;
708
+ const platformerRef = link.ref;
709
+ if (!gameEntity.body) continue;
471
710
  const options = platformerRef.options;
472
711
  if (!gameEntity.platformer) {
473
712
  gameEntity.platformer = createPlatformer3DMovementComponent(options);
@@ -487,8 +726,8 @@ var Platformer3DBehaviorSystem = class {
487
726
  if (platformerRef.fsm && gameEntity.$platformer && gameEntity.platformerState) {
488
727
  platformerRef.fsm.update(gameEntity.$platformer, gameEntity.platformerState);
489
728
  }
729
+ this.movementBehavior.updateEntity(gameEntity, delta);
490
730
  }
491
- this.movementBehavior.update(delta);
492
731
  }
493
732
  destroy(_ecs) {
494
733
  this.movementBehavior.destroy();
@@ -497,7 +736,11 @@ var Platformer3DBehaviorSystem = class {
497
736
  var Platformer3DBehavior2 = defineBehavior({
498
737
  name: "platformer-3d",
499
738
  defaultOptions,
500
- systemFactory: (ctx) => new Platformer3DBehaviorSystem(ctx.world, ctx.scene),
739
+ systemFactory: (ctx) => new Platformer3DBehaviorSystem(
740
+ ctx.world,
741
+ ctx.scene,
742
+ ctx.getBehaviorLinks
743
+ ),
501
744
  createHandle: (ref) => ({
502
745
  getState: () => ref.fsm?.getState() ?? "idle" /* Idle */,
503
746
  isGrounded: () => ref.fsm?.isGrounded() ?? false,