@datamoc/mw_games 0.41.0 → 0.43.0
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/CHANGELOG.md +105 -0
- package/README.md +62 -1
- package/REFERENCE.md +73 -8
- package/api-surface.json +696 -0
- package/data/microgm-essential.PROVENANCE.md +13 -0
- package/data/microgm-essential.sf2 +0 -0
- package/data/soundfont-catalog.json +8 -0
- package/dist/audio/AudioLoader.d.ts +28 -0
- package/dist/audio/AudioLoader.d.ts.map +1 -0
- package/dist/audio/AudioLoader.js +53 -0
- package/dist/audio/AudioLoader.js.map +1 -0
- package/dist/audio/Channels.d.ts +1 -0
- package/dist/audio/Channels.d.ts.map +1 -1
- package/dist/audio/Channels.js +2 -1
- package/dist/audio/Channels.js.map +1 -1
- package/dist/audio/MidiRender.d.ts +19 -1
- package/dist/audio/MidiRender.d.ts.map +1 -1
- package/dist/audio/MidiRender.js +24 -1
- package/dist/audio/MidiRender.js.map +1 -1
- package/dist/audio/MusicPlayer.d.ts +80 -0
- package/dist/audio/MusicPlayer.d.ts.map +1 -0
- package/dist/audio/MusicPlayer.js +147 -0
- package/dist/audio/MusicPlayer.js.map +1 -0
- package/dist/audio/SampleDecoder.d.ts +16 -2
- package/dist/audio/SampleDecoder.d.ts.map +1 -1
- package/dist/audio/SampleDecoder.js +10 -6
- package/dist/audio/SampleDecoder.js.map +1 -1
- package/dist/audio/Sound.d.ts +2 -0
- package/dist/audio/Sound.d.ts.map +1 -1
- package/dist/audio/Sound.js +3 -0
- package/dist/audio/Sound.js.map +1 -1
- package/dist/audio/SoundFont.d.ts +40 -5
- package/dist/audio/SoundFont.d.ts.map +1 -1
- package/dist/audio/SoundFont.js +52 -14
- package/dist/audio/SoundFont.js.map +1 -1
- package/dist/audio/Synth.d.ts +1 -1
- package/dist/audio/Synth.d.ts.map +1 -1
- package/dist/audio/Synth.js +4 -0
- package/dist/audio/Synth.js.map +1 -1
- package/dist/audio/index.d.ts +8 -4
- package/dist/audio/index.d.ts.map +1 -1
- package/dist/audio/index.js +4 -2
- package/dist/audio/index.js.map +1 -1
- package/dist/core/DialogueText.d.ts +25 -0
- package/dist/core/DialogueText.d.ts.map +1 -1
- package/dist/core/DialogueText.js +25 -0
- package/dist/core/DialogueText.js.map +1 -1
- package/dist/core/Motion.d.ts +20 -0
- package/dist/core/Motion.d.ts.map +1 -1
- package/dist/core/Motion.js +20 -0
- package/dist/core/Motion.js.map +1 -1
- package/dist/core/Save.d.ts +31 -0
- package/dist/core/Save.d.ts.map +1 -1
- package/dist/core/Save.js +31 -0
- package/dist/core/Save.js.map +1 -1
- package/dist/core/Twee.d.ts +34 -0
- package/dist/core/Twee.d.ts.map +1 -1
- package/dist/core/Twee.js +34 -0
- package/dist/core/Twee.js.map +1 -1
- package/dist/mw_games.global.js +22 -22
- package/dist/mwl/index.d.ts +2 -0
- package/dist/mwl/index.d.ts.map +1 -1
- package/dist/mwl/index.js +1 -0
- package/dist/mwl/index.js.map +1 -1
- package/dist/mwl/objects.d.ts +31 -0
- package/dist/mwl/objects.d.ts.map +1 -0
- package/dist/mwl/objects.js +83 -0
- package/dist/mwl/objects.js.map +1 -0
- package/dist/mwl/schema.d.ts +1 -1
- package/dist/mwl/schema.d.ts.map +1 -1
- package/dist/mwl/tables.d.ts +2 -0
- package/dist/mwl/tables.d.ts.map +1 -1
- package/dist/mwl/tables.js +13 -0
- package/dist/mwl/tables.js.map +1 -1
- package/dist/physics/XpbdWorld.d.ts +68 -5
- package/dist/physics/XpbdWorld.d.ts.map +1 -1
- package/dist/physics/XpbdWorld.js +135 -6
- package/dist/physics/XpbdWorld.js.map +1 -1
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +21 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/three-d/Collision3D.d.ts +46 -2
- package/dist/three-d/Collision3D.d.ts.map +1 -1
- package/dist/three-d/Collision3D.js +46 -2
- package/dist/three-d/Collision3D.js.map +1 -1
- package/dist/three-d/Grid.d.ts +12 -1
- package/dist/three-d/Grid.d.ts.map +1 -1
- package/dist/three-d/Grid.js +12 -1
- package/dist/three-d/Grid.js.map +1 -1
- package/dist/three-d/Vox.d.ts +13 -1
- package/dist/three-d/Vox.d.ts.map +1 -1
- package/dist/three-d/Vox.js +13 -1
- package/dist/three-d/Vox.js.map +1 -1
- package/dist/two-d/Game.d.ts +21 -1
- package/dist/two-d/Game.d.ts.map +1 -1
- package/dist/two-d/Game.js +73 -6
- package/dist/two-d/Game.js.map +1 -1
- package/dist/two-d/Scene2D.d.ts +24 -0
- package/dist/two-d/Scene2D.d.ts.map +1 -1
- package/dist/two-d/Scene2D.js +30 -0
- package/dist/two-d/Scene2D.js.map +1 -1
- package/dist/two-d/render/SceneTransition.d.ts +272 -0
- package/dist/two-d/render/SceneTransition.d.ts.map +1 -0
- package/dist/two-d/render/SceneTransition.js +431 -0
- package/dist/two-d/render/SceneTransition.js.map +1 -0
- package/dist/two-d/render/index.d.ts +2 -0
- package/dist/two-d/render/index.d.ts.map +1 -1
- package/dist/two-d/render/index.js +1 -0
- package/dist/two-d/render/index.js.map +1 -1
- package/dist/two-d/ui/Button.d.ts +23 -0
- package/dist/two-d/ui/Button.d.ts.map +1 -1
- package/dist/two-d/ui/Button.js +37 -0
- package/dist/two-d/ui/Button.js.map +1 -1
- package/dist/two-d/ui/DropdownView.d.ts +49 -3
- package/dist/two-d/ui/DropdownView.d.ts.map +1 -1
- package/dist/two-d/ui/DropdownView.js +150 -9
- package/dist/two-d/ui/DropdownView.js.map +1 -1
- package/dist/two-d/ui/ScrollBox.d.ts +90 -0
- package/dist/two-d/ui/ScrollBox.d.ts.map +1 -1
- package/dist/two-d/ui/ScrollBox.js +206 -4
- package/dist/two-d/ui/ScrollBox.js.map +1 -1
- package/dist/two-d/ui/ScrollText.d.ts +122 -0
- package/dist/two-d/ui/ScrollText.d.ts.map +1 -0
- package/dist/two-d/ui/ScrollText.js +202 -0
- package/dist/two-d/ui/ScrollText.js.map +1 -0
- package/dist/two-d/ui/Tooltip.d.ts +70 -1
- package/dist/two-d/ui/Tooltip.d.ts.map +1 -1
- package/dist/two-d/ui/Tooltip.js +115 -3
- package/dist/two-d/ui/Tooltip.js.map +1 -1
- package/dist/two-d/ui/Window.d.ts +35 -2
- package/dist/two-d/ui/Window.d.ts.map +1 -1
- package/dist/two-d/ui/Window.js +58 -3
- package/dist/two-d/ui/Window.js.map +1 -1
- package/dist/two-d/ui/index.d.ts +7 -5
- package/dist/two-d/ui/index.d.ts.map +1 -1
- package/dist/two-d/ui/index.js +3 -2
- package/dist/two-d/ui/index.js.map +1 -1
- package/dist/two-d/ui/theme.d.ts +18 -0
- package/dist/two-d/ui/theme.d.ts.map +1 -1
- package/dist/two-d/ui/theme.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -3
- package/sbom.cdx.json +462 -243
- package/tools/browser-driver.mjs +3 -3
- package/tools/browser-smoke.mjs +19 -2
- package/tools/build-tablebases.mjs +7 -3
- package/tools/compile-openings.mjs +7 -3
- package/tools/compile-resources.d.mts +8 -0
- package/tools/compile-resources.mjs +62 -14
- package/tools/emit-page.d.mts +9 -0
- package/tools/emit-page.mjs +57 -4
- package/tools/find-chrome.mjs +4 -2
- package/tools/init.mjs +11 -3
- package/tools/play-uci.mjs +9 -5
- package/tools/svg-optimise.d.mts +8 -0
- package/tools/svg-optimise.mjs +45 -0
- package/tools/vite.mjs +10 -1
- package/tools.md +5 -2
package/tools/browser-driver.mjs
CHANGED
|
@@ -361,12 +361,12 @@ function findOnPath(names) {
|
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
/**
|
|
364
|
-
* Locates a browser executable without a driver install:
|
|
365
|
-
* `FIREFOX_PATH`
|
|
364
|
+
* Locates a browser executable without a driver install: `CHROME_PATH` (or `CHROME`) /
|
|
365
|
+
* `FIREFOX_PATH` overrides first, then well-known install locations. Returns null
|
|
366
366
|
* when neither yields anything, so callers skip rather than fail.
|
|
367
367
|
*/
|
|
368
368
|
export function findBrowser(kind) {
|
|
369
|
-
const override =
|
|
369
|
+
const override = kind === 'chrome' ? process.env.CHROME_PATH || process.env.CHROME : process.env.FIREFOX_PATH;
|
|
370
370
|
if (override) return override;
|
|
371
371
|
const onPath = findOnPath(kind === 'chrome' ? ['chrome', 'chromium', 'google-chrome'] : ['firefox']);
|
|
372
372
|
if (onPath) return onPath;
|
package/tools/browser-smoke.mjs
CHANGED
|
@@ -29,6 +29,23 @@ export async function waitForGame(page, url, pageErrors = []) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Waits for the page to draw a few frames instead of sleeping a fixed time: a software renderer can need
|
|
34
|
+
* far longer than any fixed pause, and a fast one far less. Capped, since a hidden tab throttles frames.
|
|
35
|
+
*/
|
|
36
|
+
async function settle(page, frames = 3, capMs = 5000) {
|
|
37
|
+
await page.evaluate(
|
|
38
|
+
([count, cap]) =>
|
|
39
|
+
new Promise((done) => {
|
|
40
|
+
const timer = setTimeout(done, cap);
|
|
41
|
+
const next = (left) =>
|
|
42
|
+
left === 0 ? (clearTimeout(timer), done()) : requestAnimationFrame(() => next(left - 1));
|
|
43
|
+
next(count);
|
|
44
|
+
}),
|
|
45
|
+
[frames, capMs],
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
/**
|
|
33
50
|
* Opens a built page from `file://` in headless Chrome and asserts it actually rendered.
|
|
34
51
|
*
|
|
@@ -76,11 +93,11 @@ export async function smokePage({
|
|
|
76
93
|
await waitForGame(page, url, pageErrors);
|
|
77
94
|
|
|
78
95
|
//let the scene finish its first frames before reading pixels back
|
|
79
|
-
await page
|
|
96
|
+
await settle(page);
|
|
80
97
|
if (keyToPress) {
|
|
81
98
|
await page.keyboard.press(keyToPress);
|
|
82
99
|
//give whatever the key opened time to lay itself out and draw
|
|
83
|
-
await page
|
|
100
|
+
await settle(page);
|
|
84
101
|
}
|
|
85
102
|
if (after) await after(page);
|
|
86
103
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join, relative } from 'node:path';
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
//the framework's own modules: the TypeScript sources inside this repository, the built `dist` inside an
|
|
7
|
+
//installed package, which ships no `src`
|
|
8
|
+
const fromSource = existsSync(new URL('../src/board/tablebase.ts', import.meta.url));
|
|
9
|
+
const load = (path) => import(fromSource ? `../src/${path}.ts` : `../dist/${path}.js`);
|
|
10
|
+
const { huffEncode } = await load('board/tablebase');
|
|
11
|
+
const { spawn } = await load('threads/spawn');
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* Generates pawnless endgame tablebases (WDL plus distance-to-mate) and ships
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
5
5
|
import { join, relative } from 'node:path';
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
//the framework's own modules: the TypeScript sources inside this repository, the built `dist` inside an
|
|
7
|
+
//installed package, which ships no `src`
|
|
8
|
+
const fromSource = existsSync(new URL('../src/board/chess.ts', import.meta.url));
|
|
9
|
+
const load = (path) => import(fromSource ? `../src/${path}.ts` : `../dist/${path}.js`);
|
|
10
|
+
const { applyMove, legalMoves, positionKey, sq, squareName, startingChess } = await load('board/chess');
|
|
11
|
+
const { parseUciMove } = await load('board/openings');
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* Compiles opening tables (`eco`, `name`, `pgn` TSV rows, one per line after the header)
|
|
@@ -12,6 +12,8 @@ export interface CompileResourcesOptions {
|
|
|
12
12
|
toWebp?: boolean;
|
|
13
13
|
webpLossless?: boolean;
|
|
14
14
|
webpQuality?: number;
|
|
15
|
+
/** minify embedded `.svg` assets through the optional `svgo` package (version 4 or newer); false by default */
|
|
16
|
+
svgo?: boolean;
|
|
15
17
|
/** Subset an embedded `.sf2` to the programs the embedded `.mid`/`.midi` files use. Default true; `false` embeds every SoundFont whole. */
|
|
16
18
|
soundFont?: boolean;
|
|
17
19
|
/** Explicit `{ bank, program }` list to subset a SoundFont to, overriding the MIDI-derived list. */
|
|
@@ -26,9 +28,13 @@ export interface CompileResourcesGroup {
|
|
|
26
28
|
|
|
27
29
|
export interface CompileResourcesResult {
|
|
28
30
|
groups: CompileResourcesGroup[];
|
|
31
|
+
/** the asset path of every embedded file, as game code passes it to `load()` */
|
|
32
|
+
keys: string[];
|
|
29
33
|
rawBytes: number;
|
|
30
34
|
embeddedBytes: number;
|
|
31
35
|
webpConverted: number;
|
|
36
|
+
/** SVG assets embedded smaller than their source because of `svgo` */
|
|
37
|
+
svgOptimised: number;
|
|
32
38
|
skipped: number;
|
|
33
39
|
/** the `CREDITS.txt` text when the asset folder holds a `credits.json`, else null */
|
|
34
40
|
credits: string | null;
|
|
@@ -50,6 +56,8 @@ export interface SoundFontEmbed {
|
|
|
50
56
|
programs: number | null;
|
|
51
57
|
rawBytes: number;
|
|
52
58
|
embeddedBytes: number;
|
|
59
|
+
/** the programs the game's MIDI files ask for that this font does not carry; empty when none */
|
|
60
|
+
missing: SoundFontUsage[];
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
/** parses a `--soundfont-usage=bank:program,...` CLI value; undefined when the flag is absent */
|
|
@@ -121,6 +121,10 @@ export async function* walk(dir) {
|
|
|
121
121
|
* pixel-art tileset's exact colours survive; set false only with `webpQuality` to opt into
|
|
122
122
|
* lossy re-encoding for photographic source art)
|
|
123
123
|
* @param options.webpQuality 0-100, used only when `webpLossless` is false (default 90)
|
|
124
|
+
* @param options.svgo minify embedded `.svg` assets through the optional `svgo` package (default
|
|
125
|
+
* false - see `tools/svg-optimise.mjs`). The key never changes, only the embedded bytes, and
|
|
126
|
+
* only when the optimised file is actually smaller. A file svgo cannot parse fails the build by
|
|
127
|
+
* name rather than shipping unoptimised in silence.
|
|
124
128
|
* @param options.soundFont subset an embedded `.sf2` or `.sf3` to the programs the embedded
|
|
125
129
|
* `.mid`/`.midi` files use (default true); `false` embeds every SoundFont whole
|
|
126
130
|
* @param options.soundFontUsage explicit `{ bank, program }` list to subset to, for a
|
|
@@ -141,6 +145,7 @@ export async function compileResources({
|
|
|
141
145
|
toWebp: convertToWebp = false,
|
|
142
146
|
webpLossless = true,
|
|
143
147
|
webpQuality = 90,
|
|
148
|
+
svgo: optimiseSvgFiles = false,
|
|
144
149
|
soundFont = true,
|
|
145
150
|
soundFontUsage,
|
|
146
151
|
} = {}) {
|
|
@@ -205,13 +210,16 @@ export async function compileResources({
|
|
|
205
210
|
let rawBytes = 0;
|
|
206
211
|
let embeddedBytes = 0;
|
|
207
212
|
let webpConverted = 0;
|
|
213
|
+
let svgOptimised = 0;
|
|
208
214
|
|
|
209
215
|
// only paid when toWebp is actually used, so a build that never asks for it never imports
|
|
210
216
|
// (or requires installing) sharp
|
|
211
217
|
const toWebp = convertToWebp ? (await import('./webp-convert.mjs')).toWebp : null;
|
|
218
|
+
const optimiseSvg = optimiseSvgFiles ? (await import('./svg-optimise.mjs')).optimiseSvg : null;
|
|
212
219
|
|
|
213
220
|
const creditsFile = join(from, 'credits.json');
|
|
214
221
|
let credits = null;
|
|
222
|
+
const keys = [];
|
|
215
223
|
|
|
216
224
|
for (const file of files) {
|
|
217
225
|
if (selected && !selected.has(keyOf(file))) continue;
|
|
@@ -240,6 +248,19 @@ export async function compileResources({
|
|
|
240
248
|
}
|
|
241
249
|
}
|
|
242
250
|
|
|
251
|
+
if (optimiseSvg && ext === '.svg') {
|
|
252
|
+
let optimised;
|
|
253
|
+
try {
|
|
254
|
+
optimised = await optimiseSvg(data);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
throw new Error(`cannot optimise SVG asset ${key}: ${error.message ?? error}`);
|
|
257
|
+
}
|
|
258
|
+
if (optimised.length < data.length) {
|
|
259
|
+
bytes = optimised;
|
|
260
|
+
svgOptimised++;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
243
264
|
if (isSoundFontKey(key)) {
|
|
244
265
|
if (usage !== undefined && soundFontTools) {
|
|
245
266
|
try {
|
|
@@ -247,7 +268,14 @@ export async function compileResources({
|
|
|
247
268
|
} catch (error) {
|
|
248
269
|
throw new Error(`cannot subset SoundFont asset ${key}: ${error.message ?? error}`);
|
|
249
270
|
}
|
|
250
|
-
soundFonts.push({
|
|
271
|
+
soundFonts.push({
|
|
272
|
+
key,
|
|
273
|
+
programs: usage.length,
|
|
274
|
+
rawBytes: data.length,
|
|
275
|
+
embeddedBytes: bytes.length,
|
|
276
|
+
//the programs the MIDI files ask for that this font does not carry
|
|
277
|
+
missing: soundFontTools.missingSoundFontPrograms(data, usage),
|
|
278
|
+
});
|
|
251
279
|
} else {
|
|
252
280
|
soundFonts.push({ key, programs: null, rawBytes: data.length, embeddedBytes: data.length });
|
|
253
281
|
}
|
|
@@ -256,6 +284,7 @@ export async function compileResources({
|
|
|
256
284
|
const name = group(key);
|
|
257
285
|
if (!groups.has(name)) groups.set(name, {});
|
|
258
286
|
groups.get(name)[key] = `data:${mimeType};base64,${bytes.toString('base64')}`;
|
|
287
|
+
keys.push(key);
|
|
259
288
|
rawBytes += data.length;
|
|
260
289
|
embeddedBytes += bytes.length;
|
|
261
290
|
}
|
|
@@ -273,7 +302,17 @@ export async function compileResources({
|
|
|
273
302
|
|
|
274
303
|
if (credits) await writeFile(join(to, 'CREDITS.txt'), credits, 'utf8');
|
|
275
304
|
|
|
276
|
-
return {
|
|
305
|
+
return {
|
|
306
|
+
groups: written,
|
|
307
|
+
keys,
|
|
308
|
+
rawBytes,
|
|
309
|
+
embeddedBytes,
|
|
310
|
+
webpConverted,
|
|
311
|
+
svgOptimised,
|
|
312
|
+
skipped,
|
|
313
|
+
credits,
|
|
314
|
+
soundFonts,
|
|
315
|
+
};
|
|
277
316
|
}
|
|
278
317
|
|
|
279
318
|
/** the plain-text credits for a manifest, one block per entry; throws on an entry that cannot be attributed */
|
|
@@ -302,7 +341,7 @@ async function runCli() {
|
|
|
302
341
|
const [from, to] = args.filter((a) => !a.startsWith('--'));
|
|
303
342
|
if (!from || !to) {
|
|
304
343
|
console.error(
|
|
305
|
-
'usage: node compile-resources.mjs <asset folder> <output folder> [--to-webp] [--webp-lossy[=quality]] [--no-soundfont-subset] [--soundfont-usage=bank:program,...]',
|
|
344
|
+
'usage: node compile-resources.mjs <asset folder> <output folder> [--to-webp] [--webp-lossy[=quality]] [--svgo] [--no-soundfont-subset] [--soundfont-usage=bank:program,...]',
|
|
306
345
|
);
|
|
307
346
|
process.exit(1);
|
|
308
347
|
}
|
|
@@ -315,17 +354,19 @@ async function runCli() {
|
|
|
315
354
|
args.find((a) => a.startsWith('--soundfont-usage='))?.slice('--soundfont-usage='.length),
|
|
316
355
|
);
|
|
317
356
|
|
|
318
|
-
const { groups, rawBytes, embeddedBytes, webpConverted, skipped, soundFonts } =
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
357
|
+
const { groups, rawBytes, embeddedBytes, webpConverted, svgOptimised, skipped, soundFonts } =
|
|
358
|
+
await compileResources({
|
|
359
|
+
from,
|
|
360
|
+
to,
|
|
361
|
+
//a flat folder has no subdirectories to group by, so everything lands in one file
|
|
362
|
+
groupBy: (key) => (key.includes('/') ? key.slice(0, key.indexOf('/')) : 'assets'),
|
|
363
|
+
toWebp,
|
|
364
|
+
webpLossless,
|
|
365
|
+
webpQuality,
|
|
366
|
+
svgo: args.includes('--svgo'),
|
|
367
|
+
soundFont: !args.includes('--no-soundfont-subset'),
|
|
368
|
+
soundFontUsage,
|
|
369
|
+
});
|
|
329
370
|
|
|
330
371
|
const kb = (n) => (n / 1024).toFixed(1).padStart(8) + ' KB';
|
|
331
372
|
for (const { name, count, size } of groups) {
|
|
@@ -338,7 +379,14 @@ async function runCli() {
|
|
|
338
379
|
? ` ${font.key}: SoundFont embedded whole (${kb(font.rawBytes)}), no shipped MIDI to subset against`
|
|
339
380
|
: ` ${font.key}: SoundFont subset to ${font.programs} program(s), ${kb(font.rawBytes)} -> ${kb(font.embeddedBytes)}`,
|
|
340
381
|
);
|
|
382
|
+
if (font.missing?.length > 0) {
|
|
383
|
+
console.log(
|
|
384
|
+
` warning: ${font.key} does not carry ${font.missing.map(({ bank, program }) => `${bank}:${program}`).join(', ')}: ` +
|
|
385
|
+
"they play the font's piano, or the built-in synthesizer with `missingProgram: 'synth'`",
|
|
386
|
+
);
|
|
387
|
+
}
|
|
341
388
|
}
|
|
389
|
+
if (args.includes('--svgo')) console.log(` ${svgOptimised} SVG(s) optimised`);
|
|
342
390
|
if (toWebp) {
|
|
343
391
|
console.log(
|
|
344
392
|
` ${webpConverted} image(s) converted to WebP (${webpLossless ? 'lossless' : `lossy, quality ${webpQuality}`}), ` +
|
package/tools/emit-page.d.mts
CHANGED
|
@@ -20,12 +20,16 @@ export interface EmitPageOptions {
|
|
|
20
20
|
toWebp?: boolean;
|
|
21
21
|
webpLossless?: boolean;
|
|
22
22
|
webpQuality?: number;
|
|
23
|
+
/** minify embedded `.svg` assets through the optional `svgo` package; false by default */
|
|
24
|
+
svgo?: boolean;
|
|
23
25
|
/** Subset an embedded `.sf2` to the programs the embedded `.mid`/`.midi` files use. Default true; `false` embeds every SoundFont whole. */
|
|
24
26
|
soundFont?: boolean;
|
|
25
27
|
/** Explicit `{ bank, program }` list to subset a SoundFont to, overriding the MIDI-derived list. */
|
|
26
28
|
soundFontUsage?: readonly SoundFontUsage[];
|
|
27
29
|
/** the page's Content-Security-Policy: on by default, `false` to leave it out */
|
|
28
30
|
csp?: boolean | CspOptions;
|
|
31
|
+
/** fails the build when the page a player downloads (the standalone file, else the page and its scripts) is larger than this many bytes */
|
|
32
|
+
maxPageBytes?: number;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
export interface CspOptions {
|
|
@@ -42,10 +46,15 @@ export interface EmitPageResult {
|
|
|
42
46
|
rawBytes: number;
|
|
43
47
|
embeddedBytes: number;
|
|
44
48
|
webpConverted: number;
|
|
49
|
+
svgOptimised: number;
|
|
45
50
|
/** one entry per embedded `.sf2`: the programs kept (null when embedded whole) and the byte saving */
|
|
46
51
|
soundFonts: SoundFontEmbed[];
|
|
47
52
|
compressed: CompressRow[] | null;
|
|
48
53
|
single: SingleFileResult | null;
|
|
54
|
+
/** embedded assets whose path is never named in the game's script: a hint, since a path built at run time is not seen */
|
|
55
|
+
unusedAssets: string[];
|
|
56
|
+
/** bytes a player downloads to open the page */
|
|
57
|
+
pageBytes: number;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
/** finishes a vite build as a page that opens from `file://` */
|
package/tools/emit-page.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFile, writeFile, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { readFile, readdir, writeFile, rm, stat } from 'node:fs/promises';
|
|
3
3
|
import { realpathSync } from 'node:fs';
|
|
4
4
|
import { join, resolve as resolvePath } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -29,7 +29,7 @@ import { withContentSecurityPolicy } from './csp.mjs';
|
|
|
29
29
|
* usage: mwg-emit <project folder> [--dist=<dir>] [--assets=<dir>] [--no-csp] [--connect=<origin>]...
|
|
30
30
|
* [--no-compress] [--xz]
|
|
31
31
|
* [--single-file] [--single-file-compress[=level]]
|
|
32
|
-
* [--single-file-brotli[=quality]] [--to-webp] [--webp-lossy[=quality]]
|
|
32
|
+
* [--single-file-brotli[=quality]] [--to-webp] [--webp-lossy[=quality]] [--svgo]
|
|
33
33
|
* [--no-soundfont-subset] [--soundfont-usage=bank:program,...]
|
|
34
34
|
*
|
|
35
35
|
* `--dist` defaults to `<project folder>/dist` and `--assets` to `<project folder>/assets`; with
|
|
@@ -53,6 +53,9 @@ import { withContentSecurityPolicy } from './csp.mjs';
|
|
|
53
53
|
* `--to-webp` converts `.png`/`.jpg`/`.jpeg` example assets to WebP before embedding (see
|
|
54
54
|
* `tools/webp-convert.mjs`), lossless by default; `--webp-lossy[=quality]` opts into lossy
|
|
55
55
|
* re-encoding instead. Needs the optional `sharp` devDependency installed.
|
|
56
|
+
*
|
|
57
|
+
* `--svgo` minifies embedded `.svg` assets (see `tools/svg-optimise.mjs`), keeping the `viewBox` and
|
|
58
|
+
* every id. Needs the optional `svgo` package installed.
|
|
56
59
|
*/
|
|
57
60
|
|
|
58
61
|
/**
|
|
@@ -74,9 +77,11 @@ export async function emitPage({
|
|
|
74
77
|
toWebp = false,
|
|
75
78
|
webpLossless = true,
|
|
76
79
|
webpQuality = 90,
|
|
80
|
+
svgo = false,
|
|
77
81
|
soundFont = true,
|
|
78
82
|
soundFontUsage,
|
|
79
83
|
csp = true,
|
|
84
|
+
maxPageBytes,
|
|
80
85
|
}) {
|
|
81
86
|
if (!dist) throw new Error('emitPage needs the dist folder of a vite build');
|
|
82
87
|
if (!assets && assetFiles?.length) throw new Error(`unknown asset file ${assetFiles[0]}: no assets folder`);
|
|
@@ -89,10 +94,11 @@ export async function emitPage({
|
|
|
89
94
|
toWebp,
|
|
90
95
|
webpLossless,
|
|
91
96
|
webpQuality,
|
|
97
|
+
svgo,
|
|
92
98
|
soundFont,
|
|
93
99
|
soundFontUsage,
|
|
94
100
|
})
|
|
95
|
-
: { groups: [], rawBytes: 0, embeddedBytes: 0, webpConverted: 0, soundFonts: [] };
|
|
101
|
+
: { groups: [], keys: [], rawBytes: 0, embeddedBytes: 0, webpConverted: 0, svgOptimised: 0, soundFonts: [] };
|
|
96
102
|
|
|
97
103
|
const html = await readFile(join(dist, 'index.html'), 'utf8');
|
|
98
104
|
const classic = toClassicScript(html);
|
|
@@ -119,7 +125,38 @@ export async function emitPage({
|
|
|
119
125
|
const standalone = await readFile(single.path, 'utf8');
|
|
120
126
|
await writeFile(single.path, withContentSecurityPolicy(standalone, { ...cspOptions, scripts }), 'utf8');
|
|
121
127
|
}
|
|
122
|
-
|
|
128
|
+
const unusedAssets = await unreferencedAssets(dist, classic.src, compiled.keys);
|
|
129
|
+
const pageBytes = await pageWeight(dist, single, compiled.groups);
|
|
130
|
+
if (maxPageBytes !== undefined && pageBytes > maxPageBytes) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`the page weighs ${kb(pageBytes)}, over the ${kb(maxPageBytes)} budget (maxPageBytes)` +
|
|
133
|
+
(unusedAssets.length
|
|
134
|
+
? `; ${unusedAssets.length} embedded asset(s) are never named in the game code: ${unusedAssets.slice(0, 5).join(', ')}`
|
|
135
|
+
: ''),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return { dist, entry: classic.src, ...compiled, compressed, single, unusedAssets, pageBytes };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** the embedded assets whose path appears nowhere in the game's script: a hint (a path built at run time is not seen), never an error */
|
|
142
|
+
async function unreferencedAssets(dist, entry, keys) {
|
|
143
|
+
if (keys.length === 0) return [];
|
|
144
|
+
const code = await readFile(join(dist, entry), 'utf8').catch(() => '');
|
|
145
|
+
return keys.filter((key) => !code.includes(key)).sort();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** the bytes a player downloads to open the page: the single file when there is one, else the page, the game script and the asset scripts */
|
|
149
|
+
async function pageWeight(dist, single, groups) {
|
|
150
|
+
const files = single
|
|
151
|
+
? [single.path]
|
|
152
|
+
: [
|
|
153
|
+
join(dist, 'index.html'),
|
|
154
|
+
...(await readdir(dist)).filter((name) => name.endsWith('.js')).map((name) => join(dist, name)),
|
|
155
|
+
...groups.map((g) => join(dist, 'assets', `${g.name}.js`)),
|
|
156
|
+
];
|
|
157
|
+
let total = 0;
|
|
158
|
+
for (const file of files) total += (await stat(file)).size;
|
|
159
|
+
return total;
|
|
123
160
|
}
|
|
124
161
|
|
|
125
162
|
const kb = (n) => (n / 1024).toFixed(1) + ' KB';
|
|
@@ -162,8 +199,10 @@ async function main(argv) {
|
|
|
162
199
|
toWebp: argv.includes('--to-webp'),
|
|
163
200
|
webpLossless: !webpLossyFlag,
|
|
164
201
|
webpQuality: webpLossyFlag?.includes('=') ? Number(webpLossyFlag.split('=')[1]) : 90,
|
|
202
|
+
svgo: argv.includes('--svgo'),
|
|
165
203
|
soundFont: !argv.includes('--no-soundfont-subset'),
|
|
166
204
|
soundFontUsage,
|
|
205
|
+
maxPageBytes: value('max-page-bytes') ? Number(value('max-page-bytes')) : undefined,
|
|
167
206
|
csp: argv.includes('--no-csp')
|
|
168
207
|
? false
|
|
169
208
|
: { connect: argv.filter((a) => a.startsWith('--connect=')).map((a) => a.slice('--connect='.length)) },
|
|
@@ -179,12 +218,26 @@ async function main(argv) {
|
|
|
179
218
|
`${kb(result.rawBytes)} -> ${kb(result.embeddedBytes)} embedded`,
|
|
180
219
|
);
|
|
181
220
|
}
|
|
221
|
+
if (argv.includes('--svgo')) console.log(` ${result.svgOptimised} SVG(s) optimised`);
|
|
222
|
+
console.log(` page weight ${kb(result.pageBytes)}`);
|
|
223
|
+
if (result.unusedAssets.length > 0) {
|
|
224
|
+
console.log(
|
|
225
|
+
` warning: ${result.unusedAssets.length} embedded asset(s) are never named in the game code, ` +
|
|
226
|
+
`so they only add weight (a path built at run time is not seen): ${result.unusedAssets.join(', ')}`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
182
229
|
for (const font of result.soundFonts ?? []) {
|
|
183
230
|
console.log(
|
|
184
231
|
font.programs === null
|
|
185
232
|
? ` ${font.key}: SoundFont embedded whole (${kb(font.rawBytes)}), no shipped MIDI to subset against`
|
|
186
233
|
: ` ${font.key}: SoundFont subset to ${font.programs} program(s), ${kb(font.rawBytes)} -> ${kb(font.embeddedBytes)}`,
|
|
187
234
|
);
|
|
235
|
+
if (font.missing?.length > 0) {
|
|
236
|
+
console.log(
|
|
237
|
+
` warning: ${font.key} does not carry ${font.missing.map(({ bank, program }) => `${bank}:${program}`).join(', ')}: ` +
|
|
238
|
+
"they play the font's piano, or the built-in synthesizer with `missingProgram: 'synth'`",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
188
241
|
}
|
|
189
242
|
console.log(' open index.html directly - no server needed');
|
|
190
243
|
if (result.compressed) {
|
package/tools/find-chrome.mjs
CHANGED
|
@@ -17,9 +17,11 @@ async function playwright() {
|
|
|
17
17
|
/**
|
|
18
18
|
* The browser to drive: the platform's usual Chrome (or, on Windows, Edge), then a Chromium that
|
|
19
19
|
* Playwright itself installed (`PLAYWRIGHT_BROWSERS_PATH`, common in CI images and containers),
|
|
20
|
-
* then whatever is on PATH. `CHROME_PATH` overrides all of them
|
|
20
|
+
* then whatever is on PATH. `CHROME_PATH` or `CHROME` overrides all of them.
|
|
21
21
|
*/
|
|
22
22
|
export async function findChrome() {
|
|
23
|
+
const override = process.env.CHROME_PATH || process.env.CHROME;
|
|
24
|
+
if (override) return override;
|
|
23
25
|
const candidates =
|
|
24
26
|
process.platform === 'win32'
|
|
25
27
|
? [
|
|
@@ -93,7 +95,7 @@ async function playwrightChromium() {
|
|
|
93
95
|
* page setup and assertions; this is the launch they all had in common.
|
|
94
96
|
*/
|
|
95
97
|
export async function launchChrome({ argsEnv = 'MWG_BENCHMARK_CHROME_ARGS' } = {}) {
|
|
96
|
-
const executablePath =
|
|
98
|
+
const executablePath = await findChrome();
|
|
97
99
|
const extraArgs = (process.env[argsEnv] ?? '').split(' ').filter(Boolean);
|
|
98
100
|
const { chromium } = await playwright();
|
|
99
101
|
return chromium.launch({
|
package/tools/init.mjs
CHANGED
|
@@ -26,7 +26,13 @@ export function initFiles(name) {
|
|
|
26
26
|
private: true,
|
|
27
27
|
version: '0.0.0',
|
|
28
28
|
type: 'module',
|
|
29
|
-
scripts: {
|
|
29
|
+
scripts: {
|
|
30
|
+
dev: 'vite',
|
|
31
|
+
build: 'vite build',
|
|
32
|
+
check: 'tsc --noEmit',
|
|
33
|
+
test: 'node --test',
|
|
34
|
+
'hooks:install': 'git config core.hooksPath .githooks',
|
|
35
|
+
},
|
|
30
36
|
dependencies: { '@datamoc/mw_games': `^${PACKAGE_VERSION}`, 'pixi.js': '^8.20.1' },
|
|
31
37
|
devDependencies: { typescript: '^7.0.2', vite: '^8.2.2' },
|
|
32
38
|
},
|
|
@@ -101,7 +107,9 @@ export function initFiles(name) {
|
|
|
101
107
|
'',
|
|
102
108
|
].join('\n'),
|
|
103
109
|
'assets/.gitkeep': '',
|
|
104
|
-
'
|
|
110
|
+
'.gitignore': 'node_modules/\ndist/\n.opencode/\n.coord-session\n',
|
|
111
|
+
'.githooks/pre-commit': '#!/bin/sh\nset -e\nnpm run check\nnpm test\n',
|
|
112
|
+
'README.md': `# ${name}\n\n\`npm install\`, then \`npm run dev\` to play it while you edit, \`npm run build\` to write \`dist/\`.\nOpen \`dist/index.html\` (or \`dist/standalone.html\`) by double-clicking it: no server needed.\n\nPut images and sounds under \`assets/\`; the build compiles them into the page. An \`assets/credits.json\`\n(\`[{ "name": "...", "license": "...", "author": "...", "url": "..." }]\`) becomes \`dist/CREDITS.txt\`.\n\nAfter \`git init\`, run \`npm run hooks:install\` to enable the included pre-commit hook.\nIt runs \`npm run check\` and \`npm test\`; add your tests under \`tests/\`.\n`,
|
|
105
113
|
};
|
|
106
114
|
}
|
|
107
115
|
|
|
@@ -121,7 +129,7 @@ export function initProject(folder) {
|
|
|
121
129
|
)) {
|
|
122
130
|
const file = join(target, path);
|
|
123
131
|
mkdirSync(dirname(file), { recursive: true });
|
|
124
|
-
writeFileSync(file, content, 'utf8');
|
|
132
|
+
writeFileSync(file, content, { encoding: 'utf8', mode: path === '.githooks/pre-commit' ? 0o755 : 0o644 });
|
|
125
133
|
written.push(path);
|
|
126
134
|
}
|
|
127
135
|
return { folder: target, files: written };
|
package/tools/play-uci.mjs
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
4
4
|
import { join, extname } from 'node:path';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
//the framework's own modules: the TypeScript sources inside this repository, the built `dist` inside an
|
|
7
|
+
//installed package, which ships no `src`
|
|
8
|
+
const fromSource = existsSync(new URL('../src/board/chess.ts', import.meta.url));
|
|
9
|
+
const load = (path) => import(fromSource ? `../src/${path}.ts` : `../dist/${path}.js`);
|
|
10
|
+
const { applyMove, cloneChess, legalMoves, parseFen, squareName, startingChess } = await load('board/chess');
|
|
11
|
+
const { parseUciMove, probeBook } = await load('board/openings');
|
|
12
|
+
const { searchTourney } = await load('board/tourney');
|
|
13
|
+
const { loadTablebaseEnding, probeTablebase } = await load('board/tablebase');
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* A UCI adapter over the tournament engine: `position`/`go` on stdin, `bestmove`
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** the svgo configuration `optimiseSvg` runs */
|
|
2
|
+
export declare const SVGO_CONFIG: {
|
|
3
|
+
multipass: boolean;
|
|
4
|
+
plugins: { name: string; params: { overrides: Record<string, boolean> } }[];
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/** the SVG bytes minified; needs the optional `svgo` package, and throws for a file that is not valid SVG */
|
|
8
|
+
export declare function optimiseSvg(buffer: Buffer): Promise<Buffer>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minifies an SVG for embedding, the SVG half of what `webp-convert` is for raster images, kept as
|
|
3
|
+
* its own module so `tools/compile-resources.mjs` never has to import an optimiser itself: it
|
|
4
|
+
* dynamically imports `svgo`, which a game that opts in installs in its own project the way it does
|
|
5
|
+
* `sharp` for WebP (`npm install --save-dev svgo`). A game that never passes `svgo: true` carries
|
|
6
|
+
* no new dependency at all.
|
|
7
|
+
*
|
|
8
|
+
* Optimising must not change what the game can see or name. Ids are left alone (`cleanupIds` is off:
|
|
9
|
+
* a `<use href="#id">`, a gradient reference or a style selector inside the file, or an id the game
|
|
10
|
+
* looks up, would break if minified), and the `viewBox` stays because svgo 4's default preset no
|
|
11
|
+
* longer removes it (a game scales a texture by it), which is why this needs svgo 4 or newer.
|
|
12
|
+
* Everything else in the default preset applies: comments, metadata, editor data, default
|
|
13
|
+
* attributes, redundant groups, path precision.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** the svgo configuration `optimiseSvg` runs, exposed so a game can see exactly what it opts into */
|
|
17
|
+
export const SVGO_CONFIG = {
|
|
18
|
+
multipass: true,
|
|
19
|
+
plugins: [{ name: 'preset-default', params: { overrides: { cleanupIds: false } } }],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let svgoModule;
|
|
23
|
+
async function loadSvgo() {
|
|
24
|
+
if (svgoModule) return svgoModule;
|
|
25
|
+
try {
|
|
26
|
+
svgoModule = await import('svgo');
|
|
27
|
+
} catch (err) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'SVG optimisation needs the optional `svgo` package, which is not installed. ' +
|
|
30
|
+
'Run `npm install --save-dev svgo` in your project to use `svgo: true`, or leave ' +
|
|
31
|
+
'it off (the default) to embed SVG files as they are.',
|
|
32
|
+
{ cause: err },
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return svgoModule;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {Buffer} buffer the SVG file's bytes (UTF-8)
|
|
40
|
+
* @returns {Promise<Buffer>} the optimised SVG; svgo throws for a file that is not valid SVG
|
|
41
|
+
*/
|
|
42
|
+
export async function optimiseSvg(buffer) {
|
|
43
|
+
const { optimize } = await loadSvgo();
|
|
44
|
+
return Buffer.from(optimize(buffer.toString('utf8'), SVGO_CONFIG).data, 'utf8');
|
|
45
|
+
}
|
package/tools/vite.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { buildArtifactSbom, serialize, shippedFiles } from './sbom.mjs';
|
|
|
16
16
|
* affects the build only, so the dev server still serves the asset folder normally.
|
|
17
17
|
* An embedded `.sf2` or `.sf3` is subset at build time to the programs the embedded `.mid` files
|
|
18
18
|
* use (`soundFont: false` embeds it whole, `soundFontUsage` names the programs instead).
|
|
19
|
+
* `svgo: true` minifies embedded `.svg` assets (needs the optional `svgo` package).
|
|
19
20
|
* At the end of the build it runs `emitPage` over the output folder with the same
|
|
20
21
|
* options. A setting the game's own config gives explicitly still wins, since vite merges a
|
|
21
22
|
* plugin's `config` result under the user's.
|
|
@@ -79,7 +80,15 @@ export function mwgPage(options = {}) {
|
|
|
79
80
|
const { assets: _assets, sbom = true, ...emitOptions } = options;
|
|
80
81
|
const source = assets ? resolve(root, assets) : undefined;
|
|
81
82
|
const dist = resolve(root, outDir);
|
|
82
|
-
await emitPage({
|
|
83
|
+
const page = await emitPage({
|
|
84
|
+
...emitOptions,
|
|
85
|
+
dist,
|
|
86
|
+
assets: source && existsSync(source) ? source : undefined,
|
|
87
|
+
});
|
|
88
|
+
if (page.unusedAssets.length > 0)
|
|
89
|
+
console.warn(
|
|
90
|
+
`mwg-page: ${page.unusedAssets.length} embedded asset(s) are never named in the game code: ${page.unusedAssets.join(', ')}`,
|
|
91
|
+
);
|
|
83
92
|
if (sbom) {
|
|
84
93
|
const manifest = join(root, 'package.json');
|
|
85
94
|
const packageJson = existsSync(manifest)
|
package/tools.md
CHANGED
|
@@ -23,7 +23,8 @@ by double-clicking. Run from the repo root.
|
|
|
23
23
|
|
|
24
24
|
| tool | what it does | run |
|
|
25
25
|
| --- | --- | --- |
|
|
26
|
-
| `compile-resources` | Turns an asset folder into `data:` URI scripts under `window.__MWG_ASSETS__`, grouped by top-level folder. `toWebp: true` converts `.png`/`.jpg`/`.jpeg` to WebP first (lossless by default, only kept when actually smaller, key unchanged) via the optional `sharp` devDependency, dynamically imported only when used (item 350) | called by the example builds; `node tools/compile-resources.mjs <asset folder> <output folder> [--to-webp] [--webp-lossy[=quality]]` |
|
|
26
|
+
| `compile-resources` | Turns an asset folder into `data:` URI scripts under `window.__MWG_ASSETS__`, grouped by top-level folder. `toWebp: true` converts `.png`/`.jpg`/`.jpeg` to WebP first (lossless by default, only kept when actually smaller, key unchanged) via the optional `sharp` devDependency, dynamically imported only when used (item 350). `svgo: true` minifies `.svg` assets through the optional `svgo` package (version 4 or newer, ids and `viewBox` kept, only kept when smaller). Each subset SoundFont's result lists the programs the game's MIDI files ask for that the font does not carry, and `mwg-emit` warns about them | called by the example builds; `node tools/compile-resources.mjs <asset folder> <output folder> [--to-webp] [--webp-lossy[=quality]] [--svgo]` |
|
|
27
|
+
| `svg-optimise` | Minifies one SVG file's bytes through `svgo` with ids and the `viewBox` left alone; the standalone half of `compile-resources`'s `svgo` option (item 428). On the repository's own 30 SVGs: 13.6% smaller, worst rendered pixel difference 1 in 255 | `optimiseSvg` from `@datamoc/mw_games/tools/svg-optimise`, or `mwg-emit <folder> --svgo` |
|
|
27
28
|
| `webp-convert` | Converts one raster image buffer (PNG/JPEG) to WebP, lossless by default; the standalone half of `compile-resources`'s `toWebp` option (item 350) | `node tools/webp-convert.mjs <file.png\|file.jpg> [--lossy[=quality]]` |
|
|
28
29
|
| `emit-page` | Finishes a vite build so it opens from `file://` with no server: compiles the assets, rewrites the entry to a classic deferred script with the asset scripts before it, then optionally precompresses and writes a single-file variant (item 381) | `emitPage(options)` from `@datamoc/mw_games/tools/emit-page`, the `mwg-emit <folder> [--dist=] [--assets=]` command, or called by each `example:*:build` script here |
|
|
29
30
|
| `init` | `mwg-init <folder>` writes a starter project (vite config using `mwgPage`, one scene, tsconfig, scripts) that builds to a double-clickable page | `npx mwg-init my-game` |
|
|
@@ -199,7 +200,9 @@ same build.
|
|
|
199
200
|
| tool | what it does | run |
|
|
200
201
|
| --- | --- | --- |
|
|
201
202
|
| `roadmap-progress` | Reports CLOSED.md + ROADMAP.md completion: numbered items in batches of 25, plus the 1.0 exit checklist on its own row (checks to run, not capabilities, so they stay out of the numbered totals), optionally in a browser | `npm run roadmap:progress` |
|
|
202
|
-
| `build-webpage-examples` / `build-webpage-docs` | Generate the live examples and Documentation page under `webpage/` (`REFERENCE.md` guide + TypeDoc under `documentation/api/`) | `npm run webpage:examples`, `npm run webpage:docs` |
|
|
203
|
+
| `build-webpage-examples` / `build-webpage-docs` | Generate the live examples and Documentation page under `webpage/` (`REFERENCE.md` guide + TypeDoc under `documentation/api/` + the examples index) | `npm run webpage:examples`, `npm run webpage:docs` |
|
|
204
|
+
| `build-bridge-tutorial` / `bins-smoke` | `build-bridge-tutorial` writes `webpage/tutorials/bridge/index.html` from `tools/templates/bridge-tutorial.html`, quoting the code blocks of `examples/bridge` so a test can check them. `bins-smoke` packs the project, installs the tarball in an empty folder and runs every `mwg-*` bin with `--help` on the Node that runs it; CI runs it on Node 20.19.0. Repository tools, not shipped | `npm run build:bridge-tutorial`, `npm run bins:smoke` |
|
|
205
|
+
| `build-example-index` | Writes `documentation/examples.html`, read from source text (no typedoc, no renderer): for each public module the symbols that carry an `@example`, each linked to its typedoc page when one exists, with the runnable `examples/` that import the module. Part of `webpage:docs`; a repository tool, not shipped (item 430) | `npm run webpage:docs`, or `node tools/build-example-index.mjs [outFile]` |
|
|
203
206
|
| `make-example-diagrams` / `make-architecture-diagrams` | Rebuild the generated diagrams under `webpage/assets` (never hand-drawn) | `npm run webpage:diagrams` |
|
|
204
207
|
| `api-report` | Regenerates `API_REPORT.md` from the built declarations; `--check` compares instead. A CI gate | `npm run api:report`, `npm run api:check` |
|
|
205
208
|
| `project-stats` | Writes `PROJECT_STATS.json`/`PROJECT_STATS.md` and the website statistics page; `--check` compares. A CI gate | `npm run stats:write`, `npm run stats:check` |
|