@zylem/game-lib 0.6.0 → 0.6.3

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 (54) hide show
  1. package/README.md +9 -16
  2. package/dist/actions.d.ts +30 -21
  3. package/dist/actions.js +628 -145
  4. package/dist/actions.js.map +1 -1
  5. package/dist/behavior/platformer-3d.d.ts +296 -0
  6. package/dist/behavior/platformer-3d.js +518 -0
  7. package/dist/behavior/platformer-3d.js.map +1 -0
  8. package/dist/behavior/ricochet-2d.d.ts +274 -0
  9. package/dist/behavior/ricochet-2d.js +394 -0
  10. package/dist/behavior/ricochet-2d.js.map +1 -0
  11. package/dist/behavior/screen-wrap.d.ts +86 -0
  12. package/dist/behavior/screen-wrap.js +195 -0
  13. package/dist/behavior/screen-wrap.js.map +1 -0
  14. package/dist/behavior/thruster.d.ts +10 -0
  15. package/dist/behavior/thruster.js +234 -0
  16. package/dist/behavior/thruster.js.map +1 -0
  17. package/dist/behavior/world-boundary-2d.d.ts +141 -0
  18. package/dist/behavior/world-boundary-2d.js +181 -0
  19. package/dist/behavior/world-boundary-2d.js.map +1 -0
  20. package/dist/behavior-descriptor-BWNWmIjv.d.ts +142 -0
  21. package/dist/{blueprints-BOCc3Wve.d.ts → blueprints-BWGz8fII.d.ts} +2 -2
  22. package/dist/camera-B5e4c78l.d.ts +468 -0
  23. package/dist/camera.d.ts +3 -2
  24. package/dist/camera.js +962 -166
  25. package/dist/camera.js.map +1 -1
  26. package/dist/composition-DrzFrbqI.d.ts +218 -0
  27. package/dist/{core-CZhozNRH.d.ts → core-DAkskq6Y.d.ts} +97 -65
  28. package/dist/core.d.ts +12 -6
  29. package/dist/core.js +4449 -1052
  30. package/dist/core.js.map +1 -1
  31. package/dist/{entities-BAxfJOkk.d.ts → entities-DC9ce_vx.d.ts} +154 -45
  32. package/dist/entities.d.ts +5 -2
  33. package/dist/entities.js +2505 -722
  34. package/dist/entities.js.map +1 -1
  35. package/dist/entity-BpbZqg19.d.ts +1100 -0
  36. package/dist/entity-types-DAu8sGJH.d.ts +26 -0
  37. package/dist/global-change-Dc8uCKi2.d.ts +25 -0
  38. package/dist/main.d.ts +472 -29
  39. package/dist/main.js +11877 -6124
  40. package/dist/main.js.map +1 -1
  41. package/dist/{stage-types-CD21XoIU.d.ts → stage-types-BFsm3qsZ.d.ts} +255 -26
  42. package/dist/stage.d.ts +11 -6
  43. package/dist/stage.js +3462 -491
  44. package/dist/stage.js.map +1 -1
  45. package/dist/thruster-DhRaJnoL.d.ts +172 -0
  46. package/dist/world-Be5m1XC1.d.ts +31 -0
  47. package/package.json +21 -4
  48. package/dist/behaviors.d.ts +0 -106
  49. package/dist/behaviors.js +0 -398
  50. package/dist/behaviors.js.map +0 -1
  51. package/dist/camera-CpbDr4-V.d.ts +0 -116
  52. package/dist/entity-COvRtFNG.d.ts +0 -395
  53. package/dist/moveable-B_vyA6cw.d.ts +0 -67
  54. package/dist/transformable-CUhvyuYO.d.ts +0 -67
package/dist/entities.js CHANGED
@@ -1,10 +1,8 @@
1
1
  // src/lib/entities/box.ts
2
- import { ColliderDesc as ColliderDesc2 } from "@dimforge/rapier3d-compat";
3
- import { BoxGeometry, Color as Color4 } from "three";
4
- import { Vector3 as Vector33 } from "three";
2
+ import { Vector3 as Vector38 } from "three";
5
3
 
6
4
  // src/lib/entities/entity.ts
7
- import { ShaderMaterial } from "three";
5
+ import { Mesh, ShaderMaterial, Group } from "three";
8
6
 
9
7
  // src/lib/systems/transformable.system.ts
10
8
  import {
@@ -48,6 +46,9 @@ var BaseNode = class _BaseNode {
48
46
  markedForRemoval = false;
49
47
  /**
50
48
  * Lifecycle callback arrays - use onSetup(), onUpdate(), etc. to add callbacks
49
+ * Uses `any` for the type parameter to avoid invariance issues when subclasses
50
+ * are assigned to BaseNode references. Type safety is enforced by the public
51
+ * onSetup/onUpdate/etc. methods which are typed with `this`.
51
52
  */
52
53
  lifecycleCallbacks = {
53
54
  setup: [],
@@ -161,12 +162,16 @@ var BaseNode = class _BaseNode {
161
162
  if (typeof this._update === "function") {
162
163
  this._update(params);
163
164
  }
165
+ if (typeof this._tickActions === "function") {
166
+ this._tickActions(params.delta);
167
+ }
164
168
  for (const callback of this.lifecycleCallbacks.update) {
165
169
  callback(params);
166
170
  }
167
171
  this.children.forEach((child) => child.nodeUpdate(params));
168
172
  }
169
173
  nodeDestroy(params) {
174
+ if (this.markedForRemoval) return;
170
175
  this.children.forEach((child) => child.nodeDestroy(params));
171
176
  for (const callback of this.lifecycleCallbacks.destroy) {
172
177
  callback(params);
@@ -175,6 +180,12 @@ var BaseNode = class _BaseNode {
175
180
  this._destroy(params);
176
181
  }
177
182
  this.markedForRemoval = true;
183
+ for (const callback of this.lifecycleCallbacks.cleanup) {
184
+ callback(params);
185
+ }
186
+ if (typeof this._cleanup === "function") {
187
+ this._cleanup(params);
188
+ }
178
189
  }
179
190
  async nodeLoaded(params) {
180
191
  if (typeof this._loaded === "function") {
@@ -184,12 +195,13 @@ var BaseNode = class _BaseNode {
184
195
  callback(params);
185
196
  }
186
197
  }
187
- async nodeCleanup(params) {
198
+ nodeCleanup(params) {
199
+ this.children.forEach((child) => child.nodeCleanup?.(params));
188
200
  for (const callback of this.lifecycleCallbacks.cleanup) {
189
201
  callback(params);
190
202
  }
191
203
  if (typeof this._cleanup === "function") {
192
- await this._cleanup(params);
204
+ this._cleanup(params);
193
205
  }
194
206
  }
195
207
  // ─────────────────────────────────────────────────────────────────────────────
@@ -203,136 +215,378 @@ var BaseNode = class _BaseNode {
203
215
  }
204
216
  };
205
217
 
206
- // src/lib/entities/entity.ts
207
- var GameEntity = class extends BaseNode {
208
- behaviors = [];
209
- group;
210
- mesh;
211
- materials;
212
- bodyDesc = null;
213
- body = null;
214
- colliderDesc;
215
- collider;
216
- custom = {};
217
- debugInfo = {};
218
- debugMaterial;
219
- collisionDelegate = {
220
- collision: []
221
- };
222
- collisionType;
223
- behaviorCallbackMap = {
224
- setup: [],
225
- update: [],
226
- destroy: [],
227
- collision: []
228
- };
218
+ // ../../node_modules/.pnpm/mitt@3.0.1/node_modules/mitt/dist/mitt.mjs
219
+ function mitt_default(n) {
220
+ return { all: n = n || /* @__PURE__ */ new Map(), on: function(t, e) {
221
+ var i = n.get(t);
222
+ i ? i.push(e) : n.set(t, [e]);
223
+ }, off: function(t, e) {
224
+ var i = n.get(t);
225
+ i && (e ? i.splice(i.indexOf(e) >>> 0, 1) : n.set(t, []));
226
+ }, emit: function(t, e) {
227
+ var i = n.get(t);
228
+ i && i.slice().map(function(n2) {
229
+ n2(e);
230
+ }), (i = n.get("*")) && i.slice().map(function(n2) {
231
+ n2(t, e);
232
+ });
233
+ } };
234
+ }
235
+
236
+ // src/lib/events/event-emitter-delegate.ts
237
+ var EventEmitterDelegate = class {
238
+ emitter;
239
+ unsubscribes = [];
229
240
  constructor() {
230
- super();
231
- }
232
- create() {
233
- const { position: setupPosition } = this.options;
234
- const { x, y, z } = setupPosition || { x: 0, y: 0, z: 0 };
235
- this.behaviors = [
236
- { component: position, values: { x, y, z } },
237
- { component: scale, values: { x: 0, y: 0, z: 0 } },
238
- { component: rotation, values: { x: 0, y: 0, z: 0, w: 0 } }
239
- ];
240
- this.name = this.options.name || "";
241
- return this;
241
+ this.emitter = mitt_default();
242
242
  }
243
243
  /**
244
- * Add collision callbacks
244
+ * Dispatch an event to all listeners.
245
245
  */
246
- onCollision(...callbacks) {
247
- const existing = this.collisionDelegate.collision ?? [];
248
- this.collisionDelegate.collision = [...existing, ...callbacks];
249
- return this;
246
+ dispatch(event, payload) {
247
+ this.emitter.emit(event, payload);
250
248
  }
251
249
  /**
252
- * Entity-specific setup - runs behavior callbacks
253
- * (User callbacks are handled by BaseNode's lifecycleCallbacks.setup)
250
+ * Subscribe to an event. Returns an unsubscribe function.
254
251
  */
255
- _setup(params) {
256
- this.behaviorCallbackMap.setup.forEach((callback) => {
257
- callback({ ...params, me: this });
258
- });
259
- }
260
- async _loaded(_params) {
252
+ listen(event, handler) {
253
+ this.emitter.on(event, handler);
254
+ const unsub = () => this.emitter.off(event, handler);
255
+ this.unsubscribes.push(unsub);
256
+ return unsub;
261
257
  }
262
258
  /**
263
- * Entity-specific update - updates materials and runs behavior callbacks
264
- * (User callbacks are handled by BaseNode's lifecycleCallbacks.update)
259
+ * Subscribe to all events.
265
260
  */
266
- _update(params) {
267
- this.updateMaterials(params);
268
- this.behaviorCallbackMap.update.forEach((callback) => {
269
- callback({ ...params, me: this });
270
- });
261
+ listenAll(handler) {
262
+ this.emitter.on("*", handler);
263
+ const unsub = () => this.emitter.off("*", handler);
264
+ this.unsubscribes.push(unsub);
265
+ return unsub;
271
266
  }
272
267
  /**
273
- * Entity-specific destroy - runs behavior callbacks
274
- * (User callbacks are handled by BaseNode's lifecycleCallbacks.destroy)
268
+ * Clean up all subscriptions.
275
269
  */
276
- _destroy(params) {
277
- this.behaviorCallbackMap.destroy.forEach((callback) => {
278
- callback({ ...params, me: this });
279
- });
280
- }
281
- async _cleanup(_params) {
282
- }
283
- _collision(other, globals) {
284
- if (this.collisionDelegate.collision?.length) {
285
- const callbacks = this.collisionDelegate.collision;
286
- callbacks.forEach((callback) => {
287
- callback({ entity: this, other, globals });
288
- });
289
- }
290
- this.behaviorCallbackMap.collision.forEach((callback) => {
291
- callback({ entity: this, other, globals });
292
- });
293
- }
294
- addBehavior(behaviorCallback) {
295
- const handler = behaviorCallback.handler;
296
- if (handler) {
297
- this.behaviorCallbackMap[behaviorCallback.type].push(handler);
298
- }
299
- return this;
300
- }
301
- addBehaviors(behaviorCallbacks) {
302
- behaviorCallbacks.forEach((callback) => {
303
- const handler = callback.handler;
304
- if (handler) {
305
- this.behaviorCallbackMap[callback.type].push(handler);
306
- }
307
- });
308
- return this;
270
+ dispose() {
271
+ this.unsubscribes.forEach((fn) => fn());
272
+ this.unsubscribes = [];
273
+ this.emitter.all.clear();
309
274
  }
310
- updateMaterials(params) {
311
- if (!this.materials?.length) {
312
- return;
275
+ };
276
+
277
+ // src/lib/events/zylem-events.ts
278
+ var zylemEventBus = mitt_default();
279
+
280
+ // src/lib/actions/capabilities/transform-store.ts
281
+ import { proxy } from "valtio";
282
+ function createTransformStore(initial) {
283
+ const defaultState = {
284
+ position: { x: 0, y: 0, z: 0 },
285
+ rotation: { x: 0, y: 0, z: 0, w: 1 },
286
+ velocity: { x: 0, y: 0, z: 0 },
287
+ angularVelocity: { x: 0, y: 0, z: 0 },
288
+ dirty: {
289
+ position: false,
290
+ rotation: false,
291
+ velocity: false,
292
+ angularVelocity: false
313
293
  }
314
- for (const material of this.materials) {
315
- if (material instanceof ShaderMaterial) {
316
- if (material.uniforms) {
317
- material.uniforms.iTime && (material.uniforms.iTime.value += params.delta);
318
- }
319
- }
294
+ };
295
+ return proxy({
296
+ ...defaultState,
297
+ ...initial,
298
+ // Ensure dirty flags are properly initialized even if partial initial state
299
+ dirty: {
300
+ ...defaultState.dirty,
301
+ ...initial?.dirty
320
302
  }
303
+ });
304
+ }
305
+
306
+ // src/lib/actions/capabilities/moveable.ts
307
+ import { Vector3 } from "three";
308
+ function moveX(entity, delta) {
309
+ if (!entity.transformStore) return;
310
+ entity.transformStore.velocity.x = delta;
311
+ entity.transformStore.dirty.velocity = true;
312
+ }
313
+ function moveY(entity, delta) {
314
+ if (!entity.transformStore) return;
315
+ entity.transformStore.velocity.y = delta;
316
+ entity.transformStore.dirty.velocity = true;
317
+ }
318
+ function moveZ(entity, delta) {
319
+ if (!entity.transformStore) return;
320
+ entity.transformStore.velocity.z = delta;
321
+ entity.transformStore.dirty.velocity = true;
322
+ }
323
+ function moveXY(entity, deltaX, deltaY) {
324
+ if (!entity.transformStore) return;
325
+ entity.transformStore.velocity.x = deltaX;
326
+ entity.transformStore.velocity.y = deltaY;
327
+ entity.transformStore.dirty.velocity = true;
328
+ }
329
+ function moveXZ(entity, deltaX, deltaZ) {
330
+ if (!entity.transformStore) return;
331
+ entity.transformStore.velocity.x = deltaX;
332
+ entity.transformStore.velocity.z = deltaZ;
333
+ entity.transformStore.dirty.velocity = true;
334
+ }
335
+ function move(entity, vector) {
336
+ 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;
341
+ }
342
+ function resetVelocity(entity) {
343
+ if (!entity.body) return;
344
+ entity.body.setLinvel(new Vector3(0, 0, 0), true);
345
+ entity.body.setLinearDamping(5);
346
+ }
347
+ function moveForwardXY(entity, delta, rotation2DAngle) {
348
+ const deltaX = Math.sin(-rotation2DAngle) * delta;
349
+ const deltaY = Math.cos(-rotation2DAngle) * delta;
350
+ moveXY(entity, deltaX, deltaY);
351
+ }
352
+ function getPosition(entity) {
353
+ if (!entity.body) return null;
354
+ return entity.body.translation();
355
+ }
356
+ function getVelocity(entity) {
357
+ if (!entity.body) return null;
358
+ return entity.body.linvel();
359
+ }
360
+ function setPosition(entity, x, y, z) {
361
+ if (!entity.body) return;
362
+ entity.body.setTranslation({ x, y, z }, true);
363
+ }
364
+ function setPositionX(entity, x) {
365
+ if (!entity.body) return;
366
+ const { y, z } = entity.body.translation();
367
+ entity.body.setTranslation({ x, y, z }, true);
368
+ }
369
+ function setPositionY(entity, y) {
370
+ if (!entity.body) return;
371
+ const { x, z } = entity.body.translation();
372
+ entity.body.setTranslation({ x, y, z }, true);
373
+ }
374
+ function setPositionZ(entity, z) {
375
+ if (!entity.body) return;
376
+ const { x, y } = entity.body.translation();
377
+ entity.body.setTranslation({ x, y, z }, true);
378
+ }
379
+ function wrapAroundXY(entity, boundsX, boundsY) {
380
+ const position2 = getPosition(entity);
381
+ if (!position2) return;
382
+ const { x, y } = position2;
383
+ const newX = x > boundsX ? -boundsX : x < -boundsX ? boundsX : x;
384
+ const newY = y > boundsY ? -boundsY : y < -boundsY ? boundsY : y;
385
+ if (newX !== x || newY !== y) {
386
+ setPosition(entity, newX, newY, 0);
321
387
  }
322
- buildInfo() {
323
- const info = {};
324
- info.name = this.name;
325
- info.uuid = this.uuid;
326
- info.eid = this.eid.toString();
327
- return info;
388
+ }
389
+ function wrapAround3D(entity, boundsX, boundsY, boundsZ) {
390
+ const position2 = getPosition(entity);
391
+ if (!position2) return;
392
+ const { x, y, z } = position2;
393
+ const newX = x > boundsX ? -boundsX : x < -boundsX ? boundsX : x;
394
+ const newY = y > boundsY ? -boundsY : y < -boundsY ? boundsY : y;
395
+ const newZ = z > boundsZ ? -boundsZ : z < -boundsZ ? boundsZ : z;
396
+ if (newX !== x || newY !== y || newZ !== z) {
397
+ setPosition(entity, newX, newY, newZ);
328
398
  }
329
- };
399
+ }
400
+ function makeMoveable(entity) {
401
+ const moveable = entity;
402
+ if (!moveable.transformStore) {
403
+ moveable.transformStore = createTransformStore();
404
+ }
405
+ moveable.moveX = (delta) => moveX(entity, delta);
406
+ moveable.moveY = (delta) => moveY(entity, delta);
407
+ moveable.moveZ = (delta) => moveZ(entity, delta);
408
+ moveable.moveXY = (deltaX, deltaY) => moveXY(entity, deltaX, deltaY);
409
+ moveable.moveXZ = (deltaX, deltaZ) => moveXZ(entity, deltaX, deltaZ);
410
+ moveable.move = (vector) => move(entity, vector);
411
+ moveable.resetVelocity = () => resetVelocity(entity);
412
+ moveable.moveForwardXY = (delta, rotation2DAngle) => moveForwardXY(entity, delta, rotation2DAngle);
413
+ moveable.getPosition = () => getPosition(entity);
414
+ moveable.getVelocity = () => getVelocity(entity);
415
+ moveable.setPosition = (x, y, z) => setPosition(entity, x, y, z);
416
+ moveable.setPositionX = (x) => setPositionX(entity, x);
417
+ moveable.setPositionY = (y) => setPositionY(entity, y);
418
+ moveable.setPositionZ = (z) => setPositionZ(entity, z);
419
+ moveable.wrapAroundXY = (boundsX, boundsY) => wrapAroundXY(entity, boundsX, boundsY);
420
+ moveable.wrapAround3D = (boundsX, boundsY, boundsZ) => wrapAround3D(entity, boundsX, boundsY, boundsZ);
421
+ return moveable;
422
+ }
330
423
 
331
- // src/lib/entities/builder.ts
332
- import { BufferGeometry as BufferGeometry2, Mesh as Mesh3, Color as Color3 } from "three";
424
+ // src/lib/actions/capabilities/rotatable.ts
425
+ import { Euler, Vector3 as Vector32, MathUtils, Quaternion as Quaternion2 } from "three";
426
+ function rotateInDirection(entity, moveVector) {
427
+ if (!entity.body) return;
428
+ const rotate = Math.atan2(-moveVector.x, moveVector.z);
429
+ rotateYEuler(entity, rotate);
430
+ }
431
+ function rotateYEuler(entity, amount) {
432
+ rotateEuler(entity, new Vector32(0, -amount, 0));
433
+ }
434
+ 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);
438
+ }
439
+ function rotateY(entity, delta) {
440
+ if (!entity.transformStore) return;
441
+ const halfAngle = delta / 2;
442
+ const deltaW = Math.cos(halfAngle);
443
+ const deltaY = Math.sin(halfAngle);
444
+ const q = entity.transformStore.rotation;
445
+ const newW = q.w * deltaW - q.y * deltaY;
446
+ const newX = q.x * deltaW + q.z * deltaY;
447
+ const newY = q.y * deltaW + q.w * deltaY;
448
+ const newZ = q.z * deltaW - q.x * deltaY;
449
+ entity.transformStore.rotation.w = newW;
450
+ entity.transformStore.rotation.x = newX;
451
+ entity.transformStore.rotation.y = newY;
452
+ entity.transformStore.rotation.z = newZ;
453
+ entity.transformStore.dirty.rotation = true;
454
+ }
455
+ function rotateX(entity, delta) {
456
+ if (!entity.transformStore) return;
457
+ const halfAngle = delta / 2;
458
+ const deltaW = Math.cos(halfAngle);
459
+ const deltaX = Math.sin(halfAngle);
460
+ const q = entity.transformStore.rotation;
461
+ const newW = q.w * deltaW - q.x * deltaX;
462
+ const newX = q.x * deltaW + q.w * deltaX;
463
+ const newY = q.y * deltaW + q.z * deltaX;
464
+ const newZ = q.z * deltaW - q.y * deltaX;
465
+ entity.transformStore.rotation.w = newW;
466
+ entity.transformStore.rotation.x = newX;
467
+ entity.transformStore.rotation.y = newY;
468
+ entity.transformStore.rotation.z = newZ;
469
+ entity.transformStore.dirty.rotation = true;
470
+ }
471
+ function rotateZ(entity, delta) {
472
+ if (!entity.transformStore) return;
473
+ const halfAngle = delta / 2;
474
+ const deltaW = Math.cos(halfAngle);
475
+ const deltaZ = Math.sin(halfAngle);
476
+ const q = entity.transformStore.rotation;
477
+ const newW = q.w * deltaW - q.z * deltaZ;
478
+ const newX = q.x * deltaW - q.y * deltaZ;
479
+ const newY = q.y * deltaW + q.x * deltaZ;
480
+ const newZ = q.z * deltaW + q.w * deltaZ;
481
+ entity.transformStore.rotation.w = newW;
482
+ entity.transformStore.rotation.x = newX;
483
+ entity.transformStore.rotation.y = newY;
484
+ entity.transformStore.rotation.z = newZ;
485
+ entity.transformStore.dirty.rotation = true;
486
+ }
487
+ function setRotationY(entity, y) {
488
+ if (!entity.transformStore) return;
489
+ const halfAngle = y / 2;
490
+ const w = Math.cos(halfAngle);
491
+ const yComponent = Math.sin(halfAngle);
492
+ entity.transformStore.rotation.w = w;
493
+ entity.transformStore.rotation.x = 0;
494
+ entity.transformStore.rotation.y = yComponent;
495
+ entity.transformStore.rotation.z = 0;
496
+ entity.transformStore.dirty.rotation = true;
497
+ }
498
+ function setRotationDegreesY(entity, y) {
499
+ if (!entity.body) return;
500
+ setRotationY(entity, MathUtils.degToRad(y));
501
+ }
502
+ function setRotationX(entity, x) {
503
+ if (!entity.transformStore) return;
504
+ const halfAngle = x / 2;
505
+ const w = Math.cos(halfAngle);
506
+ const xComponent = Math.sin(halfAngle);
507
+ entity.transformStore.rotation.w = w;
508
+ entity.transformStore.rotation.x = xComponent;
509
+ entity.transformStore.rotation.y = 0;
510
+ entity.transformStore.rotation.z = 0;
511
+ entity.transformStore.dirty.rotation = true;
512
+ }
513
+ function setRotationDegreesX(entity, x) {
514
+ if (!entity.body) return;
515
+ setRotationX(entity, MathUtils.degToRad(x));
516
+ }
517
+ function setRotationZ(entity, z) {
518
+ if (!entity.transformStore) return;
519
+ const halfAngle = z / 2;
520
+ const w = Math.cos(halfAngle);
521
+ const zComponent = Math.sin(halfAngle);
522
+ entity.transformStore.rotation.w = w;
523
+ entity.transformStore.rotation.x = 0;
524
+ entity.transformStore.rotation.y = 0;
525
+ entity.transformStore.rotation.z = zComponent;
526
+ entity.transformStore.dirty.rotation = true;
527
+ }
528
+ function setRotationDegreesZ(entity, z) {
529
+ if (!entity.body) return;
530
+ setRotationZ(entity, MathUtils.degToRad(z));
531
+ }
532
+ function setRotation(entity, x, y, z) {
533
+ if (!entity.transformStore) return;
534
+ const quat = new Quaternion2().setFromEuler(new Euler(x, y, z));
535
+ entity.transformStore.rotation.w = quat.w;
536
+ entity.transformStore.rotation.x = quat.x;
537
+ entity.transformStore.rotation.y = quat.y;
538
+ entity.transformStore.rotation.z = quat.z;
539
+ entity.transformStore.dirty.rotation = true;
540
+ }
541
+ function setRotationDegrees(entity, x, y, z) {
542
+ if (!entity.body) return;
543
+ setRotation(entity, MathUtils.degToRad(x), MathUtils.degToRad(y), MathUtils.degToRad(z));
544
+ }
545
+ function getRotation(entity) {
546
+ if (!entity.body) return null;
547
+ return entity.body.rotation();
548
+ }
549
+ function makeRotatable(entity) {
550
+ const rotatableEntity = entity;
551
+ if (!rotatableEntity.transformStore) {
552
+ rotatableEntity.transformStore = createTransformStore();
553
+ }
554
+ rotatableEntity.rotateInDirection = (moveVector) => rotateInDirection(entity, moveVector);
555
+ rotatableEntity.rotateYEuler = (amount) => rotateYEuler(entity, amount);
556
+ rotatableEntity.rotateEuler = (rotation2) => rotateEuler(entity, rotation2);
557
+ rotatableEntity.rotateX = (delta) => rotateX(entity, delta);
558
+ rotatableEntity.rotateY = (delta) => rotateY(entity, delta);
559
+ rotatableEntity.rotateZ = (delta) => rotateZ(entity, delta);
560
+ rotatableEntity.setRotationY = (y) => setRotationY(entity, y);
561
+ rotatableEntity.setRotationX = (x) => setRotationX(entity, x);
562
+ rotatableEntity.setRotationZ = (z) => setRotationZ(entity, z);
563
+ rotatableEntity.setRotationDegreesY = (y) => setRotationDegreesY(entity, y);
564
+ rotatableEntity.setRotationDegreesX = (x) => setRotationDegreesX(entity, x);
565
+ rotatableEntity.setRotationDegreesZ = (z) => setRotationDegreesZ(entity, z);
566
+ rotatableEntity.setRotationDegrees = (x, y, z) => setRotationDegrees(entity, x, y, z);
567
+ rotatableEntity.setRotation = (x, y, z) => setRotation(entity, x, y, z);
568
+ rotatableEntity.getRotation = () => getRotation(entity);
569
+ return rotatableEntity;
570
+ }
571
+
572
+ // src/lib/actions/capabilities/transformable.ts
573
+ function makeTransformable(entity) {
574
+ const withMovement = makeMoveable(entity);
575
+ const withRotation = makeRotatable(withMovement);
576
+ return withRotation;
577
+ }
578
+
579
+ // src/lib/entities/parts/collision-factories.ts
580
+ import {
581
+ ActiveCollisionTypes as ActiveCollisionTypes2,
582
+ ColliderDesc as ColliderDesc2,
583
+ RigidBodyDesc as RigidBodyDesc2,
584
+ RigidBodyType as RigidBodyType2
585
+ } from "@dimforge/rapier3d-compat";
586
+ import { Vector3 as Vector34 } from "three";
333
587
 
334
588
  // src/lib/collision/collision-builder.ts
335
- import { ActiveCollisionTypes, ColliderDesc, RigidBodyDesc, RigidBodyType, Vector3 } from "@dimforge/rapier3d-compat";
589
+ import { ActiveCollisionTypes, ColliderDesc, RigidBodyDesc, RigidBodyType, Vector3 as Vector33 } from "@dimforge/rapier3d-compat";
336
590
  var typeToGroup = /* @__PURE__ */ new Map();
337
591
  var nextGroupId = 0;
338
592
  function getOrCreateCollisionGroupId(type) {
@@ -354,7 +608,7 @@ function createCollisionFilter(allowedTypes) {
354
608
  var CollisionBuilder = class {
355
609
  static = false;
356
610
  sensor = false;
357
- gravity = new Vector3(0, 0, 0);
611
+ gravity = new Vector33(0, 0, 0);
358
612
  build(options) {
359
613
  const bodyDesc = this.bodyDesc({
360
614
  isDynamicBody: !this.static
@@ -379,7 +633,7 @@ var CollisionBuilder = class {
379
633
  return this;
380
634
  }
381
635
  collider(options) {
382
- const size = options.size ?? new Vector3(1, 1, 1);
636
+ const size = options.size ?? new Vector33(1, 1, 1);
383
637
  const half = { x: size.x / 2, y: size.y / 2, z: size.z / 2 };
384
638
  let colliderDesc = ColliderDesc.cuboid(half.x, half.y, half.z);
385
639
  return colliderDesc;
@@ -391,283 +645,484 @@ var CollisionBuilder = class {
391
645
  }
392
646
  };
393
647
 
394
- // src/lib/graphics/mesh.ts
395
- import { Mesh as Mesh2 } from "three";
396
- var MeshBuilder = class {
397
- _build(meshOptions, geometry, materials) {
398
- const { batched, material } = meshOptions;
399
- if (batched) {
400
- console.warn("warning: mesh batching is not implemented");
401
- }
402
- const mesh = new Mesh2(geometry, materials.at(-1));
403
- mesh.position.set(0, 0, 0);
404
- mesh.castShadow = true;
405
- mesh.receiveShadow = true;
406
- return mesh;
648
+ // src/lib/entities/parts/collision-factories.ts
649
+ function isCollisionComponent(obj) {
650
+ return obj && obj.__kind === "collision";
651
+ }
652
+ function buildBodyDesc(isStatic) {
653
+ const type = isStatic ? RigidBodyType2.Fixed : RigidBodyType2.Dynamic;
654
+ return new RigidBodyDesc2(type).setTranslation(0, 0, 0).setGravityScale(1).setCanSleep(false).setCcdEnabled(true);
655
+ }
656
+ function applyCollisionOptions(colliderDesc, opts) {
657
+ if (opts.offset) {
658
+ colliderDesc.setTranslation(opts.offset.x, opts.offset.y, opts.offset.z);
407
659
  }
408
- _postBuild() {
409
- return;
660
+ if (opts.sensor) {
661
+ colliderDesc.setSensor(true);
662
+ colliderDesc.activeCollisionTypes = ActiveCollisionTypes2.KINEMATIC_FIXED;
410
663
  }
411
- };
412
-
413
- // src/lib/graphics/material.ts
414
- import {
415
- Color as Color2,
416
- MeshPhongMaterial,
417
- MeshStandardMaterial,
418
- RepeatWrapping,
419
- ShaderMaterial as ShaderMaterial2,
420
- TextureLoader,
421
- Vector2,
422
- Vector3 as Vector32
423
- } from "three";
424
-
425
- // src/lib/core/utility/strings.ts
426
- function sortedStringify(obj) {
427
- const sortedObj = Object.keys(obj).sort().reduce((acc, key) => {
428
- acc[key] = obj[key];
429
- return acc;
430
- }, {});
431
- return JSON.stringify(sortedObj);
432
- }
433
- function shortHash(objString) {
434
- let hash = 0;
435
- for (let i = 0; i < objString.length; i++) {
436
- hash = Math.imul(31, hash) + objString.charCodeAt(i) | 0;
664
+ if (opts.collisionType) {
665
+ const groupId = getOrCreateCollisionGroupId(opts.collisionType);
666
+ let filter = 65535;
667
+ if (opts.collisionFilter) {
668
+ filter = createCollisionFilter(opts.collisionFilter);
669
+ }
670
+ colliderDesc.setCollisionGroups(groupId << 16 | filter);
437
671
  }
438
- return hash.toString(36);
672
+ }
673
+ function makeComponent(colliderDesc, opts) {
674
+ applyCollisionOptions(colliderDesc, opts);
675
+ return {
676
+ __kind: "collision",
677
+ bodyDesc: buildBodyDesc(opts.static ?? false),
678
+ colliderDesc
679
+ };
680
+ }
681
+ function boxCollision(opts = {}) {
682
+ const size = opts.size ?? { x: 1, y: 1, z: 1 };
683
+ const desc = ColliderDesc2.cuboid(size.x / 2, size.y / 2, size.z / 2);
684
+ return makeComponent(desc, opts);
685
+ }
686
+ function sphereCollision(opts = {}) {
687
+ const desc = ColliderDesc2.ball(opts.radius ?? 1);
688
+ return makeComponent(desc, opts);
689
+ }
690
+ function coneCollision(opts = {}) {
691
+ const radius = opts.radius ?? 1;
692
+ const height = opts.height ?? 2;
693
+ const desc = ColliderDesc2.cone(height / 2, radius);
694
+ return makeComponent(desc, opts);
695
+ }
696
+ function pyramidCollision(opts = {}) {
697
+ const radius = opts.radius ?? 1;
698
+ const height = opts.height ?? 2;
699
+ const desc = ColliderDesc2.cone(height / 2, radius);
700
+ return makeComponent(desc, opts);
701
+ }
702
+ function cylinderCollision(opts = {}) {
703
+ const radius = Math.max(opts.radiusTop ?? 1, opts.radiusBottom ?? 1);
704
+ const height = opts.height ?? 2;
705
+ const desc = ColliderDesc2.cylinder(height / 2, radius);
706
+ return makeComponent(desc, opts);
707
+ }
708
+ function pillCollision(opts = {}) {
709
+ const radius = opts.radius ?? 0.5;
710
+ const length = opts.length ?? 1;
711
+ const desc = ColliderDesc2.capsule(length / 2, radius);
712
+ return makeComponent(desc, opts);
713
+ }
714
+ function zoneCollision(opts = {}) {
715
+ const size = opts.size ?? { x: 1, y: 1, z: 1 };
716
+ const desc = ColliderDesc2.cuboid(size.x / 2, size.y / 2, size.z / 2);
717
+ desc.setSensor(true);
718
+ desc.activeCollisionTypes = ActiveCollisionTypes2.KINEMATIC_FIXED;
719
+ return makeComponent(desc, { ...opts, static: opts.static ?? true, sensor: true });
439
720
  }
440
721
 
441
- // src/lib/graphics/shaders/fragment/stars.glsl
442
- var stars_default = "#include <common>\n\nuniform vec3 iResolution;\nuniform float iTime;\nvarying vec2 vUv;\n\n// Credit goes to:\n// https://www.shadertoy.com/view/mtyGWy\n\nvec3 palette( float t ) {\n vec3 a = vec3(0.5, 0.5, 0.5);\n vec3 b = vec3(0.5, 0.5, 0.5);\n vec3 c = vec3(1.0, 1.0, 1.0);\n vec3 d = vec3(0.263,0.416,0.557);\n\n return a + b*cos( 6.28318*(c*t+d) );\n}\n\nvoid mainImage( out vec4 fragColor, in vec2 fragCoord ) {\n vec2 uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;\n vec2 uv0 = uv;\n vec3 finalColor = vec3(0.0);\n \n for (float i = 0.0; i < 4.0; i++) {\n uv = fract(uv * 1.5) - 0.5;\n\n float d = length(uv) * exp(-length(uv0));\n\n vec3 col = palette(length(uv0) + i*.4 + iTime*.4);\n\n d = sin(d*5. + iTime)/5.;\n d = abs(d);\n\n d = pow(0.01 / d, 1.2);\n\n finalColor += col * d;\n }\n \n fragColor = vec4(finalColor, 1.0);\n}\n \nvoid main() {\n mainImage(gl_FragColor, vUv);\n}";
443
-
444
- // src/lib/graphics/shaders/fragment/fire.glsl
445
- var fire_default = "#include <common>\n \nuniform vec3 iResolution;\nuniform float iTime;\nuniform vec2 iOffset;\nvarying vec2 vUv;\n\nfloat snoise(vec3 uv, float res)\n{\n const vec3 s = vec3(1e0, 1e2, 1e3);\n \n uv *= res;\n \n vec3 uv0 = floor(mod(uv, res))*s;\n vec3 uv1 = floor(mod(uv+vec3(1.), res))*s;\n \n vec3 f = fract(uv); f = f*f*(3.0-2.0*f);\n\n vec4 v = vec4(uv0.x+uv0.y+uv0.z, uv1.x+uv0.y+uv0.z,\n uv0.x+uv1.y+uv0.z, uv1.x+uv1.y+uv0.z);\n\n vec4 r = fract(sin(v*1e-1)*1e3);\n float r0 = mix(mix(r.x, r.y, f.x), mix(r.z, r.w, f.x), f.y);\n \n r = fract(sin((v + uv1.z - uv0.z)*1e-1)*1e3);\n float r1 = mix(mix(r.x, r.y, f.x), mix(r.z, r.w, f.x), f.y);\n \n return mix(r0, r1, f.z)*2.-1.;\n}\n\nvoid mainImage( out vec4 fragColor, in vec2 fragCoord ) {\n vec2 p = -.5 + fragCoord.xy / iResolution.xy;\n p.x *= iResolution.x/iResolution.y;\n \n float color = 3.0 - (3.*length(2.*p));\n \n vec3 coord = vec3(atan(p.x,p.y)/6.2832+.5, length(p)*.4, .5);\n \n for(int i = 1; i <= 7; i++)\n {\n float power = pow(2.0, float(i));\n color += (1.5 / power) * snoise(coord + vec3(0.,-iTime*.05, iTime*.01), power*16.);\n }\n fragColor = vec4( color, pow(max(color,0.),2.)*0.4, pow(max(color,0.),3.)*0.15 , 1.0);\n}\n\nvoid main() {\n mainImage(gl_FragColor, vUv);\n}";
722
+ // src/lib/entities/common.ts
723
+ import { Color, Vector3 as Vector35 } from "three";
446
724
 
447
- // src/lib/graphics/shaders/fragment/standard.glsl
448
- var standard_default = "uniform sampler2D tDiffuse;\nvarying vec2 vUv;\n\nvoid main() {\n vec4 texel = texture2D( tDiffuse, vUv );\n\n gl_FragColor = texel;\n}";
725
+ // src/lib/graphics/shaders/vertex/object.shader.ts
726
+ var objectVertexShader = `
727
+ uniform vec2 uvScale;
728
+ varying vec2 vUv;
449
729
 
450
- // src/lib/graphics/shaders/fragment/debug.glsl
451
- var debug_default = "varying vec3 vBarycentric;\nuniform vec3 baseColor;\nuniform vec3 wireframeColor;\nuniform float wireframeThickness;\n\nfloat edgeFactor() {\n vec3 d = fwidth(vBarycentric);\n vec3 a3 = smoothstep(vec3(0.0), d * wireframeThickness, vBarycentric);\n return min(min(a3.x, a3.y), a3.z);\n}\n\nvoid main() {\n float edge = edgeFactor();\n\n vec3 wireColor = wireframeColor;\n\n vec3 finalColor = mix(wireColor, baseColor, edge);\n \n gl_FragColor = vec4(finalColor, 1.0);\n}\n";
730
+ void main() {
731
+ vUv = uv;
732
+ vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
733
+ gl_Position = projectionMatrix * mvPosition;
734
+ }
735
+ `;
452
736
 
453
- // src/lib/graphics/shaders/vertex/object-shader.glsl
454
- var object_shader_default = "uniform vec2 uvScale;\nvarying vec2 vUv;\n\nvoid main() {\n vUv = uv;\n vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );\n gl_Position = projectionMatrix * mvPosition;\n}";
737
+ // src/lib/graphics/shaders/standard.shader.ts
738
+ var fragment = `
739
+ uniform sampler2D tDiffuse;
740
+ varying vec2 vUv;
455
741
 
456
- // src/lib/graphics/shaders/vertex/debug.glsl
457
- var debug_default2 = "varying vec3 vBarycentric;\n\nvoid main() {\n vec3 barycentric = vec3(0.0);\n int index = gl_VertexID % 3;\n if (index == 0) barycentric = vec3(1.0, 0.0, 0.0);\n else if (index == 1) barycentric = vec3(0.0, 1.0, 0.0);\n else barycentric = vec3(0.0, 0.0, 1.0);\n vBarycentric = barycentric;\n \n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n";
742
+ void main() {
743
+ vec4 texel = texture2D( tDiffuse, vUv );
458
744
 
459
- // src/lib/core/preset-shader.ts
460
- var starShader = {
461
- fragment: stars_default,
462
- vertex: object_shader_default
463
- };
464
- var fireShader = {
465
- fragment: fire_default,
466
- vertex: object_shader_default
467
- };
745
+ gl_FragColor = texel;
746
+ }
747
+ `;
468
748
  var standardShader = {
469
- fragment: standard_default,
470
- vertex: object_shader_default
749
+ vertex: objectVertexShader,
750
+ fragment
471
751
  };
472
- var debugShader = {
473
- fragment: debug_default,
474
- vertex: debug_default2
752
+
753
+ // src/lib/entities/common.ts
754
+ var commonDefaults = {
755
+ position: new Vector35(0, 0, 0),
756
+ material: {
757
+ color: new Color("#ffffff"),
758
+ shader: standardShader
759
+ },
760
+ collision: {
761
+ static: false
762
+ }
475
763
  };
476
- var shaderMap = /* @__PURE__ */ new Map();
477
- shaderMap.set("standard", standardShader);
478
- shaderMap.set("fire", fireShader);
479
- shaderMap.set("star", starShader);
480
- shaderMap.set("debug", debugShader);
481
- var preset_shader_default = shaderMap;
764
+ function mergeArgs(args, defaults) {
765
+ const configArgs = args.filter(
766
+ (arg) => !(arg instanceof BaseNode)
767
+ );
768
+ let merged = { ...defaults };
769
+ for (const opt of configArgs) {
770
+ merged = { ...merged, ...opt };
771
+ }
772
+ return merged;
773
+ }
482
774
 
483
- // src/lib/graphics/material.ts
484
- var MaterialBuilder = class _MaterialBuilder {
485
- static batchMaterialMap = /* @__PURE__ */ new Map();
486
- materials = [];
487
- batchMaterial(options, entityType) {
488
- const batchKey = shortHash(sortedStringify(options));
489
- const mappedObject = _MaterialBuilder.batchMaterialMap.get(batchKey);
490
- if (mappedObject) {
491
- const count = mappedObject.geometryMap.get(entityType);
492
- if (count) {
493
- mappedObject.geometryMap.set(entityType, count + 1);
775
+ // src/lib/entities/entity.ts
776
+ var GameEntity = class extends BaseNode {
777
+ behaviors = [];
778
+ group;
779
+ mesh;
780
+ materials;
781
+ bodyDesc = null;
782
+ body = null;
783
+ colliderDesc;
784
+ collider;
785
+ custom = {};
786
+ // Compound entity support: multiple colliders and meshes
787
+ /** All collider descriptions for this entity (including primary) */
788
+ colliderDescs = [];
789
+ /** All colliders attached to this entity's rigid body */
790
+ colliders = [];
791
+ /** Additional meshes for compound visual entities (added via add()) */
792
+ compoundMeshes = [];
793
+ debugInfo = {};
794
+ debugMaterial;
795
+ collisionDelegate = {
796
+ collision: []
797
+ };
798
+ collisionType;
799
+ // Instancing support
800
+ /** Batch key for instanced rendering (null if not instanced) */
801
+ batchKey = null;
802
+ /** Index within the instanced mesh batch */
803
+ instanceId = -1;
804
+ /** Whether this entity uses instanced rendering */
805
+ isInstanced = false;
806
+ // Event delegate for dispatch/listen API
807
+ eventDelegate = new EventEmitterDelegate();
808
+ // Behavior references (new ECS pattern)
809
+ behaviorRefs = [];
810
+ // Transform store for batched physics updates (auto-created in create())
811
+ transformStore;
812
+ // Movement & rotation methods are assigned at runtime by makeTransformable.
813
+ // The `implements` clause above ensures the type contract; declarations below
814
+ // satisfy the compiler while the constructor fills them in.
815
+ moveX;
816
+ moveY;
817
+ moveZ;
818
+ moveXY;
819
+ moveXZ;
820
+ move;
821
+ resetVelocity;
822
+ moveForwardXY;
823
+ getPosition;
824
+ getVelocity;
825
+ setPosition;
826
+ setPositionX;
827
+ setPositionY;
828
+ setPositionZ;
829
+ wrapAroundXY;
830
+ wrapAround3D;
831
+ rotateInDirection;
832
+ rotateYEuler;
833
+ rotateEuler;
834
+ rotateX;
835
+ rotateY;
836
+ rotateZ;
837
+ setRotationY;
838
+ setRotationX;
839
+ setRotationZ;
840
+ setRotationDegrees;
841
+ setRotationDegreesY;
842
+ setRotationDegreesX;
843
+ setRotationDegreesZ;
844
+ setRotation;
845
+ getRotation;
846
+ constructor() {
847
+ super();
848
+ this.transformStore = createTransformStore();
849
+ makeTransformable(this);
850
+ }
851
+ // ─────────────────────────────────────────────────────────────────────────────
852
+ // Composable add() - accepts Mesh, CollisionComponent, or child entities
853
+ // ─────────────────────────────────────────────────────────────────────────────
854
+ /**
855
+ * Add meshes, collision components, or child entities to this entity.
856
+ * Supports fluent chaining: `entity.add(boxMesh()).add(boxCollision())`.
857
+ *
858
+ * - `Mesh`: First mesh becomes the primary mesh; subsequent meshes are
859
+ * compound meshes grouped together.
860
+ * - `CollisionComponent`: First sets bodyDesc + colliderDesc; subsequent
861
+ * add extra colliders to the same rigid body.
862
+ * - `NodeInterface`: Added as a child entity (delegates to BaseNode.add).
863
+ */
864
+ add(...components) {
865
+ for (const component of components) {
866
+ if (component instanceof Mesh) {
867
+ this.addMeshComponent(component);
868
+ } else if (isCollisionComponent(component)) {
869
+ this.addCollisionComponent(component);
494
870
  } else {
495
- mappedObject.geometryMap.set(entityType, 1);
871
+ super.add(component);
496
872
  }
497
- } else {
498
- _MaterialBuilder.batchMaterialMap.set(
499
- batchKey,
500
- {
501
- geometryMap: /* @__PURE__ */ new Map([[entityType, 1]]),
502
- material: this.materials[0]
873
+ }
874
+ return this;
875
+ }
876
+ addMeshComponent(mesh) {
877
+ if (!this.mesh) {
878
+ this.mesh = mesh;
879
+ if (!this.materials) {
880
+ this.materials = [];
881
+ }
882
+ if (mesh.material) {
883
+ const mat = mesh.material;
884
+ if (Array.isArray(mat)) {
885
+ this.materials.push(...mat);
886
+ } else {
887
+ this.materials.push(mat);
503
888
  }
504
- );
889
+ }
890
+ } else {
891
+ this.compoundMeshes.push(mesh);
892
+ if (!this.group) {
893
+ this.group = new Group();
894
+ this.group.add(this.mesh);
895
+ }
896
+ this.group.add(mesh);
505
897
  }
506
898
  }
507
- async build(options, entityType) {
508
- const { path, repeat, color, shader } = options;
509
- if (shader) this.withShader(shader);
510
- if (color) this.withColor(color);
511
- await this.setTexture(path ?? null, repeat);
512
- if (this.materials.length === 0) {
513
- this.setColor(new Color2("#ffffff"));
899
+ addCollisionComponent(collision) {
900
+ if (!this.bodyDesc) {
901
+ this.bodyDesc = collision.bodyDesc;
902
+ this.colliderDesc = collision.colliderDesc;
903
+ this.colliderDescs.push(collision.colliderDesc);
904
+ const pos = this.options?.position ?? { x: 0, y: 0, z: 0 };
905
+ this.bodyDesc.setTranslation(pos.x, pos.y, pos.z);
906
+ } else {
907
+ this.colliderDescs.push(collision.colliderDesc);
514
908
  }
515
- this.batchMaterial(options, entityType);
516
909
  }
517
- withColor(color) {
518
- this.setColor(color);
519
- return this;
910
+ // ─────────────────────────────────────────────────────────────────────────────
911
+ // Actions API -- entity-scoped, self-contained stateful actions
912
+ // ─────────────────────────────────────────────────────────────────────────────
913
+ _actions = [];
914
+ /**
915
+ * Run a fire-and-forget action. Auto-removed when done.
916
+ * @example `me.runAction(moveBy({ x: 10, duration: 0.3 }))`
917
+ */
918
+ runAction(action) {
919
+ this._actions.push(action);
920
+ return action;
520
921
  }
521
- withShader(shaderType) {
522
- this.setShader(shaderType);
523
- return this;
922
+ /**
923
+ * Register a persistent action (throttle, onPress). Not removed when done.
924
+ * @example `const press = entity.action(onPress())`
925
+ */
926
+ action(action) {
927
+ action.persistent = true;
928
+ this._actions.push(action);
929
+ return action;
524
930
  }
525
- async setTexture(texturePath = null, repeat = new Vector2(1, 1)) {
526
- if (!texturePath) {
527
- return;
931
+ /**
932
+ * Tick all registered actions. Called automatically before user onUpdate callbacks.
933
+ *
934
+ * Resets velocity/angularVelocity accumulation before ticking so that
935
+ * actions can compose via `+=` without cross-frame build-up.
936
+ * (The existing move helpers like `moveXY` use `=` which doesn't accumulate,
937
+ * but action composition needs additive writes on a clean slate each frame.)
938
+ */
939
+ _tickActions(delta) {
940
+ if (this._actions.length === 0) return;
941
+ const store = this.transformStore;
942
+ store.velocity.x = 0;
943
+ store.velocity.y = 0;
944
+ store.velocity.z = 0;
945
+ store.dirty.velocity = false;
946
+ store.angularVelocity.x = 0;
947
+ store.angularVelocity.y = 0;
948
+ store.angularVelocity.z = 0;
949
+ store.dirty.angularVelocity = false;
950
+ for (let i = this._actions.length - 1; i >= 0; i--) {
951
+ const act = this._actions[i];
952
+ act.tick(this, delta);
953
+ if (act.done && !act.persistent) {
954
+ this._actions.splice(i, 1);
955
+ }
956
+ }
957
+ if (this._actions.length === 0) {
958
+ store.velocity.x = 0;
959
+ store.velocity.y = 0;
960
+ store.velocity.z = 0;
961
+ store.dirty.velocity = false;
962
+ store.angularVelocity.x = 0;
963
+ store.angularVelocity.y = 0;
964
+ store.angularVelocity.z = 0;
965
+ store.dirty.angularVelocity = false;
528
966
  }
529
- const loader = new TextureLoader();
530
- const texture = await loader.loadAsync(texturePath);
531
- texture.repeat = repeat;
532
- texture.wrapS = RepeatWrapping;
533
- texture.wrapT = RepeatWrapping;
534
- const material = new MeshPhongMaterial({
535
- map: texture
536
- });
537
- this.materials.push(material);
538
- }
539
- setColor(color) {
540
- const material = new MeshStandardMaterial({
541
- color,
542
- emissiveIntensity: 0.5,
543
- lightMapIntensity: 0.5,
544
- fog: true
545
- });
546
- this.materials.push(material);
547
- }
548
- setShader(customShader) {
549
- const { fragment, vertex } = preset_shader_default.get(customShader) ?? preset_shader_default.get("standard");
550
- const shader = new ShaderMaterial2({
551
- uniforms: {
552
- iResolution: { value: new Vector32(1, 1, 1) },
553
- iTime: { value: 0 },
554
- tDiffuse: { value: null },
555
- tDepth: { value: null },
556
- tNormal: { value: null }
557
- },
558
- vertexShader: vertex,
559
- fragmentShader: fragment
560
- });
561
- this.materials.push(shader);
562
967
  }
563
- };
564
-
565
- // src/lib/entities/builder.ts
566
- var EntityCollisionBuilder = class extends CollisionBuilder {
567
- };
568
- var EntityMeshBuilder = class extends MeshBuilder {
569
- build(options) {
570
- return new BufferGeometry2();
968
+ create() {
969
+ const { position: setupPosition } = this.options;
970
+ const { x, y, z } = setupPosition || { x: 0, y: 0, z: 0 };
971
+ this.behaviors = [
972
+ { component: position, values: { x, y, z } },
973
+ { component: scale, values: { x: 0, y: 0, z: 0 } },
974
+ { component: rotation, values: { x: 0, y: 0, z: 0, w: 0 } }
975
+ ];
976
+ this.name = this.options.name || "";
977
+ return this;
571
978
  }
572
- postBuild() {
573
- return;
979
+ /**
980
+ * Add collision callbacks
981
+ */
982
+ onCollision(...callbacks) {
983
+ const existing = this.collisionDelegate.collision ?? [];
984
+ this.collisionDelegate.collision = [...existing, ...callbacks];
985
+ return this;
574
986
  }
575
- };
576
- var EntityBuilder = class {
577
- meshBuilder;
578
- collisionBuilder;
579
- materialBuilder;
580
- options;
581
- entity;
582
- constructor(options, entity, meshBuilder, collisionBuilder) {
583
- this.options = options;
584
- this.entity = entity;
585
- this.meshBuilder = meshBuilder;
586
- this.collisionBuilder = collisionBuilder;
587
- this.materialBuilder = new MaterialBuilder();
588
- const builders = {
589
- meshBuilder: this.meshBuilder,
590
- collisionBuilder: this.collisionBuilder,
591
- materialBuilder: this.materialBuilder
987
+ /**
988
+ * Use a behavior on this entity via typed descriptor.
989
+ * Behaviors will be auto-registered as systems when the entity is spawned.
990
+ * @param descriptor The behavior descriptor (import from behaviors module)
991
+ * @param options Optional overrides for the behavior's default options
992
+ * @returns BehaviorHandle with behavior-specific methods for lazy FSM access
993
+ */
994
+ use(descriptor, options) {
995
+ const behaviorRef = {
996
+ descriptor,
997
+ options: { ...descriptor.defaultOptions, ...options }
998
+ };
999
+ this.behaviorRefs.push(behaviorRef);
1000
+ const baseHandle = {
1001
+ getFSM: () => behaviorRef.fsm ?? null,
1002
+ getOptions: () => behaviorRef.options,
1003
+ ref: behaviorRef
1004
+ };
1005
+ const customMethods = descriptor.createHandle?.(behaviorRef) ?? {};
1006
+ return {
1007
+ ...baseHandle,
1008
+ ...customMethods
592
1009
  };
593
- this.options._builders = builders;
594
1010
  }
595
- withPosition(setupPosition) {
596
- this.options.position = setupPosition;
597
- return this;
1011
+ /**
1012
+ * Get all behavior references attached to this entity.
1013
+ * Used by the stage to auto-register required systems.
1014
+ */
1015
+ getBehaviorRefs() {
1016
+ return this.behaviorRefs;
598
1017
  }
599
- async withMaterial(options, entityType) {
600
- if (this.materialBuilder) {
601
- await this.materialBuilder.build(options, entityType);
1018
+ /**
1019
+ * Entity-specific setup - resets actions for a fresh stage session.
1020
+ * (User callbacks are handled by BaseNode's lifecycleCallbacks.setup)
1021
+ */
1022
+ _setup(params) {
1023
+ for (let i = this._actions.length - 1; i >= 0; i--) {
1024
+ const act = this._actions[i];
1025
+ if (act.done && !act.persistent) {
1026
+ this._actions.splice(i, 1);
1027
+ } else {
1028
+ act.reset();
1029
+ }
602
1030
  }
603
- return this;
604
1031
  }
605
- applyMaterialToGroup(group, materials) {
606
- group.traverse((child) => {
607
- if (child instanceof Mesh3) {
608
- if (child.type === "SkinnedMesh" && materials[0] && !child.material.map) {
609
- child.material = materials[0];
610
- }
611
- }
612
- child.castShadow = true;
613
- child.receiveShadow = true;
614
- });
1032
+ async _loaded(_params) {
615
1033
  }
616
- async build() {
617
- const entity = this.entity;
618
- if (this.materialBuilder) {
619
- entity.materials = this.materialBuilder.materials;
620
- }
621
- if (this.meshBuilder && entity.materials) {
622
- const geometry = this.meshBuilder.build(this.options);
623
- entity.mesh = this.meshBuilder._build(this.options, geometry, entity.materials);
624
- this.meshBuilder.postBuild();
625
- }
626
- if (entity.group && entity.materials) {
627
- this.applyMaterialToGroup(entity.group, entity.materials);
1034
+ /**
1035
+ * Entity-specific update - updates materials.
1036
+ * Transform changes are applied by the stage after all update callbacks complete.
1037
+ * (User callbacks are handled by BaseNode's lifecycleCallbacks.update)
1038
+ */
1039
+ _update(params) {
1040
+ this.updateMaterials(params);
1041
+ }
1042
+ /**
1043
+ * Entity-specific destroy -- reserved for consumer game logic.
1044
+ * Engine-internal resource disposal runs in _cleanup() instead.
1045
+ */
1046
+ _destroy(params) {
1047
+ }
1048
+ /**
1049
+ * Engine-internal resource cleanup -- runs automatically after destroy.
1050
+ * Disposes GPU/DOM resources (meshes, materials, debug material).
1051
+ *
1052
+ * Note: actions, collision callbacks, and behavior refs are intentionally
1053
+ * NOT cleared here -- they are registered by consumer code at module level
1054
+ * and must persist across stage reloads. Actions are reset in _setup().
1055
+ */
1056
+ _cleanup(_params) {
1057
+ this.disposeEvents();
1058
+ for (const m of this.compoundMeshes) {
1059
+ m.geometry?.dispose();
1060
+ if (Array.isArray(m.material)) {
1061
+ m.material.forEach((mat) => mat.dispose());
1062
+ } else {
1063
+ m.material?.dispose();
1064
+ }
628
1065
  }
629
- if (this.collisionBuilder) {
630
- this.collisionBuilder.withCollision(this.options?.collision || {});
631
- const [bodyDesc, colliderDesc] = this.collisionBuilder.build(this.options);
632
- entity.bodyDesc = bodyDesc;
633
- entity.colliderDesc = colliderDesc;
634
- const { x, y, z } = this.options.position || { x: 0, y: 0, z: 0 };
635
- entity.bodyDesc.setTranslation(x, y, z);
1066
+ this.compoundMeshes.length = 0;
1067
+ this.debugMaterial?.dispose();
1068
+ this.debugMaterial = void 0;
1069
+ this.group?.removeFromParent();
1070
+ }
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
+ });
636
1077
  }
637
- if (this.options.collisionType) {
638
- entity.collisionType = this.options.collisionType;
1078
+ }
1079
+ updateMaterials(params) {
1080
+ if (!this.materials?.length) {
1081
+ return;
639
1082
  }
640
- if (this.options.color instanceof Color3) {
641
- const applyColor = (material) => {
642
- const anyMat = material;
643
- if (anyMat && anyMat.color && anyMat.color.set) {
644
- anyMat.color.set(this.options.color);
1083
+ for (const material of this.materials) {
1084
+ if (material instanceof ShaderMaterial) {
1085
+ if (material.uniforms) {
1086
+ material.uniforms.iTime && (material.uniforms.iTime.value += params.delta);
645
1087
  }
646
- };
647
- if (entity.materials?.length) {
648
- for (const mat of entity.materials) applyColor(mat);
649
- }
650
- if (entity.mesh && entity.mesh.material) {
651
- const mat = entity.mesh.material;
652
- if (Array.isArray(mat)) mat.forEach(applyColor);
653
- else applyColor(mat);
654
- }
655
- if (entity.group) {
656
- entity.group.traverse((child) => {
657
- if (child instanceof Mesh3 && child.material) {
658
- const mat = child.material;
659
- if (Array.isArray(mat)) mat.forEach(applyColor);
660
- else applyColor(mat);
661
- }
662
- });
663
1088
  }
664
1089
  }
665
- return entity;
1090
+ }
1091
+ buildInfo() {
1092
+ const info = {};
1093
+ info.name = this.name;
1094
+ info.uuid = this.uuid;
1095
+ info.eid = this.eid.toString();
1096
+ return info;
1097
+ }
1098
+ // ─────────────────────────────────────────────────────────────────────────────
1099
+ // Events API
1100
+ // ─────────────────────────────────────────────────────────────────────────────
1101
+ /**
1102
+ * Dispatch an event from this entity.
1103
+ * Events are emitted both locally and to the global event bus.
1104
+ */
1105
+ dispatch(event, payload) {
1106
+ this.eventDelegate.dispatch(event, payload);
1107
+ zylemEventBus.emit(event, payload);
1108
+ }
1109
+ /**
1110
+ * Listen for events on this entity instance.
1111
+ * @returns Unsubscribe function
1112
+ */
1113
+ listen(event, handler) {
1114
+ return this.eventDelegate.listen(event, handler);
1115
+ }
1116
+ /**
1117
+ * Clean up entity event subscriptions.
1118
+ */
1119
+ disposeEvents() {
1120
+ this.eventDelegate.dispose();
666
1121
  }
667
1122
  };
668
1123
 
669
1124
  // src/lib/entities/delegates/debug.ts
670
- import { MeshStandardMaterial as MeshStandardMaterial2, MeshBasicMaterial, MeshPhongMaterial as MeshPhongMaterial2 } from "three";
1125
+ import { MeshStandardMaterial, MeshBasicMaterial, MeshPhongMaterial } from "three";
671
1126
  function hasDebugInfo(obj) {
672
1127
  return obj && typeof obj.getDebugInfo === "function";
673
1128
  }
@@ -711,7 +1166,7 @@ var DebugDelegate = class {
711
1166
  const info = {
712
1167
  type: material.type
713
1168
  };
714
- if (material instanceof MeshStandardMaterial2 || material instanceof MeshBasicMaterial || material instanceof MeshPhongMaterial2) {
1169
+ if (material instanceof MeshStandardMaterial || material instanceof MeshBasicMaterial || material instanceof MeshPhongMaterial) {
715
1170
  info.color = `#${material.color.getHexString()}`;
716
1171
  info.opacity = material.opacity;
717
1172
  info.transparent = material.transparent;
@@ -761,110 +1216,959 @@ var DebugDelegate = class {
761
1216
  }
762
1217
  };
763
1218
 
764
- // src/lib/entities/delegates/loader.ts
765
- function isLoadable(obj) {
766
- return typeof obj?.load === "function" && typeof obj?.data === "function";
1219
+ // src/lib/entities/parts/mesh-factories.ts
1220
+ import {
1221
+ BoxGeometry,
1222
+ CapsuleGeometry,
1223
+ ConeGeometry,
1224
+ CylinderGeometry,
1225
+ Mesh as Mesh2,
1226
+ MeshStandardMaterial as MeshStandardMaterial3,
1227
+ SphereGeometry
1228
+ } from "three";
1229
+
1230
+ // src/lib/graphics/material.ts
1231
+ import { Color as Color3, Vector2 as Vector22, Vector3 as Vector37 } from "three";
1232
+ import {
1233
+ MeshPhongMaterial as MeshPhongMaterial2,
1234
+ MeshStandardMaterial as MeshStandardMaterial2,
1235
+ RepeatWrapping as RepeatWrapping2,
1236
+ ShaderMaterial as ShaderMaterial2
1237
+ } from "three";
1238
+ import {
1239
+ MeshBasicNodeMaterial,
1240
+ MeshStandardNodeMaterial
1241
+ } from "three/webgpu";
1242
+
1243
+ // src/lib/core/utility/strings.ts
1244
+ function sortedStringify(obj) {
1245
+ const sortedObj = Object.keys(obj).sort().reduce((acc, key) => {
1246
+ acc[key] = obj[key];
1247
+ return acc;
1248
+ }, {});
1249
+ return JSON.stringify(sortedObj);
767
1250
  }
768
- var EntityLoader = class {
769
- entityReference;
770
- constructor(entity) {
771
- this.entityReference = entity;
1251
+ function shortHash(objString) {
1252
+ let hash = 0;
1253
+ for (let i = 0; i < objString.length; i++) {
1254
+ hash = Math.imul(31, hash) + objString.charCodeAt(i) | 0;
772
1255
  }
773
- async load() {
774
- if (this.entityReference.load) {
775
- await this.entityReference.load();
776
- }
1256
+ return hash.toString(36);
1257
+ }
1258
+
1259
+ // src/lib/core/asset-manager.ts
1260
+ import { LoadingManager, Cache } from "three";
1261
+
1262
+ // src/lib/core/loaders/texture-loader.ts
1263
+ import { TextureLoader, RepeatWrapping } from "three";
1264
+ var TextureLoaderAdapter = class {
1265
+ loader;
1266
+ constructor() {
1267
+ this.loader = new TextureLoader();
777
1268
  }
778
- async data() {
779
- if (this.entityReference.data) {
780
- return this.entityReference.data();
1269
+ isSupported(url) {
1270
+ const ext = url.split(".").pop()?.toLowerCase();
1271
+ return ["png", "jpg", "jpeg", "gif", "webp", "bmp", "tga"].includes(ext || "");
1272
+ }
1273
+ async load(url, options) {
1274
+ const texture = await this.loader.loadAsync(url, (event) => {
1275
+ if (options?.onProgress && event.lengthComputable) {
1276
+ options.onProgress(event.loaded / event.total);
1277
+ }
1278
+ });
1279
+ if (options?.repeat) {
1280
+ texture.repeat.copy(options.repeat);
781
1281
  }
782
- return null;
1282
+ texture.wrapS = options?.wrapS ?? RepeatWrapping;
1283
+ texture.wrapT = options?.wrapT ?? RepeatWrapping;
1284
+ return texture;
1285
+ }
1286
+ /**
1287
+ * Clone a texture for independent usage
1288
+ */
1289
+ clone(texture) {
1290
+ const cloned = texture.clone();
1291
+ cloned.needsUpdate = true;
1292
+ return cloned;
783
1293
  }
784
1294
  };
785
1295
 
786
- // src/lib/entities/create.ts
787
- async function createEntity(params) {
788
- const {
789
- args,
790
- defaultConfig,
791
- EntityClass,
792
- BuilderClass,
793
- entityType,
794
- MeshBuilderClass,
795
- CollisionBuilderClass
796
- } = params;
797
- let builder = null;
798
- let configuration;
799
- const configurationIndex = args.findIndex((node) => !(node instanceof BaseNode));
800
- if (configurationIndex !== -1) {
801
- const subArgs = args.splice(configurationIndex, 1);
802
- configuration = subArgs.find((node) => !(node instanceof BaseNode));
1296
+ // src/lib/core/loaders/gltf-loader.ts
1297
+ import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
1298
+ var GLTFLoaderAdapter = class {
1299
+ loader;
1300
+ constructor() {
1301
+ this.loader = new GLTFLoader();
803
1302
  }
804
- const mergedConfiguration = configuration ? { ...defaultConfig, ...configuration } : defaultConfig;
805
- args.push(mergedConfiguration);
806
- for (const arg of args) {
807
- if (arg instanceof BaseNode) {
808
- continue;
1303
+ isSupported(url) {
1304
+ const ext = url.split(".").pop()?.toLowerCase();
1305
+ return ["gltf", "glb"].includes(ext || "");
1306
+ }
1307
+ async load(url, options) {
1308
+ if (options?.useAsyncFetch) {
1309
+ return this.loadWithAsyncFetch(url, options);
809
1310
  }
810
- let entityData = null;
811
- const entity = new EntityClass(arg);
1311
+ return this.loadMainThread(url, options);
1312
+ }
1313
+ /**
1314
+ * Load using native fetch + parseAsync
1315
+ * Both fetch and parsing are async, keeping the main thread responsive
1316
+ */
1317
+ async loadWithAsyncFetch(url, options) {
812
1318
  try {
813
- if (isLoadable(entity)) {
814
- const loader = new EntityLoader(entity);
815
- await loader.load();
816
- entityData = await loader.data();
1319
+ const response = await fetch(url);
1320
+ if (!response.ok) {
1321
+ throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
817
1322
  }
1323
+ const buffer = await response.arrayBuffer();
1324
+ const gltf = await this.loader.parseAsync(buffer, url);
1325
+ return {
1326
+ object: gltf.scene,
1327
+ animations: gltf.animations,
1328
+ gltf
1329
+ };
818
1330
  } catch (error) {
819
- console.error("Error creating entity with loader:", error);
1331
+ console.error(`Async fetch GLTF load failed for ${url}, falling back to loader.load():`, error);
1332
+ return this.loadMainThread(url, options);
820
1333
  }
821
- builder = new BuilderClass(
822
- arg,
823
- entity,
824
- MeshBuilderClass ? new MeshBuilderClass(entityData) : null,
825
- CollisionBuilderClass ? new CollisionBuilderClass(entityData) : null
826
- );
827
- if (arg.material) {
828
- await builder.withMaterial(arg.material, entityType);
1334
+ }
1335
+ async loadMainThread(url, options) {
1336
+ return new Promise((resolve, reject) => {
1337
+ this.loader.load(
1338
+ url,
1339
+ (gltf) => {
1340
+ resolve({
1341
+ object: gltf.scene,
1342
+ animations: gltf.animations,
1343
+ gltf
1344
+ });
1345
+ },
1346
+ (event) => {
1347
+ if (options?.onProgress && event.lengthComputable) {
1348
+ options.onProgress(event.loaded / event.total);
1349
+ }
1350
+ },
1351
+ (error) => reject(error)
1352
+ );
1353
+ });
1354
+ }
1355
+ /**
1356
+ * Clone a loaded GLTF scene for reuse
1357
+ */
1358
+ clone(result) {
1359
+ return {
1360
+ object: result.object.clone(),
1361
+ animations: result.animations?.map((anim) => anim.clone()),
1362
+ gltf: result.gltf
1363
+ };
1364
+ }
1365
+ };
1366
+
1367
+ // src/lib/core/loaders/fbx-loader.ts
1368
+ import { FBXLoader } from "three/addons/loaders/FBXLoader.js";
1369
+ var FBXLoaderAdapter = class {
1370
+ loader;
1371
+ constructor() {
1372
+ this.loader = new FBXLoader();
1373
+ }
1374
+ isSupported(url) {
1375
+ const ext = url.split(".").pop()?.toLowerCase();
1376
+ return ext === "fbx";
1377
+ }
1378
+ async load(url, options) {
1379
+ if (options?.useAsyncFetch) {
1380
+ return this.loadWithAsyncFetch(url, options);
829
1381
  }
1382
+ return this.loadMainThread(url, options);
830
1383
  }
831
- if (!builder) {
832
- throw new Error(`missing options for ${String(entityType)}, builder is not initialized.`);
1384
+ /**
1385
+ * Load using native fetch + parse
1386
+ */
1387
+ async loadWithAsyncFetch(url, _options) {
1388
+ try {
1389
+ const response = await fetch(url);
1390
+ if (!response.ok) {
1391
+ throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
1392
+ }
1393
+ const buffer = await response.arrayBuffer();
1394
+ const object = this.loader.parse(buffer, url);
1395
+ return {
1396
+ object,
1397
+ animations: object.animations || []
1398
+ };
1399
+ } catch (error) {
1400
+ console.error(`Async fetch FBX load failed for ${url}, falling back to loader.load():`, error);
1401
+ return this.loadMainThread(url, _options);
1402
+ }
833
1403
  }
834
- return await builder.build();
835
- }
1404
+ async loadMainThread(url, options) {
1405
+ return new Promise((resolve, reject) => {
1406
+ this.loader.load(
1407
+ url,
1408
+ (object) => {
1409
+ resolve({
1410
+ object,
1411
+ animations: object.animations || []
1412
+ });
1413
+ },
1414
+ (event) => {
1415
+ if (options?.onProgress && event.lengthComputable) {
1416
+ options.onProgress(event.loaded / event.total);
1417
+ }
1418
+ },
1419
+ (error) => reject(error)
1420
+ );
1421
+ });
1422
+ }
1423
+ /**
1424
+ * Clone a loaded FBX object for reuse
1425
+ */
1426
+ clone(result) {
1427
+ return {
1428
+ object: result.object.clone(),
1429
+ animations: result.animations?.map((anim) => anim.clone())
1430
+ };
1431
+ }
1432
+ };
836
1433
 
837
- // src/lib/entities/box.ts
838
- var boxDefaults = {
839
- size: new Vector33(1, 1, 1),
840
- position: new Vector33(0, 0, 0),
841
- collision: {
842
- static: false
843
- },
844
- material: {
845
- color: new Color4("#ffffff"),
846
- shader: "standard"
1434
+ // src/lib/core/loaders/obj-loader.ts
1435
+ import { OBJLoader } from "three/addons/loaders/OBJLoader.js";
1436
+ import { MTLLoader } from "three/addons/loaders/MTLLoader.js";
1437
+ var OBJLoaderAdapter = class {
1438
+ loader;
1439
+ mtlLoader;
1440
+ constructor() {
1441
+ this.loader = new OBJLoader();
1442
+ this.mtlLoader = new MTLLoader();
1443
+ }
1444
+ isSupported(url) {
1445
+ const ext = url.split(".").pop()?.toLowerCase();
1446
+ return ext === "obj";
1447
+ }
1448
+ async load(url, options) {
1449
+ if (options?.mtlPath) {
1450
+ await this.loadMTL(options.mtlPath);
1451
+ }
1452
+ return new Promise((resolve, reject) => {
1453
+ this.loader.load(
1454
+ url,
1455
+ (object) => {
1456
+ resolve({
1457
+ object,
1458
+ animations: []
1459
+ });
1460
+ },
1461
+ (event) => {
1462
+ if (options?.onProgress && event.lengthComputable) {
1463
+ options.onProgress(event.loaded / event.total);
1464
+ }
1465
+ },
1466
+ (error) => reject(error)
1467
+ );
1468
+ });
1469
+ }
1470
+ async loadMTL(url) {
1471
+ return new Promise((resolve, reject) => {
1472
+ this.mtlLoader.load(
1473
+ url,
1474
+ (materials) => {
1475
+ materials.preload();
1476
+ this.loader.setMaterials(materials);
1477
+ resolve();
1478
+ },
1479
+ void 0,
1480
+ (error) => reject(error)
1481
+ );
1482
+ });
1483
+ }
1484
+ /**
1485
+ * Clone a loaded OBJ object for reuse
1486
+ */
1487
+ clone(result) {
1488
+ return {
1489
+ object: result.object.clone(),
1490
+ animations: []
1491
+ };
847
1492
  }
848
1493
  };
849
- var BoxCollisionBuilder = class extends EntityCollisionBuilder {
850
- collider(options) {
851
- const size = options.size || new Vector33(1, 1, 1);
852
- const half = { x: size.x / 2, y: size.y / 2, z: size.z / 2 };
853
- let colliderDesc = ColliderDesc2.cuboid(half.x, half.y, half.z);
854
- return colliderDesc;
1494
+
1495
+ // src/lib/core/loaders/audio-loader.ts
1496
+ import { AudioLoader } from "three";
1497
+ var AudioLoaderAdapter = class {
1498
+ loader;
1499
+ constructor() {
1500
+ this.loader = new AudioLoader();
1501
+ }
1502
+ isSupported(url) {
1503
+ const ext = url.split(".").pop()?.toLowerCase();
1504
+ return ["mp3", "ogg", "wav", "flac", "aac", "m4a"].includes(ext || "");
1505
+ }
1506
+ async load(url, options) {
1507
+ return new Promise((resolve, reject) => {
1508
+ this.loader.load(
1509
+ url,
1510
+ (buffer) => resolve(buffer),
1511
+ (event) => {
1512
+ if (options?.onProgress && event.lengthComputable) {
1513
+ options.onProgress(event.loaded / event.total);
1514
+ }
1515
+ },
1516
+ (error) => reject(error)
1517
+ );
1518
+ });
855
1519
  }
856
1520
  };
857
- var BoxMeshBuilder = class extends EntityMeshBuilder {
858
- build(options) {
859
- const size = options.size ?? new Vector33(1, 1, 1);
860
- return new BoxGeometry(size.x, size.y, size.z);
1521
+
1522
+ // src/lib/core/loaders/file-loader.ts
1523
+ import { FileLoader } from "three";
1524
+ var FileLoaderAdapter = class {
1525
+ loader;
1526
+ constructor() {
1527
+ this.loader = new FileLoader();
1528
+ }
1529
+ isSupported(_url) {
1530
+ return true;
1531
+ }
1532
+ async load(url, options) {
1533
+ const responseType = options?.responseType ?? "text";
1534
+ this.loader.setResponseType(responseType);
1535
+ return new Promise((resolve, reject) => {
1536
+ this.loader.load(
1537
+ url,
1538
+ (data) => resolve(data),
1539
+ (event) => {
1540
+ if (options?.onProgress && event.lengthComputable) {
1541
+ options.onProgress(event.loaded / event.total);
1542
+ }
1543
+ },
1544
+ (error) => reject(error)
1545
+ );
1546
+ });
861
1547
  }
862
1548
  };
863
- var BoxBuilder = class extends EntityBuilder {
864
- createEntity(options) {
865
- return new ZylemBox(options);
1549
+ var JsonLoaderAdapter = class {
1550
+ fileLoader;
1551
+ constructor() {
1552
+ this.fileLoader = new FileLoaderAdapter();
1553
+ }
1554
+ isSupported(url) {
1555
+ const ext = url.split(".").pop()?.toLowerCase();
1556
+ return ext === "json";
1557
+ }
1558
+ async load(url, options) {
1559
+ const data = await this.fileLoader.load(url, { ...options, responseType: "json" });
1560
+ return data;
866
1561
  }
867
1562
  };
1563
+
1564
+ // src/lib/core/asset-manager.ts
1565
+ var AssetManager = class _AssetManager {
1566
+ static instance = null;
1567
+ // Caches for different asset types
1568
+ textureCache = /* @__PURE__ */ new Map();
1569
+ modelCache = /* @__PURE__ */ new Map();
1570
+ audioCache = /* @__PURE__ */ new Map();
1571
+ fileCache = /* @__PURE__ */ new Map();
1572
+ jsonCache = /* @__PURE__ */ new Map();
1573
+ // Loaders
1574
+ textureLoader;
1575
+ gltfLoader;
1576
+ fbxLoader;
1577
+ objLoader;
1578
+ audioLoader;
1579
+ fileLoader;
1580
+ jsonLoader;
1581
+ // Loading manager for progress tracking
1582
+ loadingManager;
1583
+ // Event emitter
1584
+ events;
1585
+ // Stats
1586
+ stats = {
1587
+ texturesLoaded: 0,
1588
+ modelsLoaded: 0,
1589
+ audioLoaded: 0,
1590
+ filesLoaded: 0,
1591
+ cacheHits: 0,
1592
+ cacheMisses: 0
1593
+ };
1594
+ constructor() {
1595
+ this.textureLoader = new TextureLoaderAdapter();
1596
+ this.gltfLoader = new GLTFLoaderAdapter();
1597
+ this.fbxLoader = new FBXLoaderAdapter();
1598
+ this.objLoader = new OBJLoaderAdapter();
1599
+ this.audioLoader = new AudioLoaderAdapter();
1600
+ this.fileLoader = new FileLoaderAdapter();
1601
+ this.jsonLoader = new JsonLoaderAdapter();
1602
+ this.loadingManager = new LoadingManager();
1603
+ this.loadingManager.onProgress = (url, loaded, total) => {
1604
+ this.events.emit("batch:progress", { loaded, total });
1605
+ };
1606
+ this.events = mitt_default();
1607
+ Cache.enabled = true;
1608
+ }
1609
+ /**
1610
+ * Get the singleton instance
1611
+ */
1612
+ static getInstance() {
1613
+ if (!_AssetManager.instance) {
1614
+ _AssetManager.instance = new _AssetManager();
1615
+ }
1616
+ return _AssetManager.instance;
1617
+ }
1618
+ /**
1619
+ * Reset the singleton (useful for testing)
1620
+ */
1621
+ static resetInstance() {
1622
+ if (_AssetManager.instance) {
1623
+ _AssetManager.instance.clearCache();
1624
+ _AssetManager.instance = null;
1625
+ }
1626
+ }
1627
+ // ==================== TEXTURE LOADING ====================
1628
+ /**
1629
+ * Load a texture with caching
1630
+ */
1631
+ async loadTexture(url, options) {
1632
+ return this.loadWithCache(
1633
+ url,
1634
+ "texture" /* TEXTURE */,
1635
+ this.textureCache,
1636
+ () => this.textureLoader.load(url, options),
1637
+ options,
1638
+ (texture) => {
1639
+ if (!options?.clone) return texture;
1640
+ const cloned = this.textureLoader.clone(texture);
1641
+ if (options.repeat) {
1642
+ cloned.repeat.copy(options.repeat);
1643
+ }
1644
+ if (options.wrapS !== void 0) {
1645
+ cloned.wrapS = options.wrapS;
1646
+ }
1647
+ if (options.wrapT !== void 0) {
1648
+ cloned.wrapT = options.wrapT;
1649
+ }
1650
+ return cloned;
1651
+ }
1652
+ );
1653
+ }
1654
+ // ==================== MODEL LOADING ====================
1655
+ /**
1656
+ * Load a GLTF/GLB model with caching
1657
+ */
1658
+ async loadGLTF(url, options) {
1659
+ return this.loadWithCache(
1660
+ url,
1661
+ "gltf" /* GLTF */,
1662
+ this.modelCache,
1663
+ () => this.gltfLoader.load(url, options),
1664
+ options,
1665
+ (result) => options?.clone ? this.gltfLoader.clone(result) : result
1666
+ );
1667
+ }
1668
+ /**
1669
+ * Load an FBX model with caching
1670
+ */
1671
+ async loadFBX(url, options) {
1672
+ return this.loadWithCache(
1673
+ url,
1674
+ "fbx" /* FBX */,
1675
+ this.modelCache,
1676
+ () => this.fbxLoader.load(url, options),
1677
+ options,
1678
+ (result) => options?.clone ? this.fbxLoader.clone(result) : result
1679
+ );
1680
+ }
1681
+ /**
1682
+ * Load an OBJ model with caching
1683
+ */
1684
+ async loadOBJ(url, options) {
1685
+ const cacheKey = options?.mtlPath ? `${url}:${options.mtlPath}` : url;
1686
+ return this.loadWithCache(
1687
+ cacheKey,
1688
+ "obj" /* OBJ */,
1689
+ this.modelCache,
1690
+ () => this.objLoader.load(url, options),
1691
+ options,
1692
+ (result) => options?.clone ? this.objLoader.clone(result) : result
1693
+ );
1694
+ }
1695
+ /**
1696
+ * Auto-detect model type and load
1697
+ */
1698
+ async loadModel(url, options) {
1699
+ const ext = url.split(".").pop()?.toLowerCase();
1700
+ switch (ext) {
1701
+ case "gltf":
1702
+ case "glb":
1703
+ return this.loadGLTF(url, options);
1704
+ case "fbx":
1705
+ return this.loadFBX(url, options);
1706
+ case "obj":
1707
+ return this.loadOBJ(url, options);
1708
+ default:
1709
+ throw new Error(`Unsupported model format: ${ext}`);
1710
+ }
1711
+ }
1712
+ // ==================== AUDIO LOADING ====================
1713
+ /**
1714
+ * Load an audio buffer with caching
1715
+ */
1716
+ async loadAudio(url, options) {
1717
+ return this.loadWithCache(
1718
+ url,
1719
+ "audio" /* AUDIO */,
1720
+ this.audioCache,
1721
+ () => this.audioLoader.load(url, options),
1722
+ options
1723
+ );
1724
+ }
1725
+ // ==================== FILE LOADING ====================
1726
+ /**
1727
+ * Load a raw file with caching
1728
+ */
1729
+ async loadFile(url, options) {
1730
+ const cacheKey = options?.responseType ? `${url}:${options.responseType}` : url;
1731
+ return this.loadWithCache(
1732
+ cacheKey,
1733
+ "file" /* FILE */,
1734
+ this.fileCache,
1735
+ () => this.fileLoader.load(url, options),
1736
+ options
1737
+ );
1738
+ }
1739
+ /**
1740
+ * Load a JSON file with caching
1741
+ */
1742
+ async loadJSON(url, options) {
1743
+ return this.loadWithCache(
1744
+ url,
1745
+ "json" /* JSON */,
1746
+ this.jsonCache,
1747
+ () => this.jsonLoader.load(url, options),
1748
+ options
1749
+ );
1750
+ }
1751
+ // ==================== BATCH LOADING ====================
1752
+ /**
1753
+ * Load multiple assets in parallel
1754
+ */
1755
+ async loadBatch(items) {
1756
+ const results = /* @__PURE__ */ new Map();
1757
+ const promises = items.map(async (item) => {
1758
+ try {
1759
+ let result;
1760
+ switch (item.type) {
1761
+ case "texture" /* TEXTURE */:
1762
+ result = await this.loadTexture(item.url, item.options);
1763
+ break;
1764
+ case "gltf" /* GLTF */:
1765
+ result = await this.loadGLTF(item.url, item.options);
1766
+ break;
1767
+ case "fbx" /* FBX */:
1768
+ result = await this.loadFBX(item.url, item.options);
1769
+ break;
1770
+ case "obj" /* OBJ */:
1771
+ result = await this.loadOBJ(item.url, item.options);
1772
+ break;
1773
+ case "audio" /* AUDIO */:
1774
+ result = await this.loadAudio(item.url, item.options);
1775
+ break;
1776
+ case "file" /* FILE */:
1777
+ result = await this.loadFile(item.url, item.options);
1778
+ break;
1779
+ case "json" /* JSON */:
1780
+ result = await this.loadJSON(item.url, item.options);
1781
+ break;
1782
+ default:
1783
+ throw new Error(`Unknown asset type: ${item.type}`);
1784
+ }
1785
+ results.set(item.url, result);
1786
+ } catch (error) {
1787
+ this.events.emit("asset:error", {
1788
+ url: item.url,
1789
+ type: item.type,
1790
+ error
1791
+ });
1792
+ throw error;
1793
+ }
1794
+ });
1795
+ await Promise.all(promises);
1796
+ this.events.emit("batch:complete", { urls: items.map((i) => i.url) });
1797
+ return results;
1798
+ }
1799
+ /**
1800
+ * Preload assets without returning results
1801
+ */
1802
+ async preload(items) {
1803
+ await this.loadBatch(items);
1804
+ }
1805
+ // ==================== CACHE MANAGEMENT ====================
1806
+ /**
1807
+ * Check if an asset is cached
1808
+ */
1809
+ isCached(url) {
1810
+ return this.textureCache.has(url) || this.modelCache.has(url) || this.audioCache.has(url) || this.fileCache.has(url) || this.jsonCache.has(url);
1811
+ }
1812
+ /**
1813
+ * Clear all caches or a specific URL
1814
+ */
1815
+ clearCache(url) {
1816
+ if (url) {
1817
+ this.textureCache.delete(url);
1818
+ this.modelCache.delete(url);
1819
+ this.audioCache.delete(url);
1820
+ this.fileCache.delete(url);
1821
+ this.jsonCache.delete(url);
1822
+ } else {
1823
+ this.textureCache.clear();
1824
+ this.modelCache.clear();
1825
+ this.audioCache.clear();
1826
+ this.fileCache.clear();
1827
+ this.jsonCache.clear();
1828
+ Cache.clear();
1829
+ }
1830
+ }
1831
+ /**
1832
+ * Get cache statistics
1833
+ */
1834
+ getStats() {
1835
+ return { ...this.stats };
1836
+ }
1837
+ // ==================== EVENTS ====================
1838
+ /**
1839
+ * Subscribe to asset manager events
1840
+ */
1841
+ on(event, handler) {
1842
+ this.events.on(event, handler);
1843
+ }
1844
+ /**
1845
+ * Unsubscribe from asset manager events
1846
+ */
1847
+ off(event, handler) {
1848
+ this.events.off(event, handler);
1849
+ }
1850
+ // ==================== PRIVATE HELPERS ====================
1851
+ /**
1852
+ * Generic cache wrapper for loading assets
1853
+ */
1854
+ async loadWithCache(url, type, cache, loader, options, cloner) {
1855
+ if (options?.forceReload) {
1856
+ cache.delete(url);
1857
+ }
1858
+ const cached = cache.get(url);
1859
+ if (cached) {
1860
+ this.stats.cacheHits++;
1861
+ this.events.emit("asset:loaded", { url, type, fromCache: true });
1862
+ const result = await cached.promise;
1863
+ return cloner ? cloner(result) : result;
1864
+ }
1865
+ this.stats.cacheMisses++;
1866
+ this.events.emit("asset:loading", { url, type });
1867
+ const promise = loader();
1868
+ const entry = {
1869
+ promise,
1870
+ loadedAt: Date.now()
1871
+ };
1872
+ cache.set(url, entry);
1873
+ try {
1874
+ const result = await promise;
1875
+ entry.result = result;
1876
+ this.updateStats(type);
1877
+ this.events.emit("asset:loaded", { url, type, fromCache: false });
1878
+ return cloner ? cloner(result) : result;
1879
+ } catch (error) {
1880
+ cache.delete(url);
1881
+ this.events.emit("asset:error", { url, type, error });
1882
+ throw error;
1883
+ }
1884
+ }
1885
+ updateStats(type) {
1886
+ switch (type) {
1887
+ case "texture" /* TEXTURE */:
1888
+ this.stats.texturesLoaded++;
1889
+ break;
1890
+ case "gltf" /* GLTF */:
1891
+ case "fbx" /* FBX */:
1892
+ case "obj" /* OBJ */:
1893
+ this.stats.modelsLoaded++;
1894
+ break;
1895
+ case "audio" /* AUDIO */:
1896
+ this.stats.audioLoaded++;
1897
+ break;
1898
+ case "file" /* FILE */:
1899
+ case "json" /* JSON */:
1900
+ this.stats.filesLoaded++;
1901
+ break;
1902
+ }
1903
+ }
1904
+ };
1905
+ var assetManager = AssetManager.getInstance();
1906
+
1907
+ // src/lib/graphics/material.ts
1908
+ import {
1909
+ uniform,
1910
+ uv,
1911
+ time,
1912
+ vec3,
1913
+ vec4,
1914
+ float,
1915
+ Fn
1916
+ } from "three/tsl";
1917
+ function isTSLShader(shader) {
1918
+ return "colorNode" in shader;
1919
+ }
1920
+ function isGLSLShader(shader) {
1921
+ return "fragment" in shader && "vertex" in shader;
1922
+ }
1923
+ var MaterialBuilder = class _MaterialBuilder {
1924
+ static batchMaterialMap = /* @__PURE__ */ new Map();
1925
+ materials = [];
1926
+ /** Whether to use TSL/NodeMaterial (for WebGPU compatibility) */
1927
+ useTSL;
1928
+ constructor(useTSL = false) {
1929
+ this.useTSL = useTSL;
1930
+ }
1931
+ batchMaterial(options, entityType) {
1932
+ const batchKey = shortHash(sortedStringify(options));
1933
+ const mappedObject = _MaterialBuilder.batchMaterialMap.get(batchKey);
1934
+ if (mappedObject) {
1935
+ const count = mappedObject.geometryMap.get(entityType);
1936
+ if (count) {
1937
+ mappedObject.geometryMap.set(entityType, count + 1);
1938
+ } else {
1939
+ mappedObject.geometryMap.set(entityType, 1);
1940
+ }
1941
+ } else {
1942
+ _MaterialBuilder.batchMaterialMap.set(batchKey, {
1943
+ geometryMap: /* @__PURE__ */ new Map([[entityType, 1]]),
1944
+ material: this.materials[0]
1945
+ });
1946
+ }
1947
+ }
1948
+ build(options, entityType) {
1949
+ const { path, normalMap, repeat, color, shader, useTSL } = options;
1950
+ const shouldUseTSL = useTSL ?? this.useTSL;
1951
+ if (shader) {
1952
+ if (isTSLShader(shader)) {
1953
+ this.setTSLShader(shader);
1954
+ } else if (isGLSLShader(shader)) {
1955
+ if (shouldUseTSL) {
1956
+ console.warn("MaterialBuilder: GLSL shader provided but TSL mode requested. Using GLSL.");
1957
+ }
1958
+ this.setShader(shader);
1959
+ }
1960
+ } else if (path) {
1961
+ this.setTexture(path, repeat, shouldUseTSL);
1962
+ }
1963
+ if (color) {
1964
+ this.withColor(color, shouldUseTSL);
1965
+ }
1966
+ if (this.materials.length === 0) {
1967
+ this.setColor(new Color3("#ffffff"), shouldUseTSL);
1968
+ }
1969
+ if (normalMap && this.materials.length > 0) {
1970
+ this.setNormalMap(normalMap, repeat);
1971
+ }
1972
+ this.batchMaterial(options, entityType);
1973
+ }
1974
+ withColor(color, useTSL = false) {
1975
+ this.setColor(color, useTSL);
1976
+ return this;
1977
+ }
1978
+ withShader(shader) {
1979
+ this.setShader(shader);
1980
+ return this;
1981
+ }
1982
+ withTSLShader(shader) {
1983
+ this.setTSLShader(shader);
1984
+ return this;
1985
+ }
1986
+ /**
1987
+ * Set texture - loads in background (deferred).
1988
+ * Material is created immediately with null map, texture applies when loaded.
1989
+ */
1990
+ setTexture(texturePath = null, repeat = new Vector22(1, 1), useTSL = false) {
1991
+ if (!texturePath) {
1992
+ return;
1993
+ }
1994
+ if (useTSL) {
1995
+ const material = new MeshStandardNodeMaterial();
1996
+ this.materials.push(material);
1997
+ assetManager.loadTexture(texturePath, {
1998
+ clone: true,
1999
+ repeat
2000
+ }).then((texture) => {
2001
+ texture.wrapS = RepeatWrapping2;
2002
+ texture.wrapT = RepeatWrapping2;
2003
+ material.map = texture;
2004
+ material.needsUpdate = true;
2005
+ });
2006
+ } else {
2007
+ const material = new MeshPhongMaterial2({
2008
+ map: null
2009
+ });
2010
+ this.materials.push(material);
2011
+ assetManager.loadTexture(texturePath, {
2012
+ clone: true,
2013
+ repeat
2014
+ }).then((texture) => {
2015
+ texture.wrapS = RepeatWrapping2;
2016
+ texture.wrapT = RepeatWrapping2;
2017
+ material.map = texture;
2018
+ material.needsUpdate = true;
2019
+ });
2020
+ }
2021
+ }
2022
+ /**
2023
+ * Set normal map for the current material
2024
+ */
2025
+ setNormalMap(normalMapPath, repeat = new Vector22(1, 1)) {
2026
+ const material = this.materials[this.materials.length - 1];
2027
+ if (!material) return;
2028
+ assetManager.loadTexture(normalMapPath, {
2029
+ clone: true,
2030
+ repeat
2031
+ }).then((texture) => {
2032
+ texture.wrapS = RepeatWrapping2;
2033
+ texture.wrapT = RepeatWrapping2;
2034
+ if (material instanceof MeshStandardMaterial2 || material instanceof MeshPhongMaterial2 || material instanceof MeshStandardNodeMaterial) {
2035
+ material.normalMap = texture;
2036
+ material.needsUpdate = true;
2037
+ } else if (material instanceof ShaderMaterial2) {
2038
+ if (material.uniforms.tNormal) {
2039
+ material.uniforms.tNormal.value = texture;
2040
+ material.needsUpdate = true;
2041
+ }
2042
+ if (material.uniforms.normalMap) {
2043
+ material.uniforms.normalMap.value = texture;
2044
+ material.needsUpdate = true;
2045
+ }
2046
+ }
2047
+ });
2048
+ }
2049
+ setColor(color, useTSL = false) {
2050
+ if (useTSL) {
2051
+ const material = new MeshStandardNodeMaterial();
2052
+ material.color = color;
2053
+ this.materials.push(material);
2054
+ } else {
2055
+ const material = new MeshStandardMaterial2({
2056
+ color,
2057
+ emissiveIntensity: 0.5,
2058
+ lightMapIntensity: 0.5,
2059
+ fog: true
2060
+ });
2061
+ this.materials.push(material);
2062
+ }
2063
+ }
2064
+ /**
2065
+ * Set GLSL shader (WebGL only)
2066
+ */
2067
+ setShader(customShader) {
2068
+ const { fragment: fragment2, vertex } = customShader ?? standardShader;
2069
+ const shader = new ShaderMaterial2({
2070
+ uniforms: {
2071
+ iResolution: { value: new Vector37(1, 1, 1) },
2072
+ iTime: { value: 0 },
2073
+ tDiffuse: { value: null },
2074
+ tDepth: { value: null },
2075
+ tNormal: { value: null },
2076
+ normalMap: { value: null },
2077
+ lightDir: { value: new Vector37(1, 1, 1) },
2078
+ normalStrength: { value: 1 }
2079
+ },
2080
+ vertexShader: vertex,
2081
+ fragmentShader: fragment2,
2082
+ transparent: true
2083
+ });
2084
+ this.materials.push(shader);
2085
+ }
2086
+ /**
2087
+ * Set TSL shader (WebGPU compatible)
2088
+ */
2089
+ setTSLShader(tslShader) {
2090
+ const material = new MeshBasicNodeMaterial();
2091
+ material.colorNode = tslShader.colorNode;
2092
+ if (tslShader.transparent) {
2093
+ material.transparent = true;
2094
+ }
2095
+ this.materials.push(material);
2096
+ }
2097
+ };
2098
+
2099
+ // src/lib/entities/parts/mesh-factories.ts
2100
+ function buildMaterial(opts, entityType = /* @__PURE__ */ Symbol("mesh")) {
2101
+ if (opts.material || opts.color) {
2102
+ const builder = new MaterialBuilder();
2103
+ builder.build(
2104
+ { ...opts.material, color: opts.color ?? opts.material?.color },
2105
+ entityType
2106
+ );
2107
+ if (builder.materials.length > 0) {
2108
+ return builder.materials;
2109
+ }
2110
+ }
2111
+ return [new MeshStandardMaterial3()];
2112
+ }
2113
+ function finalizeMesh(mesh, opts) {
2114
+ if (opts.position) {
2115
+ mesh.position.set(opts.position.x, opts.position.y, opts.position.z);
2116
+ }
2117
+ mesh.castShadow = true;
2118
+ mesh.receiveShadow = true;
2119
+ return mesh;
2120
+ }
2121
+ function boxMesh(opts = {}) {
2122
+ const size = opts.size ?? { x: 1, y: 1, z: 1 };
2123
+ const geometry = new BoxGeometry(size.x, size.y, size.z);
2124
+ const materials = buildMaterial(opts);
2125
+ return finalizeMesh(new Mesh2(geometry, materials.at(-1)), opts);
2126
+ }
2127
+ function sphereMesh(opts = {}) {
2128
+ const geometry = new SphereGeometry(opts.radius ?? 1);
2129
+ const materials = buildMaterial(opts);
2130
+ return finalizeMesh(new Mesh2(geometry, materials.at(-1)), opts);
2131
+ }
2132
+ function coneMesh(opts = {}) {
2133
+ const geometry = new ConeGeometry(
2134
+ opts.radius ?? 1,
2135
+ opts.height ?? 2,
2136
+ opts.radialSegments ?? 32
2137
+ );
2138
+ const materials = buildMaterial(opts);
2139
+ return finalizeMesh(new Mesh2(geometry, materials.at(-1)), opts);
2140
+ }
2141
+ function pyramidMesh(opts = {}) {
2142
+ const geometry = new ConeGeometry(opts.radius ?? 1, opts.height ?? 2, 4);
2143
+ const materials = buildMaterial(opts);
2144
+ return finalizeMesh(new Mesh2(geometry, materials.at(-1)), opts);
2145
+ }
2146
+ function cylinderMesh(opts = {}) {
2147
+ const geometry = new CylinderGeometry(
2148
+ opts.radiusTop ?? 1,
2149
+ opts.radiusBottom ?? 1,
2150
+ opts.height ?? 2,
2151
+ opts.radialSegments ?? 32
2152
+ );
2153
+ const materials = buildMaterial(opts);
2154
+ return finalizeMesh(new Mesh2(geometry, materials.at(-1)), opts);
2155
+ }
2156
+ function pillMesh(opts = {}) {
2157
+ const geometry = new CapsuleGeometry(
2158
+ opts.radius ?? 0.5,
2159
+ opts.length ?? 1,
2160
+ opts.capSegments ?? 10,
2161
+ opts.radialSegments ?? 20
2162
+ );
2163
+ const materials = buildMaterial(opts);
2164
+ return finalizeMesh(new Mesh2(geometry, materials.at(-1)), opts);
2165
+ }
2166
+
2167
+ // src/lib/entities/box.ts
2168
+ var boxDefaults = {
2169
+ ...commonDefaults,
2170
+ size: new Vector38(1, 1, 1)
2171
+ };
868
2172
  var BOX_TYPE = /* @__PURE__ */ Symbol("Box");
869
2173
  var ZylemBox = class _ZylemBox extends GameEntity {
870
2174
  static type = BOX_TYPE;
@@ -883,107 +2187,282 @@ var ZylemBox = class _ZylemBox extends GameEntity {
883
2187
  };
884
2188
  }
885
2189
  };
886
- async function box(...args) {
887
- return createEntity({
888
- args,
889
- defaultConfig: boxDefaults,
890
- EntityClass: ZylemBox,
891
- BuilderClass: BoxBuilder,
892
- MeshBuilderClass: BoxMeshBuilder,
893
- CollisionBuilderClass: BoxCollisionBuilder,
894
- entityType: ZylemBox.type
895
- });
2190
+ function createBox(...args) {
2191
+ const options = mergeArgs(args, boxDefaults);
2192
+ const entity = new ZylemBox(options);
2193
+ entity.add(
2194
+ boxMesh({ size: options.size, material: options.material, color: options.color }),
2195
+ boxCollision({
2196
+ size: options.size,
2197
+ static: options.collision?.static,
2198
+ sensor: options.collision?.sensor,
2199
+ collisionType: options.collisionType,
2200
+ collisionFilter: options.collisionFilter
2201
+ })
2202
+ );
2203
+ return entity;
896
2204
  }
897
2205
 
898
2206
  // src/lib/entities/sphere.ts
899
- import { ColliderDesc as ColliderDesc3 } from "@dimforge/rapier3d-compat";
900
- import { Color as Color5, SphereGeometry } from "three";
901
- import { Vector3 as Vector34 } from "three";
902
2207
  var sphereDefaults = {
903
- radius: 1,
904
- position: new Vector34(0, 0, 0),
905
- collision: {
906
- static: false
907
- },
908
- material: {
909
- color: new Color5("#ffffff"),
910
- shader: "standard"
2208
+ ...commonDefaults,
2209
+ radius: 1
2210
+ };
2211
+ var SPHERE_TYPE = /* @__PURE__ */ Symbol("Sphere");
2212
+ var ZylemSphere = class _ZylemSphere extends GameEntity {
2213
+ static type = SPHERE_TYPE;
2214
+ constructor(options) {
2215
+ super();
2216
+ this.options = { ...sphereDefaults, ...options };
2217
+ }
2218
+ buildInfo() {
2219
+ const delegate = new DebugDelegate(this);
2220
+ const baseInfo = delegate.buildDebugInfo();
2221
+ const radius = this.options.radius ?? 1;
2222
+ return {
2223
+ ...baseInfo,
2224
+ type: String(_ZylemSphere.type),
2225
+ radius: radius.toFixed(2)
2226
+ };
911
2227
  }
912
2228
  };
913
- var SphereCollisionBuilder = class extends EntityCollisionBuilder {
914
- collider(options) {
915
- const radius = options.radius ?? 1;
916
- let colliderDesc = ColliderDesc3.ball(radius);
917
- return colliderDesc;
2229
+ function createSphere(...args) {
2230
+ const options = mergeArgs(args, sphereDefaults);
2231
+ const entity = new ZylemSphere(options);
2232
+ entity.add(
2233
+ sphereMesh({ radius: options.radius, material: options.material, color: options.color }),
2234
+ sphereCollision({
2235
+ radius: options.radius,
2236
+ static: options.collision?.static,
2237
+ sensor: options.collision?.sensor,
2238
+ collisionType: options.collisionType,
2239
+ collisionFilter: options.collisionFilter
2240
+ })
2241
+ );
2242
+ return entity;
2243
+ }
2244
+
2245
+ // src/lib/entities/sprite.ts
2246
+ import { ColliderDesc as ColliderDesc3 } from "@dimforge/rapier3d-compat";
2247
+ import { Euler as Euler2, Group as Group3, Quaternion as Quaternion3, Vector3 as Vector39 } from "three";
2248
+ import {
2249
+ TextureLoader as TextureLoader2,
2250
+ SpriteMaterial,
2251
+ Sprite as ThreeSprite
2252
+ } from "three";
2253
+
2254
+ // src/lib/entities/builder.ts
2255
+ import { BufferGeometry as BufferGeometry2, Mesh as Mesh4, Color as Color4 } from "three";
2256
+
2257
+ // src/lib/graphics/mesh.ts
2258
+ import { Mesh as Mesh3 } from "three";
2259
+ var MeshBuilder = class {
2260
+ _build(meshOptions, geometry, materials) {
2261
+ const { batched, material } = meshOptions;
2262
+ if (batched) {
2263
+ console.warn("warning: mesh batching is not implemented");
2264
+ }
2265
+ const mesh = new Mesh3(geometry, materials.at(-1));
2266
+ mesh.position.set(0, 0, 0);
2267
+ mesh.castShadow = true;
2268
+ mesh.receiveShadow = true;
2269
+ return mesh;
2270
+ }
2271
+ _postBuild() {
2272
+ return;
918
2273
  }
919
2274
  };
920
- var SphereMeshBuilder = class extends EntityMeshBuilder {
2275
+
2276
+ // src/lib/entities/builder.ts
2277
+ var EntityCollisionBuilder = class extends CollisionBuilder {
2278
+ };
2279
+ var EntityMeshBuilder = class extends MeshBuilder {
921
2280
  build(options) {
922
- const radius = options.radius ?? 1;
923
- return new SphereGeometry(radius);
2281
+ return new BufferGeometry2();
2282
+ }
2283
+ postBuild() {
2284
+ return;
2285
+ }
2286
+ };
2287
+ var EntityBuilder = class {
2288
+ meshBuilder;
2289
+ collisionBuilder;
2290
+ materialBuilder;
2291
+ options;
2292
+ entity;
2293
+ constructor(options, entity, meshBuilder, collisionBuilder) {
2294
+ this.options = options;
2295
+ this.entity = entity;
2296
+ this.meshBuilder = meshBuilder;
2297
+ this.collisionBuilder = collisionBuilder;
2298
+ this.materialBuilder = new MaterialBuilder();
2299
+ const builders = {
2300
+ meshBuilder: this.meshBuilder,
2301
+ collisionBuilder: this.collisionBuilder,
2302
+ materialBuilder: this.materialBuilder
2303
+ };
2304
+ this.options._builders = builders;
2305
+ }
2306
+ withPosition(setupPosition) {
2307
+ this.options.position = setupPosition;
2308
+ return this;
2309
+ }
2310
+ withMaterial(options, entityType) {
2311
+ if (this.materialBuilder) {
2312
+ this.materialBuilder.build(options, entityType);
2313
+ }
2314
+ return this;
2315
+ }
2316
+ applyMaterialToGroup(group, materials) {
2317
+ group.traverse((child) => {
2318
+ if (child instanceof Mesh4) {
2319
+ if (child.type === "SkinnedMesh" && materials[0] && !child.material.map) {
2320
+ child.material = materials[0];
2321
+ }
2322
+ }
2323
+ child.castShadow = true;
2324
+ child.receiveShadow = true;
2325
+ });
2326
+ }
2327
+ build() {
2328
+ const entity = this.entity;
2329
+ if (this.materialBuilder) {
2330
+ entity.materials = this.materialBuilder.materials;
2331
+ }
2332
+ if (this.meshBuilder && entity.materials) {
2333
+ const geometry = this.meshBuilder.build(this.options);
2334
+ entity.mesh = this.meshBuilder._build(this.options, geometry, entity.materials);
2335
+ this.meshBuilder.postBuild();
2336
+ }
2337
+ if (entity.group && entity.materials) {
2338
+ this.applyMaterialToGroup(entity.group, entity.materials);
2339
+ }
2340
+ if (this.collisionBuilder) {
2341
+ this.collisionBuilder.withCollision(this.options?.collision || {});
2342
+ const [bodyDesc, colliderDesc] = this.collisionBuilder.build(this.options);
2343
+ entity.bodyDesc = bodyDesc;
2344
+ entity.colliderDesc = colliderDesc;
2345
+ entity.colliderDescs.push(colliderDesc);
2346
+ const { x, y, z } = this.options.position || { x: 0, y: 0, z: 0 };
2347
+ entity.bodyDesc.setTranslation(x, y, z);
2348
+ }
2349
+ if (this.options.collisionType) {
2350
+ entity.collisionType = this.options.collisionType;
2351
+ }
2352
+ if (this.options.color instanceof Color4) {
2353
+ const applyColor = (material) => {
2354
+ const anyMat = material;
2355
+ if (anyMat && anyMat.color && anyMat.color.set) {
2356
+ anyMat.color.set(this.options.color);
2357
+ }
2358
+ };
2359
+ if (entity.materials?.length) {
2360
+ for (const mat of entity.materials) applyColor(mat);
2361
+ }
2362
+ if (entity.mesh && entity.mesh.material) {
2363
+ const mat = entity.mesh.material;
2364
+ if (Array.isArray(mat)) mat.forEach(applyColor);
2365
+ else applyColor(mat);
2366
+ }
2367
+ if (entity.group) {
2368
+ entity.group.traverse((child) => {
2369
+ if (child instanceof Mesh4 && child.material) {
2370
+ const mat = child.material;
2371
+ if (Array.isArray(mat)) mat.forEach(applyColor);
2372
+ else applyColor(mat);
2373
+ }
2374
+ });
2375
+ }
2376
+ }
2377
+ return entity;
2378
+ }
2379
+ };
2380
+
2381
+ // src/lib/entities/delegates/loader.ts
2382
+ function isLoadable(obj) {
2383
+ return typeof obj?.load === "function" && typeof obj?.data === "function";
2384
+ }
2385
+ var EntityLoader = class {
2386
+ entityReference;
2387
+ constructor(entity) {
2388
+ this.entityReference = entity;
2389
+ }
2390
+ load() {
2391
+ if (this.entityReference.load) {
2392
+ this.entityReference.load();
2393
+ }
2394
+ }
2395
+ data() {
2396
+ if (this.entityReference.data) {
2397
+ return this.entityReference.data();
2398
+ }
2399
+ return null;
924
2400
  }
925
2401
  };
926
- var SphereBuilder = class extends EntityBuilder {
927
- createEntity(options) {
928
- return new ZylemSphere(options);
2402
+
2403
+ // src/lib/entities/create.ts
2404
+ function createEntity(params) {
2405
+ const {
2406
+ args,
2407
+ defaultConfig,
2408
+ EntityClass,
2409
+ BuilderClass,
2410
+ entityType,
2411
+ MeshBuilderClass,
2412
+ CollisionBuilderClass
2413
+ } = params;
2414
+ let builder = null;
2415
+ let configuration;
2416
+ const configurationIndex = args.findIndex((node) => !(node instanceof BaseNode));
2417
+ if (configurationIndex !== -1) {
2418
+ const subArgs = args.splice(configurationIndex, 1);
2419
+ configuration = subArgs.find((node) => !(node instanceof BaseNode));
929
2420
  }
930
- };
931
- var SPHERE_TYPE = /* @__PURE__ */ Symbol("Sphere");
932
- var ZylemSphere = class _ZylemSphere extends GameEntity {
933
- static type = SPHERE_TYPE;
934
- constructor(options) {
935
- super();
936
- this.options = { ...sphereDefaults, ...options };
2421
+ const mergedConfiguration = configuration ? { ...defaultConfig, ...configuration } : defaultConfig;
2422
+ args.push(mergedConfiguration);
2423
+ for (const arg of args) {
2424
+ if (arg instanceof BaseNode) {
2425
+ continue;
2426
+ }
2427
+ let entityData = null;
2428
+ const entity = new EntityClass(arg);
2429
+ try {
2430
+ if (isLoadable(entity)) {
2431
+ const loader = new EntityLoader(entity);
2432
+ loader.load();
2433
+ entityData = loader.data();
2434
+ }
2435
+ } catch (error) {
2436
+ console.error("Error creating entity with loader:", error);
2437
+ }
2438
+ builder = new BuilderClass(
2439
+ arg,
2440
+ entity,
2441
+ MeshBuilderClass ? new MeshBuilderClass(entityData) : null,
2442
+ CollisionBuilderClass ? new CollisionBuilderClass(entityData) : null
2443
+ );
2444
+ if (arg.material) {
2445
+ builder.withMaterial(arg.material, entityType);
2446
+ }
937
2447
  }
938
- buildInfo() {
939
- const delegate = new DebugDelegate(this);
940
- const baseInfo = delegate.buildDebugInfo();
941
- const radius = this.options.radius ?? 1;
942
- return {
943
- ...baseInfo,
944
- type: String(_ZylemSphere.type),
945
- radius: radius.toFixed(2)
946
- };
2448
+ if (!builder) {
2449
+ throw new Error(`missing options for ${String(entityType)}, builder is not initialized.`);
947
2450
  }
948
- };
949
- async function sphere(...args) {
950
- return createEntity({
951
- args,
952
- defaultConfig: sphereDefaults,
953
- EntityClass: ZylemSphere,
954
- BuilderClass: SphereBuilder,
955
- MeshBuilderClass: SphereMeshBuilder,
956
- CollisionBuilderClass: SphereCollisionBuilder,
957
- entityType: ZylemSphere.type
958
- });
2451
+ return builder.build();
959
2452
  }
960
2453
 
961
2454
  // src/lib/entities/sprite.ts
962
- import { ColliderDesc as ColliderDesc4 } from "@dimforge/rapier3d-compat";
963
- import { Color as Color6, Euler, Group as Group3, Quaternion as Quaternion2, Vector3 as Vector35 } from "three";
964
- import {
965
- TextureLoader as TextureLoader2,
966
- SpriteMaterial,
967
- Sprite as ThreeSprite
968
- } from "three";
969
2455
  var spriteDefaults = {
970
- size: new Vector35(1, 1, 1),
971
- position: new Vector35(0, 0, 0),
972
- collision: {
973
- static: false
974
- },
975
- material: {
976
- color: new Color6("#ffffff"),
977
- shader: "standard"
978
- },
2456
+ ...commonDefaults,
2457
+ size: new Vector39(1, 1, 1),
979
2458
  images: [],
980
2459
  animations: []
981
2460
  };
982
2461
  var SpriteCollisionBuilder = class extends EntityCollisionBuilder {
983
2462
  collider(options) {
984
- const size = options.collisionSize || options.size || new Vector35(1, 1, 1);
2463
+ const size = options.collisionSize || options.size || new Vector39(1, 1, 1);
985
2464
  const half = { x: size.x / 2, y: size.y / 2, z: size.z / 2 };
986
- let colliderDesc = ColliderDesc4.cuboid(half.x, half.y, half.z);
2465
+ let colliderDesc = ColliderDesc3.cuboid(half.x, half.y, half.z);
987
2466
  return colliderDesc;
988
2467
  }
989
2468
  };
@@ -1007,7 +2486,7 @@ var ZylemSprite = class _ZylemSprite extends GameEntity {
1007
2486
  super();
1008
2487
  this.options = { ...spriteDefaults, ...options };
1009
2488
  this.prependUpdate(this.spriteUpdate.bind(this));
1010
- this.onDestroy(this.spriteDestroy.bind(this));
2489
+ this.onCleanup(this.spriteDestroy.bind(this));
1011
2490
  }
1012
2491
  create() {
1013
2492
  this.sprites = [];
@@ -1085,20 +2564,20 @@ var ZylemSprite = class _ZylemSprite extends GameEntity {
1085
2564
  }
1086
2565
  }
1087
2566
  }
1088
- async spriteUpdate(params) {
2567
+ spriteUpdate(params) {
1089
2568
  this.sprites.forEach((_sprite) => {
1090
2569
  if (_sprite.material) {
1091
2570
  const q = this.body?.rotation();
1092
2571
  if (q) {
1093
- const quat = new Quaternion2(q.x, q.y, q.z, q.w);
1094
- const euler = new Euler().setFromQuaternion(quat, "XYZ");
2572
+ const quat = new Quaternion3(q.x, q.y, q.z, q.w);
2573
+ const euler = new Euler2().setFromQuaternion(quat, "XYZ");
1095
2574
  _sprite.material.rotation = euler.z;
1096
2575
  }
1097
2576
  _sprite.scale.set(this.options.size?.x ?? 1, this.options.size?.y ?? 1, this.options.size?.z ?? 1);
1098
2577
  }
1099
2578
  });
1100
2579
  }
1101
- async spriteDestroy(params) {
2580
+ spriteDestroy(params) {
1102
2581
  this.sprites.forEach((_sprite) => {
1103
2582
  _sprite.removeFromParent();
1104
2583
  });
@@ -1114,7 +2593,7 @@ var ZylemSprite = class _ZylemSprite extends GameEntity {
1114
2593
  };
1115
2594
  }
1116
2595
  };
1117
- async function sprite(...args) {
2596
+ function createSprite(...args) {
1118
2597
  return createEntity({
1119
2598
  args,
1120
2599
  defaultConfig: spriteDefaults,
@@ -1126,8 +2605,8 @@ async function sprite(...args) {
1126
2605
  }
1127
2606
 
1128
2607
  // src/lib/entities/plane.ts
1129
- import { ColliderDesc as ColliderDesc5 } from "@dimforge/rapier3d-compat";
1130
- import { Color as Color7, PlaneGeometry, Vector2 as Vector22, Vector3 as Vector36 } from "three";
2608
+ import { ColliderDesc as ColliderDesc4 } from "@dimforge/rapier3d-compat";
2609
+ import { PlaneGeometry, Vector2 as Vector23, Vector3 as Vector310 } from "three";
1131
2610
 
1132
2611
  // src/lib/graphics/geometries/XZPlaneGeometry.ts
1133
2612
  import { BufferGeometry as BufferGeometry3, Float32BufferAttribute } from "three";
@@ -1191,26 +2670,24 @@ var XZPlaneGeometry = class _XZPlaneGeometry extends BufferGeometry3 {
1191
2670
  // src/lib/entities/plane.ts
1192
2671
  var DEFAULT_SUBDIVISIONS = 4;
1193
2672
  var planeDefaults = {
1194
- tile: new Vector22(10, 10),
1195
- repeat: new Vector22(1, 1),
1196
- position: new Vector36(0, 0, 0),
2673
+ ...commonDefaults,
2674
+ tile: new Vector23(10, 10),
2675
+ repeat: new Vector23(1, 1),
1197
2676
  collision: {
1198
2677
  static: true
1199
2678
  },
1200
- material: {
1201
- color: new Color7("#ffffff"),
1202
- shader: "standard"
1203
- },
1204
- subdivisions: DEFAULT_SUBDIVISIONS
2679
+ subdivisions: DEFAULT_SUBDIVISIONS,
2680
+ randomizeHeight: false,
2681
+ heightScale: 1
1205
2682
  };
1206
2683
  var PlaneCollisionBuilder = class extends EntityCollisionBuilder {
1207
2684
  collider(options) {
1208
- const tile = options.tile ?? new Vector22(1, 1);
2685
+ const tile = options.tile ?? new Vector23(1, 1);
1209
2686
  const subdivisions = options.subdivisions ?? DEFAULT_SUBDIVISIONS;
1210
- const size = new Vector36(tile.x, 1, tile.y);
2687
+ const size = new Vector310(tile.x, 1, tile.y);
1211
2688
  const heightData = options._builders?.meshBuilder?.heightData;
1212
- const scale2 = new Vector36(size.x, 1, size.z);
1213
- let colliderDesc = ColliderDesc5.heightfield(
2689
+ const scale2 = new Vector310(size.x, 1, size.z);
2690
+ let colliderDesc = ColliderDesc4.heightfield(
1214
2691
  subdivisions,
1215
2692
  subdivisions,
1216
2693
  heightData,
@@ -1222,10 +2699,13 @@ var PlaneCollisionBuilder = class extends EntityCollisionBuilder {
1222
2699
  var PlaneMeshBuilder = class extends EntityMeshBuilder {
1223
2700
  heightData = new Float32Array();
1224
2701
  columnsRows = /* @__PURE__ */ new Map();
2702
+ subdivisions = DEFAULT_SUBDIVISIONS;
1225
2703
  build(options) {
1226
- const tile = options.tile ?? new Vector22(1, 1);
2704
+ const tile = options.tile ?? new Vector23(1, 1);
1227
2705
  const subdivisions = options.subdivisions ?? DEFAULT_SUBDIVISIONS;
1228
- const size = new Vector36(tile.x, 1, tile.y);
2706
+ this.subdivisions = subdivisions;
2707
+ const size = new Vector310(tile.x, 1, tile.y);
2708
+ const heightScale = options.heightScale ?? 1;
1229
2709
  const geometry = new XZPlaneGeometry(size.x, size.z, subdivisions, subdivisions);
1230
2710
  const vertexGeometry = new PlaneGeometry(size.x, size.z, subdivisions, subdivisions);
1231
2711
  const dx = size.x / subdivisions;
@@ -1233,30 +2713,40 @@ var PlaneMeshBuilder = class extends EntityMeshBuilder {
1233
2713
  const originalVertices = geometry.attributes.position.array;
1234
2714
  const vertices = vertexGeometry.attributes.position.array;
1235
2715
  const columsRows = /* @__PURE__ */ new Map();
2716
+ const heightMapData = options.heightMap;
2717
+ const useRandomHeight = options.randomizeHeight ?? false;
1236
2718
  for (let i = 0; i < vertices.length; i += 3) {
2719
+ const vertexIndex = i / 3;
1237
2720
  let row = Math.floor(Math.abs(vertices[i] + size.x / 2) / dx);
1238
2721
  let column = Math.floor(Math.abs(vertices[i + 1] - size.z / 2) / dy);
1239
- const randomHeight = Math.random() * 4;
1240
- vertices[i + 2] = randomHeight;
1241
- originalVertices[i + 1] = randomHeight;
2722
+ let height = 0;
2723
+ if (heightMapData && heightMapData.length > 0) {
2724
+ const heightIndex = vertexIndex % heightMapData.length;
2725
+ height = heightMapData[heightIndex] * heightScale;
2726
+ } else if (useRandomHeight) {
2727
+ height = Math.random() * 4 * heightScale;
2728
+ }
2729
+ vertices[i + 2] = height;
2730
+ originalVertices[i + 1] = height;
1242
2731
  if (!columsRows.get(column)) {
1243
2732
  columsRows.set(column, /* @__PURE__ */ new Map());
1244
2733
  }
1245
- columsRows.get(column).set(row, randomHeight);
2734
+ columsRows.get(column).set(row, height);
1246
2735
  }
1247
2736
  this.columnsRows = columsRows;
1248
2737
  return geometry;
1249
2738
  }
1250
2739
  postBuild() {
1251
2740
  const heights = [];
1252
- for (let i = 0; i <= DEFAULT_SUBDIVISIONS; ++i) {
1253
- for (let j = 0; j <= DEFAULT_SUBDIVISIONS; ++j) {
2741
+ for (let i = 0; i <= this.subdivisions; ++i) {
2742
+ for (let j = 0; j <= this.subdivisions; ++j) {
1254
2743
  const row = this.columnsRows.get(j);
1255
2744
  if (!row) {
2745
+ heights.push(0);
1256
2746
  continue;
1257
2747
  }
1258
2748
  const data = row.get(i);
1259
- heights.push(data);
2749
+ heights.push(data ?? 0);
1260
2750
  }
1261
2751
  }
1262
2752
  this.heightData = new Float32Array(heights);
@@ -1275,7 +2765,7 @@ var ZylemPlane = class extends GameEntity {
1275
2765
  this.options = { ...planeDefaults, ...options };
1276
2766
  }
1277
2767
  };
1278
- async function plane(...args) {
2768
+ function createPlane(...args) {
1279
2769
  return createEntity({
1280
2770
  args,
1281
2771
  defaultConfig: planeDefaults,
@@ -1288,12 +2778,11 @@ async function plane(...args) {
1288
2778
  }
1289
2779
 
1290
2780
  // src/lib/entities/zone.ts
1291
- import { ActiveCollisionTypes as ActiveCollisionTypes2, ColliderDesc as ColliderDesc6 } from "@dimforge/rapier3d-compat";
1292
- import { Vector3 as Vector37 } from "three";
2781
+ import { Vector3 as Vector311 } from "three";
1293
2782
 
1294
2783
  // src/lib/game/game-state.ts
1295
- import { proxy, subscribe } from "valtio/vanilla";
1296
- var state = proxy({
2784
+ import { proxy as proxy2, subscribe } from "valtio/vanilla";
2785
+ var state = proxy2({
1297
2786
  id: "",
1298
2787
  globals: {},
1299
2788
  time: 0
@@ -1301,28 +2790,10 @@ var state = proxy({
1301
2790
 
1302
2791
  // src/lib/entities/zone.ts
1303
2792
  var zoneDefaults = {
1304
- size: new Vector37(1, 1, 1),
1305
- position: new Vector37(0, 0, 0),
2793
+ ...commonDefaults,
2794
+ size: new Vector311(1, 1, 1),
1306
2795
  collision: {
1307
2796
  static: true
1308
- },
1309
- material: {
1310
- shader: "standard"
1311
- }
1312
- };
1313
- var ZoneCollisionBuilder = class extends EntityCollisionBuilder {
1314
- collider(options) {
1315
- const size = options.size || new Vector37(1, 1, 1);
1316
- const half = { x: size.x / 2, y: size.y / 2, z: size.z / 2 };
1317
- let colliderDesc = ColliderDesc6.cuboid(half.x, half.y, half.z);
1318
- colliderDesc.setSensor(true);
1319
- colliderDesc.activeCollisionTypes = ActiveCollisionTypes2.KINEMATIC_FIXED;
1320
- return colliderDesc;
1321
- }
1322
- };
1323
- var ZoneBuilder = class extends EntityBuilder {
1324
- createEntity(options) {
1325
- return new ZylemZone(options);
1326
2797
  }
1327
2798
  };
1328
2799
  var ZONE_TYPE = /* @__PURE__ */ Symbol("Zone");
@@ -1404,78 +2875,56 @@ var ZylemZone = class extends GameEntity {
1404
2875
  }
1405
2876
  }
1406
2877
  };
1407
- async function zone(...args) {
1408
- return createEntity({
1409
- args,
1410
- defaultConfig: zoneDefaults,
1411
- EntityClass: ZylemZone,
1412
- BuilderClass: ZoneBuilder,
1413
- CollisionBuilderClass: ZoneCollisionBuilder,
1414
- entityType: ZylemZone.type
1415
- });
2878
+ function createZone(...args) {
2879
+ const options = mergeArgs(args, zoneDefaults);
2880
+ const entity = new ZylemZone(options);
2881
+ entity.add(
2882
+ zoneCollision({
2883
+ size: options.size,
2884
+ static: options.collision?.static ?? true,
2885
+ collisionType: options.collisionType,
2886
+ collisionFilter: options.collisionFilter
2887
+ })
2888
+ );
2889
+ return entity;
1416
2890
  }
1417
2891
 
1418
2892
  // src/lib/entities/actor.ts
1419
- import { ActiveCollisionTypes as ActiveCollisionTypes3, ColliderDesc as ColliderDesc7 } from "@dimforge/rapier3d-compat";
1420
- import { SkinnedMesh, Group as Group4, Vector3 as Vector38 } from "three";
2893
+ import { ActiveCollisionTypes as ActiveCollisionTypes3, ColliderDesc as ColliderDesc5 } from "@dimforge/rapier3d-compat";
2894
+ import { MeshStandardMaterial as MeshStandardMaterial4, Group as Group4, Vector3 as Vector312 } from "three";
1421
2895
 
1422
2896
  // src/lib/core/entity-asset-loader.ts
1423
- import { FBXLoader } from "three/addons/loaders/FBXLoader.js";
1424
- import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
1425
- var FBXAssetLoader = class {
1426
- loader = new FBXLoader();
1427
- isSupported(file) {
1428
- return file.toLowerCase().endsWith("fbx" /* FBX */);
1429
- }
1430
- async load(file) {
1431
- return new Promise((resolve, reject) => {
1432
- this.loader.load(
1433
- file,
1434
- (object) => {
1435
- const animation = object.animations[0];
1436
- resolve({
1437
- object,
1438
- animation
1439
- });
1440
- },
1441
- void 0,
1442
- reject
1443
- );
1444
- });
1445
- }
1446
- };
1447
- var GLTFAssetLoader = class {
1448
- loader = new GLTFLoader();
1449
- isSupported(file) {
1450
- return file.toLowerCase().endsWith("gltf" /* GLTF */);
1451
- }
1452
- async load(file) {
1453
- return new Promise((resolve, reject) => {
1454
- this.loader.load(
1455
- file,
1456
- (gltf) => {
1457
- resolve({
1458
- object: gltf.scene,
1459
- gltf
1460
- });
1461
- },
1462
- void 0,
1463
- reject
1464
- );
1465
- });
1466
- }
1467
- };
1468
2897
  var EntityAssetLoader = class {
1469
- loaders = [
1470
- new FBXAssetLoader(),
1471
- new GLTFAssetLoader()
1472
- ];
2898
+ /**
2899
+ * Load a model file (FBX, GLTF, GLB, OBJ) using the asset manager
2900
+ */
1473
2901
  async loadFile(file) {
1474
- const loader = this.loaders.find((l) => l.isSupported(file));
1475
- if (!loader) {
1476
- throw new Error(`Unsupported file type: ${file}`);
2902
+ const ext = file.split(".").pop()?.toLowerCase();
2903
+ switch (ext) {
2904
+ case "fbx": {
2905
+ const result = await assetManager.loadFBX(file);
2906
+ return {
2907
+ object: result.object,
2908
+ animation: result.animations?.[0]
2909
+ };
2910
+ }
2911
+ case "gltf":
2912
+ case "glb": {
2913
+ const result = await assetManager.loadGLTF(file);
2914
+ return {
2915
+ object: result.object,
2916
+ gltf: result.gltf
2917
+ };
2918
+ }
2919
+ case "obj": {
2920
+ const result = await assetManager.loadOBJ(file);
2921
+ return {
2922
+ object: result.object
2923
+ };
2924
+ }
2925
+ default:
2926
+ throw new Error(`Unsupported file type: ${file}`);
1477
2927
  }
1478
- return loader.load(file);
1479
2928
  }
1480
2929
  };
1481
2930
 
@@ -1502,14 +2951,22 @@ var AnimationDelegate = class {
1502
2951
  async loadAnimations(animations) {
1503
2952
  if (!animations.length) return;
1504
2953
  const results = await Promise.all(animations.map((a) => this._assetLoader.loadFile(a.path)));
1505
- this._animations = results.filter((r) => !!r.animation).map((r) => r.animation);
1506
- if (!this._animations.length) return;
2954
+ const loadedAnimations = [];
2955
+ results.forEach((result, i) => {
2956
+ if (result.animation) {
2957
+ loadedAnimations.push({
2958
+ key: animations[i].key || i.toString(),
2959
+ clip: result.animation
2960
+ });
2961
+ }
2962
+ });
2963
+ if (!loadedAnimations.length) return;
2964
+ this._animations = loadedAnimations.map((a) => a.clip);
1507
2965
  this._mixer = new AnimationMixer(this.target);
1508
- this._animations.forEach((clip, i) => {
1509
- const key = animations[i].key || i.toString();
2966
+ loadedAnimations.forEach(({ key, clip }) => {
1510
2967
  this._actions[key] = this._mixer.clipAction(clip);
1511
2968
  });
1512
- this.playAnimation({ key: Object.keys(this._actions)[0] });
2969
+ this.playAnimation({ key: loadedAnimations[0].key });
1513
2970
  }
1514
2971
  update(delta) {
1515
2972
  if (!this._mixer || !this._currentAction) return;
@@ -1533,14 +2990,13 @@ var AnimationDelegate = class {
1533
2990
  if (!this._mixer) return;
1534
2991
  const { key, pauseAtPercentage = 0, pauseAtEnd = false, fadeToKey, fadeDuration = 0.5 } = opts;
1535
2992
  if (key === this._currentKey) return;
2993
+ const action = this._actions[key];
2994
+ if (!action) return;
1536
2995
  this._queuedKey = fadeToKey || null;
1537
2996
  this._fadeDuration = fadeDuration;
1538
2997
  this._pauseAtPercentage = pauseAtEnd ? 100 : pauseAtPercentage;
1539
2998
  this._isPaused = false;
1540
2999
  const prev = this._currentAction;
1541
- if (prev) prev.stop();
1542
- const action = this._actions[key];
1543
- if (!action) return;
1544
3000
  if (this._pauseAtPercentage > 0) {
1545
3001
  action.setLoop(LoopOnce, Infinity);
1546
3002
  action.clampWhenFinished = true;
@@ -1548,10 +3004,10 @@ var AnimationDelegate = class {
1548
3004
  action.setLoop(LoopRepeat, Infinity);
1549
3005
  action.clampWhenFinished = false;
1550
3006
  }
3007
+ action.reset().play();
1551
3008
  if (prev) {
1552
3009
  prev.crossFadeTo(action, fadeDuration, false);
1553
3010
  }
1554
- action.reset().play();
1555
3011
  this._currentAction = action;
1556
3012
  this._currentKey = key;
1557
3013
  }
@@ -1582,46 +3038,86 @@ var AnimationDelegate = class {
1582
3038
 
1583
3039
  // src/lib/entities/actor.ts
1584
3040
  var actorDefaults = {
1585
- position: { x: 0, y: 0, z: 0 },
3041
+ ...commonDefaults,
1586
3042
  collision: {
1587
3043
  static: false,
1588
- size: new Vector38(0.5, 0.5, 0.5),
1589
- position: new Vector38(0, 0, 0)
3044
+ size: new Vector312(0.5, 0.5, 0.5),
3045
+ position: new Vector312(0, 0, 0)
1590
3046
  },
1591
3047
  material: {
1592
- shader: "standard"
3048
+ shader: standardShader
1593
3049
  },
1594
3050
  animations: [],
1595
- models: []
3051
+ models: [],
3052
+ collisionShape: "capsule"
1596
3053
  };
1597
3054
  var ActorCollisionBuilder = class extends EntityCollisionBuilder {
1598
- height = 1;
1599
3055
  objectModel = null;
3056
+ collisionShape = "capsule";
1600
3057
  constructor(data) {
1601
3058
  super();
1602
3059
  this.objectModel = data.objectModel;
3060
+ this.collisionShape = data.collisionShape ?? "capsule";
1603
3061
  }
1604
- createColliderFromObjectModel(objectModel) {
1605
- if (!objectModel) return ColliderDesc7.capsule(1, 1);
1606
- const skinnedMesh = objectModel.children.find((child) => child instanceof SkinnedMesh);
1607
- const geometry = skinnedMesh.geometry;
1608
- if (geometry) {
1609
- geometry.computeBoundingBox();
1610
- if (geometry.boundingBox) {
1611
- const maxY = geometry.boundingBox.max.y;
1612
- const minY = geometry.boundingBox.min.y;
1613
- this.height = maxY - minY;
1614
- }
3062
+ collider(options) {
3063
+ if (this.collisionShape === "model") {
3064
+ return this.createColliderFromModel(this.objectModel, options);
1615
3065
  }
1616
- this.height = 1;
1617
- let colliderDesc = ColliderDesc7.capsule(this.height / 2, 1);
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);
1618
3076
  colliderDesc.setSensor(false);
1619
- colliderDesc.setTranslation(0, this.height + 0.5, 0);
3077
+ colliderDesc.setTranslation(0, halfHeight + radius, 0);
1620
3078
  colliderDesc.activeCollisionTypes = ActiveCollisionTypes3.DEFAULT;
1621
3079
  return colliderDesc;
1622
3080
  }
1623
- collider(options) {
1624
- let colliderDesc = this.createColliderFromObjectModel(this.objectModel);
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;
1625
3121
  return colliderDesc;
1626
3122
  }
1627
3123
  };
@@ -1644,21 +3140,26 @@ var ZylemActor = class extends GameEntity {
1644
3140
  this.prependUpdate(this.actorUpdate.bind(this));
1645
3141
  this.controlledRotation = true;
1646
3142
  }
1647
- async load() {
3143
+ /**
3144
+ * Initiates model and animation loading in background (deferred).
3145
+ * Call returns immediately; assets will be ready on subsequent updates.
3146
+ */
3147
+ load() {
1648
3148
  this._modelFileNames = this.options.models || [];
1649
- await this.loadModels();
1650
- if (this._object) {
1651
- this._animationDelegate = new AnimationDelegate(this._object);
1652
- await this._animationDelegate.loadAnimations(this.options.animations || []);
1653
- }
3149
+ this.loadModelsDeferred();
1654
3150
  }
1655
- async data() {
3151
+ /**
3152
+ * Returns current data synchronously.
3153
+ * May return null values if loading is still in progress.
3154
+ */
3155
+ data() {
1656
3156
  return {
1657
3157
  animations: this._animationDelegate?.animations,
1658
- objectModel: this._object
3158
+ objectModel: this._object,
3159
+ collisionShape: this.options.collisionShape
1659
3160
  };
1660
3161
  }
1661
- async actorUpdate(params) {
3162
+ actorUpdate(params) {
1662
3163
  this._animationDelegate?.update(params.delta);
1663
3164
  }
1664
3165
  /**
@@ -1689,26 +3190,79 @@ var ZylemActor = class extends GameEntity {
1689
3190
  }
1690
3191
  this._modelFileNames = [];
1691
3192
  }
1692
- async loadModels() {
3193
+ /**
3194
+ * Deferred loading - starts async load and updates entity when complete.
3195
+ * Called by synchronous load() method.
3196
+ */
3197
+ loadModelsDeferred() {
1693
3198
  if (this._modelFileNames.length === 0) return;
3199
+ this.dispatch("entity:model:loading", {
3200
+ entityId: this.uuid,
3201
+ files: this._modelFileNames
3202
+ });
1694
3203
  const promises = this._modelFileNames.map((file) => this._assetLoader.loadFile(file));
1695
- const results = await Promise.all(promises);
1696
- if (results[0]?.object) {
1697
- this._object = results[0].object;
1698
- }
1699
- if (this._object) {
1700
- this.group = new Group4();
1701
- this.group.attach(this._object);
1702
- this.group.scale.set(
1703
- this.options.scale?.x || 1,
1704
- this.options.scale?.y || 1,
1705
- this.options.scale?.z || 1
1706
- );
1707
- }
3204
+ Promise.all(promises).then((results) => {
3205
+ if (results[0]?.object) {
3206
+ this._object = results[0].object;
3207
+ }
3208
+ let meshCount = 0;
3209
+ if (this._object) {
3210
+ this._object.traverse((child) => {
3211
+ if (child.isMesh) meshCount++;
3212
+ });
3213
+ 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
+ );
3220
+ this.applyMaterialOverrides();
3221
+ this._animationDelegate = new AnimationDelegate(this._object);
3222
+ this._animationDelegate.loadAnimations(this.options.animations || []).then(() => {
3223
+ this.dispatch("entity:animation:loaded", {
3224
+ entityId: this.uuid,
3225
+ animationCount: this.options.animations?.length || 0
3226
+ });
3227
+ });
3228
+ }
3229
+ this.dispatch("entity:model:loaded", {
3230
+ entityId: this.uuid,
3231
+ success: !!this._object,
3232
+ meshCount
3233
+ });
3234
+ });
1708
3235
  }
1709
3236
  playAnimation(animationOptions) {
1710
3237
  this._animationDelegate?.playAnimation(animationOptions);
1711
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
+ applyMaterialOverrides() {
3244
+ const materialOptions = this.options.material;
3245
+ if (!materialOptions || !materialOptions.color && !materialOptions.path) {
3246
+ return;
3247
+ }
3248
+ if (!this._object) return;
3249
+ 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
+ }
3263
+ }
3264
+ });
3265
+ }
1712
3266
  get object() {
1713
3267
  return this._object;
1714
3268
  }
@@ -1745,8 +3299,8 @@ var ZylemActor = class extends GameEntity {
1745
3299
  return debugInfo;
1746
3300
  }
1747
3301
  };
1748
- async function actor(...args) {
1749
- return await createEntity({
3302
+ function createActor(...args) {
3303
+ return createEntity({
1750
3304
  args,
1751
3305
  defaultConfig: actorDefaults,
1752
3306
  EntityClass: ZylemActor,
@@ -1757,7 +3311,7 @@ async function actor(...args) {
1757
3311
  }
1758
3312
 
1759
3313
  // src/lib/entities/text.ts
1760
- import { Color as Color8, Group as Group5, Sprite as ThreeSprite2, SpriteMaterial as SpriteMaterial2, CanvasTexture, LinearFilter, Vector2 as Vector23, ClampToEdgeWrapping } from "three";
3314
+ import { Color as Color6, Group as Group5, Sprite as ThreeSprite2, SpriteMaterial as SpriteMaterial2, CanvasTexture, LinearFilter, Vector2 as Vector24, ClampToEdgeWrapping } from "three";
1761
3315
  var textDefaults = {
1762
3316
  position: void 0,
1763
3317
  text: "",
@@ -1767,7 +3321,7 @@ var textDefaults = {
1767
3321
  backgroundColor: null,
1768
3322
  padding: 4,
1769
3323
  stickToViewport: true,
1770
- screenPosition: new Vector23(24, 24),
3324
+ screenPosition: new Vector24(24, 24),
1771
3325
  zDistance: 1
1772
3326
  };
1773
3327
  var TextBuilder = class extends EntityBuilder {
@@ -1790,7 +3344,7 @@ var ZylemText = class _ZylemText extends GameEntity {
1790
3344
  this.options = { ...textDefaults, ...options };
1791
3345
  this.prependSetup(this.textSetup.bind(this));
1792
3346
  this.prependUpdate(this.textUpdate.bind(this));
1793
- this.onDestroy(this.textDestroy.bind(this));
3347
+ this.onCleanup(this.textDestroy.bind(this));
1794
3348
  }
1795
3349
  create() {
1796
3350
  this._sprite = null;
@@ -1820,10 +3374,10 @@ var ZylemText = class _ZylemText extends GameEntity {
1820
3374
  this.group?.add(this._sprite);
1821
3375
  this.redrawText(this.options.text ?? "");
1822
3376
  }
1823
- measureAndResizeCanvas(text2, fontSize, fontFamily, padding) {
3377
+ measureAndResizeCanvas(text, fontSize, fontFamily, padding) {
1824
3378
  if (!this._canvas || !this._ctx) return { sizeChanged: false };
1825
3379
  this._ctx.font = `${fontSize}px ${fontFamily}`;
1826
- const metrics = this._ctx.measureText(text2);
3380
+ const metrics = this._ctx.measureText(text);
1827
3381
  const textWidth = Math.ceil(metrics.width);
1828
3382
  const textHeight = Math.ceil(fontSize * 1.4);
1829
3383
  const nextW = Math.max(2, textWidth + padding * 2);
@@ -1835,7 +3389,7 @@ var ZylemText = class _ZylemText extends GameEntity {
1835
3389
  this._lastCanvasH = nextH;
1836
3390
  return { sizeChanged };
1837
3391
  }
1838
- drawCenteredText(text2, fontSize, fontFamily) {
3392
+ drawCenteredText(text, fontSize, fontFamily) {
1839
3393
  if (!this._canvas || !this._ctx) return;
1840
3394
  this._ctx.font = `${fontSize}px ${fontFamily}`;
1841
3395
  this._ctx.textAlign = "center";
@@ -1846,7 +3400,7 @@ var ZylemText = class _ZylemText extends GameEntity {
1846
3400
  this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
1847
3401
  }
1848
3402
  this._ctx.fillStyle = this.toCssColor(this.options.fontColor ?? "#FFFFFF");
1849
- this._ctx.fillText(text2, this._canvas.width / 2, this._canvas.height / 2);
3403
+ this._ctx.fillText(text, this._canvas.width / 2, this._canvas.height / 2);
1850
3404
  }
1851
3405
  updateTexture(sizeChanged) {
1852
3406
  if (!this._texture || !this._canvas) return;
@@ -1879,7 +3433,7 @@ var ZylemText = class _ZylemText extends GameEntity {
1879
3433
  }
1880
3434
  toCssColor(color) {
1881
3435
  if (typeof color === "string") return color;
1882
- const c = color instanceof Color8 ? color : new Color8(color);
3436
+ const c = color instanceof Color6 ? color : new Color6(color);
1883
3437
  return `#${c.getHexString()}`;
1884
3438
  }
1885
3439
  textSetup(params) {
@@ -1939,7 +3493,7 @@ var ZylemText = class _ZylemText extends GameEntity {
1939
3493
  if (!this._sprite || !this._cameraRef) return;
1940
3494
  const camera = this._cameraRef.camera;
1941
3495
  const { width, height } = this.getResolution();
1942
- const sp = this.options.screenPosition ?? new Vector23(24, 24);
3496
+ const sp = this.options.screenPosition ?? new Vector24(24, 24);
1943
3497
  const { px, py } = this.getScreenPixels(sp, width, height);
1944
3498
  const zDist = Math.max(1e-3, this.options.zDistance ?? 1);
1945
3499
  const { worldHalfW, worldHalfH } = this.computeWorldExtents(camera, zDist);
@@ -1970,7 +3524,7 @@ var ZylemText = class _ZylemText extends GameEntity {
1970
3524
  /**
1971
3525
  * Dispose of Three.js resources when the entity is destroyed.
1972
3526
  */
1973
- async textDestroy() {
3527
+ textDestroy() {
1974
3528
  this._texture?.dispose();
1975
3529
  if (this._sprite?.material) {
1976
3530
  this._sprite.material.dispose();
@@ -1986,7 +3540,7 @@ var ZylemText = class _ZylemText extends GameEntity {
1986
3540
  this._cameraRef = null;
1987
3541
  }
1988
3542
  };
1989
- async function text(...args) {
3543
+ function createText(...args) {
1990
3544
  return createEntity({
1991
3545
  args,
1992
3546
  defaultConfig: { ...textDefaults },
@@ -1997,7 +3551,7 @@ async function text(...args) {
1997
3551
  }
1998
3552
 
1999
3553
  // src/lib/entities/rect.ts
2000
- import { Color as Color9, Group as Group6, Sprite as ThreeSprite3, SpriteMaterial as SpriteMaterial3, CanvasTexture as CanvasTexture2, LinearFilter as LinearFilter2, Vector2 as Vector24, ClampToEdgeWrapping as ClampToEdgeWrapping2, ShaderMaterial as ShaderMaterial3, Mesh as Mesh4, PlaneGeometry as PlaneGeometry2, Vector3 as Vector39 } from "three";
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";
2001
3555
  var rectDefaults = {
2002
3556
  position: void 0,
2003
3557
  width: 120,
@@ -2008,9 +3562,9 @@ var rectDefaults = {
2008
3562
  radius: 0,
2009
3563
  padding: 0,
2010
3564
  stickToViewport: true,
2011
- screenPosition: new Vector24(24, 24),
3565
+ screenPosition: new Vector25(24, 24),
2012
3566
  zDistance: 1,
2013
- anchor: new Vector24(0, 0)
3567
+ anchor: new Vector25(0, 0)
2014
3568
  };
2015
3569
  var RectBuilder = class extends EntityBuilder {
2016
3570
  createEntity(options) {
@@ -2111,6 +3665,12 @@ var ZylemRect = class _ZylemRect extends GameEntity {
2111
3665
  }
2112
3666
  }
2113
3667
  }
3668
+ getWidth() {
3669
+ return this.options.width ?? 0;
3670
+ }
3671
+ getHeight() {
3672
+ return this.options.height ?? 0;
3673
+ }
2114
3674
  roundedRectPath(ctx, x, y, w, h, r) {
2115
3675
  const radius = Math.min(r, Math.floor(Math.min(w, h) / 2));
2116
3676
  ctx.moveTo(x + radius, y);
@@ -2125,9 +3685,27 @@ var ZylemRect = class _ZylemRect extends GameEntity {
2125
3685
  }
2126
3686
  toCssColor(color) {
2127
3687
  if (typeof color === "string") return color;
2128
- const c = color instanceof Color9 ? color : new Color9(color);
3688
+ const c = color instanceof Color7 ? color : new Color7(color);
2129
3689
  return `#${c.getHexString()}`;
2130
3690
  }
3691
+ /**
3692
+ * Get the viewport resolution from the renderer DOM element,
3693
+ * falling back to the camera's screenResolution if the renderer
3694
+ * is not available (e.g. when using the new RendererManager path).
3695
+ */
3696
+ getResolution() {
3697
+ try {
3698
+ const dom = this._cameraRef?.renderer?.domElement;
3699
+ if (dom) {
3700
+ return { width: dom.clientWidth || 1, height: dom.clientHeight || 1 };
3701
+ }
3702
+ } catch {
3703
+ }
3704
+ return {
3705
+ width: this._cameraRef?.screenResolution?.x ?? 1,
3706
+ height: this._cameraRef?.screenResolution?.y ?? 1
3707
+ };
3708
+ }
2131
3709
  rectSetup(params) {
2132
3710
  this._cameraRef = params.camera;
2133
3711
  if (this.options.stickToViewport && this._cameraRef) {
@@ -2143,7 +3721,7 @@ var ZylemRect = class _ZylemRect extends GameEntity {
2143
3721
  if (mat.uniforms?.tDiffuse) mat.uniforms.tDiffuse.value = this._texture;
2144
3722
  if (mat.uniforms?.iResolution && this._canvas) mat.uniforms.iResolution.value.set(this._canvas.width, this._canvas.height, 1);
2145
3723
  }
2146
- this._mesh = new Mesh4(new PlaneGeometry2(1, 1), mat);
3724
+ this._mesh = new Mesh6(new PlaneGeometry2(1, 1), mat);
2147
3725
  this.group?.add(this._mesh);
2148
3726
  this._sprite.visible = false;
2149
3727
  }
@@ -2152,17 +3730,16 @@ var ZylemRect = class _ZylemRect extends GameEntity {
2152
3730
  rectUpdate(params) {
2153
3731
  if (!this._sprite) return;
2154
3732
  if (this._cameraRef && this.options.bounds) {
2155
- const dom = this._cameraRef.renderer.domElement;
2156
3733
  const screen = this.computeScreenBoundsFromOptions(this.options.bounds);
2157
3734
  if (screen) {
2158
3735
  const { x, y, width, height } = screen;
2159
3736
  const desiredW = Math.max(2, Math.floor(width));
2160
3737
  const desiredH = Math.max(2, Math.floor(height));
2161
3738
  const changed = desiredW !== (this.options.width ?? 0) || desiredH !== (this.options.height ?? 0);
2162
- this.options.screenPosition = new Vector24(Math.floor(x), Math.floor(y));
3739
+ this.options.screenPosition = new Vector25(Math.floor(x), Math.floor(y));
2163
3740
  this.options.width = desiredW;
2164
3741
  this.options.height = desiredH;
2165
- this.options.anchor = new Vector24(0, 0);
3742
+ this.options.anchor = new Vector25(0, 0);
2166
3743
  if (changed) {
2167
3744
  this.redrawRect();
2168
3745
  }
@@ -2175,11 +3752,9 @@ var ZylemRect = class _ZylemRect extends GameEntity {
2175
3752
  updateStickyTransform() {
2176
3753
  if (!this._sprite || !this._cameraRef) return;
2177
3754
  const camera = this._cameraRef.camera;
2178
- const dom = this._cameraRef.renderer.domElement;
2179
- const width = dom.clientWidth;
2180
- const height = dom.clientHeight;
2181
- const px = (this.options.screenPosition ?? new Vector24(24, 24)).x;
2182
- const py = (this.options.screenPosition ?? new Vector24(24, 24)).y;
3755
+ 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;
2183
3758
  const zDist = Math.max(1e-3, this.options.zDistance ?? 1);
2184
3759
  let worldHalfW = 1;
2185
3760
  let worldHalfH = 1;
@@ -2210,7 +3785,7 @@ var ZylemRect = class _ZylemRect extends GameEntity {
2210
3785
  this._sprite.scale.set(scaleX, scaleY, 1);
2211
3786
  if (this._mesh) this._mesh.scale.set(scaleX, scaleY, 1);
2212
3787
  }
2213
- const anchor = this.options.anchor ?? new Vector24(0, 0);
3788
+ const anchor = this.options.anchor ?? new Vector25(0, 0);
2214
3789
  const ax = Math.min(100, Math.max(0, anchor.x)) / 100;
2215
3790
  const ay = Math.min(100, Math.max(0, anchor.y)) / 100;
2216
3791
  const offsetX = (0.5 - ax) * scaleX;
@@ -2220,22 +3795,21 @@ var ZylemRect = class _ZylemRect extends GameEntity {
2220
3795
  worldToScreen(point) {
2221
3796
  if (!this._cameraRef) return { x: 0, y: 0 };
2222
3797
  const camera = this._cameraRef.camera;
2223
- const dom = this._cameraRef.renderer.domElement;
3798
+ const { width, height } = this.getResolution();
2224
3799
  const v = point.clone().project(camera);
2225
- const x = (v.x + 1) / 2 * dom.clientWidth;
2226
- const y = (1 - v.y) / 2 * dom.clientHeight;
3800
+ const x = (v.x + 1) / 2 * width;
3801
+ const y = (1 - v.y) / 2 * height;
2227
3802
  return { x, y };
2228
3803
  }
2229
3804
  computeScreenBoundsFromOptions(bounds) {
2230
3805
  if (!this._cameraRef) return null;
2231
- const dom = this._cameraRef.renderer.domElement;
2232
3806
  if (bounds.screen) {
2233
3807
  return { ...bounds.screen };
2234
3808
  }
2235
3809
  if (bounds.world) {
2236
3810
  const { left, right, top, bottom, z = 0 } = bounds.world;
2237
- const tl = this.worldToScreen(new Vector39(left, top, z));
2238
- const br = this.worldToScreen(new Vector39(right, bottom, z));
3811
+ const tl = this.worldToScreen(new Vector313(left, top, z));
3812
+ const br = this.worldToScreen(new Vector313(right, bottom, z));
2239
3813
  const x = Math.min(tl.x, br.x);
2240
3814
  const y = Math.min(tl.y, br.y);
2241
3815
  const width = Math.abs(br.x - tl.x);
@@ -2263,7 +3837,7 @@ var ZylemRect = class _ZylemRect extends GameEntity {
2263
3837
  };
2264
3838
  }
2265
3839
  };
2266
- async function rect(...args) {
3840
+ function createRect(...args) {
2267
3841
  return createEntity({
2268
3842
  args,
2269
3843
  defaultConfig: { ...rectDefaults },
@@ -2272,15 +3846,224 @@ async function rect(...args) {
2272
3846
  entityType: ZylemRect.type
2273
3847
  });
2274
3848
  }
3849
+
3850
+ // src/lib/entities/cone.ts
3851
+ var coneDefaults = {
3852
+ ...commonDefaults,
3853
+ radius: 1,
3854
+ height: 2,
3855
+ radialSegments: 32
3856
+ };
3857
+ var CONE_TYPE = /* @__PURE__ */ Symbol("Cone");
3858
+ var ZylemCone = class _ZylemCone extends GameEntity {
3859
+ static type = CONE_TYPE;
3860
+ constructor(options) {
3861
+ super();
3862
+ this.options = { ...coneDefaults, ...options };
3863
+ }
3864
+ buildInfo() {
3865
+ const delegate = new DebugDelegate(this);
3866
+ const baseInfo = delegate.buildDebugInfo();
3867
+ const radius = this.options.radius ?? 1;
3868
+ const height = this.options.height ?? 2;
3869
+ return {
3870
+ ...baseInfo,
3871
+ type: String(_ZylemCone.type),
3872
+ radius: radius.toFixed(2),
3873
+ height: height.toFixed(2)
3874
+ };
3875
+ }
3876
+ };
3877
+ function createCone(...args) {
3878
+ const options = mergeArgs(args, coneDefaults);
3879
+ const entity = new ZylemCone(options);
3880
+ entity.add(
3881
+ coneMesh({
3882
+ radius: options.radius,
3883
+ height: options.height,
3884
+ radialSegments: options.radialSegments,
3885
+ material: options.material,
3886
+ color: options.color
3887
+ }),
3888
+ coneCollision({
3889
+ radius: options.radius,
3890
+ height: options.height,
3891
+ static: options.collision?.static,
3892
+ sensor: options.collision?.sensor,
3893
+ collisionType: options.collisionType,
3894
+ collisionFilter: options.collisionFilter
3895
+ })
3896
+ );
3897
+ return entity;
3898
+ }
3899
+
3900
+ // src/lib/entities/pyramid.ts
3901
+ var pyramidDefaults = {
3902
+ ...commonDefaults,
3903
+ radius: 1,
3904
+ height: 2
3905
+ };
3906
+ var PYRAMID_TYPE = /* @__PURE__ */ Symbol("Pyramid");
3907
+ var ZylemPyramid = class _ZylemPyramid extends GameEntity {
3908
+ static type = PYRAMID_TYPE;
3909
+ constructor(options) {
3910
+ super();
3911
+ this.options = { ...pyramidDefaults, ...options };
3912
+ }
3913
+ buildInfo() {
3914
+ const delegate = new DebugDelegate(this);
3915
+ const baseInfo = delegate.buildDebugInfo();
3916
+ const radius = this.options.radius ?? 1;
3917
+ const height = this.options.height ?? 2;
3918
+ return {
3919
+ ...baseInfo,
3920
+ type: String(_ZylemPyramid.type),
3921
+ radius: radius.toFixed(2),
3922
+ height: height.toFixed(2)
3923
+ };
3924
+ }
3925
+ };
3926
+ function createPyramid(...args) {
3927
+ const options = mergeArgs(args, pyramidDefaults);
3928
+ const entity = new ZylemPyramid(options);
3929
+ entity.add(
3930
+ pyramidMesh({
3931
+ radius: options.radius,
3932
+ height: options.height,
3933
+ material: options.material,
3934
+ color: options.color
3935
+ }),
3936
+ pyramidCollision({
3937
+ radius: options.radius,
3938
+ height: options.height,
3939
+ static: options.collision?.static,
3940
+ sensor: options.collision?.sensor,
3941
+ collisionType: options.collisionType,
3942
+ collisionFilter: options.collisionFilter
3943
+ })
3944
+ );
3945
+ return entity;
3946
+ }
3947
+
3948
+ // src/lib/entities/cylinder.ts
3949
+ var cylinderDefaults = {
3950
+ ...commonDefaults,
3951
+ radiusTop: 1,
3952
+ radiusBottom: 1,
3953
+ height: 2,
3954
+ radialSegments: 32
3955
+ };
3956
+ var CYLINDER_TYPE = /* @__PURE__ */ Symbol("Cylinder");
3957
+ var ZylemCylinder = class _ZylemCylinder extends GameEntity {
3958
+ static type = CYLINDER_TYPE;
3959
+ constructor(options) {
3960
+ super();
3961
+ this.options = { ...cylinderDefaults, ...options };
3962
+ }
3963
+ buildInfo() {
3964
+ const delegate = new DebugDelegate(this);
3965
+ const baseInfo = delegate.buildDebugInfo();
3966
+ const radiusTop = this.options.radiusTop ?? 1;
3967
+ const radiusBottom = this.options.radiusBottom ?? 1;
3968
+ const height = this.options.height ?? 2;
3969
+ return {
3970
+ ...baseInfo,
3971
+ type: String(_ZylemCylinder.type),
3972
+ radiusTop: radiusTop.toFixed(2),
3973
+ radiusBottom: radiusBottom.toFixed(2),
3974
+ height: height.toFixed(2)
3975
+ };
3976
+ }
3977
+ };
3978
+ function createCylinder(...args) {
3979
+ const options = mergeArgs(args, cylinderDefaults);
3980
+ const entity = new ZylemCylinder(options);
3981
+ entity.add(
3982
+ cylinderMesh({
3983
+ radiusTop: options.radiusTop,
3984
+ radiusBottom: options.radiusBottom,
3985
+ height: options.height,
3986
+ radialSegments: options.radialSegments,
3987
+ material: options.material,
3988
+ color: options.color
3989
+ }),
3990
+ cylinderCollision({
3991
+ radiusTop: options.radiusTop,
3992
+ radiusBottom: options.radiusBottom,
3993
+ height: options.height,
3994
+ static: options.collision?.static,
3995
+ sensor: options.collision?.sensor,
3996
+ collisionType: options.collisionType,
3997
+ collisionFilter: options.collisionFilter
3998
+ })
3999
+ );
4000
+ return entity;
4001
+ }
4002
+
4003
+ // src/lib/entities/pill.ts
4004
+ var pillDefaults = {
4005
+ ...commonDefaults,
4006
+ radius: 0.5,
4007
+ length: 1,
4008
+ capSegments: 10,
4009
+ radialSegments: 20
4010
+ };
4011
+ var PILL_TYPE = /* @__PURE__ */ Symbol("Pill");
4012
+ var ZylemPill = class _ZylemPill extends GameEntity {
4013
+ static type = PILL_TYPE;
4014
+ constructor(options) {
4015
+ super();
4016
+ this.options = { ...pillDefaults, ...options };
4017
+ }
4018
+ buildInfo() {
4019
+ const delegate = new DebugDelegate(this);
4020
+ const baseInfo = delegate.buildDebugInfo();
4021
+ const radius = this.options.radius ?? 0.5;
4022
+ const length = this.options.length ?? 1;
4023
+ return {
4024
+ ...baseInfo,
4025
+ type: String(_ZylemPill.type),
4026
+ radius: radius.toFixed(2),
4027
+ length: length.toFixed(2)
4028
+ };
4029
+ }
4030
+ };
4031
+ function createPill(...args) {
4032
+ const options = mergeArgs(args, pillDefaults);
4033
+ const entity = new ZylemPill(options);
4034
+ entity.add(
4035
+ pillMesh({
4036
+ radius: options.radius,
4037
+ length: options.length,
4038
+ capSegments: options.capSegments,
4039
+ radialSegments: options.radialSegments,
4040
+ material: options.material,
4041
+ color: options.color
4042
+ }),
4043
+ pillCollision({
4044
+ radius: options.radius,
4045
+ length: options.length,
4046
+ static: options.collision?.static,
4047
+ sensor: options.collision?.sensor,
4048
+ collisionType: options.collisionType,
4049
+ collisionFilter: options.collisionFilter
4050
+ })
4051
+ );
4052
+ return entity;
4053
+ }
2275
4054
  export {
2276
4055
  ZylemBox,
2277
- actor,
2278
- box,
2279
- plane,
2280
- rect,
2281
- sphere,
2282
- sprite,
2283
- text,
2284
- zone
4056
+ createActor,
4057
+ createBox,
4058
+ createCone,
4059
+ createCylinder,
4060
+ createPill,
4061
+ createPlane,
4062
+ createPyramid,
4063
+ createRect,
4064
+ createSphere,
4065
+ createSprite,
4066
+ createText,
4067
+ createZone
2285
4068
  };
2286
4069
  //# sourceMappingURL=entities.js.map