@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
package/dist/entities.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/lib/entities/box.ts
2
- import { Vector3 as Vector38 } from "three";
2
+ import { Vector3 as Vector39 } from "three";
3
3
 
4
4
  // src/lib/entities/entity.ts
5
5
  import { Mesh, ShaderMaterial, Group } from "three";
@@ -57,6 +57,8 @@ var BaseNode = class _BaseNode {
57
57
  destroy: [],
58
58
  cleanup: []
59
59
  };
60
+ trackLifecycleRegistrations = false;
61
+ userLifecycleRegistrations = [];
60
62
  constructor(args = []) {
61
63
  const options = args.filter((arg) => !(arg instanceof _BaseNode)).reduce((acc, opt) => ({ ...acc, ...opt }), {});
62
64
  this.options = options;
@@ -70,6 +72,7 @@ var BaseNode = class _BaseNode {
70
72
  */
71
73
  onSetup(...callbacks) {
72
74
  this.lifecycleCallbacks.setup.push(...callbacks);
75
+ this.recordLifecycleRegistration("onSetup", callbacks);
73
76
  return this;
74
77
  }
75
78
  /**
@@ -77,6 +80,7 @@ var BaseNode = class _BaseNode {
77
80
  */
78
81
  onLoaded(...callbacks) {
79
82
  this.lifecycleCallbacks.loaded.push(...callbacks);
83
+ this.recordLifecycleRegistration("onLoaded", callbacks);
80
84
  return this;
81
85
  }
82
86
  /**
@@ -84,6 +88,7 @@ var BaseNode = class _BaseNode {
84
88
  */
85
89
  onUpdate(...callbacks) {
86
90
  this.lifecycleCallbacks.update.push(...callbacks);
91
+ this.recordLifecycleRegistration("onUpdate", callbacks);
87
92
  return this;
88
93
  }
89
94
  /**
@@ -91,6 +96,7 @@ var BaseNode = class _BaseNode {
91
96
  */
92
97
  onDestroy(...callbacks) {
93
98
  this.lifecycleCallbacks.destroy.push(...callbacks);
99
+ this.recordLifecycleRegistration("onDestroy", callbacks);
94
100
  return this;
95
101
  }
96
102
  /**
@@ -98,6 +104,7 @@ var BaseNode = class _BaseNode {
98
104
  */
99
105
  onCleanup(...callbacks) {
100
106
  this.lifecycleCallbacks.cleanup.push(...callbacks);
107
+ this.recordLifecycleRegistration("onCleanup", callbacks);
101
108
  return this;
102
109
  }
103
110
  /**
@@ -105,6 +112,7 @@ var BaseNode = class _BaseNode {
105
112
  */
106
113
  prependSetup(...callbacks) {
107
114
  this.lifecycleCallbacks.setup.unshift(...callbacks);
115
+ this.recordLifecycleRegistration("prependSetup", callbacks);
108
116
  return this;
109
117
  }
110
118
  /**
@@ -112,6 +120,7 @@ var BaseNode = class _BaseNode {
112
120
  */
113
121
  prependUpdate(...callbacks) {
114
122
  this.lifecycleCallbacks.update.unshift(...callbacks);
123
+ this.recordLifecycleRegistration("prependUpdate", callbacks);
115
124
  return this;
116
125
  }
117
126
  // ─────────────────────────────────────────────────────────────────────────────
@@ -140,6 +149,42 @@ var BaseNode = class _BaseNode {
140
149
  isComposite() {
141
150
  return this.children.length > 0;
142
151
  }
152
+ enableUserLifecycleTracking() {
153
+ this.trackLifecycleRegistrations = true;
154
+ }
155
+ replayUserLifecycleRegistrationsTo(target, wrap) {
156
+ for (const registration of this.userLifecycleRegistrations) {
157
+ const callbacks = wrap ? registration.callbacks.map((callback) => wrap(callback)) : [...registration.callbacks];
158
+ switch (registration.method) {
159
+ case "onSetup":
160
+ target.onSetup(...callbacks);
161
+ break;
162
+ case "onLoaded":
163
+ target.onLoaded(...callbacks);
164
+ break;
165
+ case "onUpdate":
166
+ target.onUpdate(...callbacks);
167
+ break;
168
+ case "onDestroy":
169
+ target.onDestroy(...callbacks);
170
+ break;
171
+ case "onCleanup":
172
+ target.onCleanup(...callbacks);
173
+ break;
174
+ case "prependSetup":
175
+ target.prependSetup(...callbacks);
176
+ break;
177
+ case "prependUpdate":
178
+ target.prependUpdate(...callbacks);
179
+ break;
180
+ }
181
+ }
182
+ }
183
+ cloneChildrenInto(target) {
184
+ for (const child of this.children) {
185
+ target.add(cloneNode(child));
186
+ }
187
+ }
143
188
  // ─────────────────────────────────────────────────────────────────────────────
144
189
  // Node lifecycle execution - runs internal + callback arrays
145
190
  // ─────────────────────────────────────────────────────────────────────────────
@@ -213,7 +258,25 @@ var BaseNode = class _BaseNode {
213
258
  setOptions(options) {
214
259
  this.options = { ...this.options, ...options };
215
260
  }
261
+ recordLifecycleRegistration(method, callbacks) {
262
+ if (!this.trackLifecycleRegistrations || callbacks.length === 0) {
263
+ return;
264
+ }
265
+ this.userLifecycleRegistrations.push({
266
+ method,
267
+ callbacks: [...callbacks]
268
+ });
269
+ }
216
270
  };
271
+ function cloneNode(node) {
272
+ const maybeClone = node?.clone;
273
+ if (typeof maybeClone !== "function") {
274
+ throw new Error(
275
+ `Cannot clone child node "${node.name || node.uuid || "unknown"}": missing clone() support.`
276
+ );
277
+ }
278
+ return maybeClone.call(node);
279
+ }
217
280
 
218
281
  // ../../node_modules/.pnpm/mitt@3.0.1/node_modules/mitt/dist/mitt.mjs
219
282
  function mitt_default(n) {
@@ -285,10 +348,15 @@ function createTransformStore(initial) {
285
348
  rotation: { x: 0, y: 0, z: 0, w: 1 },
286
349
  velocity: { x: 0, y: 0, z: 0 },
287
350
  angularVelocity: { x: 0, y: 0, z: 0 },
351
+ velocityChannels: {},
288
352
  dirty: {
289
353
  position: false,
290
354
  rotation: false,
291
355
  velocity: false,
356
+ velocityX: false,
357
+ velocityY: false,
358
+ velocityZ: false,
359
+ velocityChannels: false,
292
360
  angularVelocity: false
293
361
  }
294
362
  };
@@ -305,39 +373,70 @@ function createTransformStore(initial) {
305
373
 
306
374
  // src/lib/actions/capabilities/moveable.ts
307
375
  import { Vector3 } from "three";
376
+
377
+ // src/lib/actions/capabilities/velocity-intents.ts
378
+ function setVelocityIntent(store, sourceId, vector, options = {}) {
379
+ const prev = store.velocityChannels[sourceId];
380
+ const mode = options.mode ?? prev?.mode ?? "replace";
381
+ const priority = options.priority ?? prev?.priority ?? 0;
382
+ const isAdditiveMerge = mode === "add" && prev?.mode === "add";
383
+ const x = vector.x == null ? prev?.x : isAdditiveMerge ? (prev?.x ?? 0) + vector.x : vector.x;
384
+ const y = vector.y == null ? prev?.y : isAdditiveMerge ? (prev?.y ?? 0) + vector.y : vector.y;
385
+ const z = vector.z == null ? prev?.z : isAdditiveMerge ? (prev?.z ?? 0) + vector.z : vector.z;
386
+ store.velocityChannels[sourceId] = {
387
+ x,
388
+ y,
389
+ z,
390
+ mode,
391
+ priority
392
+ };
393
+ store.dirty.velocityChannels = true;
394
+ }
395
+ function clearVelocityIntent(store, sourceId) {
396
+ if (!(sourceId in store.velocityChannels)) return;
397
+ delete store.velocityChannels[sourceId];
398
+ store.dirty.velocityChannels = true;
399
+ }
400
+
401
+ // src/lib/actions/capabilities/moveable.ts
308
402
  function moveX(entity, delta) {
309
403
  if (!entity.transformStore) return;
310
- entity.transformStore.velocity.x = delta;
311
- entity.transformStore.dirty.velocity = true;
404
+ setVelocityIntent(entity.transformStore, "actions", { x: delta }, { mode: "replace" });
312
405
  }
313
406
  function moveY(entity, delta) {
314
407
  if (!entity.transformStore) return;
315
- entity.transformStore.velocity.y = delta;
316
- entity.transformStore.dirty.velocity = true;
408
+ setVelocityIntent(entity.transformStore, "actions", { y: delta }, { mode: "replace" });
317
409
  }
318
410
  function moveZ(entity, delta) {
319
411
  if (!entity.transformStore) return;
320
- entity.transformStore.velocity.z = delta;
321
- entity.transformStore.dirty.velocity = true;
412
+ setVelocityIntent(entity.transformStore, "actions", { z: delta }, { mode: "replace" });
322
413
  }
323
414
  function moveXY(entity, deltaX, deltaY) {
324
415
  if (!entity.transformStore) return;
325
- entity.transformStore.velocity.x = deltaX;
326
- entity.transformStore.velocity.y = deltaY;
327
- entity.transformStore.dirty.velocity = true;
416
+ setVelocityIntent(
417
+ entity.transformStore,
418
+ "actions",
419
+ { x: deltaX, y: deltaY },
420
+ { mode: "replace" }
421
+ );
328
422
  }
329
423
  function moveXZ(entity, deltaX, deltaZ) {
330
424
  if (!entity.transformStore) return;
331
- entity.transformStore.velocity.x = deltaX;
332
- entity.transformStore.velocity.z = deltaZ;
333
- entity.transformStore.dirty.velocity = true;
425
+ setVelocityIntent(
426
+ entity.transformStore,
427
+ "actions",
428
+ { x: deltaX, z: deltaZ },
429
+ { mode: "replace" }
430
+ );
334
431
  }
335
432
  function move(entity, vector) {
336
433
  if (!entity.transformStore) return;
337
- entity.transformStore.velocity.x += vector.x;
338
- entity.transformStore.velocity.y += vector.y;
339
- entity.transformStore.velocity.z += vector.z;
340
- entity.transformStore.dirty.velocity = true;
434
+ setVelocityIntent(
435
+ entity.transformStore,
436
+ "actions",
437
+ { x: vector.x, y: vector.y, z: vector.z },
438
+ { mode: "add" }
439
+ );
341
440
  }
342
441
  function resetVelocity(entity) {
343
442
  if (!entity.body) return;
@@ -423,6 +522,17 @@ function makeMoveable(entity) {
423
522
 
424
523
  // src/lib/actions/capabilities/rotatable.ts
425
524
  import { Euler, Vector3 as Vector32, MathUtils, Quaternion as Quaternion2 } from "three";
525
+ function syncImmediateRotation(entity) {
526
+ if (!entity.group || !entity.transformStore) return;
527
+ entity.group.setRotationFromQuaternion(
528
+ new Quaternion2(
529
+ entity.transformStore.rotation.x,
530
+ entity.transformStore.rotation.y,
531
+ entity.transformStore.rotation.z,
532
+ entity.transformStore.rotation.w
533
+ )
534
+ );
535
+ }
426
536
  function rotateInDirection(entity, moveVector) {
427
537
  if (!entity.body) return;
428
538
  const rotate = Math.atan2(-moveVector.x, moveVector.z);
@@ -432,9 +542,16 @@ function rotateYEuler(entity, amount) {
432
542
  rotateEuler(entity, new Vector32(0, -amount, 0));
433
543
  }
434
544
  function rotateEuler(entity, rotation2) {
435
- if (!entity.group) return;
436
- const euler = new Euler(rotation2.x, rotation2.y, rotation2.z);
437
- entity.group.setRotationFromEuler(euler);
545
+ if (!entity.transformStore) return;
546
+ const quat = new Quaternion2().setFromEuler(
547
+ new Euler(rotation2.x, rotation2.y, rotation2.z)
548
+ );
549
+ entity.transformStore.rotation.w = quat.w;
550
+ entity.transformStore.rotation.x = quat.x;
551
+ entity.transformStore.rotation.y = quat.y;
552
+ entity.transformStore.rotation.z = quat.z;
553
+ entity.transformStore.dirty.rotation = true;
554
+ syncImmediateRotation(entity);
438
555
  }
439
556
  function rotateY(entity, delta) {
440
557
  if (!entity.transformStore) return;
@@ -451,6 +568,7 @@ function rotateY(entity, delta) {
451
568
  entity.transformStore.rotation.y = newY;
452
569
  entity.transformStore.rotation.z = newZ;
453
570
  entity.transformStore.dirty.rotation = true;
571
+ syncImmediateRotation(entity);
454
572
  }
455
573
  function rotateX(entity, delta) {
456
574
  if (!entity.transformStore) return;
@@ -467,6 +585,7 @@ function rotateX(entity, delta) {
467
585
  entity.transformStore.rotation.y = newY;
468
586
  entity.transformStore.rotation.z = newZ;
469
587
  entity.transformStore.dirty.rotation = true;
588
+ syncImmediateRotation(entity);
470
589
  }
471
590
  function rotateZ(entity, delta) {
472
591
  if (!entity.transformStore) return;
@@ -483,6 +602,8 @@ function rotateZ(entity, delta) {
483
602
  entity.transformStore.rotation.y = newY;
484
603
  entity.transformStore.rotation.z = newZ;
485
604
  entity.transformStore.dirty.rotation = true;
605
+ entity._rotation2DAngle = (entity._rotation2DAngle ?? 0) + delta;
606
+ syncImmediateRotation(entity);
486
607
  }
487
608
  function setRotationY(entity, y) {
488
609
  if (!entity.transformStore) return;
@@ -494,6 +615,7 @@ function setRotationY(entity, y) {
494
615
  entity.transformStore.rotation.y = yComponent;
495
616
  entity.transformStore.rotation.z = 0;
496
617
  entity.transformStore.dirty.rotation = true;
618
+ syncImmediateRotation(entity);
497
619
  }
498
620
  function setRotationDegreesY(entity, y) {
499
621
  if (!entity.body) return;
@@ -509,6 +631,7 @@ function setRotationX(entity, x) {
509
631
  entity.transformStore.rotation.y = 0;
510
632
  entity.transformStore.rotation.z = 0;
511
633
  entity.transformStore.dirty.rotation = true;
634
+ syncImmediateRotation(entity);
512
635
  }
513
636
  function setRotationDegreesX(entity, x) {
514
637
  if (!entity.body) return;
@@ -524,6 +647,8 @@ function setRotationZ(entity, z) {
524
647
  entity.transformStore.rotation.y = 0;
525
648
  entity.transformStore.rotation.z = zComponent;
526
649
  entity.transformStore.dirty.rotation = true;
650
+ entity._rotation2DAngle = z;
651
+ syncImmediateRotation(entity);
527
652
  }
528
653
  function setRotationDegreesZ(entity, z) {
529
654
  if (!entity.body) return;
@@ -537,6 +662,8 @@ function setRotation(entity, x, y, z) {
537
662
  entity.transformStore.rotation.y = quat.y;
538
663
  entity.transformStore.rotation.z = quat.z;
539
664
  entity.transformStore.dirty.rotation = true;
665
+ entity._rotation2DAngle = z;
666
+ syncImmediateRotation(entity);
540
667
  }
541
668
  function setRotationDegrees(entity, x, y, z) {
542
669
  if (!entity.body) return;
@@ -583,7 +710,7 @@ import {
583
710
  RigidBodyDesc as RigidBodyDesc2,
584
711
  RigidBodyType as RigidBodyType2
585
712
  } from "@dimforge/rapier3d-compat";
586
- import { Vector3 as Vector34 } from "three";
713
+ import { Vector3 as Vector35 } from "three";
587
714
 
588
715
  // src/lib/collision/collision-builder.ts
589
716
  import { ActiveCollisionTypes, ColliderDesc, RigidBodyDesc, RigidBodyType, Vector3 as Vector33 } from "@dimforge/rapier3d-compat";
@@ -645,10 +772,73 @@ var CollisionBuilder = class {
645
772
  }
646
773
  };
647
774
 
775
+ // src/lib/core/clone-utils.ts
776
+ import { Color, Vector2, Vector3 as Vector34 } from "three";
777
+ function isPlainObject(value) {
778
+ if (!value || typeof value !== "object") {
779
+ return false;
780
+ }
781
+ const proto = Object.getPrototypeOf(value);
782
+ return proto === Object.prototype || proto === null;
783
+ }
784
+ function deepCloneValue(value) {
785
+ if (value === null || value === void 0) {
786
+ return value;
787
+ }
788
+ if (typeof value === "function" || typeof value !== "object") {
789
+ return value;
790
+ }
791
+ if (value instanceof Color || value instanceof Vector2 || value instanceof Vector34) {
792
+ return value.clone();
793
+ }
794
+ if (value instanceof Float32Array) {
795
+ return new Float32Array(value);
796
+ }
797
+ if (Array.isArray(value)) {
798
+ return value.map((item) => deepCloneValue(item));
799
+ }
800
+ if (isPlainObject(value)) {
801
+ const cloned = {};
802
+ for (const [key, entry] of Object.entries(value)) {
803
+ if (key === "_builders") {
804
+ continue;
805
+ }
806
+ cloned[key] = deepCloneValue(entry);
807
+ }
808
+ return cloned;
809
+ }
810
+ return value;
811
+ }
812
+ function deepMergeValues(base, overrides) {
813
+ const clonedBase = deepCloneValue(base);
814
+ if (!overrides) {
815
+ return clonedBase;
816
+ }
817
+ if (!isPlainObject(clonedBase) || !isPlainObject(overrides)) {
818
+ return deepCloneValue(overrides);
819
+ }
820
+ const result = clonedBase;
821
+ for (const [key, overrideValue] of Object.entries(overrides)) {
822
+ if (key === "_builders" || overrideValue === void 0) {
823
+ continue;
824
+ }
825
+ const currentValue = result[key];
826
+ if (isPlainObject(currentValue) && isPlainObject(overrideValue)) {
827
+ result[key] = deepMergeValues(currentValue, overrideValue);
828
+ continue;
829
+ }
830
+ result[key] = deepCloneValue(overrideValue);
831
+ }
832
+ return result;
833
+ }
834
+
648
835
  // src/lib/entities/parts/collision-factories.ts
649
836
  function isCollisionComponent(obj) {
650
837
  return obj && obj.__kind === "collision";
651
838
  }
839
+ function cloneCollisionComponent(component) {
840
+ return component.cloneComponent();
841
+ }
652
842
  function buildBodyDesc(isStatic) {
653
843
  const type = isStatic ? RigidBodyType2.Fixed : RigidBodyType2.Dynamic;
654
844
  return new RigidBodyDesc2(type).setTranslation(0, 0, 0).setGravityScale(1).setCanSleep(false).setCcdEnabled(true);
@@ -670,57 +860,66 @@ function applyCollisionOptions(colliderDesc, opts) {
670
860
  colliderDesc.setCollisionGroups(groupId << 16 | filter);
671
861
  }
672
862
  }
673
- function makeComponent(colliderDesc, opts) {
863
+ function makeComponent(colliderDesc, opts, cloneComponentFactory) {
674
864
  applyCollisionOptions(colliderDesc, opts);
675
865
  return {
676
866
  __kind: "collision",
677
867
  bodyDesc: buildBodyDesc(opts.static ?? false),
678
- colliderDesc
868
+ colliderDesc,
869
+ cloneComponent: cloneComponentFactory
679
870
  };
680
871
  }
681
872
  function boxCollision(opts = {}) {
682
873
  const size = opts.size ?? { x: 1, y: 1, z: 1 };
683
874
  const desc = ColliderDesc2.cuboid(size.x / 2, size.y / 2, size.z / 2);
684
- return makeComponent(desc, opts);
875
+ const clonedOpts = deepCloneValue(opts);
876
+ return makeComponent(desc, opts, () => boxCollision(clonedOpts));
685
877
  }
686
878
  function sphereCollision(opts = {}) {
687
879
  const desc = ColliderDesc2.ball(opts.radius ?? 1);
688
- return makeComponent(desc, opts);
880
+ const clonedOpts = deepCloneValue(opts);
881
+ return makeComponent(desc, opts, () => sphereCollision(clonedOpts));
689
882
  }
690
883
  function coneCollision(opts = {}) {
691
884
  const radius = opts.radius ?? 1;
692
885
  const height = opts.height ?? 2;
693
886
  const desc = ColliderDesc2.cone(height / 2, radius);
694
- return makeComponent(desc, opts);
887
+ const clonedOpts = deepCloneValue(opts);
888
+ return makeComponent(desc, opts, () => coneCollision(clonedOpts));
695
889
  }
696
890
  function pyramidCollision(opts = {}) {
697
891
  const radius = opts.radius ?? 1;
698
892
  const height = opts.height ?? 2;
699
893
  const desc = ColliderDesc2.cone(height / 2, radius);
700
- return makeComponent(desc, opts);
894
+ const clonedOpts = deepCloneValue(opts);
895
+ return makeComponent(desc, opts, () => pyramidCollision(clonedOpts));
701
896
  }
702
897
  function cylinderCollision(opts = {}) {
703
898
  const radius = Math.max(opts.radiusTop ?? 1, opts.radiusBottom ?? 1);
704
899
  const height = opts.height ?? 2;
705
900
  const desc = ColliderDesc2.cylinder(height / 2, radius);
706
- return makeComponent(desc, opts);
901
+ const clonedOpts = deepCloneValue(opts);
902
+ return makeComponent(desc, opts, () => cylinderCollision(clonedOpts));
707
903
  }
708
904
  function pillCollision(opts = {}) {
709
905
  const radius = opts.radius ?? 0.5;
710
906
  const length = opts.length ?? 1;
711
907
  const desc = ColliderDesc2.capsule(length / 2, radius);
712
- return makeComponent(desc, opts);
908
+ const clonedOpts = deepCloneValue(opts);
909
+ return makeComponent(desc, opts, () => pillCollision(clonedOpts));
713
910
  }
714
911
  function zoneCollision(opts = {}) {
715
912
  const size = opts.size ?? { x: 1, y: 1, z: 1 };
716
913
  const desc = ColliderDesc2.cuboid(size.x / 2, size.y / 2, size.z / 2);
717
914
  desc.setSensor(true);
718
915
  desc.activeCollisionTypes = ActiveCollisionTypes2.KINEMATIC_FIXED;
719
- return makeComponent(desc, { ...opts, static: opts.static ?? true, sensor: true });
916
+ const effectiveOpts = { ...opts, static: opts.static ?? true, sensor: true };
917
+ const clonedOpts = deepCloneValue(effectiveOpts);
918
+ return makeComponent(desc, effectiveOpts, () => zoneCollision(clonedOpts));
720
919
  }
721
920
 
722
921
  // src/lib/entities/common.ts
723
- import { Color, Vector3 as Vector35 } from "three";
922
+ import { Color as Color2, Vector3 as Vector36 } from "three";
724
923
 
725
924
  // src/lib/graphics/shaders/vertex/object.shader.ts
726
925
  var objectVertexShader = `
@@ -752,9 +951,9 @@ var standardShader = {
752
951
 
753
952
  // src/lib/entities/common.ts
754
953
  var commonDefaults = {
755
- position: new Vector35(0, 0, 0),
954
+ position: new Vector36(0, 0, 0),
756
955
  material: {
757
- color: new Color("#ffffff"),
956
+ color: new Color2("#ffffff"),
758
957
  shader: standardShader
759
958
  },
760
959
  collision: {
@@ -773,6 +972,21 @@ function mergeArgs(args, defaults) {
773
972
  }
774
973
 
775
974
  // src/lib/entities/entity.ts
975
+ function isCollisionRegistrationOptions(value) {
976
+ return value != null && typeof value === "object";
977
+ }
978
+ function normalizeCollisionRegistrationOptions(options) {
979
+ return {
980
+ phase: options?.phase ?? "stay",
981
+ cooldownMs: options?.cooldownMs
982
+ };
983
+ }
984
+ function getCollisionNowMs() {
985
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
986
+ return performance.now();
987
+ }
988
+ return Date.now();
989
+ }
776
990
  var GameEntity = class extends BaseNode {
777
991
  behaviors = [];
778
992
  group;
@@ -780,6 +994,7 @@ var GameEntity = class extends BaseNode {
780
994
  materials;
781
995
  bodyDesc = null;
782
996
  body = null;
997
+ physicsAttached = false;
783
998
  colliderDesc;
784
999
  collider;
785
1000
  custom = {};
@@ -793,7 +1008,9 @@ var GameEntity = class extends BaseNode {
793
1008
  debugInfo = {};
794
1009
  debugMaterial;
795
1010
  collisionDelegate = {
796
- collision: []
1011
+ collision: [],
1012
+ cooldowns: /* @__PURE__ */ new Map(),
1013
+ nextId: 0
797
1014
  };
798
1015
  collisionType;
799
1016
  // Instancing support
@@ -807,6 +1024,9 @@ var GameEntity = class extends BaseNode {
807
1024
  eventDelegate = new EventEmitterDelegate();
808
1025
  // Behavior references (new ECS pattern)
809
1026
  behaviorRefs = [];
1027
+ cloneFactory = null;
1028
+ trackAddedComponents = false;
1029
+ trackedComponents = [];
810
1030
  // Transform store for batched physics updates (auto-created in create())
811
1031
  transformStore;
812
1032
  // Movement & rotation methods are assigned at runtime by makeTransformable.
@@ -865,8 +1085,10 @@ var GameEntity = class extends BaseNode {
865
1085
  for (const component of components) {
866
1086
  if (component instanceof Mesh) {
867
1087
  this.addMeshComponent(component);
1088
+ this.trackComponent(component);
868
1089
  } else if (isCollisionComponent(component)) {
869
1090
  this.addCollisionComponent(component);
1091
+ this.trackComponent(component);
870
1092
  } else {
871
1093
  super.add(component);
872
1094
  }
@@ -907,6 +1129,22 @@ var GameEntity = class extends BaseNode {
907
1129
  this.colliderDescs.push(collision.colliderDesc);
908
1130
  }
909
1131
  }
1132
+ trackComponent(component) {
1133
+ if (!this.trackAddedComponents) {
1134
+ return;
1135
+ }
1136
+ if (component instanceof Mesh) {
1137
+ this.trackedComponents.push({
1138
+ kind: "mesh",
1139
+ component: cloneMeshComponent(component)
1140
+ });
1141
+ return;
1142
+ }
1143
+ this.trackedComponents.push({
1144
+ kind: "collision",
1145
+ component: cloneCollisionComponent(component)
1146
+ });
1147
+ }
910
1148
  // ─────────────────────────────────────────────────────────────────────────────
911
1149
  // Actions API -- entity-scoped, self-contained stateful actions
912
1150
  // ─────────────────────────────────────────────────────────────────────────────
@@ -942,7 +1180,7 @@ var GameEntity = class extends BaseNode {
942
1180
  store.velocity.x = 0;
943
1181
  store.velocity.y = 0;
944
1182
  store.velocity.z = 0;
945
- store.dirty.velocity = false;
1183
+ clearVelocityIntent(store, "actions");
946
1184
  store.angularVelocity.x = 0;
947
1185
  store.angularVelocity.y = 0;
948
1186
  store.angularVelocity.z = 0;
@@ -958,7 +1196,7 @@ var GameEntity = class extends BaseNode {
958
1196
  store.velocity.x = 0;
959
1197
  store.velocity.y = 0;
960
1198
  store.velocity.z = 0;
961
- store.dirty.velocity = false;
1199
+ clearVelocityIntent(store, "actions");
962
1200
  store.angularVelocity.x = 0;
963
1201
  store.angularVelocity.y = 0;
964
1202
  store.angularVelocity.z = 0;
@@ -976,12 +1214,72 @@ var GameEntity = class extends BaseNode {
976
1214
  this.name = this.options.name || "";
977
1215
  return this;
978
1216
  }
979
- /**
980
- * Add collision callbacks
981
- */
982
- onCollision(...callbacks) {
983
- const existing = this.collisionDelegate.collision ?? [];
984
- this.collisionDelegate.collision = [...existing, ...callbacks];
1217
+ clone(overrides) {
1218
+ if (!this.cloneFactory) {
1219
+ throw new Error(
1220
+ `Cannot clone ${this.constructor.name}: clone() is only supported for entities created by official factory helpers.`
1221
+ );
1222
+ }
1223
+ const clone = this.cloneFactory(
1224
+ deepMergeValues(this.options, overrides)
1225
+ );
1226
+ const behaviorAliases = this.copyBehaviorRefsTo(clone);
1227
+ const wrapCallback = (callback) => wrapBehaviorCallback(callback, behaviorAliases);
1228
+ this.replayUserLifecycleRegistrationsTo(clone, wrapCallback);
1229
+ const collisionRegistrations = this.getCollisionRegistrations();
1230
+ if (collisionRegistrations.length > 0) {
1231
+ for (const registration of collisionRegistrations) {
1232
+ clone.onCollision(
1233
+ wrapCallback(registration.callback),
1234
+ registration.options
1235
+ );
1236
+ }
1237
+ }
1238
+ for (const trackedComponent of this.trackedComponents) {
1239
+ if (trackedComponent.kind === "mesh") {
1240
+ clone.add(cloneMeshComponent(trackedComponent.component));
1241
+ continue;
1242
+ }
1243
+ clone.add(cloneCollisionComponent(trackedComponent.component));
1244
+ }
1245
+ this.cloneChildrenInto(clone);
1246
+ return clone;
1247
+ }
1248
+ finalizeCloneSupport(factory) {
1249
+ this.cloneFactory = factory;
1250
+ this.enableUserLifecycleTracking();
1251
+ this.trackAddedComponents = true;
1252
+ return this;
1253
+ }
1254
+ onCollision(...args) {
1255
+ const existing = [...this.collisionDelegate.collision ?? []];
1256
+ let nextId = this.collisionDelegate.nextId ?? 0;
1257
+ if (args.length === 1 || args.length === 2 && (args[1] == null || isCollisionRegistrationOptions(args[1]))) {
1258
+ const callback = args[0];
1259
+ if (!callback) {
1260
+ return this;
1261
+ }
1262
+ const options = args[1];
1263
+ existing.push({
1264
+ id: nextId,
1265
+ callback,
1266
+ options: normalizeCollisionRegistrationOptions(options)
1267
+ });
1268
+ nextId += 1;
1269
+ this.collisionDelegate.collision = existing;
1270
+ this.collisionDelegate.nextId = nextId;
1271
+ return this;
1272
+ }
1273
+ for (const callback of args) {
1274
+ existing.push({
1275
+ id: nextId,
1276
+ callback,
1277
+ options: normalizeCollisionRegistrationOptions()
1278
+ });
1279
+ nextId += 1;
1280
+ }
1281
+ this.collisionDelegate.collision = existing;
1282
+ this.collisionDelegate.nextId = nextId;
985
1283
  return this;
986
1284
  }
987
1285
  /**
@@ -1015,6 +1313,27 @@ var GameEntity = class extends BaseNode {
1015
1313
  getBehaviorRefs() {
1016
1314
  return this.behaviorRefs;
1017
1315
  }
1316
+ getCollisionCallbacks() {
1317
+ return this.getCollisionRegistrations().map((registration) => registration.callback);
1318
+ }
1319
+ getCollisionRegistrations() {
1320
+ return (this.collisionDelegate.collision ?? []).map((registration) => ({
1321
+ id: registration.id,
1322
+ callback: registration.callback,
1323
+ options: { ...registration.options }
1324
+ }));
1325
+ }
1326
+ copyBehaviorRefsTo(target) {
1327
+ const aliases = /* @__PURE__ */ new Map();
1328
+ for (const ref of this.behaviorRefs) {
1329
+ target.use(ref.descriptor, deepCloneValue(ref.options));
1330
+ const targetRef = target.getBehaviorRefs().at(-1);
1331
+ if (targetRef) {
1332
+ aliases.set(ref, targetRef);
1333
+ }
1334
+ }
1335
+ return aliases;
1336
+ }
1018
1337
  /**
1019
1338
  * Entity-specific setup - resets actions for a fresh stage session.
1020
1339
  * (User callbacks are handled by BaseNode's lifecycleCallbacks.setup)
@@ -1068,12 +1387,29 @@ var GameEntity = class extends BaseNode {
1068
1387
  this.debugMaterial = void 0;
1069
1388
  this.group?.removeFromParent();
1070
1389
  }
1071
- _collision(other, globals) {
1072
- if (this.collisionDelegate.collision?.length) {
1073
- const callbacks = this.collisionDelegate.collision;
1074
- callbacks.forEach((callback) => {
1075
- callback({ entity: this, other, globals });
1076
- });
1390
+ _collision(other, globals, dispatch = {
1391
+ phase: "stay",
1392
+ nowMs: getCollisionNowMs()
1393
+ }) {
1394
+ if (!this.collisionDelegate.collision?.length) {
1395
+ return;
1396
+ }
1397
+ const cooldowns = this.collisionDelegate.cooldowns ?? /* @__PURE__ */ new Map();
1398
+ this.collisionDelegate.cooldowns = cooldowns;
1399
+ for (const registration of this.collisionDelegate.collision) {
1400
+ if (registration.options.phase !== dispatch.phase) {
1401
+ continue;
1402
+ }
1403
+ const cooldownMs = registration.options.cooldownMs;
1404
+ if (cooldownMs != null) {
1405
+ const cooldownKey = `${registration.id}:${other.uuid}`;
1406
+ const lastTriggeredAt = cooldowns.get(cooldownKey);
1407
+ if (lastTriggeredAt != null && dispatch.nowMs - lastTriggeredAt < cooldownMs) {
1408
+ continue;
1409
+ }
1410
+ cooldowns.set(cooldownKey, dispatch.nowMs);
1411
+ }
1412
+ registration.callback({ entity: this, other, globals });
1077
1413
  }
1078
1414
  }
1079
1415
  updateMaterials(params) {
@@ -1120,6 +1456,63 @@ var GameEntity = class extends BaseNode {
1120
1456
  this.eventDelegate.dispose();
1121
1457
  }
1122
1458
  };
1459
+ function cloneMeshComponent(mesh) {
1460
+ const clonedMesh = mesh.clone(true);
1461
+ const originalNodes = [];
1462
+ const clonedNodes = [];
1463
+ mesh.traverse((node) => originalNodes.push(node));
1464
+ clonedMesh.traverse((node) => clonedNodes.push(node));
1465
+ for (let i = 0; i < originalNodes.length; i++) {
1466
+ const originalNode = originalNodes[i];
1467
+ const clonedNode = clonedNodes[i];
1468
+ if (originalNode?.geometry?.clone) {
1469
+ clonedNode.geometry = originalNode.geometry.clone();
1470
+ }
1471
+ if (originalNode?.material) {
1472
+ clonedNode.material = Array.isArray(originalNode.material) ? originalNode.material.map((material) => material.clone()) : originalNode.material.clone();
1473
+ }
1474
+ }
1475
+ return clonedMesh;
1476
+ }
1477
+ function wrapBehaviorCallback(callback, aliases) {
1478
+ if (aliases.size === 0) {
1479
+ return callback;
1480
+ }
1481
+ return ((...args) => {
1482
+ const previousValues = Array.from(aliases.entries()).map(([sourceRef, targetRef]) => ({
1483
+ sourceRef,
1484
+ options: sourceRef.options,
1485
+ fsm: sourceRef.fsm,
1486
+ hasFSM: Object.prototype.hasOwnProperty.call(sourceRef, "fsm"),
1487
+ targetRef
1488
+ }));
1489
+ for (const entry of previousValues) {
1490
+ entry.sourceRef.options = entry.targetRef.options;
1491
+ if (entry.targetRef.fsm === void 0) {
1492
+ delete entry.sourceRef.fsm;
1493
+ } else {
1494
+ entry.sourceRef.fsm = entry.targetRef.fsm;
1495
+ }
1496
+ }
1497
+ try {
1498
+ return callback(...args);
1499
+ } finally {
1500
+ for (let i = previousValues.length - 1; i >= 0; i--) {
1501
+ const entry = previousValues[i];
1502
+ entry.sourceRef.options = entry.options;
1503
+ if (!entry.hasFSM) {
1504
+ delete entry.sourceRef.fsm;
1505
+ } else {
1506
+ entry.sourceRef.fsm = entry.fsm;
1507
+ }
1508
+ }
1509
+ }
1510
+ });
1511
+ }
1512
+ function finalizeEntityCloneSupport(entity, factory) {
1513
+ entity.finalizeCloneSupport(factory);
1514
+ return entity;
1515
+ }
1123
1516
 
1124
1517
  // src/lib/entities/delegates/debug.ts
1125
1518
  import { MeshStandardMaterial, MeshBasicMaterial, MeshPhongMaterial } from "three";
@@ -1222,13 +1615,13 @@ import {
1222
1615
  CapsuleGeometry,
1223
1616
  ConeGeometry,
1224
1617
  CylinderGeometry,
1225
- Mesh as Mesh2,
1618
+ Mesh as Mesh3,
1226
1619
  MeshStandardMaterial as MeshStandardMaterial3,
1227
1620
  SphereGeometry
1228
1621
  } from "three";
1229
1622
 
1230
1623
  // src/lib/graphics/material.ts
1231
- import { Color as Color3, Vector2 as Vector22, Vector3 as Vector37 } from "three";
1624
+ import { Color as Color4, Vector2 as Vector23, Vector3 as Vector38 } from "three";
1232
1625
  import {
1233
1626
  MeshPhongMaterial as MeshPhongMaterial2,
1234
1627
  MeshStandardMaterial as MeshStandardMaterial2,
@@ -1295,10 +1688,44 @@ var TextureLoaderAdapter = class {
1295
1688
 
1296
1689
  // src/lib/core/loaders/gltf-loader.ts
1297
1690
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
1691
+ import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
1692
+
1693
+ // src/lib/core/loaders/model-clone.ts
1694
+ import { Mesh as Mesh2 } from "three";
1695
+ import { clone as cloneSkeleton } from "three/addons/utils/SkeletonUtils.js";
1696
+ function cloneModelObject(object) {
1697
+ const clonedObject = cloneSkeleton(object);
1698
+ const originalNodes = [];
1699
+ const clonedNodes = [];
1700
+ object.traverse((node) => originalNodes.push(node));
1701
+ clonedObject.traverse((node) => clonedNodes.push(node));
1702
+ for (let i = 0; i < originalNodes.length; i++) {
1703
+ const originalNode = originalNodes[i];
1704
+ const clonedNode = clonedNodes[i];
1705
+ if (!(originalNode instanceof Mesh2) || !(clonedNode instanceof Mesh2)) {
1706
+ continue;
1707
+ }
1708
+ if (originalNode.geometry?.clone) {
1709
+ clonedNode.geometry = originalNode.geometry.clone();
1710
+ }
1711
+ if (!originalNode.material) {
1712
+ continue;
1713
+ }
1714
+ clonedNode.material = Array.isArray(originalNode.material) ? originalNode.material.map((material) => material.clone()) : originalNode.material.clone();
1715
+ }
1716
+ return clonedObject;
1717
+ }
1718
+
1719
+ // src/lib/core/loaders/gltf-loader.ts
1720
+ var DRACO_DECODER_PATH = "https://www.gstatic.com/draco/v1/decoders/";
1298
1721
  var GLTFLoaderAdapter = class {
1299
1722
  loader;
1723
+ dracoLoader;
1300
1724
  constructor() {
1725
+ this.dracoLoader = new DRACOLoader();
1726
+ this.dracoLoader.setDecoderPath(DRACO_DECODER_PATH);
1301
1727
  this.loader = new GLTFLoader();
1728
+ this.loader.setDRACOLoader(this.dracoLoader);
1302
1729
  }
1303
1730
  isSupported(url) {
1304
1731
  const ext = url.split(".").pop()?.toLowerCase();
@@ -1357,7 +1784,7 @@ var GLTFLoaderAdapter = class {
1357
1784
  */
1358
1785
  clone(result) {
1359
1786
  return {
1360
- object: result.object.clone(),
1787
+ object: cloneModelObject(result.object),
1361
1788
  animations: result.animations?.map((anim) => anim.clone()),
1362
1789
  gltf: result.gltf
1363
1790
  };
@@ -1425,7 +1852,7 @@ var FBXLoaderAdapter = class {
1425
1852
  */
1426
1853
  clone(result) {
1427
1854
  return {
1428
- object: result.object.clone(),
1855
+ object: cloneModelObject(result.object),
1429
1856
  animations: result.animations?.map((anim) => anim.clone())
1430
1857
  };
1431
1858
  }
@@ -1486,7 +1913,7 @@ var OBJLoaderAdapter = class {
1486
1913
  */
1487
1914
  clone(result) {
1488
1915
  return {
1489
- object: result.object.clone(),
1916
+ object: cloneModelObject(result.object),
1490
1917
  animations: []
1491
1918
  };
1492
1919
  }
@@ -1922,6 +2349,14 @@ function isGLSLShader(shader) {
1922
2349
  }
1923
2350
  var MaterialBuilder = class _MaterialBuilder {
1924
2351
  static batchMaterialMap = /* @__PURE__ */ new Map();
2352
+ /**
2353
+ * Clear the static batch material cache.
2354
+ * Should be called during game disposal to prevent stale material references
2355
+ * from persisting across demo/stage switches.
2356
+ */
2357
+ static clearBatchCache() {
2358
+ _MaterialBuilder.batchMaterialMap.clear();
2359
+ }
1925
2360
  materials = [];
1926
2361
  /** Whether to use TSL/NodeMaterial (for WebGPU compatibility) */
1927
2362
  useTSL;
@@ -1946,53 +2381,61 @@ var MaterialBuilder = class _MaterialBuilder {
1946
2381
  }
1947
2382
  }
1948
2383
  build(options, entityType) {
1949
- const { path, normalMap, repeat, color, shader, useTSL } = options;
2384
+ const { path, normalMap, repeat, color, shader, opacity, useTSL } = options;
1950
2385
  const shouldUseTSL = useTSL ?? this.useTSL;
1951
2386
  if (shader) {
1952
2387
  if (isTSLShader(shader)) {
1953
- this.setTSLShader(shader);
2388
+ this.setTSLShader(shader, opacity);
1954
2389
  } else if (isGLSLShader(shader)) {
1955
2390
  if (shouldUseTSL) {
1956
2391
  console.warn("MaterialBuilder: GLSL shader provided but TSL mode requested. Using GLSL.");
1957
2392
  }
1958
- this.setShader(shader);
2393
+ this.setShader(shader, opacity);
1959
2394
  }
1960
2395
  } else if (path) {
1961
- this.setTexture(path, repeat, shouldUseTSL);
2396
+ this.setTexture(path, repeat, shouldUseTSL, opacity);
1962
2397
  }
1963
2398
  if (color) {
1964
- this.withColor(color, shouldUseTSL);
2399
+ this.withColor(color, shouldUseTSL, opacity);
1965
2400
  }
1966
2401
  if (this.materials.length === 0) {
1967
- this.setColor(new Color3("#ffffff"), shouldUseTSL);
2402
+ this.setColor(new Color4("#ffffff"), shouldUseTSL, opacity);
1968
2403
  }
1969
2404
  if (normalMap && this.materials.length > 0) {
1970
2405
  this.setNormalMap(normalMap, repeat);
1971
2406
  }
1972
2407
  this.batchMaterial(options, entityType);
1973
2408
  }
1974
- withColor(color, useTSL = false) {
1975
- this.setColor(color, useTSL);
2409
+ withColor(color, useTSL = false, opacity) {
2410
+ this.setColor(color, useTSL, opacity);
1976
2411
  return this;
1977
2412
  }
1978
- withShader(shader) {
1979
- this.setShader(shader);
2413
+ withShader(shader, opacity) {
2414
+ this.setShader(shader, opacity);
1980
2415
  return this;
1981
2416
  }
1982
- withTSLShader(shader) {
1983
- this.setTSLShader(shader);
2417
+ withTSLShader(shader, opacity) {
2418
+ this.setTSLShader(shader, opacity);
1984
2419
  return this;
1985
2420
  }
2421
+ applyOpacity(material, opacity, forceTransparent = false) {
2422
+ if (opacity !== void 0) {
2423
+ material.opacity = opacity;
2424
+ }
2425
+ material.transparent = forceTransparent || opacity !== void 0 && opacity < 1;
2426
+ material.needsUpdate = true;
2427
+ }
1986
2428
  /**
1987
2429
  * Set texture - loads in background (deferred).
1988
2430
  * Material is created immediately with null map, texture applies when loaded.
1989
2431
  */
1990
- setTexture(texturePath = null, repeat = new Vector22(1, 1), useTSL = false) {
2432
+ setTexture(texturePath = null, repeat = new Vector23(1, 1), useTSL = false, opacity) {
1991
2433
  if (!texturePath) {
1992
2434
  return;
1993
2435
  }
1994
2436
  if (useTSL) {
1995
2437
  const material = new MeshStandardNodeMaterial();
2438
+ this.applyOpacity(material, opacity);
1996
2439
  this.materials.push(material);
1997
2440
  assetManager.loadTexture(texturePath, {
1998
2441
  clone: true,
@@ -2005,7 +2448,9 @@ var MaterialBuilder = class _MaterialBuilder {
2005
2448
  });
2006
2449
  } else {
2007
2450
  const material = new MeshPhongMaterial2({
2008
- map: null
2451
+ map: null,
2452
+ opacity: opacity ?? 1,
2453
+ transparent: opacity !== void 0 && opacity < 1
2009
2454
  });
2010
2455
  this.materials.push(material);
2011
2456
  assetManager.loadTexture(texturePath, {
@@ -2022,7 +2467,7 @@ var MaterialBuilder = class _MaterialBuilder {
2022
2467
  /**
2023
2468
  * Set normal map for the current material
2024
2469
  */
2025
- setNormalMap(normalMapPath, repeat = new Vector22(1, 1)) {
2470
+ setNormalMap(normalMapPath, repeat = new Vector23(1, 1)) {
2026
2471
  const material = this.materials[this.materials.length - 1];
2027
2472
  if (!material) return;
2028
2473
  assetManager.loadTexture(normalMapPath, {
@@ -2046,17 +2491,20 @@ var MaterialBuilder = class _MaterialBuilder {
2046
2491
  }
2047
2492
  });
2048
2493
  }
2049
- setColor(color, useTSL = false) {
2494
+ setColor(color, useTSL = false, opacity) {
2050
2495
  if (useTSL) {
2051
2496
  const material = new MeshStandardNodeMaterial();
2052
2497
  material.color = color;
2498
+ this.applyOpacity(material, opacity);
2053
2499
  this.materials.push(material);
2054
2500
  } else {
2055
2501
  const material = new MeshStandardMaterial2({
2056
2502
  color,
2057
2503
  emissiveIntensity: 0.5,
2058
2504
  lightMapIntensity: 0.5,
2059
- fog: true
2505
+ fog: true,
2506
+ opacity: opacity ?? 1,
2507
+ transparent: opacity !== void 0 && opacity < 1
2060
2508
  });
2061
2509
  this.materials.push(material);
2062
2510
  }
@@ -2064,34 +2512,37 @@ var MaterialBuilder = class _MaterialBuilder {
2064
2512
  /**
2065
2513
  * Set GLSL shader (WebGL only)
2066
2514
  */
2067
- setShader(customShader) {
2515
+ setShader(customShader, opacity) {
2068
2516
  const { fragment: fragment2, vertex } = customShader ?? standardShader;
2069
2517
  const shader = new ShaderMaterial2({
2070
2518
  uniforms: {
2071
- iResolution: { value: new Vector37(1, 1, 1) },
2519
+ iResolution: { value: new Vector38(1, 1, 1) },
2072
2520
  iTime: { value: 0 },
2073
2521
  tDiffuse: { value: null },
2074
2522
  tDepth: { value: null },
2075
2523
  tNormal: { value: null },
2076
2524
  normalMap: { value: null },
2077
- lightDir: { value: new Vector37(1, 1, 1) },
2525
+ lightDir: { value: new Vector38(1, 1, 1) },
2078
2526
  normalStrength: { value: 1 }
2079
2527
  },
2080
2528
  vertexShader: vertex,
2081
2529
  fragmentShader: fragment2,
2082
- transparent: true
2530
+ transparent: true,
2531
+ opacity: opacity ?? 1
2083
2532
  });
2084
2533
  this.materials.push(shader);
2085
2534
  }
2086
2535
  /**
2087
2536
  * Set TSL shader (WebGPU compatible)
2088
2537
  */
2089
- setTSLShader(tslShader) {
2538
+ setTSLShader(tslShader, opacity) {
2090
2539
  const material = new MeshBasicNodeMaterial();
2091
2540
  material.colorNode = tslShader.colorNode;
2092
- if (tslShader.transparent) {
2093
- material.transparent = true;
2094
- }
2541
+ this.applyOpacity(
2542
+ material,
2543
+ opacity,
2544
+ tslShader.transparent
2545
+ );
2095
2546
  this.materials.push(material);
2096
2547
  }
2097
2548
  };
@@ -2122,12 +2573,12 @@ function boxMesh(opts = {}) {
2122
2573
  const size = opts.size ?? { x: 1, y: 1, z: 1 };
2123
2574
  const geometry = new BoxGeometry(size.x, size.y, size.z);
2124
2575
  const materials = buildMaterial(opts);
2125
- return finalizeMesh(new Mesh2(geometry, materials.at(-1)), opts);
2576
+ return finalizeMesh(new Mesh3(geometry, materials.at(-1)), opts);
2126
2577
  }
2127
2578
  function sphereMesh(opts = {}) {
2128
2579
  const geometry = new SphereGeometry(opts.radius ?? 1);
2129
2580
  const materials = buildMaterial(opts);
2130
- return finalizeMesh(new Mesh2(geometry, materials.at(-1)), opts);
2581
+ return finalizeMesh(new Mesh3(geometry, materials.at(-1)), opts);
2131
2582
  }
2132
2583
  function coneMesh(opts = {}) {
2133
2584
  const geometry = new ConeGeometry(
@@ -2136,12 +2587,12 @@ function coneMesh(opts = {}) {
2136
2587
  opts.radialSegments ?? 32
2137
2588
  );
2138
2589
  const materials = buildMaterial(opts);
2139
- return finalizeMesh(new Mesh2(geometry, materials.at(-1)), opts);
2590
+ return finalizeMesh(new Mesh3(geometry, materials.at(-1)), opts);
2140
2591
  }
2141
2592
  function pyramidMesh(opts = {}) {
2142
2593
  const geometry = new ConeGeometry(opts.radius ?? 1, opts.height ?? 2, 4);
2143
2594
  const materials = buildMaterial(opts);
2144
- return finalizeMesh(new Mesh2(geometry, materials.at(-1)), opts);
2595
+ return finalizeMesh(new Mesh3(geometry, materials.at(-1)), opts);
2145
2596
  }
2146
2597
  function cylinderMesh(opts = {}) {
2147
2598
  const geometry = new CylinderGeometry(
@@ -2151,7 +2602,7 @@ function cylinderMesh(opts = {}) {
2151
2602
  opts.radialSegments ?? 32
2152
2603
  );
2153
2604
  const materials = buildMaterial(opts);
2154
- return finalizeMesh(new Mesh2(geometry, materials.at(-1)), opts);
2605
+ return finalizeMesh(new Mesh3(geometry, materials.at(-1)), opts);
2155
2606
  }
2156
2607
  function pillMesh(opts = {}) {
2157
2608
  const geometry = new CapsuleGeometry(
@@ -2161,13 +2612,13 @@ function pillMesh(opts = {}) {
2161
2612
  opts.radialSegments ?? 20
2162
2613
  );
2163
2614
  const materials = buildMaterial(opts);
2164
- return finalizeMesh(new Mesh2(geometry, materials.at(-1)), opts);
2615
+ return finalizeMesh(new Mesh3(geometry, materials.at(-1)), opts);
2165
2616
  }
2166
2617
 
2167
2618
  // src/lib/entities/box.ts
2168
2619
  var boxDefaults = {
2169
2620
  ...commonDefaults,
2170
- size: new Vector38(1, 1, 1)
2621
+ size: new Vector39(1, 1, 1)
2171
2622
  };
2172
2623
  var BOX_TYPE = /* @__PURE__ */ Symbol("Box");
2173
2624
  var ZylemBox = class _ZylemBox extends GameEntity {
@@ -2200,7 +2651,10 @@ function createBox(...args) {
2200
2651
  collisionFilter: options.collisionFilter
2201
2652
  })
2202
2653
  );
2203
- return entity;
2654
+ return finalizeEntityCloneSupport(
2655
+ entity,
2656
+ (cloneOptions) => createBox(cloneOptions ?? {})
2657
+ );
2204
2658
  }
2205
2659
 
2206
2660
  // src/lib/entities/sphere.ts
@@ -2239,12 +2693,15 @@ function createSphere(...args) {
2239
2693
  collisionFilter: options.collisionFilter
2240
2694
  })
2241
2695
  );
2242
- return entity;
2696
+ return finalizeEntityCloneSupport(
2697
+ entity,
2698
+ (cloneOptions) => createSphere(cloneOptions ?? {})
2699
+ );
2243
2700
  }
2244
2701
 
2245
2702
  // src/lib/entities/sprite.ts
2246
2703
  import { ColliderDesc as ColliderDesc3 } from "@dimforge/rapier3d-compat";
2247
- import { Euler as Euler2, Group as Group3, Quaternion as Quaternion3, Vector3 as Vector39 } from "three";
2704
+ import { Euler as Euler2, Group as Group3, Quaternion as Quaternion3, Vector3 as Vector310 } from "three";
2248
2705
  import {
2249
2706
  TextureLoader as TextureLoader2,
2250
2707
  SpriteMaterial,
@@ -2252,17 +2709,17 @@ import {
2252
2709
  } from "three";
2253
2710
 
2254
2711
  // src/lib/entities/builder.ts
2255
- import { BufferGeometry as BufferGeometry2, Mesh as Mesh4, Color as Color4 } from "three";
2712
+ import { BufferGeometry as BufferGeometry2, Mesh as Mesh5, Color as Color5 } from "three";
2256
2713
 
2257
2714
  // src/lib/graphics/mesh.ts
2258
- import { Mesh as Mesh3 } from "three";
2715
+ import { Mesh as Mesh4 } from "three";
2259
2716
  var MeshBuilder = class {
2260
2717
  _build(meshOptions, geometry, materials) {
2261
2718
  const { batched, material } = meshOptions;
2262
2719
  if (batched) {
2263
2720
  console.warn("warning: mesh batching is not implemented");
2264
2721
  }
2265
- const mesh = new Mesh3(geometry, materials.at(-1));
2722
+ const mesh = new Mesh4(geometry, materials.at(-1));
2266
2723
  mesh.position.set(0, 0, 0);
2267
2724
  mesh.castShadow = true;
2268
2725
  mesh.receiveShadow = true;
@@ -2315,7 +2772,7 @@ var EntityBuilder = class {
2315
2772
  }
2316
2773
  applyMaterialToGroup(group, materials) {
2317
2774
  group.traverse((child) => {
2318
- if (child instanceof Mesh4) {
2775
+ if (child instanceof Mesh5) {
2319
2776
  if (child.type === "SkinnedMesh" && materials[0] && !child.material.map) {
2320
2777
  child.material = materials[0];
2321
2778
  }
@@ -2349,7 +2806,7 @@ var EntityBuilder = class {
2349
2806
  if (this.options.collisionType) {
2350
2807
  entity.collisionType = this.options.collisionType;
2351
2808
  }
2352
- if (this.options.color instanceof Color4) {
2809
+ if (this.options.color instanceof Color5) {
2353
2810
  const applyColor = (material) => {
2354
2811
  const anyMat = material;
2355
2812
  if (anyMat && anyMat.color && anyMat.color.set) {
@@ -2366,7 +2823,7 @@ var EntityBuilder = class {
2366
2823
  }
2367
2824
  if (entity.group) {
2368
2825
  entity.group.traverse((child) => {
2369
- if (child instanceof Mesh4 && child.material) {
2826
+ if (child instanceof Mesh5 && child.material) {
2370
2827
  const mat = child.material;
2371
2828
  if (Array.isArray(mat)) mat.forEach(applyColor);
2372
2829
  else applyColor(mat);
@@ -2409,7 +2866,8 @@ function createEntity(params) {
2409
2866
  BuilderClass,
2410
2867
  entityType,
2411
2868
  MeshBuilderClass,
2412
- CollisionBuilderClass
2869
+ CollisionBuilderClass,
2870
+ cloneFactory
2413
2871
  } = params;
2414
2872
  let builder = null;
2415
2873
  let configuration;
@@ -2448,19 +2906,22 @@ function createEntity(params) {
2448
2906
  if (!builder) {
2449
2907
  throw new Error(`missing options for ${String(entityType)}, builder is not initialized.`);
2450
2908
  }
2451
- return builder.build();
2909
+ return finalizeEntityCloneSupport(
2910
+ builder.build(),
2911
+ (options) => cloneFactory(options)
2912
+ );
2452
2913
  }
2453
2914
 
2454
2915
  // src/lib/entities/sprite.ts
2455
2916
  var spriteDefaults = {
2456
2917
  ...commonDefaults,
2457
- size: new Vector39(1, 1, 1),
2918
+ size: new Vector310(1, 1, 1),
2458
2919
  images: [],
2459
2920
  animations: []
2460
2921
  };
2461
2922
  var SpriteCollisionBuilder = class extends EntityCollisionBuilder {
2462
2923
  collider(options) {
2463
- const size = options.collisionSize || options.size || new Vector39(1, 1, 1);
2924
+ const size = options.collisionSize || options.size || new Vector310(1, 1, 1);
2464
2925
  const half = { x: size.x / 2, y: size.y / 2, z: size.z / 2 };
2465
2926
  let colliderDesc = ColliderDesc3.cuboid(half.x, half.y, half.z);
2466
2927
  return colliderDesc;
@@ -2485,6 +2946,7 @@ var ZylemSprite = class _ZylemSprite extends GameEntity {
2485
2946
  constructor(options) {
2486
2947
  super();
2487
2948
  this.options = { ...spriteDefaults, ...options };
2949
+ this.prependSetup(this.spriteSetup.bind(this));
2488
2950
  this.prependUpdate(this.spriteUpdate.bind(this));
2489
2951
  this.onCleanup(this.spriteDestroy.bind(this));
2490
2952
  }
@@ -2503,6 +2965,7 @@ var ZylemSprite = class _ZylemSprite extends GameEntity {
2503
2965
  }
2504
2966
  createSpritesFromImages(images) {
2505
2967
  const textureLoader = new TextureLoader2();
2968
+ const size = this.options.size ?? new Vector310(1, 1, 1);
2506
2969
  images.forEach((image, index) => {
2507
2970
  const spriteMap = textureLoader.load(image.file);
2508
2971
  const material = new SpriteMaterial({
@@ -2511,6 +2974,8 @@ var ZylemSprite = class _ZylemSprite extends GameEntity {
2511
2974
  });
2512
2975
  const _sprite = new ThreeSprite(material);
2513
2976
  _sprite.position.normalize();
2977
+ _sprite.scale.set(size.x, size.y, size.z);
2978
+ _sprite.visible = index === 0;
2514
2979
  this.sprites.push(_sprite);
2515
2980
  this.spriteMap.set(image.name, index);
2516
2981
  });
@@ -2564,19 +3029,29 @@ var ZylemSprite = class _ZylemSprite extends GameEntity {
2564
3029
  }
2565
3030
  }
2566
3031
  }
2567
- spriteUpdate(params) {
3032
+ getCurrentRotationQuaternion() {
3033
+ if (this.transformStore?.dirty.rotation) {
3034
+ return this.transformStore.rotation;
3035
+ }
3036
+ return this.body?.rotation?.() ?? null;
3037
+ }
3038
+ syncSpriteMaterials() {
3039
+ const q = this.getCurrentRotationQuaternion();
2568
3040
  this.sprites.forEach((_sprite) => {
2569
- if (_sprite.material) {
2570
- const q = this.body?.rotation();
2571
- if (q) {
2572
- const quat = new Quaternion3(q.x, q.y, q.z, q.w);
2573
- const euler = new Euler2().setFromQuaternion(quat, "XYZ");
2574
- _sprite.material.rotation = euler.z;
2575
- }
2576
- _sprite.scale.set(this.options.size?.x ?? 1, this.options.size?.y ?? 1, this.options.size?.z ?? 1);
3041
+ if (_sprite.material && q) {
3042
+ const quat = new Quaternion3(q.x, q.y, q.z, q.w);
3043
+ const euler = new Euler2().setFromQuaternion(quat, "XYZ");
3044
+ _sprite.material.rotation = euler.z;
2577
3045
  }
3046
+ _sprite.scale.set(this.options.size?.x ?? 1, this.options.size?.y ?? 1, this.options.size?.z ?? 1);
2578
3047
  });
2579
3048
  }
3049
+ spriteSetup(_params) {
3050
+ this.syncSpriteMaterials();
3051
+ }
3052
+ spriteUpdate(params) {
3053
+ this.syncSpriteMaterials();
3054
+ }
2580
3055
  spriteDestroy(params) {
2581
3056
  this.sprites.forEach((_sprite) => {
2582
3057
  _sprite.removeFromParent();
@@ -2600,13 +3075,14 @@ function createSprite(...args) {
2600
3075
  EntityClass: ZylemSprite,
2601
3076
  BuilderClass: SpriteBuilder,
2602
3077
  CollisionBuilderClass: SpriteCollisionBuilder,
2603
- entityType: ZylemSprite.type
3078
+ entityType: ZylemSprite.type,
3079
+ cloneFactory: (options) => createSprite(options ?? {})
2604
3080
  });
2605
3081
  }
2606
3082
 
2607
3083
  // src/lib/entities/plane.ts
2608
3084
  import { ColliderDesc as ColliderDesc4 } from "@dimforge/rapier3d-compat";
2609
- import { PlaneGeometry, Vector2 as Vector23, Vector3 as Vector310 } from "three";
3085
+ import { PlaneGeometry, Vector2 as Vector24, Vector3 as Vector311 } from "three";
2610
3086
 
2611
3087
  // src/lib/graphics/geometries/XZPlaneGeometry.ts
2612
3088
  import { BufferGeometry as BufferGeometry3, Float32BufferAttribute } from "three";
@@ -2671,8 +3147,8 @@ var XZPlaneGeometry = class _XZPlaneGeometry extends BufferGeometry3 {
2671
3147
  var DEFAULT_SUBDIVISIONS = 4;
2672
3148
  var planeDefaults = {
2673
3149
  ...commonDefaults,
2674
- tile: new Vector23(10, 10),
2675
- repeat: new Vector23(1, 1),
3150
+ tile: new Vector24(10, 10),
3151
+ repeat: new Vector24(1, 1),
2676
3152
  collision: {
2677
3153
  static: true
2678
3154
  },
@@ -2682,11 +3158,11 @@ var planeDefaults = {
2682
3158
  };
2683
3159
  var PlaneCollisionBuilder = class extends EntityCollisionBuilder {
2684
3160
  collider(options) {
2685
- const tile = options.tile ?? new Vector23(1, 1);
3161
+ const tile = options.tile ?? new Vector24(1, 1);
2686
3162
  const subdivisions = options.subdivisions ?? DEFAULT_SUBDIVISIONS;
2687
- const size = new Vector310(tile.x, 1, tile.y);
3163
+ const size = new Vector311(tile.x, 1, tile.y);
2688
3164
  const heightData = options._builders?.meshBuilder?.heightData;
2689
- const scale2 = new Vector310(size.x, 1, size.z);
3165
+ const scale2 = new Vector311(size.x, 1, size.z);
2690
3166
  let colliderDesc = ColliderDesc4.heightfield(
2691
3167
  subdivisions,
2692
3168
  subdivisions,
@@ -2701,10 +3177,10 @@ var PlaneMeshBuilder = class extends EntityMeshBuilder {
2701
3177
  columnsRows = /* @__PURE__ */ new Map();
2702
3178
  subdivisions = DEFAULT_SUBDIVISIONS;
2703
3179
  build(options) {
2704
- const tile = options.tile ?? new Vector23(1, 1);
3180
+ const tile = options.tile ?? new Vector24(1, 1);
2705
3181
  const subdivisions = options.subdivisions ?? DEFAULT_SUBDIVISIONS;
2706
3182
  this.subdivisions = subdivisions;
2707
- const size = new Vector310(tile.x, 1, tile.y);
3183
+ const size = new Vector311(tile.x, 1, tile.y);
2708
3184
  const heightScale = options.heightScale ?? 1;
2709
3185
  const geometry = new XZPlaneGeometry(size.x, size.z, subdivisions, subdivisions);
2710
3186
  const vertexGeometry = new PlaneGeometry(size.x, size.z, subdivisions, subdivisions);
@@ -2773,12 +3249,13 @@ function createPlane(...args) {
2773
3249
  BuilderClass: PlaneBuilder,
2774
3250
  MeshBuilderClass: PlaneMeshBuilder,
2775
3251
  CollisionBuilderClass: PlaneCollisionBuilder,
2776
- entityType: ZylemPlane.type
3252
+ entityType: ZylemPlane.type,
3253
+ cloneFactory: (options) => createPlane(options ?? {})
2777
3254
  });
2778
3255
  }
2779
3256
 
2780
3257
  // src/lib/entities/zone.ts
2781
- import { Vector3 as Vector311 } from "three";
3258
+ import { Vector3 as Vector312 } from "three";
2782
3259
 
2783
3260
  // src/lib/game/game-state.ts
2784
3261
  import { proxy as proxy2, subscribe } from "valtio/vanilla";
@@ -2791,7 +3268,7 @@ var state = proxy2({
2791
3268
  // src/lib/entities/zone.ts
2792
3269
  var zoneDefaults = {
2793
3270
  ...commonDefaults,
2794
- size: new Vector311(1, 1, 1),
3271
+ size: new Vector312(1, 1, 1),
2795
3272
  collision: {
2796
3273
  static: true
2797
3274
  }
@@ -2886,23 +3363,34 @@ function createZone(...args) {
2886
3363
  collisionFilter: options.collisionFilter
2887
3364
  })
2888
3365
  );
2889
- return entity;
3366
+ return finalizeEntityCloneSupport(
3367
+ entity,
3368
+ (cloneOptions) => createZone(cloneOptions ?? {})
3369
+ );
2890
3370
  }
2891
3371
 
2892
3372
  // src/lib/entities/actor.ts
2893
3373
  import { ActiveCollisionTypes as ActiveCollisionTypes3, ColliderDesc as ColliderDesc5 } from "@dimforge/rapier3d-compat";
2894
- import { MeshStandardMaterial as MeshStandardMaterial4, Group as Group4, Vector3 as Vector312 } from "three";
3374
+ import {
3375
+ Box3,
3376
+ BufferAttribute,
3377
+ SkinnedMesh,
3378
+ MeshStandardMaterial as MeshStandardMaterial4,
3379
+ Group as Group4,
3380
+ Vector3 as Vector313,
3381
+ Matrix4
3382
+ } from "three";
2895
3383
 
2896
3384
  // src/lib/core/entity-asset-loader.ts
2897
3385
  var EntityAssetLoader = class {
2898
3386
  /**
2899
3387
  * Load a model file (FBX, GLTF, GLB, OBJ) using the asset manager
2900
3388
  */
2901
- async loadFile(file) {
3389
+ async loadFile(file, options) {
2902
3390
  const ext = file.split(".").pop()?.toLowerCase();
2903
3391
  switch (ext) {
2904
3392
  case "fbx": {
2905
- const result = await assetManager.loadFBX(file);
3393
+ const result = await assetManager.loadFBX(file, options);
2906
3394
  return {
2907
3395
  object: result.object,
2908
3396
  animation: result.animations?.[0]
@@ -2910,14 +3398,14 @@ var EntityAssetLoader = class {
2910
3398
  }
2911
3399
  case "gltf":
2912
3400
  case "glb": {
2913
- const result = await assetManager.loadGLTF(file);
3401
+ const result = await assetManager.loadGLTF(file, options);
2914
3402
  return {
2915
3403
  object: result.object,
2916
3404
  gltf: result.gltf
2917
3405
  };
2918
3406
  }
2919
3407
  case "obj": {
2920
- const result = await assetManager.loadOBJ(file);
3408
+ const result = await assetManager.loadOBJ(file, options);
2921
3409
  return {
2922
3410
  object: result.object
2923
3411
  };
@@ -2931,9 +3419,42 @@ var EntityAssetLoader = class {
2931
3419
  // src/lib/entities/delegates/animation.ts
2932
3420
  import {
2933
3421
  AnimationMixer,
3422
+ Bone,
2934
3423
  LoopOnce,
2935
3424
  LoopRepeat
2936
3425
  } from "three";
3426
+ function getTrackTargetName(trackName) {
3427
+ const propertyIndex = trackName.lastIndexOf(".");
3428
+ return propertyIndex >= 0 ? trackName.slice(0, propertyIndex) : trackName;
3429
+ }
3430
+ function shouldStripRootMotion(track, targetByName) {
3431
+ if (!track?.name?.endsWith(".position")) {
3432
+ return false;
3433
+ }
3434
+ const targetName = getTrackTargetName(track.name);
3435
+ const target = targetByName.get(targetName);
3436
+ if (target instanceof Bone) {
3437
+ return !(target.parent instanceof Bone);
3438
+ }
3439
+ return /hips\.position$/i.test(track.name) || /root\.position$/i.test(track.name);
3440
+ }
3441
+ function stripClipRootMotion(clip, targetByName) {
3442
+ for (const track of clip.tracks) {
3443
+ if (!shouldStripRootMotion(track, targetByName)) {
3444
+ continue;
3445
+ }
3446
+ if (!track.values || track.values.length < 3) {
3447
+ continue;
3448
+ }
3449
+ const baseX = track.values[0];
3450
+ const baseZ = track.values[2];
3451
+ for (let i = 0; i < track.values.length; i += 3) {
3452
+ track.values[i] = baseX;
3453
+ track.values[i + 2] = baseZ;
3454
+ }
3455
+ }
3456
+ return clip;
3457
+ }
2937
3458
  var AnimationDelegate = class {
2938
3459
  constructor(target) {
2939
3460
  this.target = target;
@@ -2951,12 +3472,18 @@ var AnimationDelegate = class {
2951
3472
  async loadAnimations(animations) {
2952
3473
  if (!animations.length) return;
2953
3474
  const results = await Promise.all(animations.map((a) => this._assetLoader.loadFile(a.path)));
3475
+ const targetByName = /* @__PURE__ */ new Map();
3476
+ this.target.traverse((node) => {
3477
+ if (node.name) {
3478
+ targetByName.set(node.name, node);
3479
+ }
3480
+ });
2954
3481
  const loadedAnimations = [];
2955
3482
  results.forEach((result, i) => {
2956
3483
  if (result.animation) {
2957
3484
  loadedAnimations.push({
2958
3485
  key: animations[i].key || i.toString(),
2959
- clip: result.animation
3486
+ clip: stripClipRootMotion(result.animation, targetByName)
2960
3487
  });
2961
3488
  }
2962
3489
  });
@@ -3039,11 +3566,6 @@ var AnimationDelegate = class {
3039
3566
  // src/lib/entities/actor.ts
3040
3567
  var actorDefaults = {
3041
3568
  ...commonDefaults,
3042
- collision: {
3043
- static: false,
3044
- size: new Vector312(0.5, 0.5, 0.5),
3045
- position: new Vector312(0, 0, 0)
3046
- },
3047
3569
  material: {
3048
3570
  shader: standardShader
3049
3571
  },
@@ -3051,74 +3573,333 @@ var actorDefaults = {
3051
3573
  models: [],
3052
3574
  collisionShape: "capsule"
3053
3575
  };
3576
+ function getActorScale(options) {
3577
+ return new Vector313(
3578
+ options.scale?.x ?? 1,
3579
+ options.scale?.y ?? 1,
3580
+ options.scale?.z ?? 1
3581
+ );
3582
+ }
3583
+ function getCollisionPosition(options) {
3584
+ const position2 = options.collision?.position;
3585
+ if (!position2) {
3586
+ return null;
3587
+ }
3588
+ return {
3589
+ x: position2.x ?? 0,
3590
+ y: position2.y ?? 0,
3591
+ z: position2.z ?? 0
3592
+ };
3593
+ }
3594
+ function hasExplicitCollisionSize(options) {
3595
+ return Boolean(options.collision?.size);
3596
+ }
3597
+ function warnDynamicTrimeshOnce(actor) {
3598
+ if (actor.hasWarnedDynamicTrimesh) {
3599
+ return;
3600
+ }
3601
+ actor.hasWarnedDynamicTrimesh = true;
3602
+ console.warn(
3603
+ `Actor "${actor.name || actor.uuid}" requested collisionShape="trimesh" on a dynamic body; falling back to bounds.`
3604
+ );
3605
+ }
3606
+ function resolveCollisionShape(shape, isStatic, onDynamicTrimesh) {
3607
+ const normalized = shape === "model" ? "bounds" : shape ?? "capsule";
3608
+ if (normalized === "trimesh" && !isStatic) {
3609
+ onDynamicTrimesh?.();
3610
+ return "bounds";
3611
+ }
3612
+ return normalized;
3613
+ }
3614
+ function tagTrimeshCollider(colliderDesc, vertices, indices) {
3615
+ colliderDesc.__zylemShapeData = {
3616
+ shape: "trimesh",
3617
+ vertices: Array.from(vertices),
3618
+ indices: Array.from(indices)
3619
+ };
3620
+ return colliderDesc;
3621
+ }
3622
+ function createCapsuleColliderFromDimensions(size, translation) {
3623
+ const fullWidth = size.x || 0.5;
3624
+ const fullHeight = size.y || 1;
3625
+ const fullDepth = size.z || 0.5;
3626
+ const radius = Math.max(fullWidth, fullDepth) / 2;
3627
+ const halfTotalHeight = fullHeight / 2;
3628
+ const halfCylinder = Math.max(0, halfTotalHeight - radius);
3629
+ const colliderDesc = ColliderDesc5.capsule(halfCylinder, radius);
3630
+ colliderDesc.setSensor(false);
3631
+ colliderDesc.setTranslation(translation.x, translation.y, translation.z);
3632
+ colliderDesc.activeCollisionTypes = ActiveCollisionTypes3.DEFAULT;
3633
+ return colliderDesc;
3634
+ }
3635
+ function createCapsuleCollider(options) {
3636
+ const size = options.collision?.size ?? options.size ?? { x: 0.5, y: 1, z: 0.5 };
3637
+ const fullHeight = size.y || 1;
3638
+ const collisionPosition = getCollisionPosition(options);
3639
+ return createCapsuleColliderFromDimensions(
3640
+ {
3641
+ x: size.x || 0.5,
3642
+ y: fullHeight,
3643
+ z: size.z || 0.5
3644
+ },
3645
+ collisionPosition ?? { x: 0, y: fullHeight / 2, z: 0 }
3646
+ );
3647
+ }
3648
+ function createExplicitBoxCollider(options) {
3649
+ const collisionSize = options.collision?.size;
3650
+ if (!collisionSize) {
3651
+ return null;
3652
+ }
3653
+ const halfWidth = collisionSize.x / 2;
3654
+ const halfHeight = collisionSize.y / 2;
3655
+ const halfDepth = collisionSize.z / 2;
3656
+ const colliderDesc = ColliderDesc5.cuboid(halfWidth, halfHeight, halfDepth);
3657
+ const collisionPosition = getCollisionPosition(options);
3658
+ colliderDesc.setSensor(false);
3659
+ colliderDesc.setTranslation(
3660
+ collisionPosition?.x ?? 0,
3661
+ collisionPosition?.y ?? halfHeight,
3662
+ collisionPosition?.z ?? 0
3663
+ );
3664
+ colliderDesc.activeCollisionTypes = ActiveCollisionTypes3.DEFAULT;
3665
+ return colliderDesc;
3666
+ }
3667
+ function computeActorSpaceMatrix(child, actorRoot, actorScale) {
3668
+ const actorRootInverse = actorRoot.matrixWorld.clone().invert();
3669
+ const actorSpaceMatrix = actorRootInverse.multiply(child.matrixWorld);
3670
+ return new Matrix4().makeScale(actorScale.x, actorScale.y, actorScale.z).multiply(actorSpaceMatrix);
3671
+ }
3672
+ function computeModelBounds(modelRoot, actorRoot, actorScale) {
3673
+ modelRoot.updateMatrixWorld(true);
3674
+ let foundMesh = false;
3675
+ const aggregated = new Box3();
3676
+ modelRoot.traverse((child) => {
3677
+ if (!child.isMesh) {
3678
+ return;
3679
+ }
3680
+ const mesh = child;
3681
+ const geometry = mesh.geometry;
3682
+ if (!geometry) {
3683
+ return;
3684
+ }
3685
+ let localBounds = null;
3686
+ if (mesh instanceof SkinnedMesh) {
3687
+ mesh.computeBoundingBox();
3688
+ localBounds = mesh.boundingBox?.clone() ?? null;
3689
+ } else {
3690
+ geometry.computeBoundingBox();
3691
+ localBounds = geometry.boundingBox?.clone() ?? null;
3692
+ }
3693
+ if (!localBounds) {
3694
+ return;
3695
+ }
3696
+ const transformedBounds = localBounds.applyMatrix4(computeActorSpaceMatrix(mesh, actorRoot, actorScale));
3697
+ if (!foundMesh) {
3698
+ aggregated.copy(transformedBounds);
3699
+ foundMesh = true;
3700
+ return;
3701
+ }
3702
+ aggregated.union(transformedBounds);
3703
+ });
3704
+ if (!foundMesh || aggregated.isEmpty()) {
3705
+ return null;
3706
+ }
3707
+ const size = new Vector313();
3708
+ const center = new Vector313();
3709
+ aggregated.getSize(size);
3710
+ aggregated.getCenter(center);
3711
+ return { size, center };
3712
+ }
3713
+ function computeModelTrimesh(modelRoot, actorRoot, actorScale) {
3714
+ modelRoot.updateMatrixWorld(true);
3715
+ const vertices = [];
3716
+ const indices = [];
3717
+ const bounds = new Box3();
3718
+ const point = new Vector313();
3719
+ let foundMesh = false;
3720
+ modelRoot.traverse((child) => {
3721
+ if (!child.isMesh) {
3722
+ return;
3723
+ }
3724
+ const mesh = child;
3725
+ const geometry = mesh.geometry;
3726
+ if (!geometry) {
3727
+ return;
3728
+ }
3729
+ const position2 = geometry.getAttribute("position");
3730
+ if (!(position2 instanceof BufferAttribute)) {
3731
+ return;
3732
+ }
3733
+ const transform = computeActorSpaceMatrix(mesh, actorRoot, actorScale);
3734
+ const vertexOffset = vertices.length / 3;
3735
+ for (let i = 0; i < position2.count; i++) {
3736
+ point.set(position2.getX(i), position2.getY(i), position2.getZ(i));
3737
+ point.applyMatrix4(transform);
3738
+ vertices.push(point.x, point.y, point.z);
3739
+ bounds.expandByPoint(point);
3740
+ }
3741
+ const index = geometry.getIndex();
3742
+ if (index) {
3743
+ for (let i = 0; i < index.count; i++) {
3744
+ indices.push(vertexOffset + index.getX(i));
3745
+ }
3746
+ } else {
3747
+ for (let i = 0; i < position2.count; i++) {
3748
+ indices.push(vertexOffset + i);
3749
+ }
3750
+ }
3751
+ foundMesh = true;
3752
+ });
3753
+ if (!foundMesh || vertices.length === 0 || indices.length === 0) {
3754
+ return null;
3755
+ }
3756
+ const center = new Vector313();
3757
+ bounds.getCenter(center);
3758
+ return {
3759
+ vertices: new Float32Array(vertices),
3760
+ indices: new Uint32Array(indices),
3761
+ center
3762
+ };
3763
+ }
3764
+ function createBoundsColliderFromModel(modelRoot, actorRoot, options) {
3765
+ const explicitCollider = createExplicitBoxCollider(options);
3766
+ if (explicitCollider) {
3767
+ return explicitCollider;
3768
+ }
3769
+ if (!modelRoot || !actorRoot) {
3770
+ return createCapsuleCollider(options);
3771
+ }
3772
+ const bounds = computeModelBounds(modelRoot, actorRoot, getActorScale(options));
3773
+ if (!bounds) {
3774
+ return createCapsuleCollider(options);
3775
+ }
3776
+ const colliderDesc = ColliderDesc5.cuboid(
3777
+ bounds.size.x / 2,
3778
+ bounds.size.y / 2,
3779
+ bounds.size.z / 2
3780
+ );
3781
+ const collisionPosition = getCollisionPosition(options);
3782
+ colliderDesc.setSensor(false);
3783
+ colliderDesc.setTranslation(
3784
+ collisionPosition?.x ?? bounds.center.x,
3785
+ collisionPosition?.y ?? bounds.center.y,
3786
+ collisionPosition?.z ?? bounds.center.z
3787
+ );
3788
+ colliderDesc.activeCollisionTypes = ActiveCollisionTypes3.DEFAULT;
3789
+ return colliderDesc;
3790
+ }
3791
+ function createCapsuleColliderFromModel(sizingRoot, sizingActorRoot, alignmentRoot, alignmentActorRoot, options) {
3792
+ if (hasExplicitCollisionSize(options)) {
3793
+ return createCapsuleCollider(options);
3794
+ }
3795
+ if (!sizingRoot || !sizingActorRoot) {
3796
+ return createCapsuleCollider(options);
3797
+ }
3798
+ const sizingBounds = computeModelBounds(
3799
+ sizingRoot,
3800
+ sizingActorRoot,
3801
+ getActorScale(options)
3802
+ );
3803
+ if (!sizingBounds) {
3804
+ return createCapsuleCollider(options);
3805
+ }
3806
+ const collisionPosition = getCollisionPosition(options);
3807
+ let translation = collisionPosition;
3808
+ if (!translation) {
3809
+ const alignmentBounds = alignmentRoot && alignmentActorRoot ? computeModelBounds(alignmentRoot, alignmentActorRoot, getActorScale(options)) : null;
3810
+ translation = alignmentBounds ? {
3811
+ x: alignmentBounds.center.x,
3812
+ y: alignmentBounds.center.y,
3813
+ z: alignmentBounds.center.z
3814
+ } : {
3815
+ x: sizingBounds.center.x,
3816
+ y: sizingBounds.center.y,
3817
+ z: sizingBounds.center.z
3818
+ };
3819
+ }
3820
+ return createCapsuleColliderFromDimensions(
3821
+ {
3822
+ x: sizingBounds.size.x,
3823
+ y: sizingBounds.size.y,
3824
+ z: sizingBounds.size.z
3825
+ },
3826
+ translation
3827
+ );
3828
+ }
3829
+ function createTrimeshColliderFromModel(modelRoot, actorRoot, options) {
3830
+ const explicitCollider = createExplicitBoxCollider(options);
3831
+ if (explicitCollider) {
3832
+ return explicitCollider;
3833
+ }
3834
+ if (!modelRoot || !actorRoot) {
3835
+ return createCapsuleCollider(options);
3836
+ }
3837
+ const trimesh = computeModelTrimesh(modelRoot, actorRoot, getActorScale(options));
3838
+ if (!trimesh) {
3839
+ return createCapsuleCollider(options);
3840
+ }
3841
+ const colliderDesc = tagTrimeshCollider(
3842
+ ColliderDesc5.trimesh(trimesh.vertices, trimesh.indices),
3843
+ trimesh.vertices,
3844
+ trimesh.indices
3845
+ );
3846
+ const collisionPosition = getCollisionPosition(options);
3847
+ if (collisionPosition) {
3848
+ colliderDesc.setTranslation(
3849
+ collisionPosition.x - trimesh.center.x,
3850
+ collisionPosition.y - trimesh.center.y,
3851
+ collisionPosition.z - trimesh.center.z
3852
+ );
3853
+ }
3854
+ colliderDesc.setSensor(false);
3855
+ colliderDesc.activeCollisionTypes = ActiveCollisionTypes3.DEFAULT;
3856
+ return colliderDesc;
3857
+ }
3858
+ function disposeObjectResources(object) {
3859
+ if (!object) {
3860
+ return;
3861
+ }
3862
+ object.traverse((child) => {
3863
+ if (!child.isMesh) {
3864
+ return;
3865
+ }
3866
+ const mesh = child;
3867
+ mesh.geometry?.dispose();
3868
+ if (Array.isArray(mesh.material)) {
3869
+ mesh.material.forEach((material) => material.dispose());
3870
+ } else {
3871
+ mesh.material?.dispose();
3872
+ }
3873
+ });
3874
+ }
3054
3875
  var ActorCollisionBuilder = class extends EntityCollisionBuilder {
3055
3876
  objectModel = null;
3877
+ collisionSource = null;
3056
3878
  collisionShape = "capsule";
3057
3879
  constructor(data) {
3058
3880
  super();
3059
3881
  this.objectModel = data.objectModel;
3882
+ this.collisionSource = data.collisionSource ?? data.objectModel ?? null;
3060
3883
  this.collisionShape = data.collisionShape ?? "capsule";
3061
3884
  }
3062
3885
  collider(options) {
3063
- if (this.collisionShape === "model") {
3064
- return this.createColliderFromModel(this.objectModel, options);
3886
+ const resolvedShape = resolveCollisionShape(
3887
+ this.collisionShape,
3888
+ Boolean(options.collision?.static)
3889
+ );
3890
+ if (resolvedShape === "bounds") {
3891
+ return createBoundsColliderFromModel(this.objectModel, this.objectModel, options);
3065
3892
  }
3066
- return this.createCapsuleCollider(options);
3067
- }
3068
- /**
3069
- * Create a capsule collider based on size options (character controller style).
3070
- */
3071
- createCapsuleCollider(options) {
3072
- const size = options.collision?.size ?? options.size ?? { x: 0.5, y: 1, z: 0.5 };
3073
- const halfHeight = size.y || 1;
3074
- const radius = Math.max(size.x || 0.5, size.z || 0.5);
3075
- let colliderDesc = ColliderDesc5.capsule(halfHeight, radius);
3076
- colliderDesc.setSensor(false);
3077
- colliderDesc.setTranslation(0, halfHeight + radius, 0);
3078
- colliderDesc.activeCollisionTypes = ActiveCollisionTypes3.DEFAULT;
3079
- return colliderDesc;
3080
- }
3081
- /**
3082
- * Create a collider based on model geometry (works with Mesh and SkinnedMesh).
3083
- * If collision.size and collision.position are provided, use those instead of computing from geometry.
3084
- */
3085
- createColliderFromModel(objectModel, options) {
3086
- const collisionSize = options.collision?.size;
3087
- const collisionPosition = options.collision?.position;
3088
- if (collisionSize) {
3089
- const halfWidth = collisionSize.x / 2;
3090
- const halfHeight = collisionSize.y / 2;
3091
- const halfDepth = collisionSize.z / 2;
3092
- let colliderDesc2 = ColliderDesc5.cuboid(halfWidth, halfHeight, halfDepth);
3093
- colliderDesc2.setSensor(false);
3094
- const posX = collisionPosition ? collisionPosition.x : 0;
3095
- const posY = collisionPosition ? collisionPosition.y : halfHeight;
3096
- const posZ = collisionPosition ? collisionPosition.z : 0;
3097
- colliderDesc2.setTranslation(posX, posY, posZ);
3098
- colliderDesc2.activeCollisionTypes = ActiveCollisionTypes3.DEFAULT;
3099
- return colliderDesc2;
3100
- }
3101
- if (!objectModel) return this.createCapsuleCollider(options);
3102
- let foundGeometry = null;
3103
- objectModel.traverse((child) => {
3104
- if (!foundGeometry && child.isMesh) {
3105
- foundGeometry = child.geometry;
3106
- }
3107
- });
3108
- if (!foundGeometry) return this.createCapsuleCollider(options);
3109
- const geometry = foundGeometry;
3110
- geometry.computeBoundingBox();
3111
- const box = geometry.boundingBox;
3112
- if (!box) return this.createCapsuleCollider(options);
3113
- const height = box.max.y - box.min.y;
3114
- const width = box.max.x - box.min.x;
3115
- const depth = box.max.z - box.min.z;
3116
- let colliderDesc = ColliderDesc5.cuboid(width / 2, height / 2, depth / 2);
3117
- colliderDesc.setSensor(false);
3118
- const centerY = (box.max.y + box.min.y) / 2;
3119
- colliderDesc.setTranslation(0, centerY, 0);
3120
- colliderDesc.activeCollisionTypes = ActiveCollisionTypes3.DEFAULT;
3121
- return colliderDesc;
3893
+ if (resolvedShape === "trimesh") {
3894
+ return createTrimeshColliderFromModel(this.objectModel, this.objectModel, options);
3895
+ }
3896
+ return createCapsuleColliderFromModel(
3897
+ this.collisionSource,
3898
+ this.collisionSource,
3899
+ this.objectModel,
3900
+ this.objectModel,
3901
+ options
3902
+ );
3122
3903
  }
3123
3904
  };
3124
3905
  var ActorBuilder = class extends EntityBuilder {
@@ -3130,96 +3911,228 @@ var ACTOR_TYPE = /* @__PURE__ */ Symbol("Actor");
3130
3911
  var ZylemActor = class extends GameEntity {
3131
3912
  static type = ACTOR_TYPE;
3132
3913
  _object = null;
3914
+ _collisionSource = null;
3133
3915
  _animationDelegate = null;
3134
3916
  _modelFileNames = [];
3135
3917
  _assetLoader = new EntityAssetLoader();
3918
+ _loadGeneration = 0;
3919
+ _loadStatus = "idle";
3136
3920
  controlledRotation = false;
3921
+ hasWarnedDynamicTrimesh = false;
3137
3922
  constructor(options) {
3138
3923
  super();
3139
3924
  this.options = { ...actorDefaults, ...options };
3140
3925
  this.prependUpdate(this.actorUpdate.bind(this));
3141
3926
  this.controlledRotation = true;
3142
3927
  }
3143
- /**
3144
- * Initiates model and animation loading in background (deferred).
3145
- * Call returns immediately; assets will be ready on subsequent updates.
3146
- */
3928
+ create() {
3929
+ if (this._loadStatus === "idle" && (this.options.models?.length ?? 0) > 0) {
3930
+ this.load();
3931
+ }
3932
+ return super.create();
3933
+ }
3147
3934
  load() {
3148
- this._modelFileNames = this.options.models || [];
3149
- this.loadModelsDeferred();
3935
+ if (this._loadStatus === "loading" || this._loadStatus === "loaded") {
3936
+ return;
3937
+ }
3938
+ this._modelFileNames = [...this.options.models || []];
3939
+ if (this._modelFileNames.length === 0) {
3940
+ return;
3941
+ }
3942
+ this.disposeRuntimeState(true);
3943
+ this._loadStatus = "loading";
3944
+ this.loadModelsDeferred(++this._loadGeneration);
3150
3945
  }
3151
- /**
3152
- * Returns current data synchronously.
3153
- * May return null values if loading is still in progress.
3154
- */
3155
3946
  data() {
3156
3947
  return {
3157
3948
  animations: this._animationDelegate?.animations,
3158
3949
  objectModel: this._object,
3950
+ collisionSource: this._collisionSource ?? this._object,
3159
3951
  collisionShape: this.options.collisionShape
3160
3952
  };
3161
3953
  }
3954
+ needsDeferredModelCollision() {
3955
+ if (!this.requiresLoadedCollisionSource()) {
3956
+ return false;
3957
+ }
3958
+ return !this.getRuntimeCollisionSource();
3959
+ }
3960
+ synchronizeRuntimeCollider() {
3961
+ const resolvedShape = this.getResolvedCollisionShape();
3962
+ let colliderDesc;
3963
+ if (resolvedShape === "bounds") {
3964
+ colliderDesc = createBoundsColliderFromModel(
3965
+ this._object,
3966
+ this.group ?? this._object,
3967
+ this.options
3968
+ );
3969
+ } else if (resolvedShape === "trimesh") {
3970
+ colliderDesc = createTrimeshColliderFromModel(
3971
+ this._object,
3972
+ this.group ?? this._object,
3973
+ this.options
3974
+ );
3975
+ } else {
3976
+ const collisionSource = this.getRuntimeCollisionSource();
3977
+ colliderDesc = createCapsuleColliderFromModel(
3978
+ collisionSource,
3979
+ collisionSource,
3980
+ this._object,
3981
+ this._object,
3982
+ this.options
3983
+ );
3984
+ }
3985
+ this.colliderDesc = colliderDesc;
3986
+ if (this.colliderDescs.length > 0) {
3987
+ this.colliderDescs[0] = colliderDesc;
3988
+ } else {
3989
+ this.colliderDescs.push(colliderDesc);
3990
+ }
3991
+ }
3162
3992
  actorUpdate(params) {
3163
3993
  this._animationDelegate?.update(params.delta);
3164
3994
  }
3165
- /**
3166
- * Clean up actor resources including animations, models, and groups
3167
- */
3168
3995
  actorDestroy() {
3996
+ this._loadGeneration += 1;
3997
+ this._loadStatus = "idle";
3998
+ this.disposeRuntimeState(true);
3999
+ this._modelFileNames = [];
4000
+ }
4001
+ _cleanup(params) {
4002
+ super._cleanup(params);
4003
+ this.actorDestroy();
4004
+ }
4005
+ playAnimation(animationOptions) {
4006
+ this._animationDelegate?.playAnimation(animationOptions);
4007
+ }
4008
+ get object() {
4009
+ return this._object;
4010
+ }
4011
+ get collisionSource() {
4012
+ return this._collisionSource ?? this._object;
4013
+ }
4014
+ getDebugInfo() {
4015
+ const debugInfo = {
4016
+ type: "Actor",
4017
+ models: this._modelFileNames.length > 0 ? this._modelFileNames : "none",
4018
+ modelLoaded: !!this._object,
4019
+ scale: this.options.scale ? `${this.options.scale.x}, ${this.options.scale.y}, ${this.options.scale.z}` : "1, 1, 1",
4020
+ collisionShape: this.getResolvedCollisionShape()
4021
+ };
3169
4022
  if (this._animationDelegate) {
3170
- this._animationDelegate.dispose();
3171
- this._animationDelegate = null;
4023
+ debugInfo.currentAnimation = this._animationDelegate.currentAnimationKey || "none";
4024
+ debugInfo.animationsCount = this.options.animations?.length || 0;
3172
4025
  }
3173
4026
  if (this._object) {
4027
+ let meshCount = 0;
4028
+ let vertexCount = 0;
3174
4029
  this._object.traverse((child) => {
3175
- if (child.isMesh) {
3176
- const mesh = child;
3177
- mesh.geometry?.dispose();
3178
- if (Array.isArray(mesh.material)) {
3179
- mesh.material.forEach((m) => m.dispose());
3180
- } else if (mesh.material) {
3181
- mesh.material.dispose();
3182
- }
4030
+ if (!child.isMesh) {
4031
+ return;
4032
+ }
4033
+ meshCount++;
4034
+ const geometry = child.geometry;
4035
+ if (geometry?.attributes.position) {
4036
+ vertexCount += geometry.attributes.position.count;
3183
4037
  }
3184
4038
  });
3185
- this._object = null;
4039
+ debugInfo.meshCount = meshCount;
4040
+ debugInfo.vertexCount = vertexCount;
4041
+ }
4042
+ return debugInfo;
4043
+ }
4044
+ getResolvedCollisionShape() {
4045
+ return resolveCollisionShape(
4046
+ this.options.collisionShape,
4047
+ Boolean(this.options.collision?.static),
4048
+ () => warnDynamicTrimeshOnce(this)
4049
+ );
4050
+ }
4051
+ requiresLoadedCollisionSource() {
4052
+ const resolvedShape = this.getResolvedCollisionShape();
4053
+ if (resolvedShape === "bounds" || resolvedShape === "trimesh") {
4054
+ return !hasExplicitCollisionSize(this.options);
4055
+ }
4056
+ if (resolvedShape === "capsule") {
4057
+ return !hasExplicitCollisionSize(this.options) && this.getCollisionSourceSelection().file !== null;
4058
+ }
4059
+ return false;
4060
+ }
4061
+ getRuntimeCollisionSource() {
4062
+ const resolvedShape = this.getResolvedCollisionShape();
4063
+ if (resolvedShape === "bounds" || resolvedShape === "trimesh") {
4064
+ return this._object;
4065
+ }
4066
+ return this._collisionSource ?? this._object;
4067
+ }
4068
+ getCollisionSourceSelection() {
4069
+ const modelFile = this.options.models?.[0] ?? null;
4070
+ const animationFile = this.options.animations?.[0]?.path ?? null;
4071
+ if (animationFile) {
4072
+ return {
4073
+ file: animationFile,
4074
+ reuseVisibleModel: animationFile === modelFile
4075
+ };
4076
+ }
4077
+ return {
4078
+ file: modelFile,
4079
+ reuseVisibleModel: true
4080
+ };
4081
+ }
4082
+ disposeRuntimeState(clearGroup) {
4083
+ if (this._animationDelegate) {
4084
+ this._animationDelegate.dispose();
4085
+ this._animationDelegate = null;
4086
+ }
4087
+ const collisionSource = this._collisionSource;
4088
+ disposeObjectResources(this._object);
4089
+ if (collisionSource && collisionSource !== this._object) {
4090
+ disposeObjectResources(collisionSource);
3186
4091
  }
3187
- if (this.group) {
4092
+ this._object = null;
4093
+ this._collisionSource = null;
4094
+ if (clearGroup && this.group) {
3188
4095
  this.group.clear();
3189
- this.group = null;
4096
+ this.group = void 0;
3190
4097
  }
3191
- this._modelFileNames = [];
3192
4098
  }
3193
- /**
3194
- * Deferred loading - starts async load and updates entity when complete.
3195
- * Called by synchronous load() method.
3196
- */
3197
- loadModelsDeferred() {
3198
- if (this._modelFileNames.length === 0) return;
4099
+ loadModelsDeferred(loadGeneration) {
3199
4100
  this.dispatch("entity:model:loading", {
3200
4101
  entityId: this.uuid,
3201
4102
  files: this._modelFileNames
3202
4103
  });
3203
- const promises = this._modelFileNames.map((file) => this._assetLoader.loadFile(file));
3204
- Promise.all(promises).then((results) => {
3205
- if (results[0]?.object) {
3206
- this._object = results[0].object;
4104
+ const collisionSourceSelection = this.getCollisionSourceSelection();
4105
+ const promises = this._modelFileNames.map(
4106
+ (file) => this._assetLoader.loadFile(file, { clone: true })
4107
+ );
4108
+ const collisionSourcePromise = collisionSourceSelection.file && !collisionSourceSelection.reuseVisibleModel ? this._assetLoader.loadFile(collisionSourceSelection.file, { clone: true }) : Promise.resolve(null);
4109
+ Promise.all([Promise.all(promises), collisionSourcePromise]).then(([results, collisionSourceResult]) => {
4110
+ if (loadGeneration !== this._loadGeneration) {
4111
+ results.forEach((result) => disposeObjectResources(result.object ?? null));
4112
+ if (collisionSourceResult?.object) {
4113
+ disposeObjectResources(collisionSourceResult.object);
4114
+ }
4115
+ return;
3207
4116
  }
4117
+ this._object = results[0]?.object ?? null;
4118
+ this._collisionSource = collisionSourceSelection.reuseVisibleModel ? this._object : collisionSourceResult?.object ?? this._object;
4119
+ this._loadStatus = this._object ? "loaded" : "idle";
3208
4120
  let meshCount = 0;
3209
4121
  if (this._object) {
3210
4122
  this._object.traverse((child) => {
3211
- if (child.isMesh) meshCount++;
4123
+ if (child.isMesh) {
4124
+ meshCount++;
4125
+ }
3212
4126
  });
3213
4127
  this.group = new Group4();
3214
- this.group.attach(this._object);
3215
- this.group.scale.set(
3216
- this.options.scale?.x || 1,
3217
- this.options.scale?.y || 1,
3218
- this.options.scale?.z || 1
3219
- );
4128
+ this.group.add(this._object);
4129
+ this.group.scale.copy(getActorScale(this.options));
3220
4130
  this.applyMaterialOverrides();
3221
4131
  this._animationDelegate = new AnimationDelegate(this._object);
3222
- this._animationDelegate.loadAnimations(this.options.animations || []).then(() => {
4132
+ void this._animationDelegate.loadAnimations(this.options.animations || []).then(() => {
4133
+ if (loadGeneration !== this._loadGeneration) {
4134
+ return;
4135
+ }
3223
4136
  this.dispatch("entity:animation:loaded", {
3224
4137
  entityId: this.uuid,
3225
4138
  animationCount: this.options.animations?.length || 0
@@ -3231,72 +4144,61 @@ var ZylemActor = class extends GameEntity {
3231
4144
  success: !!this._object,
3232
4145
  meshCount
3233
4146
  });
4147
+ }).catch((error) => {
4148
+ if (loadGeneration !== this._loadGeneration) {
4149
+ return;
4150
+ }
4151
+ this._loadStatus = "idle";
4152
+ console.error("Failed to load actor model", error);
4153
+ this.dispatch("entity:model:loaded", {
4154
+ entityId: this.uuid,
4155
+ success: false,
4156
+ meshCount: 0
4157
+ });
3234
4158
  });
3235
4159
  }
3236
- playAnimation(animationOptions) {
3237
- this._animationDelegate?.playAnimation(animationOptions);
3238
- }
3239
- /**
3240
- * Apply material overrides from options to all meshes in the loaded model.
3241
- * Only applies if material options are explicitly specified (not just defaults).
3242
- */
3243
4160
  applyMaterialOverrides() {
3244
4161
  const materialOptions = this.options.material;
3245
- if (!materialOptions || !materialOptions.color && !materialOptions.path) {
4162
+ if (!materialOptions || !materialOptions.color && !materialOptions.path && materialOptions.opacity === void 0) {
4163
+ return;
4164
+ }
4165
+ if (!this._object) {
3246
4166
  return;
3247
4167
  }
3248
- if (!this._object) return;
3249
4168
  this._object.traverse((child) => {
3250
- if (child.isMesh) {
3251
- const mesh = child;
3252
- if (materialOptions.color) {
3253
- const newMaterial = new MeshStandardMaterial4({
3254
- color: materialOptions.color,
3255
- emissiveIntensity: 0.5,
3256
- lightMapIntensity: 0.5,
3257
- fog: true
3258
- });
3259
- mesh.castShadow = true;
3260
- mesh.receiveShadow = true;
3261
- mesh.material = newMaterial;
3262
- }
4169
+ if (!child.isMesh) {
4170
+ return;
3263
4171
  }
3264
- });
3265
- }
3266
- get object() {
3267
- return this._object;
3268
- }
3269
- /**
3270
- * Provide custom debug information for the actor
3271
- * This will be merged with the default debug information
3272
- */
3273
- getDebugInfo() {
3274
- const debugInfo = {
3275
- type: "Actor",
3276
- models: this._modelFileNames.length > 0 ? this._modelFileNames : "none",
3277
- modelLoaded: !!this._object,
3278
- scale: this.options.scale ? `${this.options.scale.x}, ${this.options.scale.y}, ${this.options.scale.z}` : "1, 1, 1"
3279
- };
3280
- if (this._animationDelegate) {
3281
- debugInfo.currentAnimation = this._animationDelegate.currentAnimationKey || "none";
3282
- debugInfo.animationsCount = this.options.animations?.length || 0;
3283
- }
3284
- if (this._object) {
3285
- let meshCount = 0;
3286
- let vertexCount = 0;
3287
- this._object.traverse((child) => {
3288
- if (child.isMesh) {
3289
- meshCount++;
3290
- const geometry = child.geometry;
3291
- if (geometry && geometry.attributes.position) {
3292
- vertexCount += geometry.attributes.position.count;
3293
- }
4172
+ const mesh = child;
4173
+ if (materialOptions.color) {
4174
+ const newMaterial = new MeshStandardMaterial4({
4175
+ color: materialOptions.color,
4176
+ emissiveIntensity: 0.5,
4177
+ lightMapIntensity: 0.5,
4178
+ fog: true,
4179
+ opacity: materialOptions.opacity ?? 1,
4180
+ transparent: materialOptions.opacity !== void 0 && materialOptions.opacity < 1
4181
+ });
4182
+ mesh.castShadow = true;
4183
+ mesh.receiveShadow = true;
4184
+ mesh.material = newMaterial;
4185
+ return;
4186
+ }
4187
+ if (materialOptions.opacity === void 0) {
4188
+ return;
4189
+ }
4190
+ const applyOpacity = (material) => {
4191
+ const clonedMaterial = material.clone();
4192
+ if ("opacity" in clonedMaterial && "transparent" in clonedMaterial) {
4193
+ const transparentMaterial = clonedMaterial;
4194
+ transparentMaterial.opacity = materialOptions.opacity;
4195
+ transparentMaterial.transparent = materialOptions.opacity < 1;
4196
+ transparentMaterial.needsUpdate = true;
3294
4197
  }
3295
- });
3296
- debugInfo.meshCount = meshCount;
3297
- debugInfo.vertexCount = vertexCount;
3298
- }
3299
- return debugInfo;
4198
+ return clonedMaterial;
4199
+ };
4200
+ mesh.material = Array.isArray(mesh.material) ? mesh.material.map(applyOpacity) : applyOpacity(mesh.material);
4201
+ });
3300
4202
  }
3301
4203
  };
3302
4204
  function createActor(...args) {
@@ -3306,12 +4208,13 @@ function createActor(...args) {
3306
4208
  EntityClass: ZylemActor,
3307
4209
  BuilderClass: ActorBuilder,
3308
4210
  CollisionBuilderClass: ActorCollisionBuilder,
3309
- entityType: ZylemActor.type
4211
+ entityType: ZylemActor.type,
4212
+ cloneFactory: (options) => createActor(options ?? {})
3310
4213
  });
3311
4214
  }
3312
4215
 
3313
4216
  // src/lib/entities/text.ts
3314
- import { Color as Color6, Group as Group5, Sprite as ThreeSprite2, SpriteMaterial as SpriteMaterial2, CanvasTexture, LinearFilter, Vector2 as Vector24, ClampToEdgeWrapping } from "three";
4217
+ import { Color as Color7, Group as Group5, Sprite as ThreeSprite2, SpriteMaterial as SpriteMaterial2, CanvasTexture, LinearFilter, Vector2 as Vector25, ClampToEdgeWrapping } from "three";
3315
4218
  var textDefaults = {
3316
4219
  position: void 0,
3317
4220
  text: "",
@@ -3321,7 +4224,7 @@ var textDefaults = {
3321
4224
  backgroundColor: null,
3322
4225
  padding: 4,
3323
4226
  stickToViewport: true,
3324
- screenPosition: new Vector24(24, 24),
4227
+ screenPosition: new Vector25(24, 24),
3325
4228
  zDistance: 1
3326
4229
  };
3327
4230
  var TextBuilder = class extends EntityBuilder {
@@ -3433,7 +4336,7 @@ var ZylemText = class _ZylemText extends GameEntity {
3433
4336
  }
3434
4337
  toCssColor(color) {
3435
4338
  if (typeof color === "string") return color;
3436
- const c = color instanceof Color6 ? color : new Color6(color);
4339
+ const c = color instanceof Color7 ? color : new Color7(color);
3437
4340
  return `#${c.getHexString()}`;
3438
4341
  }
3439
4342
  textSetup(params) {
@@ -3493,7 +4396,7 @@ var ZylemText = class _ZylemText extends GameEntity {
3493
4396
  if (!this._sprite || !this._cameraRef) return;
3494
4397
  const camera = this._cameraRef.camera;
3495
4398
  const { width, height } = this.getResolution();
3496
- const sp = this.options.screenPosition ?? new Vector24(24, 24);
4399
+ const sp = this.options.screenPosition ?? new Vector25(24, 24);
3497
4400
  const { px, py } = this.getScreenPixels(sp, width, height);
3498
4401
  const zDist = Math.max(1e-3, this.options.zDistance ?? 1);
3499
4402
  const { worldHalfW, worldHalfH } = this.computeWorldExtents(camera, zDist);
@@ -3546,12 +4449,13 @@ function createText(...args) {
3546
4449
  defaultConfig: { ...textDefaults },
3547
4450
  EntityClass: ZylemText,
3548
4451
  BuilderClass: TextBuilder,
3549
- entityType: ZylemText.type
4452
+ entityType: ZylemText.type,
4453
+ cloneFactory: (options) => createText(options ?? {})
3550
4454
  });
3551
4455
  }
3552
4456
 
3553
4457
  // src/lib/entities/rect.ts
3554
- import { Color as Color7, Group as Group6, Sprite as ThreeSprite3, SpriteMaterial as SpriteMaterial3, CanvasTexture as CanvasTexture2, LinearFilter as LinearFilter2, Vector2 as Vector25, ClampToEdgeWrapping as ClampToEdgeWrapping2, ShaderMaterial as ShaderMaterial3, Mesh as Mesh6, PlaneGeometry as PlaneGeometry2, Vector3 as Vector313 } from "three";
4458
+ import { Color as Color8, Group as Group6, Sprite as ThreeSprite3, SpriteMaterial as SpriteMaterial3, CanvasTexture as CanvasTexture2, LinearFilter as LinearFilter2, Vector2 as Vector26, ClampToEdgeWrapping as ClampToEdgeWrapping2, ShaderMaterial as ShaderMaterial3, Mesh as Mesh7, PlaneGeometry as PlaneGeometry2, Vector3 as Vector314 } from "three";
3555
4459
  var rectDefaults = {
3556
4460
  position: void 0,
3557
4461
  width: 120,
@@ -3562,9 +4466,9 @@ var rectDefaults = {
3562
4466
  radius: 0,
3563
4467
  padding: 0,
3564
4468
  stickToViewport: true,
3565
- screenPosition: new Vector25(24, 24),
4469
+ screenPosition: new Vector26(24, 24),
3566
4470
  zDistance: 1,
3567
- anchor: new Vector25(0, 0)
4471
+ anchor: new Vector26(0, 0)
3568
4472
  };
3569
4473
  var RectBuilder = class extends EntityBuilder {
3570
4474
  createEntity(options) {
@@ -3685,7 +4589,7 @@ var ZylemRect = class _ZylemRect extends GameEntity {
3685
4589
  }
3686
4590
  toCssColor(color) {
3687
4591
  if (typeof color === "string") return color;
3688
- const c = color instanceof Color7 ? color : new Color7(color);
4592
+ const c = color instanceof Color8 ? color : new Color8(color);
3689
4593
  return `#${c.getHexString()}`;
3690
4594
  }
3691
4595
  /**
@@ -3721,7 +4625,7 @@ var ZylemRect = class _ZylemRect extends GameEntity {
3721
4625
  if (mat.uniforms?.tDiffuse) mat.uniforms.tDiffuse.value = this._texture;
3722
4626
  if (mat.uniforms?.iResolution && this._canvas) mat.uniforms.iResolution.value.set(this._canvas.width, this._canvas.height, 1);
3723
4627
  }
3724
- this._mesh = new Mesh6(new PlaneGeometry2(1, 1), mat);
4628
+ this._mesh = new Mesh7(new PlaneGeometry2(1, 1), mat);
3725
4629
  this.group?.add(this._mesh);
3726
4630
  this._sprite.visible = false;
3727
4631
  }
@@ -3736,10 +4640,10 @@ var ZylemRect = class _ZylemRect extends GameEntity {
3736
4640
  const desiredW = Math.max(2, Math.floor(width));
3737
4641
  const desiredH = Math.max(2, Math.floor(height));
3738
4642
  const changed = desiredW !== (this.options.width ?? 0) || desiredH !== (this.options.height ?? 0);
3739
- this.options.screenPosition = new Vector25(Math.floor(x), Math.floor(y));
4643
+ this.options.screenPosition = new Vector26(Math.floor(x), Math.floor(y));
3740
4644
  this.options.width = desiredW;
3741
4645
  this.options.height = desiredH;
3742
- this.options.anchor = new Vector25(0, 0);
4646
+ this.options.anchor = new Vector26(0, 0);
3743
4647
  if (changed) {
3744
4648
  this.redrawRect();
3745
4649
  }
@@ -3753,8 +4657,8 @@ var ZylemRect = class _ZylemRect extends GameEntity {
3753
4657
  if (!this._sprite || !this._cameraRef) return;
3754
4658
  const camera = this._cameraRef.camera;
3755
4659
  const { width, height } = this.getResolution();
3756
- const px = (this.options.screenPosition ?? new Vector25(24, 24)).x;
3757
- const py = (this.options.screenPosition ?? new Vector25(24, 24)).y;
4660
+ const px = (this.options.screenPosition ?? new Vector26(24, 24)).x;
4661
+ const py = (this.options.screenPosition ?? new Vector26(24, 24)).y;
3758
4662
  const zDist = Math.max(1e-3, this.options.zDistance ?? 1);
3759
4663
  let worldHalfW = 1;
3760
4664
  let worldHalfH = 1;
@@ -3785,7 +4689,7 @@ var ZylemRect = class _ZylemRect extends GameEntity {
3785
4689
  this._sprite.scale.set(scaleX, scaleY, 1);
3786
4690
  if (this._mesh) this._mesh.scale.set(scaleX, scaleY, 1);
3787
4691
  }
3788
- const anchor = this.options.anchor ?? new Vector25(0, 0);
4692
+ const anchor = this.options.anchor ?? new Vector26(0, 0);
3789
4693
  const ax = Math.min(100, Math.max(0, anchor.x)) / 100;
3790
4694
  const ay = Math.min(100, Math.max(0, anchor.y)) / 100;
3791
4695
  const offsetX = (0.5 - ax) * scaleX;
@@ -3808,8 +4712,8 @@ var ZylemRect = class _ZylemRect extends GameEntity {
3808
4712
  }
3809
4713
  if (bounds.world) {
3810
4714
  const { left, right, top, bottom, z = 0 } = bounds.world;
3811
- const tl = this.worldToScreen(new Vector313(left, top, z));
3812
- const br = this.worldToScreen(new Vector313(right, bottom, z));
4715
+ const tl = this.worldToScreen(new Vector314(left, top, z));
4716
+ const br = this.worldToScreen(new Vector314(right, bottom, z));
3813
4717
  const x = Math.min(tl.x, br.x);
3814
4718
  const y = Math.min(tl.y, br.y);
3815
4719
  const width = Math.abs(br.x - tl.x);
@@ -3843,7 +4747,8 @@ function createRect(...args) {
3843
4747
  defaultConfig: { ...rectDefaults },
3844
4748
  EntityClass: ZylemRect,
3845
4749
  BuilderClass: RectBuilder,
3846
- entityType: ZylemRect.type
4750
+ entityType: ZylemRect.type,
4751
+ cloneFactory: (options) => createRect(options ?? {})
3847
4752
  });
3848
4753
  }
3849
4754
 
@@ -3894,7 +4799,10 @@ function createCone(...args) {
3894
4799
  collisionFilter: options.collisionFilter
3895
4800
  })
3896
4801
  );
3897
- return entity;
4802
+ return finalizeEntityCloneSupport(
4803
+ entity,
4804
+ (cloneOptions) => createCone(cloneOptions ?? {})
4805
+ );
3898
4806
  }
3899
4807
 
3900
4808
  // src/lib/entities/pyramid.ts
@@ -3942,7 +4850,10 @@ function createPyramid(...args) {
3942
4850
  collisionFilter: options.collisionFilter
3943
4851
  })
3944
4852
  );
3945
- return entity;
4853
+ return finalizeEntityCloneSupport(
4854
+ entity,
4855
+ (cloneOptions) => createPyramid(cloneOptions ?? {})
4856
+ );
3946
4857
  }
3947
4858
 
3948
4859
  // src/lib/entities/cylinder.ts
@@ -3997,7 +4908,10 @@ function createCylinder(...args) {
3997
4908
  collisionFilter: options.collisionFilter
3998
4909
  })
3999
4910
  );
4000
- return entity;
4911
+ return finalizeEntityCloneSupport(
4912
+ entity,
4913
+ (cloneOptions) => createCylinder(cloneOptions ?? {})
4914
+ );
4001
4915
  }
4002
4916
 
4003
4917
  // src/lib/entities/pill.ts
@@ -4049,7 +4963,10 @@ function createPill(...args) {
4049
4963
  collisionFilter: options.collisionFilter
4050
4964
  })
4051
4965
  );
4052
- return entity;
4966
+ return finalizeEntityCloneSupport(
4967
+ entity,
4968
+ (cloneOptions) => createPill(cloneOptions ?? {})
4969
+ );
4053
4970
  }
4054
4971
  export {
4055
4972
  ZylemBox,