@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/core.js CHANGED
@@ -9,6 +9,18 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/lib/core/utility/path-utils.ts
12
+ function getByPath(obj, path) {
13
+ if (!path) return void 0;
14
+ const keys = path.split(".");
15
+ let current = obj;
16
+ for (const key of keys) {
17
+ if (current == null || typeof current !== "object") {
18
+ return void 0;
19
+ }
20
+ current = current[key];
21
+ }
22
+ return current;
23
+ }
12
24
  function setByPath(obj, path, value) {
13
25
  if (!path) return;
14
26
  const keys = path.split(".");
@@ -28,10 +40,95 @@ var init_path_utils = __esm({
28
40
  }
29
41
  });
30
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
+
31
91
  // src/lib/game/game-state.ts
32
92
  import { proxy, subscribe } from "valtio/vanilla";
33
93
  function setGlobal(path, value) {
94
+ const previousValue = getByPath(state.globals, path);
34
95
  setByPath(state.globals, path, value);
96
+ gameEventBus.emit("game:state:updated", {
97
+ path,
98
+ value,
99
+ previousValue
100
+ });
101
+ }
102
+ function onGlobalChange(path, callback) {
103
+ let previous = getByPath(state.globals, path);
104
+ const unsub = subscribe(state.globals, () => {
105
+ const current = getByPath(state.globals, path);
106
+ if (current !== previous) {
107
+ previous = current;
108
+ callback(current);
109
+ }
110
+ });
111
+ activeSubscriptions.add(unsub);
112
+ return () => {
113
+ unsub();
114
+ activeSubscriptions.delete(unsub);
115
+ };
116
+ }
117
+ function onGlobalChanges(paths, callback) {
118
+ let previousValues = paths.map((p) => getByPath(state.globals, p));
119
+ const unsub = subscribe(state.globals, () => {
120
+ const currentValues = paths.map((p) => getByPath(state.globals, p));
121
+ const hasChange = currentValues.some((val, i) => val !== previousValues[i]);
122
+ if (hasChange) {
123
+ previousValues = currentValues;
124
+ callback(currentValues);
125
+ }
126
+ });
127
+ activeSubscriptions.add(unsub);
128
+ return () => {
129
+ unsub();
130
+ activeSubscriptions.delete(unsub);
131
+ };
35
132
  }
36
133
  function getGlobals() {
37
134
  return state.globals;
@@ -44,16 +141,24 @@ function initGlobals(globals) {
44
141
  function resetGlobals() {
45
142
  state.globals = {};
46
143
  }
47
- var state;
144
+ function clearGlobalSubscriptions() {
145
+ for (const unsub of activeSubscriptions) {
146
+ unsub();
147
+ }
148
+ activeSubscriptions.clear();
149
+ }
150
+ var state, activeSubscriptions;
48
151
  var init_game_state = __esm({
49
152
  "src/lib/game/game-state.ts"() {
50
153
  "use strict";
51
154
  init_path_utils();
155
+ init_game_event_bus();
52
156
  state = proxy({
53
157
  id: "",
54
158
  globals: {},
55
159
  time: 0
56
160
  });
161
+ activeSubscriptions = /* @__PURE__ */ new Set();
57
162
  }
58
163
  });
59
164
 
@@ -119,21 +224,76 @@ var init_base_node = __esm({
119
224
  uuid = "";
120
225
  name = "";
121
226
  markedForRemoval = false;
122
- setup = () => {
123
- };
124
- loaded = () => {
125
- };
126
- update = () => {
127
- };
128
- destroy = () => {
129
- };
130
- cleanup = () => {
227
+ /**
228
+ * Lifecycle callback arrays - use onSetup(), onUpdate(), etc. to add callbacks
229
+ */
230
+ lifecycleCallbacks = {
231
+ setup: [],
232
+ loaded: [],
233
+ update: [],
234
+ destroy: [],
235
+ cleanup: []
131
236
  };
132
237
  constructor(args = []) {
133
238
  const options = args.filter((arg) => !(arg instanceof _BaseNode)).reduce((acc, opt) => ({ ...acc, ...opt }), {});
134
239
  this.options = options;
135
240
  this.uuid = nanoid();
136
241
  }
242
+ // ─────────────────────────────────────────────────────────────────────────────
243
+ // Fluent API for adding lifecycle callbacks
244
+ // ─────────────────────────────────────────────────────────────────────────────
245
+ /**
246
+ * Add setup callbacks to be executed in order during nodeSetup
247
+ */
248
+ onSetup(...callbacks) {
249
+ this.lifecycleCallbacks.setup.push(...callbacks);
250
+ return this;
251
+ }
252
+ /**
253
+ * Add loaded callbacks to be executed in order during nodeLoaded
254
+ */
255
+ onLoaded(...callbacks) {
256
+ this.lifecycleCallbacks.loaded.push(...callbacks);
257
+ return this;
258
+ }
259
+ /**
260
+ * Add update callbacks to be executed in order during nodeUpdate
261
+ */
262
+ onUpdate(...callbacks) {
263
+ this.lifecycleCallbacks.update.push(...callbacks);
264
+ return this;
265
+ }
266
+ /**
267
+ * Add destroy callbacks to be executed in order during nodeDestroy
268
+ */
269
+ onDestroy(...callbacks) {
270
+ this.lifecycleCallbacks.destroy.push(...callbacks);
271
+ return this;
272
+ }
273
+ /**
274
+ * Add cleanup callbacks to be executed in order during nodeCleanup
275
+ */
276
+ onCleanup(...callbacks) {
277
+ this.lifecycleCallbacks.cleanup.push(...callbacks);
278
+ return this;
279
+ }
280
+ /**
281
+ * Prepend setup callbacks (run before existing ones)
282
+ */
283
+ prependSetup(...callbacks) {
284
+ this.lifecycleCallbacks.setup.unshift(...callbacks);
285
+ return this;
286
+ }
287
+ /**
288
+ * Prepend update callbacks (run before existing ones)
289
+ */
290
+ prependUpdate(...callbacks) {
291
+ this.lifecycleCallbacks.update.unshift(...callbacks);
292
+ return this;
293
+ }
294
+ // ─────────────────────────────────────────────────────────────────────────────
295
+ // Tree structure
296
+ // ─────────────────────────────────────────────────────────────────────────────
137
297
  setParent(parent) {
138
298
  this.parent = parent;
139
299
  }
@@ -157,6 +317,9 @@ var init_base_node = __esm({
157
317
  isComposite() {
158
318
  return this.children.length > 0;
159
319
  }
320
+ // ─────────────────────────────────────────────────────────────────────────────
321
+ // Node lifecycle execution - runs internal + callback arrays
322
+ // ─────────────────────────────────────────────────────────────────────────────
160
323
  nodeSetup(params) {
161
324
  if (DEBUG_FLAG) {
162
325
  }
@@ -164,8 +327,8 @@ var init_base_node = __esm({
164
327
  if (typeof this._setup === "function") {
165
328
  this._setup(params);
166
329
  }
167
- if (this.setup) {
168
- this.setup(params);
330
+ for (const callback of this.lifecycleCallbacks.setup) {
331
+ callback(params);
169
332
  }
170
333
  this.children.forEach((child) => child.nodeSetup(params));
171
334
  }
@@ -176,21 +339,40 @@ var init_base_node = __esm({
176
339
  if (typeof this._update === "function") {
177
340
  this._update(params);
178
341
  }
179
- if (this.update) {
180
- this.update(params);
342
+ for (const callback of this.lifecycleCallbacks.update) {
343
+ callback(params);
181
344
  }
182
345
  this.children.forEach((child) => child.nodeUpdate(params));
183
346
  }
184
347
  nodeDestroy(params) {
185
348
  this.children.forEach((child) => child.nodeDestroy(params));
186
- if (this.destroy) {
187
- this.destroy(params);
349
+ for (const callback of this.lifecycleCallbacks.destroy) {
350
+ callback(params);
188
351
  }
189
352
  if (typeof this._destroy === "function") {
190
353
  this._destroy(params);
191
354
  }
192
355
  this.markedForRemoval = true;
193
356
  }
357
+ async nodeLoaded(params) {
358
+ if (typeof this._loaded === "function") {
359
+ await this._loaded(params);
360
+ }
361
+ for (const callback of this.lifecycleCallbacks.loaded) {
362
+ callback(params);
363
+ }
364
+ }
365
+ async nodeCleanup(params) {
366
+ for (const callback of this.lifecycleCallbacks.cleanup) {
367
+ callback(params);
368
+ }
369
+ if (typeof this._cleanup === "function") {
370
+ await this._cleanup(params);
371
+ }
372
+ }
373
+ // ─────────────────────────────────────────────────────────────────────────────
374
+ // Options
375
+ // ─────────────────────────────────────────────────────────────────────────────
194
376
  getOptions() {
195
377
  return this.options;
196
378
  }
@@ -206,57 +388,54 @@ import {
206
388
  defineSystem,
207
389
  defineQuery,
208
390
  defineComponent,
209
- Types
391
+ Types,
392
+ removeQuery
210
393
  } from "bitecs";
211
394
  import { Quaternion } from "three";
212
395
  function createTransformSystem(stage) {
213
- const transformQuery = defineQuery([position, rotation]);
396
+ const queryTerms = [position, rotation];
397
+ const transformQuery = defineQuery(queryTerms);
214
398
  const stageEntities = stage._childrenMap;
215
- return defineSystem((world) => {
399
+ const system = defineSystem((world) => {
216
400
  const entities = transformQuery(world);
217
401
  if (stageEntities === void 0) {
218
402
  return world;
219
403
  }
220
- ;
221
- for (const [key, value] of stageEntities) {
222
- const id = entities[key];
223
- const stageEntity = value;
224
- if (stageEntity === void 0 || !stageEntity?.body || stageEntity.markedForRemoval) {
404
+ for (const [key, stageEntity] of stageEntities) {
405
+ if (!stageEntity?.body || stageEntity.markedForRemoval) {
225
406
  continue;
226
407
  }
227
- const { x, y, z } = stageEntity.body.translation();
228
- position.x[id] = x;
229
- position.y[id] = y;
230
- position.z[id] = z;
231
- if (stageEntity.group) {
232
- stageEntity.group.position.set(position.x[id], position.y[id], position.z[id]);
233
- } else if (stageEntity.mesh) {
234
- stageEntity.mesh.position.set(position.x[id], position.y[id], position.z[id]);
408
+ const id = entities[key];
409
+ const body = stageEntity.body;
410
+ const target = stageEntity.group ?? stageEntity.mesh;
411
+ const translation = body.translation();
412
+ position.x[id] = translation.x;
413
+ position.y[id] = translation.y;
414
+ position.z[id] = translation.z;
415
+ if (target) {
416
+ target.position.set(translation.x, translation.y, translation.z);
235
417
  }
236
418
  if (stageEntity.controlledRotation) {
237
419
  continue;
238
420
  }
239
- const { x: rx, y: ry, z: rz, w: rw } = stageEntity.body.rotation();
240
- rotation.x[id] = rx;
241
- rotation.y[id] = ry;
242
- rotation.z[id] = rz;
243
- rotation.w[id] = rw;
244
- const newRotation = new Quaternion(
245
- rotation.x[id],
246
- rotation.y[id],
247
- rotation.z[id],
248
- rotation.w[id]
249
- );
250
- if (stageEntity.group) {
251
- stageEntity.group.setRotationFromQuaternion(newRotation);
252
- } else if (stageEntity.mesh) {
253
- stageEntity.mesh.setRotationFromQuaternion(newRotation);
421
+ const rot = body.rotation();
422
+ rotation.x[id] = rot.x;
423
+ rotation.y[id] = rot.y;
424
+ rotation.z[id] = rot.z;
425
+ rotation.w[id] = rot.w;
426
+ if (target) {
427
+ _tempQuaternion.set(rot.x, rot.y, rot.z, rot.w);
428
+ target.setRotationFromQuaternion(_tempQuaternion);
254
429
  }
255
430
  }
256
431
  return world;
257
432
  });
433
+ const destroy = (world) => {
434
+ removeQuery(world, transformQuery);
435
+ };
436
+ return { system, destroy };
258
437
  }
259
- var position, rotation, scale;
438
+ var position, rotation, scale, _tempQuaternion;
260
439
  var init_transformable_system = __esm({
261
440
  "src/lib/systems/transformable.system.ts"() {
262
441
  "use strict";
@@ -276,6 +455,7 @@ var init_transformable_system = __esm({
276
455
  y: Types.f32,
277
456
  z: Types.f32
278
457
  });
458
+ _tempQuaternion = new Quaternion();
279
459
  }
280
460
  });
281
461
 
@@ -299,11 +479,6 @@ var init_entity = __esm({
299
479
  custom = {};
300
480
  debugInfo = {};
301
481
  debugMaterial;
302
- lifeCycleDelegate = {
303
- setup: [],
304
- update: [],
305
- destroy: []
306
- };
307
482
  collisionDelegate = {
308
483
  collision: []
309
484
  };
@@ -328,67 +503,40 @@ var init_entity = __esm({
328
503
  this.name = this.options.name || "";
329
504
  return this;
330
505
  }
331
- onSetup(...callbacks) {
332
- const combineCallbacks = [...this.lifeCycleDelegate.setup ?? [], ...callbacks];
333
- this.lifeCycleDelegate = {
334
- ...this.lifeCycleDelegate,
335
- setup: combineCallbacks
336
- };
337
- return this;
338
- }
339
- onUpdate(...callbacks) {
340
- const combineCallbacks = [...this.lifeCycleDelegate.update ?? [], ...callbacks];
341
- this.lifeCycleDelegate = {
342
- ...this.lifeCycleDelegate,
343
- update: combineCallbacks
344
- };
345
- return this;
346
- }
347
- onDestroy(...callbacks) {
348
- this.lifeCycleDelegate = {
349
- ...this.lifeCycleDelegate,
350
- destroy: callbacks.length > 0 ? callbacks : void 0
351
- };
352
- return this;
353
- }
506
+ /**
507
+ * Add collision callbacks
508
+ */
354
509
  onCollision(...callbacks) {
355
- this.collisionDelegate = {
356
- collision: callbacks.length > 0 ? callbacks : void 0
357
- };
510
+ const existing = this.collisionDelegate.collision ?? [];
511
+ this.collisionDelegate.collision = [...existing, ...callbacks];
358
512
  return this;
359
513
  }
514
+ /**
515
+ * Entity-specific setup - runs behavior callbacks
516
+ * (User callbacks are handled by BaseNode's lifecycleCallbacks.setup)
517
+ */
360
518
  _setup(params) {
361
519
  this.behaviorCallbackMap.setup.forEach((callback) => {
362
520
  callback({ ...params, me: this });
363
521
  });
364
- if (this.lifeCycleDelegate.setup?.length) {
365
- const callbacks = this.lifeCycleDelegate.setup;
366
- callbacks.forEach((callback) => {
367
- callback({ ...params, me: this });
368
- });
369
- }
370
522
  }
371
523
  async _loaded(_params) {
372
524
  }
525
+ /**
526
+ * Entity-specific update - updates materials and runs behavior callbacks
527
+ * (User callbacks are handled by BaseNode's lifecycleCallbacks.update)
528
+ */
373
529
  _update(params) {
374
530
  this.updateMaterials(params);
375
- if (this.lifeCycleDelegate.update?.length) {
376
- const callbacks = this.lifeCycleDelegate.update;
377
- callbacks.forEach((callback) => {
378
- callback({ ...params, me: this });
379
- });
380
- }
381
531
  this.behaviorCallbackMap.update.forEach((callback) => {
382
532
  callback({ ...params, me: this });
383
533
  });
384
534
  }
535
+ /**
536
+ * Entity-specific destroy - runs behavior callbacks
537
+ * (User callbacks are handled by BaseNode's lifecycleCallbacks.destroy)
538
+ */
385
539
  _destroy(params) {
386
- if (this.lifeCycleDelegate.destroy?.length) {
387
- const callbacks = this.lifeCycleDelegate.destroy;
388
- callbacks.forEach((callback) => {
389
- callback({ ...params, me: this });
390
- });
391
- }
392
540
  this.behaviorCallbackMap.destroy.forEach((callback) => {
393
541
  callback({ ...params, me: this });
394
542
  });
@@ -678,6 +826,23 @@ var init_animation = __esm({
678
826
  this._currentAction = action;
679
827
  this._currentKey = key;
680
828
  }
829
+ /**
830
+ * Dispose of all animation resources
831
+ */
832
+ dispose() {
833
+ Object.values(this._actions).forEach((action) => {
834
+ action.stop();
835
+ });
836
+ if (this._mixer) {
837
+ this._mixer.stopAllAction();
838
+ this._mixer.uncacheRoot(this.target);
839
+ this._mixer = null;
840
+ }
841
+ this._actions = {};
842
+ this._animations = [];
843
+ this._currentAction = null;
844
+ this._currentKey = "";
845
+ }
681
846
  get currentAnimationKey() {
682
847
  return this._currentKey;
683
848
  }
@@ -1093,7 +1258,7 @@ var init_actor = __esm({
1093
1258
  animations: [],
1094
1259
  models: []
1095
1260
  };
1096
- ACTOR_TYPE = Symbol("Actor");
1261
+ ACTOR_TYPE = /* @__PURE__ */ Symbol("Actor");
1097
1262
  ZylemActor = class extends GameEntity {
1098
1263
  static type = ACTOR_TYPE;
1099
1264
  _object = null;
@@ -1104,9 +1269,7 @@ var init_actor = __esm({
1104
1269
  constructor(options) {
1105
1270
  super();
1106
1271
  this.options = { ...actorDefaults, ...options };
1107
- this.lifeCycleDelegate = {
1108
- update: [this.actorUpdate.bind(this)]
1109
- };
1272
+ this.prependUpdate(this.actorUpdate.bind(this));
1110
1273
  this.controlledRotation = true;
1111
1274
  }
1112
1275
  async load() {
@@ -1126,6 +1289,34 @@ var init_actor = __esm({
1126
1289
  async actorUpdate(params) {
1127
1290
  this._animationDelegate?.update(params.delta);
1128
1291
  }
1292
+ /**
1293
+ * Clean up actor resources including animations, models, and groups
1294
+ */
1295
+ actorDestroy() {
1296
+ if (this._animationDelegate) {
1297
+ this._animationDelegate.dispose();
1298
+ this._animationDelegate = null;
1299
+ }
1300
+ if (this._object) {
1301
+ this._object.traverse((child) => {
1302
+ if (child.isMesh) {
1303
+ const mesh = child;
1304
+ mesh.geometry?.dispose();
1305
+ if (Array.isArray(mesh.material)) {
1306
+ mesh.material.forEach((m) => m.dispose());
1307
+ } else if (mesh.material) {
1308
+ mesh.material.dispose();
1309
+ }
1310
+ }
1311
+ });
1312
+ this._object = null;
1313
+ }
1314
+ if (this.group) {
1315
+ this.group.clear();
1316
+ this.group = null;
1317
+ }
1318
+ this._modelFileNames = [];
1319
+ }
1129
1320
  async loadModels() {
1130
1321
  if (this._modelFileNames.length === 0) return;
1131
1322
  const promises = this._modelFileNames.map((file) => this._assetLoader.loadFile(file));
@@ -1185,25 +1376,17 @@ var init_actor = __esm({
1185
1376
  }
1186
1377
  });
1187
1378
 
1188
- // src/lib/collision/collision-delegate.ts
1379
+ // src/lib/collision/world.ts
1380
+ import RAPIER from "@dimforge/rapier3d-compat";
1189
1381
  function isCollisionHandlerDelegate(obj) {
1190
1382
  return typeof obj?.handlePostCollision === "function" && typeof obj?.handleIntersectionEvent === "function";
1191
1383
  }
1192
- var init_collision_delegate = __esm({
1193
- "src/lib/collision/collision-delegate.ts"() {
1194
- "use strict";
1195
- }
1196
- });
1197
-
1198
- // src/lib/collision/world.ts
1199
- import RAPIER from "@dimforge/rapier3d-compat";
1200
1384
  var ZylemWorld;
1201
1385
  var init_world = __esm({
1202
1386
  "src/lib/collision/world.ts"() {
1203
1387
  "use strict";
1204
1388
  init_game_state();
1205
1389
  init_actor();
1206
- init_collision_delegate();
1207
1390
  ZylemWorld = class {
1208
1391
  type = "World";
1209
1392
  world;
@@ -1409,7 +1592,11 @@ var init_zylem_scene = __esm({
1409
1592
  * Setup camera with the scene
1410
1593
  */
1411
1594
  setupCamera(scene, camera) {
1412
- scene.add(camera.cameraRig);
1595
+ if (camera.cameraRig) {
1596
+ scene.add(camera.cameraRig);
1597
+ } else {
1598
+ scene.add(camera.camera);
1599
+ }
1413
1600
  camera.setup(scene);
1414
1601
  }
1415
1602
  /**
@@ -1470,7 +1657,7 @@ var init_zylem_scene = __esm({
1470
1657
 
1471
1658
  // src/lib/stage/stage-state.ts
1472
1659
  import { Color as Color5, Vector3 as Vector35 } from "three";
1473
- import { proxy as proxy3, subscribe as subscribe2 } from "valtio/vanilla";
1660
+ import { proxy as proxy3, subscribe as subscribe3 } from "valtio/vanilla";
1474
1661
  function clearVariables(target) {
1475
1662
  variableProxyStore.delete(target);
1476
1663
  }
@@ -1558,59 +1745,346 @@ var init_lifecycle_base = __esm({
1558
1745
  }
1559
1746
  });
1560
1747
 
1561
- // src/lib/camera/perspective.ts
1562
- var Perspectives;
1563
- var init_perspective = __esm({
1564
- "src/lib/camera/perspective.ts"() {
1565
- "use strict";
1566
- Perspectives = {
1567
- FirstPerson: "first-person",
1568
- ThirdPerson: "third-person",
1569
- Isometric: "isometric",
1570
- Flat2D: "flat-2d",
1571
- Fixed2D: "fixed-2d"
1572
- };
1573
- }
1574
- });
1575
-
1576
- // src/lib/camera/third-person.ts
1577
- import { Vector3 as Vector37 } from "three";
1578
- var ThirdPersonCamera;
1579
- var init_third_person = __esm({
1580
- "src/lib/camera/third-person.ts"() {
1748
+ // src/lib/stage/debug-entity-cursor.ts
1749
+ import {
1750
+ Box3,
1751
+ BoxGeometry,
1752
+ Color as Color7,
1753
+ EdgesGeometry,
1754
+ Group as Group4,
1755
+ LineBasicMaterial,
1756
+ LineSegments,
1757
+ Mesh as Mesh3,
1758
+ MeshBasicMaterial,
1759
+ Vector3 as Vector37
1760
+ } from "three";
1761
+ var DebugEntityCursor;
1762
+ var init_debug_entity_cursor = __esm({
1763
+ "src/lib/stage/debug-entity-cursor.ts"() {
1581
1764
  "use strict";
1582
- ThirdPersonCamera = class {
1583
- distance;
1584
- screenResolution = null;
1585
- renderer = null;
1586
- scene = null;
1587
- cameraRef = null;
1588
- constructor() {
1589
- this.distance = new Vector37(0, 5, 8);
1590
- }
1591
- /**
1592
- * Setup the third person camera controller
1593
- */
1594
- setup(params) {
1595
- const { screenResolution, renderer, scene, camera } = params;
1596
- this.screenResolution = screenResolution;
1597
- this.renderer = renderer;
1765
+ DebugEntityCursor = class {
1766
+ scene;
1767
+ container;
1768
+ fillMesh;
1769
+ edgeLines;
1770
+ currentColor = new Color7(65280);
1771
+ bbox = new Box3();
1772
+ size = new Vector37();
1773
+ center = new Vector37();
1774
+ constructor(scene) {
1598
1775
  this.scene = scene;
1599
- this.cameraRef = camera;
1776
+ const initialGeometry = new BoxGeometry(1, 1, 1);
1777
+ this.fillMesh = new Mesh3(
1778
+ initialGeometry,
1779
+ new MeshBasicMaterial({
1780
+ color: this.currentColor,
1781
+ transparent: true,
1782
+ opacity: 0.12,
1783
+ depthWrite: false
1784
+ })
1785
+ );
1786
+ const edges = new EdgesGeometry(initialGeometry);
1787
+ this.edgeLines = new LineSegments(
1788
+ edges,
1789
+ new LineBasicMaterial({ color: this.currentColor, linewidth: 1 })
1790
+ );
1791
+ this.container = new Group4();
1792
+ this.container.name = "DebugEntityCursor";
1793
+ this.container.add(this.fillMesh);
1794
+ this.container.add(this.edgeLines);
1795
+ this.container.visible = false;
1796
+ this.scene.add(this.container);
1797
+ }
1798
+ setColor(color) {
1799
+ this.currentColor.set(color);
1800
+ this.fillMesh.material.color.set(this.currentColor);
1801
+ this.edgeLines.material.color.set(this.currentColor);
1600
1802
  }
1601
1803
  /**
1602
- * Update the third person camera
1804
+ * Update the cursor to enclose the provided Object3D using a world-space AABB.
1603
1805
  */
1604
- update(delta) {
1605
- if (!this.cameraRef.target) {
1806
+ updateFromObject(object) {
1807
+ if (!object) {
1808
+ this.hide();
1606
1809
  return;
1607
1810
  }
1608
- const desiredCameraPosition = this.cameraRef.target.group.position.clone().add(this.distance);
1609
- this.cameraRef.camera.position.lerp(desiredCameraPosition, 0.1);
1610
- this.cameraRef.camera.lookAt(this.cameraRef.target.group.position);
1611
- }
1612
- /**
1613
- * Handle resize events
1811
+ this.bbox.setFromObject(object);
1812
+ if (!isFinite(this.bbox.min.x) || !isFinite(this.bbox.max.x)) {
1813
+ this.hide();
1814
+ return;
1815
+ }
1816
+ this.bbox.getSize(this.size);
1817
+ this.bbox.getCenter(this.center);
1818
+ const newGeom = new BoxGeometry(
1819
+ Math.max(this.size.x, 1e-6),
1820
+ Math.max(this.size.y, 1e-6),
1821
+ Math.max(this.size.z, 1e-6)
1822
+ );
1823
+ this.fillMesh.geometry.dispose();
1824
+ this.fillMesh.geometry = newGeom;
1825
+ const newEdges = new EdgesGeometry(newGeom);
1826
+ this.edgeLines.geometry.dispose();
1827
+ this.edgeLines.geometry = newEdges;
1828
+ this.container.position.copy(this.center);
1829
+ this.container.visible = true;
1830
+ }
1831
+ hide() {
1832
+ this.container.visible = false;
1833
+ }
1834
+ dispose() {
1835
+ this.scene.remove(this.container);
1836
+ this.fillMesh.geometry.dispose();
1837
+ this.fillMesh.material.dispose();
1838
+ this.edgeLines.geometry.dispose();
1839
+ this.edgeLines.material.dispose();
1840
+ }
1841
+ };
1842
+ }
1843
+ });
1844
+
1845
+ // src/lib/stage/stage-debug-delegate.ts
1846
+ import { Ray } from "@dimforge/rapier3d-compat";
1847
+ import { BufferAttribute, BufferGeometry as BufferGeometry3, LineBasicMaterial as LineBasicMaterial2, LineSegments as LineSegments2, Raycaster, Vector2 as Vector22 } from "three";
1848
+ var SELECT_TOOL_COLOR, DELETE_TOOL_COLOR, StageDebugDelegate;
1849
+ var init_stage_debug_delegate = __esm({
1850
+ "src/lib/stage/stage-debug-delegate.ts"() {
1851
+ "use strict";
1852
+ init_debug_state();
1853
+ init_debug_entity_cursor();
1854
+ SELECT_TOOL_COLOR = 2293538;
1855
+ DELETE_TOOL_COLOR = 16724787;
1856
+ StageDebugDelegate = class {
1857
+ stage;
1858
+ options;
1859
+ mouseNdc = new Vector22(-2, -2);
1860
+ raycaster = new Raycaster();
1861
+ isMouseDown = false;
1862
+ disposeFns = [];
1863
+ debugCursor = null;
1864
+ debugLines = null;
1865
+ constructor(stage, options) {
1866
+ this.stage = stage;
1867
+ this.options = {
1868
+ maxRayDistance: options?.maxRayDistance ?? 5e3,
1869
+ addEntityFactory: options?.addEntityFactory ?? null
1870
+ };
1871
+ if (this.stage.scene) {
1872
+ this.debugLines = new LineSegments2(
1873
+ new BufferGeometry3(),
1874
+ new LineBasicMaterial2({ vertexColors: true })
1875
+ );
1876
+ this.stage.scene.scene.add(this.debugLines);
1877
+ this.debugLines.visible = true;
1878
+ this.debugCursor = new DebugEntityCursor(this.stage.scene.scene);
1879
+ }
1880
+ this.attachDomListeners();
1881
+ }
1882
+ update() {
1883
+ if (!debugState.enabled) return;
1884
+ if (!this.stage.scene || !this.stage.world || !this.stage.cameraRef) return;
1885
+ const { world, cameraRef } = this.stage;
1886
+ if (this.debugLines) {
1887
+ const { vertices, colors } = world.world.debugRender();
1888
+ this.debugLines.geometry.setAttribute("position", new BufferAttribute(vertices, 3));
1889
+ this.debugLines.geometry.setAttribute("color", new BufferAttribute(colors, 4));
1890
+ }
1891
+ const tool = getDebugTool();
1892
+ const isCursorTool = tool === "select" || tool === "delete";
1893
+ this.raycaster.setFromCamera(this.mouseNdc, cameraRef.camera);
1894
+ const origin = this.raycaster.ray.origin.clone();
1895
+ const direction = this.raycaster.ray.direction.clone().normalize();
1896
+ const rapierRay = new Ray(
1897
+ { x: origin.x, y: origin.y, z: origin.z },
1898
+ { x: direction.x, y: direction.y, z: direction.z }
1899
+ );
1900
+ const hit = world.world.castRay(rapierRay, this.options.maxRayDistance, true);
1901
+ if (hit && isCursorTool) {
1902
+ const rigidBody = hit.collider?._parent;
1903
+ const hoveredUuid2 = rigidBody?.userData?.uuid;
1904
+ if (hoveredUuid2) {
1905
+ const entity = this.stage._debugMap.get(hoveredUuid2);
1906
+ if (entity) setHoveredEntity(entity);
1907
+ } else {
1908
+ resetHoveredEntity();
1909
+ }
1910
+ if (this.isMouseDown) {
1911
+ this.handleActionOnHit(hoveredUuid2 ?? null, origin, direction, hit.toi);
1912
+ }
1913
+ }
1914
+ this.isMouseDown = false;
1915
+ const hoveredUuid = getHoveredEntity();
1916
+ if (!hoveredUuid) {
1917
+ this.debugCursor?.hide();
1918
+ return;
1919
+ }
1920
+ const hoveredEntity = this.stage._debugMap.get(`${hoveredUuid}`);
1921
+ const targetObject = hoveredEntity?.group ?? hoveredEntity?.mesh ?? null;
1922
+ if (!targetObject) {
1923
+ this.debugCursor?.hide();
1924
+ return;
1925
+ }
1926
+ switch (tool) {
1927
+ case "select":
1928
+ this.debugCursor?.setColor(SELECT_TOOL_COLOR);
1929
+ break;
1930
+ case "delete":
1931
+ this.debugCursor?.setColor(DELETE_TOOL_COLOR);
1932
+ break;
1933
+ default:
1934
+ this.debugCursor?.setColor(16777215);
1935
+ break;
1936
+ }
1937
+ this.debugCursor?.updateFromObject(targetObject);
1938
+ }
1939
+ dispose() {
1940
+ this.disposeFns.forEach((fn) => fn());
1941
+ this.disposeFns = [];
1942
+ this.debugCursor?.dispose();
1943
+ if (this.debugLines && this.stage.scene) {
1944
+ this.stage.scene.scene.remove(this.debugLines);
1945
+ this.debugLines.geometry.dispose();
1946
+ this.debugLines.material.dispose();
1947
+ this.debugLines = null;
1948
+ }
1949
+ }
1950
+ handleActionOnHit(hoveredUuid, origin, direction, toi) {
1951
+ const tool = getDebugTool();
1952
+ switch (tool) {
1953
+ case "select": {
1954
+ if (hoveredUuid) {
1955
+ const entity = this.stage._debugMap.get(hoveredUuid);
1956
+ if (entity) setSelectedEntity(entity);
1957
+ }
1958
+ break;
1959
+ }
1960
+ case "delete": {
1961
+ if (hoveredUuid) {
1962
+ this.stage.removeEntityByUuid(hoveredUuid);
1963
+ }
1964
+ break;
1965
+ }
1966
+ case "scale": {
1967
+ if (!this.options.addEntityFactory) break;
1968
+ const hitPosition = origin.clone().add(direction.clone().multiplyScalar(toi));
1969
+ const newNode = this.options.addEntityFactory({ position: hitPosition });
1970
+ if (newNode) {
1971
+ Promise.resolve(newNode).then((node) => {
1972
+ if (node) this.stage.spawnEntity(node);
1973
+ }).catch(() => {
1974
+ });
1975
+ }
1976
+ break;
1977
+ }
1978
+ default:
1979
+ break;
1980
+ }
1981
+ }
1982
+ attachDomListeners() {
1983
+ const canvas = this.stage.cameraRef?.renderer.domElement ?? this.stage.scene?.zylemCamera.renderer.domElement;
1984
+ if (!canvas) return;
1985
+ const onMouseMove = (e) => {
1986
+ const rect = canvas.getBoundingClientRect();
1987
+ const x = (e.clientX - rect.left) / rect.width * 2 - 1;
1988
+ const y = -((e.clientY - rect.top) / rect.height * 2 - 1);
1989
+ this.mouseNdc.set(x, y);
1990
+ };
1991
+ const onMouseDown = (e) => {
1992
+ this.isMouseDown = true;
1993
+ };
1994
+ canvas.addEventListener("mousemove", onMouseMove);
1995
+ canvas.addEventListener("mousedown", onMouseDown);
1996
+ this.disposeFns.push(() => canvas.removeEventListener("mousemove", onMouseMove));
1997
+ this.disposeFns.push(() => canvas.removeEventListener("mousedown", onMouseDown));
1998
+ }
1999
+ };
2000
+ }
2001
+ });
2002
+
2003
+ // src/lib/stage/stage-camera-debug-delegate.ts
2004
+ import { subscribe as subscribe4 } from "valtio/vanilla";
2005
+ var StageCameraDebugDelegate;
2006
+ var init_stage_camera_debug_delegate = __esm({
2007
+ "src/lib/stage/stage-camera-debug-delegate.ts"() {
2008
+ "use strict";
2009
+ init_debug_state();
2010
+ StageCameraDebugDelegate = class {
2011
+ stage;
2012
+ constructor(stage) {
2013
+ this.stage = stage;
2014
+ }
2015
+ subscribe(listener) {
2016
+ const notify = () => listener(this.snapshot());
2017
+ notify();
2018
+ return subscribe4(debugState, notify);
2019
+ }
2020
+ resolveTarget(uuid) {
2021
+ const entity = this.stage._debugMap.get(uuid) || this.stage.world?.collisionMap.get(uuid) || null;
2022
+ const target = entity?.group ?? entity?.mesh ?? null;
2023
+ return target ?? null;
2024
+ }
2025
+ snapshot() {
2026
+ return {
2027
+ enabled: debugState.enabled,
2028
+ selected: debugState.selectedEntity ? [debugState.selectedEntity.uuid] : []
2029
+ };
2030
+ }
2031
+ };
2032
+ }
2033
+ });
2034
+
2035
+ // src/lib/camera/perspective.ts
2036
+ var Perspectives;
2037
+ var init_perspective = __esm({
2038
+ "src/lib/camera/perspective.ts"() {
2039
+ "use strict";
2040
+ Perspectives = {
2041
+ FirstPerson: "first-person",
2042
+ ThirdPerson: "third-person",
2043
+ Isometric: "isometric",
2044
+ Flat2D: "flat-2d",
2045
+ Fixed2D: "fixed-2d"
2046
+ };
2047
+ }
2048
+ });
2049
+
2050
+ // src/lib/camera/third-person.ts
2051
+ import { Vector3 as Vector39 } from "three";
2052
+ var ThirdPersonCamera;
2053
+ var init_third_person = __esm({
2054
+ "src/lib/camera/third-person.ts"() {
2055
+ "use strict";
2056
+ ThirdPersonCamera = class {
2057
+ distance;
2058
+ screenResolution = null;
2059
+ renderer = null;
2060
+ scene = null;
2061
+ cameraRef = null;
2062
+ constructor() {
2063
+ this.distance = new Vector39(0, 5, 8);
2064
+ }
2065
+ /**
2066
+ * Setup the third person camera controller
2067
+ */
2068
+ setup(params) {
2069
+ const { screenResolution, renderer, scene, camera } = params;
2070
+ this.screenResolution = screenResolution;
2071
+ this.renderer = renderer;
2072
+ this.scene = scene;
2073
+ this.cameraRef = camera;
2074
+ }
2075
+ /**
2076
+ * Update the third person camera
2077
+ */
2078
+ update(delta) {
2079
+ if (!this.cameraRef.target) {
2080
+ return;
2081
+ }
2082
+ const desiredCameraPosition = this.cameraRef.target.group.position.clone().add(this.distance);
2083
+ this.cameraRef.camera.position.lerp(desiredCameraPosition, 0.1);
2084
+ this.cameraRef.camera.lookAt(this.cameraRef.target.group.position);
2085
+ }
2086
+ /**
2087
+ * Handle resize events
1614
2088
  */
1615
2089
  resize(width, height) {
1616
2090
  if (this.screenResolution) {
@@ -1765,9 +2239,184 @@ var init_render_pass = __esm({
1765
2239
  }
1766
2240
  });
1767
2241
 
1768
- // src/lib/camera/zylem-camera.ts
1769
- import { PerspectiveCamera, Vector3 as Vector38, Object3D as Object3D4, OrthographicCamera, WebGLRenderer as WebGLRenderer3 } from "three";
2242
+ // src/lib/camera/camera-debug-delegate.ts
2243
+ import { Vector3 as Vector310 } from "three";
1770
2244
  import { OrbitControls } from "three/addons/controls/OrbitControls.js";
2245
+ var CameraOrbitController;
2246
+ var init_camera_debug_delegate = __esm({
2247
+ "src/lib/camera/camera-debug-delegate.ts"() {
2248
+ "use strict";
2249
+ CameraOrbitController = class {
2250
+ camera;
2251
+ domElement;
2252
+ orbitControls = null;
2253
+ orbitTarget = null;
2254
+ orbitTargetWorldPos = new Vector310();
2255
+ debugDelegate = null;
2256
+ debugUnsubscribe = null;
2257
+ debugStateSnapshot = { enabled: false, selected: [] };
2258
+ // Saved camera state for restoration when exiting debug mode
2259
+ savedCameraPosition = null;
2260
+ savedCameraQuaternion = null;
2261
+ savedCameraZoom = null;
2262
+ constructor(camera, domElement) {
2263
+ this.camera = camera;
2264
+ this.domElement = domElement;
2265
+ }
2266
+ /**
2267
+ * Check if debug mode is currently active (orbit controls enabled).
2268
+ */
2269
+ get isActive() {
2270
+ return this.debugStateSnapshot.enabled;
2271
+ }
2272
+ /**
2273
+ * Update orbit controls each frame.
2274
+ * Should be called from the camera's update loop.
2275
+ */
2276
+ update() {
2277
+ if (this.orbitControls && this.orbitTarget) {
2278
+ this.orbitTarget.getWorldPosition(this.orbitTargetWorldPos);
2279
+ this.orbitControls.target.copy(this.orbitTargetWorldPos);
2280
+ }
2281
+ this.orbitControls?.update();
2282
+ }
2283
+ /**
2284
+ * Attach a delegate to react to debug state changes.
2285
+ */
2286
+ setDebugDelegate(delegate) {
2287
+ if (this.debugDelegate === delegate) {
2288
+ return;
2289
+ }
2290
+ this.detachDebugDelegate();
2291
+ this.debugDelegate = delegate;
2292
+ if (!delegate) {
2293
+ this.applyDebugState({ enabled: false, selected: [] });
2294
+ return;
2295
+ }
2296
+ const unsubscribe = delegate.subscribe((state2) => {
2297
+ this.applyDebugState(state2);
2298
+ });
2299
+ this.debugUnsubscribe = () => {
2300
+ unsubscribe?.();
2301
+ };
2302
+ }
2303
+ /**
2304
+ * Clean up resources.
2305
+ */
2306
+ dispose() {
2307
+ this.disableOrbitControls();
2308
+ this.detachDebugDelegate();
2309
+ }
2310
+ /**
2311
+ * Get the current debug state snapshot.
2312
+ */
2313
+ get debugState() {
2314
+ return this.debugStateSnapshot;
2315
+ }
2316
+ applyDebugState(state2) {
2317
+ const wasEnabled = this.debugStateSnapshot.enabled;
2318
+ this.debugStateSnapshot = {
2319
+ enabled: state2.enabled,
2320
+ selected: [...state2.selected]
2321
+ };
2322
+ if (state2.enabled && !wasEnabled) {
2323
+ this.saveCameraState();
2324
+ this.enableOrbitControls();
2325
+ this.updateOrbitTargetFromSelection(state2.selected);
2326
+ } else if (!state2.enabled && wasEnabled) {
2327
+ this.orbitTarget = null;
2328
+ this.disableOrbitControls();
2329
+ this.restoreCameraState();
2330
+ } else if (state2.enabled) {
2331
+ this.updateOrbitTargetFromSelection(state2.selected);
2332
+ }
2333
+ }
2334
+ enableOrbitControls() {
2335
+ if (this.orbitControls) {
2336
+ return;
2337
+ }
2338
+ this.orbitControls = new OrbitControls(this.camera, this.domElement);
2339
+ this.orbitControls.enableDamping = true;
2340
+ this.orbitControls.dampingFactor = 0.05;
2341
+ this.orbitControls.screenSpacePanning = false;
2342
+ this.orbitControls.minDistance = 1;
2343
+ this.orbitControls.maxDistance = 500;
2344
+ this.orbitControls.maxPolarAngle = Math.PI / 2;
2345
+ this.orbitControls.target.set(0, 0, 0);
2346
+ }
2347
+ disableOrbitControls() {
2348
+ if (!this.orbitControls) {
2349
+ return;
2350
+ }
2351
+ this.orbitControls.dispose();
2352
+ this.orbitControls = null;
2353
+ }
2354
+ updateOrbitTargetFromSelection(selected) {
2355
+ if (!this.debugDelegate || selected.length === 0) {
2356
+ this.orbitTarget = null;
2357
+ if (this.orbitControls) {
2358
+ this.orbitControls.target.set(0, 0, 0);
2359
+ }
2360
+ return;
2361
+ }
2362
+ for (let i = selected.length - 1; i >= 0; i -= 1) {
2363
+ const uuid = selected[i];
2364
+ const targetObject = this.debugDelegate.resolveTarget(uuid);
2365
+ if (targetObject) {
2366
+ this.orbitTarget = targetObject;
2367
+ if (this.orbitControls) {
2368
+ targetObject.getWorldPosition(this.orbitTargetWorldPos);
2369
+ this.orbitControls.target.copy(this.orbitTargetWorldPos);
2370
+ }
2371
+ return;
2372
+ }
2373
+ }
2374
+ this.orbitTarget = null;
2375
+ }
2376
+ detachDebugDelegate() {
2377
+ if (this.debugUnsubscribe) {
2378
+ try {
2379
+ this.debugUnsubscribe();
2380
+ } catch {
2381
+ }
2382
+ }
2383
+ this.debugUnsubscribe = null;
2384
+ this.debugDelegate = null;
2385
+ }
2386
+ /**
2387
+ * Save camera position, rotation, and zoom before entering debug mode.
2388
+ */
2389
+ saveCameraState() {
2390
+ this.savedCameraPosition = this.camera.position.clone();
2391
+ this.savedCameraQuaternion = this.camera.quaternion.clone();
2392
+ if ("zoom" in this.camera) {
2393
+ this.savedCameraZoom = this.camera.zoom;
2394
+ }
2395
+ }
2396
+ /**
2397
+ * Restore camera position, rotation, and zoom when exiting debug mode.
2398
+ */
2399
+ restoreCameraState() {
2400
+ if (this.savedCameraPosition) {
2401
+ this.camera.position.copy(this.savedCameraPosition);
2402
+ this.savedCameraPosition = null;
2403
+ }
2404
+ if (this.savedCameraQuaternion) {
2405
+ this.camera.quaternion.copy(this.savedCameraQuaternion);
2406
+ this.savedCameraQuaternion = null;
2407
+ }
2408
+ if (this.savedCameraZoom !== null && "zoom" in this.camera) {
2409
+ this.camera.zoom = this.savedCameraZoom;
2410
+ this.camera.updateProjectionMatrix?.();
2411
+ this.savedCameraZoom = null;
2412
+ }
2413
+ }
2414
+ };
2415
+ }
2416
+ });
2417
+
2418
+ // src/lib/camera/zylem-camera.ts
2419
+ import { PerspectiveCamera, Vector3 as Vector311, Object3D as Object3D6, OrthographicCamera, WebGLRenderer as WebGLRenderer3 } from "three";
1771
2420
  import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
1772
2421
  var ZylemCamera;
1773
2422
  var init_zylem_camera = __esm({
@@ -1777,24 +2426,21 @@ var init_zylem_camera = __esm({
1777
2426
  init_third_person();
1778
2427
  init_fixed_2d();
1779
2428
  init_render_pass();
2429
+ init_camera_debug_delegate();
1780
2430
  ZylemCamera = class {
1781
- cameraRig;
2431
+ cameraRig = null;
1782
2432
  camera;
1783
2433
  screenResolution;
1784
2434
  renderer;
1785
2435
  composer;
1786
2436
  _perspective;
1787
- orbitControls = null;
1788
2437
  target = null;
1789
2438
  sceneRef = null;
1790
2439
  frustumSize = 10;
1791
2440
  // Perspective controller delegation
1792
2441
  perspectiveController = null;
1793
- debugDelegate = null;
1794
- debugUnsubscribe = null;
1795
- debugStateSnapshot = { enabled: false, selected: [] };
1796
- orbitTarget = null;
1797
- orbitTargetWorldPos = new Vector38();
2442
+ // Debug/orbit controls delegation
2443
+ orbitController = null;
1798
2444
  constructor(perspective, screenResolution, frustumSize = 10) {
1799
2445
  this._perspective = perspective;
1800
2446
  this.screenResolution = screenResolution;
@@ -1805,26 +2451,23 @@ var init_zylem_camera = __esm({
1805
2451
  this.composer = new EffectComposer(this.renderer);
1806
2452
  const aspectRatio = screenResolution.x / screenResolution.y;
1807
2453
  this.camera = this.createCameraForPerspective(aspectRatio);
1808
- this.cameraRig = new Object3D4();
1809
- this.cameraRig.position.set(0, 3, 10);
1810
- this.cameraRig.add(this.camera);
1811
- this.camera.lookAt(new Vector38(0, 2, 0));
2454
+ if (this.needsRig()) {
2455
+ this.cameraRig = new Object3D6();
2456
+ this.cameraRig.position.set(0, 3, 10);
2457
+ this.cameraRig.add(this.camera);
2458
+ this.camera.lookAt(new Vector311(0, 2, 0));
2459
+ } else {
2460
+ this.camera.position.set(0, 0, 10);
2461
+ this.camera.lookAt(new Vector311(0, 0, 0));
2462
+ }
1812
2463
  this.initializePerspectiveController();
2464
+ this.orbitController = new CameraOrbitController(this.camera, this.renderer.domElement);
1813
2465
  }
1814
2466
  /**
1815
2467
  * Setup the camera with a scene
1816
2468
  */
1817
2469
  async setup(scene) {
1818
2470
  this.sceneRef = scene;
1819
- if (this.orbitControls === null) {
1820
- this.orbitControls = new OrbitControls(this.camera, this.renderer.domElement);
1821
- this.orbitControls.enableDamping = true;
1822
- this.orbitControls.dampingFactor = 0.05;
1823
- this.orbitControls.screenSpacePanning = false;
1824
- this.orbitControls.minDistance = 1;
1825
- this.orbitControls.maxDistance = 500;
1826
- this.orbitControls.maxPolarAngle = Math.PI / 2;
1827
- }
1828
2471
  let renderResolution = this.screenResolution.clone().divideScalar(2);
1829
2472
  renderResolution.x |= 0;
1830
2473
  renderResolution.y |= 0;
@@ -1846,16 +2489,18 @@ var init_zylem_camera = __esm({
1846
2489
  * Update camera and render
1847
2490
  */
1848
2491
  update(delta) {
1849
- if (this.orbitControls && this.orbitTarget) {
1850
- this.orbitTarget.getWorldPosition(this.orbitTargetWorldPos);
1851
- this.orbitControls.target.copy(this.orbitTargetWorldPos);
1852
- }
1853
- this.orbitControls?.update();
1854
- if (this.perspectiveController) {
2492
+ this.orbitController?.update();
2493
+ if (this.perspectiveController && !this.isDebugModeActive()) {
1855
2494
  this.perspectiveController.update(delta);
1856
2495
  }
1857
2496
  this.composer.render(delta);
1858
2497
  }
2498
+ /**
2499
+ * Check if debug mode is active (orbit controls taking over camera)
2500
+ */
2501
+ isDebugModeActive() {
2502
+ return this.orbitController?.isActive ?? false;
2503
+ }
1859
2504
  /**
1860
2505
  * Dispose renderer, composer, controls, and detach from scene
1861
2506
  */
@@ -1865,7 +2510,7 @@ var init_zylem_camera = __esm({
1865
2510
  } catch {
1866
2511
  }
1867
2512
  try {
1868
- this.disableOrbitControls();
2513
+ this.orbitController?.dispose();
1869
2514
  } catch {
1870
2515
  }
1871
2516
  try {
@@ -1877,28 +2522,13 @@ var init_zylem_camera = __esm({
1877
2522
  this.renderer.dispose();
1878
2523
  } catch {
1879
2524
  }
1880
- this.detachDebugDelegate();
1881
2525
  this.sceneRef = null;
1882
2526
  }
1883
2527
  /**
1884
2528
  * Attach a delegate to react to debug state changes.
1885
2529
  */
1886
2530
  setDebugDelegate(delegate) {
1887
- if (this.debugDelegate === delegate) {
1888
- return;
1889
- }
1890
- this.detachDebugDelegate();
1891
- this.debugDelegate = delegate;
1892
- if (!delegate) {
1893
- this.applyDebugState({ enabled: false, selected: [] });
1894
- return;
1895
- }
1896
- const unsubscribe = delegate.subscribe((state2) => {
1897
- this.applyDebugState(state2);
1898
- });
1899
- this.debugUnsubscribe = () => {
1900
- unsubscribe?.();
1901
- };
2531
+ this.orbitController?.setDebugDelegate(delegate);
1902
2532
  }
1903
2533
  /**
1904
2534
  * Resize camera and renderer
@@ -1990,15 +2620,31 @@ var init_zylem_camera = __esm({
1990
2620
  if (this._perspective === Perspectives.Flat2D || this._perspective === Perspectives.Fixed2D) {
1991
2621
  this.frustumSize = position2.z;
1992
2622
  }
1993
- this.cameraRig.position.set(position2.x, position2.y, position2.z);
2623
+ if (this.cameraRig) {
2624
+ this.cameraRig.position.set(position2.x, position2.y, position2.z);
2625
+ } else {
2626
+ this.camera.position.set(position2.x, position2.y, position2.z);
2627
+ }
1994
2628
  }
1995
2629
  move(position2) {
1996
2630
  this.moveCamera(position2);
1997
2631
  }
1998
2632
  rotate(pitch, yaw, roll) {
1999
- this.cameraRig.rotateX(pitch);
2000
- this.cameraRig.rotateY(yaw);
2001
- this.cameraRig.rotateZ(roll);
2633
+ if (this.cameraRig) {
2634
+ this.cameraRig.rotateX(pitch);
2635
+ this.cameraRig.rotateY(yaw);
2636
+ this.cameraRig.rotateZ(roll);
2637
+ } else {
2638
+ this.camera.rotateX(pitch);
2639
+ this.camera.rotateY(yaw);
2640
+ this.camera.rotateZ(roll);
2641
+ }
2642
+ }
2643
+ /**
2644
+ * Check if this perspective type needs a camera rig
2645
+ */
2646
+ needsRig() {
2647
+ return this._perspective === Perspectives.ThirdPerson;
2002
2648
  }
2003
2649
  /**
2004
2650
  * Get the DOM element for the renderer
@@ -2006,336 +2652,221 @@ var init_zylem_camera = __esm({
2006
2652
  getDomElement() {
2007
2653
  return this.renderer.domElement;
2008
2654
  }
2009
- applyDebugState(state2) {
2010
- this.debugStateSnapshot = {
2011
- enabled: state2.enabled,
2012
- selected: [...state2.selected]
2013
- };
2014
- if (state2.enabled) {
2015
- this.enableOrbitControls();
2016
- this.updateOrbitTargetFromSelection(state2.selected);
2017
- } else {
2018
- this.orbitTarget = null;
2019
- this.disableOrbitControls();
2020
- }
2021
- }
2022
- enableOrbitControls() {
2023
- if (this.orbitControls) {
2024
- return;
2025
- }
2026
- this.orbitControls = new OrbitControls(this.camera, this.renderer.domElement);
2027
- this.orbitControls.enableDamping = true;
2028
- this.orbitControls.dampingFactor = 0.05;
2029
- this.orbitControls.screenSpacePanning = false;
2030
- this.orbitControls.minDistance = 1;
2031
- this.orbitControls.maxDistance = 500;
2032
- this.orbitControls.maxPolarAngle = Math.PI / 2;
2655
+ };
2656
+ }
2657
+ });
2658
+
2659
+ // src/lib/stage/stage-camera-delegate.ts
2660
+ import { Vector2 as Vector25 } from "three";
2661
+ var StageCameraDelegate;
2662
+ var init_stage_camera_delegate = __esm({
2663
+ "src/lib/stage/stage-camera-delegate.ts"() {
2664
+ "use strict";
2665
+ init_zylem_camera();
2666
+ init_perspective();
2667
+ StageCameraDelegate = class {
2668
+ stage;
2669
+ constructor(stage) {
2670
+ this.stage = stage;
2033
2671
  }
2034
- disableOrbitControls() {
2035
- if (!this.orbitControls) {
2036
- return;
2037
- }
2038
- this.orbitControls.dispose();
2039
- this.orbitControls = null;
2672
+ /**
2673
+ * Create a default third-person camera based on window size.
2674
+ */
2675
+ createDefaultCamera() {
2676
+ const width = window.innerWidth;
2677
+ const height = window.innerHeight;
2678
+ const screenResolution = new Vector25(width, height);
2679
+ return new ZylemCamera(Perspectives.ThirdPerson, screenResolution);
2040
2680
  }
2041
- updateOrbitTargetFromSelection(selected) {
2042
- if (!this.debugDelegate || selected.length === 0) {
2043
- this.orbitTarget = null;
2044
- return;
2045
- }
2046
- for (let i = selected.length - 1; i >= 0; i -= 1) {
2047
- const uuid = selected[i];
2048
- const targetObject = this.debugDelegate.resolveTarget(uuid);
2049
- if (targetObject) {
2050
- this.orbitTarget = targetObject;
2051
- if (this.orbitControls) {
2052
- targetObject.getWorldPosition(this.orbitTargetWorldPos);
2053
- this.orbitControls.target.copy(this.orbitTargetWorldPos);
2054
- }
2055
- return;
2056
- }
2681
+ /**
2682
+ * Resolve the camera to use for the stage.
2683
+ * Uses the provided camera, stage camera wrapper, or creates a default.
2684
+ *
2685
+ * @param cameraOverride Optional camera override
2686
+ * @param cameraWrapper Optional camera wrapper from stage options
2687
+ * @returns The resolved ZylemCamera instance
2688
+ */
2689
+ resolveCamera(cameraOverride, cameraWrapper) {
2690
+ if (cameraOverride) {
2691
+ return cameraOverride;
2057
2692
  }
2058
- this.orbitTarget = null;
2059
- }
2060
- detachDebugDelegate() {
2061
- if (this.debugUnsubscribe) {
2062
- try {
2063
- this.debugUnsubscribe();
2064
- } catch {
2065
- }
2693
+ if (cameraWrapper) {
2694
+ return cameraWrapper.cameraRef;
2066
2695
  }
2067
- this.debugUnsubscribe = null;
2068
- this.debugDelegate = null;
2696
+ return this.createDefaultCamera();
2069
2697
  }
2070
2698
  };
2071
2699
  }
2072
2700
  });
2073
2701
 
2074
- // src/lib/camera/camera.ts
2075
- import { Vector2 as Vector24, Vector3 as Vector39 } from "three";
2076
- var CameraWrapper;
2077
- var init_camera = __esm({
2078
- "src/lib/camera/camera.ts"() {
2702
+ // src/lib/stage/stage-loading-delegate.ts
2703
+ var StageLoadingDelegate;
2704
+ var init_stage_loading_delegate = __esm({
2705
+ "src/lib/stage/stage-loading-delegate.ts"() {
2079
2706
  "use strict";
2080
- CameraWrapper = class {
2081
- cameraRef;
2082
- constructor(camera) {
2083
- this.cameraRef = camera;
2707
+ init_game_event_bus();
2708
+ StageLoadingDelegate = class {
2709
+ loadingHandlers = [];
2710
+ stageName;
2711
+ stageIndex;
2712
+ /**
2713
+ * Set stage context for event bus emissions.
2714
+ */
2715
+ setStageContext(stageName, stageIndex) {
2716
+ this.stageName = stageName;
2717
+ this.stageIndex = stageIndex;
2084
2718
  }
2085
- };
2086
- }
2087
- });
2088
-
2089
- // src/lib/stage/debug-entity-cursor.ts
2090
- import {
2091
- Box3,
2092
- BoxGeometry,
2093
- Color as Color7,
2094
- EdgesGeometry,
2095
- Group as Group4,
2096
- LineBasicMaterial,
2097
- LineSegments,
2098
- Mesh as Mesh3,
2099
- MeshBasicMaterial,
2100
- Vector3 as Vector310
2101
- } from "three";
2102
- var DebugEntityCursor;
2103
- var init_debug_entity_cursor = __esm({
2104
- "src/lib/stage/debug-entity-cursor.ts"() {
2105
- "use strict";
2106
- DebugEntityCursor = class {
2107
- scene;
2108
- container;
2109
- fillMesh;
2110
- edgeLines;
2111
- currentColor = new Color7(65280);
2112
- bbox = new Box3();
2113
- size = new Vector310();
2114
- center = new Vector310();
2115
- constructor(scene) {
2116
- this.scene = scene;
2117
- const initialGeometry = new BoxGeometry(1, 1, 1);
2118
- this.fillMesh = new Mesh3(
2119
- initialGeometry,
2120
- new MeshBasicMaterial({
2121
- color: this.currentColor,
2122
- transparent: true,
2123
- opacity: 0.12,
2124
- depthWrite: false
2125
- })
2126
- );
2127
- const edges = new EdgesGeometry(initialGeometry);
2128
- this.edgeLines = new LineSegments(
2129
- edges,
2130
- new LineBasicMaterial({ color: this.currentColor, linewidth: 1 })
2131
- );
2132
- this.container = new Group4();
2133
- this.container.name = "DebugEntityCursor";
2134
- this.container.add(this.fillMesh);
2135
- this.container.add(this.edgeLines);
2136
- this.container.visible = false;
2137
- this.scene.add(this.container);
2138
- }
2139
- setColor(color) {
2140
- this.currentColor.set(color);
2141
- this.fillMesh.material.color.set(this.currentColor);
2142
- this.edgeLines.material.color.set(this.currentColor);
2719
+ /**
2720
+ * Subscribe to loading events.
2721
+ *
2722
+ * @param callback Invoked for each loading event (start, progress, complete)
2723
+ * @returns Unsubscribe function
2724
+ */
2725
+ onLoading(callback) {
2726
+ this.loadingHandlers.push(callback);
2727
+ return () => {
2728
+ this.loadingHandlers = this.loadingHandlers.filter((h) => h !== callback);
2729
+ };
2143
2730
  }
2144
2731
  /**
2145
- * Update the cursor to enclose the provided Object3D using a world-space AABB.
2732
+ * Emit a loading event to all subscribers and to the game event bus.
2733
+ *
2734
+ * @param event The loading event to broadcast
2146
2735
  */
2147
- updateFromObject(object) {
2148
- if (!object) {
2149
- this.hide();
2150
- return;
2736
+ emit(event) {
2737
+ for (const handler of this.loadingHandlers) {
2738
+ try {
2739
+ handler(event);
2740
+ } catch (e) {
2741
+ console.error("Loading handler failed", e);
2742
+ }
2151
2743
  }
2152
- this.bbox.setFromObject(object);
2153
- if (!isFinite(this.bbox.min.x) || !isFinite(this.bbox.max.x)) {
2154
- this.hide();
2155
- return;
2744
+ const payload = {
2745
+ ...event,
2746
+ stageName: this.stageName,
2747
+ stageIndex: this.stageIndex
2748
+ };
2749
+ if (event.type === "start") {
2750
+ gameEventBus.emit("stage:loading:start", payload);
2751
+ } else if (event.type === "progress") {
2752
+ gameEventBus.emit("stage:loading:progress", payload);
2753
+ } else if (event.type === "complete") {
2754
+ gameEventBus.emit("stage:loading:complete", payload);
2156
2755
  }
2157
- this.bbox.getSize(this.size);
2158
- this.bbox.getCenter(this.center);
2159
- const newGeom = new BoxGeometry(
2160
- Math.max(this.size.x, 1e-6),
2161
- Math.max(this.size.y, 1e-6),
2162
- Math.max(this.size.z, 1e-6)
2163
- );
2164
- this.fillMesh.geometry.dispose();
2165
- this.fillMesh.geometry = newGeom;
2166
- const newEdges = new EdgesGeometry(newGeom);
2167
- this.edgeLines.geometry.dispose();
2168
- this.edgeLines.geometry = newEdges;
2169
- this.container.position.copy(this.center);
2170
- this.container.visible = true;
2171
2756
  }
2172
- hide() {
2173
- this.container.visible = false;
2757
+ /**
2758
+ * Emit a start loading event.
2759
+ */
2760
+ emitStart(message = "Loading stage...") {
2761
+ this.emit({ type: "start", message, progress: 0 });
2174
2762
  }
2763
+ /**
2764
+ * Emit a progress loading event.
2765
+ */
2766
+ emitProgress(message, current, total) {
2767
+ const progress = total > 0 ? current / total : 0;
2768
+ this.emit({ type: "progress", message, progress, current, total });
2769
+ }
2770
+ /**
2771
+ * Emit a complete loading event.
2772
+ */
2773
+ emitComplete(message = "Stage loaded") {
2774
+ this.emit({ type: "complete", message, progress: 1 });
2775
+ }
2776
+ /**
2777
+ * Clear all loading handlers.
2778
+ */
2175
2779
  dispose() {
2176
- this.scene.remove(this.container);
2177
- this.fillMesh.geometry.dispose();
2178
- this.fillMesh.material.dispose();
2179
- this.edgeLines.geometry.dispose();
2180
- this.edgeLines.material.dispose();
2780
+ this.loadingHandlers = [];
2181
2781
  }
2182
2782
  };
2183
2783
  }
2184
2784
  });
2185
2785
 
2186
- // src/lib/stage/stage-debug-delegate.ts
2187
- import { Ray } from "@dimforge/rapier3d-compat";
2188
- import { BufferAttribute, BufferGeometry as BufferGeometry3, LineBasicMaterial as LineBasicMaterial2, LineSegments as LineSegments2, Raycaster, Vector2 as Vector25 } from "three";
2189
- var SELECT_TOOL_COLOR, DELETE_TOOL_COLOR, StageDebugDelegate;
2190
- var init_stage_debug_delegate = __esm({
2191
- "src/lib/stage/stage-debug-delegate.ts"() {
2786
+ // src/lib/core/utility/options-parser.ts
2787
+ function isBaseNode(item) {
2788
+ return !!item && typeof item === "object" && typeof item.create === "function";
2789
+ }
2790
+ function isThenable(item) {
2791
+ return !!item && typeof item.then === "function";
2792
+ }
2793
+ function isCameraWrapper(item) {
2794
+ return !!item && typeof item === "object" && item.constructor?.name === "CameraWrapper";
2795
+ }
2796
+ function isConfigObject(item) {
2797
+ if (!item || typeof item !== "object") return false;
2798
+ if (isBaseNode(item)) return false;
2799
+ if (isCameraWrapper(item)) return false;
2800
+ if (isThenable(item)) return false;
2801
+ if (typeof item.then === "function") return false;
2802
+ return item.constructor === Object || item.constructor?.name === "Object";
2803
+ }
2804
+ function isEntityInput(item) {
2805
+ if (!item) return false;
2806
+ if (isBaseNode(item)) return true;
2807
+ if (typeof item === "function") return true;
2808
+ if (isThenable(item)) return true;
2809
+ return false;
2810
+ }
2811
+ var init_options_parser = __esm({
2812
+ "src/lib/core/utility/options-parser.ts"() {
2192
2813
  "use strict";
2193
- init_debug_state();
2194
- init_debug_entity_cursor();
2195
- SELECT_TOOL_COLOR = 2293538;
2196
- DELETE_TOOL_COLOR = 16724787;
2197
- StageDebugDelegate = class {
2198
- stage;
2199
- options;
2200
- mouseNdc = new Vector25(-2, -2);
2201
- raycaster = new Raycaster();
2202
- isMouseDown = false;
2203
- disposeFns = [];
2204
- debugCursor = null;
2205
- debugLines = null;
2206
- constructor(stage, options) {
2207
- this.stage = stage;
2208
- this.options = {
2209
- maxRayDistance: options?.maxRayDistance ?? 5e3,
2210
- addEntityFactory: options?.addEntityFactory ?? null
2211
- };
2212
- if (this.stage.scene) {
2213
- this.debugLines = new LineSegments2(
2214
- new BufferGeometry3(),
2215
- new LineBasicMaterial2({ vertexColors: true })
2216
- );
2217
- this.stage.scene.scene.add(this.debugLines);
2218
- this.debugLines.visible = true;
2219
- this.debugCursor = new DebugEntityCursor(this.stage.scene.scene);
2220
- }
2221
- this.attachDomListeners();
2222
- }
2223
- update() {
2224
- if (!debugState.enabled) return;
2225
- if (!this.stage.scene || !this.stage.world || !this.stage.cameraRef) return;
2226
- const { world, cameraRef } = this.stage;
2227
- if (this.debugLines) {
2228
- const { vertices, colors } = world.world.debugRender();
2229
- this.debugLines.geometry.setAttribute("position", new BufferAttribute(vertices, 3));
2230
- this.debugLines.geometry.setAttribute("color", new BufferAttribute(colors, 4));
2231
- }
2232
- const tool = getDebugTool();
2233
- const isCursorTool = tool === "select" || tool === "delete";
2234
- this.raycaster.setFromCamera(this.mouseNdc, cameraRef.camera);
2235
- const origin = this.raycaster.ray.origin.clone();
2236
- const direction = this.raycaster.ray.direction.clone().normalize();
2237
- const rapierRay = new Ray(
2238
- { x: origin.x, y: origin.y, z: origin.z },
2239
- { x: direction.x, y: direction.y, z: direction.z }
2240
- );
2241
- const hit = world.world.castRay(rapierRay, this.options.maxRayDistance, true);
2242
- if (hit && isCursorTool) {
2243
- const rigidBody = hit.collider?._parent;
2244
- const hoveredUuid2 = rigidBody?.userData?.uuid;
2245
- if (hoveredUuid2) {
2246
- const entity = this.stage._debugMap.get(hoveredUuid2);
2247
- if (entity) setHoveredEntity(entity);
2248
- } else {
2249
- resetHoveredEntity();
2250
- }
2251
- if (this.isMouseDown) {
2252
- this.handleActionOnHit(hoveredUuid2 ?? null, origin, direction, hit.toi);
2253
- }
2254
- }
2255
- this.isMouseDown = false;
2256
- const hoveredUuid = getHoveredEntity();
2257
- if (!hoveredUuid) {
2258
- this.debugCursor?.hide();
2259
- return;
2260
- }
2261
- const hoveredEntity = this.stage._debugMap.get(`${hoveredUuid}`);
2262
- const targetObject = hoveredEntity?.group ?? hoveredEntity?.mesh ?? null;
2263
- if (!targetObject) {
2264
- this.debugCursor?.hide();
2265
- return;
2266
- }
2267
- switch (tool) {
2268
- case "select":
2269
- this.debugCursor?.setColor(SELECT_TOOL_COLOR);
2270
- break;
2271
- case "delete":
2272
- this.debugCursor?.setColor(DELETE_TOOL_COLOR);
2273
- break;
2274
- default:
2275
- this.debugCursor?.setColor(16777215);
2276
- break;
2277
- }
2278
- this.debugCursor?.updateFromObject(targetObject);
2279
- }
2280
- dispose() {
2281
- this.disposeFns.forEach((fn) => fn());
2282
- this.disposeFns = [];
2283
- this.debugCursor?.dispose();
2284
- if (this.debugLines && this.stage.scene) {
2285
- this.stage.scene.scene.remove(this.debugLines);
2286
- this.debugLines.geometry.dispose();
2287
- this.debugLines.material.dispose();
2288
- this.debugLines = null;
2289
- }
2290
- }
2291
- handleActionOnHit(hoveredUuid, origin, direction, toi) {
2292
- const tool = getDebugTool();
2293
- switch (tool) {
2294
- case "select": {
2295
- if (hoveredUuid) {
2296
- const entity = this.stage._debugMap.get(hoveredUuid);
2297
- if (entity) setSelectedEntity(entity);
2298
- }
2299
- break;
2300
- }
2301
- case "delete": {
2302
- if (hoveredUuid) {
2303
- this.stage.removeEntityByUuid(hoveredUuid);
2304
- }
2305
- break;
2306
- }
2307
- case "scale": {
2308
- if (!this.options.addEntityFactory) break;
2309
- const hitPosition = origin.clone().add(direction.clone().multiplyScalar(toi));
2310
- const newNode = this.options.addEntityFactory({ position: hitPosition });
2311
- if (newNode) {
2312
- Promise.resolve(newNode).then((node) => {
2313
- if (node) this.stage.spawnEntity(node);
2314
- }).catch(() => {
2315
- });
2316
- }
2317
- break;
2318
- }
2319
- default:
2320
- break;
2321
- }
2322
- }
2323
- attachDomListeners() {
2324
- const canvas = this.stage.cameraRef?.renderer.domElement ?? this.stage.scene?.zylemCamera.renderer.domElement;
2325
- if (!canvas) return;
2326
- const onMouseMove = (e) => {
2327
- const rect = canvas.getBoundingClientRect();
2328
- const x = (e.clientX - rect.left) / rect.width * 2 - 1;
2329
- const y = -((e.clientY - rect.top) / rect.height * 2 - 1);
2330
- this.mouseNdc.set(x, y);
2331
- };
2332
- const onMouseDown = (e) => {
2333
- this.isMouseDown = true;
2334
- };
2335
- canvas.addEventListener("mousemove", onMouseMove);
2336
- canvas.addEventListener("mousedown", onMouseDown);
2337
- this.disposeFns.push(() => canvas.removeEventListener("mousemove", onMouseMove));
2338
- this.disposeFns.push(() => canvas.removeEventListener("mousedown", onMouseDown));
2814
+ }
2815
+ });
2816
+
2817
+ // src/lib/stage/stage-config.ts
2818
+ import { Vector3 as Vector312 } from "three";
2819
+ function createDefaultStageConfig() {
2820
+ return new StageConfig(
2821
+ {
2822
+ p1: ["gamepad-1", "keyboard-1"],
2823
+ p2: ["gamepad-2", "keyboard-2"]
2824
+ },
2825
+ ZylemBlueColor,
2826
+ null,
2827
+ new Vector312(0, 0, 0),
2828
+ {}
2829
+ );
2830
+ }
2831
+ function parseStageOptions(options = []) {
2832
+ const defaults = createDefaultStageConfig();
2833
+ let config = {};
2834
+ const entities = [];
2835
+ const asyncEntities = [];
2836
+ let camera;
2837
+ for (const item of options) {
2838
+ if (isCameraWrapper(item)) {
2839
+ camera = item;
2840
+ } else if (isBaseNode(item)) {
2841
+ entities.push(item);
2842
+ } else if (isEntityInput(item) && !isBaseNode(item)) {
2843
+ asyncEntities.push(item);
2844
+ } else if (isConfigObject(item)) {
2845
+ config = { ...config, ...item };
2846
+ }
2847
+ }
2848
+ const resolvedConfig = new StageConfig(
2849
+ config.inputs ?? defaults.inputs,
2850
+ config.backgroundColor ?? defaults.backgroundColor,
2851
+ config.backgroundImage ?? defaults.backgroundImage,
2852
+ config.gravity ?? defaults.gravity,
2853
+ config.variables ?? defaults.variables
2854
+ );
2855
+ return { config: resolvedConfig, entities, asyncEntities, camera };
2856
+ }
2857
+ var StageConfig;
2858
+ var init_stage_config = __esm({
2859
+ "src/lib/stage/stage-config.ts"() {
2860
+ "use strict";
2861
+ init_options_parser();
2862
+ init_vector();
2863
+ StageConfig = class {
2864
+ constructor(inputs, backgroundColor, backgroundImage, gravity, variables) {
2865
+ this.inputs = inputs;
2866
+ this.backgroundColor = backgroundColor;
2867
+ this.backgroundImage = backgroundImage;
2868
+ this.gravity = gravity;
2869
+ this.variables = variables;
2339
2870
  }
2340
2871
  };
2341
2872
  }
@@ -2343,7 +2874,8 @@ var init_stage_debug_delegate = __esm({
2343
2874
 
2344
2875
  // src/lib/stage/zylem-stage.ts
2345
2876
  import { addComponent, addEntity, createWorld as createECS, removeEntity } from "bitecs";
2346
- import { Color as Color8, Vector3 as Vector312, Vector2 as Vector26 } from "three";
2877
+ import { Color as Color9, Vector3 as Vector313 } from "three";
2878
+ import { subscribe as subscribe5 } from "valtio/vanilla";
2347
2879
  import { nanoid as nanoid2 } from "nanoid";
2348
2880
  var STAGE_TYPE, ZylemStage;
2349
2881
  var init_zylem_stage = __esm({
@@ -2357,12 +2889,13 @@ var init_zylem_stage = __esm({
2357
2889
  init_game_state();
2358
2890
  init_lifecycle_base();
2359
2891
  init_transformable_system();
2360
- init_base_node();
2361
- init_perspective();
2362
- init_camera();
2363
2892
  init_stage_debug_delegate();
2893
+ init_stage_camera_debug_delegate();
2894
+ init_stage_camera_delegate();
2895
+ init_stage_loading_delegate();
2364
2896
  init_entity();
2365
- init_zylem_camera();
2897
+ init_stage_config();
2898
+ init_options_parser();
2366
2899
  STAGE_TYPE = "Stage";
2367
2900
  ZylemStage = class extends LifeCycleBase {
2368
2901
  type = STAGE_TYPE;
@@ -2373,7 +2906,7 @@ var init_zylem_stage = __esm({
2373
2906
  p1: ["gamepad-1", "keyboard"],
2374
2907
  p2: ["gamepad-2", "keyboard"]
2375
2908
  },
2376
- gravity: new Vector312(0, 0, 0),
2909
+ gravity: new Vector313(0, 0, 0),
2377
2910
  variables: {},
2378
2911
  entities: []
2379
2912
  };
@@ -2388,16 +2921,19 @@ var init_zylem_stage = __esm({
2388
2921
  isLoaded = false;
2389
2922
  _debugMap = /* @__PURE__ */ new Map();
2390
2923
  entityAddedHandlers = [];
2391
- loadingHandlers = [];
2392
2924
  ecs = createECS();
2393
2925
  testSystem = null;
2394
2926
  transformSystem = null;
2395
2927
  debugDelegate = null;
2396
2928
  cameraDebugDelegate = null;
2929
+ debugStateUnsubscribe = null;
2397
2930
  uuid;
2398
2931
  wrapperRef = null;
2399
2932
  camera;
2400
2933
  cameraRef = null;
2934
+ // Delegates
2935
+ cameraDelegate;
2936
+ loadingDelegate;
2401
2937
  /**
2402
2938
  * Create a new stage.
2403
2939
  * @param options Stage options: partial config, camera, and initial entities or factories
@@ -2407,49 +2943,22 @@ var init_zylem_stage = __esm({
2407
2943
  this.world = null;
2408
2944
  this.scene = null;
2409
2945
  this.uuid = nanoid2();
2410
- const { config, entities, asyncEntities, camera } = this.parseOptions(options);
2411
- this.camera = camera;
2412
- this.children = entities;
2413
- this.pendingEntities = asyncEntities;
2414
- this.saveState({ ...this.state, ...config, entities: [] });
2415
- this.gravity = config.gravity ?? new Vector312(0, 0, 0);
2416
- }
2417
- parseOptions(options) {
2418
- let config = {};
2419
- const entities = [];
2420
- const asyncEntities = [];
2421
- let camera;
2422
- for (const item of options) {
2423
- if (this.isCameraWrapper(item)) {
2424
- camera = item;
2425
- } else if (this.isBaseNode(item)) {
2426
- entities.push(item);
2427
- } else if (this.isEntityInput(item)) {
2428
- asyncEntities.push(item);
2429
- } else if (this.isZylemStageConfig(item)) {
2430
- config = { ...config, ...item };
2431
- }
2432
- }
2433
- return { config, entities, asyncEntities, camera };
2434
- }
2435
- isZylemStageConfig(item) {
2436
- return item && typeof item === "object" && !(item instanceof BaseNode) && !(item instanceof CameraWrapper);
2437
- }
2438
- isBaseNode(item) {
2439
- return item && typeof item === "object" && typeof item.create === "function";
2440
- }
2441
- isCameraWrapper(item) {
2442
- return item && typeof item === "object" && item.constructor.name === "CameraWrapper";
2443
- }
2444
- isEntityInput(item) {
2445
- if (!item) return false;
2446
- if (this.isBaseNode(item)) return true;
2447
- if (typeof item === "function") return true;
2448
- if (typeof item === "object" && typeof item.then === "function") return true;
2449
- return false;
2450
- }
2451
- isThenable(value) {
2452
- return !!value && typeof value.then === "function";
2946
+ this.cameraDelegate = new StageCameraDelegate(this);
2947
+ this.loadingDelegate = new StageLoadingDelegate();
2948
+ const parsed = parseStageOptions(options);
2949
+ this.camera = parsed.camera;
2950
+ this.children = parsed.entities;
2951
+ this.pendingEntities = parsed.asyncEntities;
2952
+ this.saveState({
2953
+ ...this.state,
2954
+ inputs: parsed.config.inputs,
2955
+ backgroundColor: parsed.config.backgroundColor,
2956
+ backgroundImage: parsed.config.backgroundImage,
2957
+ gravity: parsed.config.gravity,
2958
+ variables: parsed.config.variables,
2959
+ entities: []
2960
+ });
2961
+ this.gravity = parsed.config.gravity ?? new Vector313(0, 0, 0);
2453
2962
  }
2454
2963
  handleEntityImmediatelyOrQueue(entity) {
2455
2964
  if (this.isLoaded) {
@@ -2470,42 +2979,47 @@ var init_zylem_stage = __esm({
2470
2979
  }
2471
2980
  setState() {
2472
2981
  const { backgroundColor, backgroundImage } = this.state;
2473
- const color = backgroundColor instanceof Color8 ? backgroundColor : new Color8(backgroundColor);
2982
+ const color = backgroundColor instanceof Color9 ? backgroundColor : new Color9(backgroundColor);
2474
2983
  setStageBackgroundColor(color);
2475
2984
  setStageBackgroundImage(backgroundImage);
2476
2985
  setStageVariables(this.state.variables ?? {});
2477
2986
  }
2478
2987
  /**
2479
2988
  * Load and initialize the stage's scene and world.
2989
+ * Uses generator pattern to yield control to event loop for real-time progress.
2480
2990
  * @param id DOM element id for the renderer container
2481
2991
  * @param camera Optional camera override
2482
2992
  */
2483
2993
  async load(id, camera) {
2484
2994
  this.setState();
2485
- const zylemCamera = camera || (this.camera ? this.camera.cameraRef : this.createDefaultCamera());
2995
+ const zylemCamera = this.cameraDelegate.resolveCamera(camera, this.camera);
2486
2996
  this.cameraRef = zylemCamera;
2487
2997
  this.scene = new ZylemScene(id, zylemCamera, this.state);
2488
- const physicsWorld = await ZylemWorld.loadPhysics(this.gravity ?? new Vector312(0, 0, 0));
2998
+ const physicsWorld = await ZylemWorld.loadPhysics(this.gravity ?? new Vector313(0, 0, 0));
2489
2999
  this.world = new ZylemWorld(physicsWorld);
2490
3000
  this.scene.setup();
2491
- this.emitLoading({ type: "start", message: "Loading stage...", progress: 0 });
3001
+ this.loadingDelegate.emitStart();
3002
+ await this.runEntityLoadGenerator();
3003
+ this.transformSystem = createTransformSystem(this);
3004
+ this.isLoaded = true;
3005
+ this.loadingDelegate.emitComplete();
3006
+ }
3007
+ /**
3008
+ * Generator that yields between entity loads for real-time progress updates.
3009
+ */
3010
+ *entityLoadGenerator() {
2492
3011
  const total = this.children.length + this.pendingEntities.length + this.pendingPromises.length;
2493
3012
  let current = 0;
2494
- for (let child of this.children) {
3013
+ for (const child of this.children) {
2495
3014
  this.spawnEntity(child);
2496
3015
  current++;
2497
- this.emitLoading({
2498
- type: "progress",
2499
- message: `Loaded entity ${child.name || "unknown"}`,
2500
- progress: current / total,
2501
- current,
2502
- total
2503
- });
3016
+ yield { current, total, name: child.name || "unknown" };
2504
3017
  }
2505
3018
  if (this.pendingEntities.length) {
2506
3019
  this.enqueue(...this.pendingEntities);
2507
3020
  current += this.pendingEntities.length;
2508
3021
  this.pendingEntities = [];
3022
+ yield { current, total, name: "pending entities" };
2509
3023
  }
2510
3024
  if (this.pendingPromises.length) {
2511
3025
  for (const promise of this.pendingPromises) {
@@ -2515,24 +3029,44 @@ var init_zylem_stage = __esm({
2515
3029
  }
2516
3030
  current += this.pendingPromises.length;
2517
3031
  this.pendingPromises = [];
3032
+ yield { current, total, name: "async entities" };
2518
3033
  }
2519
- this.transformSystem = createTransformSystem(this);
2520
- this.isLoaded = true;
2521
- this.emitLoading({ type: "complete", message: "Stage loaded", progress: 1 });
2522
3034
  }
2523
- createDefaultCamera() {
2524
- const width = window.innerWidth;
2525
- const height = window.innerHeight;
2526
- const screenResolution = new Vector26(width, height);
2527
- return new ZylemCamera(Perspectives.ThirdPerson, screenResolution);
3035
+ /**
3036
+ * Runs the entity load generator, yielding to the event loop between loads.
3037
+ * This allows the browser to process events and update the UI in real-time.
3038
+ */
3039
+ async runEntityLoadGenerator() {
3040
+ const gen = this.entityLoadGenerator();
3041
+ for (const progress of gen) {
3042
+ this.loadingDelegate.emitProgress(`Loaded ${progress.name}`, progress.current, progress.total);
3043
+ await new Promise((resolve) => setTimeout(resolve, 0));
3044
+ }
2528
3045
  }
2529
3046
  _setup(params) {
2530
3047
  if (!this.scene || !this.world) {
2531
3048
  this.logMissingEntities();
2532
3049
  return;
2533
3050
  }
2534
- if (debugState.enabled) {
3051
+ this.updateDebugDelegate();
3052
+ this.debugStateUnsubscribe = subscribe5(debugState, () => {
3053
+ this.updateDebugDelegate();
3054
+ });
3055
+ }
3056
+ updateDebugDelegate() {
3057
+ if (debugState.enabled && !this.debugDelegate && this.scene && this.world) {
2535
3058
  this.debugDelegate = new StageDebugDelegate(this);
3059
+ if (this.cameraRef && !this.cameraDebugDelegate) {
3060
+ this.cameraDebugDelegate = new StageCameraDebugDelegate(this);
3061
+ this.cameraRef.setDebugDelegate(this.cameraDebugDelegate);
3062
+ }
3063
+ } else if (!debugState.enabled && this.debugDelegate) {
3064
+ this.debugDelegate.dispose();
3065
+ this.debugDelegate = null;
3066
+ if (this.cameraRef) {
3067
+ this.cameraRef.setDebugDelegate(null);
3068
+ }
3069
+ this.cameraDebugDelegate = null;
2536
3070
  }
2537
3071
  }
2538
3072
  _update(params) {
@@ -2542,7 +3076,7 @@ var init_zylem_stage = __esm({
2542
3076
  return;
2543
3077
  }
2544
3078
  this.world.update(params);
2545
- this.transformSystem(this.ecs);
3079
+ this.transformSystem?.system(this.ecs);
2546
3080
  this._childrenMap.forEach((child, eid) => {
2547
3081
  child.nodeUpdate({
2548
3082
  ...params,
@@ -2576,13 +3110,20 @@ var init_zylem_stage = __esm({
2576
3110
  this._debugMap.clear();
2577
3111
  this.world?.destroy();
2578
3112
  this.scene?.destroy();
3113
+ if (this.debugStateUnsubscribe) {
3114
+ this.debugStateUnsubscribe();
3115
+ this.debugStateUnsubscribe = null;
3116
+ }
2579
3117
  this.debugDelegate?.dispose();
3118
+ this.debugDelegate = null;
2580
3119
  this.cameraRef?.setDebugDelegate(null);
2581
3120
  this.cameraDebugDelegate = null;
2582
3121
  this.isLoaded = false;
2583
3122
  this.world = null;
2584
3123
  this.scene = null;
2585
3124
  this.cameraRef = null;
3125
+ this.transformSystem?.destroy(this.ecs);
3126
+ this.transformSystem = null;
2586
3127
  resetStageVariables();
2587
3128
  clearVariables(this);
2588
3129
  }
@@ -2663,13 +3204,7 @@ var init_zylem_stage = __esm({
2663
3204
  };
2664
3205
  }
2665
3206
  onLoading(callback) {
2666
- this.loadingHandlers.push(callback);
2667
- return () => {
2668
- this.loadingHandlers = this.loadingHandlers.filter((h) => h !== callback);
2669
- };
2670
- }
2671
- emitLoading(event) {
2672
- this.loadingHandlers.forEach((h) => h(event));
3207
+ return this.loadingDelegate.onLoading(callback);
2673
3208
  }
2674
3209
  /**
2675
3210
  * Remove an entity and its resources by its UUID.
@@ -2726,16 +3261,16 @@ var init_zylem_stage = __esm({
2726
3261
  enqueue(...items) {
2727
3262
  for (const item of items) {
2728
3263
  if (!item) continue;
2729
- if (this.isBaseNode(item)) {
3264
+ if (isBaseNode(item)) {
2730
3265
  this.handleEntityImmediatelyOrQueue(item);
2731
3266
  continue;
2732
3267
  }
2733
3268
  if (typeof item === "function") {
2734
3269
  try {
2735
3270
  const result = item();
2736
- if (this.isBaseNode(result)) {
3271
+ if (isBaseNode(result)) {
2737
3272
  this.handleEntityImmediatelyOrQueue(result);
2738
- } else if (this.isThenable(result)) {
3273
+ } else if (isThenable(result)) {
2739
3274
  this.handlePromiseWithSpawnOnResolve(result);
2740
3275
  }
2741
3276
  } catch (error) {
@@ -2743,7 +3278,7 @@ var init_zylem_stage = __esm({
2743
3278
  }
2744
3279
  continue;
2745
3280
  }
2746
- if (this.isThenable(item)) {
3281
+ if (isThenable(item)) {
2747
3282
  this.handlePromiseWithSpawnOnResolve(item);
2748
3283
  }
2749
3284
  }
@@ -2752,9 +3287,24 @@ var init_zylem_stage = __esm({
2752
3287
  }
2753
3288
  });
2754
3289
 
3290
+ // src/lib/camera/camera.ts
3291
+ import { Vector2 as Vector27, Vector3 as Vector314 } from "three";
3292
+ var CameraWrapper;
3293
+ var init_camera = __esm({
3294
+ "src/lib/camera/camera.ts"() {
3295
+ "use strict";
3296
+ CameraWrapper = class {
3297
+ cameraRef;
3298
+ constructor(camera) {
3299
+ this.cameraRef = camera;
3300
+ }
3301
+ };
3302
+ }
3303
+ });
3304
+
2755
3305
  // src/lib/stage/stage-default.ts
2756
3306
  import { proxy as proxy4 } from "valtio/vanilla";
2757
- import { Vector3 as Vector313 } from "three";
3307
+ import { Vector3 as Vector315 } from "three";
2758
3308
  function getStageOptions(options) {
2759
3309
  const defaults = getStageDefaultConfig();
2760
3310
  let originalConfig = {};
@@ -2785,7 +3335,7 @@ var init_stage_default = __esm({
2785
3335
  p1: ["gamepad-1", "keyboard"],
2786
3336
  p2: ["gamepad-2", "keyboard"]
2787
3337
  },
2788
- gravity: new Vector313(0, 0, 0),
3338
+ gravity: new Vector315(0, 0, 0),
2789
3339
  variables: {}
2790
3340
  };
2791
3341
  stageDefaultsState = proxy4({
@@ -2810,30 +3360,58 @@ var init_stage = __esm({
2810
3360
  Stage = class {
2811
3361
  wrappedStage;
2812
3362
  options = [];
2813
- // TODO: these shouldn't be here maybe more like nextFrame(stageInstance, () => {})
2814
- update = () => {
2815
- };
2816
- setup = () => {
2817
- };
2818
- destroy = () => {
2819
- };
3363
+ // Entities added after construction, consumed on each load
3364
+ _pendingEntities = [];
3365
+ // Lifecycle callback arrays
3366
+ setupCallbacks = [];
3367
+ updateCallbacks = [];
3368
+ destroyCallbacks = [];
3369
+ pendingLoadingCallbacks = [];
2820
3370
  constructor(options) {
2821
3371
  this.options = options;
2822
3372
  this.wrappedStage = null;
2823
3373
  }
2824
3374
  async load(id, camera) {
2825
3375
  stageState.entities = [];
2826
- this.wrappedStage = new ZylemStage(this.options);
3376
+ const loadOptions = [...this.options, ...this._pendingEntities];
3377
+ this._pendingEntities = [];
3378
+ this.wrappedStage = new ZylemStage(loadOptions);
2827
3379
  this.wrappedStage.wrapperRef = this;
3380
+ this.pendingLoadingCallbacks.forEach((cb) => {
3381
+ this.wrappedStage.onLoading(cb);
3382
+ });
3383
+ this.pendingLoadingCallbacks = [];
2828
3384
  const zylemCamera = camera instanceof CameraWrapper ? camera.cameraRef : camera;
2829
3385
  await this.wrappedStage.load(id, zylemCamera);
2830
3386
  this.wrappedStage.onEntityAdded((child) => {
2831
3387
  const next = this.wrappedStage.buildEntityState(child);
2832
3388
  stageState.entities = [...stageState.entities, next];
2833
3389
  }, { replayExisting: true });
3390
+ this.applyLifecycleCallbacks();
3391
+ }
3392
+ applyLifecycleCallbacks() {
3393
+ if (!this.wrappedStage) return;
3394
+ if (this.setupCallbacks.length > 0) {
3395
+ this.wrappedStage.setup = (params) => {
3396
+ const extended = { ...params, stage: this };
3397
+ this.setupCallbacks.forEach((cb) => cb(extended));
3398
+ };
3399
+ }
3400
+ if (this.updateCallbacks.length > 0) {
3401
+ this.wrappedStage.update = (params) => {
3402
+ const extended = { ...params, stage: this };
3403
+ this.updateCallbacks.forEach((cb) => cb(extended));
3404
+ };
3405
+ }
3406
+ if (this.destroyCallbacks.length > 0) {
3407
+ this.wrappedStage.destroy = (params) => {
3408
+ const extended = { ...params, stage: this };
3409
+ this.destroyCallbacks.forEach((cb) => cb(extended));
3410
+ };
3411
+ }
2834
3412
  }
2835
3413
  async addEntities(entities) {
2836
- this.options.push(...entities);
3414
+ this._pendingEntities.push(...entities);
2837
3415
  if (!this.wrappedStage) {
2838
3416
  return;
2839
3417
  }
@@ -2858,28 +3436,57 @@ var init_stage = __esm({
2858
3436
  start(params) {
2859
3437
  this.wrappedStage?.nodeSetup(params);
2860
3438
  }
3439
+ // Fluent API for adding lifecycle callbacks
2861
3440
  onUpdate(...callbacks) {
2862
- if (!this.wrappedStage) {
2863
- return;
3441
+ this.updateCallbacks.push(...callbacks);
3442
+ if (this.wrappedStage) {
3443
+ this.wrappedStage.update = (params) => {
3444
+ const extended = { ...params, stage: this };
3445
+ this.updateCallbacks.forEach((cb) => cb(extended));
3446
+ };
2864
3447
  }
2865
- this.wrappedStage.update = (params) => {
2866
- const extended = { ...params, stage: this };
2867
- callbacks.forEach((cb) => cb(extended));
2868
- };
3448
+ return this;
2869
3449
  }
2870
- onSetup(callback) {
2871
- this.wrappedStage.setup = callback;
3450
+ onSetup(...callbacks) {
3451
+ this.setupCallbacks.push(...callbacks);
3452
+ if (this.wrappedStage) {
3453
+ this.wrappedStage.setup = (params) => {
3454
+ const extended = { ...params, stage: this };
3455
+ this.setupCallbacks.forEach((cb) => cb(extended));
3456
+ };
3457
+ }
3458
+ return this;
2872
3459
  }
2873
- onDestroy(callback) {
2874
- this.wrappedStage.destroy = callback;
3460
+ onDestroy(...callbacks) {
3461
+ this.destroyCallbacks.push(...callbacks);
3462
+ if (this.wrappedStage) {
3463
+ this.wrappedStage.destroy = (params) => {
3464
+ const extended = { ...params, stage: this };
3465
+ this.destroyCallbacks.forEach((cb) => cb(extended));
3466
+ };
3467
+ }
3468
+ return this;
2875
3469
  }
2876
3470
  onLoading(callback) {
2877
3471
  if (!this.wrappedStage) {
3472
+ this.pendingLoadingCallbacks.push(callback);
2878
3473
  return () => {
3474
+ this.pendingLoadingCallbacks = this.pendingLoadingCallbacks.filter((c) => c !== callback);
2879
3475
  };
2880
3476
  }
2881
3477
  return this.wrappedStage.onLoading(callback);
2882
3478
  }
3479
+ /**
3480
+ * Find an entity by name on the current stage.
3481
+ * @param name The name of the entity to find
3482
+ * @param type Optional type symbol for type inference (e.g., TEXT_TYPE, SPRITE_TYPE)
3483
+ * @returns The entity if found, or undefined
3484
+ * @example stage.getEntityByName('scoreText', TEXT_TYPE)
3485
+ */
3486
+ getEntityByName(name, type) {
3487
+ const entity = this.wrappedStage?.children.find((c) => c.name === name);
3488
+ return entity;
3489
+ }
2883
3490
  };
2884
3491
  }
2885
3492
  });
@@ -3717,8 +4324,202 @@ var GameCanvas = class {
3717
4324
  }
3718
4325
  };
3719
4326
 
3720
- // src/lib/game/zylem-game.ts
4327
+ // src/lib/game/game-debug-delegate.ts
4328
+ init_debug_state();
3721
4329
  import Stats from "stats.js";
4330
+ import { subscribe as subscribe2 } from "valtio/vanilla";
4331
+ var GameDebugDelegate = class {
4332
+ statsRef = null;
4333
+ unsubscribe = null;
4334
+ constructor() {
4335
+ this.updateDebugUI();
4336
+ this.unsubscribe = subscribe2(debugState, () => {
4337
+ this.updateDebugUI();
4338
+ });
4339
+ }
4340
+ /**
4341
+ * Called every frame - wraps stats.begin()
4342
+ */
4343
+ begin() {
4344
+ this.statsRef?.begin();
4345
+ }
4346
+ /**
4347
+ * Called every frame - wraps stats.end()
4348
+ */
4349
+ end() {
4350
+ this.statsRef?.end();
4351
+ }
4352
+ updateDebugUI() {
4353
+ if (debugState.enabled && !this.statsRef) {
4354
+ this.statsRef = new Stats();
4355
+ this.statsRef.showPanel(0);
4356
+ this.statsRef.dom.style.position = "absolute";
4357
+ this.statsRef.dom.style.bottom = "0";
4358
+ this.statsRef.dom.style.right = "0";
4359
+ this.statsRef.dom.style.top = "auto";
4360
+ this.statsRef.dom.style.left = "auto";
4361
+ document.body.appendChild(this.statsRef.dom);
4362
+ } else if (!debugState.enabled && this.statsRef) {
4363
+ if (this.statsRef.dom.parentNode) {
4364
+ this.statsRef.dom.parentNode.removeChild(this.statsRef.dom);
4365
+ }
4366
+ this.statsRef = null;
4367
+ }
4368
+ }
4369
+ dispose() {
4370
+ if (this.unsubscribe) {
4371
+ this.unsubscribe();
4372
+ this.unsubscribe = null;
4373
+ }
4374
+ if (this.statsRef?.dom?.parentNode) {
4375
+ this.statsRef.dom.parentNode.removeChild(this.statsRef.dom);
4376
+ }
4377
+ this.statsRef = null;
4378
+ }
4379
+ };
4380
+
4381
+ // src/lib/game/game-loading-delegate.ts
4382
+ var GAME_LOADING_EVENT = "GAME_LOADING_EVENT";
4383
+ var GameLoadingDelegate = class {
4384
+ loadingHandlers = [];
4385
+ stageLoadingUnsubscribes = [];
4386
+ /**
4387
+ * Subscribe to loading events from the game.
4388
+ * Events include stage context (stageName, stageIndex).
4389
+ *
4390
+ * @param callback Invoked for each loading event
4391
+ * @returns Unsubscribe function
4392
+ */
4393
+ onLoading(callback) {
4394
+ this.loadingHandlers.push(callback);
4395
+ return () => {
4396
+ this.loadingHandlers = this.loadingHandlers.filter((h) => h !== callback);
4397
+ };
4398
+ }
4399
+ /**
4400
+ * Emit a loading event to all subscribers and dispatch to window.
4401
+ */
4402
+ emit(event) {
4403
+ for (const handler of this.loadingHandlers) {
4404
+ console.log("Game loading event", event);
4405
+ try {
4406
+ handler(event);
4407
+ } catch (e) {
4408
+ console.error("Game loading handler failed", e);
4409
+ }
4410
+ }
4411
+ if (typeof window !== "undefined") {
4412
+ window.dispatchEvent(new CustomEvent(GAME_LOADING_EVENT, { detail: event }));
4413
+ }
4414
+ }
4415
+ /**
4416
+ * Wire up a stage's loading events to this delegate.
4417
+ *
4418
+ * @param stage The stage to wire up
4419
+ * @param stageIndex The index of the stage
4420
+ */
4421
+ wireStageLoading(stage, stageIndex) {
4422
+ const unsub = stage.onLoading((event) => {
4423
+ this.emit({
4424
+ type: event.type,
4425
+ message: event.message ?? "",
4426
+ progress: event.progress ?? 0,
4427
+ current: event.current,
4428
+ total: event.total,
4429
+ stageName: stage.uuid ?? `Stage ${stageIndex}`,
4430
+ stageIndex
4431
+ });
4432
+ });
4433
+ if (typeof unsub === "function") {
4434
+ this.stageLoadingUnsubscribes.push(unsub);
4435
+ }
4436
+ }
4437
+ /**
4438
+ * Unsubscribe from all stage loading events.
4439
+ */
4440
+ unwireAllStages() {
4441
+ for (const unsub of this.stageLoadingUnsubscribes) {
4442
+ try {
4443
+ unsub();
4444
+ } catch {
4445
+ }
4446
+ }
4447
+ this.stageLoadingUnsubscribes = [];
4448
+ }
4449
+ /**
4450
+ * Clean up all handlers.
4451
+ */
4452
+ dispose() {
4453
+ this.unwireAllStages();
4454
+ this.loadingHandlers = [];
4455
+ }
4456
+ };
4457
+
4458
+ // src/lib/game/zylem-game.ts
4459
+ init_game_event_bus();
4460
+
4461
+ // src/lib/game/game-renderer-observer.ts
4462
+ var GameRendererObserver = class {
4463
+ container = null;
4464
+ camera = null;
4465
+ gameCanvas = null;
4466
+ config = null;
4467
+ mounted = false;
4468
+ setGameCanvas(canvas) {
4469
+ this.gameCanvas = canvas;
4470
+ this.tryMount();
4471
+ }
4472
+ setConfig(config) {
4473
+ this.config = config;
4474
+ this.tryMount();
4475
+ }
4476
+ setContainer(container) {
4477
+ this.container = container;
4478
+ this.tryMount();
4479
+ }
4480
+ setCamera(camera) {
4481
+ this.camera = camera;
4482
+ this.tryMount();
4483
+ }
4484
+ /**
4485
+ * Attempt to mount renderer if all dependencies are available.
4486
+ */
4487
+ tryMount() {
4488
+ if (this.mounted) return;
4489
+ if (!this.container || !this.camera || !this.gameCanvas) return;
4490
+ const dom = this.camera.getDomElement();
4491
+ const internal = this.config?.internalResolution;
4492
+ this.gameCanvas.mountRenderer(dom, (cssW, cssH) => {
4493
+ if (!this.camera) return;
4494
+ if (internal) {
4495
+ this.camera.setPixelRatio(1);
4496
+ this.camera.resize(internal.width, internal.height);
4497
+ } else {
4498
+ const dpr = window.devicePixelRatio || 1;
4499
+ this.camera.setPixelRatio(dpr);
4500
+ this.camera.resize(cssW, cssH);
4501
+ }
4502
+ });
4503
+ this.mounted = true;
4504
+ }
4505
+ /**
4506
+ * Reset state for stage transitions.
4507
+ */
4508
+ reset() {
4509
+ this.camera = null;
4510
+ this.mounted = false;
4511
+ }
4512
+ dispose() {
4513
+ this.container = null;
4514
+ this.camera = null;
4515
+ this.gameCanvas = null;
4516
+ this.config = null;
4517
+ this.mounted = false;
4518
+ }
4519
+ };
4520
+
4521
+ // src/lib/game/zylem-game.ts
4522
+ var ZYLEM_STATE_DISPATCH = "zylem:state:dispatch";
3722
4523
  var ZylemGame = class _ZylemGame {
3723
4524
  id;
3724
4525
  initialGlobals = {};
@@ -3733,7 +4534,6 @@ var ZylemGame = class _ZylemGame {
3733
4534
  timer;
3734
4535
  inputManager;
3735
4536
  wrapperRef;
3736
- statsRef = null;
3737
4537
  defaultCamera = null;
3738
4538
  container = null;
3739
4539
  canvas = null;
@@ -3742,6 +4542,10 @@ var ZylemGame = class _ZylemGame {
3742
4542
  gameCanvas = null;
3743
4543
  animationFrameId = null;
3744
4544
  isDisposed = false;
4545
+ debugDelegate = null;
4546
+ loadingDelegate = new GameLoadingDelegate();
4547
+ rendererObserver = new GameRendererObserver();
4548
+ eventBusUnsubscribes = [];
3745
4549
  static FRAME_LIMIT = 120;
3746
4550
  static FRAME_DURATION = 1e3 / _ZylemGame.FRAME_LIMIT;
3747
4551
  static MAX_DELTA_SECONDS = 1 / 30;
@@ -3773,42 +4577,33 @@ var ZylemGame = class _ZylemGame {
3773
4577
  this.gameCanvas.applyBodyBackground();
3774
4578
  this.gameCanvas.mountCanvas();
3775
4579
  this.gameCanvas.centerIfFullscreen();
4580
+ this.rendererObserver.setGameCanvas(this.gameCanvas);
4581
+ if (this.resolvedConfig) {
4582
+ this.rendererObserver.setConfig(this.resolvedConfig);
4583
+ }
4584
+ if (this.container) {
4585
+ this.rendererObserver.setContainer(this.container);
4586
+ }
4587
+ this.subscribeToEventBus();
3776
4588
  }
3777
4589
  loadDebugOptions(options) {
3778
- debugState.enabled = Boolean(options.debug);
3779
- if (options.debug) {
3780
- this.statsRef = new Stats();
3781
- this.statsRef.showPanel(0);
3782
- this.statsRef.dom.style.position = "absolute";
3783
- this.statsRef.dom.style.bottom = "0";
3784
- this.statsRef.dom.style.right = "0";
3785
- this.statsRef.dom.style.top = "auto";
3786
- this.statsRef.dom.style.left = "auto";
3787
- document.body.appendChild(this.statsRef.dom);
4590
+ if (options.debug !== void 0) {
4591
+ debugState.enabled = Boolean(options.debug);
3788
4592
  }
4593
+ this.debugDelegate = new GameDebugDelegate();
3789
4594
  }
3790
- async loadStage(stage) {
4595
+ loadStage(stage, stageIndex = 0) {
3791
4596
  this.unloadCurrentStage();
3792
4597
  const config = stage.options[0];
3793
- await stage.load(this.id, config?.camera);
3794
- this.stageMap.set(stage.wrappedStage.uuid, stage);
3795
- this.currentStageId = stage.wrappedStage.uuid;
3796
- this.defaultCamera = stage.wrappedStage.cameraRef;
3797
- if (this.container && this.defaultCamera) {
3798
- const dom = this.defaultCamera.getDomElement();
3799
- const internal = this.resolvedConfig?.internalResolution;
3800
- this.gameCanvas?.mountRenderer(dom, (cssW, cssH) => {
3801
- if (!this.defaultCamera) return;
3802
- if (internal) {
3803
- this.defaultCamera.setPixelRatio(1);
3804
- this.defaultCamera.resize(internal.width, internal.height);
3805
- } else {
3806
- const dpr = window.devicePixelRatio || 1;
3807
- this.defaultCamera.setPixelRatio(dpr);
3808
- this.defaultCamera.resize(cssW, cssH);
3809
- }
3810
- });
3811
- }
4598
+ this.loadingDelegate.wireStageLoading(stage, stageIndex);
4599
+ return stage.load(this.id, config?.camera).then(() => {
4600
+ this.stageMap.set(stage.wrappedStage.uuid, stage);
4601
+ this.currentStageId = stage.wrappedStage.uuid;
4602
+ this.defaultCamera = stage.wrappedStage.cameraRef;
4603
+ if (this.defaultCamera) {
4604
+ this.rendererObserver.setCamera(this.defaultCamera);
4605
+ }
4606
+ });
3812
4607
  }
3813
4608
  unloadCurrentStage() {
3814
4609
  if (!this.currentStageId) return;
@@ -3823,8 +4618,12 @@ var ZylemGame = class _ZylemGame {
3823
4618
  } catch (e) {
3824
4619
  console.error("Failed to destroy previous stage", e);
3825
4620
  }
4621
+ current.wrappedStage = null;
3826
4622
  }
3827
4623
  this.stageMap.delete(this.currentStageId);
4624
+ this.currentStageId = "";
4625
+ this.defaultCamera = null;
4626
+ this.rendererObserver.reset();
3828
4627
  }
3829
4628
  setGlobals(options) {
3830
4629
  this.initialGlobals = { ...options.globals };
@@ -3859,7 +4658,7 @@ var ZylemGame = class _ZylemGame {
3859
4658
  this.loop(0);
3860
4659
  }
3861
4660
  loop(timestamp) {
3862
- this.statsRef && this.statsRef.begin();
4661
+ this.debugDelegate?.begin();
3863
4662
  if (!isPaused()) {
3864
4663
  this.timer.update(timestamp);
3865
4664
  const stage = this.currentStage();
@@ -3869,14 +4668,14 @@ var ZylemGame = class _ZylemGame {
3869
4668
  if (this.customUpdate) {
3870
4669
  this.customUpdate(clampedParams);
3871
4670
  }
3872
- if (stage) {
4671
+ if (stage && stage.wrappedStage) {
3873
4672
  stage.wrappedStage.nodeUpdate({ ...clampedParams, me: stage.wrappedStage });
3874
4673
  }
3875
4674
  this.totalTime += clampedParams.delta;
3876
4675
  state.time = this.totalTime;
3877
4676
  this.previousTimeStamp = timestamp;
3878
4677
  }
3879
- this.statsRef && this.statsRef.end();
4678
+ this.debugDelegate?.end();
3880
4679
  this.outOfLoop();
3881
4680
  if (!this.isDisposed) {
3882
4681
  this.animationFrameId = requestAnimationFrame(this.loop.bind(this));
@@ -3889,9 +4688,13 @@ var ZylemGame = class _ZylemGame {
3889
4688
  this.animationFrameId = null;
3890
4689
  }
3891
4690
  this.unloadCurrentStage();
3892
- if (this.statsRef && this.statsRef.dom && this.statsRef.dom.parentNode) {
3893
- this.statsRef.dom.parentNode.removeChild(this.statsRef.dom);
4691
+ if (this.debugDelegate) {
4692
+ this.debugDelegate.dispose();
4693
+ this.debugDelegate = null;
3894
4694
  }
4695
+ this.eventBusUnsubscribes.forEach((unsub) => unsub());
4696
+ this.eventBusUnsubscribes = [];
4697
+ this.rendererObserver.dispose();
3895
4698
  this.timer.dispose();
3896
4699
  if (this.customDestroy) {
3897
4700
  this.customDestroy({
@@ -3912,6 +4715,52 @@ var ZylemGame = class _ZylemGame {
3912
4715
  currentStage() {
3913
4716
  return this.getStage(this.currentStageId);
3914
4717
  }
4718
+ /**
4719
+ * Subscribe to loading events from the game.
4720
+ * Events include stage context (stageName, stageIndex).
4721
+ * @param callback Invoked for each loading event
4722
+ * @returns Unsubscribe function
4723
+ */
4724
+ onLoading(callback) {
4725
+ return this.loadingDelegate.onLoading(callback);
4726
+ }
4727
+ /**
4728
+ * Subscribe to the game event bus for stage loading and state events.
4729
+ * Emits window events for cross-application communication.
4730
+ */
4731
+ subscribeToEventBus() {
4732
+ const emitLoadingWindowEvent = (payload) => {
4733
+ if (typeof window !== "undefined") {
4734
+ const event = {
4735
+ type: payload.type,
4736
+ message: payload.message ?? "",
4737
+ progress: payload.progress ?? 0,
4738
+ current: payload.current,
4739
+ total: payload.total,
4740
+ stageName: payload.stageName,
4741
+ stageIndex: payload.stageIndex
4742
+ };
4743
+ window.dispatchEvent(new CustomEvent(GAME_LOADING_EVENT, { detail: event }));
4744
+ }
4745
+ };
4746
+ const emitStateDispatchEvent = (payload) => {
4747
+ if (typeof window !== "undefined") {
4748
+ const detail = {
4749
+ scope: "game",
4750
+ path: payload.path,
4751
+ value: payload.value,
4752
+ previousValue: payload.previousValue
4753
+ };
4754
+ window.dispatchEvent(new CustomEvent(ZYLEM_STATE_DISPATCH, { detail }));
4755
+ }
4756
+ };
4757
+ this.eventBusUnsubscribes.push(
4758
+ gameEventBus.on("stage:loading:start", emitLoadingWindowEvent),
4759
+ gameEventBus.on("stage:loading:progress", emitLoadingWindowEvent),
4760
+ gameEventBus.on("stage:loading:complete", emitLoadingWindowEvent),
4761
+ gameEventBus.on("game:state:updated", emitStateDispatchEvent)
4762
+ );
4763
+ }
3915
4764
  };
3916
4765
 
3917
4766
  // src/lib/game/game.ts
@@ -4042,7 +4891,7 @@ init_stage();
4042
4891
  init_entity();
4043
4892
  init_builder();
4044
4893
  init_create();
4045
- import { Color as Color9, Group as Group5, Sprite as ThreeSprite, SpriteMaterial, CanvasTexture, LinearFilter, Vector2 as Vector27, ClampToEdgeWrapping } from "three";
4894
+ import { Color as Color10, Group as Group5, Sprite as ThreeSprite, SpriteMaterial, CanvasTexture, LinearFilter, Vector2 as Vector28, ClampToEdgeWrapping } from "three";
4046
4895
 
4047
4896
  // src/lib/entities/delegates/debug.ts
4048
4897
  import { MeshStandardMaterial as MeshStandardMaterial2, MeshBasicMaterial as MeshBasicMaterial2, MeshPhongMaterial as MeshPhongMaterial2 } from "three";
@@ -4149,7 +4998,7 @@ var textDefaults = {
4149
4998
  backgroundColor: null,
4150
4999
  padding: 4,
4151
5000
  stickToViewport: true,
4152
- screenPosition: new Vector27(24, 24),
5001
+ screenPosition: new Vector28(24, 24),
4153
5002
  zDistance: 1
4154
5003
  };
4155
5004
  var TextBuilder = class extends EntityBuilder {
@@ -4157,7 +5006,7 @@ var TextBuilder = class extends EntityBuilder {
4157
5006
  return new ZylemText(options);
4158
5007
  }
4159
5008
  };
4160
- var TEXT_TYPE = Symbol("Text");
5009
+ var TEXT_TYPE = /* @__PURE__ */ Symbol("Text");
4161
5010
  var ZylemText = class _ZylemText extends GameEntity {
4162
5011
  static type = TEXT_TYPE;
4163
5012
  _sprite = null;
@@ -4170,12 +5019,20 @@ var ZylemText = class _ZylemText extends GameEntity {
4170
5019
  constructor(options) {
4171
5020
  super();
4172
5021
  this.options = { ...textDefaults, ...options };
5022
+ this.prependSetup(this.textSetup.bind(this));
5023
+ this.prependUpdate(this.textUpdate.bind(this));
5024
+ this.onDestroy(this.textDestroy.bind(this));
5025
+ }
5026
+ create() {
5027
+ this._sprite = null;
5028
+ this._texture = null;
5029
+ this._canvas = null;
5030
+ this._ctx = null;
5031
+ this._lastCanvasW = 0;
5032
+ this._lastCanvasH = 0;
4173
5033
  this.group = new Group5();
4174
5034
  this.createSprite();
4175
- this.lifeCycleDelegate = {
4176
- setup: [this.textSetup.bind(this)],
4177
- update: [this.textUpdate.bind(this)]
4178
- };
5035
+ return super.create();
4179
5036
  }
4180
5037
  createSprite() {
4181
5038
  this._canvas = document.createElement("canvas");
@@ -4253,7 +5110,7 @@ var ZylemText = class _ZylemText extends GameEntity {
4253
5110
  }
4254
5111
  toCssColor(color) {
4255
5112
  if (typeof color === "string") return color;
4256
- const c = color instanceof Color9 ? color : new Color9(color);
5113
+ const c = color instanceof Color10 ? color : new Color10(color);
4257
5114
  return `#${c.getHexString()}`;
4258
5115
  }
4259
5116
  textSetup(params) {
@@ -4313,7 +5170,7 @@ var ZylemText = class _ZylemText extends GameEntity {
4313
5170
  if (!this._sprite || !this._cameraRef) return;
4314
5171
  const camera = this._cameraRef.camera;
4315
5172
  const { width, height } = this.getResolution();
4316
- const sp = this.options.screenPosition ?? new Vector27(24, 24);
5173
+ const sp = this.options.screenPosition ?? new Vector28(24, 24);
4317
5174
  const { px, py } = this.getScreenPixels(sp, width, height);
4318
5175
  const zDist = Math.max(1e-3, this.options.zDistance ?? 1);
4319
5176
  const { worldHalfW, worldHalfH } = this.computeWorldExtents(camera, zDist);
@@ -4341,6 +5198,24 @@ var ZylemText = class _ZylemText extends GameEntity {
4341
5198
  sticky: this.options.stickToViewport
4342
5199
  };
4343
5200
  }
5201
+ /**
5202
+ * Dispose of Three.js resources when the entity is destroyed.
5203
+ */
5204
+ async textDestroy() {
5205
+ this._texture?.dispose();
5206
+ if (this._sprite?.material) {
5207
+ this._sprite.material.dispose();
5208
+ }
5209
+ if (this._sprite) {
5210
+ this._sprite.removeFromParent();
5211
+ }
5212
+ this.group?.removeFromParent();
5213
+ this._sprite = null;
5214
+ this._texture = null;
5215
+ this._canvas = null;
5216
+ this._ctx = null;
5217
+ this._cameraRef = null;
5218
+ }
4344
5219
  };
4345
5220
  async function text(...args) {
4346
5221
  return createEntity({
@@ -4358,20 +5233,20 @@ init_builder();
4358
5233
  init_builder();
4359
5234
  init_create();
4360
5235
  import { ColliderDesc as ColliderDesc3 } from "@dimforge/rapier3d-compat";
4361
- import { Color as Color10, Euler, Group as Group6, Quaternion as Quaternion2, Vector3 as Vector314 } from "three";
5236
+ import { Color as Color11, Euler, Group as Group6, Quaternion as Quaternion3, Vector3 as Vector316 } from "three";
4362
5237
  import {
4363
5238
  TextureLoader as TextureLoader3,
4364
5239
  SpriteMaterial as SpriteMaterial2,
4365
5240
  Sprite as ThreeSprite2
4366
5241
  } from "three";
4367
5242
  var spriteDefaults = {
4368
- size: new Vector314(1, 1, 1),
4369
- position: new Vector314(0, 0, 0),
5243
+ size: new Vector316(1, 1, 1),
5244
+ position: new Vector316(0, 0, 0),
4370
5245
  collision: {
4371
5246
  static: false
4372
5247
  },
4373
5248
  material: {
4374
- color: new Color10("#ffffff"),
5249
+ color: new Color11("#ffffff"),
4375
5250
  shader: "standard"
4376
5251
  },
4377
5252
  images: [],
@@ -4379,7 +5254,7 @@ var spriteDefaults = {
4379
5254
  };
4380
5255
  var SpriteCollisionBuilder = class extends EntityCollisionBuilder {
4381
5256
  collider(options) {
4382
- const size = options.collisionSize || options.size || new Vector314(1, 1, 1);
5257
+ const size = options.collisionSize || options.size || new Vector316(1, 1, 1);
4383
5258
  const half = { x: size.x / 2, y: size.y / 2, z: size.z / 2 };
4384
5259
  let colliderDesc = ColliderDesc3.cuboid(half.x, half.y, half.z);
4385
5260
  return colliderDesc;
@@ -4390,7 +5265,7 @@ var SpriteBuilder = class extends EntityBuilder {
4390
5265
  return new ZylemSprite(options);
4391
5266
  }
4392
5267
  };
4393
- var SPRITE_TYPE = Symbol("Sprite");
5268
+ var SPRITE_TYPE = /* @__PURE__ */ Symbol("Sprite");
4394
5269
  var ZylemSprite = class _ZylemSprite extends GameEntity {
4395
5270
  static type = SPRITE_TYPE;
4396
5271
  sprites = [];
@@ -4404,12 +5279,21 @@ var ZylemSprite = class _ZylemSprite extends GameEntity {
4404
5279
  constructor(options) {
4405
5280
  super();
4406
5281
  this.options = { ...spriteDefaults, ...options };
4407
- this.createSpritesFromImages(options?.images || []);
4408
- this.createAnimations(options?.animations || []);
4409
- this.lifeCycleDelegate = {
4410
- update: [this.spriteUpdate.bind(this)],
4411
- destroy: [this.spriteDestroy.bind(this)]
4412
- };
5282
+ this.prependUpdate(this.spriteUpdate.bind(this));
5283
+ this.onDestroy(this.spriteDestroy.bind(this));
5284
+ }
5285
+ create() {
5286
+ this.sprites = [];
5287
+ this.spriteMap.clear();
5288
+ this.animations.clear();
5289
+ this.currentAnimation = null;
5290
+ this.currentAnimationFrame = "";
5291
+ this.currentAnimationIndex = 0;
5292
+ this.currentAnimationTime = 0;
5293
+ this.group = void 0;
5294
+ this.createSpritesFromImages(this.options?.images || []);
5295
+ this.createAnimations(this.options?.animations || []);
5296
+ return super.create();
4413
5297
  }
4414
5298
  createSpritesFromImages(images) {
4415
5299
  const textureLoader = new TextureLoader3();
@@ -4479,7 +5363,7 @@ var ZylemSprite = class _ZylemSprite extends GameEntity {
4479
5363
  if (_sprite.material) {
4480
5364
  const q = this.body?.rotation();
4481
5365
  if (q) {
4482
- const quat = new Quaternion2(q.x, q.y, q.z, q.w);
5366
+ const quat = new Quaternion3(q.x, q.y, q.z, q.w);
4483
5367
  const euler = new Euler().setFromQuaternion(quat, "XYZ");
4484
5368
  _sprite.material.rotation = euler.z;
4485
5369
  }
@@ -4563,12 +5447,15 @@ init_game_state();
4563
5447
  var Game = class {
4564
5448
  wrappedGame = null;
4565
5449
  options;
4566
- update = () => {
4567
- };
4568
- setup = () => {
4569
- };
4570
- destroy = () => {
4571
- };
5450
+ // Lifecycle callback arrays
5451
+ setupCallbacks = [];
5452
+ updateCallbacks = [];
5453
+ destroyCallbacks = [];
5454
+ pendingLoadingCallbacks = [];
5455
+ // Game-scoped global change subscriptions
5456
+ globalChangeCallbacks = [];
5457
+ globalChangesCallbacks = [];
5458
+ activeGlobalSubscriptions = [];
4572
5459
  refErrorMessage = "lost reference to game";
4573
5460
  constructor(options) {
4574
5461
  this.options = options;
@@ -4580,10 +5467,29 @@ var Game = class {
4580
5467
  initGlobals(globals);
4581
5468
  }
4582
5469
  }
5470
+ // Fluent API for adding lifecycle callbacks
5471
+ onSetup(...callbacks) {
5472
+ this.setupCallbacks.push(...callbacks);
5473
+ return this;
5474
+ }
5475
+ onUpdate(...callbacks) {
5476
+ this.updateCallbacks.push(...callbacks);
5477
+ return this;
5478
+ }
5479
+ onDestroy(...callbacks) {
5480
+ this.destroyCallbacks.push(...callbacks);
5481
+ return this;
5482
+ }
4583
5483
  async start() {
5484
+ resetGlobals();
5485
+ const globals = extractGlobalsFromOptions(this.options);
5486
+ if (globals) {
5487
+ initGlobals(globals);
5488
+ }
4584
5489
  const game = await this.load();
4585
5490
  this.wrappedGame = game;
4586
5491
  this.setOverrides();
5492
+ this.registerGlobalSubscriptions();
4587
5493
  game.start();
4588
5494
  return this;
4589
5495
  }
@@ -4594,6 +5500,9 @@ var Game = class {
4594
5500
  ...options,
4595
5501
  ...resolved
4596
5502
  }, this);
5503
+ for (const callback of this.pendingLoadingCallbacks) {
5504
+ game.onLoading(callback);
5505
+ }
4597
5506
  await game.loadStage(options.stages[0]);
4598
5507
  return game;
4599
5508
  }
@@ -4602,9 +5511,59 @@ var Game = class {
4602
5511
  console.error(this.refErrorMessage);
4603
5512
  return;
4604
5513
  }
4605
- this.wrappedGame.customSetup = this.setup;
4606
- this.wrappedGame.customUpdate = this.update;
4607
- this.wrappedGame.customDestroy = this.destroy;
5514
+ this.wrappedGame.customSetup = (params) => {
5515
+ this.setupCallbacks.forEach((cb) => cb(params));
5516
+ };
5517
+ this.wrappedGame.customUpdate = (params) => {
5518
+ this.updateCallbacks.forEach((cb) => cb(params));
5519
+ };
5520
+ this.wrappedGame.customDestroy = (params) => {
5521
+ this.destroyCallbacks.forEach((cb) => cb(params));
5522
+ };
5523
+ }
5524
+ /**
5525
+ * Subscribe to changes on a global value. Subscriptions are registered
5526
+ * when the game starts and cleaned up when disposed.
5527
+ * The callback receives the value and the current stage.
5528
+ * @example game.onGlobalChange('score', (val, stage) => console.log(val));
5529
+ */
5530
+ onGlobalChange(path, callback) {
5531
+ this.globalChangeCallbacks.push({ path, callback });
5532
+ return this;
5533
+ }
5534
+ /**
5535
+ * Subscribe to changes on multiple global paths. Subscriptions are registered
5536
+ * when the game starts and cleaned up when disposed.
5537
+ * The callback receives the values and the current stage.
5538
+ * @example game.onGlobalChanges(['score', 'lives'], ([score, lives], stage) => console.log(score, lives));
5539
+ */
5540
+ onGlobalChanges(paths, callback) {
5541
+ this.globalChangesCallbacks.push({ paths, callback });
5542
+ return this;
5543
+ }
5544
+ /**
5545
+ * Register all stored global change callbacks.
5546
+ * Called internally during start().
5547
+ */
5548
+ registerGlobalSubscriptions() {
5549
+ for (const { path, callback } of this.globalChangeCallbacks) {
5550
+ const unsub = onGlobalChange(path, (value) => {
5551
+ callback(value, this.getCurrentStage());
5552
+ });
5553
+ this.activeGlobalSubscriptions.push(unsub);
5554
+ }
5555
+ for (const { paths, callback } of this.globalChangesCallbacks) {
5556
+ const unsub = onGlobalChanges(paths, (values) => {
5557
+ callback(values, this.getCurrentStage());
5558
+ });
5559
+ this.activeGlobalSubscriptions.push(unsub);
5560
+ }
5561
+ }
5562
+ /**
5563
+ * Get the current stage wrapper.
5564
+ */
5565
+ getCurrentStage() {
5566
+ return this.wrappedGame?.currentStage() ?? null;
4608
5567
  }
4609
5568
  async pause() {
4610
5569
  setPaused(true);
@@ -4623,19 +5582,19 @@ var Game = class {
4623
5582
  }
4624
5583
  await this.wrappedGame.loadStage(this.wrappedGame.stages[0]);
4625
5584
  }
4626
- async previousStage() {
5585
+ previousStage() {
4627
5586
  if (!this.wrappedGame) {
4628
5587
  console.error(this.refErrorMessage);
4629
5588
  return;
4630
5589
  }
4631
5590
  const currentStageId = this.wrappedGame.currentStageId;
4632
- const currentIndex = this.wrappedGame.stages.findIndex((s) => s.wrappedStage.uuid === currentStageId);
5591
+ const currentIndex = this.wrappedGame.stages.findIndex((s) => s.wrappedStage?.uuid === currentStageId);
4633
5592
  const previousStage = this.wrappedGame.stages[currentIndex - 1];
4634
5593
  if (!previousStage) {
4635
5594
  console.error("previous stage called on first stage");
4636
5595
  return;
4637
5596
  }
4638
- await this.wrappedGame.loadStage(previousStage);
5597
+ this.wrappedGame.loadStage(previousStage);
4639
5598
  }
4640
5599
  async loadStageFromId(stageId) {
4641
5600
  if (!this.wrappedGame) {
@@ -4651,39 +5610,61 @@ var Game = class {
4651
5610
  console.error(`Failed to load stage ${stageId}`, e);
4652
5611
  }
4653
5612
  }
4654
- async nextStage() {
5613
+ nextStage() {
4655
5614
  if (!this.wrappedGame) {
4656
5615
  console.error(this.refErrorMessage);
4657
5616
  return;
4658
5617
  }
4659
5618
  if (stageState2.next) {
4660
5619
  const nextId = stageState2.next.id;
4661
- await StageManager.transitionForward(nextId);
5620
+ StageManager.transitionForward(nextId);
4662
5621
  if (stageState2.current) {
4663
- const stage = await StageFactory.createFromBlueprint(stageState2.current);
4664
- await this.wrappedGame.loadStage(stage);
5622
+ StageFactory.createFromBlueprint(stageState2.current).then((stage) => {
5623
+ this.wrappedGame?.loadStage(stage);
5624
+ });
4665
5625
  return;
4666
5626
  }
4667
5627
  }
4668
5628
  const currentStageId = this.wrappedGame.currentStageId;
4669
- const currentIndex = this.wrappedGame.stages.findIndex((s) => s.wrappedStage.uuid === currentStageId);
5629
+ const currentIndex = this.wrappedGame.stages.findIndex((s) => s.wrappedStage?.uuid === currentStageId);
4670
5630
  const nextStage = this.wrappedGame.stages[currentIndex + 1];
4671
5631
  if (!nextStage) {
4672
5632
  console.error("next stage called on last stage");
4673
5633
  return;
4674
5634
  }
4675
- await this.wrappedGame.loadStage(nextStage);
5635
+ this.wrappedGame.loadStage(nextStage);
4676
5636
  }
4677
5637
  async goToStage() {
4678
5638
  }
4679
5639
  async end() {
4680
5640
  }
4681
5641
  dispose() {
5642
+ for (const unsub of this.activeGlobalSubscriptions) {
5643
+ unsub();
5644
+ }
5645
+ this.activeGlobalSubscriptions = [];
4682
5646
  if (this.wrappedGame) {
4683
5647
  this.wrappedGame.dispose();
4684
5648
  }
5649
+ clearGlobalSubscriptions();
5650
+ resetGlobals();
4685
5651
  }
5652
+ /**
5653
+ * Subscribe to loading events from the game.
5654
+ * Events include stage context (stageName, stageIndex).
5655
+ * @param callback Invoked for each loading event
5656
+ * @returns Unsubscribe function
5657
+ */
4686
5658
  onLoading(callback) {
5659
+ if (this.wrappedGame) {
5660
+ return this.wrappedGame.onLoading(callback);
5661
+ }
5662
+ this.pendingLoadingCallbacks.push(callback);
5663
+ return () => {
5664
+ this.pendingLoadingCallbacks = this.pendingLoadingCallbacks.filter((c) => c !== callback);
5665
+ if (this.wrappedGame) {
5666
+ }
5667
+ };
4687
5668
  }
4688
5669
  };
4689
5670
  function createGame(...options) {
@@ -4692,7 +5673,7 @@ function createGame(...options) {
4692
5673
 
4693
5674
  // src/lib/core/vessel.ts
4694
5675
  init_base_node();
4695
- var VESSEL_TYPE = Symbol("vessel");
5676
+ var VESSEL_TYPE = /* @__PURE__ */ Symbol("vessel");
4696
5677
  var Vessel = class extends BaseNode {
4697
5678
  static type = VESSEL_TYPE;
4698
5679
  _setup(_params) {