@datamoc/mw_games 0.41.0 → 0.42.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/REFERENCE.md +16 -3
  3. package/api-surface.json +24 -0
  4. package/dist/audio/MidiRender.d.ts +3 -1
  5. package/dist/audio/MidiRender.d.ts.map +1 -1
  6. package/dist/audio/MidiRender.js +3 -1
  7. package/dist/audio/MidiRender.js.map +1 -1
  8. package/dist/audio/SoundFont.d.ts +35 -2
  9. package/dist/audio/SoundFont.d.ts.map +1 -1
  10. package/dist/audio/SoundFont.js +43 -7
  11. package/dist/audio/SoundFont.js.map +1 -1
  12. package/dist/audio/index.d.ts +2 -2
  13. package/dist/audio/index.d.ts.map +1 -1
  14. package/dist/audio/index.js +1 -1
  15. package/dist/audio/index.js.map +1 -1
  16. package/dist/core/DialogueText.d.ts +25 -0
  17. package/dist/core/DialogueText.d.ts.map +1 -1
  18. package/dist/core/DialogueText.js +25 -0
  19. package/dist/core/DialogueText.js.map +1 -1
  20. package/dist/core/Motion.d.ts +20 -0
  21. package/dist/core/Motion.d.ts.map +1 -1
  22. package/dist/core/Motion.js +20 -0
  23. package/dist/core/Motion.js.map +1 -1
  24. package/dist/core/Save.d.ts +31 -0
  25. package/dist/core/Save.d.ts.map +1 -1
  26. package/dist/core/Save.js +31 -0
  27. package/dist/core/Save.js.map +1 -1
  28. package/dist/core/Twee.d.ts +34 -0
  29. package/dist/core/Twee.d.ts.map +1 -1
  30. package/dist/core/Twee.js +34 -0
  31. package/dist/core/Twee.js.map +1 -1
  32. package/dist/mw_games.global.js +11 -11
  33. package/dist/physics/XpbdWorld.d.ts +45 -0
  34. package/dist/physics/XpbdWorld.d.ts.map +1 -1
  35. package/dist/physics/XpbdWorld.js +45 -0
  36. package/dist/physics/XpbdWorld.js.map +1 -1
  37. package/dist/two-d/ui/theme.d.ts +18 -0
  38. package/dist/two-d/ui/theme.d.ts.map +1 -1
  39. package/dist/two-d/ui/theme.js.map +1 -1
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/package.json +8 -1
  43. package/sbom.cdx.json +466 -6
  44. package/tools/compile-resources.d.mts +6 -0
  45. package/tools/compile-resources.mjs +50 -14
  46. package/tools/emit-page.d.mts +3 -0
  47. package/tools/emit-page.mjs +15 -2
  48. package/tools/svg-optimise.d.mts +8 -0
  49. package/tools/svg-optimise.mjs +45 -0
  50. package/tools/vite.mjs +1 -0
  51. package/tools.md +4 -2
@@ -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,10 +210,12 @@ 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;
@@ -240,6 +247,19 @@ export async function compileResources({
240
247
  }
241
248
  }
242
249
 
250
+ if (optimiseSvg && ext === '.svg') {
251
+ let optimised;
252
+ try {
253
+ optimised = await optimiseSvg(data);
254
+ } catch (error) {
255
+ throw new Error(`cannot optimise SVG asset ${key}: ${error.message ?? error}`);
256
+ }
257
+ if (optimised.length < data.length) {
258
+ bytes = optimised;
259
+ svgOptimised++;
260
+ }
261
+ }
262
+
243
263
  if (isSoundFontKey(key)) {
244
264
  if (usage !== undefined && soundFontTools) {
245
265
  try {
@@ -247,7 +267,14 @@ export async function compileResources({
247
267
  } catch (error) {
248
268
  throw new Error(`cannot subset SoundFont asset ${key}: ${error.message ?? error}`);
249
269
  }
250
- soundFonts.push({ key, programs: usage.length, rawBytes: data.length, embeddedBytes: bytes.length });
270
+ soundFonts.push({
271
+ key,
272
+ programs: usage.length,
273
+ rawBytes: data.length,
274
+ embeddedBytes: bytes.length,
275
+ //the programs the MIDI files ask for that this font does not carry
276
+ missing: soundFontTools.missingSoundFontPrograms(data, usage),
277
+ });
251
278
  } else {
252
279
  soundFonts.push({ key, programs: null, rawBytes: data.length, embeddedBytes: data.length });
253
280
  }
@@ -273,7 +300,7 @@ export async function compileResources({
273
300
 
274
301
  if (credits) await writeFile(join(to, 'CREDITS.txt'), credits, 'utf8');
275
302
 
276
- return { groups: written, rawBytes, embeddedBytes, webpConverted, skipped, credits, soundFonts };
303
+ return { groups: written, rawBytes, embeddedBytes, webpConverted, svgOptimised, skipped, credits, soundFonts };
277
304
  }
278
305
 
279
306
  /** the plain-text credits for a manifest, one block per entry; throws on an entry that cannot be attributed */
@@ -302,7 +329,7 @@ async function runCli() {
302
329
  const [from, to] = args.filter((a) => !a.startsWith('--'));
303
330
  if (!from || !to) {
304
331
  console.error(
305
- 'usage: node compile-resources.mjs <asset folder> <output folder> [--to-webp] [--webp-lossy[=quality]] [--no-soundfont-subset] [--soundfont-usage=bank:program,...]',
332
+ 'usage: node compile-resources.mjs <asset folder> <output folder> [--to-webp] [--webp-lossy[=quality]] [--svgo] [--no-soundfont-subset] [--soundfont-usage=bank:program,...]',
306
333
  );
307
334
  process.exit(1);
308
335
  }
@@ -315,17 +342,19 @@ async function runCli() {
315
342
  args.find((a) => a.startsWith('--soundfont-usage='))?.slice('--soundfont-usage='.length),
316
343
  );
317
344
 
318
- const { groups, rawBytes, embeddedBytes, webpConverted, skipped, soundFonts } = await compileResources({
319
- from,
320
- to,
321
- //a flat folder has no subdirectories to group by, so everything lands in one file
322
- groupBy: (key) => (key.includes('/') ? key.slice(0, key.indexOf('/')) : 'assets'),
323
- toWebp,
324
- webpLossless,
325
- webpQuality,
326
- soundFont: !args.includes('--no-soundfont-subset'),
327
- soundFontUsage,
328
- });
345
+ const { groups, rawBytes, embeddedBytes, webpConverted, svgOptimised, skipped, soundFonts } =
346
+ await compileResources({
347
+ from,
348
+ to,
349
+ //a flat folder has no subdirectories to group by, so everything lands in one file
350
+ groupBy: (key) => (key.includes('/') ? key.slice(0, key.indexOf('/')) : 'assets'),
351
+ toWebp,
352
+ webpLossless,
353
+ webpQuality,
354
+ svgo: args.includes('--svgo'),
355
+ soundFont: !args.includes('--no-soundfont-subset'),
356
+ soundFontUsage,
357
+ });
329
358
 
330
359
  const kb = (n) => (n / 1024).toFixed(1).padStart(8) + ' KB';
331
360
  for (const { name, count, size } of groups) {
@@ -338,7 +367,14 @@ async function runCli() {
338
367
  ? ` ${font.key}: SoundFont embedded whole (${kb(font.rawBytes)}), no shipped MIDI to subset against`
339
368
  : ` ${font.key}: SoundFont subset to ${font.programs} program(s), ${kb(font.rawBytes)} -> ${kb(font.embeddedBytes)}`,
340
369
  );
370
+ if (font.missing?.length > 0) {
371
+ console.log(
372
+ ` warning: ${font.key} does not carry ${font.missing.map(({ bank, program }) => `${bank}:${program}`).join(', ')}: ` +
373
+ "they play the font's piano, or the built-in synthesizer with `missingProgram: 'synth'`",
374
+ );
375
+ }
341
376
  }
377
+ if (args.includes('--svgo')) console.log(` ${svgOptimised} SVG(s) optimised`);
342
378
  if (toWebp) {
343
379
  console.log(
344
380
  ` ${webpConverted} image(s) converted to WebP (${webpLossless ? 'lossless' : `lossy, quality ${webpQuality}`}), ` +
@@ -20,6 +20,8 @@ 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. */
@@ -42,6 +44,7 @@ export interface EmitPageResult {
42
44
  rawBytes: number;
43
45
  embeddedBytes: number;
44
46
  webpConverted: number;
47
+ svgOptimised: number;
45
48
  /** one entry per embedded `.sf2`: the programs kept (null when embedded whole) and the byte saving */
46
49
  soundFonts: SoundFontEmbed[];
47
50
  compressed: CompressRow[] | null;
@@ -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,6 +77,7 @@ 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,
@@ -89,10 +93,11 @@ export async function emitPage({
89
93
  toWebp,
90
94
  webpLossless,
91
95
  webpQuality,
96
+ svgo,
92
97
  soundFont,
93
98
  soundFontUsage,
94
99
  })
95
- : { groups: [], rawBytes: 0, embeddedBytes: 0, webpConverted: 0, soundFonts: [] };
100
+ : { groups: [], rawBytes: 0, embeddedBytes: 0, webpConverted: 0, svgOptimised: 0, soundFonts: [] };
96
101
 
97
102
  const html = await readFile(join(dist, 'index.html'), 'utf8');
98
103
  const classic = toClassicScript(html);
@@ -162,6 +167,7 @@ async function main(argv) {
162
167
  toWebp: argv.includes('--to-webp'),
163
168
  webpLossless: !webpLossyFlag,
164
169
  webpQuality: webpLossyFlag?.includes('=') ? Number(webpLossyFlag.split('=')[1]) : 90,
170
+ svgo: argv.includes('--svgo'),
165
171
  soundFont: !argv.includes('--no-soundfont-subset'),
166
172
  soundFontUsage,
167
173
  csp: argv.includes('--no-csp')
@@ -179,12 +185,19 @@ async function main(argv) {
179
185
  `${kb(result.rawBytes)} -> ${kb(result.embeddedBytes)} embedded`,
180
186
  );
181
187
  }
188
+ if (argv.includes('--svgo')) console.log(` ${result.svgOptimised} SVG(s) optimised`);
182
189
  for (const font of result.soundFonts ?? []) {
183
190
  console.log(
184
191
  font.programs === null
185
192
  ? ` ${font.key}: SoundFont embedded whole (${kb(font.rawBytes)}), no shipped MIDI to subset against`
186
193
  : ` ${font.key}: SoundFont subset to ${font.programs} program(s), ${kb(font.rawBytes)} -> ${kb(font.embeddedBytes)}`,
187
194
  );
195
+ if (font.missing?.length > 0) {
196
+ console.log(
197
+ ` warning: ${font.key} does not carry ${font.missing.map(({ bank, program }) => `${bank}:${program}`).join(', ')}: ` +
198
+ "they play the font's piano, or the built-in synthesizer with `missingProgram: 'synth'`",
199
+ );
200
+ }
188
201
  }
189
202
  console.log(' open index.html directly - no server needed');
190
203
  if (result.compressed) {
@@ -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.
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,8 @@ 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-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
205
  | `make-example-diagrams` / `make-architecture-diagrams` | Rebuild the generated diagrams under `webpage/assets` (never hand-drawn) | `npm run webpage:diagrams` |
204
206
  | `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
207
  | `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` |