@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/stage.js CHANGED
@@ -1,12 +1,56 @@
1
1
  // src/lib/stage/zylem-stage.ts
2
2
  import { addComponent, addEntity, createWorld as createECS, removeEntity } from "bitecs";
3
- import { Color as Color6, Vector3 as Vector310, Vector2 as Vector25 } from "three";
3
+ import { Color as Color7, Vector3 as Vector311 } from "three";
4
4
 
5
5
  // src/lib/collision/world.ts
6
6
  import RAPIER from "@dimforge/rapier3d-compat";
7
7
 
8
8
  // src/lib/game/game-state.ts
9
9
  import { proxy, subscribe } from "valtio/vanilla";
10
+
11
+ // src/lib/game/game-event-bus.ts
12
+ var GameEventBus = class {
13
+ listeners = /* @__PURE__ */ new Map();
14
+ /**
15
+ * Subscribe to an event type.
16
+ */
17
+ on(event, callback) {
18
+ if (!this.listeners.has(event)) {
19
+ this.listeners.set(event, /* @__PURE__ */ new Set());
20
+ }
21
+ this.listeners.get(event).add(callback);
22
+ return () => this.off(event, callback);
23
+ }
24
+ /**
25
+ * Unsubscribe from an event type.
26
+ */
27
+ off(event, callback) {
28
+ this.listeners.get(event)?.delete(callback);
29
+ }
30
+ /**
31
+ * Emit an event to all subscribers.
32
+ */
33
+ emit(event, payload) {
34
+ const callbacks = this.listeners.get(event);
35
+ if (!callbacks) return;
36
+ for (const cb of callbacks) {
37
+ try {
38
+ cb(payload);
39
+ } catch (e) {
40
+ console.error(`Error in event handler for ${event}`, e);
41
+ }
42
+ }
43
+ }
44
+ /**
45
+ * Clear all listeners.
46
+ */
47
+ dispose() {
48
+ this.listeners.clear();
49
+ }
50
+ };
51
+ var gameEventBus = new GameEventBus();
52
+
53
+ // src/lib/game/game-state.ts
10
54
  var state = proxy({
11
55
  id: "",
12
56
  globals: {},
@@ -28,7 +72,8 @@ import {
28
72
  defineSystem,
29
73
  defineQuery,
30
74
  defineComponent,
31
- Types
75
+ Types,
76
+ removeQuery
32
77
  } from "bitecs";
33
78
  import { Quaternion } from "three";
34
79
  var position = defineComponent({
@@ -47,52 +92,49 @@ var scale = defineComponent({
47
92
  y: Types.f32,
48
93
  z: Types.f32
49
94
  });
95
+ var _tempQuaternion = new Quaternion();
50
96
  function createTransformSystem(stage) {
51
- const transformQuery = defineQuery([position, rotation]);
97
+ const queryTerms = [position, rotation];
98
+ const transformQuery = defineQuery(queryTerms);
52
99
  const stageEntities = stage._childrenMap;
53
- return defineSystem((world) => {
100
+ const system = defineSystem((world) => {
54
101
  const entities = transformQuery(world);
55
102
  if (stageEntities === void 0) {
56
103
  return world;
57
104
  }
58
- ;
59
- for (const [key, value] of stageEntities) {
60
- const id = entities[key];
61
- const stageEntity = value;
62
- if (stageEntity === void 0 || !stageEntity?.body || stageEntity.markedForRemoval) {
105
+ for (const [key, stageEntity] of stageEntities) {
106
+ if (!stageEntity?.body || stageEntity.markedForRemoval) {
63
107
  continue;
64
108
  }
65
- const { x, y, z } = stageEntity.body.translation();
66
- position.x[id] = x;
67
- position.y[id] = y;
68
- position.z[id] = z;
69
- if (stageEntity.group) {
70
- stageEntity.group.position.set(position.x[id], position.y[id], position.z[id]);
71
- } else if (stageEntity.mesh) {
72
- stageEntity.mesh.position.set(position.x[id], position.y[id], position.z[id]);
109
+ const id = entities[key];
110
+ const body = stageEntity.body;
111
+ const target = stageEntity.group ?? stageEntity.mesh;
112
+ const translation = body.translation();
113
+ position.x[id] = translation.x;
114
+ position.y[id] = translation.y;
115
+ position.z[id] = translation.z;
116
+ if (target) {
117
+ target.position.set(translation.x, translation.y, translation.z);
73
118
  }
74
119
  if (stageEntity.controlledRotation) {
75
120
  continue;
76
121
  }
77
- const { x: rx, y: ry, z: rz, w: rw } = stageEntity.body.rotation();
78
- rotation.x[id] = rx;
79
- rotation.y[id] = ry;
80
- rotation.z[id] = rz;
81
- rotation.w[id] = rw;
82
- const newRotation = new Quaternion(
83
- rotation.x[id],
84
- rotation.y[id],
85
- rotation.z[id],
86
- rotation.w[id]
87
- );
88
- if (stageEntity.group) {
89
- stageEntity.group.setRotationFromQuaternion(newRotation);
90
- } else if (stageEntity.mesh) {
91
- stageEntity.mesh.setRotationFromQuaternion(newRotation);
122
+ const rot = body.rotation();
123
+ rotation.x[id] = rot.x;
124
+ rotation.y[id] = rot.y;
125
+ rotation.z[id] = rot.z;
126
+ rotation.w[id] = rot.w;
127
+ if (target) {
128
+ _tempQuaternion.set(rot.x, rot.y, rot.z, rot.w);
129
+ target.setRotationFromQuaternion(_tempQuaternion);
92
130
  }
93
131
  }
94
132
  return world;
95
133
  });
134
+ const destroy = (world) => {
135
+ removeQuery(world, transformQuery);
136
+ };
137
+ return { system, destroy };
96
138
  }
97
139
 
98
140
  // src/lib/core/flags.ts
@@ -108,21 +150,76 @@ var BaseNode = class _BaseNode {
108
150
  uuid = "";
109
151
  name = "";
110
152
  markedForRemoval = false;
111
- setup = () => {
112
- };
113
- loaded = () => {
114
- };
115
- update = () => {
116
- };
117
- destroy = () => {
118
- };
119
- cleanup = () => {
153
+ /**
154
+ * Lifecycle callback arrays - use onSetup(), onUpdate(), etc. to add callbacks
155
+ */
156
+ lifecycleCallbacks = {
157
+ setup: [],
158
+ loaded: [],
159
+ update: [],
160
+ destroy: [],
161
+ cleanup: []
120
162
  };
121
163
  constructor(args = []) {
122
164
  const options = args.filter((arg) => !(arg instanceof _BaseNode)).reduce((acc, opt) => ({ ...acc, ...opt }), {});
123
165
  this.options = options;
124
166
  this.uuid = nanoid();
125
167
  }
168
+ // ─────────────────────────────────────────────────────────────────────────────
169
+ // Fluent API for adding lifecycle callbacks
170
+ // ─────────────────────────────────────────────────────────────────────────────
171
+ /**
172
+ * Add setup callbacks to be executed in order during nodeSetup
173
+ */
174
+ onSetup(...callbacks) {
175
+ this.lifecycleCallbacks.setup.push(...callbacks);
176
+ return this;
177
+ }
178
+ /**
179
+ * Add loaded callbacks to be executed in order during nodeLoaded
180
+ */
181
+ onLoaded(...callbacks) {
182
+ this.lifecycleCallbacks.loaded.push(...callbacks);
183
+ return this;
184
+ }
185
+ /**
186
+ * Add update callbacks to be executed in order during nodeUpdate
187
+ */
188
+ onUpdate(...callbacks) {
189
+ this.lifecycleCallbacks.update.push(...callbacks);
190
+ return this;
191
+ }
192
+ /**
193
+ * Add destroy callbacks to be executed in order during nodeDestroy
194
+ */
195
+ onDestroy(...callbacks) {
196
+ this.lifecycleCallbacks.destroy.push(...callbacks);
197
+ return this;
198
+ }
199
+ /**
200
+ * Add cleanup callbacks to be executed in order during nodeCleanup
201
+ */
202
+ onCleanup(...callbacks) {
203
+ this.lifecycleCallbacks.cleanup.push(...callbacks);
204
+ return this;
205
+ }
206
+ /**
207
+ * Prepend setup callbacks (run before existing ones)
208
+ */
209
+ prependSetup(...callbacks) {
210
+ this.lifecycleCallbacks.setup.unshift(...callbacks);
211
+ return this;
212
+ }
213
+ /**
214
+ * Prepend update callbacks (run before existing ones)
215
+ */
216
+ prependUpdate(...callbacks) {
217
+ this.lifecycleCallbacks.update.unshift(...callbacks);
218
+ return this;
219
+ }
220
+ // ─────────────────────────────────────────────────────────────────────────────
221
+ // Tree structure
222
+ // ─────────────────────────────────────────────────────────────────────────────
126
223
  setParent(parent) {
127
224
  this.parent = parent;
128
225
  }
@@ -146,6 +243,9 @@ var BaseNode = class _BaseNode {
146
243
  isComposite() {
147
244
  return this.children.length > 0;
148
245
  }
246
+ // ─────────────────────────────────────────────────────────────────────────────
247
+ // Node lifecycle execution - runs internal + callback arrays
248
+ // ─────────────────────────────────────────────────────────────────────────────
149
249
  nodeSetup(params) {
150
250
  if (DEBUG_FLAG) {
151
251
  }
@@ -153,8 +253,8 @@ var BaseNode = class _BaseNode {
153
253
  if (typeof this._setup === "function") {
154
254
  this._setup(params);
155
255
  }
156
- if (this.setup) {
157
- this.setup(params);
256
+ for (const callback of this.lifecycleCallbacks.setup) {
257
+ callback(params);
158
258
  }
159
259
  this.children.forEach((child) => child.nodeSetup(params));
160
260
  }
@@ -165,21 +265,40 @@ var BaseNode = class _BaseNode {
165
265
  if (typeof this._update === "function") {
166
266
  this._update(params);
167
267
  }
168
- if (this.update) {
169
- this.update(params);
268
+ for (const callback of this.lifecycleCallbacks.update) {
269
+ callback(params);
170
270
  }
171
271
  this.children.forEach((child) => child.nodeUpdate(params));
172
272
  }
173
273
  nodeDestroy(params) {
174
274
  this.children.forEach((child) => child.nodeDestroy(params));
175
- if (this.destroy) {
176
- this.destroy(params);
275
+ for (const callback of this.lifecycleCallbacks.destroy) {
276
+ callback(params);
177
277
  }
178
278
  if (typeof this._destroy === "function") {
179
279
  this._destroy(params);
180
280
  }
181
281
  this.markedForRemoval = true;
182
282
  }
283
+ async nodeLoaded(params) {
284
+ if (typeof this._loaded === "function") {
285
+ await this._loaded(params);
286
+ }
287
+ for (const callback of this.lifecycleCallbacks.loaded) {
288
+ callback(params);
289
+ }
290
+ }
291
+ async nodeCleanup(params) {
292
+ for (const callback of this.lifecycleCallbacks.cleanup) {
293
+ callback(params);
294
+ }
295
+ if (typeof this._cleanup === "function") {
296
+ await this._cleanup(params);
297
+ }
298
+ }
299
+ // ─────────────────────────────────────────────────────────────────────────────
300
+ // Options
301
+ // ─────────────────────────────────────────────────────────────────────────────
183
302
  getOptions() {
184
303
  return this.options;
185
304
  }
@@ -201,11 +320,6 @@ var GameEntity = class extends BaseNode {
201
320
  custom = {};
202
321
  debugInfo = {};
203
322
  debugMaterial;
204
- lifeCycleDelegate = {
205
- setup: [],
206
- update: [],
207
- destroy: []
208
- };
209
323
  collisionDelegate = {
210
324
  collision: []
211
325
  };
@@ -230,67 +344,40 @@ var GameEntity = class extends BaseNode {
230
344
  this.name = this.options.name || "";
231
345
  return this;
232
346
  }
233
- onSetup(...callbacks) {
234
- const combineCallbacks = [...this.lifeCycleDelegate.setup ?? [], ...callbacks];
235
- this.lifeCycleDelegate = {
236
- ...this.lifeCycleDelegate,
237
- setup: combineCallbacks
238
- };
239
- return this;
240
- }
241
- onUpdate(...callbacks) {
242
- const combineCallbacks = [...this.lifeCycleDelegate.update ?? [], ...callbacks];
243
- this.lifeCycleDelegate = {
244
- ...this.lifeCycleDelegate,
245
- update: combineCallbacks
246
- };
247
- return this;
248
- }
249
- onDestroy(...callbacks) {
250
- this.lifeCycleDelegate = {
251
- ...this.lifeCycleDelegate,
252
- destroy: callbacks.length > 0 ? callbacks : void 0
253
- };
254
- return this;
255
- }
347
+ /**
348
+ * Add collision callbacks
349
+ */
256
350
  onCollision(...callbacks) {
257
- this.collisionDelegate = {
258
- collision: callbacks.length > 0 ? callbacks : void 0
259
- };
351
+ const existing = this.collisionDelegate.collision ?? [];
352
+ this.collisionDelegate.collision = [...existing, ...callbacks];
260
353
  return this;
261
354
  }
355
+ /**
356
+ * Entity-specific setup - runs behavior callbacks
357
+ * (User callbacks are handled by BaseNode's lifecycleCallbacks.setup)
358
+ */
262
359
  _setup(params) {
263
360
  this.behaviorCallbackMap.setup.forEach((callback) => {
264
361
  callback({ ...params, me: this });
265
362
  });
266
- if (this.lifeCycleDelegate.setup?.length) {
267
- const callbacks = this.lifeCycleDelegate.setup;
268
- callbacks.forEach((callback) => {
269
- callback({ ...params, me: this });
270
- });
271
- }
272
363
  }
273
364
  async _loaded(_params) {
274
365
  }
366
+ /**
367
+ * Entity-specific update - updates materials and runs behavior callbacks
368
+ * (User callbacks are handled by BaseNode's lifecycleCallbacks.update)
369
+ */
275
370
  _update(params) {
276
371
  this.updateMaterials(params);
277
- if (this.lifeCycleDelegate.update?.length) {
278
- const callbacks = this.lifeCycleDelegate.update;
279
- callbacks.forEach((callback) => {
280
- callback({ ...params, me: this });
281
- });
282
- }
283
372
  this.behaviorCallbackMap.update.forEach((callback) => {
284
373
  callback({ ...params, me: this });
285
374
  });
286
375
  }
376
+ /**
377
+ * Entity-specific destroy - runs behavior callbacks
378
+ * (User callbacks are handled by BaseNode's lifecycleCallbacks.destroy)
379
+ */
287
380
  _destroy(params) {
288
- if (this.lifeCycleDelegate.destroy?.length) {
289
- const callbacks = this.lifeCycleDelegate.destroy;
290
- callbacks.forEach((callback) => {
291
- callback({ ...params, me: this });
292
- });
293
- }
294
381
  this.behaviorCallbackMap.destroy.forEach((callback) => {
295
382
  callback({ ...params, me: this });
296
383
  });
@@ -481,6 +568,23 @@ var AnimationDelegate = class {
481
568
  this._currentAction = action;
482
569
  this._currentKey = key;
483
570
  }
571
+ /**
572
+ * Dispose of all animation resources
573
+ */
574
+ dispose() {
575
+ Object.values(this._actions).forEach((action) => {
576
+ action.stop();
577
+ });
578
+ if (this._mixer) {
579
+ this._mixer.stopAllAction();
580
+ this._mixer.uncacheRoot(this.target);
581
+ this._mixer = null;
582
+ }
583
+ this._actions = {};
584
+ this._animations = [];
585
+ this._currentAction = null;
586
+ this._currentKey = "";
587
+ }
484
588
  get currentAnimationKey() {
485
589
  return this._currentKey;
486
590
  }
@@ -506,7 +610,7 @@ var actorDefaults = {
506
610
  animations: [],
507
611
  models: []
508
612
  };
509
- var ACTOR_TYPE = Symbol("Actor");
613
+ var ACTOR_TYPE = /* @__PURE__ */ Symbol("Actor");
510
614
  var ZylemActor = class extends GameEntity {
511
615
  static type = ACTOR_TYPE;
512
616
  _object = null;
@@ -517,9 +621,7 @@ var ZylemActor = class extends GameEntity {
517
621
  constructor(options) {
518
622
  super();
519
623
  this.options = { ...actorDefaults, ...options };
520
- this.lifeCycleDelegate = {
521
- update: [this.actorUpdate.bind(this)]
522
- };
624
+ this.prependUpdate(this.actorUpdate.bind(this));
523
625
  this.controlledRotation = true;
524
626
  }
525
627
  async load() {
@@ -539,6 +641,34 @@ var ZylemActor = class extends GameEntity {
539
641
  async actorUpdate(params) {
540
642
  this._animationDelegate?.update(params.delta);
541
643
  }
644
+ /**
645
+ * Clean up actor resources including animations, models, and groups
646
+ */
647
+ actorDestroy() {
648
+ if (this._animationDelegate) {
649
+ this._animationDelegate.dispose();
650
+ this._animationDelegate = null;
651
+ }
652
+ if (this._object) {
653
+ this._object.traverse((child) => {
654
+ if (child.isMesh) {
655
+ const mesh = child;
656
+ mesh.geometry?.dispose();
657
+ if (Array.isArray(mesh.material)) {
658
+ mesh.material.forEach((m) => m.dispose());
659
+ } else if (mesh.material) {
660
+ mesh.material.dispose();
661
+ }
662
+ }
663
+ });
664
+ this._object = null;
665
+ }
666
+ if (this.group) {
667
+ this.group.clear();
668
+ this.group = null;
669
+ }
670
+ this._modelFileNames = [];
671
+ }
542
672
  async loadModels() {
543
673
  if (this._modelFileNames.length === 0) return;
544
674
  const promises = this._modelFileNames.map((file) => this._assetLoader.loadFile(file));
@@ -596,12 +726,10 @@ var ZylemActor = class extends GameEntity {
596
726
  }
597
727
  };
598
728
 
599
- // src/lib/collision/collision-delegate.ts
729
+ // src/lib/collision/world.ts
600
730
  function isCollisionHandlerDelegate(obj) {
601
731
  return typeof obj?.handlePostCollision === "function" && typeof obj?.handleIntersectionEvent === "function";
602
732
  }
603
-
604
- // src/lib/collision/world.ts
605
733
  var ZylemWorld = class {
606
734
  type = "World";
607
735
  world;
@@ -827,7 +955,11 @@ var ZylemScene = class {
827
955
  * Setup camera with the scene
828
956
  */
829
957
  setupCamera(scene, camera) {
830
- scene.add(camera.cameraRig);
958
+ if (camera.cameraRig) {
959
+ scene.add(camera.cameraRig);
960
+ } else {
961
+ scene.add(camera.camera);
962
+ }
831
963
  camera.setup(scene);
832
964
  }
833
965
  /**
@@ -922,6 +1054,9 @@ var ZylemBlueColor = new Color4("#0333EC");
922
1054
  var Vec0 = new Vector34(0, 0, 0);
923
1055
  var Vec1 = new Vector34(1, 1, 1);
924
1056
 
1057
+ // src/lib/stage/zylem-stage.ts
1058
+ import { subscribe as subscribe4 } from "valtio/vanilla";
1059
+
925
1060
  // src/lib/core/lifecycle-base.ts
926
1061
  var LifeCycleBase = class {
927
1062
  update = () => {
@@ -959,67 +1094,334 @@ var LifeCycleBase = class {
959
1094
  // src/lib/stage/zylem-stage.ts
960
1095
  import { nanoid as nanoid2 } from "nanoid";
961
1096
 
962
- // src/lib/camera/perspective.ts
963
- var Perspectives = {
964
- FirstPerson: "first-person",
965
- ThirdPerson: "third-person",
966
- Isometric: "isometric",
967
- Flat2D: "flat-2d",
968
- Fixed2D: "fixed-2d"
969
- };
970
-
971
- // src/lib/camera/camera.ts
972
- import { Vector2 as Vector23, Vector3 as Vector37 } from "three";
973
-
974
- // src/lib/camera/zylem-camera.ts
975
- import { PerspectiveCamera, Vector3 as Vector36, Object3D as Object3D4, OrthographicCamera, WebGLRenderer as WebGLRenderer3 } from "three";
976
- import { OrbitControls } from "three/addons/controls/OrbitControls.js";
1097
+ // src/lib/stage/stage-debug-delegate.ts
1098
+ import { Ray } from "@dimforge/rapier3d-compat";
1099
+ import { BufferAttribute, BufferGeometry as BufferGeometry2, LineBasicMaterial as LineBasicMaterial2, LineSegments as LineSegments2, Raycaster, Vector2 } from "three";
977
1100
 
978
- // src/lib/camera/third-person.ts
979
- import { Vector3 as Vector35 } from "three";
980
- var ThirdPersonCamera = class {
981
- distance;
982
- screenResolution = null;
983
- renderer = null;
984
- scene = null;
985
- cameraRef = null;
986
- constructor() {
987
- this.distance = new Vector35(0, 5, 8);
988
- }
989
- /**
990
- * Setup the third person camera controller
991
- */
992
- setup(params) {
993
- const { screenResolution, renderer, scene, camera } = params;
994
- this.screenResolution = screenResolution;
995
- this.renderer = renderer;
1101
+ // src/lib/stage/debug-entity-cursor.ts
1102
+ import {
1103
+ Box3,
1104
+ BoxGeometry,
1105
+ Color as Color5,
1106
+ EdgesGeometry,
1107
+ Group as Group3,
1108
+ LineBasicMaterial,
1109
+ LineSegments,
1110
+ Mesh as Mesh2,
1111
+ MeshBasicMaterial,
1112
+ Vector3 as Vector35
1113
+ } from "three";
1114
+ var DebugEntityCursor = class {
1115
+ scene;
1116
+ container;
1117
+ fillMesh;
1118
+ edgeLines;
1119
+ currentColor = new Color5(65280);
1120
+ bbox = new Box3();
1121
+ size = new Vector35();
1122
+ center = new Vector35();
1123
+ constructor(scene) {
996
1124
  this.scene = scene;
997
- this.cameraRef = camera;
1125
+ const initialGeometry = new BoxGeometry(1, 1, 1);
1126
+ this.fillMesh = new Mesh2(
1127
+ initialGeometry,
1128
+ new MeshBasicMaterial({
1129
+ color: this.currentColor,
1130
+ transparent: true,
1131
+ opacity: 0.12,
1132
+ depthWrite: false
1133
+ })
1134
+ );
1135
+ const edges = new EdgesGeometry(initialGeometry);
1136
+ this.edgeLines = new LineSegments(
1137
+ edges,
1138
+ new LineBasicMaterial({ color: this.currentColor, linewidth: 1 })
1139
+ );
1140
+ this.container = new Group3();
1141
+ this.container.name = "DebugEntityCursor";
1142
+ this.container.add(this.fillMesh);
1143
+ this.container.add(this.edgeLines);
1144
+ this.container.visible = false;
1145
+ this.scene.add(this.container);
1146
+ }
1147
+ setColor(color) {
1148
+ this.currentColor.set(color);
1149
+ this.fillMesh.material.color.set(this.currentColor);
1150
+ this.edgeLines.material.color.set(this.currentColor);
998
1151
  }
999
1152
  /**
1000
- * Update the third person camera
1153
+ * Update the cursor to enclose the provided Object3D using a world-space AABB.
1001
1154
  */
1002
- update(delta) {
1003
- if (!this.cameraRef.target) {
1155
+ updateFromObject(object) {
1156
+ if (!object) {
1157
+ this.hide();
1004
1158
  return;
1005
1159
  }
1006
- const desiredCameraPosition = this.cameraRef.target.group.position.clone().add(this.distance);
1007
- this.cameraRef.camera.position.lerp(desiredCameraPosition, 0.1);
1008
- this.cameraRef.camera.lookAt(this.cameraRef.target.group.position);
1009
- }
1010
- /**
1011
- * Handle resize events
1012
- */
1013
- resize(width, height) {
1014
- if (this.screenResolution) {
1015
- this.screenResolution.set(width, height);
1160
+ this.bbox.setFromObject(object);
1161
+ if (!isFinite(this.bbox.min.x) || !isFinite(this.bbox.max.x)) {
1162
+ this.hide();
1163
+ return;
1016
1164
  }
1165
+ this.bbox.getSize(this.size);
1166
+ this.bbox.getCenter(this.center);
1167
+ const newGeom = new BoxGeometry(
1168
+ Math.max(this.size.x, 1e-6),
1169
+ Math.max(this.size.y, 1e-6),
1170
+ Math.max(this.size.z, 1e-6)
1171
+ );
1172
+ this.fillMesh.geometry.dispose();
1173
+ this.fillMesh.geometry = newGeom;
1174
+ const newEdges = new EdgesGeometry(newGeom);
1175
+ this.edgeLines.geometry.dispose();
1176
+ this.edgeLines.geometry = newEdges;
1177
+ this.container.position.copy(this.center);
1178
+ this.container.visible = true;
1017
1179
  }
1018
- /**
1019
- * Set the distance from the target
1020
- */
1021
- setDistance(distance) {
1022
- this.distance = distance;
1180
+ hide() {
1181
+ this.container.visible = false;
1182
+ }
1183
+ dispose() {
1184
+ this.scene.remove(this.container);
1185
+ this.fillMesh.geometry.dispose();
1186
+ this.fillMesh.material.dispose();
1187
+ this.edgeLines.geometry.dispose();
1188
+ this.edgeLines.material.dispose();
1189
+ }
1190
+ };
1191
+
1192
+ // src/lib/stage/stage-debug-delegate.ts
1193
+ var SELECT_TOOL_COLOR = 2293538;
1194
+ var DELETE_TOOL_COLOR = 16724787;
1195
+ var StageDebugDelegate = class {
1196
+ stage;
1197
+ options;
1198
+ mouseNdc = new Vector2(-2, -2);
1199
+ raycaster = new Raycaster();
1200
+ isMouseDown = false;
1201
+ disposeFns = [];
1202
+ debugCursor = null;
1203
+ debugLines = null;
1204
+ constructor(stage, options) {
1205
+ this.stage = stage;
1206
+ this.options = {
1207
+ maxRayDistance: options?.maxRayDistance ?? 5e3,
1208
+ addEntityFactory: options?.addEntityFactory ?? null
1209
+ };
1210
+ if (this.stage.scene) {
1211
+ this.debugLines = new LineSegments2(
1212
+ new BufferGeometry2(),
1213
+ new LineBasicMaterial2({ vertexColors: true })
1214
+ );
1215
+ this.stage.scene.scene.add(this.debugLines);
1216
+ this.debugLines.visible = true;
1217
+ this.debugCursor = new DebugEntityCursor(this.stage.scene.scene);
1218
+ }
1219
+ this.attachDomListeners();
1220
+ }
1221
+ update() {
1222
+ if (!debugState.enabled) return;
1223
+ if (!this.stage.scene || !this.stage.world || !this.stage.cameraRef) return;
1224
+ const { world, cameraRef } = this.stage;
1225
+ if (this.debugLines) {
1226
+ const { vertices, colors } = world.world.debugRender();
1227
+ this.debugLines.geometry.setAttribute("position", new BufferAttribute(vertices, 3));
1228
+ this.debugLines.geometry.setAttribute("color", new BufferAttribute(colors, 4));
1229
+ }
1230
+ const tool = getDebugTool();
1231
+ const isCursorTool = tool === "select" || tool === "delete";
1232
+ this.raycaster.setFromCamera(this.mouseNdc, cameraRef.camera);
1233
+ const origin = this.raycaster.ray.origin.clone();
1234
+ const direction = this.raycaster.ray.direction.clone().normalize();
1235
+ const rapierRay = new Ray(
1236
+ { x: origin.x, y: origin.y, z: origin.z },
1237
+ { x: direction.x, y: direction.y, z: direction.z }
1238
+ );
1239
+ const hit = world.world.castRay(rapierRay, this.options.maxRayDistance, true);
1240
+ if (hit && isCursorTool) {
1241
+ const rigidBody = hit.collider?._parent;
1242
+ const hoveredUuid2 = rigidBody?.userData?.uuid;
1243
+ if (hoveredUuid2) {
1244
+ const entity = this.stage._debugMap.get(hoveredUuid2);
1245
+ if (entity) setHoveredEntity(entity);
1246
+ } else {
1247
+ resetHoveredEntity();
1248
+ }
1249
+ if (this.isMouseDown) {
1250
+ this.handleActionOnHit(hoveredUuid2 ?? null, origin, direction, hit.toi);
1251
+ }
1252
+ }
1253
+ this.isMouseDown = false;
1254
+ const hoveredUuid = getHoveredEntity();
1255
+ if (!hoveredUuid) {
1256
+ this.debugCursor?.hide();
1257
+ return;
1258
+ }
1259
+ const hoveredEntity = this.stage._debugMap.get(`${hoveredUuid}`);
1260
+ const targetObject = hoveredEntity?.group ?? hoveredEntity?.mesh ?? null;
1261
+ if (!targetObject) {
1262
+ this.debugCursor?.hide();
1263
+ return;
1264
+ }
1265
+ switch (tool) {
1266
+ case "select":
1267
+ this.debugCursor?.setColor(SELECT_TOOL_COLOR);
1268
+ break;
1269
+ case "delete":
1270
+ this.debugCursor?.setColor(DELETE_TOOL_COLOR);
1271
+ break;
1272
+ default:
1273
+ this.debugCursor?.setColor(16777215);
1274
+ break;
1275
+ }
1276
+ this.debugCursor?.updateFromObject(targetObject);
1277
+ }
1278
+ dispose() {
1279
+ this.disposeFns.forEach((fn) => fn());
1280
+ this.disposeFns = [];
1281
+ this.debugCursor?.dispose();
1282
+ if (this.debugLines && this.stage.scene) {
1283
+ this.stage.scene.scene.remove(this.debugLines);
1284
+ this.debugLines.geometry.dispose();
1285
+ this.debugLines.material.dispose();
1286
+ this.debugLines = null;
1287
+ }
1288
+ }
1289
+ handleActionOnHit(hoveredUuid, origin, direction, toi) {
1290
+ const tool = getDebugTool();
1291
+ switch (tool) {
1292
+ case "select": {
1293
+ if (hoveredUuid) {
1294
+ const entity = this.stage._debugMap.get(hoveredUuid);
1295
+ if (entity) setSelectedEntity(entity);
1296
+ }
1297
+ break;
1298
+ }
1299
+ case "delete": {
1300
+ if (hoveredUuid) {
1301
+ this.stage.removeEntityByUuid(hoveredUuid);
1302
+ }
1303
+ break;
1304
+ }
1305
+ case "scale": {
1306
+ if (!this.options.addEntityFactory) break;
1307
+ const hitPosition = origin.clone().add(direction.clone().multiplyScalar(toi));
1308
+ const newNode = this.options.addEntityFactory({ position: hitPosition });
1309
+ if (newNode) {
1310
+ Promise.resolve(newNode).then((node) => {
1311
+ if (node) this.stage.spawnEntity(node);
1312
+ }).catch(() => {
1313
+ });
1314
+ }
1315
+ break;
1316
+ }
1317
+ default:
1318
+ break;
1319
+ }
1320
+ }
1321
+ attachDomListeners() {
1322
+ const canvas = this.stage.cameraRef?.renderer.domElement ?? this.stage.scene?.zylemCamera.renderer.domElement;
1323
+ if (!canvas) return;
1324
+ const onMouseMove = (e) => {
1325
+ const rect = canvas.getBoundingClientRect();
1326
+ const x = (e.clientX - rect.left) / rect.width * 2 - 1;
1327
+ const y = -((e.clientY - rect.top) / rect.height * 2 - 1);
1328
+ this.mouseNdc.set(x, y);
1329
+ };
1330
+ const onMouseDown = (e) => {
1331
+ this.isMouseDown = true;
1332
+ };
1333
+ canvas.addEventListener("mousemove", onMouseMove);
1334
+ canvas.addEventListener("mousedown", onMouseDown);
1335
+ this.disposeFns.push(() => canvas.removeEventListener("mousemove", onMouseMove));
1336
+ this.disposeFns.push(() => canvas.removeEventListener("mousedown", onMouseDown));
1337
+ }
1338
+ };
1339
+
1340
+ // src/lib/stage/stage-camera-debug-delegate.ts
1341
+ import { subscribe as subscribe3 } from "valtio/vanilla";
1342
+ var StageCameraDebugDelegate = class {
1343
+ stage;
1344
+ constructor(stage) {
1345
+ this.stage = stage;
1346
+ }
1347
+ subscribe(listener) {
1348
+ const notify = () => listener(this.snapshot());
1349
+ notify();
1350
+ return subscribe3(debugState, notify);
1351
+ }
1352
+ resolveTarget(uuid) {
1353
+ const entity = this.stage._debugMap.get(uuid) || this.stage.world?.collisionMap.get(uuid) || null;
1354
+ const target = entity?.group ?? entity?.mesh ?? null;
1355
+ return target ?? null;
1356
+ }
1357
+ snapshot() {
1358
+ return {
1359
+ enabled: debugState.enabled,
1360
+ selected: debugState.selectedEntity ? [debugState.selectedEntity.uuid] : []
1361
+ };
1362
+ }
1363
+ };
1364
+
1365
+ // src/lib/stage/stage-camera-delegate.ts
1366
+ import { Vector2 as Vector24 } from "three";
1367
+
1368
+ // src/lib/camera/zylem-camera.ts
1369
+ import { PerspectiveCamera, Vector3 as Vector39, Object3D as Object3D6, OrthographicCamera, WebGLRenderer as WebGLRenderer3 } from "three";
1370
+
1371
+ // src/lib/camera/perspective.ts
1372
+ var Perspectives = {
1373
+ FirstPerson: "first-person",
1374
+ ThirdPerson: "third-person",
1375
+ Isometric: "isometric",
1376
+ Flat2D: "flat-2d",
1377
+ Fixed2D: "fixed-2d"
1378
+ };
1379
+
1380
+ // src/lib/camera/third-person.ts
1381
+ import { Vector3 as Vector37 } from "three";
1382
+ var ThirdPersonCamera = class {
1383
+ distance;
1384
+ screenResolution = null;
1385
+ renderer = null;
1386
+ scene = null;
1387
+ cameraRef = null;
1388
+ constructor() {
1389
+ this.distance = new Vector37(0, 5, 8);
1390
+ }
1391
+ /**
1392
+ * Setup the third person camera controller
1393
+ */
1394
+ setup(params) {
1395
+ const { screenResolution, renderer, scene, camera } = params;
1396
+ this.screenResolution = screenResolution;
1397
+ this.renderer = renderer;
1398
+ this.scene = scene;
1399
+ this.cameraRef = camera;
1400
+ }
1401
+ /**
1402
+ * Update the third person camera
1403
+ */
1404
+ update(delta) {
1405
+ if (!this.cameraRef.target) {
1406
+ return;
1407
+ }
1408
+ const desiredCameraPosition = this.cameraRef.target.group.position.clone().add(this.distance);
1409
+ this.cameraRef.camera.position.lerp(desiredCameraPosition, 0.1);
1410
+ this.cameraRef.camera.lookAt(this.cameraRef.target.group.position);
1411
+ }
1412
+ /**
1413
+ * Handle resize events
1414
+ */
1415
+ resize(width, height) {
1416
+ if (this.screenResolution) {
1417
+ this.screenResolution.set(width, height);
1418
+ }
1419
+ }
1420
+ /**
1421
+ * Set the distance from the target
1422
+ */
1423
+ setDistance(distance) {
1424
+ this.distance = distance;
1023
1425
  }
1024
1426
  };
1025
1427
 
@@ -1146,109 +1548,42 @@ var RenderPass = class extends Pass {
1146
1548
  }
1147
1549
  };
1148
1550
 
1149
- // src/lib/camera/zylem-camera.ts
1150
- var ZylemCamera = class {
1151
- cameraRig;
1551
+ // src/lib/camera/camera-debug-delegate.ts
1552
+ import { Vector3 as Vector38 } from "three";
1553
+ import { OrbitControls } from "three/addons/controls/OrbitControls.js";
1554
+ var CameraOrbitController = class {
1152
1555
  camera;
1153
- screenResolution;
1154
- renderer;
1155
- composer;
1156
- _perspective;
1556
+ domElement;
1157
1557
  orbitControls = null;
1158
- target = null;
1159
- sceneRef = null;
1160
- frustumSize = 10;
1161
- // Perspective controller delegation
1162
- perspectiveController = null;
1558
+ orbitTarget = null;
1559
+ orbitTargetWorldPos = new Vector38();
1163
1560
  debugDelegate = null;
1164
1561
  debugUnsubscribe = null;
1165
1562
  debugStateSnapshot = { enabled: false, selected: [] };
1166
- orbitTarget = null;
1167
- orbitTargetWorldPos = new Vector36();
1168
- constructor(perspective, screenResolution, frustumSize = 10) {
1169
- this._perspective = perspective;
1170
- this.screenResolution = screenResolution;
1171
- this.frustumSize = frustumSize;
1172
- this.renderer = new WebGLRenderer3({ antialias: false, alpha: true });
1173
- this.renderer.setSize(screenResolution.x, screenResolution.y);
1174
- this.renderer.shadowMap.enabled = true;
1175
- this.composer = new EffectComposer(this.renderer);
1176
- const aspectRatio = screenResolution.x / screenResolution.y;
1177
- this.camera = this.createCameraForPerspective(aspectRatio);
1178
- this.cameraRig = new Object3D4();
1179
- this.cameraRig.position.set(0, 3, 10);
1180
- this.cameraRig.add(this.camera);
1181
- this.camera.lookAt(new Vector36(0, 2, 0));
1182
- this.initializePerspectiveController();
1563
+ // Saved camera state for restoration when exiting debug mode
1564
+ savedCameraPosition = null;
1565
+ savedCameraQuaternion = null;
1566
+ savedCameraZoom = null;
1567
+ constructor(camera, domElement) {
1568
+ this.camera = camera;
1569
+ this.domElement = domElement;
1183
1570
  }
1184
1571
  /**
1185
- * Setup the camera with a scene
1572
+ * Check if debug mode is currently active (orbit controls enabled).
1186
1573
  */
1187
- async setup(scene) {
1188
- this.sceneRef = scene;
1189
- if (this.orbitControls === null) {
1190
- this.orbitControls = new OrbitControls(this.camera, this.renderer.domElement);
1191
- this.orbitControls.enableDamping = true;
1192
- this.orbitControls.dampingFactor = 0.05;
1193
- this.orbitControls.screenSpacePanning = false;
1194
- this.orbitControls.minDistance = 1;
1195
- this.orbitControls.maxDistance = 500;
1196
- this.orbitControls.maxPolarAngle = Math.PI / 2;
1197
- }
1198
- let renderResolution = this.screenResolution.clone().divideScalar(2);
1199
- renderResolution.x |= 0;
1200
- renderResolution.y |= 0;
1201
- const pass = new RenderPass(renderResolution, scene, this.camera);
1202
- this.composer.addPass(pass);
1203
- if (this.perspectiveController) {
1204
- this.perspectiveController.setup({
1205
- screenResolution: this.screenResolution,
1206
- renderer: this.renderer,
1207
- scene,
1208
- camera: this
1209
- });
1210
- }
1211
- this.renderer.setAnimationLoop((delta) => {
1212
- this.update(delta || 0);
1213
- });
1574
+ get isActive() {
1575
+ return this.debugStateSnapshot.enabled;
1214
1576
  }
1215
1577
  /**
1216
- * Update camera and render
1578
+ * Update orbit controls each frame.
1579
+ * Should be called from the camera's update loop.
1217
1580
  */
1218
- update(delta) {
1581
+ update() {
1219
1582
  if (this.orbitControls && this.orbitTarget) {
1220
1583
  this.orbitTarget.getWorldPosition(this.orbitTargetWorldPos);
1221
1584
  this.orbitControls.target.copy(this.orbitTargetWorldPos);
1222
1585
  }
1223
1586
  this.orbitControls?.update();
1224
- if (this.perspectiveController) {
1225
- this.perspectiveController.update(delta);
1226
- }
1227
- this.composer.render(delta);
1228
- }
1229
- /**
1230
- * Dispose renderer, composer, controls, and detach from scene
1231
- */
1232
- destroy() {
1233
- try {
1234
- this.renderer.setAnimationLoop(null);
1235
- } catch {
1236
- }
1237
- try {
1238
- this.disableOrbitControls();
1239
- } catch {
1240
- }
1241
- try {
1242
- this.composer?.passes?.forEach((p) => p.dispose?.());
1243
- this.composer?.dispose?.();
1244
- } catch {
1245
- }
1246
- try {
1247
- this.renderer.dispose();
1248
- } catch {
1249
- }
1250
- this.detachDebugDelegate();
1251
- this.sceneRef = null;
1252
1587
  }
1253
1588
  /**
1254
1589
  * Attach a delegate to react to debug state changes.
@@ -1271,135 +1606,48 @@ var ZylemCamera = class {
1271
1606
  };
1272
1607
  }
1273
1608
  /**
1274
- * Resize camera and renderer
1609
+ * Clean up resources.
1275
1610
  */
1276
- resize(width, height) {
1277
- this.screenResolution.set(width, height);
1278
- this.renderer.setSize(width, height, false);
1279
- this.composer.setSize(width, height);
1280
- if (this.camera instanceof PerspectiveCamera) {
1281
- this.camera.aspect = width / height;
1282
- this.camera.updateProjectionMatrix();
1283
- }
1284
- if (this.perspectiveController) {
1285
- this.perspectiveController.resize(width, height);
1286
- }
1287
- }
1288
- /**
1289
- * Update renderer pixel ratio (DPR)
1290
- */
1291
- setPixelRatio(dpr) {
1292
- const safe = Math.max(1, Number.isFinite(dpr) ? dpr : 1);
1293
- this.renderer.setPixelRatio(safe);
1294
- }
1295
- /**
1296
- * Create camera based on perspective type
1297
- */
1298
- createCameraForPerspective(aspectRatio) {
1299
- switch (this._perspective) {
1300
- case Perspectives.ThirdPerson:
1301
- return this.createThirdPersonCamera(aspectRatio);
1302
- case Perspectives.FirstPerson:
1303
- return this.createFirstPersonCamera(aspectRatio);
1304
- case Perspectives.Isometric:
1305
- return this.createIsometricCamera(aspectRatio);
1306
- case Perspectives.Flat2D:
1307
- return this.createFlat2DCamera(aspectRatio);
1308
- case Perspectives.Fixed2D:
1309
- return this.createFixed2DCamera(aspectRatio);
1310
- default:
1311
- return this.createThirdPersonCamera(aspectRatio);
1312
- }
1313
- }
1314
- /**
1315
- * Initialize perspective-specific controller
1316
- */
1317
- initializePerspectiveController() {
1318
- switch (this._perspective) {
1319
- case Perspectives.ThirdPerson:
1320
- this.perspectiveController = new ThirdPersonCamera();
1321
- break;
1322
- case Perspectives.Fixed2D:
1323
- this.perspectiveController = new Fixed2DCamera();
1324
- break;
1325
- default:
1326
- this.perspectiveController = new ThirdPersonCamera();
1327
- }
1328
- }
1329
- createThirdPersonCamera(aspectRatio) {
1330
- return new PerspectiveCamera(75, aspectRatio, 0.1, 1e3);
1331
- }
1332
- createFirstPersonCamera(aspectRatio) {
1333
- return new PerspectiveCamera(75, aspectRatio, 0.1, 1e3);
1334
- }
1335
- createIsometricCamera(aspectRatio) {
1336
- return new OrthographicCamera(
1337
- this.frustumSize * aspectRatio / -2,
1338
- this.frustumSize * aspectRatio / 2,
1339
- this.frustumSize / 2,
1340
- this.frustumSize / -2,
1341
- 1,
1342
- 1e3
1343
- );
1344
- }
1345
- createFlat2DCamera(aspectRatio) {
1346
- return new OrthographicCamera(
1347
- this.frustumSize * aspectRatio / -2,
1348
- this.frustumSize * aspectRatio / 2,
1349
- this.frustumSize / 2,
1350
- this.frustumSize / -2,
1351
- 1,
1352
- 1e3
1353
- );
1354
- }
1355
- createFixed2DCamera(aspectRatio) {
1356
- return this.createFlat2DCamera(aspectRatio);
1357
- }
1358
- // Movement methods
1359
- moveCamera(position2) {
1360
- if (this._perspective === Perspectives.Flat2D || this._perspective === Perspectives.Fixed2D) {
1361
- this.frustumSize = position2.z;
1362
- }
1363
- this.cameraRig.position.set(position2.x, position2.y, position2.z);
1364
- }
1365
- move(position2) {
1366
- this.moveCamera(position2);
1367
- }
1368
- rotate(pitch, yaw, roll) {
1369
- this.cameraRig.rotateX(pitch);
1370
- this.cameraRig.rotateY(yaw);
1371
- this.cameraRig.rotateZ(roll);
1611
+ dispose() {
1612
+ this.disableOrbitControls();
1613
+ this.detachDebugDelegate();
1372
1614
  }
1373
1615
  /**
1374
- * Get the DOM element for the renderer
1616
+ * Get the current debug state snapshot.
1375
1617
  */
1376
- getDomElement() {
1377
- return this.renderer.domElement;
1618
+ get debugState() {
1619
+ return this.debugStateSnapshot;
1378
1620
  }
1379
1621
  applyDebugState(state2) {
1622
+ const wasEnabled = this.debugStateSnapshot.enabled;
1380
1623
  this.debugStateSnapshot = {
1381
1624
  enabled: state2.enabled,
1382
1625
  selected: [...state2.selected]
1383
1626
  };
1384
- if (state2.enabled) {
1627
+ if (state2.enabled && !wasEnabled) {
1628
+ this.saveCameraState();
1385
1629
  this.enableOrbitControls();
1386
1630
  this.updateOrbitTargetFromSelection(state2.selected);
1387
- } else {
1631
+ } else if (!state2.enabled && wasEnabled) {
1388
1632
  this.orbitTarget = null;
1389
1633
  this.disableOrbitControls();
1634
+ this.restoreCameraState();
1635
+ } else if (state2.enabled) {
1636
+ this.updateOrbitTargetFromSelection(state2.selected);
1390
1637
  }
1391
1638
  }
1392
1639
  enableOrbitControls() {
1393
1640
  if (this.orbitControls) {
1394
1641
  return;
1395
1642
  }
1396
- this.orbitControls = new OrbitControls(this.camera, this.renderer.domElement);
1643
+ this.orbitControls = new OrbitControls(this.camera, this.domElement);
1397
1644
  this.orbitControls.enableDamping = true;
1398
1645
  this.orbitControls.dampingFactor = 0.05;
1399
1646
  this.orbitControls.screenSpacePanning = false;
1400
1647
  this.orbitControls.minDistance = 1;
1401
1648
  this.orbitControls.maxDistance = 500;
1402
1649
  this.orbitControls.maxPolarAngle = Math.PI / 2;
1650
+ this.orbitControls.target.set(0, 0, 0);
1403
1651
  }
1404
1652
  disableOrbitControls() {
1405
1653
  if (!this.orbitControls) {
@@ -1411,6 +1659,9 @@ var ZylemCamera = class {
1411
1659
  updateOrbitTargetFromSelection(selected) {
1412
1660
  if (!this.debugDelegate || selected.length === 0) {
1413
1661
  this.orbitTarget = null;
1662
+ if (this.orbitControls) {
1663
+ this.orbitControls.target.set(0, 0, 0);
1664
+ }
1414
1665
  return;
1415
1666
  }
1416
1667
  for (let i = selected.length - 1; i >= 0; i -= 1) {
@@ -1437,259 +1688,453 @@ var ZylemCamera = class {
1437
1688
  this.debugUnsubscribe = null;
1438
1689
  this.debugDelegate = null;
1439
1690
  }
1440
- };
1441
-
1442
- // src/lib/camera/camera.ts
1443
- var CameraWrapper = class {
1444
- cameraRef;
1445
- constructor(camera) {
1446
- this.cameraRef = camera;
1691
+ /**
1692
+ * Save camera position, rotation, and zoom before entering debug mode.
1693
+ */
1694
+ saveCameraState() {
1695
+ this.savedCameraPosition = this.camera.position.clone();
1696
+ this.savedCameraQuaternion = this.camera.quaternion.clone();
1697
+ if ("zoom" in this.camera) {
1698
+ this.savedCameraZoom = this.camera.zoom;
1699
+ }
1700
+ }
1701
+ /**
1702
+ * Restore camera position, rotation, and zoom when exiting debug mode.
1703
+ */
1704
+ restoreCameraState() {
1705
+ if (this.savedCameraPosition) {
1706
+ this.camera.position.copy(this.savedCameraPosition);
1707
+ this.savedCameraPosition = null;
1708
+ }
1709
+ if (this.savedCameraQuaternion) {
1710
+ this.camera.quaternion.copy(this.savedCameraQuaternion);
1711
+ this.savedCameraQuaternion = null;
1712
+ }
1713
+ if (this.savedCameraZoom !== null && "zoom" in this.camera) {
1714
+ this.camera.zoom = this.savedCameraZoom;
1715
+ this.camera.updateProjectionMatrix?.();
1716
+ this.savedCameraZoom = null;
1717
+ }
1447
1718
  }
1448
1719
  };
1449
1720
 
1450
- // src/lib/stage/stage-debug-delegate.ts
1451
- import { Ray } from "@dimforge/rapier3d-compat";
1452
- import { BufferAttribute, BufferGeometry as BufferGeometry2, LineBasicMaterial as LineBasicMaterial2, LineSegments as LineSegments2, Raycaster, Vector2 as Vector24 } from "three";
1453
-
1454
- // src/lib/stage/debug-entity-cursor.ts
1455
- import {
1456
- Box3,
1457
- BoxGeometry,
1458
- Color as Color5,
1459
- EdgesGeometry,
1460
- Group as Group3,
1461
- LineBasicMaterial,
1462
- LineSegments,
1463
- Mesh as Mesh2,
1464
- MeshBasicMaterial,
1465
- Vector3 as Vector38
1466
- } from "three";
1467
- var DebugEntityCursor = class {
1468
- scene;
1469
- container;
1470
- fillMesh;
1471
- edgeLines;
1472
- currentColor = new Color5(65280);
1473
- bbox = new Box3();
1474
- size = new Vector38();
1475
- center = new Vector38();
1476
- constructor(scene) {
1477
- this.scene = scene;
1478
- const initialGeometry = new BoxGeometry(1, 1, 1);
1479
- this.fillMesh = new Mesh2(
1480
- initialGeometry,
1481
- new MeshBasicMaterial({
1482
- color: this.currentColor,
1483
- transparent: true,
1484
- opacity: 0.12,
1485
- depthWrite: false
1486
- })
1487
- );
1488
- const edges = new EdgesGeometry(initialGeometry);
1489
- this.edgeLines = new LineSegments(
1490
- edges,
1491
- new LineBasicMaterial({ color: this.currentColor, linewidth: 1 })
1492
- );
1493
- this.container = new Group3();
1494
- this.container.name = "DebugEntityCursor";
1495
- this.container.add(this.fillMesh);
1496
- this.container.add(this.edgeLines);
1497
- this.container.visible = false;
1498
- this.scene.add(this.container);
1499
- }
1500
- setColor(color) {
1501
- this.currentColor.set(color);
1502
- this.fillMesh.material.color.set(this.currentColor);
1503
- this.edgeLines.material.color.set(this.currentColor);
1721
+ // src/lib/camera/zylem-camera.ts
1722
+ var ZylemCamera = class {
1723
+ cameraRig = null;
1724
+ camera;
1725
+ screenResolution;
1726
+ renderer;
1727
+ composer;
1728
+ _perspective;
1729
+ target = null;
1730
+ sceneRef = null;
1731
+ frustumSize = 10;
1732
+ // Perspective controller delegation
1733
+ perspectiveController = null;
1734
+ // Debug/orbit controls delegation
1735
+ orbitController = null;
1736
+ constructor(perspective, screenResolution, frustumSize = 10) {
1737
+ this._perspective = perspective;
1738
+ this.screenResolution = screenResolution;
1739
+ this.frustumSize = frustumSize;
1740
+ this.renderer = new WebGLRenderer3({ antialias: false, alpha: true });
1741
+ this.renderer.setSize(screenResolution.x, screenResolution.y);
1742
+ this.renderer.shadowMap.enabled = true;
1743
+ this.composer = new EffectComposer(this.renderer);
1744
+ const aspectRatio = screenResolution.x / screenResolution.y;
1745
+ this.camera = this.createCameraForPerspective(aspectRatio);
1746
+ if (this.needsRig()) {
1747
+ this.cameraRig = new Object3D6();
1748
+ this.cameraRig.position.set(0, 3, 10);
1749
+ this.cameraRig.add(this.camera);
1750
+ this.camera.lookAt(new Vector39(0, 2, 0));
1751
+ } else {
1752
+ this.camera.position.set(0, 0, 10);
1753
+ this.camera.lookAt(new Vector39(0, 0, 0));
1754
+ }
1755
+ this.initializePerspectiveController();
1756
+ this.orbitController = new CameraOrbitController(this.camera, this.renderer.domElement);
1504
1757
  }
1505
1758
  /**
1506
- * Update the cursor to enclose the provided Object3D using a world-space AABB.
1759
+ * Setup the camera with a scene
1507
1760
  */
1508
- updateFromObject(object) {
1509
- if (!object) {
1510
- this.hide();
1511
- return;
1512
- }
1513
- this.bbox.setFromObject(object);
1514
- if (!isFinite(this.bbox.min.x) || !isFinite(this.bbox.max.x)) {
1515
- this.hide();
1516
- return;
1761
+ async setup(scene) {
1762
+ this.sceneRef = scene;
1763
+ let renderResolution = this.screenResolution.clone().divideScalar(2);
1764
+ renderResolution.x |= 0;
1765
+ renderResolution.y |= 0;
1766
+ const pass = new RenderPass(renderResolution, scene, this.camera);
1767
+ this.composer.addPass(pass);
1768
+ if (this.perspectiveController) {
1769
+ this.perspectiveController.setup({
1770
+ screenResolution: this.screenResolution,
1771
+ renderer: this.renderer,
1772
+ scene,
1773
+ camera: this
1774
+ });
1517
1775
  }
1518
- this.bbox.getSize(this.size);
1519
- this.bbox.getCenter(this.center);
1520
- const newGeom = new BoxGeometry(
1521
- Math.max(this.size.x, 1e-6),
1522
- Math.max(this.size.y, 1e-6),
1523
- Math.max(this.size.z, 1e-6)
1524
- );
1525
- this.fillMesh.geometry.dispose();
1526
- this.fillMesh.geometry = newGeom;
1527
- const newEdges = new EdgesGeometry(newGeom);
1528
- this.edgeLines.geometry.dispose();
1529
- this.edgeLines.geometry = newEdges;
1530
- this.container.position.copy(this.center);
1531
- this.container.visible = true;
1776
+ this.renderer.setAnimationLoop((delta) => {
1777
+ this.update(delta || 0);
1778
+ });
1532
1779
  }
1533
- hide() {
1534
- this.container.visible = false;
1780
+ /**
1781
+ * Update camera and render
1782
+ */
1783
+ update(delta) {
1784
+ this.orbitController?.update();
1785
+ if (this.perspectiveController && !this.isDebugModeActive()) {
1786
+ this.perspectiveController.update(delta);
1787
+ }
1788
+ this.composer.render(delta);
1535
1789
  }
1536
- dispose() {
1537
- this.scene.remove(this.container);
1538
- this.fillMesh.geometry.dispose();
1539
- this.fillMesh.material.dispose();
1540
- this.edgeLines.geometry.dispose();
1541
- this.edgeLines.material.dispose();
1790
+ /**
1791
+ * Check if debug mode is active (orbit controls taking over camera)
1792
+ */
1793
+ isDebugModeActive() {
1794
+ return this.orbitController?.isActive ?? false;
1542
1795
  }
1543
- };
1544
-
1545
- // src/lib/stage/stage-debug-delegate.ts
1546
- var SELECT_TOOL_COLOR = 2293538;
1547
- var DELETE_TOOL_COLOR = 16724787;
1548
- var StageDebugDelegate = class {
1549
- stage;
1550
- options;
1551
- mouseNdc = new Vector24(-2, -2);
1552
- raycaster = new Raycaster();
1553
- isMouseDown = false;
1554
- disposeFns = [];
1555
- debugCursor = null;
1556
- debugLines = null;
1557
- constructor(stage, options) {
1558
- this.stage = stage;
1559
- this.options = {
1560
- maxRayDistance: options?.maxRayDistance ?? 5e3,
1561
- addEntityFactory: options?.addEntityFactory ?? null
1562
- };
1563
- if (this.stage.scene) {
1564
- this.debugLines = new LineSegments2(
1565
- new BufferGeometry2(),
1566
- new LineBasicMaterial2({ vertexColors: true })
1567
- );
1568
- this.stage.scene.scene.add(this.debugLines);
1569
- this.debugLines.visible = true;
1570
- this.debugCursor = new DebugEntityCursor(this.stage.scene.scene);
1796
+ /**
1797
+ * Dispose renderer, composer, controls, and detach from scene
1798
+ */
1799
+ destroy() {
1800
+ try {
1801
+ this.renderer.setAnimationLoop(null);
1802
+ } catch {
1571
1803
  }
1572
- this.attachDomListeners();
1573
- }
1574
- update() {
1575
- if (!debugState.enabled) return;
1576
- if (!this.stage.scene || !this.stage.world || !this.stage.cameraRef) return;
1577
- const { world, cameraRef } = this.stage;
1578
- if (this.debugLines) {
1579
- const { vertices, colors } = world.world.debugRender();
1580
- this.debugLines.geometry.setAttribute("position", new BufferAttribute(vertices, 3));
1581
- this.debugLines.geometry.setAttribute("color", new BufferAttribute(colors, 4));
1804
+ try {
1805
+ this.orbitController?.dispose();
1806
+ } catch {
1582
1807
  }
1583
- const tool = getDebugTool();
1584
- const isCursorTool = tool === "select" || tool === "delete";
1585
- this.raycaster.setFromCamera(this.mouseNdc, cameraRef.camera);
1586
- const origin = this.raycaster.ray.origin.clone();
1587
- const direction = this.raycaster.ray.direction.clone().normalize();
1588
- const rapierRay = new Ray(
1589
- { x: origin.x, y: origin.y, z: origin.z },
1590
- { x: direction.x, y: direction.y, z: direction.z }
1591
- );
1592
- const hit = world.world.castRay(rapierRay, this.options.maxRayDistance, true);
1593
- if (hit && isCursorTool) {
1594
- const rigidBody = hit.collider?._parent;
1595
- const hoveredUuid2 = rigidBody?.userData?.uuid;
1596
- if (hoveredUuid2) {
1597
- const entity = this.stage._debugMap.get(hoveredUuid2);
1598
- if (entity) setHoveredEntity(entity);
1599
- } else {
1600
- resetHoveredEntity();
1601
- }
1602
- if (this.isMouseDown) {
1603
- this.handleActionOnHit(hoveredUuid2 ?? null, origin, direction, hit.toi);
1604
- }
1808
+ try {
1809
+ this.composer?.passes?.forEach((p) => p.dispose?.());
1810
+ this.composer?.dispose?.();
1811
+ } catch {
1605
1812
  }
1606
- this.isMouseDown = false;
1607
- const hoveredUuid = getHoveredEntity();
1608
- if (!hoveredUuid) {
1609
- this.debugCursor?.hide();
1610
- return;
1813
+ try {
1814
+ this.renderer.dispose();
1815
+ } catch {
1611
1816
  }
1612
- const hoveredEntity = this.stage._debugMap.get(`${hoveredUuid}`);
1613
- const targetObject = hoveredEntity?.group ?? hoveredEntity?.mesh ?? null;
1614
- if (!targetObject) {
1615
- this.debugCursor?.hide();
1616
- return;
1817
+ this.sceneRef = null;
1818
+ }
1819
+ /**
1820
+ * Attach a delegate to react to debug state changes.
1821
+ */
1822
+ setDebugDelegate(delegate) {
1823
+ this.orbitController?.setDebugDelegate(delegate);
1824
+ }
1825
+ /**
1826
+ * Resize camera and renderer
1827
+ */
1828
+ resize(width, height) {
1829
+ this.screenResolution.set(width, height);
1830
+ this.renderer.setSize(width, height, false);
1831
+ this.composer.setSize(width, height);
1832
+ if (this.camera instanceof PerspectiveCamera) {
1833
+ this.camera.aspect = width / height;
1834
+ this.camera.updateProjectionMatrix();
1617
1835
  }
1618
- switch (tool) {
1619
- case "select":
1620
- this.debugCursor?.setColor(SELECT_TOOL_COLOR);
1621
- break;
1622
- case "delete":
1623
- this.debugCursor?.setColor(DELETE_TOOL_COLOR);
1836
+ if (this.perspectiveController) {
1837
+ this.perspectiveController.resize(width, height);
1838
+ }
1839
+ }
1840
+ /**
1841
+ * Update renderer pixel ratio (DPR)
1842
+ */
1843
+ setPixelRatio(dpr) {
1844
+ const safe = Math.max(1, Number.isFinite(dpr) ? dpr : 1);
1845
+ this.renderer.setPixelRatio(safe);
1846
+ }
1847
+ /**
1848
+ * Create camera based on perspective type
1849
+ */
1850
+ createCameraForPerspective(aspectRatio) {
1851
+ switch (this._perspective) {
1852
+ case Perspectives.ThirdPerson:
1853
+ return this.createThirdPersonCamera(aspectRatio);
1854
+ case Perspectives.FirstPerson:
1855
+ return this.createFirstPersonCamera(aspectRatio);
1856
+ case Perspectives.Isometric:
1857
+ return this.createIsometricCamera(aspectRatio);
1858
+ case Perspectives.Flat2D:
1859
+ return this.createFlat2DCamera(aspectRatio);
1860
+ case Perspectives.Fixed2D:
1861
+ return this.createFixed2DCamera(aspectRatio);
1862
+ default:
1863
+ return this.createThirdPersonCamera(aspectRatio);
1864
+ }
1865
+ }
1866
+ /**
1867
+ * Initialize perspective-specific controller
1868
+ */
1869
+ initializePerspectiveController() {
1870
+ switch (this._perspective) {
1871
+ case Perspectives.ThirdPerson:
1872
+ this.perspectiveController = new ThirdPersonCamera();
1624
1873
  break;
1625
- default:
1626
- this.debugCursor?.setColor(16777215);
1874
+ case Perspectives.Fixed2D:
1875
+ this.perspectiveController = new Fixed2DCamera();
1627
1876
  break;
1877
+ default:
1878
+ this.perspectiveController = new ThirdPersonCamera();
1628
1879
  }
1629
- this.debugCursor?.updateFromObject(targetObject);
1630
1880
  }
1631
- dispose() {
1632
- this.disposeFns.forEach((fn) => fn());
1633
- this.disposeFns = [];
1634
- this.debugCursor?.dispose();
1635
- if (this.debugLines && this.stage.scene) {
1636
- this.stage.scene.scene.remove(this.debugLines);
1637
- this.debugLines.geometry.dispose();
1638
- this.debugLines.material.dispose();
1639
- this.debugLines = null;
1881
+ createThirdPersonCamera(aspectRatio) {
1882
+ return new PerspectiveCamera(75, aspectRatio, 0.1, 1e3);
1883
+ }
1884
+ createFirstPersonCamera(aspectRatio) {
1885
+ return new PerspectiveCamera(75, aspectRatio, 0.1, 1e3);
1886
+ }
1887
+ createIsometricCamera(aspectRatio) {
1888
+ return new OrthographicCamera(
1889
+ this.frustumSize * aspectRatio / -2,
1890
+ this.frustumSize * aspectRatio / 2,
1891
+ this.frustumSize / 2,
1892
+ this.frustumSize / -2,
1893
+ 1,
1894
+ 1e3
1895
+ );
1896
+ }
1897
+ createFlat2DCamera(aspectRatio) {
1898
+ return new OrthographicCamera(
1899
+ this.frustumSize * aspectRatio / -2,
1900
+ this.frustumSize * aspectRatio / 2,
1901
+ this.frustumSize / 2,
1902
+ this.frustumSize / -2,
1903
+ 1,
1904
+ 1e3
1905
+ );
1906
+ }
1907
+ createFixed2DCamera(aspectRatio) {
1908
+ return this.createFlat2DCamera(aspectRatio);
1909
+ }
1910
+ // Movement methods
1911
+ moveCamera(position2) {
1912
+ if (this._perspective === Perspectives.Flat2D || this._perspective === Perspectives.Fixed2D) {
1913
+ this.frustumSize = position2.z;
1914
+ }
1915
+ if (this.cameraRig) {
1916
+ this.cameraRig.position.set(position2.x, position2.y, position2.z);
1917
+ } else {
1918
+ this.camera.position.set(position2.x, position2.y, position2.z);
1640
1919
  }
1641
1920
  }
1642
- handleActionOnHit(hoveredUuid, origin, direction, toi) {
1643
- const tool = getDebugTool();
1644
- switch (tool) {
1645
- case "select": {
1646
- if (hoveredUuid) {
1647
- const entity = this.stage._debugMap.get(hoveredUuid);
1648
- if (entity) setSelectedEntity(entity);
1649
- }
1650
- break;
1651
- }
1652
- case "delete": {
1653
- if (hoveredUuid) {
1654
- this.stage.removeEntityByUuid(hoveredUuid);
1655
- }
1656
- break;
1657
- }
1658
- case "scale": {
1659
- if (!this.options.addEntityFactory) break;
1660
- const hitPosition = origin.clone().add(direction.clone().multiplyScalar(toi));
1661
- const newNode = this.options.addEntityFactory({ position: hitPosition });
1662
- if (newNode) {
1663
- Promise.resolve(newNode).then((node) => {
1664
- if (node) this.stage.spawnEntity(node);
1665
- }).catch(() => {
1666
- });
1667
- }
1668
- break;
1669
- }
1670
- default:
1671
- break;
1921
+ move(position2) {
1922
+ this.moveCamera(position2);
1923
+ }
1924
+ rotate(pitch, yaw, roll) {
1925
+ if (this.cameraRig) {
1926
+ this.cameraRig.rotateX(pitch);
1927
+ this.cameraRig.rotateY(yaw);
1928
+ this.cameraRig.rotateZ(roll);
1929
+ } else {
1930
+ this.camera.rotateX(pitch);
1931
+ this.camera.rotateY(yaw);
1932
+ this.camera.rotateZ(roll);
1672
1933
  }
1673
1934
  }
1674
- attachDomListeners() {
1675
- const canvas = this.stage.cameraRef?.renderer.domElement ?? this.stage.scene?.zylemCamera.renderer.domElement;
1676
- if (!canvas) return;
1677
- const onMouseMove = (e) => {
1678
- const rect = canvas.getBoundingClientRect();
1679
- const x = (e.clientX - rect.left) / rect.width * 2 - 1;
1680
- const y = -((e.clientY - rect.top) / rect.height * 2 - 1);
1681
- this.mouseNdc.set(x, y);
1935
+ /**
1936
+ * Check if this perspective type needs a camera rig
1937
+ */
1938
+ needsRig() {
1939
+ return this._perspective === Perspectives.ThirdPerson;
1940
+ }
1941
+ /**
1942
+ * Get the DOM element for the renderer
1943
+ */
1944
+ getDomElement() {
1945
+ return this.renderer.domElement;
1946
+ }
1947
+ };
1948
+
1949
+ // src/lib/stage/stage-camera-delegate.ts
1950
+ var StageCameraDelegate = class {
1951
+ stage;
1952
+ constructor(stage) {
1953
+ this.stage = stage;
1954
+ }
1955
+ /**
1956
+ * Create a default third-person camera based on window size.
1957
+ */
1958
+ createDefaultCamera() {
1959
+ const width = window.innerWidth;
1960
+ const height = window.innerHeight;
1961
+ const screenResolution = new Vector24(width, height);
1962
+ return new ZylemCamera(Perspectives.ThirdPerson, screenResolution);
1963
+ }
1964
+ /**
1965
+ * Resolve the camera to use for the stage.
1966
+ * Uses the provided camera, stage camera wrapper, or creates a default.
1967
+ *
1968
+ * @param cameraOverride Optional camera override
1969
+ * @param cameraWrapper Optional camera wrapper from stage options
1970
+ * @returns The resolved ZylemCamera instance
1971
+ */
1972
+ resolveCamera(cameraOverride, cameraWrapper) {
1973
+ if (cameraOverride) {
1974
+ return cameraOverride;
1975
+ }
1976
+ if (cameraWrapper) {
1977
+ return cameraWrapper.cameraRef;
1978
+ }
1979
+ return this.createDefaultCamera();
1980
+ }
1981
+ };
1982
+
1983
+ // src/lib/stage/stage-loading-delegate.ts
1984
+ var StageLoadingDelegate = class {
1985
+ loadingHandlers = [];
1986
+ stageName;
1987
+ stageIndex;
1988
+ /**
1989
+ * Set stage context for event bus emissions.
1990
+ */
1991
+ setStageContext(stageName, stageIndex) {
1992
+ this.stageName = stageName;
1993
+ this.stageIndex = stageIndex;
1994
+ }
1995
+ /**
1996
+ * Subscribe to loading events.
1997
+ *
1998
+ * @param callback Invoked for each loading event (start, progress, complete)
1999
+ * @returns Unsubscribe function
2000
+ */
2001
+ onLoading(callback) {
2002
+ this.loadingHandlers.push(callback);
2003
+ return () => {
2004
+ this.loadingHandlers = this.loadingHandlers.filter((h) => h !== callback);
1682
2005
  };
1683
- const onMouseDown = (e) => {
1684
- this.isMouseDown = true;
2006
+ }
2007
+ /**
2008
+ * Emit a loading event to all subscribers and to the game event bus.
2009
+ *
2010
+ * @param event The loading event to broadcast
2011
+ */
2012
+ emit(event) {
2013
+ for (const handler of this.loadingHandlers) {
2014
+ try {
2015
+ handler(event);
2016
+ } catch (e) {
2017
+ console.error("Loading handler failed", e);
2018
+ }
2019
+ }
2020
+ const payload = {
2021
+ ...event,
2022
+ stageName: this.stageName,
2023
+ stageIndex: this.stageIndex
1685
2024
  };
1686
- canvas.addEventListener("mousemove", onMouseMove);
1687
- canvas.addEventListener("mousedown", onMouseDown);
1688
- this.disposeFns.push(() => canvas.removeEventListener("mousemove", onMouseMove));
1689
- this.disposeFns.push(() => canvas.removeEventListener("mousedown", onMouseDown));
2025
+ if (event.type === "start") {
2026
+ gameEventBus.emit("stage:loading:start", payload);
2027
+ } else if (event.type === "progress") {
2028
+ gameEventBus.emit("stage:loading:progress", payload);
2029
+ } else if (event.type === "complete") {
2030
+ gameEventBus.emit("stage:loading:complete", payload);
2031
+ }
2032
+ }
2033
+ /**
2034
+ * Emit a start loading event.
2035
+ */
2036
+ emitStart(message = "Loading stage...") {
2037
+ this.emit({ type: "start", message, progress: 0 });
2038
+ }
2039
+ /**
2040
+ * Emit a progress loading event.
2041
+ */
2042
+ emitProgress(message, current, total) {
2043
+ const progress = total > 0 ? current / total : 0;
2044
+ this.emit({ type: "progress", message, progress, current, total });
2045
+ }
2046
+ /**
2047
+ * Emit a complete loading event.
2048
+ */
2049
+ emitComplete(message = "Stage loaded") {
2050
+ this.emit({ type: "complete", message, progress: 1 });
2051
+ }
2052
+ /**
2053
+ * Clear all loading handlers.
2054
+ */
2055
+ dispose() {
2056
+ this.loadingHandlers = [];
1690
2057
  }
1691
2058
  };
1692
2059
 
2060
+ // src/lib/stage/stage-config.ts
2061
+ import { Vector3 as Vector310 } from "three";
2062
+
2063
+ // src/lib/core/utility/options-parser.ts
2064
+ function isBaseNode(item) {
2065
+ return !!item && typeof item === "object" && typeof item.create === "function";
2066
+ }
2067
+ function isThenable(item) {
2068
+ return !!item && typeof item.then === "function";
2069
+ }
2070
+ function isCameraWrapper(item) {
2071
+ return !!item && typeof item === "object" && item.constructor?.name === "CameraWrapper";
2072
+ }
2073
+ function isConfigObject(item) {
2074
+ if (!item || typeof item !== "object") return false;
2075
+ if (isBaseNode(item)) return false;
2076
+ if (isCameraWrapper(item)) return false;
2077
+ if (isThenable(item)) return false;
2078
+ if (typeof item.then === "function") return false;
2079
+ return item.constructor === Object || item.constructor?.name === "Object";
2080
+ }
2081
+ function isEntityInput(item) {
2082
+ if (!item) return false;
2083
+ if (isBaseNode(item)) return true;
2084
+ if (typeof item === "function") return true;
2085
+ if (isThenable(item)) return true;
2086
+ return false;
2087
+ }
2088
+
2089
+ // src/lib/stage/stage-config.ts
2090
+ var StageConfig = class {
2091
+ constructor(inputs, backgroundColor, backgroundImage, gravity, variables) {
2092
+ this.inputs = inputs;
2093
+ this.backgroundColor = backgroundColor;
2094
+ this.backgroundImage = backgroundImage;
2095
+ this.gravity = gravity;
2096
+ this.variables = variables;
2097
+ }
2098
+ };
2099
+ function createDefaultStageConfig() {
2100
+ return new StageConfig(
2101
+ {
2102
+ p1: ["gamepad-1", "keyboard-1"],
2103
+ p2: ["gamepad-2", "keyboard-2"]
2104
+ },
2105
+ ZylemBlueColor,
2106
+ null,
2107
+ new Vector310(0, 0, 0),
2108
+ {}
2109
+ );
2110
+ }
2111
+ function parseStageOptions(options = []) {
2112
+ const defaults = createDefaultStageConfig();
2113
+ let config = {};
2114
+ const entities = [];
2115
+ const asyncEntities = [];
2116
+ let camera;
2117
+ for (const item of options) {
2118
+ if (isCameraWrapper(item)) {
2119
+ camera = item;
2120
+ } else if (isBaseNode(item)) {
2121
+ entities.push(item);
2122
+ } else if (isEntityInput(item) && !isBaseNode(item)) {
2123
+ asyncEntities.push(item);
2124
+ } else if (isConfigObject(item)) {
2125
+ config = { ...config, ...item };
2126
+ }
2127
+ }
2128
+ const resolvedConfig = new StageConfig(
2129
+ config.inputs ?? defaults.inputs,
2130
+ config.backgroundColor ?? defaults.backgroundColor,
2131
+ config.backgroundImage ?? defaults.backgroundImage,
2132
+ config.gravity ?? defaults.gravity,
2133
+ config.variables ?? defaults.variables
2134
+ );
2135
+ return { config: resolvedConfig, entities, asyncEntities, camera };
2136
+ }
2137
+
1693
2138
  // src/lib/stage/zylem-stage.ts
1694
2139
  var STAGE_TYPE = "Stage";
1695
2140
  var ZylemStage = class extends LifeCycleBase {
@@ -1701,7 +2146,7 @@ var ZylemStage = class extends LifeCycleBase {
1701
2146
  p1: ["gamepad-1", "keyboard"],
1702
2147
  p2: ["gamepad-2", "keyboard"]
1703
2148
  },
1704
- gravity: new Vector310(0, 0, 0),
2149
+ gravity: new Vector311(0, 0, 0),
1705
2150
  variables: {},
1706
2151
  entities: []
1707
2152
  };
@@ -1716,16 +2161,19 @@ var ZylemStage = class extends LifeCycleBase {
1716
2161
  isLoaded = false;
1717
2162
  _debugMap = /* @__PURE__ */ new Map();
1718
2163
  entityAddedHandlers = [];
1719
- loadingHandlers = [];
1720
2164
  ecs = createECS();
1721
2165
  testSystem = null;
1722
2166
  transformSystem = null;
1723
2167
  debugDelegate = null;
1724
2168
  cameraDebugDelegate = null;
2169
+ debugStateUnsubscribe = null;
1725
2170
  uuid;
1726
2171
  wrapperRef = null;
1727
2172
  camera;
1728
2173
  cameraRef = null;
2174
+ // Delegates
2175
+ cameraDelegate;
2176
+ loadingDelegate;
1729
2177
  /**
1730
2178
  * Create a new stage.
1731
2179
  * @param options Stage options: partial config, camera, and initial entities or factories
@@ -1735,49 +2183,22 @@ var ZylemStage = class extends LifeCycleBase {
1735
2183
  this.world = null;
1736
2184
  this.scene = null;
1737
2185
  this.uuid = nanoid2();
1738
- const { config, entities, asyncEntities, camera } = this.parseOptions(options);
1739
- this.camera = camera;
1740
- this.children = entities;
1741
- this.pendingEntities = asyncEntities;
1742
- this.saveState({ ...this.state, ...config, entities: [] });
1743
- this.gravity = config.gravity ?? new Vector310(0, 0, 0);
1744
- }
1745
- parseOptions(options) {
1746
- let config = {};
1747
- const entities = [];
1748
- const asyncEntities = [];
1749
- let camera;
1750
- for (const item of options) {
1751
- if (this.isCameraWrapper(item)) {
1752
- camera = item;
1753
- } else if (this.isBaseNode(item)) {
1754
- entities.push(item);
1755
- } else if (this.isEntityInput(item)) {
1756
- asyncEntities.push(item);
1757
- } else if (this.isZylemStageConfig(item)) {
1758
- config = { ...config, ...item };
1759
- }
1760
- }
1761
- return { config, entities, asyncEntities, camera };
1762
- }
1763
- isZylemStageConfig(item) {
1764
- return item && typeof item === "object" && !(item instanceof BaseNode) && !(item instanceof CameraWrapper);
1765
- }
1766
- isBaseNode(item) {
1767
- return item && typeof item === "object" && typeof item.create === "function";
1768
- }
1769
- isCameraWrapper(item) {
1770
- return item && typeof item === "object" && item.constructor.name === "CameraWrapper";
1771
- }
1772
- isEntityInput(item) {
1773
- if (!item) return false;
1774
- if (this.isBaseNode(item)) return true;
1775
- if (typeof item === "function") return true;
1776
- if (typeof item === "object" && typeof item.then === "function") return true;
1777
- return false;
1778
- }
1779
- isThenable(value) {
1780
- return !!value && typeof value.then === "function";
2186
+ this.cameraDelegate = new StageCameraDelegate(this);
2187
+ this.loadingDelegate = new StageLoadingDelegate();
2188
+ const parsed = parseStageOptions(options);
2189
+ this.camera = parsed.camera;
2190
+ this.children = parsed.entities;
2191
+ this.pendingEntities = parsed.asyncEntities;
2192
+ this.saveState({
2193
+ ...this.state,
2194
+ inputs: parsed.config.inputs,
2195
+ backgroundColor: parsed.config.backgroundColor,
2196
+ backgroundImage: parsed.config.backgroundImage,
2197
+ gravity: parsed.config.gravity,
2198
+ variables: parsed.config.variables,
2199
+ entities: []
2200
+ });
2201
+ this.gravity = parsed.config.gravity ?? new Vector311(0, 0, 0);
1781
2202
  }
1782
2203
  handleEntityImmediatelyOrQueue(entity) {
1783
2204
  if (this.isLoaded) {
@@ -1798,42 +2219,47 @@ var ZylemStage = class extends LifeCycleBase {
1798
2219
  }
1799
2220
  setState() {
1800
2221
  const { backgroundColor, backgroundImage } = this.state;
1801
- const color = backgroundColor instanceof Color6 ? backgroundColor : new Color6(backgroundColor);
2222
+ const color = backgroundColor instanceof Color7 ? backgroundColor : new Color7(backgroundColor);
1802
2223
  setStageBackgroundColor(color);
1803
2224
  setStageBackgroundImage(backgroundImage);
1804
2225
  setStageVariables(this.state.variables ?? {});
1805
2226
  }
1806
2227
  /**
1807
2228
  * Load and initialize the stage's scene and world.
2229
+ * Uses generator pattern to yield control to event loop for real-time progress.
1808
2230
  * @param id DOM element id for the renderer container
1809
2231
  * @param camera Optional camera override
1810
2232
  */
1811
2233
  async load(id, camera) {
1812
2234
  this.setState();
1813
- const zylemCamera = camera || (this.camera ? this.camera.cameraRef : this.createDefaultCamera());
2235
+ const zylemCamera = this.cameraDelegate.resolveCamera(camera, this.camera);
1814
2236
  this.cameraRef = zylemCamera;
1815
2237
  this.scene = new ZylemScene(id, zylemCamera, this.state);
1816
- const physicsWorld = await ZylemWorld.loadPhysics(this.gravity ?? new Vector310(0, 0, 0));
2238
+ const physicsWorld = await ZylemWorld.loadPhysics(this.gravity ?? new Vector311(0, 0, 0));
1817
2239
  this.world = new ZylemWorld(physicsWorld);
1818
2240
  this.scene.setup();
1819
- this.emitLoading({ type: "start", message: "Loading stage...", progress: 0 });
2241
+ this.loadingDelegate.emitStart();
2242
+ await this.runEntityLoadGenerator();
2243
+ this.transformSystem = createTransformSystem(this);
2244
+ this.isLoaded = true;
2245
+ this.loadingDelegate.emitComplete();
2246
+ }
2247
+ /**
2248
+ * Generator that yields between entity loads for real-time progress updates.
2249
+ */
2250
+ *entityLoadGenerator() {
1820
2251
  const total = this.children.length + this.pendingEntities.length + this.pendingPromises.length;
1821
2252
  let current = 0;
1822
- for (let child of this.children) {
2253
+ for (const child of this.children) {
1823
2254
  this.spawnEntity(child);
1824
2255
  current++;
1825
- this.emitLoading({
1826
- type: "progress",
1827
- message: `Loaded entity ${child.name || "unknown"}`,
1828
- progress: current / total,
1829
- current,
1830
- total
1831
- });
2256
+ yield { current, total, name: child.name || "unknown" };
1832
2257
  }
1833
2258
  if (this.pendingEntities.length) {
1834
2259
  this.enqueue(...this.pendingEntities);
1835
2260
  current += this.pendingEntities.length;
1836
2261
  this.pendingEntities = [];
2262
+ yield { current, total, name: "pending entities" };
1837
2263
  }
1838
2264
  if (this.pendingPromises.length) {
1839
2265
  for (const promise of this.pendingPromises) {
@@ -1843,24 +2269,44 @@ var ZylemStage = class extends LifeCycleBase {
1843
2269
  }
1844
2270
  current += this.pendingPromises.length;
1845
2271
  this.pendingPromises = [];
2272
+ yield { current, total, name: "async entities" };
1846
2273
  }
1847
- this.transformSystem = createTransformSystem(this);
1848
- this.isLoaded = true;
1849
- this.emitLoading({ type: "complete", message: "Stage loaded", progress: 1 });
1850
2274
  }
1851
- createDefaultCamera() {
1852
- const width = window.innerWidth;
1853
- const height = window.innerHeight;
1854
- const screenResolution = new Vector25(width, height);
1855
- return new ZylemCamera(Perspectives.ThirdPerson, screenResolution);
2275
+ /**
2276
+ * Runs the entity load generator, yielding to the event loop between loads.
2277
+ * This allows the browser to process events and update the UI in real-time.
2278
+ */
2279
+ async runEntityLoadGenerator() {
2280
+ const gen = this.entityLoadGenerator();
2281
+ for (const progress of gen) {
2282
+ this.loadingDelegate.emitProgress(`Loaded ${progress.name}`, progress.current, progress.total);
2283
+ await new Promise((resolve) => setTimeout(resolve, 0));
2284
+ }
1856
2285
  }
1857
2286
  _setup(params) {
1858
2287
  if (!this.scene || !this.world) {
1859
2288
  this.logMissingEntities();
1860
2289
  return;
1861
2290
  }
1862
- if (debugState.enabled) {
2291
+ this.updateDebugDelegate();
2292
+ this.debugStateUnsubscribe = subscribe4(debugState, () => {
2293
+ this.updateDebugDelegate();
2294
+ });
2295
+ }
2296
+ updateDebugDelegate() {
2297
+ if (debugState.enabled && !this.debugDelegate && this.scene && this.world) {
1863
2298
  this.debugDelegate = new StageDebugDelegate(this);
2299
+ if (this.cameraRef && !this.cameraDebugDelegate) {
2300
+ this.cameraDebugDelegate = new StageCameraDebugDelegate(this);
2301
+ this.cameraRef.setDebugDelegate(this.cameraDebugDelegate);
2302
+ }
2303
+ } else if (!debugState.enabled && this.debugDelegate) {
2304
+ this.debugDelegate.dispose();
2305
+ this.debugDelegate = null;
2306
+ if (this.cameraRef) {
2307
+ this.cameraRef.setDebugDelegate(null);
2308
+ }
2309
+ this.cameraDebugDelegate = null;
1864
2310
  }
1865
2311
  }
1866
2312
  _update(params) {
@@ -1870,7 +2316,7 @@ var ZylemStage = class extends LifeCycleBase {
1870
2316
  return;
1871
2317
  }
1872
2318
  this.world.update(params);
1873
- this.transformSystem(this.ecs);
2319
+ this.transformSystem?.system(this.ecs);
1874
2320
  this._childrenMap.forEach((child, eid) => {
1875
2321
  child.nodeUpdate({
1876
2322
  ...params,
@@ -1904,13 +2350,20 @@ var ZylemStage = class extends LifeCycleBase {
1904
2350
  this._debugMap.clear();
1905
2351
  this.world?.destroy();
1906
2352
  this.scene?.destroy();
2353
+ if (this.debugStateUnsubscribe) {
2354
+ this.debugStateUnsubscribe();
2355
+ this.debugStateUnsubscribe = null;
2356
+ }
1907
2357
  this.debugDelegate?.dispose();
2358
+ this.debugDelegate = null;
1908
2359
  this.cameraRef?.setDebugDelegate(null);
1909
2360
  this.cameraDebugDelegate = null;
1910
2361
  this.isLoaded = false;
1911
2362
  this.world = null;
1912
2363
  this.scene = null;
1913
2364
  this.cameraRef = null;
2365
+ this.transformSystem?.destroy(this.ecs);
2366
+ this.transformSystem = null;
1914
2367
  resetStageVariables();
1915
2368
  clearVariables(this);
1916
2369
  }
@@ -1991,13 +2444,7 @@ var ZylemStage = class extends LifeCycleBase {
1991
2444
  };
1992
2445
  }
1993
2446
  onLoading(callback) {
1994
- this.loadingHandlers.push(callback);
1995
- return () => {
1996
- this.loadingHandlers = this.loadingHandlers.filter((h) => h !== callback);
1997
- };
1998
- }
1999
- emitLoading(event) {
2000
- this.loadingHandlers.forEach((h) => h(event));
2447
+ return this.loadingDelegate.onLoading(callback);
2001
2448
  }
2002
2449
  /**
2003
2450
  * Remove an entity and its resources by its UUID.
@@ -2054,16 +2501,16 @@ var ZylemStage = class extends LifeCycleBase {
2054
2501
  enqueue(...items) {
2055
2502
  for (const item of items) {
2056
2503
  if (!item) continue;
2057
- if (this.isBaseNode(item)) {
2504
+ if (isBaseNode(item)) {
2058
2505
  this.handleEntityImmediatelyOrQueue(item);
2059
2506
  continue;
2060
2507
  }
2061
2508
  if (typeof item === "function") {
2062
2509
  try {
2063
2510
  const result = item();
2064
- if (this.isBaseNode(result)) {
2511
+ if (isBaseNode(result)) {
2065
2512
  this.handleEntityImmediatelyOrQueue(result);
2066
- } else if (this.isThenable(result)) {
2513
+ } else if (isThenable(result)) {
2067
2514
  this.handlePromiseWithSpawnOnResolve(result);
2068
2515
  }
2069
2516
  } catch (error) {
@@ -2071,16 +2518,25 @@ var ZylemStage = class extends LifeCycleBase {
2071
2518
  }
2072
2519
  continue;
2073
2520
  }
2074
- if (this.isThenable(item)) {
2521
+ if (isThenable(item)) {
2075
2522
  this.handlePromiseWithSpawnOnResolve(item);
2076
2523
  }
2077
2524
  }
2078
2525
  }
2079
2526
  };
2080
2527
 
2528
+ // src/lib/camera/camera.ts
2529
+ import { Vector2 as Vector26, Vector3 as Vector312 } from "three";
2530
+ var CameraWrapper = class {
2531
+ cameraRef;
2532
+ constructor(camera) {
2533
+ this.cameraRef = camera;
2534
+ }
2535
+ };
2536
+
2081
2537
  // src/lib/stage/stage-default.ts
2082
2538
  import { proxy as proxy4 } from "valtio/vanilla";
2083
- import { Vector3 as Vector311 } from "three";
2539
+ import { Vector3 as Vector313 } from "three";
2084
2540
  var initialDefaults = {
2085
2541
  backgroundColor: ZylemBlueColor,
2086
2542
  backgroundImage: null,
@@ -2088,7 +2544,7 @@ var initialDefaults = {
2088
2544
  p1: ["gamepad-1", "keyboard"],
2089
2545
  p2: ["gamepad-2", "keyboard"]
2090
2546
  },
2091
- gravity: new Vector311(0, 0, 0),
2547
+ gravity: new Vector313(0, 0, 0),
2092
2548
  variables: {}
2093
2549
  };
2094
2550
  var stageDefaultsState = proxy4({
@@ -2117,30 +2573,58 @@ function getStageDefaultConfig() {
2117
2573
  var Stage = class {
2118
2574
  wrappedStage;
2119
2575
  options = [];
2120
- // TODO: these shouldn't be here maybe more like nextFrame(stageInstance, () => {})
2121
- update = () => {
2122
- };
2123
- setup = () => {
2124
- };
2125
- destroy = () => {
2126
- };
2576
+ // Entities added after construction, consumed on each load
2577
+ _pendingEntities = [];
2578
+ // Lifecycle callback arrays
2579
+ setupCallbacks = [];
2580
+ updateCallbacks = [];
2581
+ destroyCallbacks = [];
2582
+ pendingLoadingCallbacks = [];
2127
2583
  constructor(options) {
2128
2584
  this.options = options;
2129
2585
  this.wrappedStage = null;
2130
2586
  }
2131
2587
  async load(id, camera) {
2132
2588
  stageState.entities = [];
2133
- this.wrappedStage = new ZylemStage(this.options);
2589
+ const loadOptions = [...this.options, ...this._pendingEntities];
2590
+ this._pendingEntities = [];
2591
+ this.wrappedStage = new ZylemStage(loadOptions);
2134
2592
  this.wrappedStage.wrapperRef = this;
2593
+ this.pendingLoadingCallbacks.forEach((cb) => {
2594
+ this.wrappedStage.onLoading(cb);
2595
+ });
2596
+ this.pendingLoadingCallbacks = [];
2135
2597
  const zylemCamera = camera instanceof CameraWrapper ? camera.cameraRef : camera;
2136
2598
  await this.wrappedStage.load(id, zylemCamera);
2137
2599
  this.wrappedStage.onEntityAdded((child) => {
2138
2600
  const next = this.wrappedStage.buildEntityState(child);
2139
2601
  stageState.entities = [...stageState.entities, next];
2140
2602
  }, { replayExisting: true });
2603
+ this.applyLifecycleCallbacks();
2604
+ }
2605
+ applyLifecycleCallbacks() {
2606
+ if (!this.wrappedStage) return;
2607
+ if (this.setupCallbacks.length > 0) {
2608
+ this.wrappedStage.setup = (params) => {
2609
+ const extended = { ...params, stage: this };
2610
+ this.setupCallbacks.forEach((cb) => cb(extended));
2611
+ };
2612
+ }
2613
+ if (this.updateCallbacks.length > 0) {
2614
+ this.wrappedStage.update = (params) => {
2615
+ const extended = { ...params, stage: this };
2616
+ this.updateCallbacks.forEach((cb) => cb(extended));
2617
+ };
2618
+ }
2619
+ if (this.destroyCallbacks.length > 0) {
2620
+ this.wrappedStage.destroy = (params) => {
2621
+ const extended = { ...params, stage: this };
2622
+ this.destroyCallbacks.forEach((cb) => cb(extended));
2623
+ };
2624
+ }
2141
2625
  }
2142
2626
  async addEntities(entities) {
2143
- this.options.push(...entities);
2627
+ this._pendingEntities.push(...entities);
2144
2628
  if (!this.wrappedStage) {
2145
2629
  return;
2146
2630
  }
@@ -2165,28 +2649,57 @@ var Stage = class {
2165
2649
  start(params) {
2166
2650
  this.wrappedStage?.nodeSetup(params);
2167
2651
  }
2652
+ // Fluent API for adding lifecycle callbacks
2168
2653
  onUpdate(...callbacks) {
2169
- if (!this.wrappedStage) {
2170
- return;
2654
+ this.updateCallbacks.push(...callbacks);
2655
+ if (this.wrappedStage) {
2656
+ this.wrappedStage.update = (params) => {
2657
+ const extended = { ...params, stage: this };
2658
+ this.updateCallbacks.forEach((cb) => cb(extended));
2659
+ };
2171
2660
  }
2172
- this.wrappedStage.update = (params) => {
2173
- const extended = { ...params, stage: this };
2174
- callbacks.forEach((cb) => cb(extended));
2175
- };
2661
+ return this;
2176
2662
  }
2177
- onSetup(callback) {
2178
- this.wrappedStage.setup = callback;
2663
+ onSetup(...callbacks) {
2664
+ this.setupCallbacks.push(...callbacks);
2665
+ if (this.wrappedStage) {
2666
+ this.wrappedStage.setup = (params) => {
2667
+ const extended = { ...params, stage: this };
2668
+ this.setupCallbacks.forEach((cb) => cb(extended));
2669
+ };
2670
+ }
2671
+ return this;
2179
2672
  }
2180
- onDestroy(callback) {
2181
- this.wrappedStage.destroy = callback;
2673
+ onDestroy(...callbacks) {
2674
+ this.destroyCallbacks.push(...callbacks);
2675
+ if (this.wrappedStage) {
2676
+ this.wrappedStage.destroy = (params) => {
2677
+ const extended = { ...params, stage: this };
2678
+ this.destroyCallbacks.forEach((cb) => cb(extended));
2679
+ };
2680
+ }
2681
+ return this;
2182
2682
  }
2183
2683
  onLoading(callback) {
2184
2684
  if (!this.wrappedStage) {
2685
+ this.pendingLoadingCallbacks.push(callback);
2185
2686
  return () => {
2687
+ this.pendingLoadingCallbacks = this.pendingLoadingCallbacks.filter((c) => c !== callback);
2186
2688
  };
2187
2689
  }
2188
2690
  return this.wrappedStage.onLoading(callback);
2189
2691
  }
2692
+ /**
2693
+ * Find an entity by name on the current stage.
2694
+ * @param name The name of the entity to find
2695
+ * @param type Optional type symbol for type inference (e.g., TEXT_TYPE, SPRITE_TYPE)
2696
+ * @returns The entity if found, or undefined
2697
+ * @example stage.getEntityByName('scoreText', TEXT_TYPE)
2698
+ */
2699
+ getEntityByName(name, type) {
2700
+ const entity = this.wrappedStage?.children.find((c) => c.name === name);
2701
+ return entity;
2702
+ }
2190
2703
  };
2191
2704
  function createStage(...options) {
2192
2705
  const _options = getStageOptions(options);
@@ -2194,7 +2707,7 @@ function createStage(...options) {
2194
2707
  }
2195
2708
 
2196
2709
  // src/lib/stage/entity-spawner.ts
2197
- import { Euler, Quaternion as Quaternion2, Vector2 as Vector26 } from "three";
2710
+ import { Euler, Quaternion as Quaternion3, Vector2 as Vector27 } from "three";
2198
2711
  function entitySpawner(factory) {
2199
2712
  return {
2200
2713
  spawn: async (stage, x, y) => {
@@ -2202,7 +2715,7 @@ function entitySpawner(factory) {
2202
2715
  stage.add(instance);
2203
2716
  return instance;
2204
2717
  },
2205
- spawnRelative: async (source, stage, offset = new Vector26(0, 1)) => {
2718
+ spawnRelative: async (source, stage, offset = new Vector27(0, 1)) => {
2206
2719
  if (!source.body) {
2207
2720
  console.warn("body missing for entity during spawnRelative");
2208
2721
  return void 0;
@@ -2211,7 +2724,7 @@ function entitySpawner(factory) {
2211
2724
  let rz = source._rotation2DAngle ?? 0;
2212
2725
  try {
2213
2726
  const r = source.body.rotation();
2214
- const q = new Quaternion2(r.x, r.y, r.z, r.w);
2727
+ const q = new Quaternion3(r.x, r.y, r.z, r.w);
2215
2728
  const e = new Euler().setFromQuaternion(q, "XYZ");
2216
2729
  rz = e.z;
2217
2730
  } catch {
@@ -2224,8 +2737,31 @@ function entitySpawner(factory) {
2224
2737
  }
2225
2738
  };
2226
2739
  }
2740
+
2741
+ // src/lib/stage/stage-events.ts
2742
+ import { subscribe as subscribe5 } from "valtio/vanilla";
2743
+ var STAGE_STATE_CHANGE = "STAGE_STATE_CHANGE";
2744
+ function initStageStateDispatcher() {
2745
+ return subscribe5(stageState, () => {
2746
+ const detail = {
2747
+ entities: stageState.entities,
2748
+ variables: stageState.variables
2749
+ };
2750
+ window.dispatchEvent(new CustomEvent(STAGE_STATE_CHANGE, { detail }));
2751
+ });
2752
+ }
2753
+ function dispatchStageState() {
2754
+ const detail = {
2755
+ entities: stageState.entities,
2756
+ variables: stageState.variables
2757
+ };
2758
+ window.dispatchEvent(new CustomEvent(STAGE_STATE_CHANGE, { detail }));
2759
+ }
2227
2760
  export {
2761
+ STAGE_STATE_CHANGE,
2228
2762
  createStage,
2229
- entitySpawner
2763
+ dispatchStageState,
2764
+ entitySpawner,
2765
+ initStageStateDispatcher
2230
2766
  };
2231
2767
  //# sourceMappingURL=stage.js.map