minimojs 1.0.0-alpha.42 → 1.0.0-alpha.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/internal/AssetSystem.d.ts +2 -0
- package/dist/internal/AssetSystem.js +145 -5
- package/dist/minimo-format.d.ts +144 -3
- package/dist/minimo-format.js +189 -6
- package/dist/minimo-leaderboard.d.ts +3 -3
- package/dist/minimo-leaderboard.js +3 -3
- package/dist/minimo-multiplayer.d.ts +8 -8
- package/dist/minimo-multiplayer.js +9 -9
- package/dist/minimo.d.ts +13 -2
- package/dist/minimo.js +14 -3
- package/package.json +1 -1
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { checkFramesFitImage, frameKey, parseTextureAtlas, spritesheetFrames, } from "../minimo-format.js";
|
|
2
|
+
async function fetchJson(src) {
|
|
3
|
+
const response = await fetch(src);
|
|
4
|
+
if (!response.ok) {
|
|
5
|
+
throw new Error(`MinimoJS: Failed to load "${src}" (${response.status}).`);
|
|
6
|
+
}
|
|
7
|
+
return response.json();
|
|
8
|
+
}
|
|
1
9
|
/**
|
|
2
10
|
* Images and sounds, loaded by key. Both kinds share one queue, one progress
|
|
3
11
|
* count, one failure report, and one namespace of keys, so a game's loading
|
|
@@ -6,8 +14,9 @@
|
|
|
6
14
|
* @internal
|
|
7
15
|
*/
|
|
8
16
|
export class AssetSystem {
|
|
9
|
-
constructor(_soundLoader = null) {
|
|
17
|
+
constructor(_soundLoader = null, _dataLoader = fetchJson) {
|
|
10
18
|
this._soundLoader = _soundLoader;
|
|
19
|
+
this._dataLoader = _dataLoader;
|
|
11
20
|
/** @internal */
|
|
12
21
|
this._queuedImages = new Map();
|
|
13
22
|
/** @internal */
|
|
@@ -32,6 +41,16 @@ export class AssetSystem {
|
|
|
32
41
|
this._loadedSounds = new Map();
|
|
33
42
|
/** @internal */
|
|
34
43
|
this._soundSources = new Map();
|
|
44
|
+
/** Spritesheets and atlases, by key. @internal */
|
|
45
|
+
this._sheets = new Map();
|
|
46
|
+
/** Each atlas's JSON source, by the atlas's key. @internal */
|
|
47
|
+
this._atlasSources = new Map();
|
|
48
|
+
/** @internal */
|
|
49
|
+
this._queuedAtlases = new Map();
|
|
50
|
+
/** @internal */
|
|
51
|
+
this._loadedAtlases = new Map();
|
|
52
|
+
/** The sheet that made each frame key. @internal */
|
|
53
|
+
this._frameOwners = new Map();
|
|
35
54
|
}
|
|
36
55
|
queueImage(key, src) {
|
|
37
56
|
const safeKey = key.trim();
|
|
@@ -73,6 +92,10 @@ export class AssetSystem {
|
|
|
73
92
|
registerAssetFile(file, source) {
|
|
74
93
|
for (const entry of file.assets) {
|
|
75
94
|
const owner = this._assetFileByKey.get(entry.key);
|
|
95
|
+
const sheet = this._frameOwners.get(entry.key);
|
|
96
|
+
if (sheet !== undefined) {
|
|
97
|
+
throw new Error(`MinimoJS: Asset key "${entry.key}" is a frame of "${sheet}". Keys must be unique in the whole game.`);
|
|
98
|
+
}
|
|
76
99
|
if (owner === undefined) {
|
|
77
100
|
if (this._imageSources.has(entry.key) || this._loadedImages.has(entry.key)) {
|
|
78
101
|
throw new Error(`MinimoJS: Asset key "${entry.key}" is already registered with loadImage() or createTexture(). Keys must be unique in the whole game.`);
|
|
@@ -93,6 +116,25 @@ export class AssetSystem {
|
|
|
93
116
|
else if (entry.type === "sound") {
|
|
94
117
|
this.enqueueSound(entry.key, entry.src.trim());
|
|
95
118
|
}
|
|
119
|
+
else if (entry.type === "spritesheet" || entry.type === "atlas") {
|
|
120
|
+
const { src, frameWidth, frameHeight, data } = entry;
|
|
121
|
+
if (!this._sheets.has(entry.key)) {
|
|
122
|
+
this._sheets.set(entry.key, {
|
|
123
|
+
type: entry.type,
|
|
124
|
+
frameWidth: frameWidth ?? 0,
|
|
125
|
+
frameHeight: frameHeight ?? 0,
|
|
126
|
+
built: false,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
this.enqueueImage(entry.key, src.trim());
|
|
130
|
+
if (data !== undefined) {
|
|
131
|
+
const safeData = data.trim();
|
|
132
|
+
this._atlasSources.set(entry.key, safeData);
|
|
133
|
+
if (!this._loadedAtlases.has(entry.key)) {
|
|
134
|
+
this._queuedAtlases.set(entry.key, safeData);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
96
138
|
}
|
|
97
139
|
const isFirstRegistration = !this._registeredAssetFiles.has(source);
|
|
98
140
|
this._registeredAssetFiles.add(source);
|
|
@@ -159,9 +201,13 @@ export class AssetSystem {
|
|
|
159
201
|
hasSound(key) {
|
|
160
202
|
return this._loadedSounds.has(key);
|
|
161
203
|
}
|
|
162
|
-
/**
|
|
204
|
+
/** The key of the spritesheet or atlas whose frame `key` is, if it is one. */
|
|
205
|
+
getFrameOwner(key) {
|
|
206
|
+
return this._frameOwners.get(key);
|
|
207
|
+
}
|
|
208
|
+
/** Counts the queued images, sounds, and atlas files that are not loaded yet. */
|
|
163
209
|
getQueuedAssetCount() {
|
|
164
|
-
let total =
|
|
210
|
+
let total = this._queuedAtlases.size;
|
|
165
211
|
for (const key of this._queuedImages.keys()) {
|
|
166
212
|
if (!this._loadedImages.has(key)) {
|
|
167
213
|
total++;
|
|
@@ -184,13 +230,15 @@ export class AssetSystem {
|
|
|
184
230
|
async loadQueuedAssets(onProgress) {
|
|
185
231
|
const images = [...this._queuedImages.entries()].filter(([key]) => !this._loadedImages.has(key));
|
|
186
232
|
const sounds = [...this._queuedSounds.entries()].filter(([key]) => !this._loadedSounds.has(key));
|
|
187
|
-
const
|
|
233
|
+
const atlases = [...this._queuedAtlases.entries()];
|
|
234
|
+
const total = images.length + sounds.length + atlases.length;
|
|
188
235
|
let settled = 0;
|
|
189
236
|
const failed = [];
|
|
190
237
|
onProgress?.(0, total, null);
|
|
191
238
|
if (total === 0) {
|
|
192
239
|
this._queuedImages.clear();
|
|
193
240
|
this._queuedSounds.clear();
|
|
241
|
+
this.buildSheets(failed);
|
|
194
242
|
return failed;
|
|
195
243
|
}
|
|
196
244
|
const settle = (key) => {
|
|
@@ -223,13 +271,94 @@ export class AssetSystem {
|
|
|
223
271
|
}
|
|
224
272
|
settle(key);
|
|
225
273
|
}),
|
|
274
|
+
...atlases.map(async ([key, src]) => {
|
|
275
|
+
try {
|
|
276
|
+
this._loadedAtlases.set(key, parseTextureAtlas(await this._dataLoader(src)));
|
|
277
|
+
this._queuedAtlases.delete(key);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
console.error(`MinimoJS: The atlas "${key}" could not be read from "${src}".`, error);
|
|
281
|
+
failed.push(key);
|
|
282
|
+
}
|
|
283
|
+
settle(key);
|
|
284
|
+
}),
|
|
226
285
|
]);
|
|
227
286
|
for (const key of [...this._queuedImages.keys()]) {
|
|
228
287
|
if (this._loadedImages.has(key)) {
|
|
229
288
|
this._queuedImages.delete(key);
|
|
230
289
|
}
|
|
231
290
|
}
|
|
232
|
-
|
|
291
|
+
this.buildSheets(failed);
|
|
292
|
+
// An atlas whose image and JSON both failed is one failure, under its key.
|
|
293
|
+
return [...new Set(failed)];
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Makes the frames of every spritesheet and atlas whose image, and atlas
|
|
297
|
+
* file, are loaded and whose frames are not made yet. A sheet whose frames
|
|
298
|
+
* cannot be made is added to `failed` once, with the reason on the console.
|
|
299
|
+
*
|
|
300
|
+
* @internal
|
|
301
|
+
*/
|
|
302
|
+
buildSheets(failed) {
|
|
303
|
+
for (const [key, sheet] of this._sheets) {
|
|
304
|
+
if (sheet.built)
|
|
305
|
+
continue;
|
|
306
|
+
const image = this._loadedImages.get(key);
|
|
307
|
+
const atlas = this._loadedAtlases.get(key);
|
|
308
|
+
if (!image || (sheet.type === "atlas" && !atlas))
|
|
309
|
+
continue;
|
|
310
|
+
try {
|
|
311
|
+
const width = imageWidth(image);
|
|
312
|
+
const height = imageHeight(image);
|
|
313
|
+
const frames = sheet.type === "atlas"
|
|
314
|
+
? atlas.frames
|
|
315
|
+
: spritesheetFrames({ key, frameWidth: sheet.frameWidth, frameHeight: sheet.frameHeight }, width, height);
|
|
316
|
+
checkFramesFitImage(key, frames, width, height);
|
|
317
|
+
for (const frame of frames) {
|
|
318
|
+
const name = frameKey(key, frame.name);
|
|
319
|
+
if (this.isKeyTaken(name)) {
|
|
320
|
+
throw new Error(`MinimoJS: The frame "${frame.name}" of "${key}" would be the key "${name}", which is already in use. Keys must be unique in the whole game.`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (const frame of frames) {
|
|
324
|
+
const name = frameKey(key, frame.name);
|
|
325
|
+
this._loadedImages.set(name, this.cutFrame(image, frame));
|
|
326
|
+
this._frameOwners.set(name, key);
|
|
327
|
+
this.bumpImageVersion(name);
|
|
328
|
+
}
|
|
329
|
+
sheet.built = true;
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
console.error(error instanceof Error ? error.message : error);
|
|
333
|
+
if (!failed.includes(key))
|
|
334
|
+
failed.push(key);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** @internal */
|
|
339
|
+
isKeyTaken(key) {
|
|
340
|
+
return (this._loadedImages.has(key) ||
|
|
341
|
+
this._imageSources.has(key) ||
|
|
342
|
+
this._soundSources.has(key) ||
|
|
343
|
+
this._assetFileByKey.has(key) ||
|
|
344
|
+
this._frameOwners.has(key));
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Copies one frame into a texture of its untrimmed size, at the place the
|
|
348
|
+
* trimmed rectangle had in it.
|
|
349
|
+
*
|
|
350
|
+
* @internal
|
|
351
|
+
*/
|
|
352
|
+
cutFrame(image, frame) {
|
|
353
|
+
const canvas = document.createElement("canvas");
|
|
354
|
+
canvas.width = frame.sourceWidth;
|
|
355
|
+
canvas.height = frame.sourceHeight;
|
|
356
|
+
const ctx = canvas.getContext("2d");
|
|
357
|
+
if (!ctx) {
|
|
358
|
+
throw new Error("MinimoJS: Could not acquire a texture rendering context.");
|
|
359
|
+
}
|
|
360
|
+
ctx.drawImage(image, frame.x, frame.y, frame.width, frame.height, frame.offsetX, frame.offsetY, frame.width, frame.height);
|
|
361
|
+
return canvas;
|
|
233
362
|
}
|
|
234
363
|
/** Returns the source registered for an image or sound key, if any. */
|
|
235
364
|
getAssetSource(key) {
|
|
@@ -242,6 +371,7 @@ export class AssetSystem {
|
|
|
242
371
|
discardQueuedAssets() {
|
|
243
372
|
this._queuedImages.clear();
|
|
244
373
|
this._queuedSounds.clear();
|
|
374
|
+
this._queuedAtlases.clear();
|
|
245
375
|
}
|
|
246
376
|
/** @internal */
|
|
247
377
|
refuseSoundKey(safeKey, caller) {
|
|
@@ -251,6 +381,10 @@ export class AssetSystem {
|
|
|
251
381
|
}
|
|
252
382
|
/** @internal */
|
|
253
383
|
refuseAssetFileKey(safeKey, caller) {
|
|
384
|
+
const sheet = this._frameOwners.get(safeKey);
|
|
385
|
+
if (sheet !== undefined) {
|
|
386
|
+
throw new Error(`MinimoJS: ${caller} cannot use the key "${safeKey}", which is a frame of "${sheet}". Keys must be unique in the whole game.`);
|
|
387
|
+
}
|
|
254
388
|
if (this._assetFileByKey.has(safeKey)) {
|
|
255
389
|
throw new Error(`MinimoJS: ${caller} cannot use the key "${safeKey}", which an asset file declares. Keys must be unique in the whole game.`);
|
|
256
390
|
}
|
|
@@ -279,3 +413,9 @@ export class AssetSystem {
|
|
|
279
413
|
this._imageVersions.set(key, (this._imageVersions.get(key) ?? 0) + 1);
|
|
280
414
|
}
|
|
281
415
|
}
|
|
416
|
+
function imageWidth(image) {
|
|
417
|
+
return image.naturalWidth || image.width;
|
|
418
|
+
}
|
|
419
|
+
function imageHeight(image) {
|
|
420
|
+
return image.naturalHeight || image.height;
|
|
421
|
+
}
|
package/dist/minimo-format.d.ts
CHANGED
|
@@ -51,6 +51,97 @@ export interface SoundAsset {
|
|
|
51
51
|
/** The audio file's URL or path, as `game.loadSound(key, src)` takes it. Non-empty. */
|
|
52
52
|
src: string;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* A spritesheet declared in an asset file: one image cut into a grid of equal
|
|
56
|
+
* frames. The image loads under `key`, as an `image` entry's does, and each
|
|
57
|
+
* frame becomes an image of its own, keyed `<key>/<n>`, numbered from `0`
|
|
58
|
+
* left to right and top to bottom, such as `"frog/0"` and `"frog/1"`. A frame
|
|
59
|
+
* key works wherever an image key does: `ImageSprite`, backgrounds, designs,
|
|
60
|
+
* and `game.getImage`.
|
|
61
|
+
*
|
|
62
|
+
* The grid starts at the image's top-left corner, with no margin or spacing;
|
|
63
|
+
* pixels left over at the right or bottom edge belong to no frame.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```json
|
|
67
|
+
* { "key": "frog", "type": "spritesheet", "src": "assets/frog-frames.png", "frameWidth": 320, "frameHeight": 320 }
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export interface SpritesheetAsset {
|
|
71
|
+
/** The key of the whole image, and the prefix of its frames' keys. Unique in the whole game. */
|
|
72
|
+
key: string;
|
|
73
|
+
/** Always `"spritesheet"`. */
|
|
74
|
+
type: "spritesheet";
|
|
75
|
+
/** The image's URL or path, as `game.loadImage(key, src)` takes it. Non-empty. */
|
|
76
|
+
src: string;
|
|
77
|
+
/** Width of every frame in pixels. A positive integer. */
|
|
78
|
+
frameWidth: number;
|
|
79
|
+
/** Height of every frame in pixels. A positive integer. */
|
|
80
|
+
frameHeight: number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A texture atlas declared in an asset file: one image holding named frames
|
|
84
|
+
* of any size, described by a JSON file in the JSON Hash format that
|
|
85
|
+
* TexturePacker (*JSON (Hash)*) and free-tex-packer (`JsonHash`) export; see
|
|
86
|
+
* {@link parseTextureAtlas} for what is read from it. The image loads under
|
|
87
|
+
* `key`, and each frame becomes an image of its own, keyed `<key>/<frame>`
|
|
88
|
+
* with the frame's name as the packer wrote it, such as `"pond/lotus"`.
|
|
89
|
+
*
|
|
90
|
+
* The image is the one `src` names; the JSON's own `meta.image`, the file
|
|
91
|
+
* name the packer gave the image, is ignored, so the image may be stored
|
|
92
|
+
* under any name.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```json
|
|
96
|
+
* { "key": "pond", "type": "atlas", "src": "assets/pond.png", "data": "assets/pond.json" }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export interface AtlasAsset {
|
|
100
|
+
/** The key of the whole image, and the prefix of its frames' keys. Unique in the whole game. */
|
|
101
|
+
key: string;
|
|
102
|
+
/** Always `"atlas"`. */
|
|
103
|
+
type: "atlas";
|
|
104
|
+
/** The image's URL or path, as `game.loadImage(key, src)` takes it. Non-empty. */
|
|
105
|
+
src: string;
|
|
106
|
+
/** The URL or path of the atlas JSON, in the JSON Hash format. Non-empty. */
|
|
107
|
+
data: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* One frame of a spritesheet or an atlas: where it is in the image, and the
|
|
111
|
+
* size of the picture it came from. A frame trimmed of its transparent border
|
|
112
|
+
* is drawn at `offsetX`, `offsetY` inside a texture of `sourceWidth` by
|
|
113
|
+
* `sourceHeight`, so a sprite of it is the size of the untrimmed picture and
|
|
114
|
+
* trimming never moves what it shows.
|
|
115
|
+
*/
|
|
116
|
+
export interface TextureFrame {
|
|
117
|
+
/** The frame's name, which follows the sheet's key in its image key: `<key>/<name>`. */
|
|
118
|
+
name: string;
|
|
119
|
+
/** Left edge of the frame in the image, in pixels. */
|
|
120
|
+
x: number;
|
|
121
|
+
/** Top edge of the frame in the image, in pixels. */
|
|
122
|
+
y: number;
|
|
123
|
+
/** Width of the frame in the image, in pixels. */
|
|
124
|
+
width: number;
|
|
125
|
+
/** Height of the frame in the image, in pixels. */
|
|
126
|
+
height: number;
|
|
127
|
+
/** Width of the picture before trimming, the texture's width. */
|
|
128
|
+
sourceWidth: number;
|
|
129
|
+
/** Height of the picture before trimming, the texture's height. */
|
|
130
|
+
sourceHeight: number;
|
|
131
|
+
/** Where the frame's left edge sits in the untrimmed picture. */
|
|
132
|
+
offsetX: number;
|
|
133
|
+
/** Where the frame's top edge sits in the untrimmed picture. */
|
|
134
|
+
offsetY: number;
|
|
135
|
+
}
|
|
136
|
+
/** A texture atlas, as {@link parseTextureAtlas} reads it from its JSON. */
|
|
137
|
+
export interface TextureAtlas {
|
|
138
|
+
/** The atlas's frames, in the JSON's order. */
|
|
139
|
+
frames: TextureFrame[];
|
|
140
|
+
/** The image size the JSON's `meta.size` states, or `null` when it states none. */
|
|
141
|
+
imageWidth: number | null;
|
|
142
|
+
/** The image size the JSON's `meta.size` states, or `null` when it states none. */
|
|
143
|
+
imageHeight: number | null;
|
|
144
|
+
}
|
|
54
145
|
/**
|
|
55
146
|
* An entry whose `type` this engine does not support, such as one written for
|
|
56
147
|
* a newer engine. It is kept as written, so a tool that saves the file does
|
|
@@ -80,7 +171,9 @@ export interface UnknownAsset {
|
|
|
80
171
|
* "assets": [
|
|
81
172
|
* { "key": "player", "type": "image", "src": "assets/player.png" },
|
|
82
173
|
* { "key": "bat", "type": "image", "src": "assets/bat.png" },
|
|
83
|
-
* { "key": "jump", "type": "sound", "src": "assets/jump.mp3" }
|
|
174
|
+
* { "key": "jump", "type": "sound", "src": "assets/jump.mp3" },
|
|
175
|
+
* { "key": "frog", "type": "spritesheet", "src": "assets/frog.png", "frameWidth": 64, "frameHeight": 64 },
|
|
176
|
+
* { "key": "pond", "type": "atlas", "src": "assets/pond.png", "data": "assets/pond.json" }
|
|
84
177
|
* ]
|
|
85
178
|
* }
|
|
86
179
|
* ```
|
|
@@ -91,8 +184,12 @@ export interface AssetFile {
|
|
|
91
184
|
/** The version of the file's shape. Version `1` is the only one so far. */
|
|
92
185
|
version: 1;
|
|
93
186
|
/** The declared assets, in file order. */
|
|
94
|
-
assets:
|
|
187
|
+
assets: AssetEntry[];
|
|
95
188
|
}
|
|
189
|
+
/** One entry of an asset file. */
|
|
190
|
+
export type AssetEntry = ImageAsset | SoundAsset | SpritesheetAsset | AtlasAsset | UnknownAsset;
|
|
191
|
+
/** The entry types this engine loads. */
|
|
192
|
+
export declare const ASSET_TYPES: readonly string[];
|
|
96
193
|
/**
|
|
97
194
|
* Validates the content of an asset file and returns a copy of it.
|
|
98
195
|
*
|
|
@@ -103,7 +200,9 @@ export interface AssetFile {
|
|
|
103
200
|
* - an entry without a string `key` and `type`;
|
|
104
201
|
* - an empty key, or one with leading or trailing spaces;
|
|
105
202
|
* - a key repeated in the file, whatever the types of its entries;
|
|
106
|
-
* - an `image` or `
|
|
203
|
+
* - an `image`, `sound`, `spritesheet`, or `atlas` entry without a non-empty string `src`;
|
|
204
|
+
* - a `spritesheet` whose `frameWidth` or `frameHeight` is not a positive integer;
|
|
205
|
+
* - an `atlas` without a non-empty string `data`.
|
|
107
206
|
*
|
|
108
207
|
* An entry of a type this engine does not support is kept unchanged, without
|
|
109
208
|
* checking its other fields, so nothing is lost when a tool saves the file.
|
|
@@ -117,6 +216,48 @@ export interface AssetFile {
|
|
|
117
216
|
* ```
|
|
118
217
|
*/
|
|
119
218
|
export declare function parseAssetFile(json: unknown): AssetFile;
|
|
219
|
+
/**
|
|
220
|
+
* The image key of a spritesheet's or an atlas's frame: the sheet's key, a
|
|
221
|
+
* slash, and the frame's name, such as `"frog/0"` or `"pond/lotus"`.
|
|
222
|
+
*/
|
|
223
|
+
export declare function frameKey(sheetKey: string, frameName: string): string;
|
|
224
|
+
/**
|
|
225
|
+
* The frames of a spritesheet whose image is `imageWidth` by `imageHeight`
|
|
226
|
+
* pixels: every whole cell of the grid, numbered from `0` left to right and
|
|
227
|
+
* top to bottom. Throws when the image holds no whole frame.
|
|
228
|
+
*/
|
|
229
|
+
export declare function spritesheetFrames(sheet: Pick<SpritesheetAsset, "key" | "frameWidth" | "frameHeight">, imageWidth: number, imageHeight: number): TextureFrame[];
|
|
230
|
+
/**
|
|
231
|
+
* Reads a texture atlas in the JSON Hash format, as TexturePacker
|
|
232
|
+
* (*JSON (Hash)*) and free-tex-packer (`JsonHash`) export it, and returns its
|
|
233
|
+
* frames.
|
|
234
|
+
*
|
|
235
|
+
* From each entry of `frames` it reads `frame`, the rectangle in the image;
|
|
236
|
+
* `sourceSize`, the picture's size before trimming; and `spriteSourceSize`,
|
|
237
|
+
* where the trimmed rectangle sat in it. The last two may be left out for an
|
|
238
|
+
* untrimmed frame. Other fields, such as `pivot`, `anchor`, `trimmed`, and
|
|
239
|
+
* the rest of `meta`, are ignored.
|
|
240
|
+
*
|
|
241
|
+
* It throws an error naming what is wrong, and which packer setting changes
|
|
242
|
+
* it, for:
|
|
243
|
+
* - a value that is not an object with a `frames` object, including the JSON
|
|
244
|
+
* Array format, whose `frames` is a list, and multi-image atlases, which
|
|
245
|
+
* list `textures`;
|
|
246
|
+
* - no frames, or a frame name that is empty or has leading or trailing spaces;
|
|
247
|
+
* - a rotated frame, `"rotated": true`;
|
|
248
|
+
* - a `meta.scale` other than 1;
|
|
249
|
+
* - a rectangle that is not made of whole, non-negative pixels, or a trimmed
|
|
250
|
+
* rectangle that does not fit in its source size;
|
|
251
|
+
* - a frame outside the image size `meta.size` states.
|
|
252
|
+
*
|
|
253
|
+
* @param json - The parsed atlas JSON.
|
|
254
|
+
*/
|
|
255
|
+
export declare function parseTextureAtlas(json: unknown): TextureAtlas;
|
|
256
|
+
/**
|
|
257
|
+
* Throws unless every frame lies inside an image of `imageWidth` by
|
|
258
|
+
* `imageHeight` pixels. `sheet` names the sheet in the error.
|
|
259
|
+
*/
|
|
260
|
+
export declare function checkFramesFitImage(sheet: string, frames: readonly TextureFrame[], imageWidth: number, imageHeight: number): void;
|
|
120
261
|
/**
|
|
121
262
|
* The type of a design object. Each built-in type maps to one runtime class:
|
|
122
263
|
* `image` to `ImageSprite`, `emoji` to `EmojiSprite`, `text` to `TextSprite`,
|
package/dist/minimo-format.js
CHANGED
|
@@ -12,9 +12,13 @@
|
|
|
12
12
|
const ASSET_FILE_FORMAT = "minimo-assets";
|
|
13
13
|
/** The newest asset file `version` this engine reads. */
|
|
14
14
|
const ASSET_FILE_VERSION = 1;
|
|
15
|
+
/** The entry types this engine loads. */
|
|
16
|
+
export const ASSET_TYPES = Object.freeze(["image", "sound", "spritesheet", "atlas"]);
|
|
15
17
|
const ASSET_FILE_FIELDS = new Set(["format", "version", "assets"]);
|
|
16
18
|
/** The fields of each supported entry type; `image` and `sound` share theirs. */
|
|
17
19
|
const SRC_ASSET_FIELDS = new Set(["key", "type", "src"]);
|
|
20
|
+
const SPRITESHEET_ASSET_FIELDS = new Set(["key", "type", "src", "frameWidth", "frameHeight"]);
|
|
21
|
+
const ATLAS_ASSET_FIELDS = new Set(["key", "type", "src", "data"]);
|
|
18
22
|
/**
|
|
19
23
|
* Validates the content of an asset file and returns a copy of it.
|
|
20
24
|
*
|
|
@@ -25,7 +29,9 @@ const SRC_ASSET_FIELDS = new Set(["key", "type", "src"]);
|
|
|
25
29
|
* - an entry without a string `key` and `type`;
|
|
26
30
|
* - an empty key, or one with leading or trailing spaces;
|
|
27
31
|
* - a key repeated in the file, whatever the types of its entries;
|
|
28
|
-
* - an `image` or `
|
|
32
|
+
* - an `image`, `sound`, `spritesheet`, or `atlas` entry without a non-empty string `src`;
|
|
33
|
+
* - a `spritesheet` whose `frameWidth` or `frameHeight` is not a positive integer;
|
|
34
|
+
* - an `atlas` without a non-empty string `data`.
|
|
29
35
|
*
|
|
30
36
|
* An entry of a type this engine does not support is kept unchanged, without
|
|
31
37
|
* checking its other fields, so nothing is lost when a tool saves the file.
|
|
@@ -82,13 +88,25 @@ export function parseAssetFile(json) {
|
|
|
82
88
|
if (typeof type !== "string" || type.length === 0) {
|
|
83
89
|
throw assetError(`${where} ("${key}") must have a string "type", found ${describe(type)}.`);
|
|
84
90
|
}
|
|
91
|
+
const named = `${where} ("${key}")`;
|
|
85
92
|
if (type === "image" || type === "sound") {
|
|
86
|
-
rejectUnknownFields(entry, SRC_ASSET_FIELDS,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
rejectUnknownFields(entry, SRC_ASSET_FIELDS, named);
|
|
94
|
+
assets.push({ key, type, src: requireText(entry, "src", named) });
|
|
95
|
+
}
|
|
96
|
+
else if (type === "spritesheet") {
|
|
97
|
+
rejectUnknownFields(entry, SPRITESHEET_ASSET_FIELDS, named);
|
|
98
|
+
const src = requireText(entry, "src", named);
|
|
99
|
+
for (const field of ["frameWidth", "frameHeight"]) {
|
|
100
|
+
const value = entry[field];
|
|
101
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
102
|
+
throw assetError(`${named} must have a positive integer "${field}", found ${describe(value)}.`);
|
|
103
|
+
}
|
|
90
104
|
}
|
|
91
|
-
assets.push({ key, type, src });
|
|
105
|
+
assets.push({ key, type, src, frameWidth: entry.frameWidth, frameHeight: entry.frameHeight });
|
|
106
|
+
}
|
|
107
|
+
else if (type === "atlas") {
|
|
108
|
+
rejectUnknownFields(entry, ATLAS_ASSET_FIELDS, named);
|
|
109
|
+
assets.push({ key, type, src: requireText(entry, "src", named), data: requireText(entry, "data", named) });
|
|
92
110
|
}
|
|
93
111
|
else {
|
|
94
112
|
assets.push({ ...entry, key, type });
|
|
@@ -97,6 +115,164 @@ export function parseAssetFile(json) {
|
|
|
97
115
|
return { format: ASSET_FILE_FORMAT, version: ASSET_FILE_VERSION, assets };
|
|
98
116
|
}
|
|
99
117
|
// ---------------------------------------------------------------------------
|
|
118
|
+
// Spritesheets and texture atlases
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
/**
|
|
121
|
+
* The image key of a spritesheet's or an atlas's frame: the sheet's key, a
|
|
122
|
+
* slash, and the frame's name, such as `"frog/0"` or `"pond/lotus"`.
|
|
123
|
+
*/
|
|
124
|
+
export function frameKey(sheetKey, frameName) {
|
|
125
|
+
return `${sheetKey}/${frameName}`;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The frames of a spritesheet whose image is `imageWidth` by `imageHeight`
|
|
129
|
+
* pixels: every whole cell of the grid, numbered from `0` left to right and
|
|
130
|
+
* top to bottom. Throws when the image holds no whole frame.
|
|
131
|
+
*/
|
|
132
|
+
export function spritesheetFrames(sheet, imageWidth, imageHeight) {
|
|
133
|
+
const columns = Math.floor(imageWidth / sheet.frameWidth);
|
|
134
|
+
const rows = Math.floor(imageHeight / sheet.frameHeight);
|
|
135
|
+
if (columns < 1 || rows < 1) {
|
|
136
|
+
throw new Error(`MinimoJS: The spritesheet "${sheet.key}" is ${imageWidth} by ${imageHeight} pixels, smaller than one ${sheet.frameWidth} by ${sheet.frameHeight} frame.`);
|
|
137
|
+
}
|
|
138
|
+
const frames = [];
|
|
139
|
+
for (let row = 0; row < rows; row++) {
|
|
140
|
+
for (let column = 0; column < columns; column++) {
|
|
141
|
+
frames.push({
|
|
142
|
+
name: String(frames.length),
|
|
143
|
+
x: column * sheet.frameWidth,
|
|
144
|
+
y: row * sheet.frameHeight,
|
|
145
|
+
width: sheet.frameWidth,
|
|
146
|
+
height: sheet.frameHeight,
|
|
147
|
+
sourceWidth: sheet.frameWidth,
|
|
148
|
+
sourceHeight: sheet.frameHeight,
|
|
149
|
+
offsetX: 0,
|
|
150
|
+
offsetY: 0,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return frames;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Reads a texture atlas in the JSON Hash format, as TexturePacker
|
|
158
|
+
* (*JSON (Hash)*) and free-tex-packer (`JsonHash`) export it, and returns its
|
|
159
|
+
* frames.
|
|
160
|
+
*
|
|
161
|
+
* From each entry of `frames` it reads `frame`, the rectangle in the image;
|
|
162
|
+
* `sourceSize`, the picture's size before trimming; and `spriteSourceSize`,
|
|
163
|
+
* where the trimmed rectangle sat in it. The last two may be left out for an
|
|
164
|
+
* untrimmed frame. Other fields, such as `pivot`, `anchor`, `trimmed`, and
|
|
165
|
+
* the rest of `meta`, are ignored.
|
|
166
|
+
*
|
|
167
|
+
* It throws an error naming what is wrong, and which packer setting changes
|
|
168
|
+
* it, for:
|
|
169
|
+
* - a value that is not an object with a `frames` object, including the JSON
|
|
170
|
+
* Array format, whose `frames` is a list, and multi-image atlases, which
|
|
171
|
+
* list `textures`;
|
|
172
|
+
* - no frames, or a frame name that is empty or has leading or trailing spaces;
|
|
173
|
+
* - a rotated frame, `"rotated": true`;
|
|
174
|
+
* - a `meta.scale` other than 1;
|
|
175
|
+
* - a rectangle that is not made of whole, non-negative pixels, or a trimmed
|
|
176
|
+
* rectangle that does not fit in its source size;
|
|
177
|
+
* - a frame outside the image size `meta.size` states.
|
|
178
|
+
*
|
|
179
|
+
* @param json - The parsed atlas JSON.
|
|
180
|
+
*/
|
|
181
|
+
export function parseTextureAtlas(json) {
|
|
182
|
+
if (!isPlainObject(json)) {
|
|
183
|
+
throw atlasError("an atlas must be a JSON object.");
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(json.textures)) {
|
|
186
|
+
throw atlasError(`it holds several images ("textures"). Export one image per atlas, in the JSON Hash format, with TexturePacker's Multipack off.`);
|
|
187
|
+
}
|
|
188
|
+
if (Array.isArray(json.frames)) {
|
|
189
|
+
throw atlasError(`"frames" is a list, the JSON Array format. Export the JSON Hash format: TexturePacker's "JSON (Hash)", free-tex-packer's "JSON (hash)".`);
|
|
190
|
+
}
|
|
191
|
+
if (!isPlainObject(json.frames)) {
|
|
192
|
+
throw atlasError(`"frames" must be an object of frames by name, found ${describe(json.frames)}.`);
|
|
193
|
+
}
|
|
194
|
+
const meta = isPlainObject(json.meta) ? json.meta : {};
|
|
195
|
+
if (meta.scale !== undefined && Number(meta.scale) !== 1) {
|
|
196
|
+
throw atlasError(`"meta.scale" is ${describe(meta.scale)}. Export the atlas at scale 1.`);
|
|
197
|
+
}
|
|
198
|
+
let imageWidth = null;
|
|
199
|
+
let imageHeight = null;
|
|
200
|
+
if (isPlainObject(meta.size)) {
|
|
201
|
+
imageWidth = requirePixels(meta.size.w, `"meta.size.w"`, 1);
|
|
202
|
+
imageHeight = requirePixels(meta.size.h, `"meta.size.h"`, 1);
|
|
203
|
+
}
|
|
204
|
+
const frames = [];
|
|
205
|
+
for (const [name, entry] of Object.entries(json.frames)) {
|
|
206
|
+
const where = `the frame "${name}"`;
|
|
207
|
+
if (name.length === 0 || name.trim() !== name) {
|
|
208
|
+
throw atlasError(`the frame name "${name}" is empty or has leading or trailing spaces.`);
|
|
209
|
+
}
|
|
210
|
+
if (!isPlainObject(entry)) {
|
|
211
|
+
throw atlasError(`${where} must be an object, found ${describe(entry)}.`);
|
|
212
|
+
}
|
|
213
|
+
if (entry.rotated === true) {
|
|
214
|
+
throw atlasError(`${where} is rotated. Export the atlas without rotation: TexturePacker's "Allow rotation", free-tex-packer's "allowRotation".`);
|
|
215
|
+
}
|
|
216
|
+
if (!isPlainObject(entry.frame)) {
|
|
217
|
+
throw atlasError(`${where} must have a "frame" rectangle, found ${describe(entry.frame)}.`);
|
|
218
|
+
}
|
|
219
|
+
const x = requirePixels(entry.frame.x, `${where}'s "frame.x"`, 0);
|
|
220
|
+
const y = requirePixels(entry.frame.y, `${where}'s "frame.y"`, 0);
|
|
221
|
+
const width = requirePixels(entry.frame.w, `${where}'s "frame.w"`, 1);
|
|
222
|
+
const height = requirePixels(entry.frame.h, `${where}'s "frame.h"`, 1);
|
|
223
|
+
let sourceWidth = width;
|
|
224
|
+
let sourceHeight = height;
|
|
225
|
+
if (entry.sourceSize !== undefined) {
|
|
226
|
+
if (!isPlainObject(entry.sourceSize)) {
|
|
227
|
+
throw atlasError(`${where}'s "sourceSize" must be an object, found ${describe(entry.sourceSize)}.`);
|
|
228
|
+
}
|
|
229
|
+
sourceWidth = requirePixels(entry.sourceSize.w, `${where}'s "sourceSize.w"`, 1);
|
|
230
|
+
sourceHeight = requirePixels(entry.sourceSize.h, `${where}'s "sourceSize.h"`, 1);
|
|
231
|
+
}
|
|
232
|
+
let offsetX = 0;
|
|
233
|
+
let offsetY = 0;
|
|
234
|
+
if (entry.spriteSourceSize !== undefined) {
|
|
235
|
+
if (!isPlainObject(entry.spriteSourceSize)) {
|
|
236
|
+
throw atlasError(`${where}'s "spriteSourceSize" must be an object, found ${describe(entry.spriteSourceSize)}.`);
|
|
237
|
+
}
|
|
238
|
+
offsetX = requirePixels(entry.spriteSourceSize.x, `${where}'s "spriteSourceSize.x"`, 0);
|
|
239
|
+
offsetY = requirePixels(entry.spriteSourceSize.y, `${where}'s "spriteSourceSize.y"`, 0);
|
|
240
|
+
}
|
|
241
|
+
if (offsetX + width > sourceWidth || offsetY + height > sourceHeight) {
|
|
242
|
+
throw atlasError(`${where}, ${width} by ${height} pixels at ${offsetX}, ${offsetY}, does not fit in its ${sourceWidth} by ${sourceHeight} "sourceSize".`);
|
|
243
|
+
}
|
|
244
|
+
if (imageWidth !== null && imageHeight !== null && (x + width > imageWidth || y + height > imageHeight)) {
|
|
245
|
+
throw atlasError(`${where} reaches outside the ${imageWidth} by ${imageHeight} image "meta.size" states.`);
|
|
246
|
+
}
|
|
247
|
+
frames.push({ name, x, y, width, height, sourceWidth, sourceHeight, offsetX, offsetY });
|
|
248
|
+
}
|
|
249
|
+
if (frames.length === 0) {
|
|
250
|
+
throw atlasError(`it has no frames.`);
|
|
251
|
+
}
|
|
252
|
+
return { frames, imageWidth, imageHeight };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Throws unless every frame lies inside an image of `imageWidth` by
|
|
256
|
+
* `imageHeight` pixels. `sheet` names the sheet in the error.
|
|
257
|
+
*/
|
|
258
|
+
export function checkFramesFitImage(sheet, frames, imageWidth, imageHeight) {
|
|
259
|
+
for (const frame of frames) {
|
|
260
|
+
if (frame.x + frame.width > imageWidth || frame.y + frame.height > imageHeight) {
|
|
261
|
+
throw new Error(`MinimoJS: The frame "${frame.name}" of "${sheet}" reaches outside its ${imageWidth} by ${imageHeight} image. Check that the atlas JSON was packed for this image.`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function requirePixels(value, where, minimum) {
|
|
266
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < minimum) {
|
|
267
|
+
const expected = minimum === 0 ? "a whole number of pixels, 0 or more" : "a whole number of pixels, 1 or more";
|
|
268
|
+
throw atlasError(`${where} must be ${expected}, found ${describe(value)}.`);
|
|
269
|
+
}
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
function atlasError(message) {
|
|
273
|
+
return new Error(`MinimoJS: Invalid atlas: ${message}`);
|
|
274
|
+
}
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
100
276
|
// Design files
|
|
101
277
|
// ---------------------------------------------------------------------------
|
|
102
278
|
/** The `format` value of every design file. */
|
|
@@ -711,6 +887,13 @@ export class Design {
|
|
|
711
887
|
return this.objects.filter((object) => object.tags?.includes(tag) === true);
|
|
712
888
|
}
|
|
713
889
|
}
|
|
890
|
+
function requireText(entry, field, where) {
|
|
891
|
+
const value = entry[field];
|
|
892
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
893
|
+
throw assetError(`${where} must have a non-empty string "${field}", found ${describe(value)}.`);
|
|
894
|
+
}
|
|
895
|
+
return value;
|
|
896
|
+
}
|
|
714
897
|
function isPlainObject(value) {
|
|
715
898
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
716
899
|
}
|
|
@@ -116,9 +116,9 @@ export interface LeaderboardShowOptions {
|
|
|
116
116
|
* On Minimo Games, the creator sets up a game's leaderboards, up to 5, on the
|
|
117
117
|
* game's Edit leaderboards page, or an AI agent with the `saveGameLeaderboard`
|
|
118
118
|
* tool; the game names one by its key. Their settings come from there, and a
|
|
119
|
-
* key that no leaderboard of the game has is unavailable. Leaderboards
|
|
120
|
-
*
|
|
121
|
-
* tables but saves no new records.
|
|
119
|
+
* key that no leaderboard of the game has is unavailable. Leaderboards save
|
|
120
|
+
* new records only while the creator's plan includes them: otherwise the game
|
|
121
|
+
* shows its tables but saves no new records.
|
|
122
122
|
*
|
|
123
123
|
* Scores of a leaderboard that takes Minimo Games accounts go to one table
|
|
124
124
|
* that every player sees. Minimo Games posts them for the signed-in player, and
|
|
@@ -265,9 +265,9 @@ export function renderLeaderboardHtml(table, view) {
|
|
|
265
265
|
* On Minimo Games, the creator sets up a game's leaderboards, up to 5, on the
|
|
266
266
|
* game's Edit leaderboards page, or an AI agent with the `saveGameLeaderboard`
|
|
267
267
|
* tool; the game names one by its key. Their settings come from there, and a
|
|
268
|
-
* key that no leaderboard of the game has is unavailable. Leaderboards
|
|
269
|
-
*
|
|
270
|
-
* tables but saves no new records.
|
|
268
|
+
* key that no leaderboard of the game has is unavailable. Leaderboards save
|
|
269
|
+
* new records only while the creator's plan includes them: otherwise the game
|
|
270
|
+
* shows its tables but saves no new records.
|
|
271
271
|
*
|
|
272
272
|
* Scores of a leaderboard that takes Minimo Games accounts go to one table
|
|
273
273
|
* that every player sees. Minimo Games posts them for the signed-in player, and
|
|
@@ -122,7 +122,7 @@ export interface RoomResultsOptions {
|
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
124
124
|
* What a room promise is refused with. `reason` is one of:
|
|
125
|
-
* `unavailable` (not on Minimo Games, or the
|
|
125
|
+
* `unavailable` (not on Minimo Games, or the creator's plan lacks multiplayer),
|
|
126
126
|
* `cancelled` (the player closed the lobby), `room_not_found`, `full`,
|
|
127
127
|
* `in_progress`, `removed`, `not_your_turn`, `stale` (another change came
|
|
128
128
|
* first; read the room again and retry), `not_host`, `wrong_phase`,
|
|
@@ -288,8 +288,8 @@ export declare class Room {
|
|
|
288
288
|
* frame: it suits card games, board games, word games, and races against the
|
|
289
289
|
* clock, not action games.
|
|
290
290
|
*
|
|
291
|
-
* It works only inside Minimo Games, and only while the game's creator
|
|
292
|
-
*
|
|
291
|
+
* It works only inside Minimo Games, and only while the game's creator's plan
|
|
292
|
+
* includes it. Players need no account: a guest types a name once.
|
|
293
293
|
*
|
|
294
294
|
* {@link Multiplayer.lobby} shows the lobby over the game, in HTML: creating a
|
|
295
295
|
* room, joining one by its code, sharing its link, and waiting for the others
|
|
@@ -341,8 +341,8 @@ export declare class Multiplayer {
|
|
|
341
341
|
/** The room the player is in, or `null`. */
|
|
342
342
|
get room(): Room | null;
|
|
343
343
|
/**
|
|
344
|
-
* Whether multiplayer can be played here: inside Minimo Games,
|
|
345
|
-
*
|
|
344
|
+
* Whether multiplayer can be played here: inside Minimo Games, on a game
|
|
345
|
+
* whose creator's plan includes it.
|
|
346
346
|
*/
|
|
347
347
|
available(): Promise<boolean>;
|
|
348
348
|
/** Creates a room, without the lobby. The player is its host. */
|
|
@@ -353,7 +353,7 @@ export declare class Multiplayer {
|
|
|
353
353
|
* The room this player is expected in, before any screen is shown: the room
|
|
354
354
|
* whose link they opened, an invitation, or the room they were in on this
|
|
355
355
|
* device before the page reloaded. `null` on a normal visit, outside
|
|
356
|
-
* Minimo Games, and when the game's creator
|
|
356
|
+
* Minimo Games, and when the game's creator's plan lacks multiplayer.
|
|
357
357
|
*
|
|
358
358
|
* It opens nothing, joins nothing, and can be asked again: the room stays
|
|
359
359
|
* pending until the player is in it. {@link Multiplayer.lobby} then takes
|
|
@@ -375,8 +375,8 @@ export declare class Multiplayer {
|
|
|
375
375
|
* game from a room's link goes straight into that room, and one who reloads
|
|
376
376
|
* the page returns to theirs.
|
|
377
377
|
*
|
|
378
|
-
* Refused with `unavailable` outside Minimo Games or when the game's creator
|
|
379
|
-
*
|
|
378
|
+
* Refused with `unavailable` outside Minimo Games or when the game's creator's
|
|
379
|
+
* plan lacks multiplayer, after the lobby says so, and with `cancelled` when the
|
|
380
380
|
* player closes it.
|
|
381
381
|
*/
|
|
382
382
|
lobby(): Promise<Room>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What a room promise is refused with. `reason` is one of:
|
|
3
|
-
* `unavailable` (not on Minimo Games, or the
|
|
3
|
+
* `unavailable` (not on Minimo Games, or the creator's plan lacks multiplayer),
|
|
4
4
|
* `cancelled` (the player closed the lobby), `room_not_found`, `full`,
|
|
5
5
|
* `in_progress`, `removed`, `not_your_turn`, `stale` (another change came
|
|
6
6
|
* first; read the room again and retry), `not_host`, `wrong_phase`,
|
|
@@ -752,8 +752,8 @@ export class Room {
|
|
|
752
752
|
* frame: it suits card games, board games, word games, and races against the
|
|
753
753
|
* clock, not action games.
|
|
754
754
|
*
|
|
755
|
-
* It works only inside Minimo Games, and only while the game's creator
|
|
756
|
-
*
|
|
755
|
+
* It works only inside Minimo Games, and only while the game's creator's plan
|
|
756
|
+
* includes it. Players need no account: a guest types a name once.
|
|
757
757
|
*
|
|
758
758
|
* {@link Multiplayer.lobby} shows the lobby over the game, in HTML: creating a
|
|
759
759
|
* room, joining one by its code, sharing its link, and waiting for the others
|
|
@@ -817,8 +817,8 @@ export class Multiplayer {
|
|
|
817
817
|
return this._room;
|
|
818
818
|
}
|
|
819
819
|
/**
|
|
820
|
-
* Whether multiplayer can be played here: inside Minimo Games,
|
|
821
|
-
*
|
|
820
|
+
* Whether multiplayer can be played here: inside Minimo Games, on a game
|
|
821
|
+
* whose creator's plan includes it.
|
|
822
822
|
*/
|
|
823
823
|
async available() {
|
|
824
824
|
const info = await connectHost();
|
|
@@ -859,7 +859,7 @@ export class Multiplayer {
|
|
|
859
859
|
const info = await request("hello", {}, HOST_TIMEOUT_MS * 2);
|
|
860
860
|
this._info = info;
|
|
861
861
|
if (!info.available) {
|
|
862
|
-
throw new MultiplayerError(info.preview ? "Multiplayer is
|
|
862
|
+
throw new MultiplayerError(info.preview ? "Multiplayer is not on this account's plan: this preview opens rooms once it is" : "Multiplayer is not available for this game", "unavailable");
|
|
863
863
|
}
|
|
864
864
|
if (!this._listening) {
|
|
865
865
|
this._listening = true;
|
|
@@ -924,7 +924,7 @@ export class Multiplayer {
|
|
|
924
924
|
* The room this player is expected in, before any screen is shown: the room
|
|
925
925
|
* whose link they opened, an invitation, or the room they were in on this
|
|
926
926
|
* device before the page reloaded. `null` on a normal visit, outside
|
|
927
|
-
* Minimo Games, and when the game's creator
|
|
927
|
+
* Minimo Games, and when the game's creator's plan lacks multiplayer.
|
|
928
928
|
*
|
|
929
929
|
* It opens nothing, joins nothing, and can be asked again: the room stays
|
|
930
930
|
* pending until the player is in it. {@link Multiplayer.lobby} then takes
|
|
@@ -976,8 +976,8 @@ export class Multiplayer {
|
|
|
976
976
|
* game from a room's link goes straight into that room, and one who reloads
|
|
977
977
|
* the page returns to theirs.
|
|
978
978
|
*
|
|
979
|
-
* Refused with `unavailable` outside Minimo Games or when the game's creator
|
|
980
|
-
*
|
|
979
|
+
* Refused with `unavailable` outside Minimo Games or when the game's creator's
|
|
980
|
+
* plan lacks multiplayer, after the lobby says so, and with `cancelled` when the
|
|
981
981
|
* player closes it.
|
|
982
982
|
*/
|
|
983
983
|
async lobby() {
|
package/dist/minimo.d.ts
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
import { SoundPlayback } from "./internal/SoundSystem.js";
|
|
11
11
|
import type { PlaySoundOptions } from "./internal/SoundSystem.js";
|
|
12
12
|
import type { DesignObject } from "./minimo-format.js";
|
|
13
|
-
export { Design, parseAssetFile } from "./minimo-format.js";
|
|
13
|
+
export { Design, parseAssetFile, parseTextureAtlas, frameKey } from "./minimo-format.js";
|
|
14
14
|
export { SoundPlayback } from "./internal/SoundSystem.js";
|
|
15
15
|
export type { PlaySoundOptions, SoundPlaybackState } from "./internal/SoundSystem.js";
|
|
16
|
-
export type { AssetFile, DesignBackground, DesignEmoji, DesignFile, DesignFileV1, DesignFileV2, DesignFileV3, DesignImage, DesignModule, DesignObject, DesignObjectBase, DesignObjectType, DesignOutline, DesignPage, DesignSpriteBase, DesignText, ImageAsset, SoundAsset, UnknownAsset, } from "./minimo-format.js";
|
|
16
|
+
export type { AssetEntry, AssetFile, AtlasAsset, DesignBackground, DesignEmoji, DesignFile, DesignFileV1, DesignFileV2, DesignFileV3, DesignImage, DesignModule, DesignObject, DesignObjectBase, DesignObjectType, DesignOutline, DesignPage, DesignSpriteBase, DesignText, ImageAsset, SoundAsset, SpritesheetAsset, TextureAtlas, TextureFrame, UnknownAsset, } from "./minimo-format.js";
|
|
17
17
|
/**
|
|
18
18
|
* Optional tuning values for {@link Game.animateExplode}.
|
|
19
19
|
*
|
|
@@ -1596,6 +1596,17 @@ export declare class Game {
|
|
|
1596
1596
|
* - An entry of a type this engine does not support is skipped, with a
|
|
1597
1597
|
* console warning naming it, so the game still loads every asset it can.
|
|
1598
1598
|
*
|
|
1599
|
+
* A `spritesheet` or `atlas` entry loads its image under its key, and its
|
|
1600
|
+
* atlas JSON with it, and once both are in makes each frame an image of its
|
|
1601
|
+
* own, keyed `<key>/<frame>`, such as `"frog/0"` or `"pond/lotus"`. A frame
|
|
1602
|
+
* key works wherever an image key does: {@link ImageSprite}, backgrounds,
|
|
1603
|
+
* designs, and {@link Game.getImage}. A trimmed atlas frame is restored to
|
|
1604
|
+
* its untrimmed size, so it measures and sits as the picture it came from.
|
|
1605
|
+
* A frame key cannot be used by any other image, sound, or texture. A sheet
|
|
1606
|
+
* whose frames cannot be made, such as an atlas that names a rectangle
|
|
1607
|
+
* outside its image, fails with its key, as a missing image does, and the
|
|
1608
|
+
* console says why. See {@link SpritesheetAsset} and {@link AtlasAsset}.
|
|
1609
|
+
*
|
|
1599
1610
|
* @param assets - The content of a `*.assets.json` file. See {@link AssetFile}.
|
|
1600
1611
|
*
|
|
1601
1612
|
* @example
|
package/dist/minimo.js
CHANGED
|
@@ -22,8 +22,8 @@ import { TextSystem } from "./internal/TextSystem.js";
|
|
|
22
22
|
import { TrailSystem } from "./internal/TrailSystem.js";
|
|
23
23
|
import { TransitionSystem } from "./internal/TransitionSystem.js";
|
|
24
24
|
import { TimerSystem } from "./internal/TimerSystem.js";
|
|
25
|
-
import { Design, designDecorations, designModules, enabledDecorations, findFirst, parseAssetFile, parseDesignObject, } from "./minimo-format.js";
|
|
26
|
-
export { Design, parseAssetFile } from "./minimo-format.js";
|
|
25
|
+
import { ASSET_TYPES, Design, designDecorations, designModules, enabledDecorations, findFirst, parseAssetFile, parseDesignObject, } from "./minimo-format.js";
|
|
26
|
+
export { Design, parseAssetFile, parseTextureAtlas, frameKey } from "./minimo-format.js";
|
|
27
27
|
export { SoundPlayback } from "./internal/SoundSystem.js";
|
|
28
28
|
/** How long a scene may load before it dims and its progress ring appears, so a fast load shows neither. */
|
|
29
29
|
const SCENE_LOADING_INDICATOR_DELAY_MS = 250;
|
|
@@ -1777,6 +1777,17 @@ export class Game {
|
|
|
1777
1777
|
* - An entry of a type this engine does not support is skipped, with a
|
|
1778
1778
|
* console warning naming it, so the game still loads every asset it can.
|
|
1779
1779
|
*
|
|
1780
|
+
* A `spritesheet` or `atlas` entry loads its image under its key, and its
|
|
1781
|
+
* atlas JSON with it, and once both are in makes each frame an image of its
|
|
1782
|
+
* own, keyed `<key>/<frame>`, such as `"frog/0"` or `"pond/lotus"`. A frame
|
|
1783
|
+
* key works wherever an image key does: {@link ImageSprite}, backgrounds,
|
|
1784
|
+
* designs, and {@link Game.getImage}. A trimmed atlas frame is restored to
|
|
1785
|
+
* its untrimmed size, so it measures and sits as the picture it came from.
|
|
1786
|
+
* A frame key cannot be used by any other image, sound, or texture. A sheet
|
|
1787
|
+
* whose frames cannot be made, such as an atlas that names a rectangle
|
|
1788
|
+
* outside its image, fails with its key, as a missing image does, and the
|
|
1789
|
+
* console says why. See {@link SpritesheetAsset} and {@link AtlasAsset}.
|
|
1790
|
+
*
|
|
1780
1791
|
* @param assets - The content of a `*.assets.json` file. See {@link AssetFile}.
|
|
1781
1792
|
*
|
|
1782
1793
|
* @example
|
|
@@ -1795,7 +1806,7 @@ export class Game {
|
|
|
1795
1806
|
if (!isFirstLoad)
|
|
1796
1807
|
return;
|
|
1797
1808
|
for (const entry of file.assets) {
|
|
1798
|
-
if (
|
|
1809
|
+
if (!ASSET_TYPES.includes(entry.type)) {
|
|
1799
1810
|
console.warn(`MinimoJS: Skipped the asset "${entry.key}", whose type "${entry.type}" this version of MinimoJS does not support.`);
|
|
1800
1811
|
}
|
|
1801
1812
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minimojs",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.43",
|
|
4
4
|
"description": "MinimoJS v1 — ultra-minimal, flat, deterministic 2D web game engine. Image-first sprites with emoji as a zero-asset alternative, rAF loop, TypeScript-first, LLM-friendly.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/minimo.js",
|