minimojs 1.0.0-alpha.41 → 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/internal/DesignNamingCheck.js +7 -1
- package/dist/internal/RenderSystem.js +28 -5
- package/dist/minimo-format.d.ts +225 -14
- package/dist/minimo-format.js +261 -8
- 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-outline.d.ts +106 -0
- package/dist/minimo-outline.js +299 -0
- package/dist/minimo.d.ts +13 -2
- package/dist/minimo.js +64 -4
- package/package.json +1 -1
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. */
|
|
@@ -142,14 +318,20 @@ const SPRITE_SPECS = {
|
|
|
142
318
|
bodyOffsetY: spec("number", 0),
|
|
143
319
|
...DATA,
|
|
144
320
|
};
|
|
321
|
+
const OUTLINE_SPECS = {
|
|
322
|
+
outlineWidth: spec("number", 0),
|
|
323
|
+
outlineColor: spec("string", "#ffffff"),
|
|
324
|
+
};
|
|
145
325
|
const IMAGE_SPECS = {
|
|
146
326
|
imageKey: requiredString(),
|
|
147
327
|
...SPRITE_SPECS,
|
|
328
|
+
...OUTLINE_SPECS,
|
|
148
329
|
};
|
|
149
330
|
const EMOJI_SPECS = {
|
|
150
331
|
sprite: requiredString(),
|
|
151
332
|
size: spec("number", 32),
|
|
152
333
|
...SPRITE_SPECS,
|
|
334
|
+
...OUTLINE_SPECS,
|
|
153
335
|
};
|
|
154
336
|
const TEXT_SPECS = {
|
|
155
337
|
text: requiredString(),
|
|
@@ -173,6 +355,7 @@ const TEXT_SPECS = {
|
|
|
173
355
|
strokeWidth: spec("number", 0),
|
|
174
356
|
...SPRITE_SPECS,
|
|
175
357
|
color: spec("string", "#ffffff"),
|
|
358
|
+
...OUTLINE_SPECS,
|
|
176
359
|
};
|
|
177
360
|
const BACKGROUND_SPECS = {
|
|
178
361
|
imageKey: requiredString(),
|
|
@@ -283,6 +466,62 @@ export const designModules = Object.freeze({
|
|
|
283
466
|
sizeOption: null,
|
|
284
467
|
}),
|
|
285
468
|
});
|
|
469
|
+
/**
|
|
470
|
+
* The optional modules that decorate design objects of the core's types,
|
|
471
|
+
* by id. Unlike a {@link DesignModule}, a decoration adds no object: it is a
|
|
472
|
+
* look of an `image`, `emoji`, or `text` object, held in properties of that
|
|
473
|
+
* object, which the module draws.
|
|
474
|
+
*
|
|
475
|
+
* A tool that draws a design creates the module, with `new Class(game)`,
|
|
476
|
+
* before creating an object whose `enabledBy` property is above `0`.
|
|
477
|
+
*
|
|
478
|
+
* @example
|
|
479
|
+
* ```ts
|
|
480
|
+
* for (const schema of Object.values(designDecorations)) {
|
|
481
|
+
* const used = design.objects.some((object) => (object[schema.enabledBy] ?? 0) > 0);
|
|
482
|
+
* if (used) console.log(`${schema.className} from ${schema.file}`);
|
|
483
|
+
* }
|
|
484
|
+
* ```
|
|
485
|
+
*/
|
|
486
|
+
export const designDecorations = Object.freeze({
|
|
487
|
+
outline: Object.freeze({
|
|
488
|
+
module: "outline",
|
|
489
|
+
file: "minimo-outline.js",
|
|
490
|
+
className: "Outline",
|
|
491
|
+
types: Object.freeze(["image", "emoji", "text"]),
|
|
492
|
+
properties: Object.freeze(Object.keys(OUTLINE_SPECS)),
|
|
493
|
+
enabledBy: "outlineWidth",
|
|
494
|
+
}),
|
|
495
|
+
});
|
|
496
|
+
/**
|
|
497
|
+
* The first property of `object` that belongs to a decoration, or `null`.
|
|
498
|
+
*
|
|
499
|
+
* @internal
|
|
500
|
+
*/
|
|
501
|
+
export function findDecorationProperty(object) {
|
|
502
|
+
const fields = object;
|
|
503
|
+
for (const schema of Object.values(designDecorations)) {
|
|
504
|
+
if (!schema.types.includes(object.type))
|
|
505
|
+
continue;
|
|
506
|
+
for (const name of schema.properties) {
|
|
507
|
+
if (name in fields)
|
|
508
|
+
return name;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* The decorations whose `enabledBy` property is above `0` in `object`.
|
|
515
|
+
*
|
|
516
|
+
* @internal
|
|
517
|
+
*/
|
|
518
|
+
export function enabledDecorations(object) {
|
|
519
|
+
const fields = object;
|
|
520
|
+
return Object.values(designDecorations).filter((schema) => {
|
|
521
|
+
const value = fields[schema.enabledBy];
|
|
522
|
+
return schema.types.includes(object.type) && typeof value === "number" && value > 0;
|
|
523
|
+
});
|
|
524
|
+
}
|
|
286
525
|
/** The properties every module object takes, whatever its module. */
|
|
287
526
|
const MODULE_OBJECT_FIELDS = new Set(["id", "type", "module", "kind", "options", "name", "tags", "data"]);
|
|
288
527
|
/** Checks a module object, already checked as a `module`, against its module's schema. */
|
|
@@ -437,7 +676,7 @@ function parseDesignFile(json) {
|
|
|
437
676
|
}
|
|
438
677
|
// Every version is current, so none is migrated: version 1 is a design
|
|
439
678
|
// without pages, version 2 one divided into pages, and version 3 either,
|
|
440
|
-
// holding objects of an optional module.
|
|
679
|
+
// holding objects of an optional module or properties of a decoration.
|
|
441
680
|
for (const field of Object.keys(json)) {
|
|
442
681
|
if (!DESIGN_FILE_FIELDS[version].has(field)) {
|
|
443
682
|
const hint = field === "pages" ? ` A design with pages has "version" 2.` : field === "objects" ? ` In version 2, objects belong to "pages".` : "";
|
|
@@ -459,6 +698,12 @@ function parseDesignFile(json) {
|
|
|
459
698
|
if (object.type === "module" && version < 3) {
|
|
460
699
|
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
700
|
}
|
|
701
|
+
if (version < 3) {
|
|
702
|
+
const decorated = findDecorationProperty(object);
|
|
703
|
+
if (decorated !== null) {
|
|
704
|
+
throw fail(`${at} sets "${decorated}", which only a design with "version" 3 holds, so that an older MinimoJS refuses the design by its version.`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
462
707
|
const id = object.id.toLowerCase();
|
|
463
708
|
const first = whereById.get(id);
|
|
464
709
|
if (first !== undefined) {
|
|
@@ -573,7 +818,8 @@ export class Design {
|
|
|
573
818
|
* a `data` that is not a JSON object, a page without a UUID `id` or with
|
|
574
819
|
* a missing, padded, or repeated `name`, and a `module` object in a design
|
|
575
820
|
* older than version 3 or with a module, kind, property, or option that
|
|
576
|
-
* {@link designModules} does not list
|
|
821
|
+
* {@link designModules} does not list, and a property of a decoration, such
|
|
822
|
+
* as `outlineWidth`, in a design older than version 3.
|
|
577
823
|
*
|
|
578
824
|
* @param json - The content of a `*.design.json` file. See {@link DesignFile}.
|
|
579
825
|
*/
|
|
@@ -641,6 +887,13 @@ export class Design {
|
|
|
641
887
|
return this.objects.filter((object) => object.tags?.includes(tag) === true);
|
|
642
888
|
}
|
|
643
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
|
+
}
|
|
644
897
|
function isPlainObject(value) {
|
|
645
898
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
646
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() {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Game, Sprite } from "./minimo.js";
|
|
2
|
+
/** How {@link Outline.set} outlines a sprite. */
|
|
3
|
+
export interface OutlineOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Width of the outline in the sprite's own pixels, before
|
|
6
|
+
* {@link Sprite.scale}, so it grows and shrinks with the sprite. `0`
|
|
7
|
+
* removes the outline. A width above {@link Outline.MAX_WIDTH} draws that
|
|
8
|
+
* width.
|
|
9
|
+
*/
|
|
10
|
+
width: number;
|
|
11
|
+
/**
|
|
12
|
+
* CSS color of the outline, such as `"#fff5df"`, which may be translucent,
|
|
13
|
+
* such as `"rgba(0, 0, 0, 0.5)"`. Default `"#ffffff"`.
|
|
14
|
+
*/
|
|
15
|
+
color?: string;
|
|
16
|
+
}
|
|
17
|
+
/** The outline a sprite has, as {@link Outline.get} reports it. */
|
|
18
|
+
export interface OutlineStyle {
|
|
19
|
+
/** Width in the sprite's own pixels, before scale. */
|
|
20
|
+
readonly width: number;
|
|
21
|
+
/** CSS color. */
|
|
22
|
+
readonly color: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Draws a solid outline around the visible shape of image, emoji, and text
|
|
26
|
+
* sprites, so a character or an answer stands out from what is behind it.
|
|
27
|
+
*
|
|
28
|
+
* The outline follows the shape's transparent edges, not its box, and is
|
|
29
|
+
* part of the sprite's own picture: it moves, turns, flips, scales, fades,
|
|
30
|
+
* and changes layer with the sprite, and it shows in trails, wipes, and
|
|
31
|
+
* explosions of it. Only what is drawn grows. The sprite's size, collision
|
|
32
|
+
* body, the area that takes taps, and its pivot stay as they were.
|
|
33
|
+
*
|
|
34
|
+
* Each outline is built once and kept, so moving or turning a sprite costs
|
|
35
|
+
* nothing more. A new texture, new text, or a new width or color builds it
|
|
36
|
+
* again. Sprites that look the same share one outline.
|
|
37
|
+
*
|
|
38
|
+
* {@link DrawSprite}s, and the sprites made from it such as a flame or a die,
|
|
39
|
+
* are not outlined: they are redrawn every frame, which would rebuild the
|
|
40
|
+
* outline every frame.
|
|
41
|
+
*
|
|
42
|
+
* Create the module in the scene's `onCreate()`. It needs no `update()`.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { Outline } from "./minimo-outline.js";
|
|
47
|
+
*
|
|
48
|
+
* onCreate() {
|
|
49
|
+
* const outlines = new Outline(game);
|
|
50
|
+
* const frog = game.add(new ImageSprite(game, "frog", 360, 640));
|
|
51
|
+
* outlines.set(frog, { width: 4, color: "#fff5df" });
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* ## In a design
|
|
56
|
+
*
|
|
57
|
+
* An `image`, `emoji`, or `text` object in a design takes an outline through
|
|
58
|
+
* two properties, which Studio shows among the object's properties:
|
|
59
|
+
*
|
|
60
|
+
* - `outlineWidth`: the width in the object's own pixels. `0`, the default,
|
|
61
|
+
* draws none.
|
|
62
|
+
* - `outlineColor`: its CSS color. Default `"#ffffff"`.
|
|
63
|
+
*
|
|
64
|
+
* ```json
|
|
65
|
+
* { "id": "…", "type": "image", "name": "frog", "imageKey": "frog",
|
|
66
|
+
* "x": 360, "y": 640, "outlineWidth": 4, "outlineColor": "#fff5df" }
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* A design holding either property has `"version": 3`. The outline is a look
|
|
70
|
+
* of the object, not an object of its own: there is no `module` object for it.
|
|
71
|
+
* Create the module before the design's objects, or creating an object whose
|
|
72
|
+
* `outlineWidth` is above `0` throws an error naming it:
|
|
73
|
+
*
|
|
74
|
+
* ```ts
|
|
75
|
+
* onCreate() {
|
|
76
|
+
* this.outlines = new Outline(game);
|
|
77
|
+
* const screen = game.createObjectList(menu.objects);
|
|
78
|
+
* const frog = screen.findByName("frog");
|
|
79
|
+
* // Change it while the game plays, such as on the chosen answer:
|
|
80
|
+
* this.outlines.set(frog, { width: 6, color: "#ffd84a" });
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare class Outline {
|
|
85
|
+
/** The widest outline drawn, in the sprite's own pixels. */
|
|
86
|
+
static readonly MAX_WIDTH = 64;
|
|
87
|
+
/** The game whose sprites it outlines. */
|
|
88
|
+
readonly game: Game;
|
|
89
|
+
constructor(game: Game);
|
|
90
|
+
/**
|
|
91
|
+
* Outlines `sprite`, or changes its outline. A width of `0` removes it.
|
|
92
|
+
* @param sprite - An {@link ImageSprite}, {@link EmojiSprite}, or {@link TextSprite}, or a subclass of one.
|
|
93
|
+
* @param options - The width, and the color, white by default.
|
|
94
|
+
*/
|
|
95
|
+
set(sprite: Sprite, options: OutlineOptions): void;
|
|
96
|
+
/**
|
|
97
|
+
* The outline `sprite` has, or `null` when it has none.
|
|
98
|
+
* @param sprite - Any sprite.
|
|
99
|
+
*/
|
|
100
|
+
get(sprite: Sprite): OutlineStyle | null;
|
|
101
|
+
/**
|
|
102
|
+
* Removes the outline of `sprite`. Nothing happens when it has none.
|
|
103
|
+
* @param sprite - Any sprite.
|
|
104
|
+
*/
|
|
105
|
+
remove(sprite: Sprite): void;
|
|
106
|
+
}
|