nova64 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +25 -8
  2. package/bin/nova64.js +165 -0
  3. package/dist/assets/console-CY_kygm3.js +14 -0
  4. package/dist/assets/console-CY_kygm3.js.map +1 -0
  5. package/dist/assets/main-l0sNRNKZ.js.map +1 -0
  6. package/dist/assets/sky/studio/nx.png +0 -0
  7. package/dist/assets/sky/studio/ny.png +0 -0
  8. package/dist/assets/sky/studio/nz.png +0 -0
  9. package/dist/assets/sky/studio/px.png +0 -0
  10. package/dist/assets/sky/studio/py.png +0 -0
  11. package/dist/assets/sky/studio/pz.png +0 -0
  12. package/dist/assets/vanilla-Dcuy32gi.js +2 -0
  13. package/dist/assets/vanilla-Dcuy32gi.js.map +1 -0
  14. package/dist/console.html +899 -0
  15. package/dist/docs/BENCHMARK.md +77 -0
  16. package/dist/docs/CHEATSHEET.md +255 -0
  17. package/dist/docs/EFFECTS_API_GUIDE.md +577 -0
  18. package/dist/docs/EFFECTS_QUICK_REFERENCE.md +331 -0
  19. package/dist/docs/FONT_CHARACTER_REFERENCE.md +219 -0
  20. package/dist/docs/FREE_GLB_ASSETS.md +330 -0
  21. package/dist/docs/FULLSCREEN_BUTTON_FEATURE.md +296 -0
  22. package/dist/docs/GAMEPAD_SUPPORT.md +348 -0
  23. package/dist/docs/GAME_IMPROVEMENTS.md +278 -0
  24. package/dist/docs/GAME_QUALITY_STATUS.md +300 -0
  25. package/dist/docs/MIGRATION_GUIDE.md +553 -0
  26. package/dist/docs/NOVA64_3D_API.md +356 -0
  27. package/dist/docs/NOVA64_API_REFERENCE.md +1406 -0
  28. package/dist/docs/NOVA64_UI_API.md +503 -0
  29. package/dist/docs/UI_SYSTEM_SUMMARY.md +445 -0
  30. package/dist/docs/VOXEL_ENGINE_GUIDE.md +662 -0
  31. package/dist/docs/VOXEL_QUICK_REFERENCE.md +386 -0
  32. package/dist/docs/api-3d.html +750 -0
  33. package/dist/docs/api-effects.html +385 -0
  34. package/dist/docs/api-improvements.md +121 -0
  35. package/dist/docs/api-skybox.html +407 -0
  36. package/dist/docs/api-sprites.html +321 -0
  37. package/dist/docs/api-voxel.html +337 -0
  38. package/dist/docs/api.html +543 -0
  39. package/dist/docs/assets.html +306 -0
  40. package/dist/docs/audio.html +340 -0
  41. package/dist/docs/blogs.html +286 -0
  42. package/dist/docs/collision.html +316 -0
  43. package/dist/docs/console.html +247 -0
  44. package/dist/docs/editor.html +297 -0
  45. package/dist/docs/font.html +247 -0
  46. package/dist/docs/framebuffer.html +247 -0
  47. package/dist/docs/fullscreen-button.html +297 -0
  48. package/dist/docs/gpu-systems.html +247 -0
  49. package/dist/docs/index.html +580 -0
  50. package/dist/docs/input.html +491 -0
  51. package/dist/docs/physics.html +311 -0
  52. package/dist/docs/screens.html +311 -0
  53. package/dist/docs/storage.html +311 -0
  54. package/dist/docs/textinput.html +332 -0
  55. package/dist/docs/ui.html +488 -0
  56. package/dist/examples/3d-advanced/code.js +695 -0
  57. package/dist/examples/adventure-comic-3d/code.js +342 -0
  58. package/dist/examples/audio-lab/code.js +150 -0
  59. package/dist/examples/boids-flocking/code.js +270 -0
  60. package/dist/examples/crystal-cathedral-3d/code.js +706 -0
  61. package/dist/examples/cyberpunk-city-3d/code.js +1383 -0
  62. package/dist/examples/demoscene/README.md +192 -0
  63. package/dist/examples/demoscene/code.js +1081 -0
  64. package/dist/examples/demoscene/meta.json +21 -0
  65. package/dist/examples/dungeon-crawler-3d/code.js +1117 -0
  66. package/dist/examples/f-zero-nova-3d/code.js +865 -0
  67. package/dist/examples/f-zero-nova-3d/code_old.js +1555 -0
  68. package/dist/examples/fps-demo-3d/code.js +744 -0
  69. package/dist/examples/game-of-life-3d/code.js +338 -0
  70. package/dist/examples/generative-art/code.js +632 -0
  71. package/dist/examples/hello-3d/code.js +325 -0
  72. package/dist/examples/hello-skybox/code.js +183 -0
  73. package/dist/examples/hello-world/code.js +19 -0
  74. package/dist/examples/input-showcase/code.js +109 -0
  75. package/dist/examples/instancing-demo/code.js +315 -0
  76. package/dist/examples/minecraft-demo/code.js +387 -0
  77. package/dist/examples/model-viewer-3d/code.js +114 -0
  78. package/dist/examples/mystical-realm-3d/code.js +1203 -0
  79. package/dist/examples/nature-explorer-3d/code.js +1318 -0
  80. package/dist/examples/particles-demo/code.js +522 -0
  81. package/dist/examples/pbr-showcase/code.js +140 -0
  82. package/dist/examples/physics-demo-3d/code.js +948 -0
  83. package/dist/examples/screen-demo/code.js +267 -0
  84. package/dist/examples/shooter-demo-3d/code.js +1286 -0
  85. package/dist/examples/space-combat-3d/IMPLEMENTATION_SUMMARY.md +109 -0
  86. package/dist/examples/space-combat-3d/README.md +135 -0
  87. package/dist/examples/space-combat-3d/code.js +1332 -0
  88. package/dist/examples/space-harrier-3d/code.js +923 -0
  89. package/dist/examples/star-fox-nova-3d/code.js +1116 -0
  90. package/dist/examples/star-fox-nova-3d/code_backup.js +410 -0
  91. package/dist/examples/star-fox-nova-3d/code_broken.js +1821 -0
  92. package/dist/examples/storage-quest/code.js +209 -0
  93. package/dist/examples/strider-demo-3d/IMPROVEMENT_OPTIONS.md +285 -0
  94. package/dist/examples/strider-demo-3d/cache-test.html +132 -0
  95. package/dist/examples/strider-demo-3d/code-fixed.js +582 -0
  96. package/dist/examples/strider-demo-3d/code-old.js +1537 -0
  97. package/dist/examples/strider-demo-3d/code.js +1462 -0
  98. package/dist/examples/strider-demo-3d/code.js.bak2 +1169 -0
  99. package/dist/examples/strider-demo-3d/fix-game.sh +53 -0
  100. package/dist/examples/super-plumber-64/README.md +128 -0
  101. package/dist/examples/super-plumber-64/code.js +1185 -0
  102. package/dist/examples/super-plumber-64/index.html +88 -0
  103. package/dist/examples/test-2d-overlay/code.js +32 -0
  104. package/dist/examples/test-font/code.js +51 -0
  105. package/dist/examples/test-minimal/code.js +21 -0
  106. package/dist/examples/ui-demo/code.js +306 -0
  107. package/dist/examples/wing-commander-space/README.md +180 -0
  108. package/dist/examples/wing-commander-space/code.js +1285 -0
  109. package/dist/examples/wizardry-3d/CHANGELOG.md +366 -0
  110. package/dist/examples/wizardry-3d/code.js +3928 -0
  111. package/dist/index.html +666 -0
  112. package/dist/os9-shell/assets/index-DIHfrTaW.css +1 -0
  113. package/dist/os9-shell/assets/index-KchE_ngx.js +483 -0
  114. package/dist/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  115. package/dist/os9-shell/index.html +23 -0
  116. package/dist/os9-shell/nova-icon.svg +12 -0
  117. package/dist/runtime/api-2d.js +1158 -0
  118. package/dist/runtime/api-3d/camera.js +73 -0
  119. package/dist/runtime/api-3d/instancing.js +180 -0
  120. package/dist/runtime/api-3d/lights.js +51 -0
  121. package/dist/runtime/api-3d/materials.js +47 -0
  122. package/dist/runtime/api-3d/models.js +84 -0
  123. package/dist/runtime/api-3d/particles.js +296 -0
  124. package/dist/runtime/api-3d/pbr.js +113 -0
  125. package/dist/runtime/api-3d/primitives.js +304 -0
  126. package/dist/runtime/api-3d/scene.js +169 -0
  127. package/dist/runtime/api-3d/transforms.js +161 -0
  128. package/dist/runtime/api-3d.js +166 -0
  129. package/dist/runtime/api-effects.js +840 -0
  130. package/dist/runtime/api-gameutils.js +476 -0
  131. package/dist/runtime/api-generative.js +610 -0
  132. package/dist/runtime/api-presets.js +85 -0
  133. package/dist/runtime/api-skybox.js +232 -0
  134. package/dist/runtime/api-sprites.js +100 -0
  135. package/dist/runtime/api-voxel.js +712 -0
  136. package/dist/runtime/api.js +201 -0
  137. package/dist/runtime/assets.js +27 -0
  138. package/dist/runtime/audio.js +114 -0
  139. package/dist/runtime/collision.js +47 -0
  140. package/dist/runtime/console.js +101 -0
  141. package/dist/runtime/editor.js +233 -0
  142. package/dist/runtime/font.js +233 -0
  143. package/dist/runtime/framebuffer.js +28 -0
  144. package/dist/runtime/fullscreen-button.js +185 -0
  145. package/dist/runtime/gpu-canvas2d.js +47 -0
  146. package/dist/runtime/gpu-threejs.js +643 -0
  147. package/dist/runtime/gpu-webgl2.js +310 -0
  148. package/dist/runtime/index.d.ts +682 -0
  149. package/dist/runtime/index.js +22 -0
  150. package/dist/runtime/input.js +225 -0
  151. package/dist/runtime/logger.js +60 -0
  152. package/dist/runtime/physics.js +101 -0
  153. package/dist/runtime/screens.js +213 -0
  154. package/dist/runtime/storage.js +38 -0
  155. package/dist/runtime/store.js +151 -0
  156. package/dist/runtime/textinput.js +68 -0
  157. package/dist/runtime/ui/buttons.js +124 -0
  158. package/dist/runtime/ui/panels.js +105 -0
  159. package/dist/runtime/ui/text.js +86 -0
  160. package/dist/runtime/ui/widgets.js +141 -0
  161. package/dist/runtime/ui.js +111 -0
  162. package/index.html +6 -1
  163. package/package.json +9 -2
  164. package/public/assets/sky/studio/nx.png +0 -0
  165. package/public/assets/sky/studio/ny.png +0 -0
  166. package/public/assets/sky/studio/nz.png +0 -0
  167. package/public/assets/sky/studio/px.png +0 -0
  168. package/public/assets/sky/studio/py.png +0 -0
  169. package/public/assets/sky/studio/pz.png +0 -0
  170. package/public/os9-shell/assets/index-KchE_ngx.js +483 -0
  171. package/public/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  172. package/public/os9-shell/index.html +10 -1
  173. package/runtime/api-2d.js +301 -21
  174. package/runtime/api-3d/pbr.js +45 -1
  175. package/runtime/api-3d.js +1 -0
  176. package/runtime/api-effects.js +90 -3
  177. package/runtime/api-gameutils.js +476 -0
  178. package/runtime/api-generative.js +610 -0
  179. package/runtime/api-skybox.js +54 -0
  180. package/runtime/api-voxel.js +139 -28
  181. package/runtime/gpu-threejs.js +13 -9
  182. package/runtime/ui.js +2 -2
  183. package/src/main.js +20 -0
  184. package/public/os9-shell/assets/index-B1Uvacma.js +0 -32825
  185. package/public/os9-shell/assets/index-B1Uvacma.js.map +0 -1
@@ -0,0 +1,476 @@
1
+ // runtime/api-gameutils.js
2
+ // Nova64 Game Utility API
3
+ // Reusable gameplay systems: screen shake, cooldowns, invulnerability, spawning, object pools.
4
+ // All pure logic — no GPU dependency.
5
+
6
+ // ── Screen Shake ──────────────────────────────────────────────────────────────
7
+
8
+ /**
9
+ * createShake(opts?) — create a screen-shake state.
10
+ * decay: how fast shake dies (default 4)
11
+ * maxMag: clamp magnitude (default 20)
12
+ * Returns a shake object to pass to triggerShake/updateShake/getShakeOffset.
13
+ */
14
+ function createShake(opts = {}) {
15
+ return {
16
+ mag: 0,
17
+ x: 0,
18
+ y: 0,
19
+ decay: opts.decay ?? 4,
20
+ maxMag: opts.maxMag ?? 20,
21
+ };
22
+ }
23
+
24
+ /** triggerShake(shake, magnitude) — fire a shake impulse. Stacks with existing shake. */
25
+ function triggerShake(shake, magnitude) {
26
+ shake.mag = Math.min(shake.mag + magnitude, shake.maxMag);
27
+ }
28
+
29
+ /** updateShake(shake, dt) — call every frame in update(). Updates shake.x, shake.y offsets. */
30
+ function updateShake(shake, dt) {
31
+ if (shake.mag > 0.01) {
32
+ shake.x = (Math.random() - 0.5) * shake.mag * 1.5;
33
+ shake.y = (Math.random() - 0.5) * shake.mag * 1.5;
34
+ shake.mag -= shake.decay * dt;
35
+ } else {
36
+ shake.mag = 0;
37
+ shake.x = 0;
38
+ shake.y = 0;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * getShakeOffset(shake) — returns [x, y] pixel offset.
44
+ * Add to your camera position or 2D draw offsets.
45
+ */
46
+ function getShakeOffset(shake) {
47
+ return [shake.x, shake.y];
48
+ }
49
+
50
+ // ── Cooldown Timers ───────────────────────────────────────────────────────────
51
+
52
+ /**
53
+ * createCooldown(duration) — create a cooldown timer.
54
+ * Returns an object with { remaining, duration }.
55
+ */
56
+ function createCooldown(duration) {
57
+ return { remaining: 0, duration };
58
+ }
59
+
60
+ /** useCooldown(cd) — try to use the ability. Returns true if ready, false if on cooldown. */
61
+ function useCooldown(cd) {
62
+ if (cd.remaining > 0) return false;
63
+ cd.remaining = cd.duration;
64
+ return true;
65
+ }
66
+
67
+ /** cooldownReady(cd) — check if cooldown is available. */
68
+ function cooldownReady(cd) {
69
+ return cd.remaining <= 0;
70
+ }
71
+
72
+ /** cooldownProgress(cd) — returns 0..1 (0 = just fired, 1 = fully ready). */
73
+ function cooldownProgress(cd) {
74
+ if (cd.duration <= 0) return 1;
75
+ return Math.max(0, 1 - cd.remaining / cd.duration);
76
+ }
77
+
78
+ /** updateCooldown(cd, dt) — tick a single cooldown. Call in update(). */
79
+ function updateCooldown(cd, dt) {
80
+ if (cd.remaining > 0) cd.remaining = Math.max(0, cd.remaining - dt);
81
+ }
82
+
83
+ /**
84
+ * createCooldownSet(defs) — create multiple named cooldowns at once.
85
+ * defs: { attack: 0.3, dash: 0.6, missile: 1.5, ... }
86
+ * Returns an object with the same keys, each containing a cooldown.
87
+ */
88
+ function createCooldownSet(defs) {
89
+ const set = {};
90
+ for (const [name, dur] of Object.entries(defs)) {
91
+ set[name] = createCooldown(dur);
92
+ }
93
+ return set;
94
+ }
95
+
96
+ /** updateCooldowns(set, dt) — tick all cooldowns in a set. */
97
+ function updateCooldowns(set, dt) {
98
+ for (const key in set) {
99
+ updateCooldown(set[key], dt);
100
+ }
101
+ }
102
+
103
+ // ── Hit State / Invulnerability ───────────────────────────────────────────────
104
+
105
+ /**
106
+ * createHitState(opts?) — create a hit/invulnerability tracker.
107
+ * invulnDuration: seconds of invincibility after hit (default 0.8)
108
+ * blinkRate: speed of visibility blink during invuln (default 25)
109
+ */
110
+ function createHitState(opts = {}) {
111
+ return {
112
+ invulnTimer: 0,
113
+ invulnDuration: opts.invulnDuration ?? 0.8,
114
+ blinkRate: opts.blinkRate ?? 25,
115
+ flashTimer: 0,
116
+ };
117
+ }
118
+
119
+ /**
120
+ * triggerHit(hitState) — call when entity takes damage.
121
+ * Returns false if currently invulnerable (damage should be ignored).
122
+ * Returns true if the hit lands (apply damage, starts invuln).
123
+ */
124
+ function triggerHit(hitState) {
125
+ if (hitState.invulnTimer > 0) return false;
126
+ hitState.invulnTimer = hitState.invulnDuration;
127
+ hitState.flashTimer = 0.1;
128
+ return true;
129
+ }
130
+
131
+ /** isInvulnerable(hitState) — true while invulnerability is active. */
132
+ function isInvulnerable(hitState) {
133
+ return hitState.invulnTimer > 0;
134
+ }
135
+
136
+ /** isVisible(hitState, time) — returns true/false for blink effect during invuln. Always true when not invuln. */
137
+ function isVisible(hitState, time) {
138
+ if (hitState.invulnTimer <= 0) return true;
139
+ return Math.sin(time * hitState.blinkRate) > 0;
140
+ }
141
+
142
+ /** isFlashing(hitState) — true for the brief white-flash moment right after being hit. */
143
+ function isFlashing(hitState) {
144
+ return hitState.flashTimer > 0;
145
+ }
146
+
147
+ /** updateHitState(hitState, dt) — tick timers. Call in update(). */
148
+ function updateHitState(hitState, dt) {
149
+ if (hitState.invulnTimer > 0) hitState.invulnTimer = Math.max(0, hitState.invulnTimer - dt);
150
+ if (hitState.flashTimer > 0) hitState.flashTimer = Math.max(0, hitState.flashTimer - dt);
151
+ }
152
+
153
+ // ── Spawn Wave Manager ────────────────────────────────────────────────────────
154
+
155
+ /**
156
+ * createSpawner(opts) — create a wave-based spawn manager.
157
+ * waveInterval: seconds between waves (default 10)
158
+ * baseCount: enemies in wave 1 (default 3)
159
+ * countGrowth: extra enemies per wave (default 1)
160
+ * maxCount: cap per wave (default 20)
161
+ * spawnFn: function(waveNum, index, count) called for each spawn
162
+ */
163
+ function createSpawner(opts = {}) {
164
+ return {
165
+ wave: 0,
166
+ timer: opts.initialDelay ?? opts.waveInterval ?? 10,
167
+ waveInterval: opts.waveInterval ?? 10,
168
+ baseCount: opts.baseCount ?? 3,
169
+ countGrowth: opts.countGrowth ?? 1,
170
+ maxCount: opts.maxCount ?? 20,
171
+ spawnFn: opts.spawnFn ?? null,
172
+ active: true,
173
+ totalSpawned: 0,
174
+ };
175
+ }
176
+
177
+ /** updateSpawner(spawner, dt) — tick the spawner. Calls spawnFn when a wave fires. */
178
+ function updateSpawner(spawner, dt) {
179
+ if (!spawner.active) return;
180
+ spawner.timer -= dt;
181
+ if (spawner.timer <= 0) {
182
+ spawner.wave++;
183
+ spawner.timer = spawner.waveInterval;
184
+ const count = Math.min(
185
+ spawner.baseCount + (spawner.wave - 1) * spawner.countGrowth,
186
+ spawner.maxCount
187
+ );
188
+ if (spawner.spawnFn) {
189
+ for (let i = 0; i < count; i++) {
190
+ spawner.spawnFn(spawner.wave, i, count);
191
+ spawner.totalSpawned++;
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ /** triggerWave(spawner) — force an immediate wave spawn. */
198
+ function triggerWave(spawner) {
199
+ spawner.timer = 0;
200
+ }
201
+
202
+ /** getSpawnerWave(spawner) — returns current wave number. */
203
+ function getSpawnerWave(spawner) {
204
+ return spawner.wave;
205
+ }
206
+
207
+ // ── Object Pool ───────────────────────────────────────────────────────────────
208
+
209
+ /**
210
+ * createPool(maxSize, factory?) — create a fixed-size object pool.
211
+ * maxSize: max live objects (default 100)
212
+ * factory: fn() → new object (default: () => ({}))
213
+ *
214
+ * Returns pool with .spawn(initFn), .forEach(fn), .recycle(), .count, .items
215
+ */
216
+ function createPool(maxSize = 100, factory) {
217
+ const _factory = factory ?? (() => ({}));
218
+ const items = [];
219
+ for (let i = 0; i < maxSize; i++) {
220
+ const obj = _factory();
221
+ obj._poolAlive = false;
222
+ items.push(obj);
223
+ }
224
+
225
+ return {
226
+ items,
227
+
228
+ /** spawn(initFn?) — activate a pooled object. initFn(obj) sets it up. Returns obj or null if full. */
229
+ spawn(initFn) {
230
+ for (let i = 0; i < items.length; i++) {
231
+ if (!items[i]._poolAlive) {
232
+ items[i]._poolAlive = true;
233
+ if (initFn) initFn(items[i]);
234
+ return items[i];
235
+ }
236
+ }
237
+ return null; // Pool exhausted
238
+ },
239
+
240
+ /** forEach(fn) — iterate only alive objects. fn(obj, index) — return false to kill that object. */
241
+ forEach(fn) {
242
+ for (let i = 0; i < items.length; i++) {
243
+ if (!items[i]._poolAlive) continue;
244
+ const result = fn(items[i], i);
245
+ if (result === false) items[i]._poolAlive = false;
246
+ }
247
+ },
248
+
249
+ /** kill(obj) — deactivate a specific pooled object. */
250
+ kill(obj) {
251
+ obj._poolAlive = false;
252
+ },
253
+
254
+ /** recycle() — deactivate all objects. */
255
+ recycle() {
256
+ for (let i = 0; i < items.length; i++) items[i]._poolAlive = false;
257
+ },
258
+
259
+ /** count — number of currently alive objects. */
260
+ get count() {
261
+ let n = 0;
262
+ for (let i = 0; i < items.length; i++) if (items[i]._poolAlive) n++;
263
+ return n;
264
+ },
265
+ };
266
+ }
267
+
268
+ // ── Floating Text System ──────────────────────────────────────────────────────
269
+
270
+ /**
271
+ * createFloatingTextSystem() — manages floating text (damage numbers, pickups, etc.)
272
+ * Returns system with .spawn(), .update(), .getTexts()
273
+ * Drawing is done separately via drawFloatingTexts() or drawFloatingTexts3D() in the 2D API.
274
+ */
275
+ function createFloatingTextSystem() {
276
+ const texts = [];
277
+
278
+ return {
279
+ /**
280
+ * spawn(text, x, y, opts?) — add a 2D floating text at screen coordinates.
281
+ * color: 0xRRGGBB (default white)
282
+ * duration: lifetime in seconds (default 1.0)
283
+ * riseSpeed: pixels/sec upward drift (default 30)
284
+ * vx, vy: custom velocity overrides
285
+ * scale: text scale (default 1)
286
+ * z: world-space Z (for 3D texts — use with drawFloatingTexts3D)
287
+ * vz: world-space Z velocity (default 0)
288
+ */
289
+ spawn(text, x, y, opts = {}) {
290
+ const is3D = opts.z !== undefined;
291
+ texts.push({
292
+ text: String(text),
293
+ x,
294
+ y,
295
+ z: opts.z,
296
+ vx: opts.vx ?? 0,
297
+ vy: opts.vy ?? (is3D ? (opts.riseSpeed ?? 2) : -(opts.riseSpeed ?? 30)),
298
+ vz: opts.vz ?? 0,
299
+ timer: opts.duration ?? 1.0,
300
+ maxTimer: opts.duration ?? 1.0,
301
+ color: opts.color ?? 0xffffff,
302
+ scale: opts.scale ?? 1,
303
+ });
304
+ },
305
+
306
+ /** update(dt) — tick all texts. Call in update(). */
307
+ update(dt) {
308
+ for (let i = texts.length - 1; i >= 0; i--) {
309
+ const t = texts[i];
310
+ t.x += t.vx * dt;
311
+ t.y += t.vy * dt;
312
+ if (t.z !== undefined) t.z += t.vz * dt;
313
+ t.timer -= dt;
314
+ if (t.timer <= 0) texts.splice(i, 1);
315
+ }
316
+ },
317
+
318
+ /** getTexts() — returns array of active texts for drawing. */
319
+ getTexts() {
320
+ return texts;
321
+ },
322
+
323
+ /** clear() — remove all texts. */
324
+ clear() {
325
+ texts.length = 0;
326
+ },
327
+
328
+ /** count — number of active texts. */
329
+ get count() {
330
+ return texts.length;
331
+ },
332
+ };
333
+ }
334
+
335
+ // ── State Machine ─────────────────────────────────────────────────────────────
336
+
337
+ /**
338
+ * createStateMachine(initialState) — simple finite state machine.
339
+ * Great for game states (title, playing, paused, gameover) or enemy AI.
340
+ */
341
+ function createStateMachine(initialState) {
342
+ let current = initialState;
343
+ let elapsed = 0;
344
+ const handlers = {};
345
+
346
+ return {
347
+ /** on(state, { enter?, update?, exit? }) — register handlers for a state. */
348
+ on(state, fns) {
349
+ handlers[state] = fns;
350
+ return this;
351
+ },
352
+
353
+ /** switchTo(state) — transition to a new state. Calls exit() on old, enter() on new. */
354
+ switchTo(state) {
355
+ if (handlers[current]?.exit) handlers[current].exit();
356
+ current = state;
357
+ elapsed = 0;
358
+ if (handlers[current]?.enter) handlers[current].enter();
359
+ },
360
+
361
+ /** update(dt) — tick the current state. Calls the state's update(dt, elapsed). */
362
+ update(dt) {
363
+ elapsed += dt;
364
+ if (handlers[current]?.update) handlers[current].update(dt, elapsed);
365
+ },
366
+
367
+ /** getState() — returns current state name. */
368
+ getState() {
369
+ return current;
370
+ },
371
+
372
+ /** getElapsed() — seconds spent in current state. */
373
+ getElapsed() {
374
+ return elapsed;
375
+ },
376
+
377
+ /** is(state) — check if currently in a state. */
378
+ is(state) {
379
+ return current === state;
380
+ },
381
+ };
382
+ }
383
+
384
+ // ── Eased Timer ───────────────────────────────────────────────────────────────
385
+
386
+ /**
387
+ * createTimer(duration, opts?) — one-shot or looping timer with progress.
388
+ * loop: boolean (default false)
389
+ * onComplete: callback when timer finishes
390
+ */
391
+ function createTimer(duration, opts = {}) {
392
+ return {
393
+ elapsed: 0,
394
+ duration,
395
+ loop: opts.loop ?? false,
396
+ onComplete: opts.onComplete ?? null,
397
+ done: false,
398
+
399
+ /** update(dt) — tick the timer. */
400
+ update(dt) {
401
+ if (this.done && !this.loop) return;
402
+ this.elapsed += dt;
403
+ if (this.elapsed >= this.duration) {
404
+ if (this.loop) {
405
+ this.elapsed -= this.duration;
406
+ } else {
407
+ this.elapsed = this.duration;
408
+ this.done = true;
409
+ }
410
+ if (this.onComplete) this.onComplete();
411
+ }
412
+ },
413
+
414
+ /** progress() — returns 0..1 normalized progress. */
415
+ progress() {
416
+ return Math.min(1, this.elapsed / this.duration);
417
+ },
418
+
419
+ /** reset() — restart the timer. */
420
+ reset() {
421
+ this.elapsed = 0;
422
+ this.done = false;
423
+ },
424
+ };
425
+ }
426
+
427
+ // ── Module Export ──────────────────────────────────────────────────────────────
428
+
429
+ export function gameUtilsApi() {
430
+ return {
431
+ exposeTo(target) {
432
+ Object.assign(target, {
433
+ // Screen Shake
434
+ createShake,
435
+ triggerShake,
436
+ updateShake,
437
+ getShakeOffset,
438
+
439
+ // Cooldowns
440
+ createCooldown,
441
+ useCooldown,
442
+ cooldownReady,
443
+ cooldownProgress,
444
+ updateCooldown,
445
+ createCooldownSet,
446
+ updateCooldowns,
447
+
448
+ // Hit State / Invulnerability
449
+ createHitState,
450
+ triggerHit,
451
+ isInvulnerable,
452
+ isVisible,
453
+ isFlashing,
454
+ updateHitState,
455
+
456
+ // Spawn Waves
457
+ createSpawner,
458
+ updateSpawner,
459
+ triggerWave,
460
+ getSpawnerWave,
461
+
462
+ // Object Pool
463
+ createPool,
464
+
465
+ // Floating Text
466
+ createFloatingTextSystem,
467
+
468
+ // State Machine
469
+ createStateMachine,
470
+
471
+ // Timer
472
+ createTimer,
473
+ });
474
+ },
475
+ };
476
+ }