minimojs 1.0.0-alpha.3 → 1.0.0-alpha.30
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 +85 -285
- package/dist/internal/AnimationSystem.js +345 -0
- package/dist/internal/AssetSystem.d.ts +1 -0
- package/dist/internal/AssetSystem.js +186 -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 +1 -0
- package/dist/internal/SoundSystem.js +55 -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 +407 -0
- package/dist/minimo-format.js +446 -0
- package/dist/minimo.d.ts +1754 -225
- package/dist/minimo.js +2551 -761
- 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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
export class AssetSystem {
|
|
3
|
+
constructor() {
|
|
4
|
+
/** @internal */
|
|
5
|
+
this._queuedImages = new Map();
|
|
6
|
+
/** @internal */
|
|
7
|
+
this._loadedImages = new Map();
|
|
8
|
+
/** @internal */
|
|
9
|
+
this._imageSources = new Map();
|
|
10
|
+
/** @internal */
|
|
11
|
+
this._imageVersions = new Map();
|
|
12
|
+
/**
|
|
13
|
+
* The asset file that declared each key, as the object passed to
|
|
14
|
+
* `loadAssets`. Keys are unique in the whole game, so a key belongs to
|
|
15
|
+
* exactly one asset file, or to `loadImage` and `createTexture`.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
this._assetFileByKey = new Map();
|
|
20
|
+
/** @internal */
|
|
21
|
+
this._registeredAssetFiles = new WeakSet();
|
|
22
|
+
}
|
|
23
|
+
queueImage(key, src) {
|
|
24
|
+
const safeKey = key.trim();
|
|
25
|
+
const safeSrc = src.trim();
|
|
26
|
+
if (safeKey.length === 0) {
|
|
27
|
+
throw new Error("MinimoJS: Image keys must be non-empty strings.");
|
|
28
|
+
}
|
|
29
|
+
if (safeSrc.length === 0) {
|
|
30
|
+
throw new Error("MinimoJS: Image sources must be non-empty strings.");
|
|
31
|
+
}
|
|
32
|
+
this.refuseAssetFileKey(safeKey, "loadImage()");
|
|
33
|
+
this.enqueueImage(safeKey, safeSrc);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Registers a parsed asset file and queues its images. `source` is the
|
|
37
|
+
* object the game passed, which identifies the file, so loading the same
|
|
38
|
+
* file again, as a scene's `onPreload()` does on every entry, only queues
|
|
39
|
+
* images that are not loaded.
|
|
40
|
+
*
|
|
41
|
+
* Every key is checked before anything is queued, so a refused file
|
|
42
|
+
* registers nothing. Returns `true` the first time a file is registered.
|
|
43
|
+
*/
|
|
44
|
+
registerAssetFile(file, source) {
|
|
45
|
+
for (const entry of file.assets) {
|
|
46
|
+
const owner = this._assetFileByKey.get(entry.key);
|
|
47
|
+
if (owner === undefined) {
|
|
48
|
+
if (this._imageSources.has(entry.key) || this._loadedImages.has(entry.key)) {
|
|
49
|
+
throw new Error(`MinimoJS: Asset key "${entry.key}" is already registered with loadImage() or createTexture(). Keys must be unique in the whole game.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (owner !== source) {
|
|
53
|
+
throw new Error(`MinimoJS: Asset key "${entry.key}" is declared by two asset files. Keys must be unique in the whole game.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const entry of file.assets) {
|
|
57
|
+
this._assetFileByKey.set(entry.key, source);
|
|
58
|
+
if (entry.type === "image") {
|
|
59
|
+
this.enqueueImage(entry.key, entry.src.trim());
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const isFirstRegistration = !this._registeredAssetFiles.has(source);
|
|
63
|
+
this._registeredAssetFiles.add(source);
|
|
64
|
+
return isFirstRegistration;
|
|
65
|
+
}
|
|
66
|
+
/** @internal */
|
|
67
|
+
enqueueImage(safeKey, safeSrc) {
|
|
68
|
+
const existingSrc = this._imageSources.get(safeKey);
|
|
69
|
+
if (existingSrc !== undefined && existingSrc !== safeSrc) {
|
|
70
|
+
throw new Error(`MinimoJS: Image key "${safeKey}" is already registered with a different source.`);
|
|
71
|
+
}
|
|
72
|
+
this._imageSources.set(safeKey, safeSrc);
|
|
73
|
+
this._queuedImages.set(safeKey, safeSrc);
|
|
74
|
+
}
|
|
75
|
+
createTexture(key, width, height, painter) {
|
|
76
|
+
const safeKey = key.trim();
|
|
77
|
+
if (safeKey.length === 0) {
|
|
78
|
+
throw new Error("MinimoJS: Texture keys must be non-empty strings.");
|
|
79
|
+
}
|
|
80
|
+
if (typeof painter !== "function") {
|
|
81
|
+
throw new Error("MinimoJS: createTexture() requires a painter function.");
|
|
82
|
+
}
|
|
83
|
+
this.refuseAssetFileKey(safeKey, "createTexture()");
|
|
84
|
+
const canvas = document.createElement("canvas");
|
|
85
|
+
canvas.width = Math.max(1, Math.round(Number.isFinite(width) ? width : 1));
|
|
86
|
+
canvas.height = Math.max(1, Math.round(Number.isFinite(height) ? height : 1));
|
|
87
|
+
const ctx = canvas.getContext("2d");
|
|
88
|
+
if (!ctx) {
|
|
89
|
+
throw new Error("MinimoJS: Could not acquire a texture rendering context.");
|
|
90
|
+
}
|
|
91
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
92
|
+
painter(ctx, canvas);
|
|
93
|
+
this._queuedImages.delete(safeKey);
|
|
94
|
+
this._imageSources.delete(safeKey);
|
|
95
|
+
this._loadedImages.set(safeKey, canvas);
|
|
96
|
+
this.bumpImageVersion(safeKey);
|
|
97
|
+
return canvas;
|
|
98
|
+
}
|
|
99
|
+
getImage(key) {
|
|
100
|
+
return this._loadedImages.get(key);
|
|
101
|
+
}
|
|
102
|
+
getImageVersion(key) {
|
|
103
|
+
return this._imageVersions.get(key) ?? 0;
|
|
104
|
+
}
|
|
105
|
+
hasImage(key) {
|
|
106
|
+
return this._loadedImages.has(key);
|
|
107
|
+
}
|
|
108
|
+
getQueuedImageCount() {
|
|
109
|
+
let total = 0;
|
|
110
|
+
for (const key of this._queuedImages.keys()) {
|
|
111
|
+
if (!this._loadedImages.has(key)) {
|
|
112
|
+
total++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return total;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Loads every queued image that is not loaded yet, settling all of them
|
|
119
|
+
* before returning instead of stopping at the first failure.
|
|
120
|
+
*
|
|
121
|
+
* Loaded images leave the queue; failed ones stay in it, so a retry loads
|
|
122
|
+
* only them. Returns the keys that failed, empty when all loaded.
|
|
123
|
+
*/
|
|
124
|
+
async loadQueuedImages(onProgress) {
|
|
125
|
+
const entries = [...this._queuedImages.entries()].filter(([key]) => !this._loadedImages.has(key));
|
|
126
|
+
const total = entries.length;
|
|
127
|
+
let settled = 0;
|
|
128
|
+
const failed = [];
|
|
129
|
+
onProgress?.(0, total, null);
|
|
130
|
+
if (total === 0) {
|
|
131
|
+
this._queuedImages.clear();
|
|
132
|
+
return failed;
|
|
133
|
+
}
|
|
134
|
+
await Promise.all(entries.map(async ([key, src]) => {
|
|
135
|
+
try {
|
|
136
|
+
const image = await this.loadImage(src);
|
|
137
|
+
this._loadedImages.set(key, image);
|
|
138
|
+
this.bumpImageVersion(key);
|
|
139
|
+
this._queuedImages.delete(key);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
failed.push(key);
|
|
143
|
+
}
|
|
144
|
+
settled += 1;
|
|
145
|
+
onProgress?.(settled, total, key);
|
|
146
|
+
}));
|
|
147
|
+
for (const key of [...this._queuedImages.keys()]) {
|
|
148
|
+
if (this._loadedImages.has(key)) {
|
|
149
|
+
this._queuedImages.delete(key);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return failed;
|
|
153
|
+
}
|
|
154
|
+
/** Returns the source registered for an image key, if any. */
|
|
155
|
+
getImageSource(key) {
|
|
156
|
+
return this._imageSources.get(key);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Drops queued images that were not loaded, so a later load does not pick
|
|
160
|
+
* them up. Their keys may be registered again.
|
|
161
|
+
*/
|
|
162
|
+
discardQueuedImages() {
|
|
163
|
+
this._queuedImages.clear();
|
|
164
|
+
}
|
|
165
|
+
/** @internal */
|
|
166
|
+
refuseAssetFileKey(safeKey, caller) {
|
|
167
|
+
if (this._assetFileByKey.has(safeKey)) {
|
|
168
|
+
throw new Error(`MinimoJS: ${caller} cannot use the key "${safeKey}", which an asset file declares. Keys must be unique in the whole game.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** @internal */
|
|
172
|
+
loadImage(src) {
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
const image = new Image();
|
|
175
|
+
image.onload = () => resolve(image);
|
|
176
|
+
image.onerror = () => {
|
|
177
|
+
reject(new Error(`MinimoJS: Failed to load image "${src}".`));
|
|
178
|
+
};
|
|
179
|
+
image.src = src;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/** @internal */
|
|
183
|
+
bumpImageVersion(key) {
|
|
184
|
+
this._imageVersions.set(key, (this._imageVersions.get(key) ?? 0) + 1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -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 {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
export class CanvasSystem {
|
|
3
|
+
constructor(/** @internal */ canvas) {
|
|
4
|
+
this.canvas = canvas;
|
|
5
|
+
/** @internal */
|
|
6
|
+
this.onResize = () => this.applyResponsiveCanvasLayout();
|
|
7
|
+
/** @internal */
|
|
8
|
+
this.onViewportChange = () => this.applyResponsiveCanvasLayout();
|
|
9
|
+
}
|
|
10
|
+
initialize() {
|
|
11
|
+
const mountCanvas = () => {
|
|
12
|
+
if (document.body && !this.canvas.isConnected) {
|
|
13
|
+
document.body.appendChild(this.canvas);
|
|
14
|
+
}
|
|
15
|
+
this.applyResponsiveCanvasLayout();
|
|
16
|
+
};
|
|
17
|
+
if (document.body) {
|
|
18
|
+
mountCanvas();
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
window.addEventListener("DOMContentLoaded", mountCanvas, { once: true });
|
|
22
|
+
}
|
|
23
|
+
window.addEventListener("resize", this.onResize);
|
|
24
|
+
window.visualViewport?.addEventListener("resize", this.onViewportChange);
|
|
25
|
+
window.visualViewport?.addEventListener("scroll", this.onViewportChange);
|
|
26
|
+
}
|
|
27
|
+
/** @internal */
|
|
28
|
+
applyResponsiveCanvasLayout() {
|
|
29
|
+
if (!document.body)
|
|
30
|
+
return;
|
|
31
|
+
const viewportW = Math.max(1, Math.floor(window.visualViewport?.width ??
|
|
32
|
+
window.innerWidth ??
|
|
33
|
+
document.documentElement.clientWidth ??
|
|
34
|
+
1));
|
|
35
|
+
const viewportH = Math.max(1, Math.floor(window.visualViewport?.height ??
|
|
36
|
+
window.innerHeight ??
|
|
37
|
+
document.documentElement.clientHeight ??
|
|
38
|
+
1));
|
|
39
|
+
document.body.style.margin = "0";
|
|
40
|
+
document.body.style.height = `${viewportH}px`;
|
|
41
|
+
document.body.style.minHeight = `${viewportH}px`;
|
|
42
|
+
document.body.style.display = "grid";
|
|
43
|
+
document.body.style.placeItems = "center";
|
|
44
|
+
document.body.style.touchAction = "manipulation";
|
|
45
|
+
document.body.style.overflow = "hidden";
|
|
46
|
+
const scale = Math.min(viewportW / this.canvas.width, viewportH / this.canvas.height);
|
|
47
|
+
const safeScale = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
48
|
+
this.canvas.style.width = `${Math.floor(this.canvas.width * safeScale)}px`;
|
|
49
|
+
this.canvas.style.height = `${Math.floor(this.canvas.height * safeScale)}px`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// A compile-time check, with no runtime code, that every property of a design
|
|
2
|
+
// object, except `id` and `type`, exists on its runtime class under the same
|
|
3
|
+
// name and with a compatible type (X-002). Designs use the runtime's names so
|
|
4
|
+
// agents learn one vocabulary for code and files. The build fails when a
|
|
5
|
+
// property is misnamed; a property that must be named differently would be
|
|
6
|
+
// declared here as an explicit exception.
|
|
7
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|