@datamoc/mw_games 0.40.0 → 0.41.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 (90) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/REFERENCE.md +2115 -0
  3. package/api-surface.json +48 -0
  4. package/data/microgm-opt.PROVENANCE.md +3 -0
  5. package/data/soundfont-catalog.json +183 -0
  6. package/dist/ai/index.d.ts +10 -0
  7. package/dist/ai/index.d.ts.map +1 -1
  8. package/dist/ai/index.js.map +1 -1
  9. package/dist/ai/lua.d.ts +6 -0
  10. package/dist/ai/lua.d.ts.map +1 -1
  11. package/dist/ai/lua.js.map +1 -1
  12. package/dist/audio/SampleDecoder.d.ts +41 -0
  13. package/dist/audio/SampleDecoder.d.ts.map +1 -0
  14. package/dist/audio/SampleDecoder.js +70 -0
  15. package/dist/audio/SampleDecoder.js.map +1 -0
  16. package/dist/audio/SoundFont.d.ts +59 -0
  17. package/dist/audio/SoundFont.d.ts.map +1 -1
  18. package/dist/audio/SoundFont.js +248 -2
  19. package/dist/audio/SoundFont.js.map +1 -1
  20. package/dist/audio/index.d.ts +3 -1
  21. package/dist/audio/index.d.ts.map +1 -1
  22. package/dist/audio/index.js +2 -1
  23. package/dist/audio/index.js.map +1 -1
  24. package/dist/mw_games.global.js +11 -11
  25. package/dist/mwl/presentation.d.ts +16 -0
  26. package/dist/mwl/presentation.d.ts.map +1 -1
  27. package/dist/mwl/presentation.js.map +1 -1
  28. package/dist/mwl/pyroeis-runtime.d.ts +7 -0
  29. package/dist/mwl/pyroeis-runtime.d.ts.map +1 -1
  30. package/dist/mwl/pyroeis-runtime.js.map +1 -1
  31. package/dist/mwl/pyroeis.d.ts +3 -0
  32. package/dist/mwl/pyroeis.d.ts.map +1 -1
  33. package/dist/mwl/pyroeis.js.map +1 -1
  34. package/dist/physics/PhysicsWorld.d.ts +3 -0
  35. package/dist/physics/PhysicsWorld.d.ts.map +1 -1
  36. package/dist/physics/PhysicsWorld.js.map +1 -1
  37. package/dist/physics/XpbdWorld.d.ts +11 -0
  38. package/dist/physics/XpbdWorld.d.ts.map +1 -1
  39. package/dist/physics/XpbdWorld.js.map +1 -1
  40. package/dist/physics/cloth/index.d.ts +20 -19
  41. package/dist/physics/cloth/index.d.ts.map +1 -1
  42. package/dist/physics/cloth/index.js +19 -0
  43. package/dist/physics/cloth/index.js.map +1 -1
  44. package/dist/physics/spring/index.d.ts +19 -18
  45. package/dist/physics/spring/index.d.ts.map +1 -1
  46. package/dist/physics/spring/index.js +23 -0
  47. package/dist/physics/spring/index.js.map +1 -1
  48. package/dist/three-d/Character3D.d.ts +8 -7
  49. package/dist/three-d/Character3D.d.ts.map +1 -1
  50. package/dist/three-d/Character3D.js +7 -0
  51. package/dist/three-d/Character3D.js.map +1 -1
  52. package/dist/three-d/Collision3D.d.ts +4 -0
  53. package/dist/three-d/Collision3D.d.ts.map +1 -1
  54. package/dist/three-d/Collision3D.js.map +1 -1
  55. package/dist/three-d/Engine3D.d.ts +2 -0
  56. package/dist/three-d/Engine3D.d.ts.map +1 -1
  57. package/dist/three-d/Engine3D.js.map +1 -1
  58. package/dist/three-d/Grid.d.ts +5 -0
  59. package/dist/three-d/Grid.d.ts.map +1 -1
  60. package/dist/three-d/Grid.js.map +1 -1
  61. package/dist/three-d/Heightmap.d.ts +3 -0
  62. package/dist/three-d/Heightmap.d.ts.map +1 -1
  63. package/dist/three-d/Heightmap.js.map +1 -1
  64. package/dist/three-d/TileGrid3D.d.ts +6 -0
  65. package/dist/three-d/TileGrid3D.d.ts.map +1 -1
  66. package/dist/three-d/TileGrid3D.js.map +1 -1
  67. package/dist/three-d/Vox.d.ts +7 -0
  68. package/dist/three-d/Vox.d.ts.map +1 -1
  69. package/dist/three-d/Vox.js.map +1 -1
  70. package/dist/two-d/presentation/MwlPresentation.d.ts +7 -0
  71. package/dist/two-d/presentation/MwlPresentation.d.ts.map +1 -1
  72. package/dist/two-d/presentation/MwlPresentation.js.map +1 -1
  73. package/dist/version.d.ts +1 -1
  74. package/dist/version.js +1 -1
  75. package/package.json +16 -1
  76. package/sbom.cdx.json +139 -139
  77. package/tools/benchmark-browser.mjs +6 -0
  78. package/tools/bundle-size.mjs +4 -0
  79. package/tools/compile-resources.mjs +3 -2
  80. package/tools/init.mjs +3 -1
  81. package/tools/make-example-assets.mjs +4 -0
  82. package/tools/multiplayer-server.mjs +4 -0
  83. package/tools/mwl.mjs +7 -0
  84. package/tools/play-uci.mjs +4 -0
  85. package/tools/sbom.mjs +4 -0
  86. package/tools/soundfonts.d.mts +79 -0
  87. package/tools/soundfonts.mjs +301 -0
  88. package/tools/verify-scenarios.mjs +6 -2
  89. package/tools/vite.mjs +1 -1
  90. package/tools.md +216 -0
package/tools.md ADDED
@@ -0,0 +1,216 @@
1
+ # Tools
2
+
3
+ Developer-side scripts in `tools/`. Nothing here reaches the player: these author
4
+ content, verify it, or package it, and the shipped game is still a folder that opens
5
+ by double-clicking. Run from the repo root.
6
+
7
+ ## Content and authoring
8
+
9
+ | tool | what it does | run |
10
+ | --- | --- | --- |
11
+ | `i18n-edit` | Split-screen terminal editor for `mwg/i18n` catalogs: reference language left, translation right, either side any language, per-string sound cues, JSON/FTL auto-detected, with a `--check` mode for CI. Panes preview markdown as bold/italic and flag placeholder drift (`{dmg:03d}` dropped or reshaped); `v` toggles raw source, `Ctrl+P` previews the row's cue through the OS player (`MWG_SFX_PLAYER` overrides) | `npx mwg-i18n <base> <target> [--check]` in a game (shipped, item 384), `npm run i18n:edit -- <base> <target>` here |
12
+ | `make-example-assets` | Generates `examples/assets` (tileset, sounds). Borrowed art stays out for licence reasons, so this draws everything; a game can prototype with the same set before its own art exists (item 389) | `npm run assets` here; `npx mwg-placeholder-assets <folder> [--force]` in a game (refuses to overwrite without `--force`), or `writePlaceholderAssets` from `@datamoc/mw_games/tools/make-example-assets` |
13
+ | `soundfonts` | Picks a SoundFont other than the bundled `microgm-opt` (WTFPL, 2.6 MB, the default). `list` prints the catalogue (`data/soundfont-catalog.json`, a licence-classified snapshot of the marmooo free-soundfonts collection: public domain, MIT and CC-BY fonts fetch freely, share-alike, copyleft and unclear ones need `--accept-licence`); `fetch <font>` downloads only the instruments the game's MIDI files use (`--midi`) or a list (`--programs 0,24,128:0`), and merges them into one SoundFont 3 (the compressed samples untouched, no decoding, no `ffmpeg`) beside a `.credits.txt` that keeps the licence with the file; the game decodes it where it runs with `loadSoundFont`. `fetch --sf2` and `convert <in.sf3> <out.sf2>` decode to a plain SoundFont 2 instead, about eight times larger, which needs `ffmpeg` on PATH (developer machines only). The result goes in the asset folder, where the build subsets it to the programs the game plays | `npx mwg-soundfonts list`, `npx mwg-soundfonts fetch GeneralUser_GS_v2.0.3 --midi assets/*.mid --accept-licence`, or `fetchSoundFont` from `@datamoc/mw_games/tools/soundfonts` |
14
+ | `extract-rgssad` | Decrypts an RPG Maker XP/VX archive to plain files (vendored MIT decoder, developer-side only); an entry whose name would land outside `outDir` is refused (item 389) | `npm run extract:rgssad -- <archive.rgssad> <outDir>` here; `npx mwg-extract-rgssad <archive> <outDir>` or `extractRgssad` from `@datamoc/mw_games/tools/extract-rgssad` in a game |
15
+ | `multiplayer-server` | The reference lockstep server for `core.LockstepClient`: rooms, one shared tick, `null` for a missed input, messages capped at `maxMessageBytes` (64 KB) and read through `parseInbound`, room names bounded, `maxClientsPerRoom` (64). Speaks `ws:`, so a deployment puts it behind a TLS proxy (items 129, 385) | `npx mwg-lockstep-server [port] [--host=]` (127.0.0.1 unless `--host`), or `createLockstepServer(options)` from `@datamoc/mw_games/tools/multiplayer-server`; needs the optional peer `ws` |
16
+ | `mwl` | Compiles MWL content (`[game]`/`[scenario]`/`[side]`/`[unit]`/`[event]`/`[campaign]`) into a game-data module, an i18n catalog and an asset manifest | `npm run mwl -- build <contentDir> -o <outDir>`, or `node node_modules/@datamoc/mw_games/tools/mwl.mjs build <contentDir> -o <outDir>` from an install. It is a command line, not an importable module (it dispatches when loaded), so it has no package subpath; script against the typed library instead: `@datamoc/mw_games/mwl` |
17
+ | `extract-html` | Splits the resources embedded in an HTML file (scripts, CSS, images) back out into files | `npm run extract:html -- <page.html> -o <outDir>`, or `extractHtml` from `@datamoc/mw_games/tools/extract-html` |
18
+ | `compile-openings` | Compiles opening tables (`eco`, `name`, `pgn` TSV rows) into a position-keyed book JSON a game checks in beside its own code: SAN resolved against the real move generator, every position keeping its continuations, the longest line ending there naming it. Rows are vendored CC0 data (`data/openings/`), never fetched | `node tools/compile-openings.mjs <tsv folder> <book.json>`, or `npx mwg-openings <tsv folder> <book.json>` in a game |
19
+ | `build-tablebases` | Solves pawnless white-extras-against-lone-king endings by retrograde analysis and ships them compiled as Huffman-coded JSON under `data/tablebases` (3-piece: `KQK KRK KBK KNK`; 4-piece like `KBNK KBBK` generate the same way but stay a tool run away, full 4-piece tables are tens of megabytes each). One `threads.spawn` task per ending over capped lanes (4-piece lanes stay narrow: hundreds of megabytes of arrays each); captures convert into the already-solved smaller tables, so 3-piece endings solve first. Golden counts: KQK 0/200896/99448/223944, KRK 0/201700/98644/223944, KBK/KNK all draws, KBBK 0/5628080/21071792/6854560, KBNK 0/11188168/8816020/13550244 (unknown/loss/draw/win) | `node tools/build-tablebases.mjs <outDir> [ending ...] [--jobs=N]`, or `npx mwg-tablebases <outDir> [ending ...]` in a game |
20
+ | `play-uci` | A UCI adapter over the tournament engine for GUIs and rating runs: `position`/`go` on stdin, `bestmove` on stdout. Move choice runs book, then tablebase, then `searchTourney`; a Node tool, never part of the browser bundle | `node tools/play-uci.mjs [--book <book.json>] [--tables <dir>] [--depth N]`, or `npx mwg-uci` in a game |
21
+
22
+ ## Shipping (`file://`)
23
+
24
+ | tool | what it does | run |
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]]` |
27
+ | `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
+ | `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
+ | `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` |
30
+ | `verify-scenarios` | `mwg-scenarios <built index.html>` runs every game-owned scenario in one Chrome page, using registered scene controls for bot turns, assertions and cleanup | `npx mwg-scenarios dist/index.html --turns 3` |
31
+
32
+ | `vite` | The Vite plugin a game uses instead of either: `mwgPage(options)` sets the `file://` build configuration (IIFE `game.js`, `base: './'`, `assets/` as the public folder) and runs `emitPage` when `vite build` finishes (item 381) | `plugins: [mwgPage()]` from `@datamoc/mw_games/tools/vite`; `mwgPage({ singleFile: { compress: true } })` also writes one self-contained `standalone.html` (about 250 KB for Pixi + mwg + a small game) |
33
+ | `browser-smoke` | Opens a built page from `file://` in headless Chrome and fails unless it rendered: no page error, an mwg game ready on a WebGL/WebGPU canvas, non-flat pixels, then a screenshot (item 383). `find-chrome` beside it picks the system Chrome, then the newest Chromium Playwright installed, then PATH; `CHROME_PATH` overrides | `vite build && npx mwg-smoke dist` in a game's CI (needs the optional peer `playwright-core`), or `smokePage(options)` from `@datamoc/mw_games/tools/browser-smoke` |
34
+ | `csp` | The Content-Security-Policy `emitPage` writes into every page by default (item 374): scripts only from the page's own folder plus the hashed inline ones, images/fonts/media only `data:`/`blob:`, network (`connect-src`) only to declared origins, `'unsafe-eval'` kept because Pixi 8 needs it | automatic; `csp: { connect: ['https://api.example'] }` or `csp: false` on `emitPage`/`mwgPage`, `--connect=`/`--no-csp` on `mwg-emit`, or `withContentSecurityPolicy(html)` from `@datamoc/mw_games/tools/csp` |
35
+ | `classic-html` | Rewrites a bundler build's `<script type="module">` entry tag to a classic deferred script, the shipped, reusable half of what `emit-page` does for this repo's own examples (item 306) | `import { toClassicScript } from '@datamoc/mw_games/tools/classic-html'` in a game's own build script, or `node tools/classic-html.mjs dist/index.html` here |
36
+ | `compress-dist` | Writes `.gz`/`.br` siblings for large build outputs, into the folder it is handed, plus `.xz` where smaller behind the opt-in `xz` option (needs the system `xz` binary). Files compress in parallel over async `zlib`, `MWG_COMPRESS_JOBS` names the lane count, rows keep walk order | used by the packaging flow |
37
+ | `single-file` | Inlines every `<script src>` an `emit-page` output loads into one additional standalone HTML file, no sibling `.js` at all; `compress: true` compresses each script first (gzip by default, or `algorithm: 'brotli'`), unpacked at load through the browser's own `DecompressionStream` behind a splash screen. Brotli needs `DecompressionStream('br')`, unsupported in a directly-tested current Chrome (153) as of this writing - gzip is the broadly-supported default, brotli an advanced option that degrades to a clear, catchable error rather than hanging (items 349, 350) | `node tools/single-file.mjs <dist folder> [--compress[=level]] [--brotli[=quality]] [--no-splash] [--output name.html]`, `emit-page --single-file [--single-file-compress[=level]] [--single-file-brotli[=quality]]`, or `buildSingleFile` from `@datamoc/mw_games/tools/single-file`. `output` is a file name written inside `dist`, not a path: one with a separator is refused |
38
+ | `asset-report` | Lists every file a built page ships, with raw/gzip/brotli weights and each file's share of the compressed page, plus the embedded source assets with byte-identical duplicates grouped (item 422) | `mwg-assets dist [--assets=assets] [--json=report.json]`, or `assetReport(options)` from `@datamoc/mw_games/tools/asset-report` |
39
+
40
+ ### Scenario verification contract
41
+
42
+ Expose `window.__MWG_SCENARIOS__` in a verification build with `controls` (a
43
+ `createSceneControls()` registry from `@datamoc/mw_games/testing`) and `scenarios`:
44
+
45
+ ```js
46
+ window.__MWG_SCENARIOS__ = {
47
+ controls,
48
+ scenarios: [
49
+ { id: 'tutorial', enter: 'enterTutorial', turn: ['playHumanBot', 'playEnemyBots', 'advanceDialogue'],
50
+ verify: 'checkTutorial', leave: 'destroyScenario' },
51
+ ],
52
+ };
53
+ ```
54
+
55
+ Each name is a registered step. The game implements scenario entry, bots for all sides,
56
+ dialogue advancement and checks for map, units, outcomes or campaign references. A check
57
+ throws on failure; an optional `leave` step handles cleanup even after a failed entry or
58
+ turn. The tool validates the whole selected plan before entering any scenario, runs each
59
+ turn's steps in order, then checks it. Empty plans and filters matching nothing fail.
60
+ Exceptions and console errors fail the report. A timed-out step aborts the run and closes
61
+ the browser because its promise may still be changing scene state.
62
+
63
+ Run `mwg-scenarios dist/index.html --turns 3 --timeout-ms 120000 --only tutorial`.
64
+ The page opens from `file://` once, and the JSON report names every attempted scenario,
65
+ completed turn count and failure. Install the optional `playwright-core` development peer
66
+ to use this tool. `CHROME_PATH` selects Chrome; `MWG_VISUAL_CHROME_ARGS` supplies runner flags.
67
+
68
+ ## Packaging as a native app
69
+
70
+ The packaging scaffolding is part of this repository, not of the published package. Its
71
+ `files` list ships `dist/`, the `tools/*.mjs` helpers (see the table above), the licence and the markdown
72
+ documents, and none of its packaging scripts (`desktop:*`, `mobile:*`, `cap:*`) do: they all
73
+ point into this checkout. A game that installs `@datamoc/mw_games` from the registry
74
+ therefore supplies its own native shell; the recipe for each target is below.
75
+
76
+ Build the game without `compress-dist`'s `.gz`/`.br` siblings for any native target: a
77
+ packaged app has no server to negotiate `Content-Encoding` with, so they are dead weight
78
+ (the opt-in `.xz` siblings are the same), and on Android the asset merge fails outright
79
+ (`Duplicate resources` from AAPT, which reads `game.js` and `game.js.gz` as colliding).
80
+
81
+ ### Desktop (Windows, WebView2)
82
+
83
+ The reference host is `desktop/MwgDesktopHost` - two files, `MwgDesktopHost.csproj` and
84
+ `Program.cs`. Copy that one folder into your project and point it at the game's built
85
+ `dist/`:
86
+
87
+ ```powershell
88
+ cp -r <a clone of mwg>/desktop/MwgDesktopHost ./desktop/MwgDesktopHost
89
+ dotnet run --project desktop/MwgDesktopHost -- dist/index.html
90
+ ```
91
+
92
+ The argument is resolved against the directory the command runs from, so it can be relative;
93
+ with no argument the host loads this repository's own tower-defense example, which is what
94
+ `npm run desktop:run` does. It needs the .NET 8 SDK (`winget install Microsoft.DotNet.SDK.8`)
95
+ and the WebView2 Runtime, already present on current Windows.
96
+
97
+ The host serves the page through `CoreWebView2.SetVirtualHostNameToFolderMapping`, mapping
98
+ `https://mwg.local/` to the page's own folder rather than navigating to `file://`. That real
99
+ origin is what lets `assets.fetchWithByteProgress`, a `LockstepClient` WebSocket connection,
100
+ or any other genuine network call work inside the app; `file://` blocks all of them. A game
101
+ that needs none of that can use `desktop/webview2` instead, which opens the same built page
102
+ from `file://` and takes the same page argument.
103
+
104
+ `dotnet publish -c Release -r win-x64 --self-contained true` inside the host project produces
105
+ a distributable build.
106
+
107
+ ### Mobile (Capacitor, Android)
108
+
109
+ `android/` and the `cap:*` scripts are this repository's own scaffold, so copy the shape of
110
+ them rather than the files: a game creates its own Capacitor project. Toolchain first, since
111
+ these are the two that stop a clean machine:
112
+
113
+ - **JDK 21** (`winget install EclipseAdoptium.Temurin.21.JDK`), with `JAVA_HOME` pointing at it.
114
+ Capacitor's Android Gradle Plugin will not compile under JDK 17: `invalid source release: 21`.
115
+ - **Android SDK** with `platform-tools`, and either `ANDROID_HOME` set or an
116
+ `android/local.properties` written by hand with `sdk.dir=C:/path/to/Android/Sdk`. Forward
117
+ slashes work; backslashes have to be doubled, and a properties file with single backslashes
118
+ silently loses every separator and fails much later as an unreadable path.
119
+
120
+ Then, from the game's own folder:
121
+
122
+ ```powershell
123
+ npm install --save-dev @capacitor/cli @capacitor/core @capacitor/android
124
+ # capacitor.config.json: { "appId": "com.example.yourgame", "appName": "Your Game", "webDir": "dist" }
125
+ npx cap add android # once: scaffolds android/
126
+ npx cap sync android # after every web rebuild: copies dist/ into the project
127
+ cd android && ./gradlew assembleDebug # gradlew.bat on Windows
128
+ adb install -r app/build/outputs/apk/debug/app-debug.apk
129
+ adb shell am start -n com.example.yourgame/.MainActivity
130
+ ```
131
+
132
+ `assembleDebug` produces an installable APK; a release build needs signing, which is yours to
133
+ arrange. `adb` comes with the SDK's `platform-tools`; an emulator is
134
+ `emulator -avd <name>`, and a cold boot takes a minute before `adb install` will accept it.
135
+
136
+ Three things about the generated project are worth knowing before the first bad build, all
137
+ three because `cap add android` writes them once and never revisits them:
138
+
139
+ - **Do not ship `.gz`/`.br` sidecars.** The asset merge fails outright:
140
+ `mergeDebugAssets FAILED ... [public/game.js.gz]: Duplicate resources`, because AAPT reads
141
+ `game.js` and `game.js.gz` as colliding. Build the web output without `compress-dist` for a
142
+ packaged app anyway: there is no server to negotiate `Content-Encoding`.
143
+ - **`ignoreAssetsPattern`,** in `android/app/build.gradle`: the generated line ends `!*~`. Add
144
+ `:!*.gz:!*.br` to it, which is what this repository's own `android/` carries. That is the
145
+ second half of the same problem, and it is why MWG tracks `android/` in git at all - a fresh
146
+ `cap add android` would lose it.
147
+ - **`versionCode`/`versionName`,** in the same file: written as `1` and `"1.0"` and never
148
+ revised, so an app that ships twice without stamping them cannot be installed over itself
149
+ (`INSTALL_FAILED_VERSION_DOWNGRADE`; Android refuses an update whose `versionCode` has not
150
+ increased). Stamp both from the release that produced the build.
151
+
152
+ On some Windows hosts the Capacitor CLI itself refuses to start: Node 26 can return `ENOMEM`
153
+ from `os.userInfo()`, which Capacitor's terminal helper calls at load. The framework's own
154
+ `cap:cli` script works around it, and a game that hits it can too:
155
+
156
+ ```js
157
+ // tools/capacitor-node-workaround.cjs, then: node --require ./tools/capacitor-node-workaround.cjs
158
+ // node_modules/@capacitor/cli/bin/capacitor add android
159
+ const os = require('node:os');
160
+ try {
161
+ os.userInfo();
162
+ } catch (error) {
163
+ if (error?.syscall !== 'uv_os_get_passwd') throw error;
164
+ os.userInfo = () => ({
165
+ uid: -1,
166
+ gid: -1,
167
+ username: process.env.USERNAME || 'unknown',
168
+ homedir: process.env.USERPROFILE || process.cwd(),
169
+ shell: process.env.COMSPEC || null,
170
+ });
171
+ }
172
+ ```
173
+
174
+ Verified end to end from a folder outside this repository, against the published package: a
175
+ game built with `tools/classic-html`'s `toClassicScript`, `cap add android`, `cap sync`, a real
176
+ `gradlew assembleDebug`, then installed and launched on a `Medium_Phone_API_35` emulator, where
177
+ the game rendered and its pixels were checked rather than assumed. The duplicate-resource
178
+ failure above and the `ignoreAssetsPattern` fix that clears it were both reproduced on that
179
+ same build.
180
+
181
+ ## Verification and benchmarks
182
+
183
+ | tool | what it does | run |
184
+ | --- | --- | --- |
185
+ | `benchmark-browser` | Measures a built page's frame rate in headless Chrome: mean fps, 95th-percentile frame time, JS heap, a per-page history compared with its best run (item 387) | `npm run benchmark:browser` here; `npx mwg-bench dist [--frames=] [--min-fps=] [--max-p95=] [--history=]` in a game (optional peer `playwright-core`), or `measurePage(options)` from `@datamoc/mw_games/tools/benchmark-browser` |
186
+ | `benchmark-simulation` | Headless throughput for `mwg/simulation` (no frame, canvas, or Pixi involved) | `npm run benchmark:simulation` |
187
+ | `benchmark-animation` | CSS/SVG element animation vs Pixi sprites vs both, at increasing counts, sampling rAF intervals in headless Chrome. A measurement, not a gate | `npm run benchmark:animation` |
188
+ | `graphics-capabilities` | Probes the host's WebGL capabilities headless | `npm run graphics:capabilities` |
189
+ | `visual-smoke` | Opens one built example from `file://` in headless Chrome, fails on a page error or a blank canvas, and writes a screenshot. Runs per pull request in CI | `npm run visual:smoke:ui` |
190
+ | `package-smoke` | `npm pack`, installs the tarball outside the repo, builds the tutorial's tiny game and opens both that and the standalone global from `file://` | `npm run package:smoke` |
191
+ | `motion-smoke` | Opens the interface example three ways (no preference, an emulated `prefers-reduced-motion: reduce`, and a flip while the page is open) and asserts the demo's own state: the preference arrives, stops the motion, and is seen when it changes | `npm run motion:smoke` |
192
+ | `build-all-examples` | Runs every `example:*:build` script, so a vite/emit-page regression in any example is caught, not just the benchmarked ones | `npm run examples:build` |
193
+ | `bundle-size` | The size budget: raw and gzipped bytes against a committed baseline, failing past a tolerance (2 % by default). Here, the global bundle and the published `dist`; in a game, every file a player receives plus their total (item 386) | `npm run size:check`/`size:update` here; `npx mwg-size dist [--baseline=size-budget.json] [--update] [--tolerance=0.02]` in a game |
194
+ | `audit-adoptions` | The consumer adoption auditor: reads the `api-surface.json` manifest plus a consumer-authored reasons file and config (type-only directories, subpath deny-lists, per-pattern import budgets), then reports exports neither imported nor reasoned, shadowing collisions, and config violations, with `file:line` and a non-zero exit | `npx mwg-audit-adoptions --manifest api-surface.json --imports imports.json --locals locals.json --config audit.json`, or `auditAdoptions(manifest, imports, locals, config)` from `@datamoc/mw_games/tools/audit-adoptions` |
195
+ | `browser-driver` | The dependency-free browser harness: a self-served static directory on an OS-chosen free port, with Chrome over CDP and Firefox over WebDriver BiDi behind one surface (`goto`/`waitReady`/`tap`/`evaluate`/`script`/`screenshot`/`consoleErrors`/`close`), reusing the `browser-smoke` readiness contract and no pixel-diff baseline | `driveChrome()`/`driveFirefox()` plus `serveDir(root)` from `@datamoc/mw_games/tools/browser-driver` |
196
+
197
+ ## Project chores
198
+
199
+ | tool | what it does | run |
200
+ | --- | --- | --- |
201
+ | `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
+ | `make-example-diagrams` / `make-architecture-diagrams` | Rebuild the generated diagrams under `webpage/assets` (never hand-drawn) | `npm run webpage:diagrams` |
204
+ | `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
+ | `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` |
206
+ | `ci-status` | Waits for every workflow run of `HEAD` and fails if any of them did | `npm run ci:status` |
207
+ | `npm-audit` | Dependency audit that fails for high or critical advisories | `npm run audit` |
208
+ | `sbom` | Two CycloneDX SBOMs, both reproducible (no timestamp, a content-derived serial). The lockfile SBOM of any npm project (`buildSbom`; `mwg-sbom [folder] [--out=] [--check]`; this repository's own committed `sbom.cdx.json`, gated in CI and now shipped in the package). And the SBOM of what a build ships (`buildArtifactSbom`, written as `dist/sbom.cdx.json` by `mwgPage()`): only the packages whose modules reached the bundle, from vite's module graph, plus each shipped file's SHA-256 (items 315, 380) | `npm run sbom`, `npm run sbom:check` here; `npx mwg-sbom` or `mwgPage()` in a game |
209
+
210
+ ### Selecting embedded assets
211
+
212
+ The Vite plugin accepts `mwgPage({ assets: 'assets', assetFiles: ['hero.svg', 'levels/one.json'] })`.
213
+ Paths are relative to the asset folder. Omission embeds every supported file as before; an
214
+ empty list embeds none. Unknown files and unsupported formats named explicitly fail the build.
215
+ The same `assetFiles` option is available on `emitPage` and `compileResources`. Dev serving is
216
+ unchanged. Include `credits.json` in an explicit list to generate its `CREDITS.txt` attribution.