minimojs 1.0.0-alpha.39 → 1.0.0-alpha.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/minimo-dice.d.ts +26 -1
- package/dist/minimo-dice.js +56 -2
- package/dist/minimo-fire.d.ts +40 -0
- package/dist/minimo-fire.js +106 -7
- package/dist/minimo-format.d.ts +132 -15
- package/dist/minimo-format.js +139 -13
- package/dist/minimo.d.ts +8 -1
- package/dist/minimo.js +59 -1
- package/package.json +1 -1
package/dist/minimo-dice.d.ts
CHANGED
|
@@ -93,6 +93,28 @@ export interface DieRollOptions {
|
|
|
93
93
|
* const value = 1 + Math.floor(game.random() * 20);
|
|
94
94
|
* await d20.roll(value, { spin: { x: 540, y: -360, z: 90 }, ms: 1200 });
|
|
95
95
|
* ```
|
|
96
|
+
*
|
|
97
|
+
* ## In a design
|
|
98
|
+
*
|
|
99
|
+
* A design can place dice, so the creator moves them in the design editor. A
|
|
100
|
+
* die is a `module` object of a version 3 design whose `kind` is `"d4"`,
|
|
101
|
+
* `"d6"`, `"d8"`, `"d10"`, `"d12"`, or `"d20"`, and a design sets only its
|
|
102
|
+
* `x` and `y`. Its look and size are the set's, as for {@link Dice.create}
|
|
103
|
+
* without options, and it shows its 1 until the game rolls it.
|
|
104
|
+
*
|
|
105
|
+
* ```json
|
|
106
|
+
* { "id": "9a2c4e6f-1b3d-4f5a-8c7e-0d2f4b6a8c1e", "name": "d20", "type": "module", "module": "dice", "kind": "d20", "x": 360, "y": 640 }
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* Create the set in the scene's `onCreate()` before the design's objects,
|
|
110
|
+
* then find the die by its name:
|
|
111
|
+
*
|
|
112
|
+
* ```ts
|
|
113
|
+
* onCreate() {
|
|
114
|
+
* this.dice = new Dice(game, { color: "#f4f1ea" });
|
|
115
|
+
* this.d20 = game.createObjectList(board.objects).findByName("d20");
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
96
118
|
*/
|
|
97
119
|
export declare class Dice {
|
|
98
120
|
/** The game the dice are added to. */
|
|
@@ -111,7 +133,10 @@ export declare class Dice {
|
|
|
111
133
|
create(sides: DieSides, options?: DieOptions): Die;
|
|
112
134
|
/** Removes a die from the game and from this set. */
|
|
113
135
|
remove(die: Die): void;
|
|
114
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* Advances every die of the set. Call it once per frame from `onUpdate`.
|
|
138
|
+
* A die the game destroyed, such as by `game.reset()`, leaves the set.
|
|
139
|
+
*/
|
|
115
140
|
update(dt: number): void;
|
|
116
141
|
}
|
|
117
142
|
/**
|
package/dist/minimo-dice.js
CHANGED
|
@@ -31,6 +31,28 @@ const HIGHLIGHT = normalize(add(LIGHT, [0, 0, 1]));
|
|
|
31
31
|
* const value = 1 + Math.floor(game.random() * 20);
|
|
32
32
|
* await d20.roll(value, { spin: { x: 540, y: -360, z: 90 }, ms: 1200 });
|
|
33
33
|
* ```
|
|
34
|
+
*
|
|
35
|
+
* ## In a design
|
|
36
|
+
*
|
|
37
|
+
* A design can place dice, so the creator moves them in the design editor. A
|
|
38
|
+
* die is a `module` object of a version 3 design whose `kind` is `"d4"`,
|
|
39
|
+
* `"d6"`, `"d8"`, `"d10"`, `"d12"`, or `"d20"`, and a design sets only its
|
|
40
|
+
* `x` and `y`. Its look and size are the set's, as for {@link Dice.create}
|
|
41
|
+
* without options, and it shows its 1 until the game rolls it.
|
|
42
|
+
*
|
|
43
|
+
* ```json
|
|
44
|
+
* { "id": "9a2c4e6f-1b3d-4f5a-8c7e-0d2f4b6a8c1e", "name": "d20", "type": "module", "module": "dice", "kind": "d20", "x": 360, "y": 640 }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* Create the set in the scene's `onCreate()` before the design's objects,
|
|
48
|
+
* then find the die by its name:
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* onCreate() {
|
|
52
|
+
* this.dice = new Dice(game, { color: "#f4f1ea" });
|
|
53
|
+
* this.d20 = game.createObjectList(board.objects).findByName("d20");
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
34
56
|
*/
|
|
35
57
|
export class Dice {
|
|
36
58
|
constructor(game, look = {}) {
|
|
@@ -38,6 +60,7 @@ export class Dice {
|
|
|
38
60
|
this._dice = [];
|
|
39
61
|
this.game = game;
|
|
40
62
|
this.look = { ...look };
|
|
63
|
+
game._registerDesignModule("dice", (obj) => this._fromDesign(obj));
|
|
41
64
|
}
|
|
42
65
|
/** The dice of this set, in the order they were made. */
|
|
43
66
|
get dice() {
|
|
@@ -62,10 +85,34 @@ export class Dice {
|
|
|
62
85
|
this._dice.splice(index, 1);
|
|
63
86
|
this.game.destroySprite(die);
|
|
64
87
|
}
|
|
65
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Builds a die placed in a design, with the set's look. The game adds it.
|
|
90
|
+
*
|
|
91
|
+
* @internal
|
|
92
|
+
*/
|
|
93
|
+
_fromDesign(obj) {
|
|
94
|
+
const die = new Die(Number(obj.kind.slice(1)), { ...this.look });
|
|
95
|
+
die._pending = true;
|
|
96
|
+
this._dice.push(die);
|
|
97
|
+
return die;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Advances every die of the set. Call it once per frame from `onUpdate`.
|
|
101
|
+
* A die the game destroyed, such as by `game.reset()`, leaves the set.
|
|
102
|
+
*/
|
|
66
103
|
update(dt) {
|
|
67
|
-
|
|
104
|
+
const alive = new Set(this.game.getSprites());
|
|
105
|
+
for (const die of [...this._dice]) {
|
|
106
|
+
if (!alive.has(die)) {
|
|
107
|
+
// A die a design built waits to be added, as with createObject's
|
|
108
|
+
// `add: false`; one that was in the game and left it is dropped.
|
|
109
|
+
if (!die._pending)
|
|
110
|
+
this._dice.splice(this._dice.indexOf(die), 1);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
die._pending = false;
|
|
68
114
|
die.update(dt);
|
|
115
|
+
}
|
|
69
116
|
}
|
|
70
117
|
}
|
|
71
118
|
/**
|
|
@@ -99,6 +146,8 @@ export class Die extends DrawSprite {
|
|
|
99
146
|
this._landing = null;
|
|
100
147
|
/** @internal */
|
|
101
148
|
this._waiting = [];
|
|
149
|
+
/** @internal Built from a design and not yet seen in the game. */
|
|
150
|
+
this._pending = false;
|
|
102
151
|
this.sides = sides;
|
|
103
152
|
this.size = size;
|
|
104
153
|
this.bodyWidth = size;
|
|
@@ -109,6 +158,11 @@ export class Die extends DrawSprite {
|
|
|
109
158
|
this._look = look;
|
|
110
159
|
this._orientation = restOrientation(solid, this._checkValue(options.value ?? 1));
|
|
111
160
|
}
|
|
161
|
+
/** @internal A design editor selects the die itself, not the room its surface keeps for turning. */
|
|
162
|
+
_editorBox() {
|
|
163
|
+
const scale = Number.isFinite(this.scale) ? Math.max(0, this.scale) : 1;
|
|
164
|
+
return { cx: this.renderX, cy: this.renderY, w: this.size * scale, h: this.size * scale };
|
|
165
|
+
}
|
|
112
166
|
/** The look of this die. Change it with {@link Die.setLook}. */
|
|
113
167
|
get look() {
|
|
114
168
|
return this._look;
|
package/dist/minimo-fire.d.ts
CHANGED
|
@@ -131,6 +131,39 @@ type InternalKind = FlameKind | "burst" | "explosion";
|
|
|
131
131
|
* fire.attach(fireball, { kind: "trail", palette: "blue" });
|
|
132
132
|
* torch.extinguish(600);
|
|
133
133
|
* ```
|
|
134
|
+
*
|
|
135
|
+
* ## In a design
|
|
136
|
+
*
|
|
137
|
+
* A design can place `torch`, `campfire`, `burning`, and `trail` flames, so
|
|
138
|
+
* the creator moves and resizes them in the design editor. Each is a
|
|
139
|
+
* `module` object of a version 3 design. It takes `x`, `y`, `alpha`,
|
|
140
|
+
* `visible`, `layer`, `ignoreScroll`, and the collision body's properties,
|
|
141
|
+
* never `scale` or `rotation`: its `size` option resizes it. Its `options`
|
|
142
|
+
* are `size`, `intensity`, `palette` (a palette's name), `style`, `smoke`,
|
|
143
|
+
* `sparks`, `smokeColor`, and `light`, each as in {@link FlameOptions}.
|
|
144
|
+
*
|
|
145
|
+
* ```json
|
|
146
|
+
* { "id": "3e7b1c9a-5d2f-4a8e-b0c6-9f1d2e4a7b35", "name": "left-torch", "type": "module", "module": "fire", "kind": "torch", "x": 120, "y": 200, "layer": 2, "options": { "size": 28, "palette": "blue" } }
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* A placed flame looks as its options say and the module's defaults, never
|
|
150
|
+
* as the look given to `new Fire(game, …)`, so the game shows what the editor
|
|
151
|
+
* showed, and it is already burning when its screen opens. `burst` and
|
|
152
|
+
* `explosion` are moments, made in code. Create the fire in the scene's
|
|
153
|
+
* `onCreate()` before the design's objects, then find a flame by its name
|
|
154
|
+
* to change it while the game plays, and advance the fire every frame:
|
|
155
|
+
*
|
|
156
|
+
* ```ts
|
|
157
|
+
* onCreate() {
|
|
158
|
+
* this.fire = new Fire(game);
|
|
159
|
+
* const room = game.createObjectList(dungeon.objects);
|
|
160
|
+
* this.torch = room.findByName("left-torch"); // later: this.torch.extinguish()
|
|
161
|
+
* room.findByName("barrel-fire").follow(room.findByName("barrel"));
|
|
162
|
+
* }
|
|
163
|
+
* onUpdate(dt) {
|
|
164
|
+
* this.fire.update(dt);
|
|
165
|
+
* }
|
|
166
|
+
* ```
|
|
134
167
|
*/
|
|
135
168
|
export declare class Fire {
|
|
136
169
|
/** The game the flames are added to. */
|
|
@@ -225,6 +258,13 @@ export declare class Flame extends DrawSprite {
|
|
|
225
258
|
get out(): boolean;
|
|
226
259
|
/** The sprite this flame follows, or `null`. */
|
|
227
260
|
get target(): Sprite | null;
|
|
261
|
+
/**
|
|
262
|
+
* Makes the flame follow `sprite` every frame, keeping the offset it has
|
|
263
|
+
* from it now, as a flame placed over a sprite in a design keeps where the
|
|
264
|
+
* creator put it. It hides with the sprite and goes out when the sprite is
|
|
265
|
+
* destroyed. `null` stops following and leaves the flame where it is.
|
|
266
|
+
*/
|
|
267
|
+
follow(sprite: Sprite | null): void;
|
|
228
268
|
/**
|
|
229
269
|
* Changes the intensity over `ms` milliseconds.
|
|
230
270
|
* @returns A promise that resolves when it gets there, or when another change replaces it.
|
package/dist/minimo-fire.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { DrawSprite } from "./minimo.js";
|
|
2
2
|
const KINDS = {
|
|
3
|
-
torch: { spread: 0.16, spreadY: 0.04, rise: 4.2, life: 0.62, radius: 0.42, rate: 46, smoke: 0.15, sparks: 0.25, light: true, width: 3.6, up: 3.8, down: 1.6, bodyWidth: 0.6, bodyHeight: 1.1, bodyRise: 0.5 },
|
|
4
|
-
campfire: { spread: 0.4, spreadY: 0.05, rise: 3.4, life: 0.72, radius: 0.34, rate: 80, smoke: 0.4, sparks: 0.5, light: true, width: 3.8, up: 3.8, down: 1.5, bodyWidth: 0.85, bodyHeight: 1, bodyRise: 0.45 },
|
|
5
|
-
burning: { spread: 0.45, spreadY: 0.35, rise: 2.1, life: 0.5, radius: 0.24, rate: 90, smoke: 0.3, sparks: 0.25, light: false, width: 2.4, up: 2.6, down: 1, bodyWidth: 0.9, bodyHeight: 0.9, bodyRise: 0.1 },
|
|
6
|
-
trail: { spread: 0.12, spreadY: 0.12, rise: 0.35, life: 0.45, radius: 0.55, rate: 160, smoke: 0.35, sparks: 0.3, light: false, width: 14, up: 7, down: 7, bodyWidth: 0.8, bodyHeight: 0.8, bodyRise: 0 },
|
|
7
|
-
burst: { spread: 0.42, spreadY: 0.06, rise: 3.2, life: 0.7, radius: 0.34, rate: 80, smoke: 0.5, sparks: 0.5, light: true, width: 4, up: 4, down: 1.8, bodyWidth: 0.85, bodyHeight: 1, bodyRise: 0.45 },
|
|
8
|
-
explosion: { spread: 0, spreadY: 0, rise: 0, life: 0.5, radius: 0.34, rate: 0, smoke: 0.7, sparks: 0.7, light: true, width: 3.2, up: 2.8, down: 1.7, bodyWidth: 0.8, bodyHeight: 0.8, bodyRise: 0 },
|
|
3
|
+
torch: { spread: 0.16, spreadY: 0.04, rise: 4.2, life: 0.62, radius: 0.42, rate: 46, smoke: 0.15, sparks: 0.25, light: true, width: 3.6, up: 3.8, down: 1.6, bodyWidth: 0.6, bodyHeight: 1.1, bodyRise: 0.5, boxWidth: 0.7, boxHeight: 1.9, boxRise: 0.8 },
|
|
4
|
+
campfire: { spread: 0.4, spreadY: 0.05, rise: 3.4, life: 0.72, radius: 0.34, rate: 80, smoke: 0.4, sparks: 0.5, light: true, width: 3.8, up: 3.8, down: 1.5, bodyWidth: 0.85, bodyHeight: 1, bodyRise: 0.45, boxWidth: 1.2, boxHeight: 1.8, boxRise: 0.75 },
|
|
5
|
+
burning: { spread: 0.45, spreadY: 0.35, rise: 2.1, life: 0.5, radius: 0.24, rate: 90, smoke: 0.3, sparks: 0.25, light: false, width: 2.4, up: 2.6, down: 1, bodyWidth: 0.9, bodyHeight: 0.9, bodyRise: 0.1, boxWidth: 1.3, boxHeight: 1.6, boxRise: 0.5 },
|
|
6
|
+
trail: { spread: 0.12, spreadY: 0.12, rise: 0.35, life: 0.45, radius: 0.55, rate: 160, smoke: 0.35, sparks: 0.3, light: false, width: 14, up: 7, down: 7, bodyWidth: 0.8, bodyHeight: 0.8, bodyRise: 0, boxWidth: 1.2, boxHeight: 1.2, boxRise: 0 },
|
|
7
|
+
burst: { spread: 0.42, spreadY: 0.06, rise: 3.2, life: 0.7, radius: 0.34, rate: 80, smoke: 0.5, sparks: 0.5, light: true, width: 4, up: 4, down: 1.8, bodyWidth: 0.85, bodyHeight: 1, bodyRise: 0.45, boxWidth: 1.2, boxHeight: 1.8, boxRise: 0.75 },
|
|
8
|
+
explosion: { spread: 0, spreadY: 0, rise: 0, life: 0.5, radius: 0.34, rate: 0, smoke: 0.7, sparks: 0.7, light: true, width: 3.2, up: 2.8, down: 1.7, bodyWidth: 0.8, bodyHeight: 0.8, bodyRise: 0, boxWidth: 2, boxHeight: 2, boxRise: 0 },
|
|
9
9
|
};
|
|
10
|
+
/** How long of burning a flame placed in a design has behind it when it first appears, in seconds. */
|
|
11
|
+
const PREWARM_SECONDS = 1.5;
|
|
12
|
+
const PREWARM_STEP = 1 / 30;
|
|
10
13
|
const PALETTES = {
|
|
11
14
|
fire: ["#fff4b8", "#ffab2e", "#e8401c"],
|
|
12
15
|
blue: ["#eef9ff", "#5cc8ff", "#2c4fff"],
|
|
@@ -39,6 +42,39 @@ const SPARK = 2;
|
|
|
39
42
|
* fire.attach(fireball, { kind: "trail", palette: "blue" });
|
|
40
43
|
* torch.extinguish(600);
|
|
41
44
|
* ```
|
|
45
|
+
*
|
|
46
|
+
* ## In a design
|
|
47
|
+
*
|
|
48
|
+
* A design can place `torch`, `campfire`, `burning`, and `trail` flames, so
|
|
49
|
+
* the creator moves and resizes them in the design editor. Each is a
|
|
50
|
+
* `module` object of a version 3 design. It takes `x`, `y`, `alpha`,
|
|
51
|
+
* `visible`, `layer`, `ignoreScroll`, and the collision body's properties,
|
|
52
|
+
* never `scale` or `rotation`: its `size` option resizes it. Its `options`
|
|
53
|
+
* are `size`, `intensity`, `palette` (a palette's name), `style`, `smoke`,
|
|
54
|
+
* `sparks`, `smokeColor`, and `light`, each as in {@link FlameOptions}.
|
|
55
|
+
*
|
|
56
|
+
* ```json
|
|
57
|
+
* { "id": "3e7b1c9a-5d2f-4a8e-b0c6-9f1d2e4a7b35", "name": "left-torch", "type": "module", "module": "fire", "kind": "torch", "x": 120, "y": 200, "layer": 2, "options": { "size": 28, "palette": "blue" } }
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* A placed flame looks as its options say and the module's defaults, never
|
|
61
|
+
* as the look given to `new Fire(game, …)`, so the game shows what the editor
|
|
62
|
+
* showed, and it is already burning when its screen opens. `burst` and
|
|
63
|
+
* `explosion` are moments, made in code. Create the fire in the scene's
|
|
64
|
+
* `onCreate()` before the design's objects, then find a flame by its name
|
|
65
|
+
* to change it while the game plays, and advance the fire every frame:
|
|
66
|
+
*
|
|
67
|
+
* ```ts
|
|
68
|
+
* onCreate() {
|
|
69
|
+
* this.fire = new Fire(game);
|
|
70
|
+
* const room = game.createObjectList(dungeon.objects);
|
|
71
|
+
* this.torch = room.findByName("left-torch"); // later: this.torch.extinguish()
|
|
72
|
+
* room.findByName("barrel-fire").follow(room.findByName("barrel"));
|
|
73
|
+
* }
|
|
74
|
+
* onUpdate(dt) {
|
|
75
|
+
* this.fire.update(dt);
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
42
78
|
*/
|
|
43
79
|
export class Fire {
|
|
44
80
|
constructor(game, options = {}) {
|
|
@@ -61,6 +97,7 @@ export class Fire {
|
|
|
61
97
|
if (maxParticles === undefined && game.isMobileDevice() && cores <= 4)
|
|
62
98
|
max = Math.round(max * 0.6);
|
|
63
99
|
this._maxParticles = max;
|
|
100
|
+
game._registerDesignModule("fire", (obj) => this._fromDesign(obj));
|
|
64
101
|
}
|
|
65
102
|
/** The flames burning now, in the order they were made. */
|
|
66
103
|
get flames() {
|
|
@@ -147,9 +184,13 @@ export class Fire {
|
|
|
147
184
|
const alive = new Set(this.game.getSprites());
|
|
148
185
|
for (const flame of [...this._flames]) {
|
|
149
186
|
if (!alive.has(flame)) {
|
|
150
|
-
|
|
187
|
+
// A flame a design built waits to be added, as with createObject's
|
|
188
|
+
// `add: false`; one that was in the game and left it is dropped.
|
|
189
|
+
if (!flame._pending)
|
|
190
|
+
this._drop(flame);
|
|
151
191
|
continue;
|
|
152
192
|
}
|
|
193
|
+
flame._pending = false;
|
|
153
194
|
if (flame._target && !alive.has(flame._target)) {
|
|
154
195
|
flame._target = null;
|
|
155
196
|
void flame.extinguish(250);
|
|
@@ -159,6 +200,32 @@ export class Fire {
|
|
|
159
200
|
this.remove(flame);
|
|
160
201
|
}
|
|
161
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Builds a flame placed in a design, with the look its options give it over
|
|
205
|
+
* the module's defaults, never the fire's own look. The game adds it.
|
|
206
|
+
*
|
|
207
|
+
* @internal
|
|
208
|
+
*/
|
|
209
|
+
_fromDesign(obj) {
|
|
210
|
+
const kind = obj.kind;
|
|
211
|
+
checkKind(kind);
|
|
212
|
+
const options = (obj.options ?? {});
|
|
213
|
+
const spec = KINDS[kind];
|
|
214
|
+
const flame = new Flame(this, kind, {
|
|
215
|
+
size: options.size,
|
|
216
|
+
intensity: options.intensity,
|
|
217
|
+
light: options.light,
|
|
218
|
+
palette: options.palette ?? "fire",
|
|
219
|
+
style: options.style ?? "soft",
|
|
220
|
+
smoke: options.smoke ?? spec.smoke,
|
|
221
|
+
sparks: options.sparks ?? spec.sparks,
|
|
222
|
+
smokeColor: options.smokeColor ?? DEFAULT_SMOKE,
|
|
223
|
+
});
|
|
224
|
+
flame._pending = true;
|
|
225
|
+
flame._prewarm = true;
|
|
226
|
+
this._flames.push(flame);
|
|
227
|
+
return flame;
|
|
228
|
+
}
|
|
162
229
|
/** @internal */
|
|
163
230
|
_add(flame) {
|
|
164
231
|
this._flames.push(flame);
|
|
@@ -210,6 +277,10 @@ export class Flame extends DrawSprite {
|
|
|
210
277
|
this._expiresIn = Infinity;
|
|
211
278
|
/** @internal No more particles are emitted; the flame is removed once the last one is gone. */
|
|
212
279
|
this._out = false;
|
|
280
|
+
/** @internal Built from a design and not yet seen in the game. */
|
|
281
|
+
this._pending = false;
|
|
282
|
+
/** @internal Burns for a moment before its first frame, as a flame placed in a design does. */
|
|
283
|
+
this._prewarm = false;
|
|
213
284
|
/** @internal */
|
|
214
285
|
this._tween = null;
|
|
215
286
|
/** @internal */
|
|
@@ -276,6 +347,22 @@ export class Flame extends DrawSprite {
|
|
|
276
347
|
get target() {
|
|
277
348
|
return this._target;
|
|
278
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Makes the flame follow `sprite` every frame, keeping the offset it has
|
|
352
|
+
* from it now, as a flame placed over a sprite in a design keeps where the
|
|
353
|
+
* creator put it. It hides with the sprite and goes out when the sprite is
|
|
354
|
+
* destroyed. `null` stops following and leaves the flame where it is.
|
|
355
|
+
*/
|
|
356
|
+
follow(sprite) {
|
|
357
|
+
if (sprite === null) {
|
|
358
|
+
this._target = null;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
this._target = sprite;
|
|
362
|
+
this._offsetX = this.x - sprite.x;
|
|
363
|
+
this._offsetY = this.y - sprite.y;
|
|
364
|
+
this._expiresIn = Infinity;
|
|
365
|
+
}
|
|
279
366
|
/**
|
|
280
367
|
* Changes the intensity over `ms` milliseconds.
|
|
281
368
|
* @returns A promise that resolves when it gets there, or when another change replaces it.
|
|
@@ -312,8 +399,20 @@ export class Flame extends DrawSprite {
|
|
|
312
399
|
// the surface's center is above it.
|
|
313
400
|
return this.height / 2 - this._baseY;
|
|
314
401
|
}
|
|
402
|
+
/** @internal */
|
|
403
|
+
_editorBox() {
|
|
404
|
+
const spec = this._spec;
|
|
405
|
+
return { cx: this.x, cy: this.y - spec.boxRise * this.size, w: spec.boxWidth * this.size, h: spec.boxHeight * this.size };
|
|
406
|
+
}
|
|
315
407
|
/** @internal Called by the fire once per frame. */
|
|
316
408
|
_advance(dt) {
|
|
409
|
+
if (this._prewarm) {
|
|
410
|
+
// Placed where the design says only once the game has applied its
|
|
411
|
+
// position, so its burning is caught up on its first frame.
|
|
412
|
+
this._prewarm = false;
|
|
413
|
+
for (let t = 0; t < PREWARM_SECONDS; t += PREWARM_STEP)
|
|
414
|
+
this._advance(PREWARM_STEP);
|
|
415
|
+
}
|
|
317
416
|
this._time += dt;
|
|
318
417
|
this._advanceTween(dt);
|
|
319
418
|
this._goOutWhenDown();
|
package/dist/minimo-format.d.ts
CHANGED
|
@@ -118,11 +118,12 @@ export interface AssetFile {
|
|
|
118
118
|
*/
|
|
119
119
|
export declare function parseAssetFile(json: unknown): AssetFile;
|
|
120
120
|
/**
|
|
121
|
-
* The
|
|
121
|
+
* The type of a design object. Each built-in type maps to one runtime class:
|
|
122
122
|
* `image` to `ImageSprite`, `emoji` to `EmojiSprite`, `text` to `TextSprite`,
|
|
123
|
-
* and `background` to `BackgroundLayer`.
|
|
123
|
+
* and `background` to `BackgroundLayer`. A `module` object is built by the
|
|
124
|
+
* optional module it names, such as a `Flame` by the `fire` module.
|
|
124
125
|
*/
|
|
125
|
-
export type DesignObjectType = "image" | "emoji" | "text" | "background";
|
|
126
|
+
export type DesignObjectType = "image" | "emoji" | "text" | "background" | "module";
|
|
126
127
|
/**
|
|
127
128
|
* The properties every design object has, whatever its type.
|
|
128
129
|
*
|
|
@@ -301,18 +302,45 @@ export interface DesignBackground extends DesignObjectBase {
|
|
|
301
302
|
/** How the image fits its rectangle. Default: `"cover"`. */
|
|
302
303
|
fit?: "none" | "stretch" | "contain" | "cover";
|
|
303
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* An object of an optional module in a design file, such as a torch of the
|
|
307
|
+
* `fire` module. It names its module and its kind, and the module builds it,
|
|
308
|
+
* so the scene creates the module, such as `new Fire(game)`, before creating
|
|
309
|
+
* the design's objects. A design holding one is version 3.
|
|
310
|
+
*
|
|
311
|
+
* Each module takes only some of the sprite properties and its own
|
|
312
|
+
* `options`, both listed in {@link designModules}; anything else about the
|
|
313
|
+
* object, such as what it does while the game plays, is set in code. The
|
|
314
|
+
* kinds a module places and its options are documented in that module's API.
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```json
|
|
318
|
+
* { "id": "3e7b1c9a-5d2f-4a8e-b0c6-9f1d2e4a7b35", "name": "left-torch", "type": "module", "module": "fire", "kind": "torch", "x": 120, "y": 200, "options": { "size": 28 } }
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
export interface DesignModule extends DesignSpriteBase {
|
|
322
|
+
/** Always `"module"`. */
|
|
323
|
+
type: "module";
|
|
324
|
+
/** Required. The module that builds the object, such as `"fire"`: a key of {@link designModules}. */
|
|
325
|
+
module: string;
|
|
326
|
+
/** Required. What the module builds, such as `"torch"`: one of its `kinds` in {@link designModules}. */
|
|
327
|
+
kind: string;
|
|
328
|
+
/** The module's own settings for the object, such as `{ "size": 28 }`. Default: `{}`. */
|
|
329
|
+
options?: Record<string, unknown>;
|
|
330
|
+
}
|
|
304
331
|
/** Any object of a design file. Its `type` says which. */
|
|
305
|
-
export type DesignObject = DesignImage | DesignEmoji | DesignText | DesignBackground;
|
|
332
|
+
export type DesignObject = DesignImage | DesignEmoji | DesignText | DesignBackground | DesignModule;
|
|
306
333
|
/**
|
|
307
334
|
* The content of a design file, `*.design.json`, which a game reads through
|
|
308
335
|
* {@link Design} and creates with `game.createObject` and
|
|
309
336
|
* `game.createObjectList`. A design without pages is a {@link DesignFileV1},
|
|
310
337
|
* one flat list of objects; a design divided into pages is a
|
|
311
|
-
* {@link DesignFileV2}
|
|
312
|
-
*
|
|
313
|
-
*
|
|
338
|
+
* {@link DesignFileV2}; a design holding objects of an optional module, with
|
|
339
|
+
* or without pages, is a {@link DesignFileV3}. All three are current: a
|
|
340
|
+
* design is written in the lowest version that holds it, so it also loads in
|
|
341
|
+
* an older MinimoJS whenever it can.
|
|
314
342
|
*/
|
|
315
|
-
export type DesignFile = DesignFileV1 | DesignFileV2;
|
|
343
|
+
export type DesignFile = DesignFileV1 | DesignFileV2 | DesignFileV3;
|
|
316
344
|
/**
|
|
317
345
|
* A design file without pages: one flat list of objects.
|
|
318
346
|
*
|
|
@@ -366,7 +394,35 @@ export interface DesignFileV2 {
|
|
|
366
394
|
/** The pages, at least one, in file order. */
|
|
367
395
|
pages: DesignPage[];
|
|
368
396
|
}
|
|
369
|
-
/**
|
|
397
|
+
/**
|
|
398
|
+
* A design file holding objects of an optional module, {@link DesignModule}.
|
|
399
|
+
* Like the earlier versions, it has either a flat list of `objects` or
|
|
400
|
+
* `pages`, never both; it is version 3 only because an engine older than
|
|
401
|
+
* module objects cannot read it.
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* ```json
|
|
405
|
+
* {
|
|
406
|
+
* "format": "minimo-design",
|
|
407
|
+
* "version": 3,
|
|
408
|
+
* "objects": [
|
|
409
|
+
* { "id": "6f1c2a0e-3b7d-4c1e-9a52-0d8e4f6b1a27", "type": "background", "imageKey": "dungeon" },
|
|
410
|
+
* { "id": "3e7b1c9a-5d2f-4a8e-b0c6-9f1d2e4a7b35", "name": "left-torch", "type": "module", "module": "fire", "kind": "torch", "x": 120, "y": 200 }
|
|
411
|
+
* ]
|
|
412
|
+
* }
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
export interface DesignFileV3 {
|
|
416
|
+
/** Always `"minimo-design"`. */
|
|
417
|
+
format: "minimo-design";
|
|
418
|
+
/** `3`: the design holds objects of an optional module. */
|
|
419
|
+
version: 3;
|
|
420
|
+
/** The objects of a design without pages, in file order. Present exactly when `pages` is not. */
|
|
421
|
+
objects?: DesignObject[];
|
|
422
|
+
/** The pages of a design divided into pages, in file order. Present exactly when `objects` is not. */
|
|
423
|
+
pages?: DesignPage[];
|
|
424
|
+
}
|
|
425
|
+
/** One page of a design file with pages: a named group of the design's objects. */
|
|
370
426
|
export interface DesignPage {
|
|
371
427
|
/**
|
|
372
428
|
* Required. A UUID unique among the file's pages. Tools use it to keep a
|
|
@@ -405,7 +461,8 @@ export interface DesignProperty {
|
|
|
405
461
|
readonly required: boolean;
|
|
406
462
|
/**
|
|
407
463
|
* The runtime object's value when the file omits the property.
|
|
408
|
-
* `undefined` for a required property and for
|
|
464
|
+
* `undefined` for a required property, for `name`, and for a module option
|
|
465
|
+
* whose default depends on the object's kind.
|
|
409
466
|
*/
|
|
410
467
|
readonly default: unknown;
|
|
411
468
|
}
|
|
@@ -414,9 +471,11 @@ export interface DesignProperty {
|
|
|
414
471
|
* with its JSON type, whether it is required, and its default.
|
|
415
472
|
*
|
|
416
473
|
* It is the runtime form of {@link DesignImage}, {@link DesignEmoji},
|
|
417
|
-
* {@link DesignText},
|
|
418
|
-
* it against, for tools that cannot read TypeScript
|
|
419
|
-
* editor's property panel. `new Design(json)` validates
|
|
474
|
+
* {@link DesignText}, {@link DesignBackground}, and {@link DesignModule},
|
|
475
|
+
* which the compiler checks it against, for tools that cannot read TypeScript
|
|
476
|
+
* types, such as an editor's property panel. `new Design(json)` validates
|
|
477
|
+
* objects with it. A `module` object takes only the properties its module
|
|
478
|
+
* lists in {@link designModules}.
|
|
420
479
|
*
|
|
421
480
|
* @example
|
|
422
481
|
* ```ts
|
|
@@ -426,6 +485,62 @@ export interface DesignProperty {
|
|
|
426
485
|
* ```
|
|
427
486
|
*/
|
|
428
487
|
export declare const designObjectProperties: Readonly<Record<DesignObjectType, readonly DesignProperty[]>>;
|
|
488
|
+
/** One of a module's own options, as a {@link DesignModule}'s `options` holds it. */
|
|
489
|
+
export interface DesignModuleOption extends DesignProperty {
|
|
490
|
+
/** The smallest number accepted; `null` when there is no lower bound or the option is not a number. */
|
|
491
|
+
readonly min: number | null;
|
|
492
|
+
/** The largest number accepted; `null` when there is no upper bound or the option is not a number. */
|
|
493
|
+
readonly max: number | null;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* What a design may hold of one optional module, for the engine that
|
|
497
|
+
* validates designs and for tools such as an editor. See {@link designModules}.
|
|
498
|
+
*/
|
|
499
|
+
export interface DesignModuleSchema {
|
|
500
|
+
/** The module's id, as a {@link DesignModule}'s `module` names it, such as `"fire"`. */
|
|
501
|
+
readonly module: string;
|
|
502
|
+
/** The module's file, next to `minimo.js`, such as `"minimo-fire.js"`. */
|
|
503
|
+
readonly file: string;
|
|
504
|
+
/**
|
|
505
|
+
* The class the module exports, which a scene creates with
|
|
506
|
+
* `new Class(game)` before creating the design's objects, and whose
|
|
507
|
+
* `update(dt)` advances them, such as `"Fire"`.
|
|
508
|
+
*/
|
|
509
|
+
readonly className: string;
|
|
510
|
+
/** The kinds a design can place, such as `"torch"`. The module makes any other kind in code only. */
|
|
511
|
+
readonly kinds: readonly string[];
|
|
512
|
+
/**
|
|
513
|
+
* The sprite properties of {@link DesignSpriteBase} a design may set on the
|
|
514
|
+
* module's objects, besides `name`, `tags`, and `data`, which every object takes.
|
|
515
|
+
*/
|
|
516
|
+
readonly properties: readonly string[];
|
|
517
|
+
/** The module's own options, which `options` may hold. */
|
|
518
|
+
readonly options: readonly DesignModuleOption[];
|
|
519
|
+
/**
|
|
520
|
+
* The number option that sets how big an object is, which an editor's
|
|
521
|
+
* scale handle changes, such as `"size"`; `null` when a design does not
|
|
522
|
+
* size the module's objects.
|
|
523
|
+
*/
|
|
524
|
+
readonly sizeOption: string | null;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* The optional modules whose objects a design can hold, by id, with the kinds
|
|
528
|
+
* each places, the sprite properties and options it takes, and how a tool
|
|
529
|
+
* loads it. A design object of any other module, kind, property, or option is
|
|
530
|
+
* refused: what a design does not hold is set in code.
|
|
531
|
+
*
|
|
532
|
+
* A design holds how an object looks when its screen opens. What the game
|
|
533
|
+
* changes while it plays, such as a flame going out or a die's roll, is the
|
|
534
|
+
* module's API, used in code on the object the design created.
|
|
535
|
+
*
|
|
536
|
+
* @example
|
|
537
|
+
* ```ts
|
|
538
|
+
* for (const [id, schema] of Object.entries(designModules)) {
|
|
539
|
+
* console.log(id, schema.kinds, schema.options.map((option) => option.name));
|
|
540
|
+
* }
|
|
541
|
+
* ```
|
|
542
|
+
*/
|
|
543
|
+
export declare const designModules: Readonly<Record<string, DesignModuleSchema>>;
|
|
429
544
|
/**
|
|
430
545
|
* The data of a design file, `*.design.json`, validated: its objects, its
|
|
431
546
|
* pages, and searches on them. A `Design` needs no game and creates nothing; pass its
|
|
@@ -478,8 +593,10 @@ export declare class Design {
|
|
|
478
593
|
* `"minimo-design"`, a `version` newer than this engine reads, an object
|
|
479
594
|
* whose `type` is not one of the built-in types, an `id` that is not a UUID
|
|
480
595
|
* or is used twice, a value of the wrong type, a missing required property,
|
|
481
|
-
* a `data` that is not a JSON object,
|
|
482
|
-
* a missing, padded, or repeated `name
|
|
596
|
+
* a `data` that is not a JSON object, a page without a UUID `id` or with
|
|
597
|
+
* a missing, padded, or repeated `name`, and a `module` object in a design
|
|
598
|
+
* older than version 3 or with a module, kind, property, or option that
|
|
599
|
+
* {@link designModules} does not list.
|
|
483
600
|
*
|
|
484
601
|
* @param json - The content of a `*.design.json` file. See {@link DesignFile}.
|
|
485
602
|
*/
|
package/dist/minimo-format.js
CHANGED
|
@@ -102,7 +102,7 @@ export function parseAssetFile(json) {
|
|
|
102
102
|
/** The `format` value of every design file. */
|
|
103
103
|
const DESIGN_FILE_FORMAT = "minimo-design";
|
|
104
104
|
/** The newest design file `version` this engine reads. */
|
|
105
|
-
const DESIGN_FILE_VERSION =
|
|
105
|
+
const DESIGN_FILE_VERSION = 3;
|
|
106
106
|
function spec(type, defaultValue) {
|
|
107
107
|
return { type, nullable: false, values: null, required: false, default: defaultValue };
|
|
108
108
|
}
|
|
@@ -188,6 +188,12 @@ const BACKGROUND_SPECS = {
|
|
|
188
188
|
fit: oneOf(["none", "stretch", "contain", "cover"], "cover"),
|
|
189
189
|
...DATA,
|
|
190
190
|
};
|
|
191
|
+
const MODULE_SPECS = {
|
|
192
|
+
module: requiredString(),
|
|
193
|
+
kind: requiredString(),
|
|
194
|
+
options: spec("object", {}),
|
|
195
|
+
...SPRITE_SPECS,
|
|
196
|
+
};
|
|
191
197
|
// `any`: each spec has its own value type, which no longer matters once checked.
|
|
192
198
|
function describeProperties(specs) {
|
|
193
199
|
return Object.freeze(Object.entries(specs).map(([name, { type, nullable, values, required, default: defaultValue }]) => Object.freeze({ name, type, nullable, values, required, default: defaultValue })));
|
|
@@ -197,9 +203,11 @@ function describeProperties(specs) {
|
|
|
197
203
|
* with its JSON type, whether it is required, and its default.
|
|
198
204
|
*
|
|
199
205
|
* It is the runtime form of {@link DesignImage}, {@link DesignEmoji},
|
|
200
|
-
* {@link DesignText},
|
|
201
|
-
* it against, for tools that cannot read TypeScript
|
|
202
|
-
* editor's property panel. `new Design(json)` validates
|
|
206
|
+
* {@link DesignText}, {@link DesignBackground}, and {@link DesignModule},
|
|
207
|
+
* which the compiler checks it against, for tools that cannot read TypeScript
|
|
208
|
+
* types, such as an editor's property panel. `new Design(json)` validates
|
|
209
|
+
* objects with it. A `module` object takes only the properties its module
|
|
210
|
+
* lists in {@link designModules}.
|
|
203
211
|
*
|
|
204
212
|
* @example
|
|
205
213
|
* ```ts
|
|
@@ -213,10 +221,113 @@ export const designObjectProperties = Object.freeze({
|
|
|
213
221
|
emoji: describeProperties(EMOJI_SPECS),
|
|
214
222
|
text: describeProperties(TEXT_SPECS),
|
|
215
223
|
background: describeProperties(BACKGROUND_SPECS),
|
|
224
|
+
module: describeProperties(MODULE_SPECS),
|
|
225
|
+
});
|
|
226
|
+
function option(name, type, defaultValue, { values = null, min = null, max = null } = {}) {
|
|
227
|
+
return Object.freeze({ name, type, nullable: false, values, required: false, default: defaultValue, min, max });
|
|
228
|
+
}
|
|
229
|
+
function moduleSchema(schema) {
|
|
230
|
+
return Object.freeze({
|
|
231
|
+
...schema,
|
|
232
|
+
kinds: Object.freeze([...schema.kinds]),
|
|
233
|
+
properties: Object.freeze([...schema.properties]),
|
|
234
|
+
options: Object.freeze([...schema.options]),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The optional modules whose objects a design can hold, by id, with the kinds
|
|
239
|
+
* each places, the sprite properties and options it takes, and how a tool
|
|
240
|
+
* loads it. A design object of any other module, kind, property, or option is
|
|
241
|
+
* refused: what a design does not hold is set in code.
|
|
242
|
+
*
|
|
243
|
+
* A design holds how an object looks when its screen opens. What the game
|
|
244
|
+
* changes while it plays, such as a flame going out or a die's roll, is the
|
|
245
|
+
* module's API, used in code on the object the design created.
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```ts
|
|
249
|
+
* for (const [id, schema] of Object.entries(designModules)) {
|
|
250
|
+
* console.log(id, schema.kinds, schema.options.map((option) => option.name));
|
|
251
|
+
* }
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
export const designModules = Object.freeze({
|
|
255
|
+
fire: moduleSchema({
|
|
256
|
+
module: "fire",
|
|
257
|
+
file: "minimo-fire.js",
|
|
258
|
+
className: "Fire",
|
|
259
|
+
kinds: ["torch", "campfire", "burning", "trail"],
|
|
260
|
+
// A flame is neither rotated nor scaled, since its particles are placed in
|
|
261
|
+
// the world: its `size` resizes it.
|
|
262
|
+
properties: ["x", "y", "alpha", "visible", "layer", "ignoreScroll", "bodyWidth", "bodyHeight", "bodyOffsetX", "bodyOffsetY"],
|
|
263
|
+
options: [
|
|
264
|
+
option("size", "number", 32, { min: 4 }),
|
|
265
|
+
option("intensity", "number", 1, { min: 0, max: 1 }),
|
|
266
|
+
option("palette", "string", "fire", { values: ["fire", "blue", "green", "purple"] }),
|
|
267
|
+
option("style", "string", "soft", { values: ["soft", "pixel"] }),
|
|
268
|
+
option("smoke", "number", undefined, { min: 0, max: 1 }),
|
|
269
|
+
option("sparks", "number", undefined, { min: 0, max: 1 }),
|
|
270
|
+
option("smokeColor", "string", "#4a4440"),
|
|
271
|
+
option("light", "boolean", undefined),
|
|
272
|
+
],
|
|
273
|
+
sizeOption: "size",
|
|
274
|
+
}),
|
|
275
|
+
dice: moduleSchema({
|
|
276
|
+
module: "dice",
|
|
277
|
+
file: "minimo-dice.js",
|
|
278
|
+
className: "Dice",
|
|
279
|
+
kinds: ["d4", "d6", "d8", "d10", "d12", "d20"],
|
|
280
|
+
// A design places a die; its look, its size, and its value are the game's.
|
|
281
|
+
properties: ["x", "y"],
|
|
282
|
+
options: [],
|
|
283
|
+
sizeOption: null,
|
|
284
|
+
}),
|
|
216
285
|
});
|
|
286
|
+
/** The properties every module object takes, whatever its module. */
|
|
287
|
+
const MODULE_OBJECT_FIELDS = new Set(["id", "type", "module", "kind", "options", "name", "tags", "data"]);
|
|
288
|
+
/** Checks a module object, already checked as a `module`, against its module's schema. */
|
|
289
|
+
function checkModuleObject(object, label, fail) {
|
|
290
|
+
const quoted = (names) => names.map((name) => `"${name}"`).join(", ");
|
|
291
|
+
const moduleId = object.module;
|
|
292
|
+
const schema = Object.prototype.hasOwnProperty.call(designModules, moduleId) ? designModules[moduleId] : undefined;
|
|
293
|
+
if (schema === undefined) {
|
|
294
|
+
throw fail(`${label} belongs to the module "${moduleId}", whose objects a design cannot hold. The modules a design can hold objects of are ${quoted(Object.keys(designModules))}.`);
|
|
295
|
+
}
|
|
296
|
+
const kind = object.kind;
|
|
297
|
+
if (!schema.kinds.includes(kind)) {
|
|
298
|
+
throw fail(`${label} has the kind "${kind}", which a design cannot place. A "${moduleId}" object is one of ${quoted(schema.kinds)}; anything else the module makes is made in code.`);
|
|
299
|
+
}
|
|
300
|
+
for (const field of Object.keys(object)) {
|
|
301
|
+
if (MODULE_OBJECT_FIELDS.has(field) || schema.properties.includes(field))
|
|
302
|
+
continue;
|
|
303
|
+
throw fail(`${label} sets "${field}", which a "${moduleId}" object in a design does not take. It takes ${quoted(schema.properties)}; set anything else in code.`);
|
|
304
|
+
}
|
|
305
|
+
const options = (object.options ?? {});
|
|
306
|
+
for (const [name, value] of Object.entries(options)) {
|
|
307
|
+
const spec = schema.options.find((candidate) => candidate.name === name);
|
|
308
|
+
if (spec === undefined) {
|
|
309
|
+
const known = schema.options.length === 0 ? "takes no options" : `takes the options ${quoted(schema.options.map((candidate) => candidate.name))}`;
|
|
310
|
+
throw fail(`${label} has the option "${name}", which a "${moduleId}" object in a design does not take. It ${known}; set anything else in code.`);
|
|
311
|
+
}
|
|
312
|
+
const inRange = typeof value !== "number" || ((spec.min === null || value >= spec.min) && (spec.max === null || value <= spec.max));
|
|
313
|
+
if (!isValidValue(value, spec) || !inRange) {
|
|
314
|
+
throw fail(`${label} has the option "${name}" set to ${describe(value)}, but it must be ${describeExpected(spec)}${describeRange(spec)}.`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function describeRange(option) {
|
|
319
|
+
if (option.min !== null && option.max !== null)
|
|
320
|
+
return ` from ${option.min} to ${option.max}`;
|
|
321
|
+
if (option.min !== null)
|
|
322
|
+
return ` of at least ${option.min}`;
|
|
323
|
+
if (option.max !== null)
|
|
324
|
+
return ` of at most ${option.max}`;
|
|
325
|
+
return "";
|
|
326
|
+
}
|
|
217
327
|
const DESIGN_FILE_FIELDS = {
|
|
218
328
|
1: new Set(["format", "version", "objects"]),
|
|
219
329
|
2: new Set(["format", "version", "pages"]),
|
|
330
|
+
3: new Set(["format", "version", "objects", "pages"]),
|
|
220
331
|
};
|
|
221
332
|
const DESIGN_PAGE_FIELDS = new Set(["id", "name", "objects"]);
|
|
222
333
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -265,6 +376,8 @@ export function parseDesignObject(json, where, kind = "design object") {
|
|
|
265
376
|
throw fail(`${label} must have "${property.name}", which every "${type}" object requires.`);
|
|
266
377
|
}
|
|
267
378
|
}
|
|
379
|
+
if (type === "module")
|
|
380
|
+
checkModuleObject(copy, label, fail);
|
|
268
381
|
return copy;
|
|
269
382
|
}
|
|
270
383
|
function isValidValue(value, property) {
|
|
@@ -322,14 +435,18 @@ function parseDesignFile(json) {
|
|
|
322
435
|
if (version > DESIGN_FILE_VERSION) {
|
|
323
436
|
throw fail(`"version" ${version} is newer than this engine reads (${DESIGN_FILE_VERSION}). Use a newer MinimoJS.`);
|
|
324
437
|
}
|
|
325
|
-
//
|
|
326
|
-
// without pages,
|
|
438
|
+
// Every version is current, so none is migrated: version 1 is a design
|
|
439
|
+
// without pages, version 2 one divided into pages, and version 3 either,
|
|
440
|
+
// holding objects of an optional module.
|
|
327
441
|
for (const field of Object.keys(json)) {
|
|
328
442
|
if (!DESIGN_FILE_FIELDS[version].has(field)) {
|
|
329
443
|
const hint = field === "pages" ? ` A design with pages has "version" 2.` : field === "objects" ? ` In version 2, objects belong to "pages".` : "";
|
|
330
444
|
throw fail(`the design has an unknown field "${field}".${hint}`);
|
|
331
445
|
}
|
|
332
446
|
}
|
|
447
|
+
if (version === 3 && "objects" in json === "pages" in json) {
|
|
448
|
+
throw fail(`a version 3 design has either "objects" or "pages", and this one has ${"objects" in json ? "both" : "neither"}.`);
|
|
449
|
+
}
|
|
333
450
|
// An object's id is unique in the whole file, across pages.
|
|
334
451
|
const whereById = new Map();
|
|
335
452
|
const parseObjects = (entries, where) => {
|
|
@@ -339,6 +456,9 @@ function parseDesignFile(json) {
|
|
|
339
456
|
return entries.map((entry, index) => {
|
|
340
457
|
const at = `${where === "" ? "" : `${where}.`}objects[${index}]`;
|
|
341
458
|
const object = parseDesignObject(entry, at, "design");
|
|
459
|
+
if (object.type === "module" && version < 3) {
|
|
460
|
+
throw fail(`${at} is a "module" object, which only a design with "version" 3 holds, so that an older MinimoJS refuses the design by its version.`);
|
|
461
|
+
}
|
|
342
462
|
const id = object.id.toLowerCase();
|
|
343
463
|
const first = whereById.get(id);
|
|
344
464
|
if (first !== undefined) {
|
|
@@ -351,6 +471,9 @@ function parseDesignFile(json) {
|
|
|
351
471
|
if (version === 1) {
|
|
352
472
|
return { format: DESIGN_FILE_FORMAT, version: 1, objects: parseObjects(json.objects, "") };
|
|
353
473
|
}
|
|
474
|
+
if (version === 3 && "objects" in json) {
|
|
475
|
+
return { format: DESIGN_FILE_FORMAT, version: 3, objects: parseObjects(json.objects, "") };
|
|
476
|
+
}
|
|
354
477
|
if (!Array.isArray(json.pages)) {
|
|
355
478
|
throw fail(`"pages" must be a list, found ${describe(json.pages)}.`);
|
|
356
479
|
}
|
|
@@ -390,7 +513,7 @@ function parseDesignFile(json) {
|
|
|
390
513
|
pageIndexByName.set(name, index);
|
|
391
514
|
return { id, name, objects: parseObjects(entry.objects, where) };
|
|
392
515
|
});
|
|
393
|
-
return { format: DESIGN_FILE_FORMAT, version: 2, pages };
|
|
516
|
+
return version === 3 ? { format: DESIGN_FILE_FORMAT, version: 3, pages } : { format: DESIGN_FILE_FORMAT, version: 2, pages };
|
|
394
517
|
}
|
|
395
518
|
/**
|
|
396
519
|
* Returns the first item `match` accepts, in order, or throws an error saying
|
|
@@ -447,20 +570,23 @@ export class Design {
|
|
|
447
570
|
* `"minimo-design"`, a `version` newer than this engine reads, an object
|
|
448
571
|
* whose `type` is not one of the built-in types, an `id` that is not a UUID
|
|
449
572
|
* or is used twice, a value of the wrong type, a missing required property,
|
|
450
|
-
* a `data` that is not a JSON object,
|
|
451
|
-
* a missing, padded, or repeated `name
|
|
573
|
+
* a `data` that is not a JSON object, a page without a UUID `id` or with
|
|
574
|
+
* a missing, padded, or repeated `name`, and a `module` object in a design
|
|
575
|
+
* older than version 3 or with a module, kind, property, or option that
|
|
576
|
+
* {@link designModules} does not list.
|
|
452
577
|
*
|
|
453
578
|
* @param json - The content of a `*.design.json` file. See {@link DesignFile}.
|
|
454
579
|
*/
|
|
455
580
|
constructor(json) {
|
|
456
581
|
const file = parseDesignFile(json);
|
|
457
|
-
|
|
582
|
+
const pages = "pages" in file ? file.pages : undefined;
|
|
583
|
+
if (pages === undefined) {
|
|
458
584
|
this.pages = Object.freeze([]);
|
|
459
|
-
this.objects = Object.freeze(file.objects);
|
|
585
|
+
this.objects = Object.freeze(("objects" in file ? file.objects : undefined) ?? []);
|
|
460
586
|
}
|
|
461
587
|
else {
|
|
462
|
-
this.pages = Object.freeze(
|
|
463
|
-
this.objects = Object.freeze(
|
|
588
|
+
this.pages = Object.freeze(pages.map((page) => Object.freeze({ ...page, objects: Object.freeze(page.objects) })));
|
|
589
|
+
this.objects = Object.freeze(pages.flatMap((page) => page.objects));
|
|
464
590
|
}
|
|
465
591
|
}
|
|
466
592
|
/**
|
package/dist/minimo.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { DesignObject } from "./minimo-format.js";
|
|
|
13
13
|
export { Design, parseAssetFile } 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, DesignImage, DesignObject, DesignObjectBase, DesignObjectType, DesignPage, DesignSpriteBase, DesignText, ImageAsset, SoundAsset, UnknownAsset, } from "./minimo-format.js";
|
|
16
|
+
export type { AssetFile, DesignBackground, DesignEmoji, DesignFile, DesignFileV1, DesignFileV2, DesignFileV3, DesignImage, DesignModule, DesignObject, DesignObjectBase, DesignObjectType, DesignPage, DesignSpriteBase, DesignText, ImageAsset, SoundAsset, UnknownAsset, } from "./minimo-format.js";
|
|
17
17
|
/**
|
|
18
18
|
* Optional tuning values for {@link Game.animateExplode}.
|
|
19
19
|
*
|
|
@@ -1655,6 +1655,12 @@ export declare class Game {
|
|
|
1655
1655
|
* its tags. Then every property the data holds is applied, including
|
|
1656
1656
|
* `name`, `tags`, and a copy of `data`.
|
|
1657
1657
|
*
|
|
1658
|
+
* A `module` object is built by its module, such as a `Flame` by the
|
|
1659
|
+
* `fire` module, which the scene must have created first, such as with
|
|
1660
|
+
* `new Fire(game)` in `onCreate()`; otherwise the creation throws an error
|
|
1661
|
+
* naming the module. Tag builders do not apply to it, and its tags are added
|
|
1662
|
+
* to those the module gives it.
|
|
1663
|
+
*
|
|
1658
1664
|
* Each call creates a new object, so one template can be created any number
|
|
1659
1665
|
* of times. Set its position afterwards to place it; the object is never
|
|
1660
1666
|
* drawn at the template's position.
|
|
@@ -1727,6 +1733,7 @@ export declare class Game {
|
|
|
1727
1733
|
* naming the object and both tags.
|
|
1728
1734
|
* - Builders are scene state: {@link Game.reset} clears them, as it clears
|
|
1729
1735
|
* timers, so each scene registers its own in `onCreate()`.
|
|
1736
|
+
* - A `module` object is always built by its module, never by a builder.
|
|
1730
1737
|
*
|
|
1731
1738
|
* @param tag - The tag whose objects the builder builds.
|
|
1732
1739
|
* @param builder - Receives the object's data and the game, and returns the object, not yet added.
|
package/dist/minimo.js
CHANGED
|
@@ -22,7 +22,7 @@ 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, findFirst, parseAssetFile, parseDesignObject } from "./minimo-format.js";
|
|
25
|
+
import { Design, designModules, findFirst, parseAssetFile, parseDesignObject } from "./minimo-format.js";
|
|
26
26
|
export { Design, parseAssetFile } 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. */
|
|
@@ -294,6 +294,17 @@ export class Sprite {
|
|
|
294
294
|
const safeScale = Number.isFinite(this.scale) ? this.scale : 1;
|
|
295
295
|
return this.renderY + this.bodyOffsetY * Math.max(0, safeScale);
|
|
296
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* The box, in world space, that a design editor selects and aligns this
|
|
299
|
+
* sprite by, when it is not the sprite's own: a module's object whose
|
|
300
|
+
* surface is larger than what the player sees, such as a flame with room
|
|
301
|
+
* for its glow, reports the visible part. `null` uses the sprite's bounds.
|
|
302
|
+
*
|
|
303
|
+
* @internal
|
|
304
|
+
*/
|
|
305
|
+
_editorBox() {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
297
308
|
getAnchorOffsetX() {
|
|
298
309
|
return 0;
|
|
299
310
|
}
|
|
@@ -1376,6 +1387,7 @@ export class Game {
|
|
|
1376
1387
|
/** @internal */ this._onCreate = null;
|
|
1377
1388
|
/** @internal */ this._currentScene = null;
|
|
1378
1389
|
/** @internal */ this._buildersByTag = new Map();
|
|
1390
|
+
/** @internal */ this._moduleBuilders = new Map();
|
|
1379
1391
|
/**
|
|
1380
1392
|
* Horizontal scroll offset of the world camera, in pixels.
|
|
1381
1393
|
* The canvas viewport is shifted left by `scrollX` — sprites with higher `x`
|
|
@@ -1839,6 +1851,12 @@ export class Game {
|
|
|
1839
1851
|
* its tags. Then every property the data holds is applied, including
|
|
1840
1852
|
* `name`, `tags`, and a copy of `data`.
|
|
1841
1853
|
*
|
|
1854
|
+
* A `module` object is built by its module, such as a `Flame` by the
|
|
1855
|
+
* `fire` module, which the scene must have created first, such as with
|
|
1856
|
+
* `new Fire(game)` in `onCreate()`; otherwise the creation throws an error
|
|
1857
|
+
* naming the module. Tag builders do not apply to it, and its tags are added
|
|
1858
|
+
* to those the module gives it.
|
|
1859
|
+
*
|
|
1842
1860
|
* Each call creates a new object, so one template can be created any number
|
|
1843
1861
|
* of times. Set its position afterwards to place it; the object is never
|
|
1844
1862
|
* drawn at the template's position.
|
|
@@ -1930,6 +1948,7 @@ export class Game {
|
|
|
1930
1948
|
* naming the object and both tags.
|
|
1931
1949
|
* - Builders are scene state: {@link Game.reset} clears them, as it clears
|
|
1932
1950
|
* timers, so each scene registers its own in `onCreate()`.
|
|
1951
|
+
* - A `module` object is always built by its module, never by a builder.
|
|
1933
1952
|
*
|
|
1934
1953
|
* @param tag - The tag whose objects the builder builds.
|
|
1935
1954
|
* @param builder - Receives the object's data and the game, and returns the object, not yet added.
|
|
@@ -1966,8 +1985,46 @@ export class Game {
|
|
|
1966
1985
|
throw new Error(`MinimoJS: ${describeDesignObject(obj)} uses the image key "${obj.imageKey}", which is not loaded. Declare it in an asset file and load that file with game.loadAssets() in an onPreload hook.`);
|
|
1967
1986
|
}
|
|
1968
1987
|
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Makes {@link Game.createObject} and {@link Game.createObjectList} build
|
|
1990
|
+
* the design's objects of `module` with `builder`. A module calls it when
|
|
1991
|
+
* it is created; the first module created for an id keeps it until
|
|
1992
|
+
* {@link Game.reset} clears it, as it clears builders.
|
|
1993
|
+
*
|
|
1994
|
+
* @internal
|
|
1995
|
+
*/
|
|
1996
|
+
_registerDesignModule(module, builder) {
|
|
1997
|
+
if (!this._moduleBuilders.has(module))
|
|
1998
|
+
this._moduleBuilders.set(module, builder);
|
|
1999
|
+
}
|
|
2000
|
+
/** @internal */
|
|
2001
|
+
_buildModuleObject(obj) {
|
|
2002
|
+
const builder = this._moduleBuilders.get(obj.module);
|
|
2003
|
+
if (builder === undefined) {
|
|
2004
|
+
const className = designModules[obj.module]?.className ?? "the module";
|
|
2005
|
+
throw new Error(`MinimoJS: ${describeDesignObject(obj)} is a "${obj.module}" object, but this scene has not created the ${obj.module} module. Create it in the scene's onCreate(), such as with new ${className}(game), before creating the design's objects.`);
|
|
2006
|
+
}
|
|
2007
|
+
const created = builder(obj, this);
|
|
2008
|
+
if (!(created instanceof Sprite)) {
|
|
2009
|
+
throw new Error(`MinimoJS: The ${obj.module} module did not build a sprite for ${describeDesignObject(obj)}.`);
|
|
2010
|
+
}
|
|
2011
|
+
const target = created;
|
|
2012
|
+
for (const [field, value] of Object.entries(obj)) {
|
|
2013
|
+
if (field === "id" || field === "type" || field === "module" || field === "kind" || field === "options")
|
|
2014
|
+
continue;
|
|
2015
|
+
// The module's own tags, such as "fire", stay beside the design's.
|
|
2016
|
+
if (field === "tags") {
|
|
2017
|
+
created.tags = [...new Set([...created.tags, ...value])];
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
target[field] = value;
|
|
2021
|
+
}
|
|
2022
|
+
return created;
|
|
2023
|
+
}
|
|
1969
2024
|
/** @internal */
|
|
1970
2025
|
_buildDesignObject(obj) {
|
|
2026
|
+
if (obj.type === "module")
|
|
2027
|
+
return this._buildModuleObject(obj);
|
|
1971
2028
|
const matchingTags = [...new Set(obj.tags ?? [])].filter((tag) => this._buildersByTag.has(tag));
|
|
1972
2029
|
if (matchingTags.length > 1) {
|
|
1973
2030
|
throw new Error(`MinimoJS: ${describeDesignObject(obj)} matches the builders of two of its tags, "${matchingTags[0]}" and "${matchingTags[1]}". An object may match at most one builder.`);
|
|
@@ -3109,6 +3166,7 @@ export class Game {
|
|
|
3109
3166
|
this._textSystem.clear();
|
|
3110
3167
|
this._inputSystem.resetPressedState();
|
|
3111
3168
|
this._buildersByTag.clear();
|
|
3169
|
+
this._moduleBuilders.clear();
|
|
3112
3170
|
this.scrollX = 0;
|
|
3113
3171
|
this.scrollY = 0;
|
|
3114
3172
|
this._loopSystem.invokeCreate();
|
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.40",
|
|
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",
|