@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/entities.js CHANGED
@@ -11,7 +11,8 @@ import {
11
11
  defineSystem,
12
12
  defineQuery,
13
13
  defineComponent,
14
- Types
14
+ Types,
15
+ removeQuery
15
16
  } from "bitecs";
16
17
  import { Quaternion } from "three";
17
18
  var position = defineComponent({
@@ -30,6 +31,7 @@ var scale = defineComponent({
30
31
  y: Types.f32,
31
32
  z: Types.f32
32
33
  });
34
+ var _tempQuaternion = new Quaternion();
33
35
 
34
36
  // src/lib/core/flags.ts
35
37
  var DEBUG_FLAG = import.meta.env.VITE_DEBUG_FLAG === "true";
@@ -44,21 +46,76 @@ var BaseNode = class _BaseNode {
44
46
  uuid = "";
45
47
  name = "";
46
48
  markedForRemoval = false;
47
- setup = () => {
48
- };
49
- loaded = () => {
50
- };
51
- update = () => {
52
- };
53
- destroy = () => {
54
- };
55
- cleanup = () => {
49
+ /**
50
+ * Lifecycle callback arrays - use onSetup(), onUpdate(), etc. to add callbacks
51
+ */
52
+ lifecycleCallbacks = {
53
+ setup: [],
54
+ loaded: [],
55
+ update: [],
56
+ destroy: [],
57
+ cleanup: []
56
58
  };
57
59
  constructor(args = []) {
58
60
  const options = args.filter((arg) => !(arg instanceof _BaseNode)).reduce((acc, opt) => ({ ...acc, ...opt }), {});
59
61
  this.options = options;
60
62
  this.uuid = nanoid();
61
63
  }
64
+ // ─────────────────────────────────────────────────────────────────────────────
65
+ // Fluent API for adding lifecycle callbacks
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+ /**
68
+ * Add setup callbacks to be executed in order during nodeSetup
69
+ */
70
+ onSetup(...callbacks) {
71
+ this.lifecycleCallbacks.setup.push(...callbacks);
72
+ return this;
73
+ }
74
+ /**
75
+ * Add loaded callbacks to be executed in order during nodeLoaded
76
+ */
77
+ onLoaded(...callbacks) {
78
+ this.lifecycleCallbacks.loaded.push(...callbacks);
79
+ return this;
80
+ }
81
+ /**
82
+ * Add update callbacks to be executed in order during nodeUpdate
83
+ */
84
+ onUpdate(...callbacks) {
85
+ this.lifecycleCallbacks.update.push(...callbacks);
86
+ return this;
87
+ }
88
+ /**
89
+ * Add destroy callbacks to be executed in order during nodeDestroy
90
+ */
91
+ onDestroy(...callbacks) {
92
+ this.lifecycleCallbacks.destroy.push(...callbacks);
93
+ return this;
94
+ }
95
+ /**
96
+ * Add cleanup callbacks to be executed in order during nodeCleanup
97
+ */
98
+ onCleanup(...callbacks) {
99
+ this.lifecycleCallbacks.cleanup.push(...callbacks);
100
+ return this;
101
+ }
102
+ /**
103
+ * Prepend setup callbacks (run before existing ones)
104
+ */
105
+ prependSetup(...callbacks) {
106
+ this.lifecycleCallbacks.setup.unshift(...callbacks);
107
+ return this;
108
+ }
109
+ /**
110
+ * Prepend update callbacks (run before existing ones)
111
+ */
112
+ prependUpdate(...callbacks) {
113
+ this.lifecycleCallbacks.update.unshift(...callbacks);
114
+ return this;
115
+ }
116
+ // ─────────────────────────────────────────────────────────────────────────────
117
+ // Tree structure
118
+ // ─────────────────────────────────────────────────────────────────────────────
62
119
  setParent(parent) {
63
120
  this.parent = parent;
64
121
  }
@@ -82,6 +139,9 @@ var BaseNode = class _BaseNode {
82
139
  isComposite() {
83
140
  return this.children.length > 0;
84
141
  }
142
+ // ─────────────────────────────────────────────────────────────────────────────
143
+ // Node lifecycle execution - runs internal + callback arrays
144
+ // ─────────────────────────────────────────────────────────────────────────────
85
145
  nodeSetup(params) {
86
146
  if (DEBUG_FLAG) {
87
147
  }
@@ -89,8 +149,8 @@ var BaseNode = class _BaseNode {
89
149
  if (typeof this._setup === "function") {
90
150
  this._setup(params);
91
151
  }
92
- if (this.setup) {
93
- this.setup(params);
152
+ for (const callback of this.lifecycleCallbacks.setup) {
153
+ callback(params);
94
154
  }
95
155
  this.children.forEach((child) => child.nodeSetup(params));
96
156
  }
@@ -101,21 +161,40 @@ var BaseNode = class _BaseNode {
101
161
  if (typeof this._update === "function") {
102
162
  this._update(params);
103
163
  }
104
- if (this.update) {
105
- this.update(params);
164
+ for (const callback of this.lifecycleCallbacks.update) {
165
+ callback(params);
106
166
  }
107
167
  this.children.forEach((child) => child.nodeUpdate(params));
108
168
  }
109
169
  nodeDestroy(params) {
110
170
  this.children.forEach((child) => child.nodeDestroy(params));
111
- if (this.destroy) {
112
- this.destroy(params);
171
+ for (const callback of this.lifecycleCallbacks.destroy) {
172
+ callback(params);
113
173
  }
114
174
  if (typeof this._destroy === "function") {
115
175
  this._destroy(params);
116
176
  }
117
177
  this.markedForRemoval = true;
118
178
  }
179
+ async nodeLoaded(params) {
180
+ if (typeof this._loaded === "function") {
181
+ await this._loaded(params);
182
+ }
183
+ for (const callback of this.lifecycleCallbacks.loaded) {
184
+ callback(params);
185
+ }
186
+ }
187
+ async nodeCleanup(params) {
188
+ for (const callback of this.lifecycleCallbacks.cleanup) {
189
+ callback(params);
190
+ }
191
+ if (typeof this._cleanup === "function") {
192
+ await this._cleanup(params);
193
+ }
194
+ }
195
+ // ─────────────────────────────────────────────────────────────────────────────
196
+ // Options
197
+ // ─────────────────────────────────────────────────────────────────────────────
119
198
  getOptions() {
120
199
  return this.options;
121
200
  }
@@ -137,11 +216,6 @@ var GameEntity = class extends BaseNode {
137
216
  custom = {};
138
217
  debugInfo = {};
139
218
  debugMaterial;
140
- lifeCycleDelegate = {
141
- setup: [],
142
- update: [],
143
- destroy: []
144
- };
145
219
  collisionDelegate = {
146
220
  collision: []
147
221
  };
@@ -166,67 +240,40 @@ var GameEntity = class extends BaseNode {
166
240
  this.name = this.options.name || "";
167
241
  return this;
168
242
  }
169
- onSetup(...callbacks) {
170
- const combineCallbacks = [...this.lifeCycleDelegate.setup ?? [], ...callbacks];
171
- this.lifeCycleDelegate = {
172
- ...this.lifeCycleDelegate,
173
- setup: combineCallbacks
174
- };
175
- return this;
176
- }
177
- onUpdate(...callbacks) {
178
- const combineCallbacks = [...this.lifeCycleDelegate.update ?? [], ...callbacks];
179
- this.lifeCycleDelegate = {
180
- ...this.lifeCycleDelegate,
181
- update: combineCallbacks
182
- };
183
- return this;
184
- }
185
- onDestroy(...callbacks) {
186
- this.lifeCycleDelegate = {
187
- ...this.lifeCycleDelegate,
188
- destroy: callbacks.length > 0 ? callbacks : void 0
189
- };
190
- return this;
191
- }
243
+ /**
244
+ * Add collision callbacks
245
+ */
192
246
  onCollision(...callbacks) {
193
- this.collisionDelegate = {
194
- collision: callbacks.length > 0 ? callbacks : void 0
195
- };
247
+ const existing = this.collisionDelegate.collision ?? [];
248
+ this.collisionDelegate.collision = [...existing, ...callbacks];
196
249
  return this;
197
250
  }
251
+ /**
252
+ * Entity-specific setup - runs behavior callbacks
253
+ * (User callbacks are handled by BaseNode's lifecycleCallbacks.setup)
254
+ */
198
255
  _setup(params) {
199
256
  this.behaviorCallbackMap.setup.forEach((callback) => {
200
257
  callback({ ...params, me: this });
201
258
  });
202
- if (this.lifeCycleDelegate.setup?.length) {
203
- const callbacks = this.lifeCycleDelegate.setup;
204
- callbacks.forEach((callback) => {
205
- callback({ ...params, me: this });
206
- });
207
- }
208
259
  }
209
260
  async _loaded(_params) {
210
261
  }
262
+ /**
263
+ * Entity-specific update - updates materials and runs behavior callbacks
264
+ * (User callbacks are handled by BaseNode's lifecycleCallbacks.update)
265
+ */
211
266
  _update(params) {
212
267
  this.updateMaterials(params);
213
- if (this.lifeCycleDelegate.update?.length) {
214
- const callbacks = this.lifeCycleDelegate.update;
215
- callbacks.forEach((callback) => {
216
- callback({ ...params, me: this });
217
- });
218
- }
219
268
  this.behaviorCallbackMap.update.forEach((callback) => {
220
269
  callback({ ...params, me: this });
221
270
  });
222
271
  }
272
+ /**
273
+ * Entity-specific destroy - runs behavior callbacks
274
+ * (User callbacks are handled by BaseNode's lifecycleCallbacks.destroy)
275
+ */
223
276
  _destroy(params) {
224
- if (this.lifeCycleDelegate.destroy?.length) {
225
- const callbacks = this.lifeCycleDelegate.destroy;
226
- callbacks.forEach((callback) => {
227
- callback({ ...params, me: this });
228
- });
229
- }
230
277
  this.behaviorCallbackMap.destroy.forEach((callback) => {
231
278
  callback({ ...params, me: this });
232
279
  });
@@ -818,7 +865,7 @@ var BoxBuilder = class extends EntityBuilder {
818
865
  return new ZylemBox(options);
819
866
  }
820
867
  };
821
- var BOX_TYPE = Symbol("Box");
868
+ var BOX_TYPE = /* @__PURE__ */ Symbol("Box");
822
869
  var ZylemBox = class _ZylemBox extends GameEntity {
823
870
  static type = BOX_TYPE;
824
871
  constructor(options) {
@@ -881,7 +928,7 @@ var SphereBuilder = class extends EntityBuilder {
881
928
  return new ZylemSphere(options);
882
929
  }
883
930
  };
884
- var SPHERE_TYPE = Symbol("Sphere");
931
+ var SPHERE_TYPE = /* @__PURE__ */ Symbol("Sphere");
885
932
  var ZylemSphere = class _ZylemSphere extends GameEntity {
886
933
  static type = SPHERE_TYPE;
887
934
  constructor(options) {
@@ -945,7 +992,7 @@ var SpriteBuilder = class extends EntityBuilder {
945
992
  return new ZylemSprite(options);
946
993
  }
947
994
  };
948
- var SPRITE_TYPE = Symbol("Sprite");
995
+ var SPRITE_TYPE = /* @__PURE__ */ Symbol("Sprite");
949
996
  var ZylemSprite = class _ZylemSprite extends GameEntity {
950
997
  static type = SPRITE_TYPE;
951
998
  sprites = [];
@@ -959,12 +1006,21 @@ var ZylemSprite = class _ZylemSprite extends GameEntity {
959
1006
  constructor(options) {
960
1007
  super();
961
1008
  this.options = { ...spriteDefaults, ...options };
962
- this.createSpritesFromImages(options?.images || []);
963
- this.createAnimations(options?.animations || []);
964
- this.lifeCycleDelegate = {
965
- update: [this.spriteUpdate.bind(this)],
966
- destroy: [this.spriteDestroy.bind(this)]
967
- };
1009
+ this.prependUpdate(this.spriteUpdate.bind(this));
1010
+ this.onDestroy(this.spriteDestroy.bind(this));
1011
+ }
1012
+ create() {
1013
+ this.sprites = [];
1014
+ this.spriteMap.clear();
1015
+ this.animations.clear();
1016
+ this.currentAnimation = null;
1017
+ this.currentAnimationFrame = "";
1018
+ this.currentAnimationIndex = 0;
1019
+ this.currentAnimationTime = 0;
1020
+ this.group = void 0;
1021
+ this.createSpritesFromImages(this.options?.images || []);
1022
+ this.createAnimations(this.options?.animations || []);
1023
+ return super.create();
968
1024
  }
969
1025
  createSpritesFromImages(images) {
970
1026
  const textureLoader = new TextureLoader2();
@@ -1211,7 +1267,7 @@ var PlaneBuilder = class extends EntityBuilder {
1211
1267
  return new ZylemPlane(options);
1212
1268
  }
1213
1269
  };
1214
- var PLANE_TYPE = Symbol("Plane");
1270
+ var PLANE_TYPE = /* @__PURE__ */ Symbol("Plane");
1215
1271
  var ZylemPlane = class extends GameEntity {
1216
1272
  static type = PLANE_TYPE;
1217
1273
  constructor(options) {
@@ -1269,7 +1325,7 @@ var ZoneBuilder = class extends EntityBuilder {
1269
1325
  return new ZylemZone(options);
1270
1326
  }
1271
1327
  };
1272
- var ZONE_TYPE = Symbol("Zone");
1328
+ var ZONE_TYPE = /* @__PURE__ */ Symbol("Zone");
1273
1329
  var ZylemZone = class extends GameEntity {
1274
1330
  static type = ZONE_TYPE;
1275
1331
  _enteredZone = /* @__PURE__ */ new Map();
@@ -1499,6 +1555,23 @@ var AnimationDelegate = class {
1499
1555
  this._currentAction = action;
1500
1556
  this._currentKey = key;
1501
1557
  }
1558
+ /**
1559
+ * Dispose of all animation resources
1560
+ */
1561
+ dispose() {
1562
+ Object.values(this._actions).forEach((action) => {
1563
+ action.stop();
1564
+ });
1565
+ if (this._mixer) {
1566
+ this._mixer.stopAllAction();
1567
+ this._mixer.uncacheRoot(this.target);
1568
+ this._mixer = null;
1569
+ }
1570
+ this._actions = {};
1571
+ this._animations = [];
1572
+ this._currentAction = null;
1573
+ this._currentKey = "";
1574
+ }
1502
1575
  get currentAnimationKey() {
1503
1576
  return this._currentKey;
1504
1577
  }
@@ -1557,7 +1630,7 @@ var ActorBuilder = class extends EntityBuilder {
1557
1630
  return new ZylemActor(options);
1558
1631
  }
1559
1632
  };
1560
- var ACTOR_TYPE = Symbol("Actor");
1633
+ var ACTOR_TYPE = /* @__PURE__ */ Symbol("Actor");
1561
1634
  var ZylemActor = class extends GameEntity {
1562
1635
  static type = ACTOR_TYPE;
1563
1636
  _object = null;
@@ -1568,9 +1641,7 @@ var ZylemActor = class extends GameEntity {
1568
1641
  constructor(options) {
1569
1642
  super();
1570
1643
  this.options = { ...actorDefaults, ...options };
1571
- this.lifeCycleDelegate = {
1572
- update: [this.actorUpdate.bind(this)]
1573
- };
1644
+ this.prependUpdate(this.actorUpdate.bind(this));
1574
1645
  this.controlledRotation = true;
1575
1646
  }
1576
1647
  async load() {
@@ -1590,6 +1661,34 @@ var ZylemActor = class extends GameEntity {
1590
1661
  async actorUpdate(params) {
1591
1662
  this._animationDelegate?.update(params.delta);
1592
1663
  }
1664
+ /**
1665
+ * Clean up actor resources including animations, models, and groups
1666
+ */
1667
+ actorDestroy() {
1668
+ if (this._animationDelegate) {
1669
+ this._animationDelegate.dispose();
1670
+ this._animationDelegate = null;
1671
+ }
1672
+ if (this._object) {
1673
+ this._object.traverse((child) => {
1674
+ if (child.isMesh) {
1675
+ const mesh = child;
1676
+ mesh.geometry?.dispose();
1677
+ if (Array.isArray(mesh.material)) {
1678
+ mesh.material.forEach((m) => m.dispose());
1679
+ } else if (mesh.material) {
1680
+ mesh.material.dispose();
1681
+ }
1682
+ }
1683
+ });
1684
+ this._object = null;
1685
+ }
1686
+ if (this.group) {
1687
+ this.group.clear();
1688
+ this.group = null;
1689
+ }
1690
+ this._modelFileNames = [];
1691
+ }
1593
1692
  async loadModels() {
1594
1693
  if (this._modelFileNames.length === 0) return;
1595
1694
  const promises = this._modelFileNames.map((file) => this._assetLoader.loadFile(file));
@@ -1676,7 +1775,7 @@ var TextBuilder = class extends EntityBuilder {
1676
1775
  return new ZylemText(options);
1677
1776
  }
1678
1777
  };
1679
- var TEXT_TYPE = Symbol("Text");
1778
+ var TEXT_TYPE = /* @__PURE__ */ Symbol("Text");
1680
1779
  var ZylemText = class _ZylemText extends GameEntity {
1681
1780
  static type = TEXT_TYPE;
1682
1781
  _sprite = null;
@@ -1689,12 +1788,20 @@ var ZylemText = class _ZylemText extends GameEntity {
1689
1788
  constructor(options) {
1690
1789
  super();
1691
1790
  this.options = { ...textDefaults, ...options };
1791
+ this.prependSetup(this.textSetup.bind(this));
1792
+ this.prependUpdate(this.textUpdate.bind(this));
1793
+ this.onDestroy(this.textDestroy.bind(this));
1794
+ }
1795
+ create() {
1796
+ this._sprite = null;
1797
+ this._texture = null;
1798
+ this._canvas = null;
1799
+ this._ctx = null;
1800
+ this._lastCanvasW = 0;
1801
+ this._lastCanvasH = 0;
1692
1802
  this.group = new Group5();
1693
1803
  this.createSprite();
1694
- this.lifeCycleDelegate = {
1695
- setup: [this.textSetup.bind(this)],
1696
- update: [this.textUpdate.bind(this)]
1697
- };
1804
+ return super.create();
1698
1805
  }
1699
1806
  createSprite() {
1700
1807
  this._canvas = document.createElement("canvas");
@@ -1860,6 +1967,24 @@ var ZylemText = class _ZylemText extends GameEntity {
1860
1967
  sticky: this.options.stickToViewport
1861
1968
  };
1862
1969
  }
1970
+ /**
1971
+ * Dispose of Three.js resources when the entity is destroyed.
1972
+ */
1973
+ async textDestroy() {
1974
+ this._texture?.dispose();
1975
+ if (this._sprite?.material) {
1976
+ this._sprite.material.dispose();
1977
+ }
1978
+ if (this._sprite) {
1979
+ this._sprite.removeFromParent();
1980
+ }
1981
+ this.group?.removeFromParent();
1982
+ this._sprite = null;
1983
+ this._texture = null;
1984
+ this._canvas = null;
1985
+ this._ctx = null;
1986
+ this._cameraRef = null;
1987
+ }
1863
1988
  };
1864
1989
  async function text(...args) {
1865
1990
  return createEntity({
@@ -1892,7 +2017,7 @@ var RectBuilder = class extends EntityBuilder {
1892
2017
  return new ZylemRect(options);
1893
2018
  }
1894
2019
  };
1895
- var RECT_TYPE = Symbol("Rect");
2020
+ var RECT_TYPE = /* @__PURE__ */ Symbol("Rect");
1896
2021
  var ZylemRect = class _ZylemRect extends GameEntity {
1897
2022
  static type = RECT_TYPE;
1898
2023
  _sprite = null;
@@ -1908,10 +2033,8 @@ var ZylemRect = class _ZylemRect extends GameEntity {
1908
2033
  this.options = { ...rectDefaults, ...options };
1909
2034
  this.group = new Group6();
1910
2035
  this.createSprite();
1911
- this.lifeCycleDelegate = {
1912
- setup: [this.rectSetup.bind(this)],
1913
- update: [this.rectUpdate.bind(this)]
1914
- };
2036
+ this.prependSetup(this.rectSetup.bind(this));
2037
+ this.prependUpdate(this.rectUpdate.bind(this));
1915
2038
  }
1916
2039
  createSprite() {
1917
2040
  this._canvas = document.createElement("canvas");