@zylem/game-lib 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -40,10 +40,64 @@ var init_path_utils = __esm({
40
40
  }
41
41
  });
42
42
 
43
+ // src/lib/game/game-event-bus.ts
44
+ var GameEventBus, gameEventBus;
45
+ var init_game_event_bus = __esm({
46
+ "src/lib/game/game-event-bus.ts"() {
47
+ "use strict";
48
+ GameEventBus = class {
49
+ listeners = /* @__PURE__ */ new Map();
50
+ /**
51
+ * Subscribe to an event type.
52
+ */
53
+ on(event, callback) {
54
+ if (!this.listeners.has(event)) {
55
+ this.listeners.set(event, /* @__PURE__ */ new Set());
56
+ }
57
+ this.listeners.get(event).add(callback);
58
+ return () => this.off(event, callback);
59
+ }
60
+ /**
61
+ * Unsubscribe from an event type.
62
+ */
63
+ off(event, callback) {
64
+ this.listeners.get(event)?.delete(callback);
65
+ }
66
+ /**
67
+ * Emit an event to all subscribers.
68
+ */
69
+ emit(event, payload) {
70
+ const callbacks = this.listeners.get(event);
71
+ if (!callbacks) return;
72
+ for (const cb of callbacks) {
73
+ try {
74
+ cb(payload);
75
+ } catch (e) {
76
+ console.error(`Error in event handler for ${event}`, e);
77
+ }
78
+ }
79
+ }
80
+ /**
81
+ * Clear all listeners.
82
+ */
83
+ dispose() {
84
+ this.listeners.clear();
85
+ }
86
+ };
87
+ gameEventBus = new GameEventBus();
88
+ }
89
+ });
90
+
43
91
  // src/lib/game/game-state.ts
44
92
  import { proxy, subscribe } from "valtio/vanilla";
45
93
  function setGlobal(path, value) {
94
+ const previousValue = getByPath(state.globals, path);
46
95
  setByPath(state.globals, path, value);
96
+ gameEventBus.emit("game:state:updated", {
97
+ path,
98
+ value,
99
+ previousValue
100
+ });
47
101
  }
48
102
  function createGlobal(path, defaultValue) {
49
103
  const existing = getByPath(state.globals, path);
@@ -58,17 +112,22 @@ function getGlobal(path) {
58
112
  }
59
113
  function onGlobalChange(path, callback) {
60
114
  let previous = getByPath(state.globals, path);
61
- return subscribe(state.globals, () => {
115
+ const unsub = subscribe(state.globals, () => {
62
116
  const current = getByPath(state.globals, path);
63
117
  if (current !== previous) {
64
118
  previous = current;
65
119
  callback(current);
66
120
  }
67
121
  });
122
+ activeSubscriptions.add(unsub);
123
+ return () => {
124
+ unsub();
125
+ activeSubscriptions.delete(unsub);
126
+ };
68
127
  }
69
128
  function onGlobalChanges(paths, callback) {
70
129
  let previousValues = paths.map((p) => getByPath(state.globals, p));
71
- return subscribe(state.globals, () => {
130
+ const unsub = subscribe(state.globals, () => {
72
131
  const currentValues = paths.map((p) => getByPath(state.globals, p));
73
132
  const hasChange = currentValues.some((val, i) => val !== previousValues[i]);
74
133
  if (hasChange) {
@@ -76,6 +135,11 @@ function onGlobalChanges(paths, callback) {
76
135
  callback(currentValues);
77
136
  }
78
137
  });
138
+ activeSubscriptions.add(unsub);
139
+ return () => {
140
+ unsub();
141
+ activeSubscriptions.delete(unsub);
142
+ };
79
143
  }
80
144
  function getGlobals() {
81
145
  return state.globals;
@@ -88,16 +152,24 @@ function initGlobals(globals) {
88
152
  function resetGlobals() {
89
153
  state.globals = {};
90
154
  }
91
- var state;
155
+ function clearGlobalSubscriptions() {
156
+ for (const unsub of activeSubscriptions) {
157
+ unsub();
158
+ }
159
+ activeSubscriptions.clear();
160
+ }
161
+ var state, activeSubscriptions;
92
162
  var init_game_state = __esm({
93
163
  "src/lib/game/game-state.ts"() {
94
164
  "use strict";
95
165
  init_path_utils();
166
+ init_game_event_bus();
96
167
  state = proxy({
97
168
  id: "",
98
169
  globals: {},
99
170
  time: 0
100
171
  });
172
+ activeSubscriptions = /* @__PURE__ */ new Set();
101
173
  }
102
174
  });
103
175
 
@@ -112,6 +184,9 @@ function setPaused(paused) {
112
184
  function getDebugTool() {
113
185
  return debugState.tool;
114
186
  }
187
+ function setDebugTool(tool) {
188
+ debugState.tool = tool;
189
+ }
115
190
  function setSelectedEntity(entity) {
116
191
  debugState.selectedEntity = entity;
117
192
  }
@@ -163,21 +238,76 @@ var init_base_node = __esm({
163
238
  uuid = "";
164
239
  name = "";
165
240
  markedForRemoval = false;
166
- setup = () => {
167
- };
168
- loaded = () => {
169
- };
170
- update = () => {
171
- };
172
- destroy = () => {
173
- };
174
- cleanup = () => {
241
+ /**
242
+ * Lifecycle callback arrays - use onSetup(), onUpdate(), etc. to add callbacks
243
+ */
244
+ lifecycleCallbacks = {
245
+ setup: [],
246
+ loaded: [],
247
+ update: [],
248
+ destroy: [],
249
+ cleanup: []
175
250
  };
176
251
  constructor(args = []) {
177
252
  const options = args.filter((arg) => !(arg instanceof _BaseNode)).reduce((acc, opt) => ({ ...acc, ...opt }), {});
178
253
  this.options = options;
179
254
  this.uuid = nanoid();
180
255
  }
256
+ // ─────────────────────────────────────────────────────────────────────────────
257
+ // Fluent API for adding lifecycle callbacks
258
+ // ─────────────────────────────────────────────────────────────────────────────
259
+ /**
260
+ * Add setup callbacks to be executed in order during nodeSetup
261
+ */
262
+ onSetup(...callbacks) {
263
+ this.lifecycleCallbacks.setup.push(...callbacks);
264
+ return this;
265
+ }
266
+ /**
267
+ * Add loaded callbacks to be executed in order during nodeLoaded
268
+ */
269
+ onLoaded(...callbacks) {
270
+ this.lifecycleCallbacks.loaded.push(...callbacks);
271
+ return this;
272
+ }
273
+ /**
274
+ * Add update callbacks to be executed in order during nodeUpdate
275
+ */
276
+ onUpdate(...callbacks) {
277
+ this.lifecycleCallbacks.update.push(...callbacks);
278
+ return this;
279
+ }
280
+ /**
281
+ * Add destroy callbacks to be executed in order during nodeDestroy
282
+ */
283
+ onDestroy(...callbacks) {
284
+ this.lifecycleCallbacks.destroy.push(...callbacks);
285
+ return this;
286
+ }
287
+ /**
288
+ * Add cleanup callbacks to be executed in order during nodeCleanup
289
+ */
290
+ onCleanup(...callbacks) {
291
+ this.lifecycleCallbacks.cleanup.push(...callbacks);
292
+ return this;
293
+ }
294
+ /**
295
+ * Prepend setup callbacks (run before existing ones)
296
+ */
297
+ prependSetup(...callbacks) {
298
+ this.lifecycleCallbacks.setup.unshift(...callbacks);
299
+ return this;
300
+ }
301
+ /**
302
+ * Prepend update callbacks (run before existing ones)
303
+ */
304
+ prependUpdate(...callbacks) {
305
+ this.lifecycleCallbacks.update.unshift(...callbacks);
306
+ return this;
307
+ }
308
+ // ─────────────────────────────────────────────────────────────────────────────
309
+ // Tree structure
310
+ // ─────────────────────────────────────────────────────────────────────────────
181
311
  setParent(parent) {
182
312
  this.parent = parent;
183
313
  }
@@ -201,6 +331,9 @@ var init_base_node = __esm({
201
331
  isComposite() {
202
332
  return this.children.length > 0;
203
333
  }
334
+ // ─────────────────────────────────────────────────────────────────────────────
335
+ // Node lifecycle execution - runs internal + callback arrays
336
+ // ─────────────────────────────────────────────────────────────────────────────
204
337
  nodeSetup(params) {
205
338
  if (DEBUG_FLAG) {
206
339
  }
@@ -208,8 +341,8 @@ var init_base_node = __esm({
208
341
  if (typeof this._setup === "function") {
209
342
  this._setup(params);
210
343
  }
211
- if (this.setup) {
212
- this.setup(params);
344
+ for (const callback of this.lifecycleCallbacks.setup) {
345
+ callback(params);
213
346
  }
214
347
  this.children.forEach((child) => child.nodeSetup(params));
215
348
  }
@@ -220,21 +353,40 @@ var init_base_node = __esm({
220
353
  if (typeof this._update === "function") {
221
354
  this._update(params);
222
355
  }
223
- if (this.update) {
224
- this.update(params);
356
+ for (const callback of this.lifecycleCallbacks.update) {
357
+ callback(params);
225
358
  }
226
359
  this.children.forEach((child) => child.nodeUpdate(params));
227
360
  }
228
361
  nodeDestroy(params) {
229
362
  this.children.forEach((child) => child.nodeDestroy(params));
230
- if (this.destroy) {
231
- this.destroy(params);
363
+ for (const callback of this.lifecycleCallbacks.destroy) {
364
+ callback(params);
232
365
  }
233
366
  if (typeof this._destroy === "function") {
234
367
  this._destroy(params);
235
368
  }
236
369
  this.markedForRemoval = true;
237
370
  }
371
+ async nodeLoaded(params) {
372
+ if (typeof this._loaded === "function") {
373
+ await this._loaded(params);
374
+ }
375
+ for (const callback of this.lifecycleCallbacks.loaded) {
376
+ callback(params);
377
+ }
378
+ }
379
+ async nodeCleanup(params) {
380
+ for (const callback of this.lifecycleCallbacks.cleanup) {
381
+ callback(params);
382
+ }
383
+ if (typeof this._cleanup === "function") {
384
+ await this._cleanup(params);
385
+ }
386
+ }
387
+ // ─────────────────────────────────────────────────────────────────────────────
388
+ // Options
389
+ // ─────────────────────────────────────────────────────────────────────────────
238
390
  getOptions() {
239
391
  return this.options;
240
392
  }
@@ -250,57 +402,54 @@ import {
250
402
  defineSystem,
251
403
  defineQuery,
252
404
  defineComponent,
253
- Types
405
+ Types,
406
+ removeQuery
254
407
  } from "bitecs";
255
408
  import { Quaternion } from "three";
256
409
  function createTransformSystem(stage) {
257
- const transformQuery = defineQuery([position, rotation]);
410
+ const queryTerms = [position, rotation];
411
+ const transformQuery = defineQuery(queryTerms);
258
412
  const stageEntities = stage._childrenMap;
259
- return defineSystem((world) => {
413
+ const system = defineSystem((world) => {
260
414
  const entities = transformQuery(world);
261
415
  if (stageEntities === void 0) {
262
416
  return world;
263
417
  }
264
- ;
265
- for (const [key, value] of stageEntities) {
266
- const id = entities[key];
267
- const stageEntity = value;
268
- if (stageEntity === void 0 || !stageEntity?.body || stageEntity.markedForRemoval) {
418
+ for (const [key, stageEntity] of stageEntities) {
419
+ if (!stageEntity?.body || stageEntity.markedForRemoval) {
269
420
  continue;
270
421
  }
271
- const { x, y, z } = stageEntity.body.translation();
272
- position.x[id] = x;
273
- position.y[id] = y;
274
- position.z[id] = z;
275
- if (stageEntity.group) {
276
- stageEntity.group.position.set(position.x[id], position.y[id], position.z[id]);
277
- } else if (stageEntity.mesh) {
278
- stageEntity.mesh.position.set(position.x[id], position.y[id], position.z[id]);
422
+ const id = entities[key];
423
+ const body = stageEntity.body;
424
+ const target = stageEntity.group ?? stageEntity.mesh;
425
+ const translation = body.translation();
426
+ position.x[id] = translation.x;
427
+ position.y[id] = translation.y;
428
+ position.z[id] = translation.z;
429
+ if (target) {
430
+ target.position.set(translation.x, translation.y, translation.z);
279
431
  }
280
432
  if (stageEntity.controlledRotation) {
281
433
  continue;
282
434
  }
283
- const { x: rx, y: ry, z: rz, w: rw } = stageEntity.body.rotation();
284
- rotation.x[id] = rx;
285
- rotation.y[id] = ry;
286
- rotation.z[id] = rz;
287
- rotation.w[id] = rw;
288
- const newRotation = new Quaternion(
289
- rotation.x[id],
290
- rotation.y[id],
291
- rotation.z[id],
292
- rotation.w[id]
293
- );
294
- if (stageEntity.group) {
295
- stageEntity.group.setRotationFromQuaternion(newRotation);
296
- } else if (stageEntity.mesh) {
297
- stageEntity.mesh.setRotationFromQuaternion(newRotation);
435
+ const rot = body.rotation();
436
+ rotation.x[id] = rot.x;
437
+ rotation.y[id] = rot.y;
438
+ rotation.z[id] = rot.z;
439
+ rotation.w[id] = rot.w;
440
+ if (target) {
441
+ _tempQuaternion.set(rot.x, rot.y, rot.z, rot.w);
442
+ target.setRotationFromQuaternion(_tempQuaternion);
298
443
  }
299
444
  }
300
445
  return world;
301
446
  });
447
+ const destroy2 = (world) => {
448
+ removeQuery(world, transformQuery);
449
+ };
450
+ return { system, destroy: destroy2 };
302
451
  }
303
- var position, rotation, scale;
452
+ var position, rotation, scale, _tempQuaternion;
304
453
  var init_transformable_system = __esm({
305
454
  "src/lib/systems/transformable.system.ts"() {
306
455
  "use strict";
@@ -320,6 +469,7 @@ var init_transformable_system = __esm({
320
469
  y: Types.f32,
321
470
  z: Types.f32
322
471
  });
472
+ _tempQuaternion = new Quaternion();
323
473
  }
324
474
  });
325
475
 
@@ -343,11 +493,6 @@ var init_entity = __esm({
343
493
  custom = {};
344
494
  debugInfo = {};
345
495
  debugMaterial;
346
- lifeCycleDelegate = {
347
- setup: [],
348
- update: [],
349
- destroy: []
350
- };
351
496
  collisionDelegate = {
352
497
  collision: []
353
498
  };
@@ -372,67 +517,40 @@ var init_entity = __esm({
372
517
  this.name = this.options.name || "";
373
518
  return this;
374
519
  }
375
- onSetup(...callbacks) {
376
- const combineCallbacks = [...this.lifeCycleDelegate.setup ?? [], ...callbacks];
377
- this.lifeCycleDelegate = {
378
- ...this.lifeCycleDelegate,
379
- setup: combineCallbacks
380
- };
381
- return this;
382
- }
383
- onUpdate(...callbacks) {
384
- const combineCallbacks = [...this.lifeCycleDelegate.update ?? [], ...callbacks];
385
- this.lifeCycleDelegate = {
386
- ...this.lifeCycleDelegate,
387
- update: combineCallbacks
388
- };
389
- return this;
390
- }
391
- onDestroy(...callbacks) {
392
- this.lifeCycleDelegate = {
393
- ...this.lifeCycleDelegate,
394
- destroy: callbacks.length > 0 ? callbacks : void 0
395
- };
396
- return this;
397
- }
520
+ /**
521
+ * Add collision callbacks
522
+ */
398
523
  onCollision(...callbacks) {
399
- this.collisionDelegate = {
400
- collision: callbacks.length > 0 ? callbacks : void 0
401
- };
524
+ const existing = this.collisionDelegate.collision ?? [];
525
+ this.collisionDelegate.collision = [...existing, ...callbacks];
402
526
  return this;
403
527
  }
528
+ /**
529
+ * Entity-specific setup - runs behavior callbacks
530
+ * (User callbacks are handled by BaseNode's lifecycleCallbacks.setup)
531
+ */
404
532
  _setup(params) {
405
533
  this.behaviorCallbackMap.setup.forEach((callback) => {
406
534
  callback({ ...params, me: this });
407
535
  });
408
- if (this.lifeCycleDelegate.setup?.length) {
409
- const callbacks = this.lifeCycleDelegate.setup;
410
- callbacks.forEach((callback) => {
411
- callback({ ...params, me: this });
412
- });
413
- }
414
536
  }
415
537
  async _loaded(_params) {
416
538
  }
539
+ /**
540
+ * Entity-specific update - updates materials and runs behavior callbacks
541
+ * (User callbacks are handled by BaseNode's lifecycleCallbacks.update)
542
+ */
417
543
  _update(params) {
418
544
  this.updateMaterials(params);
419
- if (this.lifeCycleDelegate.update?.length) {
420
- const callbacks = this.lifeCycleDelegate.update;
421
- callbacks.forEach((callback) => {
422
- callback({ ...params, me: this });
423
- });
424
- }
425
545
  this.behaviorCallbackMap.update.forEach((callback) => {
426
546
  callback({ ...params, me: this });
427
547
  });
428
548
  }
549
+ /**
550
+ * Entity-specific destroy - runs behavior callbacks
551
+ * (User callbacks are handled by BaseNode's lifecycleCallbacks.destroy)
552
+ */
429
553
  _destroy(params) {
430
- if (this.lifeCycleDelegate.destroy?.length) {
431
- const callbacks = this.lifeCycleDelegate.destroy;
432
- callbacks.forEach((callback) => {
433
- callback({ ...params, me: this });
434
- });
435
- }
436
554
  this.behaviorCallbackMap.destroy.forEach((callback) => {
437
555
  callback({ ...params, me: this });
438
556
  });
@@ -722,6 +840,23 @@ var init_animation = __esm({
722
840
  this._currentAction = action;
723
841
  this._currentKey = key;
724
842
  }
843
+ /**
844
+ * Dispose of all animation resources
845
+ */
846
+ dispose() {
847
+ Object.values(this._actions).forEach((action) => {
848
+ action.stop();
849
+ });
850
+ if (this._mixer) {
851
+ this._mixer.stopAllAction();
852
+ this._mixer.uncacheRoot(this.target);
853
+ this._mixer = null;
854
+ }
855
+ this._actions = {};
856
+ this._animations = [];
857
+ this._currentAction = null;
858
+ this._currentKey = "";
859
+ }
725
860
  get currentAnimationKey() {
726
861
  return this._currentKey;
727
862
  }
@@ -1220,7 +1355,7 @@ var init_actor = __esm({
1220
1355
  return new ZylemActor(options);
1221
1356
  }
1222
1357
  };
1223
- ACTOR_TYPE = Symbol("Actor");
1358
+ ACTOR_TYPE = /* @__PURE__ */ Symbol("Actor");
1224
1359
  ZylemActor = class extends GameEntity {
1225
1360
  static type = ACTOR_TYPE;
1226
1361
  _object = null;
@@ -1231,9 +1366,7 @@ var init_actor = __esm({
1231
1366
  constructor(options) {
1232
1367
  super();
1233
1368
  this.options = { ...actorDefaults, ...options };
1234
- this.lifeCycleDelegate = {
1235
- update: [this.actorUpdate.bind(this)]
1236
- };
1369
+ this.prependUpdate(this.actorUpdate.bind(this));
1237
1370
  this.controlledRotation = true;
1238
1371
  }
1239
1372
  async load() {
@@ -1253,6 +1386,34 @@ var init_actor = __esm({
1253
1386
  async actorUpdate(params) {
1254
1387
  this._animationDelegate?.update(params.delta);
1255
1388
  }
1389
+ /**
1390
+ * Clean up actor resources including animations, models, and groups
1391
+ */
1392
+ actorDestroy() {
1393
+ if (this._animationDelegate) {
1394
+ this._animationDelegate.dispose();
1395
+ this._animationDelegate = null;
1396
+ }
1397
+ if (this._object) {
1398
+ this._object.traverse((child) => {
1399
+ if (child.isMesh) {
1400
+ const mesh = child;
1401
+ mesh.geometry?.dispose();
1402
+ if (Array.isArray(mesh.material)) {
1403
+ mesh.material.forEach((m) => m.dispose());
1404
+ } else if (mesh.material) {
1405
+ mesh.material.dispose();
1406
+ }
1407
+ }
1408
+ });
1409
+ this._object = null;
1410
+ }
1411
+ if (this.group) {
1412
+ this.group.clear();
1413
+ this.group = null;
1414
+ }
1415
+ this._modelFileNames = [];
1416
+ }
1256
1417
  async loadModels() {
1257
1418
  if (this._modelFileNames.length === 0) return;
1258
1419
  const promises = this._modelFileNames.map((file) => this._assetLoader.loadFile(file));
@@ -1312,25 +1473,17 @@ var init_actor = __esm({
1312
1473
  }
1313
1474
  });
1314
1475
 
1315
- // src/lib/collision/collision-delegate.ts
1476
+ // src/lib/collision/world.ts
1477
+ import RAPIER from "@dimforge/rapier3d-compat";
1316
1478
  function isCollisionHandlerDelegate(obj) {
1317
1479
  return typeof obj?.handlePostCollision === "function" && typeof obj?.handleIntersectionEvent === "function";
1318
1480
  }
1319
- var init_collision_delegate = __esm({
1320
- "src/lib/collision/collision-delegate.ts"() {
1321
- "use strict";
1322
- }
1323
- });
1324
-
1325
- // src/lib/collision/world.ts
1326
- import RAPIER from "@dimforge/rapier3d-compat";
1327
1481
  var ZylemWorld;
1328
1482
  var init_world = __esm({
1329
1483
  "src/lib/collision/world.ts"() {
1330
1484
  "use strict";
1331
1485
  init_game_state();
1332
1486
  init_actor();
1333
- init_collision_delegate();
1334
1487
  ZylemWorld = class {
1335
1488
  type = "World";
1336
1489
  world;
@@ -1536,7 +1689,11 @@ var init_zylem_scene = __esm({
1536
1689
  * Setup camera with the scene
1537
1690
  */
1538
1691
  setupCamera(scene, camera2) {
1539
- scene.add(camera2.cameraRig);
1692
+ if (camera2.cameraRig) {
1693
+ scene.add(camera2.cameraRig);
1694
+ } else {
1695
+ scene.add(camera2.camera);
1696
+ }
1540
1697
  camera2.setup(scene);
1541
1698
  }
1542
1699
  /**
@@ -1597,7 +1754,7 @@ var init_zylem_scene = __esm({
1597
1754
 
1598
1755
  // src/lib/stage/stage-state.ts
1599
1756
  import { Color as Color5, Vector3 as Vector35 } from "three";
1600
- import { proxy as proxy3, subscribe as subscribe2 } from "valtio/vanilla";
1757
+ import { proxy as proxy3, subscribe as subscribe3 } from "valtio/vanilla";
1601
1758
  function getOrCreateVariableProxy(target) {
1602
1759
  let store = variableProxyStore.get(target);
1603
1760
  if (!store) {
@@ -1627,7 +1784,7 @@ function getVariable(target, path) {
1627
1784
  function onVariableChange(target, path, callback) {
1628
1785
  const store = getOrCreateVariableProxy(target);
1629
1786
  let previous = getByPath(store, path);
1630
- return subscribe2(store, () => {
1787
+ return subscribe3(store, () => {
1631
1788
  const current = getByPath(store, path);
1632
1789
  if (current !== previous) {
1633
1790
  previous = current;
@@ -1638,7 +1795,7 @@ function onVariableChange(target, path, callback) {
1638
1795
  function onVariableChanges(target, paths, callback) {
1639
1796
  const store = getOrCreateVariableProxy(target);
1640
1797
  let previousValues = paths.map((p) => getByPath(store, p));
1641
- return subscribe2(store, () => {
1798
+ return subscribe3(store, () => {
1642
1799
  const currentValues = paths.map((p) => getByPath(store, p));
1643
1800
  const hasChange = currentValues.some((val, i) => val !== previousValues[i]);
1644
1801
  if (hasChange) {
@@ -1735,68 +1892,355 @@ var init_lifecycle_base = __esm({
1735
1892
  }
1736
1893
  });
1737
1894
 
1738
- // src/lib/camera/perspective.ts
1739
- var Perspectives;
1740
- var init_perspective = __esm({
1741
- "src/lib/camera/perspective.ts"() {
1742
- "use strict";
1743
- Perspectives = {
1744
- FirstPerson: "first-person",
1745
- ThirdPerson: "third-person",
1746
- Isometric: "isometric",
1747
- Flat2D: "flat-2d",
1748
- Fixed2D: "fixed-2d"
1749
- };
1750
- }
1751
- });
1752
-
1753
- // src/lib/camera/third-person.ts
1754
- import { Vector3 as Vector37 } from "three";
1755
- var ThirdPersonCamera;
1756
- var init_third_person = __esm({
1757
- "src/lib/camera/third-person.ts"() {
1895
+ // src/lib/stage/debug-entity-cursor.ts
1896
+ import {
1897
+ Box3,
1898
+ BoxGeometry,
1899
+ Color as Color7,
1900
+ EdgesGeometry,
1901
+ Group as Group4,
1902
+ LineBasicMaterial,
1903
+ LineSegments,
1904
+ Mesh as Mesh4,
1905
+ MeshBasicMaterial,
1906
+ Vector3 as Vector37
1907
+ } from "three";
1908
+ var DebugEntityCursor;
1909
+ var init_debug_entity_cursor = __esm({
1910
+ "src/lib/stage/debug-entity-cursor.ts"() {
1758
1911
  "use strict";
1759
- ThirdPersonCamera = class {
1760
- distance;
1761
- screenResolution = null;
1762
- renderer = null;
1763
- scene = null;
1764
- cameraRef = null;
1765
- constructor() {
1766
- this.distance = new Vector37(0, 5, 8);
1767
- }
1768
- /**
1769
- * Setup the third person camera controller
1770
- */
1771
- setup(params) {
1772
- const { screenResolution, renderer, scene, camera: camera2 } = params;
1773
- this.screenResolution = screenResolution;
1774
- this.renderer = renderer;
1912
+ DebugEntityCursor = class {
1913
+ scene;
1914
+ container;
1915
+ fillMesh;
1916
+ edgeLines;
1917
+ currentColor = new Color7(65280);
1918
+ bbox = new Box3();
1919
+ size = new Vector37();
1920
+ center = new Vector37();
1921
+ constructor(scene) {
1775
1922
  this.scene = scene;
1776
- this.cameraRef = camera2;
1923
+ const initialGeometry = new BoxGeometry(1, 1, 1);
1924
+ this.fillMesh = new Mesh4(
1925
+ initialGeometry,
1926
+ new MeshBasicMaterial({
1927
+ color: this.currentColor,
1928
+ transparent: true,
1929
+ opacity: 0.12,
1930
+ depthWrite: false
1931
+ })
1932
+ );
1933
+ const edges = new EdgesGeometry(initialGeometry);
1934
+ this.edgeLines = new LineSegments(
1935
+ edges,
1936
+ new LineBasicMaterial({ color: this.currentColor, linewidth: 1 })
1937
+ );
1938
+ this.container = new Group4();
1939
+ this.container.name = "DebugEntityCursor";
1940
+ this.container.add(this.fillMesh);
1941
+ this.container.add(this.edgeLines);
1942
+ this.container.visible = false;
1943
+ this.scene.add(this.container);
1944
+ }
1945
+ setColor(color) {
1946
+ this.currentColor.set(color);
1947
+ this.fillMesh.material.color.set(this.currentColor);
1948
+ this.edgeLines.material.color.set(this.currentColor);
1777
1949
  }
1778
1950
  /**
1779
- * Update the third person camera
1951
+ * Update the cursor to enclose the provided Object3D using a world-space AABB.
1780
1952
  */
1781
- update(delta) {
1782
- if (!this.cameraRef.target) {
1953
+ updateFromObject(object) {
1954
+ if (!object) {
1955
+ this.hide();
1783
1956
  return;
1784
1957
  }
1785
- const desiredCameraPosition = this.cameraRef.target.group.position.clone().add(this.distance);
1786
- this.cameraRef.camera.position.lerp(desiredCameraPosition, 0.1);
1787
- this.cameraRef.camera.lookAt(this.cameraRef.target.group.position);
1788
- }
1789
- /**
1790
- * Handle resize events
1791
- */
1792
- resize(width, height) {
1793
- if (this.screenResolution) {
1794
- this.screenResolution.set(width, height);
1958
+ this.bbox.setFromObject(object);
1959
+ if (!isFinite(this.bbox.min.x) || !isFinite(this.bbox.max.x)) {
1960
+ this.hide();
1961
+ return;
1795
1962
  }
1796
- }
1797
- /**
1798
- * Set the distance from the target
1799
- */
1963
+ this.bbox.getSize(this.size);
1964
+ this.bbox.getCenter(this.center);
1965
+ const newGeom = new BoxGeometry(
1966
+ Math.max(this.size.x, 1e-6),
1967
+ Math.max(this.size.y, 1e-6),
1968
+ Math.max(this.size.z, 1e-6)
1969
+ );
1970
+ this.fillMesh.geometry.dispose();
1971
+ this.fillMesh.geometry = newGeom;
1972
+ const newEdges = new EdgesGeometry(newGeom);
1973
+ this.edgeLines.geometry.dispose();
1974
+ this.edgeLines.geometry = newEdges;
1975
+ this.container.position.copy(this.center);
1976
+ this.container.visible = true;
1977
+ }
1978
+ hide() {
1979
+ this.container.visible = false;
1980
+ }
1981
+ dispose() {
1982
+ this.scene.remove(this.container);
1983
+ this.fillMesh.geometry.dispose();
1984
+ this.fillMesh.material.dispose();
1985
+ this.edgeLines.geometry.dispose();
1986
+ this.edgeLines.material.dispose();
1987
+ }
1988
+ };
1989
+ }
1990
+ });
1991
+
1992
+ // src/lib/stage/stage-debug-delegate.ts
1993
+ import { Ray } from "@dimforge/rapier3d-compat";
1994
+ import { BufferAttribute, BufferGeometry as BufferGeometry4, LineBasicMaterial as LineBasicMaterial2, LineSegments as LineSegments2, Raycaster, Vector2 as Vector22 } from "three";
1995
+ var SELECT_TOOL_COLOR, DELETE_TOOL_COLOR, StageDebugDelegate;
1996
+ var init_stage_debug_delegate = __esm({
1997
+ "src/lib/stage/stage-debug-delegate.ts"() {
1998
+ "use strict";
1999
+ init_debug_state();
2000
+ init_debug_entity_cursor();
2001
+ SELECT_TOOL_COLOR = 2293538;
2002
+ DELETE_TOOL_COLOR = 16724787;
2003
+ StageDebugDelegate = class {
2004
+ stage;
2005
+ options;
2006
+ mouseNdc = new Vector22(-2, -2);
2007
+ raycaster = new Raycaster();
2008
+ isMouseDown = false;
2009
+ disposeFns = [];
2010
+ debugCursor = null;
2011
+ debugLines = null;
2012
+ constructor(stage, options) {
2013
+ this.stage = stage;
2014
+ this.options = {
2015
+ maxRayDistance: options?.maxRayDistance ?? 5e3,
2016
+ addEntityFactory: options?.addEntityFactory ?? null
2017
+ };
2018
+ if (this.stage.scene) {
2019
+ this.debugLines = new LineSegments2(
2020
+ new BufferGeometry4(),
2021
+ new LineBasicMaterial2({ vertexColors: true })
2022
+ );
2023
+ this.stage.scene.scene.add(this.debugLines);
2024
+ this.debugLines.visible = true;
2025
+ this.debugCursor = new DebugEntityCursor(this.stage.scene.scene);
2026
+ }
2027
+ this.attachDomListeners();
2028
+ }
2029
+ update() {
2030
+ if (!debugState.enabled) return;
2031
+ if (!this.stage.scene || !this.stage.world || !this.stage.cameraRef) return;
2032
+ const { world, cameraRef } = this.stage;
2033
+ if (this.debugLines) {
2034
+ const { vertices, colors } = world.world.debugRender();
2035
+ this.debugLines.geometry.setAttribute("position", new BufferAttribute(vertices, 3));
2036
+ this.debugLines.geometry.setAttribute("color", new BufferAttribute(colors, 4));
2037
+ }
2038
+ const tool = getDebugTool();
2039
+ const isCursorTool = tool === "select" || tool === "delete";
2040
+ this.raycaster.setFromCamera(this.mouseNdc, cameraRef.camera);
2041
+ const origin = this.raycaster.ray.origin.clone();
2042
+ const direction = this.raycaster.ray.direction.clone().normalize();
2043
+ const rapierRay = new Ray(
2044
+ { x: origin.x, y: origin.y, z: origin.z },
2045
+ { x: direction.x, y: direction.y, z: direction.z }
2046
+ );
2047
+ const hit = world.world.castRay(rapierRay, this.options.maxRayDistance, true);
2048
+ if (hit && isCursorTool) {
2049
+ const rigidBody = hit.collider?._parent;
2050
+ const hoveredUuid2 = rigidBody?.userData?.uuid;
2051
+ if (hoveredUuid2) {
2052
+ const entity = this.stage._debugMap.get(hoveredUuid2);
2053
+ if (entity) setHoveredEntity(entity);
2054
+ } else {
2055
+ resetHoveredEntity();
2056
+ }
2057
+ if (this.isMouseDown) {
2058
+ this.handleActionOnHit(hoveredUuid2 ?? null, origin, direction, hit.toi);
2059
+ }
2060
+ }
2061
+ this.isMouseDown = false;
2062
+ const hoveredUuid = getHoveredEntity();
2063
+ if (!hoveredUuid) {
2064
+ this.debugCursor?.hide();
2065
+ return;
2066
+ }
2067
+ const hoveredEntity = this.stage._debugMap.get(`${hoveredUuid}`);
2068
+ const targetObject = hoveredEntity?.group ?? hoveredEntity?.mesh ?? null;
2069
+ if (!targetObject) {
2070
+ this.debugCursor?.hide();
2071
+ return;
2072
+ }
2073
+ switch (tool) {
2074
+ case "select":
2075
+ this.debugCursor?.setColor(SELECT_TOOL_COLOR);
2076
+ break;
2077
+ case "delete":
2078
+ this.debugCursor?.setColor(DELETE_TOOL_COLOR);
2079
+ break;
2080
+ default:
2081
+ this.debugCursor?.setColor(16777215);
2082
+ break;
2083
+ }
2084
+ this.debugCursor?.updateFromObject(targetObject);
2085
+ }
2086
+ dispose() {
2087
+ this.disposeFns.forEach((fn) => fn());
2088
+ this.disposeFns = [];
2089
+ this.debugCursor?.dispose();
2090
+ if (this.debugLines && this.stage.scene) {
2091
+ this.stage.scene.scene.remove(this.debugLines);
2092
+ this.debugLines.geometry.dispose();
2093
+ this.debugLines.material.dispose();
2094
+ this.debugLines = null;
2095
+ }
2096
+ }
2097
+ handleActionOnHit(hoveredUuid, origin, direction, toi) {
2098
+ const tool = getDebugTool();
2099
+ switch (tool) {
2100
+ case "select": {
2101
+ if (hoveredUuid) {
2102
+ const entity = this.stage._debugMap.get(hoveredUuid);
2103
+ if (entity) setSelectedEntity(entity);
2104
+ }
2105
+ break;
2106
+ }
2107
+ case "delete": {
2108
+ if (hoveredUuid) {
2109
+ this.stage.removeEntityByUuid(hoveredUuid);
2110
+ }
2111
+ break;
2112
+ }
2113
+ case "scale": {
2114
+ if (!this.options.addEntityFactory) break;
2115
+ const hitPosition = origin.clone().add(direction.clone().multiplyScalar(toi));
2116
+ const newNode = this.options.addEntityFactory({ position: hitPosition });
2117
+ if (newNode) {
2118
+ Promise.resolve(newNode).then((node) => {
2119
+ if (node) this.stage.spawnEntity(node);
2120
+ }).catch(() => {
2121
+ });
2122
+ }
2123
+ break;
2124
+ }
2125
+ default:
2126
+ break;
2127
+ }
2128
+ }
2129
+ attachDomListeners() {
2130
+ const canvas = this.stage.cameraRef?.renderer.domElement ?? this.stage.scene?.zylemCamera.renderer.domElement;
2131
+ if (!canvas) return;
2132
+ const onMouseMove = (e) => {
2133
+ const rect2 = canvas.getBoundingClientRect();
2134
+ const x = (e.clientX - rect2.left) / rect2.width * 2 - 1;
2135
+ const y = -((e.clientY - rect2.top) / rect2.height * 2 - 1);
2136
+ this.mouseNdc.set(x, y);
2137
+ };
2138
+ const onMouseDown = (e) => {
2139
+ this.isMouseDown = true;
2140
+ };
2141
+ canvas.addEventListener("mousemove", onMouseMove);
2142
+ canvas.addEventListener("mousedown", onMouseDown);
2143
+ this.disposeFns.push(() => canvas.removeEventListener("mousemove", onMouseMove));
2144
+ this.disposeFns.push(() => canvas.removeEventListener("mousedown", onMouseDown));
2145
+ }
2146
+ };
2147
+ }
2148
+ });
2149
+
2150
+ // src/lib/stage/stage-camera-debug-delegate.ts
2151
+ import { subscribe as subscribe4 } from "valtio/vanilla";
2152
+ var StageCameraDebugDelegate;
2153
+ var init_stage_camera_debug_delegate = __esm({
2154
+ "src/lib/stage/stage-camera-debug-delegate.ts"() {
2155
+ "use strict";
2156
+ init_debug_state();
2157
+ StageCameraDebugDelegate = class {
2158
+ stage;
2159
+ constructor(stage) {
2160
+ this.stage = stage;
2161
+ }
2162
+ subscribe(listener) {
2163
+ const notify = () => listener(this.snapshot());
2164
+ notify();
2165
+ return subscribe4(debugState, notify);
2166
+ }
2167
+ resolveTarget(uuid) {
2168
+ const entity = this.stage._debugMap.get(uuid) || this.stage.world?.collisionMap.get(uuid) || null;
2169
+ const target = entity?.group ?? entity?.mesh ?? null;
2170
+ return target ?? null;
2171
+ }
2172
+ snapshot() {
2173
+ return {
2174
+ enabled: debugState.enabled,
2175
+ selected: debugState.selectedEntity ? [debugState.selectedEntity.uuid] : []
2176
+ };
2177
+ }
2178
+ };
2179
+ }
2180
+ });
2181
+
2182
+ // src/lib/camera/perspective.ts
2183
+ var Perspectives;
2184
+ var init_perspective = __esm({
2185
+ "src/lib/camera/perspective.ts"() {
2186
+ "use strict";
2187
+ Perspectives = {
2188
+ FirstPerson: "first-person",
2189
+ ThirdPerson: "third-person",
2190
+ Isometric: "isometric",
2191
+ Flat2D: "flat-2d",
2192
+ Fixed2D: "fixed-2d"
2193
+ };
2194
+ }
2195
+ });
2196
+
2197
+ // src/lib/camera/third-person.ts
2198
+ import { Vector3 as Vector39 } from "three";
2199
+ var ThirdPersonCamera;
2200
+ var init_third_person = __esm({
2201
+ "src/lib/camera/third-person.ts"() {
2202
+ "use strict";
2203
+ ThirdPersonCamera = class {
2204
+ distance;
2205
+ screenResolution = null;
2206
+ renderer = null;
2207
+ scene = null;
2208
+ cameraRef = null;
2209
+ constructor() {
2210
+ this.distance = new Vector39(0, 5, 8);
2211
+ }
2212
+ /**
2213
+ * Setup the third person camera controller
2214
+ */
2215
+ setup(params) {
2216
+ const { screenResolution, renderer, scene, camera: camera2 } = params;
2217
+ this.screenResolution = screenResolution;
2218
+ this.renderer = renderer;
2219
+ this.scene = scene;
2220
+ this.cameraRef = camera2;
2221
+ }
2222
+ /**
2223
+ * Update the third person camera
2224
+ */
2225
+ update(delta) {
2226
+ if (!this.cameraRef.target) {
2227
+ return;
2228
+ }
2229
+ const desiredCameraPosition = this.cameraRef.target.group.position.clone().add(this.distance);
2230
+ this.cameraRef.camera.position.lerp(desiredCameraPosition, 0.1);
2231
+ this.cameraRef.camera.lookAt(this.cameraRef.target.group.position);
2232
+ }
2233
+ /**
2234
+ * Handle resize events
2235
+ */
2236
+ resize(width, height) {
2237
+ if (this.screenResolution) {
2238
+ this.screenResolution.set(width, height);
2239
+ }
2240
+ }
2241
+ /**
2242
+ * Set the distance from the target
2243
+ */
1800
2244
  setDistance(distance) {
1801
2245
  this.distance = distance;
1802
2246
  }
@@ -1901,41 +2345,217 @@ var init_render_pass = __esm({
1901
2345
  } else {
1902
2346
  renderer.setRenderTarget(writeBuffer);
1903
2347
  }
1904
- this.fsQuad.render(renderer);
2348
+ this.fsQuad.render(renderer);
2349
+ }
2350
+ material() {
2351
+ return new THREE.ShaderMaterial({
2352
+ uniforms: {
2353
+ iTime: { value: 0 },
2354
+ tDiffuse: { value: null },
2355
+ tDepth: { value: null },
2356
+ tNormal: { value: null },
2357
+ resolution: {
2358
+ value: new THREE.Vector4(
2359
+ this.resolution.x,
2360
+ this.resolution.y,
2361
+ 1 / this.resolution.x,
2362
+ 1 / this.resolution.y
2363
+ )
2364
+ }
2365
+ },
2366
+ vertexShader: standard_default2,
2367
+ fragmentShader: standard_default
2368
+ });
2369
+ }
2370
+ dispose() {
2371
+ try {
2372
+ this.fsQuad?.dispose?.();
2373
+ } catch {
2374
+ }
2375
+ try {
2376
+ this.rgbRenderTarget?.dispose?.();
2377
+ this.normalRenderTarget?.dispose?.();
2378
+ } catch {
2379
+ }
2380
+ try {
2381
+ this.normalMaterial?.dispose?.();
2382
+ } catch {
2383
+ }
2384
+ }
2385
+ };
2386
+ }
2387
+ });
2388
+
2389
+ // src/lib/camera/camera-debug-delegate.ts
2390
+ import { Vector3 as Vector310 } from "three";
2391
+ import { OrbitControls } from "three/addons/controls/OrbitControls.js";
2392
+ var CameraOrbitController;
2393
+ var init_camera_debug_delegate = __esm({
2394
+ "src/lib/camera/camera-debug-delegate.ts"() {
2395
+ "use strict";
2396
+ CameraOrbitController = class {
2397
+ camera;
2398
+ domElement;
2399
+ orbitControls = null;
2400
+ orbitTarget = null;
2401
+ orbitTargetWorldPos = new Vector310();
2402
+ debugDelegate = null;
2403
+ debugUnsubscribe = null;
2404
+ debugStateSnapshot = { enabled: false, selected: [] };
2405
+ // Saved camera state for restoration when exiting debug mode
2406
+ savedCameraPosition = null;
2407
+ savedCameraQuaternion = null;
2408
+ savedCameraZoom = null;
2409
+ constructor(camera2, domElement) {
2410
+ this.camera = camera2;
2411
+ this.domElement = domElement;
2412
+ }
2413
+ /**
2414
+ * Check if debug mode is currently active (orbit controls enabled).
2415
+ */
2416
+ get isActive() {
2417
+ return this.debugStateSnapshot.enabled;
2418
+ }
2419
+ /**
2420
+ * Update orbit controls each frame.
2421
+ * Should be called from the camera's update loop.
2422
+ */
2423
+ update() {
2424
+ if (this.orbitControls && this.orbitTarget) {
2425
+ this.orbitTarget.getWorldPosition(this.orbitTargetWorldPos);
2426
+ this.orbitControls.target.copy(this.orbitTargetWorldPos);
2427
+ }
2428
+ this.orbitControls?.update();
2429
+ }
2430
+ /**
2431
+ * Attach a delegate to react to debug state changes.
2432
+ */
2433
+ setDebugDelegate(delegate) {
2434
+ if (this.debugDelegate === delegate) {
2435
+ return;
2436
+ }
2437
+ this.detachDebugDelegate();
2438
+ this.debugDelegate = delegate;
2439
+ if (!delegate) {
2440
+ this.applyDebugState({ enabled: false, selected: [] });
2441
+ return;
2442
+ }
2443
+ const unsubscribe = delegate.subscribe((state2) => {
2444
+ this.applyDebugState(state2);
2445
+ });
2446
+ this.debugUnsubscribe = () => {
2447
+ unsubscribe?.();
2448
+ };
2449
+ }
2450
+ /**
2451
+ * Clean up resources.
2452
+ */
2453
+ dispose() {
2454
+ this.disableOrbitControls();
2455
+ this.detachDebugDelegate();
2456
+ }
2457
+ /**
2458
+ * Get the current debug state snapshot.
2459
+ */
2460
+ get debugState() {
2461
+ return this.debugStateSnapshot;
2462
+ }
2463
+ applyDebugState(state2) {
2464
+ const wasEnabled = this.debugStateSnapshot.enabled;
2465
+ this.debugStateSnapshot = {
2466
+ enabled: state2.enabled,
2467
+ selected: [...state2.selected]
2468
+ };
2469
+ if (state2.enabled && !wasEnabled) {
2470
+ this.saveCameraState();
2471
+ this.enableOrbitControls();
2472
+ this.updateOrbitTargetFromSelection(state2.selected);
2473
+ } else if (!state2.enabled && wasEnabled) {
2474
+ this.orbitTarget = null;
2475
+ this.disableOrbitControls();
2476
+ this.restoreCameraState();
2477
+ } else if (state2.enabled) {
2478
+ this.updateOrbitTargetFromSelection(state2.selected);
2479
+ }
2480
+ }
2481
+ enableOrbitControls() {
2482
+ if (this.orbitControls) {
2483
+ return;
2484
+ }
2485
+ this.orbitControls = new OrbitControls(this.camera, this.domElement);
2486
+ this.orbitControls.enableDamping = true;
2487
+ this.orbitControls.dampingFactor = 0.05;
2488
+ this.orbitControls.screenSpacePanning = false;
2489
+ this.orbitControls.minDistance = 1;
2490
+ this.orbitControls.maxDistance = 500;
2491
+ this.orbitControls.maxPolarAngle = Math.PI / 2;
2492
+ this.orbitControls.target.set(0, 0, 0);
2493
+ }
2494
+ disableOrbitControls() {
2495
+ if (!this.orbitControls) {
2496
+ return;
2497
+ }
2498
+ this.orbitControls.dispose();
2499
+ this.orbitControls = null;
2500
+ }
2501
+ updateOrbitTargetFromSelection(selected) {
2502
+ if (!this.debugDelegate || selected.length === 0) {
2503
+ this.orbitTarget = null;
2504
+ if (this.orbitControls) {
2505
+ this.orbitControls.target.set(0, 0, 0);
2506
+ }
2507
+ return;
2508
+ }
2509
+ for (let i = selected.length - 1; i >= 0; i -= 1) {
2510
+ const uuid = selected[i];
2511
+ const targetObject = this.debugDelegate.resolveTarget(uuid);
2512
+ if (targetObject) {
2513
+ this.orbitTarget = targetObject;
2514
+ if (this.orbitControls) {
2515
+ targetObject.getWorldPosition(this.orbitTargetWorldPos);
2516
+ this.orbitControls.target.copy(this.orbitTargetWorldPos);
2517
+ }
2518
+ return;
2519
+ }
2520
+ }
2521
+ this.orbitTarget = null;
1905
2522
  }
1906
- material() {
1907
- return new THREE.ShaderMaterial({
1908
- uniforms: {
1909
- iTime: { value: 0 },
1910
- tDiffuse: { value: null },
1911
- tDepth: { value: null },
1912
- tNormal: { value: null },
1913
- resolution: {
1914
- value: new THREE.Vector4(
1915
- this.resolution.x,
1916
- this.resolution.y,
1917
- 1 / this.resolution.x,
1918
- 1 / this.resolution.y
1919
- )
1920
- }
1921
- },
1922
- vertexShader: standard_default2,
1923
- fragmentShader: standard_default
1924
- });
2523
+ detachDebugDelegate() {
2524
+ if (this.debugUnsubscribe) {
2525
+ try {
2526
+ this.debugUnsubscribe();
2527
+ } catch {
2528
+ }
2529
+ }
2530
+ this.debugUnsubscribe = null;
2531
+ this.debugDelegate = null;
1925
2532
  }
1926
- dispose() {
1927
- try {
1928
- this.fsQuad?.dispose?.();
1929
- } catch {
2533
+ /**
2534
+ * Save camera position, rotation, and zoom before entering debug mode.
2535
+ */
2536
+ saveCameraState() {
2537
+ this.savedCameraPosition = this.camera.position.clone();
2538
+ this.savedCameraQuaternion = this.camera.quaternion.clone();
2539
+ if ("zoom" in this.camera) {
2540
+ this.savedCameraZoom = this.camera.zoom;
1930
2541
  }
1931
- try {
1932
- this.rgbRenderTarget?.dispose?.();
1933
- this.normalRenderTarget?.dispose?.();
1934
- } catch {
2542
+ }
2543
+ /**
2544
+ * Restore camera position, rotation, and zoom when exiting debug mode.
2545
+ */
2546
+ restoreCameraState() {
2547
+ if (this.savedCameraPosition) {
2548
+ this.camera.position.copy(this.savedCameraPosition);
2549
+ this.savedCameraPosition = null;
1935
2550
  }
1936
- try {
1937
- this.normalMaterial?.dispose?.();
1938
- } catch {
2551
+ if (this.savedCameraQuaternion) {
2552
+ this.camera.quaternion.copy(this.savedCameraQuaternion);
2553
+ this.savedCameraQuaternion = null;
2554
+ }
2555
+ if (this.savedCameraZoom !== null && "zoom" in this.camera) {
2556
+ this.camera.zoom = this.savedCameraZoom;
2557
+ this.camera.updateProjectionMatrix?.();
2558
+ this.savedCameraZoom = null;
1939
2559
  }
1940
2560
  }
1941
2561
  };
@@ -1943,8 +2563,7 @@ var init_render_pass = __esm({
1943
2563
  });
1944
2564
 
1945
2565
  // src/lib/camera/zylem-camera.ts
1946
- import { PerspectiveCamera, Vector3 as Vector38, Object3D as Object3D4, OrthographicCamera, WebGLRenderer as WebGLRenderer3 } from "three";
1947
- import { OrbitControls } from "three/addons/controls/OrbitControls.js";
2566
+ import { PerspectiveCamera, Vector3 as Vector311, Object3D as Object3D6, OrthographicCamera, WebGLRenderer as WebGLRenderer3 } from "three";
1948
2567
  import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
1949
2568
  var ZylemCamera;
1950
2569
  var init_zylem_camera = __esm({
@@ -1954,24 +2573,21 @@ var init_zylem_camera = __esm({
1954
2573
  init_third_person();
1955
2574
  init_fixed_2d();
1956
2575
  init_render_pass();
2576
+ init_camera_debug_delegate();
1957
2577
  ZylemCamera = class {
1958
- cameraRig;
2578
+ cameraRig = null;
1959
2579
  camera;
1960
2580
  screenResolution;
1961
2581
  renderer;
1962
2582
  composer;
1963
2583
  _perspective;
1964
- orbitControls = null;
1965
2584
  target = null;
1966
2585
  sceneRef = null;
1967
2586
  frustumSize = 10;
1968
2587
  // Perspective controller delegation
1969
2588
  perspectiveController = null;
1970
- debugDelegate = null;
1971
- debugUnsubscribe = null;
1972
- debugStateSnapshot = { enabled: false, selected: [] };
1973
- orbitTarget = null;
1974
- orbitTargetWorldPos = new Vector38();
2589
+ // Debug/orbit controls delegation
2590
+ orbitController = null;
1975
2591
  constructor(perspective, screenResolution, frustumSize = 10) {
1976
2592
  this._perspective = perspective;
1977
2593
  this.screenResolution = screenResolution;
@@ -1982,26 +2598,23 @@ var init_zylem_camera = __esm({
1982
2598
  this.composer = new EffectComposer(this.renderer);
1983
2599
  const aspectRatio = screenResolution.x / screenResolution.y;
1984
2600
  this.camera = this.createCameraForPerspective(aspectRatio);
1985
- this.cameraRig = new Object3D4();
1986
- this.cameraRig.position.set(0, 3, 10);
1987
- this.cameraRig.add(this.camera);
1988
- this.camera.lookAt(new Vector38(0, 2, 0));
2601
+ if (this.needsRig()) {
2602
+ this.cameraRig = new Object3D6();
2603
+ this.cameraRig.position.set(0, 3, 10);
2604
+ this.cameraRig.add(this.camera);
2605
+ this.camera.lookAt(new Vector311(0, 2, 0));
2606
+ } else {
2607
+ this.camera.position.set(0, 0, 10);
2608
+ this.camera.lookAt(new Vector311(0, 0, 0));
2609
+ }
1989
2610
  this.initializePerspectiveController();
2611
+ this.orbitController = new CameraOrbitController(this.camera, this.renderer.domElement);
1990
2612
  }
1991
2613
  /**
1992
2614
  * Setup the camera with a scene
1993
2615
  */
1994
2616
  async setup(scene) {
1995
2617
  this.sceneRef = scene;
1996
- if (this.orbitControls === null) {
1997
- this.orbitControls = new OrbitControls(this.camera, this.renderer.domElement);
1998
- this.orbitControls.enableDamping = true;
1999
- this.orbitControls.dampingFactor = 0.05;
2000
- this.orbitControls.screenSpacePanning = false;
2001
- this.orbitControls.minDistance = 1;
2002
- this.orbitControls.maxDistance = 500;
2003
- this.orbitControls.maxPolarAngle = Math.PI / 2;
2004
- }
2005
2618
  let renderResolution = this.screenResolution.clone().divideScalar(2);
2006
2619
  renderResolution.x |= 0;
2007
2620
  renderResolution.y |= 0;
@@ -2023,16 +2636,18 @@ var init_zylem_camera = __esm({
2023
2636
  * Update camera and render
2024
2637
  */
2025
2638
  update(delta) {
2026
- if (this.orbitControls && this.orbitTarget) {
2027
- this.orbitTarget.getWorldPosition(this.orbitTargetWorldPos);
2028
- this.orbitControls.target.copy(this.orbitTargetWorldPos);
2029
- }
2030
- this.orbitControls?.update();
2031
- if (this.perspectiveController) {
2639
+ this.orbitController?.update();
2640
+ if (this.perspectiveController && !this.isDebugModeActive()) {
2032
2641
  this.perspectiveController.update(delta);
2033
2642
  }
2034
2643
  this.composer.render(delta);
2035
2644
  }
2645
+ /**
2646
+ * Check if debug mode is active (orbit controls taking over camera)
2647
+ */
2648
+ isDebugModeActive() {
2649
+ return this.orbitController?.isActive ?? false;
2650
+ }
2036
2651
  /**
2037
2652
  * Dispose renderer, composer, controls, and detach from scene
2038
2653
  */
@@ -2042,7 +2657,7 @@ var init_zylem_camera = __esm({
2042
2657
  } catch {
2043
2658
  }
2044
2659
  try {
2045
- this.disableOrbitControls();
2660
+ this.orbitController?.dispose();
2046
2661
  } catch {
2047
2662
  }
2048
2663
  try {
@@ -2054,28 +2669,13 @@ var init_zylem_camera = __esm({
2054
2669
  this.renderer.dispose();
2055
2670
  } catch {
2056
2671
  }
2057
- this.detachDebugDelegate();
2058
2672
  this.sceneRef = null;
2059
2673
  }
2060
2674
  /**
2061
2675
  * Attach a delegate to react to debug state changes.
2062
2676
  */
2063
2677
  setDebugDelegate(delegate) {
2064
- if (this.debugDelegate === delegate) {
2065
- return;
2066
- }
2067
- this.detachDebugDelegate();
2068
- this.debugDelegate = delegate;
2069
- if (!delegate) {
2070
- this.applyDebugState({ enabled: false, selected: [] });
2071
- return;
2072
- }
2073
- const unsubscribe = delegate.subscribe((state2) => {
2074
- this.applyDebugState(state2);
2075
- });
2076
- this.debugUnsubscribe = () => {
2077
- unsubscribe?.();
2078
- };
2678
+ this.orbitController?.setDebugDelegate(delegate);
2079
2679
  }
2080
2680
  /**
2081
2681
  * Resize camera and renderer
@@ -2167,15 +2767,31 @@ var init_zylem_camera = __esm({
2167
2767
  if (this._perspective === Perspectives.Flat2D || this._perspective === Perspectives.Fixed2D) {
2168
2768
  this.frustumSize = position2.z;
2169
2769
  }
2170
- this.cameraRig.position.set(position2.x, position2.y, position2.z);
2770
+ if (this.cameraRig) {
2771
+ this.cameraRig.position.set(position2.x, position2.y, position2.z);
2772
+ } else {
2773
+ this.camera.position.set(position2.x, position2.y, position2.z);
2774
+ }
2171
2775
  }
2172
2776
  move(position2) {
2173
2777
  this.moveCamera(position2);
2174
2778
  }
2175
2779
  rotate(pitch, yaw, roll) {
2176
- this.cameraRig.rotateX(pitch);
2177
- this.cameraRig.rotateY(yaw);
2178
- this.cameraRig.rotateZ(roll);
2780
+ if (this.cameraRig) {
2781
+ this.cameraRig.rotateX(pitch);
2782
+ this.cameraRig.rotateY(yaw);
2783
+ this.cameraRig.rotateZ(roll);
2784
+ } else {
2785
+ this.camera.rotateX(pitch);
2786
+ this.camera.rotateY(yaw);
2787
+ this.camera.rotateZ(roll);
2788
+ }
2789
+ }
2790
+ /**
2791
+ * Check if this perspective type needs a camera rig
2792
+ */
2793
+ needsRig() {
2794
+ return this._perspective === Perspectives.ThirdPerson;
2179
2795
  }
2180
2796
  /**
2181
2797
  * Get the DOM element for the renderer
@@ -2183,348 +2799,221 @@ var init_zylem_camera = __esm({
2183
2799
  getDomElement() {
2184
2800
  return this.renderer.domElement;
2185
2801
  }
2186
- applyDebugState(state2) {
2187
- this.debugStateSnapshot = {
2188
- enabled: state2.enabled,
2189
- selected: [...state2.selected]
2190
- };
2191
- if (state2.enabled) {
2192
- this.enableOrbitControls();
2193
- this.updateOrbitTargetFromSelection(state2.selected);
2194
- } else {
2195
- this.orbitTarget = null;
2196
- this.disableOrbitControls();
2197
- }
2198
- }
2199
- enableOrbitControls() {
2200
- if (this.orbitControls) {
2201
- return;
2202
- }
2203
- this.orbitControls = new OrbitControls(this.camera, this.renderer.domElement);
2204
- this.orbitControls.enableDamping = true;
2205
- this.orbitControls.dampingFactor = 0.05;
2206
- this.orbitControls.screenSpacePanning = false;
2207
- this.orbitControls.minDistance = 1;
2208
- this.orbitControls.maxDistance = 500;
2209
- this.orbitControls.maxPolarAngle = Math.PI / 2;
2210
- }
2211
- disableOrbitControls() {
2212
- if (!this.orbitControls) {
2213
- return;
2214
- }
2215
- this.orbitControls.dispose();
2216
- this.orbitControls = null;
2217
- }
2218
- updateOrbitTargetFromSelection(selected) {
2219
- if (!this.debugDelegate || selected.length === 0) {
2220
- this.orbitTarget = null;
2221
- return;
2222
- }
2223
- for (let i = selected.length - 1; i >= 0; i -= 1) {
2224
- const uuid = selected[i];
2225
- const targetObject = this.debugDelegate.resolveTarget(uuid);
2226
- if (targetObject) {
2227
- this.orbitTarget = targetObject;
2228
- if (this.orbitControls) {
2229
- targetObject.getWorldPosition(this.orbitTargetWorldPos);
2230
- this.orbitControls.target.copy(this.orbitTargetWorldPos);
2231
- }
2232
- return;
2233
- }
2234
- }
2235
- this.orbitTarget = null;
2236
- }
2237
- detachDebugDelegate() {
2238
- if (this.debugUnsubscribe) {
2239
- try {
2240
- this.debugUnsubscribe();
2241
- } catch {
2242
- }
2243
- }
2244
- this.debugUnsubscribe = null;
2245
- this.debugDelegate = null;
2246
- }
2247
2802
  };
2248
2803
  }
2249
2804
  });
2250
2805
 
2251
- // src/lib/camera/camera.ts
2252
- import { Vector2 as Vector24, Vector3 as Vector39 } from "three";
2253
- function camera(options) {
2254
- const screenResolution = options.screenResolution || new Vector24(window.innerWidth, window.innerHeight);
2255
- let frustumSize = 10;
2256
- if (options.perspective === "fixed-2d") {
2257
- frustumSize = options.zoom || 10;
2258
- }
2259
- const zylemCamera = new ZylemCamera(options.perspective || "third-person", screenResolution, frustumSize);
2260
- zylemCamera.move(options.position || new Vector39(0, 0, 0));
2261
- zylemCamera.camera.lookAt(options.target || new Vector39(0, 0, 0));
2262
- return new CameraWrapper(zylemCamera);
2263
- }
2264
- var CameraWrapper;
2265
- var init_camera = __esm({
2266
- "src/lib/camera/camera.ts"() {
2806
+ // src/lib/stage/stage-camera-delegate.ts
2807
+ import { Vector2 as Vector25 } from "three";
2808
+ var StageCameraDelegate;
2809
+ var init_stage_camera_delegate = __esm({
2810
+ "src/lib/stage/stage-camera-delegate.ts"() {
2267
2811
  "use strict";
2268
2812
  init_zylem_camera();
2269
- CameraWrapper = class {
2270
- cameraRef;
2271
- constructor(camera2) {
2272
- this.cameraRef = camera2;
2273
- }
2274
- };
2275
- }
2276
- });
2277
-
2278
- // src/lib/stage/debug-entity-cursor.ts
2279
- import {
2280
- Box3,
2281
- BoxGeometry,
2282
- Color as Color7,
2283
- EdgesGeometry,
2284
- Group as Group4,
2285
- LineBasicMaterial,
2286
- LineSegments,
2287
- Mesh as Mesh4,
2288
- MeshBasicMaterial,
2289
- Vector3 as Vector310
2290
- } from "three";
2291
- var DebugEntityCursor;
2292
- var init_debug_entity_cursor = __esm({
2293
- "src/lib/stage/debug-entity-cursor.ts"() {
2294
- "use strict";
2295
- DebugEntityCursor = class {
2296
- scene;
2297
- container;
2298
- fillMesh;
2299
- edgeLines;
2300
- currentColor = new Color7(65280);
2301
- bbox = new Box3();
2302
- size = new Vector310();
2303
- center = new Vector310();
2304
- constructor(scene) {
2305
- this.scene = scene;
2306
- const initialGeometry = new BoxGeometry(1, 1, 1);
2307
- this.fillMesh = new Mesh4(
2308
- initialGeometry,
2309
- new MeshBasicMaterial({
2310
- color: this.currentColor,
2311
- transparent: true,
2312
- opacity: 0.12,
2313
- depthWrite: false
2314
- })
2315
- );
2316
- const edges = new EdgesGeometry(initialGeometry);
2317
- this.edgeLines = new LineSegments(
2318
- edges,
2319
- new LineBasicMaterial({ color: this.currentColor, linewidth: 1 })
2320
- );
2321
- this.container = new Group4();
2322
- this.container.name = "DebugEntityCursor";
2323
- this.container.add(this.fillMesh);
2324
- this.container.add(this.edgeLines);
2325
- this.container.visible = false;
2326
- this.scene.add(this.container);
2327
- }
2328
- setColor(color) {
2329
- this.currentColor.set(color);
2330
- this.fillMesh.material.color.set(this.currentColor);
2331
- this.edgeLines.material.color.set(this.currentColor);
2813
+ init_perspective();
2814
+ StageCameraDelegate = class {
2815
+ stage;
2816
+ constructor(stage) {
2817
+ this.stage = stage;
2332
2818
  }
2333
2819
  /**
2334
- * Update the cursor to enclose the provided Object3D using a world-space AABB.
2820
+ * Create a default third-person camera based on window size.
2335
2821
  */
2336
- updateFromObject(object) {
2337
- if (!object) {
2338
- this.hide();
2339
- return;
2340
- }
2341
- this.bbox.setFromObject(object);
2342
- if (!isFinite(this.bbox.min.x) || !isFinite(this.bbox.max.x)) {
2343
- this.hide();
2344
- return;
2345
- }
2346
- this.bbox.getSize(this.size);
2347
- this.bbox.getCenter(this.center);
2348
- const newGeom = new BoxGeometry(
2349
- Math.max(this.size.x, 1e-6),
2350
- Math.max(this.size.y, 1e-6),
2351
- Math.max(this.size.z, 1e-6)
2352
- );
2353
- this.fillMesh.geometry.dispose();
2354
- this.fillMesh.geometry = newGeom;
2355
- const newEdges = new EdgesGeometry(newGeom);
2356
- this.edgeLines.geometry.dispose();
2357
- this.edgeLines.geometry = newEdges;
2358
- this.container.position.copy(this.center);
2359
- this.container.visible = true;
2360
- }
2361
- hide() {
2362
- this.container.visible = false;
2822
+ createDefaultCamera() {
2823
+ const width = window.innerWidth;
2824
+ const height = window.innerHeight;
2825
+ const screenResolution = new Vector25(width, height);
2826
+ return new ZylemCamera(Perspectives.ThirdPerson, screenResolution);
2363
2827
  }
2364
- dispose() {
2365
- this.scene.remove(this.container);
2366
- this.fillMesh.geometry.dispose();
2367
- this.fillMesh.material.dispose();
2368
- this.edgeLines.geometry.dispose();
2369
- this.edgeLines.material.dispose();
2828
+ /**
2829
+ * Resolve the camera to use for the stage.
2830
+ * Uses the provided camera, stage camera wrapper, or creates a default.
2831
+ *
2832
+ * @param cameraOverride Optional camera override
2833
+ * @param cameraWrapper Optional camera wrapper from stage options
2834
+ * @returns The resolved ZylemCamera instance
2835
+ */
2836
+ resolveCamera(cameraOverride, cameraWrapper) {
2837
+ if (cameraOverride) {
2838
+ return cameraOverride;
2839
+ }
2840
+ if (cameraWrapper) {
2841
+ return cameraWrapper.cameraRef;
2842
+ }
2843
+ return this.createDefaultCamera();
2370
2844
  }
2371
2845
  };
2372
2846
  }
2373
2847
  });
2374
2848
 
2375
- // src/lib/stage/stage-debug-delegate.ts
2376
- import { Ray } from "@dimforge/rapier3d-compat";
2377
- import { BufferAttribute, BufferGeometry as BufferGeometry4, LineBasicMaterial as LineBasicMaterial2, LineSegments as LineSegments2, Raycaster, Vector2 as Vector25 } from "three";
2378
- var SELECT_TOOL_COLOR, DELETE_TOOL_COLOR, StageDebugDelegate;
2379
- var init_stage_debug_delegate = __esm({
2380
- "src/lib/stage/stage-debug-delegate.ts"() {
2849
+ // src/lib/stage/stage-loading-delegate.ts
2850
+ var StageLoadingDelegate;
2851
+ var init_stage_loading_delegate = __esm({
2852
+ "src/lib/stage/stage-loading-delegate.ts"() {
2381
2853
  "use strict";
2382
- init_debug_state();
2383
- init_debug_entity_cursor();
2384
- SELECT_TOOL_COLOR = 2293538;
2385
- DELETE_TOOL_COLOR = 16724787;
2386
- StageDebugDelegate = class {
2387
- stage;
2388
- options;
2389
- mouseNdc = new Vector25(-2, -2);
2390
- raycaster = new Raycaster();
2391
- isMouseDown = false;
2392
- disposeFns = [];
2393
- debugCursor = null;
2394
- debugLines = null;
2395
- constructor(stage, options) {
2396
- this.stage = stage;
2397
- this.options = {
2398
- maxRayDistance: options?.maxRayDistance ?? 5e3,
2399
- addEntityFactory: options?.addEntityFactory ?? null
2854
+ init_game_event_bus();
2855
+ StageLoadingDelegate = class {
2856
+ loadingHandlers = [];
2857
+ stageName;
2858
+ stageIndex;
2859
+ /**
2860
+ * Set stage context for event bus emissions.
2861
+ */
2862
+ setStageContext(stageName, stageIndex) {
2863
+ this.stageName = stageName;
2864
+ this.stageIndex = stageIndex;
2865
+ }
2866
+ /**
2867
+ * Subscribe to loading events.
2868
+ *
2869
+ * @param callback Invoked for each loading event (start, progress, complete)
2870
+ * @returns Unsubscribe function
2871
+ */
2872
+ onLoading(callback) {
2873
+ this.loadingHandlers.push(callback);
2874
+ return () => {
2875
+ this.loadingHandlers = this.loadingHandlers.filter((h) => h !== callback);
2400
2876
  };
2401
- if (this.stage.scene) {
2402
- this.debugLines = new LineSegments2(
2403
- new BufferGeometry4(),
2404
- new LineBasicMaterial2({ vertexColors: true })
2405
- );
2406
- this.stage.scene.scene.add(this.debugLines);
2407
- this.debugLines.visible = true;
2408
- this.debugCursor = new DebugEntityCursor(this.stage.scene.scene);
2409
- }
2410
- this.attachDomListeners();
2411
2877
  }
2412
- update() {
2413
- if (!debugState.enabled) return;
2414
- if (!this.stage.scene || !this.stage.world || !this.stage.cameraRef) return;
2415
- const { world, cameraRef } = this.stage;
2416
- if (this.debugLines) {
2417
- const { vertices, colors } = world.world.debugRender();
2418
- this.debugLines.geometry.setAttribute("position", new BufferAttribute(vertices, 3));
2419
- this.debugLines.geometry.setAttribute("color", new BufferAttribute(colors, 4));
2420
- }
2421
- const tool = getDebugTool();
2422
- const isCursorTool = tool === "select" || tool === "delete";
2423
- this.raycaster.setFromCamera(this.mouseNdc, cameraRef.camera);
2424
- const origin = this.raycaster.ray.origin.clone();
2425
- const direction = this.raycaster.ray.direction.clone().normalize();
2426
- const rapierRay = new Ray(
2427
- { x: origin.x, y: origin.y, z: origin.z },
2428
- { x: direction.x, y: direction.y, z: direction.z }
2429
- );
2430
- const hit = world.world.castRay(rapierRay, this.options.maxRayDistance, true);
2431
- if (hit && isCursorTool) {
2432
- const rigidBody = hit.collider?._parent;
2433
- const hoveredUuid2 = rigidBody?.userData?.uuid;
2434
- if (hoveredUuid2) {
2435
- const entity = this.stage._debugMap.get(hoveredUuid2);
2436
- if (entity) setHoveredEntity(entity);
2437
- } else {
2438
- resetHoveredEntity();
2439
- }
2440
- if (this.isMouseDown) {
2441
- this.handleActionOnHit(hoveredUuid2 ?? null, origin, direction, hit.toi);
2878
+ /**
2879
+ * Emit a loading event to all subscribers and to the game event bus.
2880
+ *
2881
+ * @param event The loading event to broadcast
2882
+ */
2883
+ emit(event) {
2884
+ for (const handler of this.loadingHandlers) {
2885
+ try {
2886
+ handler(event);
2887
+ } catch (e) {
2888
+ console.error("Loading handler failed", e);
2442
2889
  }
2443
2890
  }
2444
- this.isMouseDown = false;
2445
- const hoveredUuid = getHoveredEntity();
2446
- if (!hoveredUuid) {
2447
- this.debugCursor?.hide();
2448
- return;
2449
- }
2450
- const hoveredEntity = this.stage._debugMap.get(`${hoveredUuid}`);
2451
- const targetObject = hoveredEntity?.group ?? hoveredEntity?.mesh ?? null;
2452
- if (!targetObject) {
2453
- this.debugCursor?.hide();
2454
- return;
2455
- }
2456
- switch (tool) {
2457
- case "select":
2458
- this.debugCursor?.setColor(SELECT_TOOL_COLOR);
2459
- break;
2460
- case "delete":
2461
- this.debugCursor?.setColor(DELETE_TOOL_COLOR);
2462
- break;
2463
- default:
2464
- this.debugCursor?.setColor(16777215);
2465
- break;
2891
+ const payload = {
2892
+ ...event,
2893
+ stageName: this.stageName,
2894
+ stageIndex: this.stageIndex
2895
+ };
2896
+ if (event.type === "start") {
2897
+ gameEventBus.emit("stage:loading:start", payload);
2898
+ } else if (event.type === "progress") {
2899
+ gameEventBus.emit("stage:loading:progress", payload);
2900
+ } else if (event.type === "complete") {
2901
+ gameEventBus.emit("stage:loading:complete", payload);
2466
2902
  }
2467
- this.debugCursor?.updateFromObject(targetObject);
2468
2903
  }
2469
- dispose() {
2470
- this.disposeFns.forEach((fn) => fn());
2471
- this.disposeFns = [];
2472
- this.debugCursor?.dispose();
2473
- if (this.debugLines && this.stage.scene) {
2474
- this.stage.scene.scene.remove(this.debugLines);
2475
- this.debugLines.geometry.dispose();
2476
- this.debugLines.material.dispose();
2477
- this.debugLines = null;
2478
- }
2904
+ /**
2905
+ * Emit a start loading event.
2906
+ */
2907
+ emitStart(message = "Loading stage...") {
2908
+ this.emit({ type: "start", message, progress: 0 });
2479
2909
  }
2480
- handleActionOnHit(hoveredUuid, origin, direction, toi) {
2481
- const tool = getDebugTool();
2482
- switch (tool) {
2483
- case "select": {
2484
- if (hoveredUuid) {
2485
- const entity = this.stage._debugMap.get(hoveredUuid);
2486
- if (entity) setSelectedEntity(entity);
2487
- }
2488
- break;
2489
- }
2490
- case "delete": {
2491
- if (hoveredUuid) {
2492
- this.stage.removeEntityByUuid(hoveredUuid);
2493
- }
2494
- break;
2495
- }
2496
- case "scale": {
2497
- if (!this.options.addEntityFactory) break;
2498
- const hitPosition = origin.clone().add(direction.clone().multiplyScalar(toi));
2499
- const newNode = this.options.addEntityFactory({ position: hitPosition });
2500
- if (newNode) {
2501
- Promise.resolve(newNode).then((node) => {
2502
- if (node) this.stage.spawnEntity(node);
2503
- }).catch(() => {
2504
- });
2505
- }
2506
- break;
2507
- }
2508
- default:
2509
- break;
2510
- }
2910
+ /**
2911
+ * Emit a progress loading event.
2912
+ */
2913
+ emitProgress(message, current, total) {
2914
+ const progress = total > 0 ? current / total : 0;
2915
+ this.emit({ type: "progress", message, progress, current, total });
2511
2916
  }
2512
- attachDomListeners() {
2513
- const canvas = this.stage.cameraRef?.renderer.domElement ?? this.stage.scene?.zylemCamera.renderer.domElement;
2514
- if (!canvas) return;
2515
- const onMouseMove = (e) => {
2516
- const rect2 = canvas.getBoundingClientRect();
2517
- const x = (e.clientX - rect2.left) / rect2.width * 2 - 1;
2518
- const y = -((e.clientY - rect2.top) / rect2.height * 2 - 1);
2519
- this.mouseNdc.set(x, y);
2520
- };
2521
- const onMouseDown = (e) => {
2522
- this.isMouseDown = true;
2523
- };
2524
- canvas.addEventListener("mousemove", onMouseMove);
2525
- canvas.addEventListener("mousedown", onMouseDown);
2526
- this.disposeFns.push(() => canvas.removeEventListener("mousemove", onMouseMove));
2527
- this.disposeFns.push(() => canvas.removeEventListener("mousedown", onMouseDown));
2917
+ /**
2918
+ * Emit a complete loading event.
2919
+ */
2920
+ emitComplete(message = "Stage loaded") {
2921
+ this.emit({ type: "complete", message, progress: 1 });
2922
+ }
2923
+ /**
2924
+ * Clear all loading handlers.
2925
+ */
2926
+ dispose() {
2927
+ this.loadingHandlers = [];
2928
+ }
2929
+ };
2930
+ }
2931
+ });
2932
+
2933
+ // src/lib/core/utility/options-parser.ts
2934
+ function isBaseNode(item) {
2935
+ return !!item && typeof item === "object" && typeof item.create === "function";
2936
+ }
2937
+ function isThenable(item) {
2938
+ return !!item && typeof item.then === "function";
2939
+ }
2940
+ function isCameraWrapper(item) {
2941
+ return !!item && typeof item === "object" && item.constructor?.name === "CameraWrapper";
2942
+ }
2943
+ function isConfigObject(item) {
2944
+ if (!item || typeof item !== "object") return false;
2945
+ if (isBaseNode(item)) return false;
2946
+ if (isCameraWrapper(item)) return false;
2947
+ if (isThenable(item)) return false;
2948
+ if (typeof item.then === "function") return false;
2949
+ return item.constructor === Object || item.constructor?.name === "Object";
2950
+ }
2951
+ function isEntityInput(item) {
2952
+ if (!item) return false;
2953
+ if (isBaseNode(item)) return true;
2954
+ if (typeof item === "function") return true;
2955
+ if (isThenable(item)) return true;
2956
+ return false;
2957
+ }
2958
+ var init_options_parser = __esm({
2959
+ "src/lib/core/utility/options-parser.ts"() {
2960
+ "use strict";
2961
+ }
2962
+ });
2963
+
2964
+ // src/lib/stage/stage-config.ts
2965
+ import { Vector3 as Vector312 } from "three";
2966
+ function createDefaultStageConfig() {
2967
+ return new StageConfig(
2968
+ {
2969
+ p1: ["gamepad-1", "keyboard-1"],
2970
+ p2: ["gamepad-2", "keyboard-2"]
2971
+ },
2972
+ ZylemBlueColor,
2973
+ null,
2974
+ new Vector312(0, 0, 0),
2975
+ {}
2976
+ );
2977
+ }
2978
+ function parseStageOptions(options = []) {
2979
+ const defaults = createDefaultStageConfig();
2980
+ let config = {};
2981
+ const entities = [];
2982
+ const asyncEntities = [];
2983
+ let camera2;
2984
+ for (const item of options) {
2985
+ if (isCameraWrapper(item)) {
2986
+ camera2 = item;
2987
+ } else if (isBaseNode(item)) {
2988
+ entities.push(item);
2989
+ } else if (isEntityInput(item) && !isBaseNode(item)) {
2990
+ asyncEntities.push(item);
2991
+ } else if (isConfigObject(item)) {
2992
+ config = { ...config, ...item };
2993
+ }
2994
+ }
2995
+ const resolvedConfig = new StageConfig(
2996
+ config.inputs ?? defaults.inputs,
2997
+ config.backgroundColor ?? defaults.backgroundColor,
2998
+ config.backgroundImage ?? defaults.backgroundImage,
2999
+ config.gravity ?? defaults.gravity,
3000
+ config.variables ?? defaults.variables
3001
+ );
3002
+ return { config: resolvedConfig, entities, asyncEntities, camera: camera2 };
3003
+ }
3004
+ var StageConfig;
3005
+ var init_stage_config = __esm({
3006
+ "src/lib/stage/stage-config.ts"() {
3007
+ "use strict";
3008
+ init_options_parser();
3009
+ init_vector();
3010
+ StageConfig = class {
3011
+ constructor(inputs, backgroundColor, backgroundImage, gravity, variables) {
3012
+ this.inputs = inputs;
3013
+ this.backgroundColor = backgroundColor;
3014
+ this.backgroundImage = backgroundImage;
3015
+ this.gravity = gravity;
3016
+ this.variables = variables;
2528
3017
  }
2529
3018
  };
2530
3019
  }
@@ -2532,7 +3021,8 @@ var init_stage_debug_delegate = __esm({
2532
3021
 
2533
3022
  // src/lib/stage/zylem-stage.ts
2534
3023
  import { addComponent, addEntity, createWorld as createECS, removeEntity } from "bitecs";
2535
- import { Color as Color8, Vector3 as Vector312, Vector2 as Vector26 } from "three";
3024
+ import { Color as Color9, Vector3 as Vector313 } from "three";
3025
+ import { subscribe as subscribe5 } from "valtio/vanilla";
2536
3026
  import { nanoid as nanoid2 } from "nanoid";
2537
3027
  var STAGE_TYPE, ZylemStage;
2538
3028
  var init_zylem_stage = __esm({
@@ -2546,12 +3036,13 @@ var init_zylem_stage = __esm({
2546
3036
  init_game_state();
2547
3037
  init_lifecycle_base();
2548
3038
  init_transformable_system();
2549
- init_base_node();
2550
- init_perspective();
2551
- init_camera();
2552
3039
  init_stage_debug_delegate();
3040
+ init_stage_camera_debug_delegate();
3041
+ init_stage_camera_delegate();
3042
+ init_stage_loading_delegate();
2553
3043
  init_entity();
2554
- init_zylem_camera();
3044
+ init_stage_config();
3045
+ init_options_parser();
2555
3046
  STAGE_TYPE = "Stage";
2556
3047
  ZylemStage = class extends LifeCycleBase {
2557
3048
  type = STAGE_TYPE;
@@ -2562,7 +3053,7 @@ var init_zylem_stage = __esm({
2562
3053
  p1: ["gamepad-1", "keyboard"],
2563
3054
  p2: ["gamepad-2", "keyboard"]
2564
3055
  },
2565
- gravity: new Vector312(0, 0, 0),
3056
+ gravity: new Vector313(0, 0, 0),
2566
3057
  variables: {},
2567
3058
  entities: []
2568
3059
  };
@@ -2577,16 +3068,19 @@ var init_zylem_stage = __esm({
2577
3068
  isLoaded = false;
2578
3069
  _debugMap = /* @__PURE__ */ new Map();
2579
3070
  entityAddedHandlers = [];
2580
- loadingHandlers = [];
2581
3071
  ecs = createECS();
2582
3072
  testSystem = null;
2583
3073
  transformSystem = null;
2584
3074
  debugDelegate = null;
2585
3075
  cameraDebugDelegate = null;
3076
+ debugStateUnsubscribe = null;
2586
3077
  uuid;
2587
3078
  wrapperRef = null;
2588
3079
  camera;
2589
3080
  cameraRef = null;
3081
+ // Delegates
3082
+ cameraDelegate;
3083
+ loadingDelegate;
2590
3084
  /**
2591
3085
  * Create a new stage.
2592
3086
  * @param options Stage options: partial config, camera, and initial entities or factories
@@ -2596,49 +3090,22 @@ var init_zylem_stage = __esm({
2596
3090
  this.world = null;
2597
3091
  this.scene = null;
2598
3092
  this.uuid = nanoid2();
2599
- const { config, entities, asyncEntities, camera: camera2 } = this.parseOptions(options);
2600
- this.camera = camera2;
2601
- this.children = entities;
2602
- this.pendingEntities = asyncEntities;
2603
- this.saveState({ ...this.state, ...config, entities: [] });
2604
- this.gravity = config.gravity ?? new Vector312(0, 0, 0);
2605
- }
2606
- parseOptions(options) {
2607
- let config = {};
2608
- const entities = [];
2609
- const asyncEntities = [];
2610
- let camera2;
2611
- for (const item of options) {
2612
- if (this.isCameraWrapper(item)) {
2613
- camera2 = item;
2614
- } else if (this.isBaseNode(item)) {
2615
- entities.push(item);
2616
- } else if (this.isEntityInput(item)) {
2617
- asyncEntities.push(item);
2618
- } else if (this.isZylemStageConfig(item)) {
2619
- config = { ...config, ...item };
2620
- }
2621
- }
2622
- return { config, entities, asyncEntities, camera: camera2 };
2623
- }
2624
- isZylemStageConfig(item) {
2625
- return item && typeof item === "object" && !(item instanceof BaseNode) && !(item instanceof CameraWrapper);
2626
- }
2627
- isBaseNode(item) {
2628
- return item && typeof item === "object" && typeof item.create === "function";
2629
- }
2630
- isCameraWrapper(item) {
2631
- return item && typeof item === "object" && item.constructor.name === "CameraWrapper";
2632
- }
2633
- isEntityInput(item) {
2634
- if (!item) return false;
2635
- if (this.isBaseNode(item)) return true;
2636
- if (typeof item === "function") return true;
2637
- if (typeof item === "object" && typeof item.then === "function") return true;
2638
- return false;
2639
- }
2640
- isThenable(value) {
2641
- return !!value && typeof value.then === "function";
3093
+ this.cameraDelegate = new StageCameraDelegate(this);
3094
+ this.loadingDelegate = new StageLoadingDelegate();
3095
+ const parsed = parseStageOptions(options);
3096
+ this.camera = parsed.camera;
3097
+ this.children = parsed.entities;
3098
+ this.pendingEntities = parsed.asyncEntities;
3099
+ this.saveState({
3100
+ ...this.state,
3101
+ inputs: parsed.config.inputs,
3102
+ backgroundColor: parsed.config.backgroundColor,
3103
+ backgroundImage: parsed.config.backgroundImage,
3104
+ gravity: parsed.config.gravity,
3105
+ variables: parsed.config.variables,
3106
+ entities: []
3107
+ });
3108
+ this.gravity = parsed.config.gravity ?? new Vector313(0, 0, 0);
2642
3109
  }
2643
3110
  handleEntityImmediatelyOrQueue(entity) {
2644
3111
  if (this.isLoaded) {
@@ -2659,42 +3126,47 @@ var init_zylem_stage = __esm({
2659
3126
  }
2660
3127
  setState() {
2661
3128
  const { backgroundColor, backgroundImage } = this.state;
2662
- const color = backgroundColor instanceof Color8 ? backgroundColor : new Color8(backgroundColor);
3129
+ const color = backgroundColor instanceof Color9 ? backgroundColor : new Color9(backgroundColor);
2663
3130
  setStageBackgroundColor(color);
2664
3131
  setStageBackgroundImage(backgroundImage);
2665
3132
  setStageVariables(this.state.variables ?? {});
2666
3133
  }
2667
3134
  /**
2668
3135
  * Load and initialize the stage's scene and world.
3136
+ * Uses generator pattern to yield control to event loop for real-time progress.
2669
3137
  * @param id DOM element id for the renderer container
2670
3138
  * @param camera Optional camera override
2671
3139
  */
2672
3140
  async load(id, camera2) {
2673
3141
  this.setState();
2674
- const zylemCamera = camera2 || (this.camera ? this.camera.cameraRef : this.createDefaultCamera());
3142
+ const zylemCamera = this.cameraDelegate.resolveCamera(camera2, this.camera);
2675
3143
  this.cameraRef = zylemCamera;
2676
3144
  this.scene = new ZylemScene(id, zylemCamera, this.state);
2677
- const physicsWorld = await ZylemWorld.loadPhysics(this.gravity ?? new Vector312(0, 0, 0));
3145
+ const physicsWorld = await ZylemWorld.loadPhysics(this.gravity ?? new Vector313(0, 0, 0));
2678
3146
  this.world = new ZylemWorld(physicsWorld);
2679
3147
  this.scene.setup();
2680
- this.emitLoading({ type: "start", message: "Loading stage...", progress: 0 });
3148
+ this.loadingDelegate.emitStart();
3149
+ await this.runEntityLoadGenerator();
3150
+ this.transformSystem = createTransformSystem(this);
3151
+ this.isLoaded = true;
3152
+ this.loadingDelegate.emitComplete();
3153
+ }
3154
+ /**
3155
+ * Generator that yields between entity loads for real-time progress updates.
3156
+ */
3157
+ *entityLoadGenerator() {
2681
3158
  const total = this.children.length + this.pendingEntities.length + this.pendingPromises.length;
2682
3159
  let current = 0;
2683
- for (let child of this.children) {
3160
+ for (const child of this.children) {
2684
3161
  this.spawnEntity(child);
2685
3162
  current++;
2686
- this.emitLoading({
2687
- type: "progress",
2688
- message: `Loaded entity ${child.name || "unknown"}`,
2689
- progress: current / total,
2690
- current,
2691
- total
2692
- });
3163
+ yield { current, total, name: child.name || "unknown" };
2693
3164
  }
2694
3165
  if (this.pendingEntities.length) {
2695
3166
  this.enqueue(...this.pendingEntities);
2696
3167
  current += this.pendingEntities.length;
2697
3168
  this.pendingEntities = [];
3169
+ yield { current, total, name: "pending entities" };
2698
3170
  }
2699
3171
  if (this.pendingPromises.length) {
2700
3172
  for (const promise of this.pendingPromises) {
@@ -2704,24 +3176,44 @@ var init_zylem_stage = __esm({
2704
3176
  }
2705
3177
  current += this.pendingPromises.length;
2706
3178
  this.pendingPromises = [];
3179
+ yield { current, total, name: "async entities" };
2707
3180
  }
2708
- this.transformSystem = createTransformSystem(this);
2709
- this.isLoaded = true;
2710
- this.emitLoading({ type: "complete", message: "Stage loaded", progress: 1 });
2711
3181
  }
2712
- createDefaultCamera() {
2713
- const width = window.innerWidth;
2714
- const height = window.innerHeight;
2715
- const screenResolution = new Vector26(width, height);
2716
- return new ZylemCamera(Perspectives.ThirdPerson, screenResolution);
3182
+ /**
3183
+ * Runs the entity load generator, yielding to the event loop between loads.
3184
+ * This allows the browser to process events and update the UI in real-time.
3185
+ */
3186
+ async runEntityLoadGenerator() {
3187
+ const gen = this.entityLoadGenerator();
3188
+ for (const progress of gen) {
3189
+ this.loadingDelegate.emitProgress(`Loaded ${progress.name}`, progress.current, progress.total);
3190
+ await new Promise((resolve) => setTimeout(resolve, 0));
3191
+ }
2717
3192
  }
2718
3193
  _setup(params) {
2719
3194
  if (!this.scene || !this.world) {
2720
3195
  this.logMissingEntities();
2721
3196
  return;
2722
3197
  }
2723
- if (debugState.enabled) {
3198
+ this.updateDebugDelegate();
3199
+ this.debugStateUnsubscribe = subscribe5(debugState, () => {
3200
+ this.updateDebugDelegate();
3201
+ });
3202
+ }
3203
+ updateDebugDelegate() {
3204
+ if (debugState.enabled && !this.debugDelegate && this.scene && this.world) {
2724
3205
  this.debugDelegate = new StageDebugDelegate(this);
3206
+ if (this.cameraRef && !this.cameraDebugDelegate) {
3207
+ this.cameraDebugDelegate = new StageCameraDebugDelegate(this);
3208
+ this.cameraRef.setDebugDelegate(this.cameraDebugDelegate);
3209
+ }
3210
+ } else if (!debugState.enabled && this.debugDelegate) {
3211
+ this.debugDelegate.dispose();
3212
+ this.debugDelegate = null;
3213
+ if (this.cameraRef) {
3214
+ this.cameraRef.setDebugDelegate(null);
3215
+ }
3216
+ this.cameraDebugDelegate = null;
2725
3217
  }
2726
3218
  }
2727
3219
  _update(params) {
@@ -2731,7 +3223,7 @@ var init_zylem_stage = __esm({
2731
3223
  return;
2732
3224
  }
2733
3225
  this.world.update(params);
2734
- this.transformSystem(this.ecs);
3226
+ this.transformSystem?.system(this.ecs);
2735
3227
  this._childrenMap.forEach((child, eid) => {
2736
3228
  child.nodeUpdate({
2737
3229
  ...params,
@@ -2765,13 +3257,20 @@ var init_zylem_stage = __esm({
2765
3257
  this._debugMap.clear();
2766
3258
  this.world?.destroy();
2767
3259
  this.scene?.destroy();
3260
+ if (this.debugStateUnsubscribe) {
3261
+ this.debugStateUnsubscribe();
3262
+ this.debugStateUnsubscribe = null;
3263
+ }
2768
3264
  this.debugDelegate?.dispose();
3265
+ this.debugDelegate = null;
2769
3266
  this.cameraRef?.setDebugDelegate(null);
2770
3267
  this.cameraDebugDelegate = null;
2771
3268
  this.isLoaded = false;
2772
3269
  this.world = null;
2773
3270
  this.scene = null;
2774
3271
  this.cameraRef = null;
3272
+ this.transformSystem?.destroy(this.ecs);
3273
+ this.transformSystem = null;
2775
3274
  resetStageVariables();
2776
3275
  clearVariables(this);
2777
3276
  }
@@ -2852,13 +3351,7 @@ var init_zylem_stage = __esm({
2852
3351
  };
2853
3352
  }
2854
3353
  onLoading(callback) {
2855
- this.loadingHandlers.push(callback);
2856
- return () => {
2857
- this.loadingHandlers = this.loadingHandlers.filter((h) => h !== callback);
2858
- };
2859
- }
2860
- emitLoading(event) {
2861
- this.loadingHandlers.forEach((h) => h(event));
3354
+ return this.loadingDelegate.onLoading(callback);
2862
3355
  }
2863
3356
  /**
2864
3357
  * Remove an entity and its resources by its UUID.
@@ -2915,16 +3408,16 @@ var init_zylem_stage = __esm({
2915
3408
  enqueue(...items) {
2916
3409
  for (const item of items) {
2917
3410
  if (!item) continue;
2918
- if (this.isBaseNode(item)) {
3411
+ if (isBaseNode(item)) {
2919
3412
  this.handleEntityImmediatelyOrQueue(item);
2920
3413
  continue;
2921
3414
  }
2922
3415
  if (typeof item === "function") {
2923
3416
  try {
2924
3417
  const result = item();
2925
- if (this.isBaseNode(result)) {
3418
+ if (isBaseNode(result)) {
2926
3419
  this.handleEntityImmediatelyOrQueue(result);
2927
- } else if (this.isThenable(result)) {
3420
+ } else if (isThenable(result)) {
2928
3421
  this.handlePromiseWithSpawnOnResolve(result);
2929
3422
  }
2930
3423
  } catch (error) {
@@ -2932,7 +3425,7 @@ var init_zylem_stage = __esm({
2932
3425
  }
2933
3426
  continue;
2934
3427
  }
2935
- if (this.isThenable(item)) {
3428
+ if (isThenable(item)) {
2936
3429
  this.handlePromiseWithSpawnOnResolve(item);
2937
3430
  }
2938
3431
  }
@@ -2941,9 +3434,36 @@ var init_zylem_stage = __esm({
2941
3434
  }
2942
3435
  });
2943
3436
 
3437
+ // src/lib/camera/camera.ts
3438
+ import { Vector2 as Vector27, Vector3 as Vector314 } from "three";
3439
+ function camera(options) {
3440
+ const screenResolution = options.screenResolution || new Vector27(window.innerWidth, window.innerHeight);
3441
+ let frustumSize = 10;
3442
+ if (options.perspective === "fixed-2d") {
3443
+ frustumSize = options.zoom || 10;
3444
+ }
3445
+ const zylemCamera = new ZylemCamera(options.perspective || "third-person", screenResolution, frustumSize);
3446
+ zylemCamera.move(options.position || new Vector314(0, 0, 0));
3447
+ zylemCamera.camera.lookAt(options.target || new Vector314(0, 0, 0));
3448
+ return new CameraWrapper(zylemCamera);
3449
+ }
3450
+ var CameraWrapper;
3451
+ var init_camera = __esm({
3452
+ "src/lib/camera/camera.ts"() {
3453
+ "use strict";
3454
+ init_zylem_camera();
3455
+ CameraWrapper = class {
3456
+ cameraRef;
3457
+ constructor(camera2) {
3458
+ this.cameraRef = camera2;
3459
+ }
3460
+ };
3461
+ }
3462
+ });
3463
+
2944
3464
  // src/lib/stage/stage-default.ts
2945
3465
  import { proxy as proxy4 } from "valtio/vanilla";
2946
- import { Vector3 as Vector313 } from "three";
3466
+ import { Vector3 as Vector315 } from "three";
2947
3467
  function getStageOptions(options) {
2948
3468
  const defaults = getStageDefaultConfig();
2949
3469
  let originalConfig = {};
@@ -2974,7 +3494,7 @@ var init_stage_default = __esm({
2974
3494
  p1: ["gamepad-1", "keyboard"],
2975
3495
  p2: ["gamepad-2", "keyboard"]
2976
3496
  },
2977
- gravity: new Vector313(0, 0, 0),
3497
+ gravity: new Vector315(0, 0, 0),
2978
3498
  variables: {}
2979
3499
  };
2980
3500
  stageDefaultsState = proxy4({
@@ -2999,30 +3519,58 @@ var init_stage = __esm({
2999
3519
  Stage = class {
3000
3520
  wrappedStage;
3001
3521
  options = [];
3002
- // TODO: these shouldn't be here maybe more like nextFrame(stageInstance, () => {})
3003
- update = () => {
3004
- };
3005
- setup = () => {
3006
- };
3007
- destroy = () => {
3008
- };
3522
+ // Entities added after construction, consumed on each load
3523
+ _pendingEntities = [];
3524
+ // Lifecycle callback arrays
3525
+ setupCallbacks = [];
3526
+ updateCallbacks = [];
3527
+ destroyCallbacks = [];
3528
+ pendingLoadingCallbacks = [];
3009
3529
  constructor(options) {
3010
3530
  this.options = options;
3011
3531
  this.wrappedStage = null;
3012
3532
  }
3013
3533
  async load(id, camera2) {
3014
3534
  stageState.entities = [];
3015
- this.wrappedStage = new ZylemStage(this.options);
3535
+ const loadOptions = [...this.options, ...this._pendingEntities];
3536
+ this._pendingEntities = [];
3537
+ this.wrappedStage = new ZylemStage(loadOptions);
3016
3538
  this.wrappedStage.wrapperRef = this;
3539
+ this.pendingLoadingCallbacks.forEach((cb) => {
3540
+ this.wrappedStage.onLoading(cb);
3541
+ });
3542
+ this.pendingLoadingCallbacks = [];
3017
3543
  const zylemCamera = camera2 instanceof CameraWrapper ? camera2.cameraRef : camera2;
3018
3544
  await this.wrappedStage.load(id, zylemCamera);
3019
3545
  this.wrappedStage.onEntityAdded((child) => {
3020
3546
  const next = this.wrappedStage.buildEntityState(child);
3021
3547
  stageState.entities = [...stageState.entities, next];
3022
3548
  }, { replayExisting: true });
3549
+ this.applyLifecycleCallbacks();
3550
+ }
3551
+ applyLifecycleCallbacks() {
3552
+ if (!this.wrappedStage) return;
3553
+ if (this.setupCallbacks.length > 0) {
3554
+ this.wrappedStage.setup = (params) => {
3555
+ const extended = { ...params, stage: this };
3556
+ this.setupCallbacks.forEach((cb) => cb(extended));
3557
+ };
3558
+ }
3559
+ if (this.updateCallbacks.length > 0) {
3560
+ this.wrappedStage.update = (params) => {
3561
+ const extended = { ...params, stage: this };
3562
+ this.updateCallbacks.forEach((cb) => cb(extended));
3563
+ };
3564
+ }
3565
+ if (this.destroyCallbacks.length > 0) {
3566
+ this.wrappedStage.destroy = (params) => {
3567
+ const extended = { ...params, stage: this };
3568
+ this.destroyCallbacks.forEach((cb) => cb(extended));
3569
+ };
3570
+ }
3023
3571
  }
3024
3572
  async addEntities(entities) {
3025
- this.options.push(...entities);
3573
+ this._pendingEntities.push(...entities);
3026
3574
  if (!this.wrappedStage) {
3027
3575
  return;
3028
3576
  }
@@ -3047,28 +3595,57 @@ var init_stage = __esm({
3047
3595
  start(params) {
3048
3596
  this.wrappedStage?.nodeSetup(params);
3049
3597
  }
3598
+ // Fluent API for adding lifecycle callbacks
3050
3599
  onUpdate(...callbacks) {
3051
- if (!this.wrappedStage) {
3052
- return;
3600
+ this.updateCallbacks.push(...callbacks);
3601
+ if (this.wrappedStage) {
3602
+ this.wrappedStage.update = (params) => {
3603
+ const extended = { ...params, stage: this };
3604
+ this.updateCallbacks.forEach((cb) => cb(extended));
3605
+ };
3053
3606
  }
3054
- this.wrappedStage.update = (params) => {
3055
- const extended = { ...params, stage: this };
3056
- callbacks.forEach((cb) => cb(extended));
3057
- };
3607
+ return this;
3058
3608
  }
3059
- onSetup(callback) {
3060
- this.wrappedStage.setup = callback;
3609
+ onSetup(...callbacks) {
3610
+ this.setupCallbacks.push(...callbacks);
3611
+ if (this.wrappedStage) {
3612
+ this.wrappedStage.setup = (params) => {
3613
+ const extended = { ...params, stage: this };
3614
+ this.setupCallbacks.forEach((cb) => cb(extended));
3615
+ };
3616
+ }
3617
+ return this;
3061
3618
  }
3062
- onDestroy(callback) {
3063
- this.wrappedStage.destroy = callback;
3619
+ onDestroy(...callbacks) {
3620
+ this.destroyCallbacks.push(...callbacks);
3621
+ if (this.wrappedStage) {
3622
+ this.wrappedStage.destroy = (params) => {
3623
+ const extended = { ...params, stage: this };
3624
+ this.destroyCallbacks.forEach((cb) => cb(extended));
3625
+ };
3626
+ }
3627
+ return this;
3064
3628
  }
3065
3629
  onLoading(callback) {
3066
3630
  if (!this.wrappedStage) {
3631
+ this.pendingLoadingCallbacks.push(callback);
3067
3632
  return () => {
3633
+ this.pendingLoadingCallbacks = this.pendingLoadingCallbacks.filter((c) => c !== callback);
3068
3634
  };
3069
3635
  }
3070
3636
  return this.wrappedStage.onLoading(callback);
3071
3637
  }
3638
+ /**
3639
+ * Find an entity by name on the current stage.
3640
+ * @param name The name of the entity to find
3641
+ * @param type Optional type symbol for type inference (e.g., TEXT_TYPE, SPRITE_TYPE)
3642
+ * @returns The entity if found, or undefined
3643
+ * @example stage.getEntityByName('scoreText', TEXT_TYPE)
3644
+ */
3645
+ getEntityByName(name, type) {
3646
+ const entity = this.wrappedStage?.children.find((c) => c.name === name);
3647
+ return entity;
3648
+ }
3072
3649
  };
3073
3650
  }
3074
3651
  });
@@ -3909,8 +4486,202 @@ var GameCanvas = class {
3909
4486
  }
3910
4487
  };
3911
4488
 
3912
- // src/lib/game/zylem-game.ts
4489
+ // src/lib/game/game-debug-delegate.ts
4490
+ init_debug_state();
3913
4491
  import Stats from "stats.js";
4492
+ import { subscribe as subscribe2 } from "valtio/vanilla";
4493
+ var GameDebugDelegate = class {
4494
+ statsRef = null;
4495
+ unsubscribe = null;
4496
+ constructor() {
4497
+ this.updateDebugUI();
4498
+ this.unsubscribe = subscribe2(debugState, () => {
4499
+ this.updateDebugUI();
4500
+ });
4501
+ }
4502
+ /**
4503
+ * Called every frame - wraps stats.begin()
4504
+ */
4505
+ begin() {
4506
+ this.statsRef?.begin();
4507
+ }
4508
+ /**
4509
+ * Called every frame - wraps stats.end()
4510
+ */
4511
+ end() {
4512
+ this.statsRef?.end();
4513
+ }
4514
+ updateDebugUI() {
4515
+ if (debugState.enabled && !this.statsRef) {
4516
+ this.statsRef = new Stats();
4517
+ this.statsRef.showPanel(0);
4518
+ this.statsRef.dom.style.position = "absolute";
4519
+ this.statsRef.dom.style.bottom = "0";
4520
+ this.statsRef.dom.style.right = "0";
4521
+ this.statsRef.dom.style.top = "auto";
4522
+ this.statsRef.dom.style.left = "auto";
4523
+ document.body.appendChild(this.statsRef.dom);
4524
+ } else if (!debugState.enabled && this.statsRef) {
4525
+ if (this.statsRef.dom.parentNode) {
4526
+ this.statsRef.dom.parentNode.removeChild(this.statsRef.dom);
4527
+ }
4528
+ this.statsRef = null;
4529
+ }
4530
+ }
4531
+ dispose() {
4532
+ if (this.unsubscribe) {
4533
+ this.unsubscribe();
4534
+ this.unsubscribe = null;
4535
+ }
4536
+ if (this.statsRef?.dom?.parentNode) {
4537
+ this.statsRef.dom.parentNode.removeChild(this.statsRef.dom);
4538
+ }
4539
+ this.statsRef = null;
4540
+ }
4541
+ };
4542
+
4543
+ // src/lib/game/game-loading-delegate.ts
4544
+ var GAME_LOADING_EVENT = "GAME_LOADING_EVENT";
4545
+ var GameLoadingDelegate = class {
4546
+ loadingHandlers = [];
4547
+ stageLoadingUnsubscribes = [];
4548
+ /**
4549
+ * Subscribe to loading events from the game.
4550
+ * Events include stage context (stageName, stageIndex).
4551
+ *
4552
+ * @param callback Invoked for each loading event
4553
+ * @returns Unsubscribe function
4554
+ */
4555
+ onLoading(callback) {
4556
+ this.loadingHandlers.push(callback);
4557
+ return () => {
4558
+ this.loadingHandlers = this.loadingHandlers.filter((h) => h !== callback);
4559
+ };
4560
+ }
4561
+ /**
4562
+ * Emit a loading event to all subscribers and dispatch to window.
4563
+ */
4564
+ emit(event) {
4565
+ for (const handler of this.loadingHandlers) {
4566
+ console.log("Game loading event", event);
4567
+ try {
4568
+ handler(event);
4569
+ } catch (e) {
4570
+ console.error("Game loading handler failed", e);
4571
+ }
4572
+ }
4573
+ if (typeof window !== "undefined") {
4574
+ window.dispatchEvent(new CustomEvent(GAME_LOADING_EVENT, { detail: event }));
4575
+ }
4576
+ }
4577
+ /**
4578
+ * Wire up a stage's loading events to this delegate.
4579
+ *
4580
+ * @param stage The stage to wire up
4581
+ * @param stageIndex The index of the stage
4582
+ */
4583
+ wireStageLoading(stage, stageIndex) {
4584
+ const unsub = stage.onLoading((event) => {
4585
+ this.emit({
4586
+ type: event.type,
4587
+ message: event.message ?? "",
4588
+ progress: event.progress ?? 0,
4589
+ current: event.current,
4590
+ total: event.total,
4591
+ stageName: stage.uuid ?? `Stage ${stageIndex}`,
4592
+ stageIndex
4593
+ });
4594
+ });
4595
+ if (typeof unsub === "function") {
4596
+ this.stageLoadingUnsubscribes.push(unsub);
4597
+ }
4598
+ }
4599
+ /**
4600
+ * Unsubscribe from all stage loading events.
4601
+ */
4602
+ unwireAllStages() {
4603
+ for (const unsub of this.stageLoadingUnsubscribes) {
4604
+ try {
4605
+ unsub();
4606
+ } catch {
4607
+ }
4608
+ }
4609
+ this.stageLoadingUnsubscribes = [];
4610
+ }
4611
+ /**
4612
+ * Clean up all handlers.
4613
+ */
4614
+ dispose() {
4615
+ this.unwireAllStages();
4616
+ this.loadingHandlers = [];
4617
+ }
4618
+ };
4619
+
4620
+ // src/lib/game/zylem-game.ts
4621
+ init_game_event_bus();
4622
+
4623
+ // src/lib/game/game-renderer-observer.ts
4624
+ var GameRendererObserver = class {
4625
+ container = null;
4626
+ camera = null;
4627
+ gameCanvas = null;
4628
+ config = null;
4629
+ mounted = false;
4630
+ setGameCanvas(canvas) {
4631
+ this.gameCanvas = canvas;
4632
+ this.tryMount();
4633
+ }
4634
+ setConfig(config) {
4635
+ this.config = config;
4636
+ this.tryMount();
4637
+ }
4638
+ setContainer(container) {
4639
+ this.container = container;
4640
+ this.tryMount();
4641
+ }
4642
+ setCamera(camera2) {
4643
+ this.camera = camera2;
4644
+ this.tryMount();
4645
+ }
4646
+ /**
4647
+ * Attempt to mount renderer if all dependencies are available.
4648
+ */
4649
+ tryMount() {
4650
+ if (this.mounted) return;
4651
+ if (!this.container || !this.camera || !this.gameCanvas) return;
4652
+ const dom = this.camera.getDomElement();
4653
+ const internal = this.config?.internalResolution;
4654
+ this.gameCanvas.mountRenderer(dom, (cssW, cssH) => {
4655
+ if (!this.camera) return;
4656
+ if (internal) {
4657
+ this.camera.setPixelRatio(1);
4658
+ this.camera.resize(internal.width, internal.height);
4659
+ } else {
4660
+ const dpr = window.devicePixelRatio || 1;
4661
+ this.camera.setPixelRatio(dpr);
4662
+ this.camera.resize(cssW, cssH);
4663
+ }
4664
+ });
4665
+ this.mounted = true;
4666
+ }
4667
+ /**
4668
+ * Reset state for stage transitions.
4669
+ */
4670
+ reset() {
4671
+ this.camera = null;
4672
+ this.mounted = false;
4673
+ }
4674
+ dispose() {
4675
+ this.container = null;
4676
+ this.camera = null;
4677
+ this.gameCanvas = null;
4678
+ this.config = null;
4679
+ this.mounted = false;
4680
+ }
4681
+ };
4682
+
4683
+ // src/lib/game/zylem-game.ts
4684
+ var ZYLEM_STATE_DISPATCH = "zylem:state:dispatch";
3914
4685
  var ZylemGame = class _ZylemGame {
3915
4686
  id;
3916
4687
  initialGlobals = {};
@@ -3925,7 +4696,6 @@ var ZylemGame = class _ZylemGame {
3925
4696
  timer;
3926
4697
  inputManager;
3927
4698
  wrapperRef;
3928
- statsRef = null;
3929
4699
  defaultCamera = null;
3930
4700
  container = null;
3931
4701
  canvas = null;
@@ -3934,6 +4704,10 @@ var ZylemGame = class _ZylemGame {
3934
4704
  gameCanvas = null;
3935
4705
  animationFrameId = null;
3936
4706
  isDisposed = false;
4707
+ debugDelegate = null;
4708
+ loadingDelegate = new GameLoadingDelegate();
4709
+ rendererObserver = new GameRendererObserver();
4710
+ eventBusUnsubscribes = [];
3937
4711
  static FRAME_LIMIT = 120;
3938
4712
  static FRAME_DURATION = 1e3 / _ZylemGame.FRAME_LIMIT;
3939
4713
  static MAX_DELTA_SECONDS = 1 / 30;
@@ -3965,42 +4739,33 @@ var ZylemGame = class _ZylemGame {
3965
4739
  this.gameCanvas.applyBodyBackground();
3966
4740
  this.gameCanvas.mountCanvas();
3967
4741
  this.gameCanvas.centerIfFullscreen();
4742
+ this.rendererObserver.setGameCanvas(this.gameCanvas);
4743
+ if (this.resolvedConfig) {
4744
+ this.rendererObserver.setConfig(this.resolvedConfig);
4745
+ }
4746
+ if (this.container) {
4747
+ this.rendererObserver.setContainer(this.container);
4748
+ }
4749
+ this.subscribeToEventBus();
3968
4750
  }
3969
4751
  loadDebugOptions(options) {
3970
- debugState.enabled = Boolean(options.debug);
3971
- if (options.debug) {
3972
- this.statsRef = new Stats();
3973
- this.statsRef.showPanel(0);
3974
- this.statsRef.dom.style.position = "absolute";
3975
- this.statsRef.dom.style.bottom = "0";
3976
- this.statsRef.dom.style.right = "0";
3977
- this.statsRef.dom.style.top = "auto";
3978
- this.statsRef.dom.style.left = "auto";
3979
- document.body.appendChild(this.statsRef.dom);
4752
+ if (options.debug !== void 0) {
4753
+ debugState.enabled = Boolean(options.debug);
3980
4754
  }
4755
+ this.debugDelegate = new GameDebugDelegate();
3981
4756
  }
3982
- async loadStage(stage) {
4757
+ loadStage(stage, stageIndex = 0) {
3983
4758
  this.unloadCurrentStage();
3984
4759
  const config = stage.options[0];
3985
- await stage.load(this.id, config?.camera);
3986
- this.stageMap.set(stage.wrappedStage.uuid, stage);
3987
- this.currentStageId = stage.wrappedStage.uuid;
3988
- this.defaultCamera = stage.wrappedStage.cameraRef;
3989
- if (this.container && this.defaultCamera) {
3990
- const dom = this.defaultCamera.getDomElement();
3991
- const internal = this.resolvedConfig?.internalResolution;
3992
- this.gameCanvas?.mountRenderer(dom, (cssW, cssH) => {
3993
- if (!this.defaultCamera) return;
3994
- if (internal) {
3995
- this.defaultCamera.setPixelRatio(1);
3996
- this.defaultCamera.resize(internal.width, internal.height);
3997
- } else {
3998
- const dpr = window.devicePixelRatio || 1;
3999
- this.defaultCamera.setPixelRatio(dpr);
4000
- this.defaultCamera.resize(cssW, cssH);
4001
- }
4002
- });
4003
- }
4760
+ this.loadingDelegate.wireStageLoading(stage, stageIndex);
4761
+ return stage.load(this.id, config?.camera).then(() => {
4762
+ this.stageMap.set(stage.wrappedStage.uuid, stage);
4763
+ this.currentStageId = stage.wrappedStage.uuid;
4764
+ this.defaultCamera = stage.wrappedStage.cameraRef;
4765
+ if (this.defaultCamera) {
4766
+ this.rendererObserver.setCamera(this.defaultCamera);
4767
+ }
4768
+ });
4004
4769
  }
4005
4770
  unloadCurrentStage() {
4006
4771
  if (!this.currentStageId) return;
@@ -4015,8 +4780,12 @@ var ZylemGame = class _ZylemGame {
4015
4780
  } catch (e) {
4016
4781
  console.error("Failed to destroy previous stage", e);
4017
4782
  }
4783
+ current.wrappedStage = null;
4018
4784
  }
4019
4785
  this.stageMap.delete(this.currentStageId);
4786
+ this.currentStageId = "";
4787
+ this.defaultCamera = null;
4788
+ this.rendererObserver.reset();
4020
4789
  }
4021
4790
  setGlobals(options) {
4022
4791
  this.initialGlobals = { ...options.globals };
@@ -4051,7 +4820,7 @@ var ZylemGame = class _ZylemGame {
4051
4820
  this.loop(0);
4052
4821
  }
4053
4822
  loop(timestamp) {
4054
- this.statsRef && this.statsRef.begin();
4823
+ this.debugDelegate?.begin();
4055
4824
  if (!isPaused()) {
4056
4825
  this.timer.update(timestamp);
4057
4826
  const stage = this.currentStage();
@@ -4061,14 +4830,14 @@ var ZylemGame = class _ZylemGame {
4061
4830
  if (this.customUpdate) {
4062
4831
  this.customUpdate(clampedParams);
4063
4832
  }
4064
- if (stage) {
4833
+ if (stage && stage.wrappedStage) {
4065
4834
  stage.wrappedStage.nodeUpdate({ ...clampedParams, me: stage.wrappedStage });
4066
4835
  }
4067
4836
  this.totalTime += clampedParams.delta;
4068
4837
  state.time = this.totalTime;
4069
4838
  this.previousTimeStamp = timestamp;
4070
4839
  }
4071
- this.statsRef && this.statsRef.end();
4840
+ this.debugDelegate?.end();
4072
4841
  this.outOfLoop();
4073
4842
  if (!this.isDisposed) {
4074
4843
  this.animationFrameId = requestAnimationFrame(this.loop.bind(this));
@@ -4081,9 +4850,13 @@ var ZylemGame = class _ZylemGame {
4081
4850
  this.animationFrameId = null;
4082
4851
  }
4083
4852
  this.unloadCurrentStage();
4084
- if (this.statsRef && this.statsRef.dom && this.statsRef.dom.parentNode) {
4085
- this.statsRef.dom.parentNode.removeChild(this.statsRef.dom);
4853
+ if (this.debugDelegate) {
4854
+ this.debugDelegate.dispose();
4855
+ this.debugDelegate = null;
4086
4856
  }
4857
+ this.eventBusUnsubscribes.forEach((unsub) => unsub());
4858
+ this.eventBusUnsubscribes = [];
4859
+ this.rendererObserver.dispose();
4087
4860
  this.timer.dispose();
4088
4861
  if (this.customDestroy) {
4089
4862
  this.customDestroy({
@@ -4104,6 +4877,52 @@ var ZylemGame = class _ZylemGame {
4104
4877
  currentStage() {
4105
4878
  return this.getStage(this.currentStageId);
4106
4879
  }
4880
+ /**
4881
+ * Subscribe to loading events from the game.
4882
+ * Events include stage context (stageName, stageIndex).
4883
+ * @param callback Invoked for each loading event
4884
+ * @returns Unsubscribe function
4885
+ */
4886
+ onLoading(callback) {
4887
+ return this.loadingDelegate.onLoading(callback);
4888
+ }
4889
+ /**
4890
+ * Subscribe to the game event bus for stage loading and state events.
4891
+ * Emits window events for cross-application communication.
4892
+ */
4893
+ subscribeToEventBus() {
4894
+ const emitLoadingWindowEvent = (payload) => {
4895
+ if (typeof window !== "undefined") {
4896
+ const event = {
4897
+ type: payload.type,
4898
+ message: payload.message ?? "",
4899
+ progress: payload.progress ?? 0,
4900
+ current: payload.current,
4901
+ total: payload.total,
4902
+ stageName: payload.stageName,
4903
+ stageIndex: payload.stageIndex
4904
+ };
4905
+ window.dispatchEvent(new CustomEvent(GAME_LOADING_EVENT, { detail: event }));
4906
+ }
4907
+ };
4908
+ const emitStateDispatchEvent = (payload) => {
4909
+ if (typeof window !== "undefined") {
4910
+ const detail = {
4911
+ scope: "game",
4912
+ path: payload.path,
4913
+ value: payload.value,
4914
+ previousValue: payload.previousValue
4915
+ };
4916
+ window.dispatchEvent(new CustomEvent(ZYLEM_STATE_DISPATCH, { detail }));
4917
+ }
4918
+ };
4919
+ this.eventBusUnsubscribes.push(
4920
+ gameEventBus.on("stage:loading:start", emitLoadingWindowEvent),
4921
+ gameEventBus.on("stage:loading:progress", emitLoadingWindowEvent),
4922
+ gameEventBus.on("stage:loading:complete", emitLoadingWindowEvent),
4923
+ gameEventBus.on("game:state:updated", emitStateDispatchEvent)
4924
+ );
4925
+ }
4107
4926
  };
4108
4927
 
4109
4928
  // src/lib/game/game.ts
@@ -4234,7 +5053,7 @@ init_stage();
4234
5053
  init_entity();
4235
5054
  init_builder();
4236
5055
  init_create();
4237
- import { Color as Color9, Group as Group5, Sprite as ThreeSprite, SpriteMaterial, CanvasTexture, LinearFilter, Vector2 as Vector27, ClampToEdgeWrapping } from "three";
5056
+ import { Color as Color10, Group as Group5, Sprite as ThreeSprite, SpriteMaterial, CanvasTexture, LinearFilter, Vector2 as Vector28, ClampToEdgeWrapping } from "three";
4238
5057
 
4239
5058
  // src/lib/entities/delegates/debug.ts
4240
5059
  import { MeshStandardMaterial as MeshStandardMaterial2, MeshBasicMaterial as MeshBasicMaterial2, MeshPhongMaterial as MeshPhongMaterial2 } from "three";
@@ -4341,7 +5160,7 @@ var textDefaults = {
4341
5160
  backgroundColor: null,
4342
5161
  padding: 4,
4343
5162
  stickToViewport: true,
4344
- screenPosition: new Vector27(24, 24),
5163
+ screenPosition: new Vector28(24, 24),
4345
5164
  zDistance: 1
4346
5165
  };
4347
5166
  var TextBuilder = class extends EntityBuilder {
@@ -4349,7 +5168,7 @@ var TextBuilder = class extends EntityBuilder {
4349
5168
  return new ZylemText(options);
4350
5169
  }
4351
5170
  };
4352
- var TEXT_TYPE = Symbol("Text");
5171
+ var TEXT_TYPE = /* @__PURE__ */ Symbol("Text");
4353
5172
  var ZylemText = class _ZylemText extends GameEntity {
4354
5173
  static type = TEXT_TYPE;
4355
5174
  _sprite = null;
@@ -4362,12 +5181,20 @@ var ZylemText = class _ZylemText extends GameEntity {
4362
5181
  constructor(options) {
4363
5182
  super();
4364
5183
  this.options = { ...textDefaults, ...options };
5184
+ this.prependSetup(this.textSetup.bind(this));
5185
+ this.prependUpdate(this.textUpdate.bind(this));
5186
+ this.onDestroy(this.textDestroy.bind(this));
5187
+ }
5188
+ create() {
5189
+ this._sprite = null;
5190
+ this._texture = null;
5191
+ this._canvas = null;
5192
+ this._ctx = null;
5193
+ this._lastCanvasW = 0;
5194
+ this._lastCanvasH = 0;
4365
5195
  this.group = new Group5();
4366
5196
  this.createSprite();
4367
- this.lifeCycleDelegate = {
4368
- setup: [this.textSetup.bind(this)],
4369
- update: [this.textUpdate.bind(this)]
4370
- };
5197
+ return super.create();
4371
5198
  }
4372
5199
  createSprite() {
4373
5200
  this._canvas = document.createElement("canvas");
@@ -4445,7 +5272,7 @@ var ZylemText = class _ZylemText extends GameEntity {
4445
5272
  }
4446
5273
  toCssColor(color) {
4447
5274
  if (typeof color === "string") return color;
4448
- const c = color instanceof Color9 ? color : new Color9(color);
5275
+ const c = color instanceof Color10 ? color : new Color10(color);
4449
5276
  return `#${c.getHexString()}`;
4450
5277
  }
4451
5278
  textSetup(params) {
@@ -4505,7 +5332,7 @@ var ZylemText = class _ZylemText extends GameEntity {
4505
5332
  if (!this._sprite || !this._cameraRef) return;
4506
5333
  const camera2 = this._cameraRef.camera;
4507
5334
  const { width, height } = this.getResolution();
4508
- const sp = this.options.screenPosition ?? new Vector27(24, 24);
5335
+ const sp = this.options.screenPosition ?? new Vector28(24, 24);
4509
5336
  const { px, py } = this.getScreenPixels(sp, width, height);
4510
5337
  const zDist = Math.max(1e-3, this.options.zDistance ?? 1);
4511
5338
  const { worldHalfW, worldHalfH } = this.computeWorldExtents(camera2, zDist);
@@ -4533,6 +5360,24 @@ var ZylemText = class _ZylemText extends GameEntity {
4533
5360
  sticky: this.options.stickToViewport
4534
5361
  };
4535
5362
  }
5363
+ /**
5364
+ * Dispose of Three.js resources when the entity is destroyed.
5365
+ */
5366
+ async textDestroy() {
5367
+ this._texture?.dispose();
5368
+ if (this._sprite?.material) {
5369
+ this._sprite.material.dispose();
5370
+ }
5371
+ if (this._sprite) {
5372
+ this._sprite.removeFromParent();
5373
+ }
5374
+ this.group?.removeFromParent();
5375
+ this._sprite = null;
5376
+ this._texture = null;
5377
+ this._canvas = null;
5378
+ this._ctx = null;
5379
+ this._cameraRef = null;
5380
+ }
4536
5381
  };
4537
5382
  async function text(...args) {
4538
5383
  return createEntity({
@@ -4550,20 +5395,20 @@ init_builder();
4550
5395
  init_builder();
4551
5396
  init_create();
4552
5397
  import { ColliderDesc as ColliderDesc3 } from "@dimforge/rapier3d-compat";
4553
- import { Color as Color10, Euler, Group as Group6, Quaternion as Quaternion2, Vector3 as Vector314 } from "three";
5398
+ import { Color as Color11, Euler, Group as Group6, Quaternion as Quaternion3, Vector3 as Vector316 } from "three";
4554
5399
  import {
4555
5400
  TextureLoader as TextureLoader3,
4556
5401
  SpriteMaterial as SpriteMaterial2,
4557
5402
  Sprite as ThreeSprite2
4558
5403
  } from "three";
4559
5404
  var spriteDefaults = {
4560
- size: new Vector314(1, 1, 1),
4561
- position: new Vector314(0, 0, 0),
5405
+ size: new Vector316(1, 1, 1),
5406
+ position: new Vector316(0, 0, 0),
4562
5407
  collision: {
4563
5408
  static: false
4564
5409
  },
4565
5410
  material: {
4566
- color: new Color10("#ffffff"),
5411
+ color: new Color11("#ffffff"),
4567
5412
  shader: "standard"
4568
5413
  },
4569
5414
  images: [],
@@ -4571,7 +5416,7 @@ var spriteDefaults = {
4571
5416
  };
4572
5417
  var SpriteCollisionBuilder = class extends EntityCollisionBuilder {
4573
5418
  collider(options) {
4574
- const size = options.collisionSize || options.size || new Vector314(1, 1, 1);
5419
+ const size = options.collisionSize || options.size || new Vector316(1, 1, 1);
4575
5420
  const half = { x: size.x / 2, y: size.y / 2, z: size.z / 2 };
4576
5421
  let colliderDesc = ColliderDesc3.cuboid(half.x, half.y, half.z);
4577
5422
  return colliderDesc;
@@ -4582,7 +5427,7 @@ var SpriteBuilder = class extends EntityBuilder {
4582
5427
  return new ZylemSprite(options);
4583
5428
  }
4584
5429
  };
4585
- var SPRITE_TYPE = Symbol("Sprite");
5430
+ var SPRITE_TYPE = /* @__PURE__ */ Symbol("Sprite");
4586
5431
  var ZylemSprite = class _ZylemSprite extends GameEntity {
4587
5432
  static type = SPRITE_TYPE;
4588
5433
  sprites = [];
@@ -4596,12 +5441,21 @@ var ZylemSprite = class _ZylemSprite extends GameEntity {
4596
5441
  constructor(options) {
4597
5442
  super();
4598
5443
  this.options = { ...spriteDefaults, ...options };
4599
- this.createSpritesFromImages(options?.images || []);
4600
- this.createAnimations(options?.animations || []);
4601
- this.lifeCycleDelegate = {
4602
- update: [this.spriteUpdate.bind(this)],
4603
- destroy: [this.spriteDestroy.bind(this)]
4604
- };
5444
+ this.prependUpdate(this.spriteUpdate.bind(this));
5445
+ this.onDestroy(this.spriteDestroy.bind(this));
5446
+ }
5447
+ create() {
5448
+ this.sprites = [];
5449
+ this.spriteMap.clear();
5450
+ this.animations.clear();
5451
+ this.currentAnimation = null;
5452
+ this.currentAnimationFrame = "";
5453
+ this.currentAnimationIndex = 0;
5454
+ this.currentAnimationTime = 0;
5455
+ this.group = void 0;
5456
+ this.createSpritesFromImages(this.options?.images || []);
5457
+ this.createAnimations(this.options?.animations || []);
5458
+ return super.create();
4605
5459
  }
4606
5460
  createSpritesFromImages(images) {
4607
5461
  const textureLoader = new TextureLoader3();
@@ -4671,7 +5525,7 @@ var ZylemSprite = class _ZylemSprite extends GameEntity {
4671
5525
  if (_sprite.material) {
4672
5526
  const q = this.body?.rotation();
4673
5527
  if (q) {
4674
- const quat = new Quaternion2(q.x, q.y, q.z, q.w);
5528
+ const quat = new Quaternion3(q.x, q.y, q.z, q.w);
4675
5529
  const euler = new Euler().setFromQuaternion(quat, "XYZ");
4676
5530
  _sprite.material.rotation = euler.z;
4677
5531
  }
@@ -4755,12 +5609,15 @@ init_game_state();
4755
5609
  var Game = class {
4756
5610
  wrappedGame = null;
4757
5611
  options;
4758
- update = () => {
4759
- };
4760
- setup = () => {
4761
- };
4762
- destroy = () => {
4763
- };
5612
+ // Lifecycle callback arrays
5613
+ setupCallbacks = [];
5614
+ updateCallbacks = [];
5615
+ destroyCallbacks = [];
5616
+ pendingLoadingCallbacks = [];
5617
+ // Game-scoped global change subscriptions
5618
+ globalChangeCallbacks = [];
5619
+ globalChangesCallbacks = [];
5620
+ activeGlobalSubscriptions = [];
4764
5621
  refErrorMessage = "lost reference to game";
4765
5622
  constructor(options) {
4766
5623
  this.options = options;
@@ -4772,10 +5629,29 @@ var Game = class {
4772
5629
  initGlobals(globals);
4773
5630
  }
4774
5631
  }
5632
+ // Fluent API for adding lifecycle callbacks
5633
+ onSetup(...callbacks) {
5634
+ this.setupCallbacks.push(...callbacks);
5635
+ return this;
5636
+ }
5637
+ onUpdate(...callbacks) {
5638
+ this.updateCallbacks.push(...callbacks);
5639
+ return this;
5640
+ }
5641
+ onDestroy(...callbacks) {
5642
+ this.destroyCallbacks.push(...callbacks);
5643
+ return this;
5644
+ }
4775
5645
  async start() {
5646
+ resetGlobals();
5647
+ const globals = extractGlobalsFromOptions(this.options);
5648
+ if (globals) {
5649
+ initGlobals(globals);
5650
+ }
4776
5651
  const game = await this.load();
4777
5652
  this.wrappedGame = game;
4778
5653
  this.setOverrides();
5654
+ this.registerGlobalSubscriptions();
4779
5655
  game.start();
4780
5656
  return this;
4781
5657
  }
@@ -4786,6 +5662,9 @@ var Game = class {
4786
5662
  ...options,
4787
5663
  ...resolved
4788
5664
  }, this);
5665
+ for (const callback of this.pendingLoadingCallbacks) {
5666
+ game.onLoading(callback);
5667
+ }
4789
5668
  await game.loadStage(options.stages[0]);
4790
5669
  return game;
4791
5670
  }
@@ -4794,9 +5673,59 @@ var Game = class {
4794
5673
  console.error(this.refErrorMessage);
4795
5674
  return;
4796
5675
  }
4797
- this.wrappedGame.customSetup = this.setup;
4798
- this.wrappedGame.customUpdate = this.update;
4799
- this.wrappedGame.customDestroy = this.destroy;
5676
+ this.wrappedGame.customSetup = (params) => {
5677
+ this.setupCallbacks.forEach((cb) => cb(params));
5678
+ };
5679
+ this.wrappedGame.customUpdate = (params) => {
5680
+ this.updateCallbacks.forEach((cb) => cb(params));
5681
+ };
5682
+ this.wrappedGame.customDestroy = (params) => {
5683
+ this.destroyCallbacks.forEach((cb) => cb(params));
5684
+ };
5685
+ }
5686
+ /**
5687
+ * Subscribe to changes on a global value. Subscriptions are registered
5688
+ * when the game starts and cleaned up when disposed.
5689
+ * The callback receives the value and the current stage.
5690
+ * @example game.onGlobalChange('score', (val, stage) => console.log(val));
5691
+ */
5692
+ onGlobalChange(path, callback) {
5693
+ this.globalChangeCallbacks.push({ path, callback });
5694
+ return this;
5695
+ }
5696
+ /**
5697
+ * Subscribe to changes on multiple global paths. Subscriptions are registered
5698
+ * when the game starts and cleaned up when disposed.
5699
+ * The callback receives the values and the current stage.
5700
+ * @example game.onGlobalChanges(['score', 'lives'], ([score, lives], stage) => console.log(score, lives));
5701
+ */
5702
+ onGlobalChanges(paths, callback) {
5703
+ this.globalChangesCallbacks.push({ paths, callback });
5704
+ return this;
5705
+ }
5706
+ /**
5707
+ * Register all stored global change callbacks.
5708
+ * Called internally during start().
5709
+ */
5710
+ registerGlobalSubscriptions() {
5711
+ for (const { path, callback } of this.globalChangeCallbacks) {
5712
+ const unsub = onGlobalChange(path, (value) => {
5713
+ callback(value, this.getCurrentStage());
5714
+ });
5715
+ this.activeGlobalSubscriptions.push(unsub);
5716
+ }
5717
+ for (const { paths, callback } of this.globalChangesCallbacks) {
5718
+ const unsub = onGlobalChanges(paths, (values) => {
5719
+ callback(values, this.getCurrentStage());
5720
+ });
5721
+ this.activeGlobalSubscriptions.push(unsub);
5722
+ }
5723
+ }
5724
+ /**
5725
+ * Get the current stage wrapper.
5726
+ */
5727
+ getCurrentStage() {
5728
+ return this.wrappedGame?.currentStage() ?? null;
4800
5729
  }
4801
5730
  async pause() {
4802
5731
  setPaused(true);
@@ -4815,19 +5744,19 @@ var Game = class {
4815
5744
  }
4816
5745
  await this.wrappedGame.loadStage(this.wrappedGame.stages[0]);
4817
5746
  }
4818
- async previousStage() {
5747
+ previousStage() {
4819
5748
  if (!this.wrappedGame) {
4820
5749
  console.error(this.refErrorMessage);
4821
5750
  return;
4822
5751
  }
4823
5752
  const currentStageId = this.wrappedGame.currentStageId;
4824
- const currentIndex = this.wrappedGame.stages.findIndex((s) => s.wrappedStage.uuid === currentStageId);
5753
+ const currentIndex = this.wrappedGame.stages.findIndex((s) => s.wrappedStage?.uuid === currentStageId);
4825
5754
  const previousStage = this.wrappedGame.stages[currentIndex - 1];
4826
5755
  if (!previousStage) {
4827
5756
  console.error("previous stage called on first stage");
4828
5757
  return;
4829
5758
  }
4830
- await this.wrappedGame.loadStage(previousStage);
5759
+ this.wrappedGame.loadStage(previousStage);
4831
5760
  }
4832
5761
  async loadStageFromId(stageId) {
4833
5762
  if (!this.wrappedGame) {
@@ -4843,39 +5772,61 @@ var Game = class {
4843
5772
  console.error(`Failed to load stage ${stageId}`, e);
4844
5773
  }
4845
5774
  }
4846
- async nextStage() {
5775
+ nextStage() {
4847
5776
  if (!this.wrappedGame) {
4848
5777
  console.error(this.refErrorMessage);
4849
5778
  return;
4850
5779
  }
4851
5780
  if (stageState2.next) {
4852
5781
  const nextId = stageState2.next.id;
4853
- await StageManager.transitionForward(nextId);
5782
+ StageManager.transitionForward(nextId);
4854
5783
  if (stageState2.current) {
4855
- const stage = await StageFactory.createFromBlueprint(stageState2.current);
4856
- await this.wrappedGame.loadStage(stage);
5784
+ StageFactory.createFromBlueprint(stageState2.current).then((stage) => {
5785
+ this.wrappedGame?.loadStage(stage);
5786
+ });
4857
5787
  return;
4858
5788
  }
4859
5789
  }
4860
5790
  const currentStageId = this.wrappedGame.currentStageId;
4861
- const currentIndex = this.wrappedGame.stages.findIndex((s) => s.wrappedStage.uuid === currentStageId);
5791
+ const currentIndex = this.wrappedGame.stages.findIndex((s) => s.wrappedStage?.uuid === currentStageId);
4862
5792
  const nextStage = this.wrappedGame.stages[currentIndex + 1];
4863
5793
  if (!nextStage) {
4864
5794
  console.error("next stage called on last stage");
4865
5795
  return;
4866
5796
  }
4867
- await this.wrappedGame.loadStage(nextStage);
5797
+ this.wrappedGame.loadStage(nextStage);
4868
5798
  }
4869
5799
  async goToStage() {
4870
5800
  }
4871
5801
  async end() {
4872
5802
  }
4873
5803
  dispose() {
5804
+ for (const unsub of this.activeGlobalSubscriptions) {
5805
+ unsub();
5806
+ }
5807
+ this.activeGlobalSubscriptions = [];
4874
5808
  if (this.wrappedGame) {
4875
5809
  this.wrappedGame.dispose();
4876
5810
  }
5811
+ clearGlobalSubscriptions();
5812
+ resetGlobals();
4877
5813
  }
5814
+ /**
5815
+ * Subscribe to loading events from the game.
5816
+ * Events include stage context (stageName, stageIndex).
5817
+ * @param callback Invoked for each loading event
5818
+ * @returns Unsubscribe function
5819
+ */
4878
5820
  onLoading(callback) {
5821
+ if (this.wrappedGame) {
5822
+ return this.wrappedGame.onLoading(callback);
5823
+ }
5824
+ this.pendingLoadingCallbacks.push(callback);
5825
+ return () => {
5826
+ this.pendingLoadingCallbacks = this.pendingLoadingCallbacks.filter((c) => c !== callback);
5827
+ if (this.wrappedGame) {
5828
+ }
5829
+ };
4879
5830
  }
4880
5831
  };
4881
5832
  function createGame(...options) {
@@ -4886,7 +5837,7 @@ function createGame(...options) {
4886
5837
  init_stage();
4887
5838
 
4888
5839
  // src/lib/stage/entity-spawner.ts
4889
- import { Euler as Euler2, Quaternion as Quaternion3, Vector2 as Vector28 } from "three";
5840
+ import { Euler as Euler2, Quaternion as Quaternion4, Vector2 as Vector29 } from "three";
4890
5841
  function entitySpawner(factory) {
4891
5842
  return {
4892
5843
  spawn: async (stage, x, y) => {
@@ -4894,7 +5845,7 @@ function entitySpawner(factory) {
4894
5845
  stage.add(instance);
4895
5846
  return instance;
4896
5847
  },
4897
- spawnRelative: async (source, stage, offset = new Vector28(0, 1)) => {
5848
+ spawnRelative: async (source, stage, offset = new Vector29(0, 1)) => {
4898
5849
  if (!source.body) {
4899
5850
  console.warn("body missing for entity during spawnRelative");
4900
5851
  return void 0;
@@ -4903,7 +5854,7 @@ function entitySpawner(factory) {
4903
5854
  let rz = source._rotation2DAngle ?? 0;
4904
5855
  try {
4905
5856
  const r = source.body.rotation();
4906
- const q = new Quaternion3(r.x, r.y, r.z, r.w);
5857
+ const q = new Quaternion4(r.x, r.y, r.z, r.w);
4907
5858
  const e = new Euler2().setFromQuaternion(q, "XYZ");
4908
5859
  rz = e.z;
4909
5860
  } catch {
@@ -4919,7 +5870,7 @@ function entitySpawner(factory) {
4919
5870
 
4920
5871
  // src/lib/core/vessel.ts
4921
5872
  init_base_node();
4922
- var VESSEL_TYPE = Symbol("vessel");
5873
+ var VESSEL_TYPE = /* @__PURE__ */ Symbol("vessel");
4923
5874
  var Vessel = class extends BaseNode {
4924
5875
  static type = VESSEL_TYPE;
4925
5876
  _setup(_params) {
@@ -4952,23 +5903,23 @@ init_builder();
4952
5903
  init_builder();
4953
5904
  init_builder();
4954
5905
  import { ColliderDesc as ColliderDesc4 } from "@dimforge/rapier3d-compat";
4955
- import { BoxGeometry as BoxGeometry2, Color as Color11 } from "three";
4956
- import { Vector3 as Vector315 } from "three";
5906
+ import { BoxGeometry as BoxGeometry2, Color as Color12 } from "three";
5907
+ import { Vector3 as Vector317 } from "three";
4957
5908
  init_create();
4958
5909
  var boxDefaults = {
4959
- size: new Vector315(1, 1, 1),
4960
- position: new Vector315(0, 0, 0),
5910
+ size: new Vector317(1, 1, 1),
5911
+ position: new Vector317(0, 0, 0),
4961
5912
  collision: {
4962
5913
  static: false
4963
5914
  },
4964
5915
  material: {
4965
- color: new Color11("#ffffff"),
5916
+ color: new Color12("#ffffff"),
4966
5917
  shader: "standard"
4967
5918
  }
4968
5919
  };
4969
5920
  var BoxCollisionBuilder = class extends EntityCollisionBuilder {
4970
5921
  collider(options) {
4971
- const size = options.size || new Vector315(1, 1, 1);
5922
+ const size = options.size || new Vector317(1, 1, 1);
4972
5923
  const half = { x: size.x / 2, y: size.y / 2, z: size.z / 2 };
4973
5924
  let colliderDesc = ColliderDesc4.cuboid(half.x, half.y, half.z);
4974
5925
  return colliderDesc;
@@ -4976,7 +5927,7 @@ var BoxCollisionBuilder = class extends EntityCollisionBuilder {
4976
5927
  };
4977
5928
  var BoxMeshBuilder = class extends EntityMeshBuilder {
4978
5929
  build(options) {
4979
- const size = options.size ?? new Vector315(1, 1, 1);
5930
+ const size = options.size ?? new Vector317(1, 1, 1);
4980
5931
  return new BoxGeometry2(size.x, size.y, size.z);
4981
5932
  }
4982
5933
  };
@@ -4985,7 +5936,7 @@ var BoxBuilder = class extends EntityBuilder {
4985
5936
  return new ZylemBox(options);
4986
5937
  }
4987
5938
  };
4988
- var BOX_TYPE = Symbol("Box");
5939
+ var BOX_TYPE = /* @__PURE__ */ Symbol("Box");
4989
5940
  var ZylemBox = class _ZylemBox extends GameEntity {
4990
5941
  static type = BOX_TYPE;
4991
5942
  constructor(options) {
@@ -5021,17 +5972,17 @@ init_builder();
5021
5972
  init_builder();
5022
5973
  init_builder();
5023
5974
  import { ColliderDesc as ColliderDesc5 } from "@dimforge/rapier3d-compat";
5024
- import { Color as Color12, SphereGeometry } from "three";
5025
- import { Vector3 as Vector316 } from "three";
5975
+ import { Color as Color13, SphereGeometry } from "three";
5976
+ import { Vector3 as Vector318 } from "three";
5026
5977
  init_create();
5027
5978
  var sphereDefaults = {
5028
5979
  radius: 1,
5029
- position: new Vector316(0, 0, 0),
5980
+ position: new Vector318(0, 0, 0),
5030
5981
  collision: {
5031
5982
  static: false
5032
5983
  },
5033
5984
  material: {
5034
- color: new Color12("#ffffff"),
5985
+ color: new Color13("#ffffff"),
5035
5986
  shader: "standard"
5036
5987
  }
5037
5988
  };
@@ -5053,7 +6004,7 @@ var SphereBuilder = class extends EntityBuilder {
5053
6004
  return new ZylemSphere(options);
5054
6005
  }
5055
6006
  };
5056
- var SPHERE_TYPE = Symbol("Sphere");
6007
+ var SPHERE_TYPE = /* @__PURE__ */ Symbol("Sphere");
5057
6008
  var ZylemSphere = class _ZylemSphere extends GameEntity {
5058
6009
  static type = SPHERE_TYPE;
5059
6010
  constructor(options) {
@@ -5089,7 +6040,7 @@ init_builder();
5089
6040
  init_builder();
5090
6041
  init_builder();
5091
6042
  import { ColliderDesc as ColliderDesc6 } from "@dimforge/rapier3d-compat";
5092
- import { Color as Color13, PlaneGeometry, Vector2 as Vector29, Vector3 as Vector317 } from "three";
6043
+ import { Color as Color14, PlaneGeometry, Vector2 as Vector210, Vector3 as Vector319 } from "three";
5093
6044
 
5094
6045
  // src/lib/graphics/geometries/XZPlaneGeometry.ts
5095
6046
  import { BufferGeometry as BufferGeometry5, Float32BufferAttribute } from "three";
@@ -5154,25 +6105,25 @@ var XZPlaneGeometry = class _XZPlaneGeometry extends BufferGeometry5 {
5154
6105
  init_create();
5155
6106
  var DEFAULT_SUBDIVISIONS = 4;
5156
6107
  var planeDefaults = {
5157
- tile: new Vector29(10, 10),
5158
- repeat: new Vector29(1, 1),
5159
- position: new Vector317(0, 0, 0),
6108
+ tile: new Vector210(10, 10),
6109
+ repeat: new Vector210(1, 1),
6110
+ position: new Vector319(0, 0, 0),
5160
6111
  collision: {
5161
6112
  static: true
5162
6113
  },
5163
6114
  material: {
5164
- color: new Color13("#ffffff"),
6115
+ color: new Color14("#ffffff"),
5165
6116
  shader: "standard"
5166
6117
  },
5167
6118
  subdivisions: DEFAULT_SUBDIVISIONS
5168
6119
  };
5169
6120
  var PlaneCollisionBuilder = class extends EntityCollisionBuilder {
5170
6121
  collider(options) {
5171
- const tile = options.tile ?? new Vector29(1, 1);
6122
+ const tile = options.tile ?? new Vector210(1, 1);
5172
6123
  const subdivisions = options.subdivisions ?? DEFAULT_SUBDIVISIONS;
5173
- const size = new Vector317(tile.x, 1, tile.y);
6124
+ const size = new Vector319(tile.x, 1, tile.y);
5174
6125
  const heightData = options._builders?.meshBuilder?.heightData;
5175
- const scale2 = new Vector317(size.x, 1, size.z);
6126
+ const scale2 = new Vector319(size.x, 1, size.z);
5176
6127
  let colliderDesc = ColliderDesc6.heightfield(
5177
6128
  subdivisions,
5178
6129
  subdivisions,
@@ -5186,9 +6137,9 @@ var PlaneMeshBuilder = class extends EntityMeshBuilder {
5186
6137
  heightData = new Float32Array();
5187
6138
  columnsRows = /* @__PURE__ */ new Map();
5188
6139
  build(options) {
5189
- const tile = options.tile ?? new Vector29(1, 1);
6140
+ const tile = options.tile ?? new Vector210(1, 1);
5190
6141
  const subdivisions = options.subdivisions ?? DEFAULT_SUBDIVISIONS;
5191
- const size = new Vector317(tile.x, 1, tile.y);
6142
+ const size = new Vector319(tile.x, 1, tile.y);
5192
6143
  const geometry = new XZPlaneGeometry(size.x, size.z, subdivisions, subdivisions);
5193
6144
  const vertexGeometry = new PlaneGeometry(size.x, size.z, subdivisions, subdivisions);
5194
6145
  const dx = size.x / subdivisions;
@@ -5230,7 +6181,7 @@ var PlaneBuilder = class extends EntityBuilder {
5230
6181
  return new ZylemPlane(options);
5231
6182
  }
5232
6183
  };
5233
- var PLANE_TYPE = Symbol("Plane");
6184
+ var PLANE_TYPE = /* @__PURE__ */ Symbol("Plane");
5234
6185
  var ZylemPlane = class extends GameEntity {
5235
6186
  static type = PLANE_TYPE;
5236
6187
  constructor(options) {
@@ -5257,10 +6208,10 @@ init_builder();
5257
6208
  init_create();
5258
6209
  init_game_state();
5259
6210
  import { ActiveCollisionTypes as ActiveCollisionTypes3, ColliderDesc as ColliderDesc7 } from "@dimforge/rapier3d-compat";
5260
- import { Vector3 as Vector318 } from "three";
6211
+ import { Vector3 as Vector320 } from "three";
5261
6212
  var zoneDefaults = {
5262
- size: new Vector318(1, 1, 1),
5263
- position: new Vector318(0, 0, 0),
6213
+ size: new Vector320(1, 1, 1),
6214
+ position: new Vector320(0, 0, 0),
5264
6215
  collision: {
5265
6216
  static: true
5266
6217
  },
@@ -5270,7 +6221,7 @@ var zoneDefaults = {
5270
6221
  };
5271
6222
  var ZoneCollisionBuilder = class extends EntityCollisionBuilder {
5272
6223
  collider(options) {
5273
- const size = options.size || new Vector318(1, 1, 1);
6224
+ const size = options.size || new Vector320(1, 1, 1);
5274
6225
  const half = { x: size.x / 2, y: size.y / 2, z: size.z / 2 };
5275
6226
  let colliderDesc = ColliderDesc7.cuboid(half.x, half.y, half.z);
5276
6227
  colliderDesc.setSensor(true);
@@ -5283,7 +6234,7 @@ var ZoneBuilder = class extends EntityBuilder {
5283
6234
  return new ZylemZone(options);
5284
6235
  }
5285
6236
  };
5286
- var ZONE_TYPE = Symbol("Zone");
6237
+ var ZONE_TYPE = /* @__PURE__ */ Symbol("Zone");
5287
6238
  var ZylemZone = class extends GameEntity {
5288
6239
  static type = ZONE_TYPE;
5289
6240
  _enteredZone = /* @__PURE__ */ new Map();
@@ -5380,7 +6331,7 @@ init_actor();
5380
6331
  init_entity();
5381
6332
  init_builder();
5382
6333
  init_create();
5383
- import { Color as Color14, Group as Group7, Sprite as ThreeSprite3, SpriteMaterial as SpriteMaterial3, CanvasTexture as CanvasTexture2, LinearFilter as LinearFilter2, Vector2 as Vector210, ClampToEdgeWrapping as ClampToEdgeWrapping2, ShaderMaterial as ShaderMaterial4, Mesh as Mesh5, PlaneGeometry as PlaneGeometry2, Vector3 as Vector319 } from "three";
6334
+ import { Color as Color15, Group as Group7, Sprite as ThreeSprite3, SpriteMaterial as SpriteMaterial3, CanvasTexture as CanvasTexture2, LinearFilter as LinearFilter2, Vector2 as Vector211, ClampToEdgeWrapping as ClampToEdgeWrapping2, ShaderMaterial as ShaderMaterial4, Mesh as Mesh5, PlaneGeometry as PlaneGeometry2, Vector3 as Vector321 } from "three";
5384
6335
  var rectDefaults = {
5385
6336
  position: void 0,
5386
6337
  width: 120,
@@ -5391,16 +6342,16 @@ var rectDefaults = {
5391
6342
  radius: 0,
5392
6343
  padding: 0,
5393
6344
  stickToViewport: true,
5394
- screenPosition: new Vector210(24, 24),
6345
+ screenPosition: new Vector211(24, 24),
5395
6346
  zDistance: 1,
5396
- anchor: new Vector210(0, 0)
6347
+ anchor: new Vector211(0, 0)
5397
6348
  };
5398
6349
  var RectBuilder = class extends EntityBuilder {
5399
6350
  createEntity(options) {
5400
6351
  return new ZylemRect(options);
5401
6352
  }
5402
6353
  };
5403
- var RECT_TYPE = Symbol("Rect");
6354
+ var RECT_TYPE = /* @__PURE__ */ Symbol("Rect");
5404
6355
  var ZylemRect = class _ZylemRect extends GameEntity {
5405
6356
  static type = RECT_TYPE;
5406
6357
  _sprite = null;
@@ -5416,10 +6367,8 @@ var ZylemRect = class _ZylemRect extends GameEntity {
5416
6367
  this.options = { ...rectDefaults, ...options };
5417
6368
  this.group = new Group7();
5418
6369
  this.createSprite();
5419
- this.lifeCycleDelegate = {
5420
- setup: [this.rectSetup.bind(this)],
5421
- update: [this.rectUpdate.bind(this)]
5422
- };
6370
+ this.prependSetup(this.rectSetup.bind(this));
6371
+ this.prependUpdate(this.rectUpdate.bind(this));
5423
6372
  }
5424
6373
  createSprite() {
5425
6374
  this._canvas = document.createElement("canvas");
@@ -5510,7 +6459,7 @@ var ZylemRect = class _ZylemRect extends GameEntity {
5510
6459
  }
5511
6460
  toCssColor(color) {
5512
6461
  if (typeof color === "string") return color;
5513
- const c = color instanceof Color14 ? color : new Color14(color);
6462
+ const c = color instanceof Color15 ? color : new Color15(color);
5514
6463
  return `#${c.getHexString()}`;
5515
6464
  }
5516
6465
  rectSetup(params) {
@@ -5544,10 +6493,10 @@ var ZylemRect = class _ZylemRect extends GameEntity {
5544
6493
  const desiredW = Math.max(2, Math.floor(width));
5545
6494
  const desiredH = Math.max(2, Math.floor(height));
5546
6495
  const changed = desiredW !== (this.options.width ?? 0) || desiredH !== (this.options.height ?? 0);
5547
- this.options.screenPosition = new Vector210(Math.floor(x), Math.floor(y));
6496
+ this.options.screenPosition = new Vector211(Math.floor(x), Math.floor(y));
5548
6497
  this.options.width = desiredW;
5549
6498
  this.options.height = desiredH;
5550
- this.options.anchor = new Vector210(0, 0);
6499
+ this.options.anchor = new Vector211(0, 0);
5551
6500
  if (changed) {
5552
6501
  this.redrawRect();
5553
6502
  }
@@ -5563,8 +6512,8 @@ var ZylemRect = class _ZylemRect extends GameEntity {
5563
6512
  const dom = this._cameraRef.renderer.domElement;
5564
6513
  const width = dom.clientWidth;
5565
6514
  const height = dom.clientHeight;
5566
- const px = (this.options.screenPosition ?? new Vector210(24, 24)).x;
5567
- const py = (this.options.screenPosition ?? new Vector210(24, 24)).y;
6515
+ const px = (this.options.screenPosition ?? new Vector211(24, 24)).x;
6516
+ const py = (this.options.screenPosition ?? new Vector211(24, 24)).y;
5568
6517
  const zDist = Math.max(1e-3, this.options.zDistance ?? 1);
5569
6518
  let worldHalfW = 1;
5570
6519
  let worldHalfH = 1;
@@ -5595,7 +6544,7 @@ var ZylemRect = class _ZylemRect extends GameEntity {
5595
6544
  this._sprite.scale.set(scaleX, scaleY, 1);
5596
6545
  if (this._mesh) this._mesh.scale.set(scaleX, scaleY, 1);
5597
6546
  }
5598
- const anchor = this.options.anchor ?? new Vector210(0, 0);
6547
+ const anchor = this.options.anchor ?? new Vector211(0, 0);
5599
6548
  const ax = Math.min(100, Math.max(0, anchor.x)) / 100;
5600
6549
  const ay = Math.min(100, Math.max(0, anchor.y)) / 100;
5601
6550
  const offsetX = (0.5 - ax) * scaleX;
@@ -5619,8 +6568,8 @@ var ZylemRect = class _ZylemRect extends GameEntity {
5619
6568
  }
5620
6569
  if (bounds.world) {
5621
6570
  const { left, right, top, bottom, z = 0 } = bounds.world;
5622
- const tl = this.worldToScreen(new Vector319(left, top, z));
5623
- const br = this.worldToScreen(new Vector319(right, bottom, z));
6571
+ const tl = this.worldToScreen(new Vector321(left, top, z));
6572
+ const br = this.worldToScreen(new Vector321(right, bottom, z));
5624
6573
  const x = Math.min(tl.x, br.x);
5625
6574
  const y = Math.min(tl.y, br.y);
5626
6575
  const width = Math.abs(br.x - tl.x);
@@ -6089,41 +7038,41 @@ function movementSequence2D(opts, onStep) {
6089
7038
  }
6090
7039
 
6091
7040
  // src/lib/actions/capabilities/moveable.ts
6092
- import { Vector3 as Vector320 } from "three";
7041
+ import { Vector3 as Vector322 } from "three";
6093
7042
  function moveX(entity, delta) {
6094
7043
  if (!entity.body) return;
6095
7044
  const currentVelocity = entity.body.linvel();
6096
- const newVelocity = new Vector320(delta, currentVelocity.y, currentVelocity.z);
7045
+ const newVelocity = new Vector322(delta, currentVelocity.y, currentVelocity.z);
6097
7046
  entity.body.setLinvel(newVelocity, true);
6098
7047
  }
6099
7048
  function moveY(entity, delta) {
6100
7049
  if (!entity.body) return;
6101
7050
  const currentVelocity = entity.body.linvel();
6102
- const newVelocity = new Vector320(currentVelocity.x, delta, currentVelocity.z);
7051
+ const newVelocity = new Vector322(currentVelocity.x, delta, currentVelocity.z);
6103
7052
  entity.body.setLinvel(newVelocity, true);
6104
7053
  }
6105
7054
  function moveZ(entity, delta) {
6106
7055
  if (!entity.body) return;
6107
7056
  const currentVelocity = entity.body.linvel();
6108
- const newVelocity = new Vector320(currentVelocity.x, currentVelocity.y, delta);
7057
+ const newVelocity = new Vector322(currentVelocity.x, currentVelocity.y, delta);
6109
7058
  entity.body.setLinvel(newVelocity, true);
6110
7059
  }
6111
7060
  function moveXY(entity, deltaX, deltaY) {
6112
7061
  if (!entity.body) return;
6113
7062
  const currentVelocity = entity.body.linvel();
6114
- const newVelocity = new Vector320(deltaX, deltaY, currentVelocity.z);
7063
+ const newVelocity = new Vector322(deltaX, deltaY, currentVelocity.z);
6115
7064
  entity.body.setLinvel(newVelocity, true);
6116
7065
  }
6117
7066
  function moveXZ(entity, deltaX, deltaZ) {
6118
7067
  if (!entity.body) return;
6119
7068
  const currentVelocity = entity.body.linvel();
6120
- const newVelocity = new Vector320(deltaX, currentVelocity.y, deltaZ);
7069
+ const newVelocity = new Vector322(deltaX, currentVelocity.y, deltaZ);
6121
7070
  entity.body.setLinvel(newVelocity, true);
6122
7071
  }
6123
7072
  function move(entity, vector) {
6124
7073
  if (!entity.body) return;
6125
7074
  const currentVelocity = entity.body.linvel();
6126
- const newVelocity = new Vector320(
7075
+ const newVelocity = new Vector322(
6127
7076
  currentVelocity.x + vector.x,
6128
7077
  currentVelocity.y + vector.y,
6129
7078
  currentVelocity.z + vector.z
@@ -6132,7 +7081,7 @@ function move(entity, vector) {
6132
7081
  }
6133
7082
  function resetVelocity(entity) {
6134
7083
  if (!entity.body) return;
6135
- entity.body.setLinvel(new Vector320(0, 0, 0), true);
7084
+ entity.body.setLinvel(new Vector322(0, 0, 0), true);
6136
7085
  entity.body.setLinearDamping(5);
6137
7086
  }
6138
7087
  function moveForwardXY(entity, delta, rotation2DAngle) {
@@ -6262,14 +7211,14 @@ function makeMoveable(entity) {
6262
7211
  }
6263
7212
 
6264
7213
  // src/lib/actions/capabilities/rotatable.ts
6265
- import { Euler as Euler3, Vector3 as Vector321, MathUtils, Quaternion as Quaternion4 } from "three";
7214
+ import { Euler as Euler3, Vector3 as Vector323, MathUtils, Quaternion as Quaternion5 } from "three";
6266
7215
  function rotateInDirection(entity, moveVector) {
6267
7216
  if (!entity.body) return;
6268
7217
  const rotate = Math.atan2(-moveVector.x, moveVector.z);
6269
7218
  rotateYEuler(entity, rotate);
6270
7219
  }
6271
7220
  function rotateYEuler(entity, amount) {
6272
- rotateEuler(entity, new Vector321(0, -amount, 0));
7221
+ rotateEuler(entity, new Vector323(0, -amount, 0));
6273
7222
  }
6274
7223
  function rotateEuler(entity, rotation2) {
6275
7224
  if (!entity.group) return;
@@ -6317,7 +7266,7 @@ function setRotationDegreesZ(entity, z) {
6317
7266
  }
6318
7267
  function setRotation(entity, x, y, z) {
6319
7268
  if (!entity.body) return;
6320
- const quat = new Quaternion4().setFromEuler(new Euler3(x, y, z));
7269
+ const quat = new Quaternion5().setFromEuler(new Euler3(x, y, z));
6321
7270
  entity.body.setRotation({ w: quat.w, x: quat.x, y: quat.y, z: quat.z }, true);
6322
7271
  }
6323
7272
  function setRotationDegrees(entity, x, y, z) {
@@ -6515,10 +7464,13 @@ function variableChanges(keys, callback) {
6515
7464
  // src/api/main.ts
6516
7465
  init_game_state();
6517
7466
  init_stage_state();
7467
+ init_debug_state();
6518
7468
 
6519
7469
  // src/web-components/zylem-game.ts
7470
+ init_debug_state();
6520
7471
  var ZylemGameElement = class extends HTMLElement {
6521
7472
  _game = null;
7473
+ _state = {};
6522
7474
  container;
6523
7475
  constructor() {
6524
7476
  super();
@@ -6530,6 +7482,9 @@ var ZylemGameElement = class extends HTMLElement {
6530
7482
  this.shadowRoot.appendChild(this.container);
6531
7483
  }
6532
7484
  set game(game) {
7485
+ if (this._game) {
7486
+ this._game.dispose();
7487
+ }
6533
7488
  this._game = game;
6534
7489
  game.options.push({ container: this.container });
6535
7490
  game.start();
@@ -6537,6 +7492,35 @@ var ZylemGameElement = class extends HTMLElement {
6537
7492
  get game() {
6538
7493
  return this._game;
6539
7494
  }
7495
+ set state(value) {
7496
+ this._state = value;
7497
+ this.syncDebugState();
7498
+ this.syncToolbarState();
7499
+ }
7500
+ get state() {
7501
+ return this._state;
7502
+ }
7503
+ /**
7504
+ * Sync the web component's state with the game-lib's internal debug state
7505
+ */
7506
+ syncDebugState() {
7507
+ const debugFlag = this._state.gameState?.debugFlag;
7508
+ if (debugFlag !== void 0) {
7509
+ debugState.enabled = debugFlag;
7510
+ }
7511
+ }
7512
+ /**
7513
+ * Sync toolbar state with game-lib's debug state
7514
+ */
7515
+ syncToolbarState() {
7516
+ const { tool, paused } = this._state.toolbarState ?? {};
7517
+ if (tool !== void 0) {
7518
+ setDebugTool(tool);
7519
+ }
7520
+ if (paused !== void 0) {
7521
+ setPaused(paused);
7522
+ }
7523
+ }
6540
7524
  disconnectedCallback() {
6541
7525
  if (this._game) {
6542
7526
  this._game.dispose();
@@ -6545,22 +7529,36 @@ var ZylemGameElement = class extends HTMLElement {
6545
7529
  }
6546
7530
  };
6547
7531
  customElements.define("zylem-game", ZylemGameElement);
7532
+
7533
+ // src/lib/types/entity-type-map.ts
7534
+ init_actor();
6548
7535
  export {
7536
+ ACTOR_TYPE,
7537
+ BOX_TYPE,
6549
7538
  Game,
6550
7539
  Howl,
7540
+ PLANE_TYPE,
6551
7541
  Perspectives,
6552
7542
  RAPIER2 as RAPIER,
7543
+ RECT_TYPE,
7544
+ SPHERE_TYPE,
7545
+ SPRITE_TYPE,
7546
+ StageManager,
7547
+ TEXT_TYPE,
6553
7548
  THREE2 as THREE,
7549
+ ZONE_TYPE,
6554
7550
  ZylemBox,
6555
7551
  ZylemGameElement,
6556
7552
  actor,
6557
7553
  boundary2d,
6558
7554
  box,
6559
7555
  camera,
7556
+ clearGlobalSubscriptions,
6560
7557
  createGame,
6561
7558
  createGlobal,
6562
7559
  createStage,
6563
7560
  createVariable,
7561
+ debugState,
6564
7562
  destroy,
6565
7563
  entitySpawner,
6566
7564
  gameConfig,
@@ -6588,10 +7586,13 @@ export {
6588
7586
  ricochetSound,
6589
7587
  rotatable,
6590
7588
  rotateInDirection,
7589
+ setDebugTool,
6591
7590
  setGlobal,
7591
+ setPaused,
6592
7592
  setVariable,
6593
7593
  sphere,
6594
7594
  sprite,
7595
+ stageState2 as stageState,
6595
7596
  text,
6596
7597
  variableChange,
6597
7598
  variableChanges,