@zylem/game-lib 0.6.2 → 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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -16
  3. package/dist/actions.d.ts +30 -21
  4. package/dist/actions.js +793 -146
  5. package/dist/actions.js.map +1 -1
  6. package/dist/behavior/jumper-2d.d.ts +114 -0
  7. package/dist/behavior/jumper-2d.js +711 -0
  8. package/dist/behavior/jumper-2d.js.map +1 -0
  9. package/dist/behavior/platformer-3d.d.ts +296 -0
  10. package/dist/behavior/platformer-3d.js +761 -0
  11. package/dist/behavior/platformer-3d.js.map +1 -0
  12. package/dist/behavior/ricochet-2d.d.ts +275 -0
  13. package/dist/behavior/ricochet-2d.js +425 -0
  14. package/dist/behavior/ricochet-2d.js.map +1 -0
  15. package/dist/behavior/ricochet-3d.d.ts +117 -0
  16. package/dist/behavior/ricochet-3d.js +443 -0
  17. package/dist/behavior/ricochet-3d.js.map +1 -0
  18. package/dist/behavior/screen-visibility.d.ts +79 -0
  19. package/dist/behavior/screen-visibility.js +358 -0
  20. package/dist/behavior/screen-visibility.js.map +1 -0
  21. package/dist/behavior/screen-wrap.d.ts +87 -0
  22. package/dist/behavior/screen-wrap.js +246 -0
  23. package/dist/behavior/screen-wrap.js.map +1 -0
  24. package/dist/behavior/shooter-2d.d.ts +79 -0
  25. package/dist/behavior/shooter-2d.js +180 -0
  26. package/dist/behavior/shooter-2d.js.map +1 -0
  27. package/dist/behavior/thruster.d.ts +11 -0
  28. package/dist/behavior/thruster.js +292 -0
  29. package/dist/behavior/thruster.js.map +1 -0
  30. package/dist/behavior/top-down-movement.d.ts +56 -0
  31. package/dist/behavior/top-down-movement.js +125 -0
  32. package/dist/behavior/top-down-movement.js.map +1 -0
  33. package/dist/behavior/world-boundary-2d.d.ts +142 -0
  34. package/dist/behavior/world-boundary-2d.js +235 -0
  35. package/dist/behavior/world-boundary-2d.js.map +1 -0
  36. package/dist/behavior/world-boundary-3d.d.ts +76 -0
  37. package/dist/behavior/world-boundary-3d.js +274 -0
  38. package/dist/behavior/world-boundary-3d.js.map +1 -0
  39. package/dist/behavior-descriptor-BXnVR8Ki.d.ts +159 -0
  40. package/dist/{blueprints-Cq3Ko6_G.d.ts → blueprints-DmbK2dki.d.ts} +2 -2
  41. package/dist/camera-4XO5gbQH.d.ts +905 -0
  42. package/dist/camera.d.ts +3 -2
  43. package/dist/camera.js +1653 -377
  44. package/dist/camera.js.map +1 -1
  45. package/dist/composition-BASvMKrW.d.ts +218 -0
  46. package/dist/{core-bO8TzV7u.d.ts → core-CARRaS55.d.ts} +110 -69
  47. package/dist/core.d.ts +11 -6
  48. package/dist/core.js +10766 -5626
  49. package/dist/core.js.map +1 -1
  50. package/dist/{entities-DvByhMGU.d.ts → entities-ChFirVL9.d.ts} +133 -29
  51. package/dist/entities.d.ts +5 -3
  52. package/dist/entities.js +4679 -3202
  53. package/dist/entities.js.map +1 -1
  54. package/dist/entity-vj-HTjzU.d.ts +1169 -0
  55. package/dist/global-change-2JvMaz44.d.ts +25 -0
  56. package/dist/main.d.ts +1118 -16
  57. package/dist/main.js +17538 -8499
  58. package/dist/main.js.map +1 -1
  59. package/dist/physics-pose-DCc4oE44.d.ts +25 -0
  60. package/dist/physics-protocol-BDD3P5W2.d.ts +200 -0
  61. package/dist/physics-worker.d.ts +21 -0
  62. package/dist/physics-worker.js +306 -0
  63. package/dist/physics-worker.js.map +1 -0
  64. package/dist/physics.d.ts +205 -0
  65. package/dist/physics.js +577 -0
  66. package/dist/physics.js.map +1 -0
  67. package/dist/stage-types-C19IhuzA.d.ts +731 -0
  68. package/dist/stage.d.ts +11 -7
  69. package/dist/stage.js +8024 -3852
  70. package/dist/stage.js.map +1 -1
  71. package/dist/sync-state-machine-CZyspBpj.d.ts +16 -0
  72. package/dist/thruster-23lzoPZd.d.ts +180 -0
  73. package/dist/world-DfgxoNMt.d.ts +105 -0
  74. package/package.json +53 -13
  75. package/dist/behaviors.d.ts +0 -854
  76. package/dist/behaviors.js +0 -1209
  77. package/dist/behaviors.js.map +0 -1
  78. package/dist/camera-CeJPAgGg.d.ts +0 -116
  79. package/dist/moveable-B_vyA6cw.d.ts +0 -67
  80. package/dist/stage-types-Bd-KtcYT.d.ts +0 -375
  81. package/dist/transformable-CUhvyuYO.d.ts +0 -67
  82. package/dist/world-C8tQ7Plj.d.ts +0 -774
@@ -0,0 +1,761 @@
1
+ // src/lib/behaviors/platformer-3d/components.ts
2
+ function createPlatformer3DMovementComponent(options = {}) {
3
+ return {
4
+ walkSpeed: options.walkSpeed ?? 12,
5
+ runSpeed: options.runSpeed ?? 24,
6
+ jumpForce: options.jumpForce ?? 12,
7
+ maxJumps: options.maxJumps ?? 1,
8
+ gravity: options.gravity ?? 9.82,
9
+ groundRayLength: options.groundRayLength ?? 1,
10
+ coyoteTime: options.coyoteTime ?? 0.1,
11
+ jumpBufferTime: options.jumpBufferTime ?? 0.1,
12
+ jumpCutMultiplier: options.jumpCutMultiplier ?? 0.5,
13
+ multiJumpWindowTime: options.multiJumpWindowTime ?? 0.15,
14
+ // 150ms default
15
+ debugGroundProbe: options.debugGroundProbe ?? false
16
+ };
17
+ }
18
+ function createPlatformer3DInputComponent() {
19
+ return {
20
+ moveX: 0,
21
+ moveZ: 0,
22
+ jump: false,
23
+ run: false
24
+ };
25
+ }
26
+ function createPlatformer3DStateComponent() {
27
+ return {
28
+ grounded: false,
29
+ jumping: false,
30
+ falling: false,
31
+ jumpCount: 0,
32
+ jumpStartHeight: 0,
33
+ currentSpeed: 0,
34
+ lastGroundedY: 0,
35
+ jumpPressedLastFrame: false,
36
+ collisionGrounded: false,
37
+ groundedCollisionTime: 0,
38
+ timeSinceGrounded: 0,
39
+ jumpBuffered: false,
40
+ jumpBufferTimer: 0,
41
+ jumpHeld: false,
42
+ jumpCutApplied: false,
43
+ jumpReleasedSinceLastJump: true,
44
+ timeSinceJump: 0
45
+ };
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
+
87
+ // src/lib/behaviors/platformer-3d/platformer-3d-fsm.ts
88
+ var Platformer3DState = /* @__PURE__ */ ((Platformer3DState2) => {
89
+ Platformer3DState2["Idle"] = "idle";
90
+ Platformer3DState2["Walking"] = "walking";
91
+ Platformer3DState2["Running"] = "running";
92
+ Platformer3DState2["Jumping"] = "jumping";
93
+ Platformer3DState2["Falling"] = "falling";
94
+ Platformer3DState2["Landing"] = "landing";
95
+ return Platformer3DState2;
96
+ })(Platformer3DState || {});
97
+ var Platformer3DEvent = /* @__PURE__ */ ((Platformer3DEvent2) => {
98
+ Platformer3DEvent2["Walk"] = "walk";
99
+ Platformer3DEvent2["Run"] = "run";
100
+ Platformer3DEvent2["Jump"] = "jump";
101
+ Platformer3DEvent2["Fall"] = "fall";
102
+ Platformer3DEvent2["Land"] = "land";
103
+ Platformer3DEvent2["Stop"] = "stop";
104
+ return Platformer3DEvent2;
105
+ })(Platformer3DEvent || {});
106
+ var Platformer3DFSM = class {
107
+ constructor(ctx) {
108
+ this.ctx = ctx;
109
+ this.machine = new SyncStateMachine(
110
+ "idle" /* Idle */,
111
+ [
112
+ // Idle transitions
113
+ t("idle" /* Idle */, "walk" /* Walk */, "walking" /* Walking */),
114
+ t("idle" /* Idle */, "run" /* Run */, "running" /* Running */),
115
+ t("idle" /* Idle */, "jump" /* Jump */, "jumping" /* Jumping */),
116
+ t("idle" /* Idle */, "fall" /* Fall */, "falling" /* Falling */),
117
+ // Walking transitions
118
+ t("walking" /* Walking */, "run" /* Run */, "running" /* Running */),
119
+ t("walking" /* Walking */, "jump" /* Jump */, "jumping" /* Jumping */),
120
+ t("walking" /* Walking */, "stop" /* Stop */, "idle" /* Idle */),
121
+ t("walking" /* Walking */, "fall" /* Fall */, "falling" /* Falling */),
122
+ // Running transitions
123
+ t("running" /* Running */, "walk" /* Walk */, "walking" /* Walking */),
124
+ t("running" /* Running */, "jump" /* Jump */, "jumping" /* Jumping */),
125
+ t("running" /* Running */, "stop" /* Stop */, "idle" /* Idle */),
126
+ t("running" /* Running */, "fall" /* Fall */, "falling" /* Falling */),
127
+ // Jumping transitions
128
+ t("jumping" /* Jumping */, "fall" /* Fall */, "falling" /* Falling */),
129
+ t("jumping" /* Jumping */, "land" /* Land */, "landing" /* Landing */),
130
+ t("jumping" /* Jumping */, "jump" /* Jump */, "jumping" /* Jumping */),
131
+ // Multi-jump
132
+ // Falling transitions
133
+ t("falling" /* Falling */, "land" /* Land */, "landing" /* Landing */),
134
+ // Landing transitions
135
+ t("landing" /* Landing */, "walk" /* Walk */, "walking" /* Walking */),
136
+ t("landing" /* Landing */, "run" /* Run */, "running" /* Running */),
137
+ t("landing" /* Landing */, "stop" /* Stop */, "idle" /* Idle */),
138
+ // Self-transitions (no-ops)
139
+ t("idle" /* Idle */, "stop" /* Stop */, "idle" /* Idle */)
140
+ ]
141
+ );
142
+ }
143
+ machine;
144
+ /**
145
+ * Get the current state
146
+ */
147
+ getState() {
148
+ return this.machine.getState();
149
+ }
150
+ /**
151
+ * Dispatch an event to the FSM
152
+ */
153
+ dispatch(event) {
154
+ if (this.machine.can(event)) {
155
+ this.machine.syncDispatch(event);
156
+ }
157
+ }
158
+ /**
159
+ * Check if grounded
160
+ */
161
+ isGrounded() {
162
+ const state = this.getState();
163
+ return state === "idle" /* Idle */ || state === "walking" /* Walking */ || state === "running" /* Running */ || state === "landing" /* Landing */;
164
+ }
165
+ /**
166
+ * Get current jump count from context
167
+ */
168
+ getJumpCount() {
169
+ return this.ctx.state.jumpCount;
170
+ }
171
+ /**
172
+ * Handle collision event to update ground state
173
+ */
174
+ handleCollision(ctx) {
175
+ if (ctx.contact.normal.y > 0.5) {
176
+ this.ctx.state.collisionGrounded = true;
177
+ this.ctx.state.groundedCollisionTime = performance.now();
178
+ }
179
+ }
180
+ /**
181
+ * Update FSM based on current state
182
+ */
183
+ update(input, state) {
184
+ this.ctx.input = input;
185
+ this.ctx.state = state;
186
+ const currentState = this.getState();
187
+ const hasInput = Math.abs(input.moveX) > 0.1 || Math.abs(input.moveZ) > 0.1;
188
+ const isRunning = input.run;
189
+ if (currentState === "falling" /* Falling */ && state.grounded) {
190
+ this.dispatch("land" /* Land */);
191
+ }
192
+ if (state.grounded) {
193
+ if (hasInput) {
194
+ if (isRunning) {
195
+ this.dispatch("run" /* Run */);
196
+ } else {
197
+ this.dispatch("walk" /* Walk */);
198
+ }
199
+ } else {
200
+ this.dispatch("stop" /* Stop */);
201
+ }
202
+ } else {
203
+ if (state.falling) {
204
+ this.dispatch("fall" /* Fall */);
205
+ }
206
+ }
207
+ if (input.jump && !state.jumpPressedLastFrame) {
208
+ this.dispatch("jump" /* Jump */);
209
+ }
210
+ }
211
+ };
212
+
213
+ // src/lib/behaviors/shared/ground-probe-3d.ts
214
+ import { Ray } from "@dimforge/rapier3d-compat";
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) {
333
+ this.world = world;
334
+ }
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;
341
+ const translation = entity.body.translation();
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];
348
+ ray.origin = {
349
+ x: translation.x + offset.x,
350
+ y: translation.y + originYOffset,
351
+ z: translation.z + offset.z
352
+ };
353
+ ray.dir = { x: 0, y: -1, z: 0 };
354
+ const hit = this.world.world.castRay(
355
+ ray,
356
+ options.rayLength,
357
+ true,
358
+ void 0,
359
+ void 0,
360
+ void 0,
361
+ entity.body
362
+ );
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);
411
+ }
412
+ this.debugLines.clear();
413
+ }
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);
427
+ if (!lines) {
428
+ lines = rays.map(() => {
429
+ const geometry = new BufferGeometry().setFromPoints([
430
+ new Vector3(),
431
+ new Vector3()
432
+ ]);
433
+ const material = new LineBasicMaterial({ color: 16711680 });
434
+ const line = new Line(geometry, material);
435
+ scene.add(line);
436
+ return line;
437
+ });
438
+ this.debugLines.set(uuid, lines);
439
+ }
440
+ rays.forEach((ray, index) => {
441
+ const line = lines[index];
442
+ const start = new Vector3(ray.origin.x, ray.origin.y, ray.origin.z);
443
+ const end = new Vector3(
444
+ ray.origin.x + ray.dir.x * length,
445
+ ray.origin.y + ray.dir.y * length,
446
+ ray.origin.z + ray.dir.z * length
447
+ );
448
+ line.visible = true;
449
+ line.geometry.setFromPoints([start, end]);
450
+ line.material.color.setHex(
451
+ hasGround ? 65280 : 16711680
452
+ );
453
+ });
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
+ }
501
+ /**
502
+ * Apply horizontal movement based on input
503
+ */
504
+ applyMovement(entity, delta) {
505
+ const input = entity.$platformer;
506
+ const movement = entity.platformer;
507
+ const state = entity.platformerState;
508
+ const speed = input.run ? movement.runSpeed : movement.walkSpeed;
509
+ state.currentSpeed = speed;
510
+ const moveX = input.moveX * speed;
511
+ const moveZ = input.moveZ * speed;
512
+ const currentVel = entity.body.linvel();
513
+ entity.transformStore.velocity.x = moveX;
514
+ entity.transformStore.velocity.y = currentVel.y;
515
+ entity.transformStore.velocity.z = moveZ;
516
+ entity.transformStore.dirty.velocity = true;
517
+ }
518
+ /**
519
+ * Handle jump logic with multi-jump support
520
+ */
521
+ /**
522
+ * Handle jump logic with multi-jump, coyote time, and buffering
523
+ */
524
+ handleJump(entity, delta) {
525
+ const input = entity.$platformer;
526
+ const movement = entity.platformer;
527
+ const state = entity.platformerState;
528
+ if (state.jumping || state.falling) {
529
+ state.timeSinceJump += delta;
530
+ }
531
+ if (!input.jump && state.jumpHeld) {
532
+ state.jumpReleasedSinceLastJump = true;
533
+ }
534
+ if (input.jump && !state.jumpPressedLastFrame) {
535
+ state.jumpBuffered = true;
536
+ state.jumpBufferTimer = movement.jumpBufferTime;
537
+ }
538
+ state.jumpPressedLastFrame = input.jump;
539
+ state.jumpHeld = input.jump;
540
+ if (state.jumpBuffered) {
541
+ state.jumpBufferTimer -= delta;
542
+ if (state.jumpBufferTimer <= 0) {
543
+ state.jumpBuffered = false;
544
+ }
545
+ }
546
+ const minTimeBeforeCut = 0.1;
547
+ const canApplyCut = state.timeSinceJump >= minTimeBeforeCut;
548
+ if (!input.jump && state.jumping && !state.jumpCutApplied && canApplyCut) {
549
+ const velocity = entity.body.linvel();
550
+ entity.transformStore.velocity.y = velocity.y * movement.jumpCutMultiplier;
551
+ entity.transformStore.dirty.velocity = true;
552
+ state.jumpCutApplied = true;
553
+ }
554
+ if (!state.jumpBuffered) return;
555
+ const inCoyoteWindow = !state.grounded && state.timeSinceGrounded <= movement.coyoteTime;
556
+ const isFirstJump = state.grounded || inCoyoteWindow && state.jumpCount === 0;
557
+ const hasJumpsRemaining = state.jumpCount < movement.maxJumps;
558
+ const buttonReleased = state.jumpReleasedSinceLastJump;
559
+ const inMultiJumpWindow = state.timeSinceJump >= movement.multiJumpWindowTime;
560
+ const canMultiJump = !state.grounded && hasJumpsRemaining && buttonReleased && inMultiJumpWindow;
561
+ if (isFirstJump || canMultiJump) {
562
+ state.jumpBuffered = false;
563
+ state.jumpCount++;
564
+ state.jumpReleasedSinceLastJump = false;
565
+ state.timeSinceJump = 0;
566
+ state.jumpStartHeight = entity.body.translation().y;
567
+ state.jumping = true;
568
+ state.falling = false;
569
+ state.jumpCutApplied = false;
570
+ entity.transformStore.velocity.y = movement.jumpForce;
571
+ entity.transformStore.dirty.velocity = true;
572
+ }
573
+ }
574
+ /**
575
+ * Apply gravity when not grounded
576
+ */
577
+ applyGravity(entity, delta) {
578
+ const movement = entity.platformer;
579
+ const state = entity.platformerState;
580
+ if (state.grounded) return;
581
+ if (state.jumping && state.timeSinceJump < 0.01) {
582
+ return;
583
+ }
584
+ const currentVel = entity.body.linvel();
585
+ const newYVelocity = currentVel.y - movement.gravity * delta;
586
+ entity.transformStore.velocity.y = newYVelocity;
587
+ entity.transformStore.dirty.velocity = true;
588
+ }
589
+ /**
590
+ * Update entity state based on physics
591
+ */
592
+ updateState(entity, delta) {
593
+ const state = entity.platformerState;
594
+ const wasGrounded = state.grounded;
595
+ const velocity = entity.body.linvel();
596
+ let isGrounded = false;
597
+ const isAirborne = state.jumping || state.falling;
598
+ if (isAirborne) {
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
+ });
607
+ const canLand = state.falling && !state.jumping;
608
+ const hasLanded = Math.abs(velocity.y) < 0.5;
609
+ isGrounded = nearGround && canLand && hasLanded;
610
+ } else {
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
+ });
619
+ const notFallingFast = velocity.y > -2;
620
+ isGrounded = nearGround && notFallingFast;
621
+ }
622
+ state.grounded = isGrounded;
623
+ if (state.grounded) {
624
+ state.timeSinceGrounded = 0;
625
+ state.lastGroundedY = entity.body.translation().y;
626
+ } else {
627
+ state.timeSinceGrounded += delta;
628
+ }
629
+ if (!wasGrounded && state.grounded) {
630
+ state.jumpCount = 0;
631
+ state.jumping = false;
632
+ state.falling = false;
633
+ state.jumpCutApplied = false;
634
+ }
635
+ if (velocity.y < -0.1 && !state.grounded) {
636
+ if (state.jumping && velocity.y < 0) {
637
+ state.jumping = false;
638
+ state.falling = true;
639
+ } else if (!state.jumping) {
640
+ state.falling = true;
641
+ }
642
+ }
643
+ }
644
+ /**
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.
659
+ */
660
+ update(delta) {
661
+ if (!this.world?.collisionMap) return;
662
+ for (const [, entity] of this.world.collisionMap) {
663
+ this.updateEntity(entity, delta);
664
+ }
665
+ }
666
+ /**
667
+ * Cleanup
668
+ */
669
+ destroy() {
670
+ this.groundProbe.destroy();
671
+ }
672
+ };
673
+
674
+ // src/lib/behaviors/behavior-descriptor.ts
675
+ function defineBehavior(config) {
676
+ return {
677
+ key: /* @__PURE__ */ Symbol.for(`zylem:behavior:${config.name}`),
678
+ defaultOptions: config.defaultOptions,
679
+ systemFactory: config.systemFactory,
680
+ createHandle: config.createHandle
681
+ };
682
+ }
683
+
684
+ // src/lib/behaviors/platformer-3d/platformer-3d.descriptor.ts
685
+ var defaultOptions = {
686
+ walkSpeed: 12,
687
+ runSpeed: 24,
688
+ jumpForce: 12,
689
+ maxJumps: 1,
690
+ gravity: 9.82,
691
+ groundRayLength: 1,
692
+ debugGroundProbe: false
693
+ };
694
+ var PLATFORMER_BEHAVIOR_KEY = /* @__PURE__ */ Symbol.for("zylem:behavior:platformer-3d");
695
+ var Platformer3DBehaviorSystem = class {
696
+ constructor(world, scene, getBehaviorLinks) {
697
+ this.world = world;
698
+ this.scene = scene;
699
+ this.getBehaviorLinks = getBehaviorLinks;
700
+ this.movementBehavior = new Platformer3DBehavior(world, scene);
701
+ }
702
+ movementBehavior;
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;
710
+ const options = platformerRef.options;
711
+ if (!gameEntity.platformer) {
712
+ gameEntity.platformer = createPlatformer3DMovementComponent(options);
713
+ }
714
+ if (!gameEntity.$platformer) {
715
+ gameEntity.$platformer = createPlatformer3DInputComponent();
716
+ }
717
+ if (!gameEntity.platformerState) {
718
+ gameEntity.platformerState = createPlatformer3DStateComponent();
719
+ }
720
+ if (!platformerRef.fsm && gameEntity.$platformer && gameEntity.platformerState) {
721
+ platformerRef.fsm = new Platformer3DFSM({
722
+ input: gameEntity.$platformer,
723
+ state: gameEntity.platformerState
724
+ });
725
+ }
726
+ if (platformerRef.fsm && gameEntity.$platformer && gameEntity.platformerState) {
727
+ platformerRef.fsm.update(gameEntity.$platformer, gameEntity.platformerState);
728
+ }
729
+ this.movementBehavior.updateEntity(gameEntity, delta);
730
+ }
731
+ }
732
+ destroy(_ecs) {
733
+ this.movementBehavior.destroy();
734
+ }
735
+ };
736
+ var Platformer3DBehavior2 = defineBehavior({
737
+ name: "platformer-3d",
738
+ defaultOptions,
739
+ systemFactory: (ctx) => new Platformer3DBehaviorSystem(
740
+ ctx.world,
741
+ ctx.scene,
742
+ ctx.getBehaviorLinks
743
+ ),
744
+ createHandle: (ref) => ({
745
+ getState: () => ref.fsm?.getState() ?? "idle" /* Idle */,
746
+ isGrounded: () => ref.fsm?.isGrounded() ?? false,
747
+ getJumpCount: () => ref.fsm?.getJumpCount() ?? 0,
748
+ onPlatformCollision: (ctx) => ref.fsm?.handleCollision(ctx)
749
+ })
750
+ });
751
+ export {
752
+ Platformer3DBehavior2 as Platformer3DBehavior,
753
+ Platformer3DEvent,
754
+ Platformer3DFSM,
755
+ Platformer3DBehavior as Platformer3DMovementBehavior,
756
+ Platformer3DState,
757
+ createPlatformer3DInputComponent,
758
+ createPlatformer3DMovementComponent,
759
+ createPlatformer3DStateComponent
760
+ };
761
+ //# sourceMappingURL=platformer-3d.js.map