minimojs 1.0.0-alpha.3 → 1.0.0-alpha.31
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/README.md +87 -284
- package/dist/internal/AnimationSystem.js +345 -0
- package/dist/internal/AssetSystem.d.ts +6 -0
- package/dist/internal/AssetSystem.js +273 -0
- package/dist/internal/BackgroundSystem.d.ts +1 -0
- package/dist/internal/BackgroundSystem.js +26 -0
- package/dist/internal/CanvasSystem.d.ts +1 -0
- package/dist/internal/CanvasSystem.js +51 -0
- package/dist/internal/DesignNamingCheck.d.ts +1 -0
- package/dist/internal/DesignNamingCheck.js +7 -0
- package/dist/internal/ExplosionSystem.d.ts +1 -0
- package/dist/internal/ExplosionSystem.js +540 -0
- package/dist/internal/InputSystem.d.ts +1 -0
- package/dist/internal/InputSystem.js +292 -0
- package/dist/internal/LoopSystem.d.ts +1 -0
- package/dist/internal/LoopSystem.js +61 -0
- package/dist/internal/PhysicsSystem.d.ts +1 -0
- package/dist/internal/PhysicsSystem.js +174 -0
- package/dist/internal/RenderSystem.d.ts +1 -0
- package/dist/internal/RenderSystem.js +1021 -0
- package/dist/internal/SoundSystem.d.ts +39 -0
- package/dist/internal/SoundSystem.js +292 -0
- package/dist/internal/SpriteSystem.d.ts +1 -0
- package/dist/internal/SpriteSystem.js +32 -0
- package/dist/internal/TextSystem.d.ts +1 -0
- package/dist/internal/TextSystem.js +16 -0
- package/dist/internal/TimerSystem.d.ts +1 -0
- package/dist/internal/TimerSystem.js +43 -0
- package/dist/internal/TrailSystem.d.ts +1 -0
- package/dist/internal/TrailSystem.js +116 -0
- package/dist/internal/TransitionSystem.d.ts +1 -0
- package/dist/internal/TransitionSystem.js +74 -0
- package/dist/internal/arcade-racer/ArcadeRacerCollisionSystem.d.ts +1 -0
- package/dist/internal/arcade-racer/ArcadeRacerCollisionSystem.js +198 -0
- package/dist/internal/arcade-racer/ArcadeRacerLaneSystem.d.ts +1 -0
- package/dist/internal/arcade-racer/ArcadeRacerLaneSystem.js +120 -0
- package/dist/internal/arcade-racer/ArcadeRacerRenderSystem.d.ts +1 -0
- package/dist/internal/arcade-racer/ArcadeRacerRenderSystem.js +599 -0
- package/dist/internal/arcade-racer/ArcadeRacerRoadSprite.d.ts +1 -0
- package/dist/internal/arcade-racer/ArcadeRacerRoadSprite.js +13 -0
- package/dist/internal/arcade-racer/ArcadeRacerTrackSystem.d.ts +1 -0
- package/dist/internal/arcade-racer/ArcadeRacerTrackSystem.js +447 -0
- package/dist/minimo-ambient.d.ts +299 -0
- package/dist/minimo-ambient.js +1445 -0
- package/dist/minimo-arcaderacer.d.ts +1431 -0
- package/dist/minimo-arcaderacer.js +2060 -0
- package/dist/minimo-cards.d.ts +359 -0
- package/dist/minimo-cards.js +819 -0
- package/dist/minimo-format.d.ts +429 -0
- package/dist/minimo-format.js +447 -0
- package/dist/minimo.d.ts +1815 -209
- package/dist/minimo.js +2660 -783
- package/package.json +10 -3
- package/dist/animations.js +0 -30
- package/dist/audio.js +0 -17
- package/dist/game.js +0 -1105
- package/dist/input.js +0 -185
- package/dist/internal-types.js +0 -4
- package/dist/physics.js +0 -10
- package/dist/render.js +0 -75
- package/dist/sprite.js +0 -149
- package/dist/timers.js +0 -23
- /package/dist/{pointer-info.js → internal/AnimationSystem.d.ts} +0 -0
|
@@ -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>;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Images and sounds, loaded by key. Both kinds share one queue, one progress
|
|
3
|
+
* count, one failure report, and one namespace of keys, so a game's loading
|
|
4
|
+
* screen and its retry cover every file it loads.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export class AssetSystem {
|
|
9
|
+
constructor(_soundLoader = null) {
|
|
10
|
+
this._soundLoader = _soundLoader;
|
|
11
|
+
/** @internal */
|
|
12
|
+
this._queuedImages = new Map();
|
|
13
|
+
/** @internal */
|
|
14
|
+
this._loadedImages = new Map();
|
|
15
|
+
/** @internal */
|
|
16
|
+
this._imageSources = new Map();
|
|
17
|
+
/** @internal */
|
|
18
|
+
this._imageVersions = new Map();
|
|
19
|
+
/**
|
|
20
|
+
* The asset file that declared each key, as the object passed to
|
|
21
|
+
* `loadAssets`. Keys are unique in the whole game, so a key belongs to
|
|
22
|
+
* exactly one asset file, or to `loadImage` and `createTexture`.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
this._assetFileByKey = new Map();
|
|
27
|
+
/** @internal */
|
|
28
|
+
this._registeredAssetFiles = new WeakSet();
|
|
29
|
+
/** @internal */
|
|
30
|
+
this._queuedSounds = new Map();
|
|
31
|
+
/** @internal */
|
|
32
|
+
this._loadedSounds = new Map();
|
|
33
|
+
/** @internal */
|
|
34
|
+
this._soundSources = new Map();
|
|
35
|
+
}
|
|
36
|
+
queueImage(key, src) {
|
|
37
|
+
const safeKey = key.trim();
|
|
38
|
+
const safeSrc = src.trim();
|
|
39
|
+
if (safeKey.length === 0) {
|
|
40
|
+
throw new Error("MinimoJS: Image keys must be non-empty strings.");
|
|
41
|
+
}
|
|
42
|
+
if (safeSrc.length === 0) {
|
|
43
|
+
throw new Error("MinimoJS: Image sources must be non-empty strings.");
|
|
44
|
+
}
|
|
45
|
+
this.refuseAssetFileKey(safeKey, "loadImage()");
|
|
46
|
+
this.refuseSoundKey(safeKey, "loadImage()");
|
|
47
|
+
this.enqueueImage(safeKey, safeSrc);
|
|
48
|
+
}
|
|
49
|
+
queueSound(key, src) {
|
|
50
|
+
const safeKey = key.trim();
|
|
51
|
+
const safeSrc = src.trim();
|
|
52
|
+
if (safeKey.length === 0) {
|
|
53
|
+
throw new Error("MinimoJS: Sound keys must be non-empty strings.");
|
|
54
|
+
}
|
|
55
|
+
if (safeSrc.length === 0) {
|
|
56
|
+
throw new Error("MinimoJS: Sound sources must be non-empty strings.");
|
|
57
|
+
}
|
|
58
|
+
this.refuseAssetFileKey(safeKey, "loadSound()");
|
|
59
|
+
if (this._imageSources.has(safeKey) || this._loadedImages.has(safeKey)) {
|
|
60
|
+
throw new Error(`MinimoJS: loadSound() cannot use the key "${safeKey}", which an image uses. Keys must be unique in the whole game.`);
|
|
61
|
+
}
|
|
62
|
+
this.enqueueSound(safeKey, safeSrc);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Registers a parsed asset file and queues its images. `source` is the
|
|
66
|
+
* object the game passed, which identifies the file, so loading the same
|
|
67
|
+
* file again, as a scene's `onPreload()` does on every entry, only queues
|
|
68
|
+
* images that are not loaded.
|
|
69
|
+
*
|
|
70
|
+
* Every key is checked before anything is queued, so a refused file
|
|
71
|
+
* registers nothing. Returns `true` the first time a file is registered.
|
|
72
|
+
*/
|
|
73
|
+
registerAssetFile(file, source) {
|
|
74
|
+
for (const entry of file.assets) {
|
|
75
|
+
const owner = this._assetFileByKey.get(entry.key);
|
|
76
|
+
if (owner === undefined) {
|
|
77
|
+
if (this._imageSources.has(entry.key) || this._loadedImages.has(entry.key)) {
|
|
78
|
+
throw new Error(`MinimoJS: Asset key "${entry.key}" is already registered with loadImage() or createTexture(). Keys must be unique in the whole game.`);
|
|
79
|
+
}
|
|
80
|
+
if (this._soundSources.has(entry.key)) {
|
|
81
|
+
throw new Error(`MinimoJS: Asset key "${entry.key}" is already registered with loadSound(). Keys must be unique in the whole game.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else if (owner !== source) {
|
|
85
|
+
throw new Error(`MinimoJS: Asset key "${entry.key}" is declared by two asset files. Keys must be unique in the whole game.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const entry of file.assets) {
|
|
89
|
+
this._assetFileByKey.set(entry.key, source);
|
|
90
|
+
if (entry.type === "image") {
|
|
91
|
+
this.enqueueImage(entry.key, entry.src.trim());
|
|
92
|
+
}
|
|
93
|
+
else if (entry.type === "sound") {
|
|
94
|
+
this.enqueueSound(entry.key, entry.src.trim());
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const isFirstRegistration = !this._registeredAssetFiles.has(source);
|
|
98
|
+
this._registeredAssetFiles.add(source);
|
|
99
|
+
return isFirstRegistration;
|
|
100
|
+
}
|
|
101
|
+
/** @internal */
|
|
102
|
+
enqueueImage(safeKey, safeSrc) {
|
|
103
|
+
const existingSrc = this._imageSources.get(safeKey);
|
|
104
|
+
if (existingSrc !== undefined && existingSrc !== safeSrc) {
|
|
105
|
+
throw new Error(`MinimoJS: Image key "${safeKey}" is already registered with a different source.`);
|
|
106
|
+
}
|
|
107
|
+
this._imageSources.set(safeKey, safeSrc);
|
|
108
|
+
this._queuedImages.set(safeKey, safeSrc);
|
|
109
|
+
}
|
|
110
|
+
/** @internal */
|
|
111
|
+
enqueueSound(safeKey, safeSrc) {
|
|
112
|
+
const existingSrc = this._soundSources.get(safeKey);
|
|
113
|
+
if (existingSrc !== undefined && existingSrc !== safeSrc) {
|
|
114
|
+
throw new Error(`MinimoJS: Sound key "${safeKey}" is already registered with a different source.`);
|
|
115
|
+
}
|
|
116
|
+
this._soundSources.set(safeKey, safeSrc);
|
|
117
|
+
if (!this._loadedSounds.has(safeKey)) {
|
|
118
|
+
this._queuedSounds.set(safeKey, safeSrc);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
createTexture(key, width, height, painter) {
|
|
122
|
+
const safeKey = key.trim();
|
|
123
|
+
if (safeKey.length === 0) {
|
|
124
|
+
throw new Error("MinimoJS: Texture keys must be non-empty strings.");
|
|
125
|
+
}
|
|
126
|
+
if (typeof painter !== "function") {
|
|
127
|
+
throw new Error("MinimoJS: createTexture() requires a painter function.");
|
|
128
|
+
}
|
|
129
|
+
this.refuseAssetFileKey(safeKey, "createTexture()");
|
|
130
|
+
this.refuseSoundKey(safeKey, "createTexture()");
|
|
131
|
+
const canvas = document.createElement("canvas");
|
|
132
|
+
canvas.width = Math.max(1, Math.round(Number.isFinite(width) ? width : 1));
|
|
133
|
+
canvas.height = Math.max(1, Math.round(Number.isFinite(height) ? height : 1));
|
|
134
|
+
const ctx = canvas.getContext("2d");
|
|
135
|
+
if (!ctx) {
|
|
136
|
+
throw new Error("MinimoJS: Could not acquire a texture rendering context.");
|
|
137
|
+
}
|
|
138
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
139
|
+
painter(ctx, canvas);
|
|
140
|
+
this._queuedImages.delete(safeKey);
|
|
141
|
+
this._imageSources.delete(safeKey);
|
|
142
|
+
this._loadedImages.set(safeKey, canvas);
|
|
143
|
+
this.bumpImageVersion(safeKey);
|
|
144
|
+
return canvas;
|
|
145
|
+
}
|
|
146
|
+
getImage(key) {
|
|
147
|
+
return this._loadedImages.get(key);
|
|
148
|
+
}
|
|
149
|
+
getImageVersion(key) {
|
|
150
|
+
return this._imageVersions.get(key) ?? 0;
|
|
151
|
+
}
|
|
152
|
+
hasImage(key) {
|
|
153
|
+
return this._loadedImages.has(key);
|
|
154
|
+
}
|
|
155
|
+
/** Returns a decoded sound, as the sound loader returned it, if it is loaded. */
|
|
156
|
+
getSound(key) {
|
|
157
|
+
return this._loadedSounds.get(key);
|
|
158
|
+
}
|
|
159
|
+
hasSound(key) {
|
|
160
|
+
return this._loadedSounds.has(key);
|
|
161
|
+
}
|
|
162
|
+
/** Counts the queued images and sounds that are not loaded yet. */
|
|
163
|
+
getQueuedAssetCount() {
|
|
164
|
+
let total = 0;
|
|
165
|
+
for (const key of this._queuedImages.keys()) {
|
|
166
|
+
if (!this._loadedImages.has(key)) {
|
|
167
|
+
total++;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const key of this._queuedSounds.keys()) {
|
|
171
|
+
if (!this._loadedSounds.has(key)) {
|
|
172
|
+
total++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return total;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Loads every queued image and sound that is not loaded yet, settling all
|
|
179
|
+
* of them before returning instead of stopping at the first failure.
|
|
180
|
+
*
|
|
181
|
+
* Loaded files leave the queue; failed ones stay in it, so a retry loads
|
|
182
|
+
* only them. Returns the keys that failed, empty when all loaded.
|
|
183
|
+
*/
|
|
184
|
+
async loadQueuedAssets(onProgress) {
|
|
185
|
+
const images = [...this._queuedImages.entries()].filter(([key]) => !this._loadedImages.has(key));
|
|
186
|
+
const sounds = [...this._queuedSounds.entries()].filter(([key]) => !this._loadedSounds.has(key));
|
|
187
|
+
const total = images.length + sounds.length;
|
|
188
|
+
let settled = 0;
|
|
189
|
+
const failed = [];
|
|
190
|
+
onProgress?.(0, total, null);
|
|
191
|
+
if (total === 0) {
|
|
192
|
+
this._queuedImages.clear();
|
|
193
|
+
this._queuedSounds.clear();
|
|
194
|
+
return failed;
|
|
195
|
+
}
|
|
196
|
+
const settle = (key) => {
|
|
197
|
+
settled += 1;
|
|
198
|
+
onProgress?.(settled, total, key);
|
|
199
|
+
};
|
|
200
|
+
await Promise.all([
|
|
201
|
+
...images.map(async ([key, src]) => {
|
|
202
|
+
try {
|
|
203
|
+
const image = await this.loadImage(src);
|
|
204
|
+
this._loadedImages.set(key, image);
|
|
205
|
+
this.bumpImageVersion(key);
|
|
206
|
+
this._queuedImages.delete(key);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
failed.push(key);
|
|
210
|
+
}
|
|
211
|
+
settle(key);
|
|
212
|
+
}),
|
|
213
|
+
...sounds.map(async ([key, src]) => {
|
|
214
|
+
try {
|
|
215
|
+
if (!this._soundLoader) {
|
|
216
|
+
throw new Error("MinimoJS: This game cannot load sounds.");
|
|
217
|
+
}
|
|
218
|
+
this._loadedSounds.set(key, await this._soundLoader(src));
|
|
219
|
+
this._queuedSounds.delete(key);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
failed.push(key);
|
|
223
|
+
}
|
|
224
|
+
settle(key);
|
|
225
|
+
}),
|
|
226
|
+
]);
|
|
227
|
+
for (const key of [...this._queuedImages.keys()]) {
|
|
228
|
+
if (this._loadedImages.has(key)) {
|
|
229
|
+
this._queuedImages.delete(key);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return failed;
|
|
233
|
+
}
|
|
234
|
+
/** Returns the source registered for an image or sound key, if any. */
|
|
235
|
+
getAssetSource(key) {
|
|
236
|
+
return this._imageSources.get(key) ?? this._soundSources.get(key);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Drops queued images and sounds that were not loaded, so a later load does
|
|
240
|
+
* not pick them up. Their keys may be registered again.
|
|
241
|
+
*/
|
|
242
|
+
discardQueuedAssets() {
|
|
243
|
+
this._queuedImages.clear();
|
|
244
|
+
this._queuedSounds.clear();
|
|
245
|
+
}
|
|
246
|
+
/** @internal */
|
|
247
|
+
refuseSoundKey(safeKey, caller) {
|
|
248
|
+
if (this._soundSources.has(safeKey)) {
|
|
249
|
+
throw new Error(`MinimoJS: ${caller} cannot use the key "${safeKey}", which a sound uses. Keys must be unique in the whole game.`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** @internal */
|
|
253
|
+
refuseAssetFileKey(safeKey, caller) {
|
|
254
|
+
if (this._assetFileByKey.has(safeKey)) {
|
|
255
|
+
throw new Error(`MinimoJS: ${caller} cannot use the key "${safeKey}", which an asset file declares. Keys must be unique in the whole game.`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** @internal */
|
|
259
|
+
loadImage(src) {
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
const image = new Image();
|
|
262
|
+
image.onload = () => resolve(image);
|
|
263
|
+
image.onerror = () => {
|
|
264
|
+
reject(new Error(`MinimoJS: Failed to load image "${src}".`));
|
|
265
|
+
};
|
|
266
|
+
image.src = src;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/** @internal */
|
|
270
|
+
bumpImageVersion(key) {
|
|
271
|
+
this._imageVersions.set(key, (this._imageVersions.get(key) ?? 0) + 1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
export class BackgroundSystem {
|
|
3
|
+
constructor() {
|
|
4
|
+
/** @internal */
|
|
5
|
+
this._layers = [];
|
|
6
|
+
}
|
|
7
|
+
add(layer) {
|
|
8
|
+
this._layers.push(layer);
|
|
9
|
+
return layer;
|
|
10
|
+
}
|
|
11
|
+
destroyLayer(layer) {
|
|
12
|
+
const index = this._layers.indexOf(layer);
|
|
13
|
+
if (index !== -1) {
|
|
14
|
+
this._layers.splice(index, 1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
getLayers() {
|
|
18
|
+
return [...this._layers];
|
|
19
|
+
}
|
|
20
|
+
getMutableLayers() {
|
|
21
|
+
return this._layers;
|
|
22
|
+
}
|
|
23
|
+
clearAll() {
|
|
24
|
+
this._layers = [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|