minimojs 1.0.0-alpha.28 → 1.0.0-alpha.29

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.
@@ -9,6 +9,16 @@ export class AssetSystem {
9
9
  this._imageSources = new Map();
10
10
  /** @internal */
11
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();
12
22
  }
13
23
  queueImage(key, src) {
14
24
  const safeKey = key.trim();
@@ -19,6 +29,42 @@ export class AssetSystem {
19
29
  if (safeSrc.length === 0) {
20
30
  throw new Error("MinimoJS: Image sources must be non-empty strings.");
21
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) {
22
68
  const existingSrc = this._imageSources.get(safeKey);
23
69
  if (existingSrc !== undefined && existingSrc !== safeSrc) {
24
70
  throw new Error(`MinimoJS: Image key "${safeKey}" is already registered with a different source.`);
@@ -34,6 +80,7 @@ export class AssetSystem {
34
80
  if (typeof painter !== "function") {
35
81
  throw new Error("MinimoJS: createTexture() requires a painter function.");
36
82
  }
83
+ this.refuseAssetFileKey(safeKey, "createTexture()");
37
84
  const canvas = document.createElement("canvas");
38
85
  canvas.width = Math.max(1, Math.round(Number.isFinite(width) ? width : 1));
39
86
  canvas.height = Math.max(1, Math.round(Number.isFinite(height) ? height : 1));
@@ -116,6 +163,12 @@ export class AssetSystem {
116
163
  this._queuedImages.clear();
117
164
  }
118
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 */
119
172
  loadImage(src) {
120
173
  return new Promise((resolve, reject) => {
121
174
  const image = new Image();
@@ -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,407 @@
1
+ /**
2
+ * @module minimojs/format
3
+ *
4
+ * The file formats MinimoJS reads: asset files, `*.assets.json`, and design
5
+ * files, `*.design.json`.
6
+ *
7
+ * This module uses no DOM, so tools such as an editor or a validating server
8
+ * import the same code a game runs: `import { parseAssetFile, Design } from
9
+ * "minimojs/format"`. Games reach it through `minimo.js`, which re-exports it.
10
+ */
11
+ /**
12
+ * An image declared in an asset file, loaded as
13
+ * `game.loadImage(key, src)` would load it.
14
+ *
15
+ * @example
16
+ * ```json
17
+ * { "key": "player", "type": "image", "src": "assets/player.png" }
18
+ * ```
19
+ */
20
+ export interface ImageAsset {
21
+ /**
22
+ * The key sprites and backgrounds use for this image, as in
23
+ * `new ImageSprite("player")`. Non-empty, with no leading or trailing
24
+ * spaces, and unique in the whole game: no other entry of any asset file,
25
+ * of any type, and no `loadImage` or `createTexture` call may use it.
26
+ */
27
+ key: string;
28
+ /** Always `"image"`. */
29
+ type: "image";
30
+ /** The image's URL or path, as `game.loadImage(key, src)` takes it. Non-empty. */
31
+ src: string;
32
+ }
33
+ /**
34
+ * An entry whose `type` this engine does not support, such as one written for
35
+ * a newer engine. It is kept as written, so a tool that saves the file does
36
+ * not drop it, and `game.loadAssets` warns about it and skips it. Its key
37
+ * still counts toward the game's unique keys.
38
+ */
39
+ export interface UnknownAsset {
40
+ /** The entry's key, unique in the whole game like every other. */
41
+ key: string;
42
+ /** A type this engine does not support. */
43
+ type: string;
44
+ /** The fields of that type, kept unchecked. */
45
+ [field: string]: unknown;
46
+ }
47
+ /**
48
+ * The content of an asset file, `*.assets.json`, which declares assets once,
49
+ * by key, for {@link parseAssetFile} and `game.loadAssets`.
50
+ *
51
+ * A game may have any number of asset files, such as `common.assets.json`
52
+ * and `level3.assets.json`.
53
+ *
54
+ * @example
55
+ * ```json
56
+ * {
57
+ * "format": "minimo-assets",
58
+ * "version": 1,
59
+ * "assets": [
60
+ * { "key": "player", "type": "image", "src": "assets/player.png" },
61
+ * { "key": "bat", "type": "image", "src": "assets/bat.png" }
62
+ * ]
63
+ * }
64
+ * ```
65
+ */
66
+ export interface AssetFile {
67
+ /** Always `"minimo-assets"`. */
68
+ format: "minimo-assets";
69
+ /** The version of the file's shape. Version `1` is the only one so far. */
70
+ version: 1;
71
+ /** The declared assets, in file order. */
72
+ assets: (ImageAsset | UnknownAsset)[];
73
+ }
74
+ /**
75
+ * Validates the content of an asset file and returns a copy of it.
76
+ *
77
+ * Parsing is strict. It throws an error naming what is wrong for:
78
+ * - a value that is not an asset file object, or an unknown field anywhere;
79
+ * - a `format` other than `"minimo-assets"`;
80
+ * - a `version` newer than this engine reads, or one that is not a positive integer;
81
+ * - an entry without a string `key` and `type`;
82
+ * - an empty key, or one with leading or trailing spaces;
83
+ * - a key repeated in the file, whatever the types of its entries;
84
+ * - an `image` entry without a non-empty string `src`.
85
+ *
86
+ * An entry of a type this engine does not support is kept unchanged, without
87
+ * checking its other fields, so nothing is lost when a tool saves the file.
88
+ *
89
+ * @param json - The parsed JSON of a `*.assets.json` file.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * import common from "./common.assets.json" with { type: "json" };
94
+ * const file = parseAssetFile(common);
95
+ * ```
96
+ */
97
+ export declare function parseAssetFile(json: unknown): AssetFile;
98
+ /**
99
+ * The built-in type of a design object. Each maps to one runtime class:
100
+ * `image` to `ImageSprite`, `emoji` to `EmojiSprite`, `text` to `TextSprite`,
101
+ * and `background` to `BackgroundLayer`.
102
+ */
103
+ export type DesignObjectType = "image" | "emoji" | "text" | "background";
104
+ /**
105
+ * The properties every design object has, whatever its type.
106
+ *
107
+ * Every property of a design object is named as its runtime class names it,
108
+ * so the same names work in code and in files. A property the file omits
109
+ * keeps the runtime object's default.
110
+ */
111
+ export interface DesignObjectBase {
112
+ /**
113
+ * Required. A UUID unique in the file, such as
114
+ * `"6f1c2a0e-3b7d-4c1e-9a52-0d8e4f6b1a27"`. Tools use it to select, undo,
115
+ * and compare objects, and it survives renames. It is the only property
116
+ * with no runtime counterpart.
117
+ */
118
+ id: string;
119
+ /** Optional name, which may repeat. Becomes `Sprite.name`. */
120
+ name?: string;
121
+ /** Tags for grouping and for builders. Becomes `Sprite.tags`. Default: `[]`. */
122
+ tags?: string[];
123
+ /**
124
+ * The game's own values, such as a button's action or an enemy's speed.
125
+ * Always a JSON object, never a bare number or string. Becomes a copy in
126
+ * `Sprite.data`. Default: `{}`.
127
+ */
128
+ data?: Record<string, unknown>;
129
+ }
130
+ /** The properties of every sprite type: `image`, `emoji`, and `text`. */
131
+ export interface DesignSpriteBase extends DesignObjectBase {
132
+ /** Horizontal center in world space, in pixels. Default: `0`. */
133
+ x?: number;
134
+ /** Vertical center in world space, in pixels. Positive is down. Default: `0`. */
135
+ y?: number;
136
+ /** Uniform visual scale. Default: `1`. */
137
+ scale?: number;
138
+ /** Rotation in degrees, clockwise. Default: `0`. */
139
+ rotation?: number;
140
+ /** Mirrors the sprite left to right. Default: `false`. */
141
+ flipX?: boolean;
142
+ /** Mirrors the sprite top to bottom. Default: `false`. */
143
+ flipY?: boolean;
144
+ /** Opacity, from `0` to `1`. Default: `1`. */
145
+ alpha?: number;
146
+ /** Whether the sprite is drawn. Default: `true`. */
147
+ visible?: boolean;
148
+ /** Render order; higher layers draw on top. Default: `0`. */
149
+ layer?: number;
150
+ /** CSS color, used by monochrome glyphs and by text. Default: `"#000000"`, and `"#ffffff"` for `text`. */
151
+ color?: string;
152
+ /** Draws the sprite in screen space, unaffected by scrolling. Default: `false`. */
153
+ ignoreScroll?: boolean;
154
+ /** Whether the engine leaves the sprite where it is, as a static obstacle. Default: `false`. */
155
+ isStatic?: boolean;
156
+ /** Horizontal velocity in pixels per second. Default: `0`. */
157
+ vx?: number;
158
+ /** Vertical velocity in pixels per second. Default: `0`. */
159
+ vy?: number;
160
+ /** Gravity multiplier. Default: `0`, immune to gravity. */
161
+ gravityScale?: number;
162
+ /** Width of the collision body in pixels, before scale; `null` uses the sprite's width. Default: `null`. */
163
+ bodyWidth?: number | null;
164
+ /** Height of the collision body in pixels, before scale; `null` uses the sprite's height. Default: `null`. */
165
+ bodyHeight?: number | null;
166
+ /** Horizontal offset of the body's center from the sprite's center, in pixels, before scale. Default: `0`. */
167
+ bodyOffsetX?: number;
168
+ /** Vertical offset of the body's center from the sprite's center, in pixels, before scale. Default: `0`. */
169
+ bodyOffsetY?: number;
170
+ }
171
+ /**
172
+ * An `ImageSprite` in a design file.
173
+ *
174
+ * @example
175
+ * ```json
176
+ * { "id": "a93e51b4-6c0d-4f2a-8b17-3e5d9c2f0a61", "name": "play", "type": "image", "imageKey": "btn-play", "x": 360, "y": 700 }
177
+ * ```
178
+ */
179
+ export interface DesignImage extends DesignSpriteBase {
180
+ /** Always `"image"`. */
181
+ type: "image";
182
+ /** Required. The key of an image declared in one of the game's asset files. */
183
+ imageKey: string;
184
+ }
185
+ /**
186
+ * An `EmojiSprite` in a design file.
187
+ *
188
+ * @example
189
+ * ```json
190
+ * { "id": "0b9a7c3e-2d4f-4e61-a8c5-7f1e3d2b6a90", "type": "emoji", "sprite": "⭐", "size": 64, "x": 200, "y": 300 }
191
+ * ```
192
+ */
193
+ export interface DesignEmoji extends DesignSpriteBase {
194
+ /** Always `"emoji"`. */
195
+ type: "emoji";
196
+ /** Required. The emoji to draw. */
197
+ sprite: string;
198
+ /** Base size in pixels. Default: `32`. */
199
+ size?: number;
200
+ }
201
+ /**
202
+ * A `TextSprite` in a design file.
203
+ *
204
+ * @example
205
+ * ```json
206
+ * { "id": "5c2e8f1a-9b3d-4a7e-b6c0-1d4f8e2a7c35", "type": "text", "text": "GAME OVER", "fontSize": 64, "x": 360, "y": 400 }
207
+ * ```
208
+ */
209
+ export interface DesignText extends DesignSpriteBase {
210
+ /** Always `"text"`. */
211
+ type: "text";
212
+ /** Required. The text to draw. */
213
+ text: string;
214
+ /** Font size in pixels. Default: `16`. */
215
+ fontSize?: number;
216
+ /** CSS font family. Default: `"monospace"`. */
217
+ fontFamily?: string;
218
+ /** CSS font weight. Default: `"400"`. */
219
+ fontWeight?: string;
220
+ /** Line height as a multiple of the font size. Default: `1.2`. */
221
+ lineHeight?: number;
222
+ /** Which edge of the text `x` names. Default: `"center"`. */
223
+ anchorX?: "left" | "center" | "right";
224
+ /** Which edge of the text `y` names. Default: `"middle"`. */
225
+ anchorY?: "top" | "middle" | "bottom";
226
+ /** Alignment of the lines. Default: `"center"`. */
227
+ textAlign?: "left" | "right" | "center" | "start" | "end";
228
+ /** Width at which lines wrap, in pixels; `0` never wraps. Default: `0`. */
229
+ maxWidth?: number;
230
+ /** Fixed box width in pixels; `0` fits the text. Default: `0`. */
231
+ fixedWidth?: number;
232
+ /** Fixed box height in pixels; `0` fits the text. Default: `0`. */
233
+ fixedHeight?: number;
234
+ /** Horizontal padding inside the box, in pixels. Default: `2`. */
235
+ paddingX?: number;
236
+ /** Vertical padding inside the box, in pixels. Default: `2`. */
237
+ paddingY?: number;
238
+ /** CSS color of the box; `null` draws none. Default: `null`. */
239
+ backgroundColor?: string | null;
240
+ /** CSS color of the box's border; `null` draws none. Default: `null`. */
241
+ borderColor?: string | null;
242
+ /** Width of the box's border in pixels. Default: `0`. */
243
+ borderWidth?: number;
244
+ /** Corner radius of the box in pixels. Default: `0`. */
245
+ cornerRadius?: number;
246
+ /** CSS color of the text's outline; `null` draws none. Default: `null`. */
247
+ strokeColor?: string | null;
248
+ /** Width of the text's outline in pixels. Default: `0`. */
249
+ strokeWidth?: number;
250
+ }
251
+ /**
252
+ * A `BackgroundLayer` in a design file. Unlike sprites, its `x` and `y` are
253
+ * the top-left corner in screen space.
254
+ *
255
+ * @example
256
+ * ```json
257
+ * { "id": "6f1c2a0e-3b7d-4c1e-9a52-0d8e4f6b1a27", "type": "background", "imageKey": "sky" }
258
+ * ```
259
+ */
260
+ export interface DesignBackground extends DesignObjectBase {
261
+ /** Always `"background"`. */
262
+ type: "background";
263
+ /** Required. The key of an image declared in one of the game's asset files. */
264
+ imageKey: string;
265
+ /** Left edge in screen space, in pixels. Default: `0`. */
266
+ x?: number;
267
+ /** Top edge in screen space, in pixels. Default: `0`. */
268
+ y?: number;
269
+ /** Width in pixels; `0` uses the canvas width. Default: `0`. */
270
+ width?: number;
271
+ /** Height in pixels; `0` uses the canvas height. Default: `0`. */
272
+ height?: number;
273
+ /** Opacity, from `0` to `1`. Default: `1`. */
274
+ alpha?: number;
275
+ /** Whether the layer is drawn. Default: `true`. */
276
+ visible?: boolean;
277
+ /** Order among background layers; all of them draw behind sprites. Default: `0`. */
278
+ layer?: number;
279
+ /** How the image fits its rectangle. Default: `"cover"`. */
280
+ fit?: "none" | "stretch" | "contain" | "cover";
281
+ }
282
+ /** Any object of a design file. Its `type` says which. */
283
+ export type DesignObject = DesignImage | DesignEmoji | DesignText | DesignBackground;
284
+ /**
285
+ * The content of a design file, `*.design.json`: a flat list of objects that
286
+ * a game creates with `game.createObject` and `game.createObjectList`, read
287
+ * through {@link Design}.
288
+ *
289
+ * @example
290
+ * ```json
291
+ * {
292
+ * "format": "minimo-design",
293
+ * "version": 1,
294
+ * "objects": [
295
+ * { "id": "6f1c2a0e-3b7d-4c1e-9a52-0d8e4f6b1a27", "type": "background", "imageKey": "sky" },
296
+ * { "id": "a93e51b4-6c0d-4f2a-8b17-3e5d9c2f0a61", "name": "play", "tags": ["button"], "type": "image", "imageKey": "btn-play", "x": 360, "y": 700, "data": { "action": "start" } }
297
+ * ]
298
+ * }
299
+ * ```
300
+ */
301
+ export interface DesignFile {
302
+ /** Always `"minimo-design"`. */
303
+ format: "minimo-design";
304
+ /** The version of the file's shape. Version `1` is the only one so far. */
305
+ version: 1;
306
+ /** The objects, in file order, which is the order they are created and drawn in. */
307
+ objects: DesignObject[];
308
+ }
309
+ /**
310
+ * How one property of a design object is written, for tools that read or edit
311
+ * designs without the TypeScript types, such as a property panel. See
312
+ * {@link designObjectProperties}.
313
+ */
314
+ export interface DesignProperty {
315
+ /** The property's name, in the file and on the runtime object. */
316
+ readonly name: string;
317
+ /**
318
+ * The JSON type of its value. `"string-list"` is a list of strings, as
319
+ * `tags`; `"object"` is a JSON object, as `data`.
320
+ */
321
+ readonly type: "number" | "string" | "boolean" | "string-list" | "object";
322
+ /** Whether `null` is accepted besides values of its type, as for `bodyWidth`. */
323
+ readonly nullable: boolean;
324
+ /** The only strings accepted, as for `fit`; `null` when any value of its type is. */
325
+ readonly values: readonly string[] | null;
326
+ /** Whether every object of its type must have it. A required property has no default. */
327
+ readonly required: boolean;
328
+ /**
329
+ * The runtime object's value when the file omits the property.
330
+ * `undefined` for a required property and for `name`.
331
+ */
332
+ readonly default: unknown;
333
+ }
334
+ /**
335
+ * Every property each type of design object accepts besides `id` and `type`,
336
+ * with its JSON type, whether it is required, and its default.
337
+ *
338
+ * It is the runtime form of {@link DesignImage}, {@link DesignEmoji},
339
+ * {@link DesignText}, and {@link DesignBackground}, which the compiler checks
340
+ * it against, for tools that cannot read TypeScript types, such as an
341
+ * editor's property panel. `new Design(json)` validates objects with it.
342
+ *
343
+ * @example
344
+ * ```ts
345
+ * for (const property of designObjectProperties.image) {
346
+ * console.log(property.name, property.type, property.default);
347
+ * }
348
+ * ```
349
+ */
350
+ export declare const designObjectProperties: Readonly<Record<DesignObjectType, readonly DesignProperty[]>>;
351
+ /**
352
+ * The data of a design file, `*.design.json`, validated: its objects and
353
+ * searches on them. A `Design` needs no game and creates nothing; pass its
354
+ * objects, or the results of its searches, to `game.createObject` or
355
+ * `game.createObjectList`. Data alone also serves markers, such as spawn
356
+ * points, which code reads without creating anything.
357
+ *
358
+ * Obtain the file by JSON import. One `Design` can be created any number of
359
+ * times.
360
+ *
361
+ * @example
362
+ * ```ts
363
+ * import menuJson from "./menu.design.json" with { type: "json" };
364
+ *
365
+ * const menu = new Design(menuJson);
366
+ *
367
+ * class MenuScene {
368
+ * onCreate() {
369
+ * const ui = game.createObjectList(menu.objects);
370
+ * this.play = ui.findByName("play");
371
+ * }
372
+ * }
373
+ * ```
374
+ */
375
+ export declare class Design {
376
+ /**
377
+ * Every object of the design, as object data, in file order.
378
+ */
379
+ readonly objects: readonly DesignObject[];
380
+ /**
381
+ * Validates a design file's content and keeps a copy of it.
382
+ *
383
+ * Parsing is strict. It throws an error naming what is wrong for a value
384
+ * that is not a design, an unknown field, a `format` other than
385
+ * `"minimo-design"`, a `version` newer than this engine reads, an object
386
+ * whose `type` is not one of the built-in types, an `id` that is not a UUID
387
+ * or is used twice, a value of the wrong type, a missing required property,
388
+ * and a `data` that is not a JSON object.
389
+ *
390
+ * @param json - The content of a `*.design.json` file. See {@link DesignFile}.
391
+ */
392
+ constructor(json: unknown);
393
+ /**
394
+ * Returns the first object whose `name` is `name`, in file order.
395
+ * Throws an error naming `name` when there is none.
396
+ */
397
+ findByName(name: string): DesignObject;
398
+ /** Returns every object whose `name` is `name`, in file order; an empty list when there is none. */
399
+ findAllByName(name: string): DesignObject[];
400
+ /**
401
+ * Returns the first object whose `tags` include `tag`, in file order.
402
+ * Throws an error naming `tag` when there is none.
403
+ */
404
+ findByTag(tag: string): DesignObject;
405
+ /** Returns every object whose `tags` include `tag`, in file order; an empty list when there is none. */
406
+ findAllByTag(tag: string): DesignObject[];
407
+ }