minimojs 1.0.0-alpha.4 → 1.0.0-alpha.40

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 (71) hide show
  1. package/README.md +58 -20
  2. package/dist/internal/AnimationSystem.js +345 -0
  3. package/dist/internal/AssetSystem.d.ts +6 -0
  4. package/dist/internal/AssetSystem.js +281 -0
  5. package/dist/internal/BackgroundSystem.d.ts +1 -0
  6. package/dist/internal/BackgroundSystem.js +26 -0
  7. package/dist/internal/CanvasSystem.d.ts +1 -0
  8. package/dist/internal/CanvasSystem.js +51 -0
  9. package/dist/internal/DesignNamingCheck.d.ts +1 -0
  10. package/dist/internal/DesignNamingCheck.js +7 -0
  11. package/dist/internal/ExplosionSystem.d.ts +1 -0
  12. package/dist/internal/ExplosionSystem.js +540 -0
  13. package/dist/internal/InputSystem.d.ts +1 -0
  14. package/dist/internal/InputSystem.js +292 -0
  15. package/dist/internal/LoopSystem.d.ts +1 -0
  16. package/dist/internal/LoopSystem.js +61 -0
  17. package/dist/internal/PhysicsSystem.d.ts +1 -0
  18. package/dist/internal/PhysicsSystem.js +174 -0
  19. package/dist/internal/RenderSystem.d.ts +1 -0
  20. package/dist/internal/RenderSystem.js +1021 -0
  21. package/dist/internal/SoundSystem.d.ts +39 -0
  22. package/dist/internal/SoundSystem.js +292 -0
  23. package/dist/internal/SpriteSystem.d.ts +1 -0
  24. package/dist/internal/SpriteSystem.js +32 -0
  25. package/dist/internal/TextSystem.d.ts +1 -0
  26. package/dist/internal/TextSystem.js +16 -0
  27. package/dist/internal/TimerSystem.d.ts +1 -0
  28. package/dist/internal/TimerSystem.js +43 -0
  29. package/dist/internal/TrailSystem.d.ts +1 -0
  30. package/dist/internal/TrailSystem.js +116 -0
  31. package/dist/internal/TransitionSystem.d.ts +1 -0
  32. package/dist/internal/TransitionSystem.js +74 -0
  33. package/dist/internal/arcade-racer/ArcadeRacerCollisionSystem.d.ts +1 -0
  34. package/dist/internal/arcade-racer/ArcadeRacerCollisionSystem.js +198 -0
  35. package/dist/internal/arcade-racer/ArcadeRacerLaneSystem.d.ts +1 -0
  36. package/dist/internal/arcade-racer/ArcadeRacerLaneSystem.js +120 -0
  37. package/dist/internal/arcade-racer/ArcadeRacerRenderSystem.d.ts +1 -0
  38. package/dist/internal/arcade-racer/ArcadeRacerRenderSystem.js +599 -0
  39. package/dist/internal/arcade-racer/ArcadeRacerRoadSprite.d.ts +1 -0
  40. package/dist/internal/arcade-racer/ArcadeRacerRoadSprite.js +13 -0
  41. package/dist/internal/arcade-racer/ArcadeRacerTrackSystem.d.ts +1 -0
  42. package/dist/internal/arcade-racer/ArcadeRacerTrackSystem.js +447 -0
  43. package/dist/minimo-ambient.d.ts +299 -0
  44. package/dist/minimo-ambient.js +1445 -0
  45. package/dist/minimo-arcaderacer.d.ts +1431 -0
  46. package/dist/minimo-arcaderacer.js +2060 -0
  47. package/dist/minimo-cards.d.ts +359 -0
  48. package/dist/minimo-cards.js +819 -0
  49. package/dist/minimo-dice.d.ts +206 -0
  50. package/dist/minimo-dice.js +719 -0
  51. package/dist/minimo-fire.d.ts +282 -0
  52. package/dist/minimo-fire.js +930 -0
  53. package/dist/minimo-format.d.ts +632 -0
  54. package/dist/minimo-format.js +669 -0
  55. package/dist/minimo-leaderboard.d.ts +199 -0
  56. package/dist/minimo-leaderboard.js +566 -0
  57. package/dist/minimo-multiplayer.d.ts +374 -0
  58. package/dist/minimo-multiplayer.js +1310 -0
  59. package/dist/minimo.d.ts +1690 -233
  60. package/dist/minimo.js +2587 -892
  61. package/package.json +10 -3
  62. package/dist/animations.js +0 -30
  63. package/dist/audio.js +0 -17
  64. package/dist/game.js +0 -1105
  65. package/dist/input.js +0 -185
  66. package/dist/internal-types.js +0 -4
  67. package/dist/physics.js +0 -10
  68. package/dist/render.js +0 -75
  69. package/dist/sprite.js +0 -149
  70. package/dist/timers.js +0 -23
  71. /package/dist/{pointer-info.js → internal/AnimationSystem.d.ts} +0 -0
package/README.md CHANGED
@@ -10,7 +10,7 @@ This README is intentionally high-level. It explains what the project is and how
10
10
 
11
11
  - ESM-only TypeScript-first engine
12
12
  - Single `Game` entry point
13
- - Emoji-based sprite rendering
13
+ - Image-backed sprite rendering, with emoji as a zero-asset alternative
14
14
  - rAF-driven loop (timers/animations/updates)
15
15
  - Responsive auto-centered canvas
16
16
 
@@ -21,12 +21,13 @@ This README is intentionally high-level. It explains what the project is and how
21
21
  - Runtime update delta (`dt`) is seconds
22
22
  - Coordinate system is center-based world space
23
23
  - Positive Y goes downward
24
+ - For new mobile-first games, prefer a portrait canvas of `720x1280`
24
25
 
25
26
  ## What It Intentionally Avoids
26
27
 
27
- - Scene manager/ECS architecture
28
+ - Heavy scene-manager/ECS architecture
28
29
  - Heavy physics engine features
29
- - Image spritesheets and asset pipelines
30
+ - Spritesheets, texture atlases, and asset pipelines
30
31
  - Nested subsystem APIs
31
32
  - `setTimeout` / `setInterval` game loops
32
33
 
@@ -39,29 +40,58 @@ npm install minimojs
39
40
  ## Quick Start
40
41
 
41
42
  ```ts
42
- import { Game, Sprite } from "minimojs";
43
+ import { DrawSprite, EmojiSprite, Game, type IScene } from "minimojs";
43
44
 
44
- const game = new Game(800, 600);
45
+ const game = new Game(720, 1280);
45
46
  game.gravityY = 980;
46
47
 
47
- const player = game.add(new Sprite("🐢", 400, 300, 48));
48
- player.x = 400;
49
- player.y = 300;
50
- player.gravityScale = 1;
51
-
52
- game.onUpdate = (dt) => {
53
- if (game.isKeyDown("ArrowLeft")) player.vx = -200;
54
- else if (game.isKeyDown("ArrowRight")) player.vx = 200;
55
- else player.vx = 0;
48
+ class MeterSprite extends DrawSprite {
49
+ public value = 0.5;
50
+
51
+ constructor(x: number, y: number) {
52
+ super(160, 28, x, y);
53
+ }
54
+
55
+ redraw(ctx: CanvasRenderingContext2D) {
56
+ ctx.fillStyle = "#1a2236";
57
+ ctx.fillRect(0, 0, this.width, this.height);
58
+ ctx.fillStyle = "#7ce7ff";
59
+ ctx.fillRect(0, 0, this.width * this.value, this.height);
60
+ }
61
+ }
62
+
63
+ class DemoScene implements IScene {
64
+ private player: EmojiSprite | null = null;
65
+ private meter: MeterSprite | null = null;
66
+
67
+ onCreate() {
68
+ this.player = game.add(new EmojiSprite("🐢", 360, 640, 48));
69
+ this.player.gravityScale = 1;
70
+ this.meter = game.add(new MeterSprite(360, 80));
71
+ this.meter.ignoreScroll = true;
72
+ }
73
+
74
+ onUpdate(dt: number) {
75
+ if (!this.player) return;
76
+
77
+ if (game.isKeyDown("ArrowLeft")) this.player.vx = -200;
78
+ else if (game.isKeyDown("ArrowRight")) this.player.vx = 200;
79
+ else this.player.vx = 0;
80
+
81
+ if (game.isKeyPressed(" ")) this.player.vy = -600;
82
+ if (this.meter) this.meter.value = Math.abs(this.player.vx) / 200;
83
+ game.drawText("MinimoJS", 10, 10, 16);
84
+ }
85
+ }
86
+
87
+ game.start(new DemoScene());
88
+ ```
56
89
 
57
- if (game.isKeyPressed(" ")) player.vy = -600;
58
- game.drawText("MinimoJS", 10, 10, 16);
59
- };
90
+ Note: text defaults to `monospace`, so a game that names no font downloads none. To use a web font, declare it in your HTML, register it with `game.requireFont(...)`, and set it as a `TextSprite`'s `fontFamily`.
60
91
 
61
- game.start();
62
- ```
92
+ Load images in `game.onPreload`, once, at boot. Only a game with many scenes that each carry many images of their own, such as one with many levels, should give scenes an `onPreload()` so each loads its images when the player reaches it; see `examples/level-loading/`.
63
93
 
64
- Note: `drawText()` uses `"Press Start 2P", monospace`. Load the font in your HTML if you want pixel-font styling.
94
+ Sounds come from files too. Declare them as `sound` entries in an asset file, or load them with `game.loadSound(key, src)`, and play them with `game.playSound(key)`, which returns a playback you can `pause()`, `resume()`, and `stop()`; pass `{ loop: true }` for music. MP3 plays in every browser. `game.sound` and `game.soundSequence` still synthesize tones with no file at all; see `examples/sound-files/`.
65
95
 
66
96
  ## Examples
67
97
 
@@ -70,6 +100,14 @@ Note: `drawText()` uses `"Press Start 2P", monospace`. Load the font in your HTM
70
100
  - `examples/space-invader/`
71
101
  - `examples/super-minimo-bros/`
72
102
  - `examples/scale-shift/`
103
+ - `examples/background-desert/`
104
+ - `examples/scene-lab/`
105
+ - `examples/image-sprite-sad-plush/`
106
+ - `examples/draw-sprite/`
107
+ - `examples/pseudo-3d-racer/`
108
+ - `examples/animations/`
109
+ - `examples/level-loading/`
110
+ - `examples/sound-files/`
73
111
 
74
112
  Run locally from the `minimojs` directory:
75
113
 
@@ -0,0 +1,345 @@
1
+ /** @internal */
2
+ export class AnimationSystem {
3
+ constructor() {
4
+ /** @internal */
5
+ this._animations = [];
6
+ /** @internal */
7
+ this._deformAnimations = [];
8
+ /** @internal */
9
+ this._motionAnimations = [];
10
+ /** @internal */
11
+ this._shakeAnimations = [];
12
+ /** @internal */
13
+ this._blinkAnimations = [];
14
+ /** @internal */
15
+ this._flickerAnimations = [];
16
+ /** @internal */
17
+ this._renderDataSprites = new Set();
18
+ }
19
+ update(dtMs) {
20
+ const animations = this._animations;
21
+ const toRemove = [];
22
+ for (let i = 0; i < animations.length; i++) {
23
+ const anim = animations[i];
24
+ anim.elapsed += dtMs;
25
+ const t = Math.min(anim.elapsed / anim.durationMs, 1);
26
+ anim.sprite[anim.property] = anim.from + (anim.to - anim.from) * t;
27
+ if (t >= 1) {
28
+ toRemove.push(i);
29
+ anim.onComplete?.();
30
+ }
31
+ }
32
+ for (let i = toRemove.length - 1; i >= 0; i--) {
33
+ animations.splice(toRemove[i], 1);
34
+ }
35
+ const deformAnimations = this._deformAnimations;
36
+ const deformToRemove = [];
37
+ for (let i = 0; i < deformAnimations.length; i++) {
38
+ const anim = deformAnimations[i];
39
+ anim.elapsed += dtMs;
40
+ const t = Math.min(anim.elapsed / anim.durationMs, 1);
41
+ if (t >= 1) {
42
+ deformToRemove.push(i);
43
+ anim.onComplete?.();
44
+ }
45
+ }
46
+ for (let i = deformToRemove.length - 1; i >= 0; i--) {
47
+ deformAnimations.splice(deformToRemove[i], 1);
48
+ }
49
+ const motionAnimations = this._motionAnimations;
50
+ const motionToRemove = [];
51
+ for (let i = 0; i < motionAnimations.length; i++) {
52
+ const anim = motionAnimations[i];
53
+ anim.elapsed += dtMs;
54
+ const t = Math.min(anim.elapsed / anim.durationMs, 1);
55
+ if (anim.kind === "arc") {
56
+ anim.sprite.x = anim.fromX + (anim.toX - anim.fromX) * t;
57
+ anim.sprite.y =
58
+ anim.fromY +
59
+ (anim.toY - anim.fromY) * t -
60
+ anim.magnitude * 4 * t * (1 - t);
61
+ }
62
+ else if (anim.kind === "bounce") {
63
+ anim.sprite.x = anim.fromX;
64
+ anim.sprite.y = anim.fromY - anim.magnitude * 4 * t * (1 - t);
65
+ }
66
+ else {
67
+ anim.sprite.x = anim.fromX;
68
+ anim.sprite.y = anim.fromY - Math.sin(t * Math.PI) * anim.magnitude;
69
+ }
70
+ if (t >= 1) {
71
+ motionToRemove.push(i);
72
+ anim.onComplete?.();
73
+ }
74
+ }
75
+ for (let i = motionToRemove.length - 1; i >= 0; i--) {
76
+ motionAnimations.splice(motionToRemove[i], 1);
77
+ }
78
+ this.rebuildRenderData(dtMs);
79
+ }
80
+ animateAlpha(sprite, to, durationMs, onComplete) {
81
+ const animations = this._animations.filter((a) => !(a.sprite === sprite && a.property === "alpha"));
82
+ animations.push({
83
+ sprite,
84
+ property: "alpha",
85
+ from: sprite.alpha,
86
+ to,
87
+ durationMs,
88
+ elapsed: 0,
89
+ onComplete,
90
+ });
91
+ this._animations = animations;
92
+ }
93
+ animateRotation(sprite, to, durationMs, onComplete) {
94
+ const animations = this._animations.filter((a) => !(a.sprite === sprite && a.property === "rotation"));
95
+ animations.push({
96
+ sprite,
97
+ property: "rotation",
98
+ from: sprite.rotation,
99
+ to,
100
+ durationMs,
101
+ elapsed: 0,
102
+ onComplete,
103
+ });
104
+ this._animations = animations;
105
+ }
106
+ animateDeform(sprite, toScaleX, toScaleY, pivotX, pivotY, durationMs, onComplete) {
107
+ const current = sprite._renderData;
108
+ const fromScaleX = current?.scaleX ?? 1;
109
+ const fromScaleY = current?.scaleY ?? 1;
110
+ const deformAnimations = this._deformAnimations.filter((a) => a.sprite !== sprite);
111
+ deformAnimations.push({
112
+ sprite,
113
+ fromScaleX,
114
+ fromScaleY,
115
+ toScaleX: this.sanitizeScale(toScaleX),
116
+ toScaleY: this.sanitizeScale(toScaleY),
117
+ pivotX: this.sanitizePivot(pivotX),
118
+ pivotY: this.sanitizePivot(pivotY),
119
+ durationMs,
120
+ elapsed: 0,
121
+ onComplete,
122
+ });
123
+ this._deformAnimations = deformAnimations;
124
+ }
125
+ animateShake(sprite, intensity, durationMs, onComplete) {
126
+ const animations = this._shakeAnimations.filter((a) => a.sprite !== sprite);
127
+ animations.push({
128
+ sprite,
129
+ intensity: this.sanitizeNonNegative(intensity),
130
+ durationMs: this.sanitizeDuration(durationMs),
131
+ elapsed: 0,
132
+ onComplete,
133
+ });
134
+ this._shakeAnimations = animations;
135
+ }
136
+ animateBounce(sprite, height, durationMs, onComplete) {
137
+ this.replaceMotionAnimation({
138
+ sprite,
139
+ kind: "bounce",
140
+ fromX: sprite.x,
141
+ fromY: sprite.y,
142
+ toX: sprite.x,
143
+ toY: sprite.y,
144
+ magnitude: this.sanitizeNonNegative(height),
145
+ durationMs: this.sanitizeDuration(durationMs),
146
+ elapsed: 0,
147
+ onComplete,
148
+ });
149
+ }
150
+ animateFloat(sprite, distance, durationMs, onComplete) {
151
+ this.replaceMotionAnimation({
152
+ sprite,
153
+ kind: "float",
154
+ fromX: sprite.x,
155
+ fromY: sprite.y,
156
+ toX: sprite.x,
157
+ toY: sprite.y,
158
+ magnitude: this.sanitizeNonNegative(distance),
159
+ durationMs: this.sanitizeDuration(durationMs),
160
+ elapsed: 0,
161
+ onComplete,
162
+ });
163
+ }
164
+ animateArc(sprite, toX, toY, arcHeight, durationMs, onComplete) {
165
+ this.replaceMotionAnimation({
166
+ sprite,
167
+ kind: "arc",
168
+ fromX: sprite.x,
169
+ fromY: sprite.y,
170
+ toX,
171
+ toY,
172
+ magnitude: this.sanitizeNonNegative(arcHeight),
173
+ durationMs: this.sanitizeDuration(durationMs),
174
+ elapsed: 0,
175
+ onComplete,
176
+ });
177
+ }
178
+ animateBlink(sprite, times, durationMs, onComplete) {
179
+ const animations = this._blinkAnimations.filter((a) => a.sprite !== sprite);
180
+ animations.push({
181
+ sprite,
182
+ times: Math.max(1, Math.round(Number.isFinite(times) ? times : 1)),
183
+ durationMs: this.sanitizeDuration(durationMs),
184
+ elapsed: 0,
185
+ onComplete,
186
+ });
187
+ this._blinkAnimations = animations;
188
+ }
189
+ animateFlicker(sprite, durationMs, onComplete) {
190
+ const animations = this._flickerAnimations.filter((a) => a.sprite !== sprite);
191
+ animations.push({
192
+ sprite,
193
+ durationMs: this.sanitizeDuration(durationMs),
194
+ elapsed: 0,
195
+ onComplete,
196
+ });
197
+ this._flickerAnimations = animations;
198
+ }
199
+ clearSpriteAnimations(sprite) {
200
+ this._animations = this._animations.filter((a) => a.sprite !== sprite);
201
+ this._deformAnimations = this._deformAnimations.filter((a) => a.sprite !== sprite);
202
+ this._motionAnimations = this._motionAnimations.filter((a) => a.sprite !== sprite);
203
+ this._shakeAnimations = this._shakeAnimations.filter((a) => a.sprite !== sprite);
204
+ this._blinkAnimations = this._blinkAnimations.filter((a) => a.sprite !== sprite);
205
+ this._flickerAnimations = this._flickerAnimations.filter((a) => a.sprite !== sprite);
206
+ this._renderDataSprites.delete(sprite);
207
+ sprite._renderData = null;
208
+ }
209
+ clearAll() {
210
+ for (const sprite of this._renderDataSprites) {
211
+ sprite._renderData = null;
212
+ }
213
+ this._animations = [];
214
+ this._deformAnimations = [];
215
+ this._motionAnimations = [];
216
+ this._shakeAnimations = [];
217
+ this._blinkAnimations = [];
218
+ this._flickerAnimations = [];
219
+ this._renderDataSprites.clear();
220
+ }
221
+ /** @internal */
222
+ sanitizeScale(value) {
223
+ if (!Number.isFinite(value))
224
+ return 1;
225
+ return Math.max(0, value);
226
+ }
227
+ /** @internal */
228
+ sanitizeDuration(value) {
229
+ if (!Number.isFinite(value))
230
+ return 1;
231
+ return Math.max(1, value);
232
+ }
233
+ /** @internal */
234
+ sanitizeNonNegative(value) {
235
+ if (!Number.isFinite(value))
236
+ return 0;
237
+ return Math.max(0, value);
238
+ }
239
+ /** @internal */
240
+ sanitizePivot(value) {
241
+ if (!Number.isFinite(value))
242
+ return 0.5;
243
+ return Math.max(0, Math.min(1, value));
244
+ }
245
+ /** @internal */
246
+ replaceMotionAnimation(entry) {
247
+ const animations = this._motionAnimations.filter((a) => a.sprite !== entry.sprite);
248
+ animations.push(entry);
249
+ this._motionAnimations = animations;
250
+ }
251
+ /** @internal */
252
+ rebuildRenderData(dtMs) {
253
+ const next = new Map();
254
+ const nextSprites = new Set();
255
+ const upsert = (sprite) => {
256
+ let state = next.get(sprite);
257
+ if (!state) {
258
+ state = {
259
+ scaleX: 1,
260
+ scaleY: 1,
261
+ pivotX: 0.5,
262
+ pivotY: 0.5,
263
+ offsetX: 0,
264
+ offsetY: 0,
265
+ alphaMultiplier: 1,
266
+ };
267
+ next.set(sprite, state);
268
+ nextSprites.add(sprite);
269
+ }
270
+ return state;
271
+ };
272
+ for (const anim of this._deformAnimations) {
273
+ const state = upsert(anim.sprite);
274
+ const t = Math.min(anim.elapsed / anim.durationMs, 1);
275
+ state.scaleX = anim.fromScaleX + (anim.toScaleX - anim.fromScaleX) * t;
276
+ state.scaleY = anim.fromScaleY + (anim.toScaleY - anim.fromScaleY) * t;
277
+ state.pivotX = anim.pivotX;
278
+ state.pivotY = anim.pivotY;
279
+ }
280
+ const shakeToRemove = [];
281
+ for (let i = 0; i < this._shakeAnimations.length; i++) {
282
+ const anim = this._shakeAnimations[i];
283
+ anim.elapsed += dtMs;
284
+ const t = Math.min(anim.elapsed / anim.durationMs, 1);
285
+ const decay = 1 - t;
286
+ const state = upsert(anim.sprite);
287
+ state.offsetX += (Math.random() * 2 - 1) * anim.intensity * decay;
288
+ state.offsetY += (Math.random() * 2 - 1) * anim.intensity * decay;
289
+ if (t >= 1) {
290
+ shakeToRemove.push(i);
291
+ anim.onComplete?.();
292
+ }
293
+ }
294
+ for (let i = shakeToRemove.length - 1; i >= 0; i--) {
295
+ this._shakeAnimations.splice(shakeToRemove[i], 1);
296
+ }
297
+ const blinkToRemove = [];
298
+ for (let i = 0; i < this._blinkAnimations.length; i++) {
299
+ const anim = this._blinkAnimations[i];
300
+ anim.elapsed += dtMs;
301
+ const t = Math.min(anim.elapsed / anim.durationMs, 1);
302
+ const state = upsert(anim.sprite);
303
+ const step = Math.floor(t * anim.times * 2);
304
+ state.alphaMultiplier *= step % 2 === 0 ? 1 : 0;
305
+ if (t >= 1) {
306
+ blinkToRemove.push(i);
307
+ anim.onComplete?.();
308
+ }
309
+ }
310
+ for (let i = blinkToRemove.length - 1; i >= 0; i--) {
311
+ this._blinkAnimations.splice(blinkToRemove[i], 1);
312
+ }
313
+ const flickerToRemove = [];
314
+ for (let i = 0; i < this._flickerAnimations.length; i++) {
315
+ const anim = this._flickerAnimations[i];
316
+ anim.elapsed += dtMs;
317
+ const t = Math.min(anim.elapsed / anim.durationMs, 1);
318
+ const state = upsert(anim.sprite);
319
+ state.alphaMultiplier *= 0.25 + Math.random() * 0.75;
320
+ if (t >= 1) {
321
+ flickerToRemove.push(i);
322
+ anim.onComplete?.();
323
+ }
324
+ }
325
+ for (let i = flickerToRemove.length - 1; i >= 0; i--) {
326
+ this._flickerAnimations.splice(flickerToRemove[i], 1);
327
+ }
328
+ for (const sprite of this._renderDataSprites) {
329
+ if (!nextSprites.has(sprite)) {
330
+ sprite._renderData = null;
331
+ }
332
+ }
333
+ for (const [sprite, state] of next.entries()) {
334
+ const isDefault = state.scaleX === 1 &&
335
+ state.scaleY === 1 &&
336
+ state.pivotX === 0.5 &&
337
+ state.pivotY === 0.5 &&
338
+ state.offsetX === 0 &&
339
+ state.offsetY === 0 &&
340
+ state.alphaMultiplier === 1;
341
+ sprite._renderData = isDefault ? null : state;
342
+ }
343
+ this._renderDataSprites = nextSprites;
344
+ }
345
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Fetches and decodes one audio file. The engine passes one backed by its
3
+ * `AudioContext`; the asset system only stores what it returns, so it needs no
4
+ * audio API of its own and runs in tests without a browser.
5
+ */
6
+ export type SoundLoader = (src: string) => Promise<unknown>;