@datamoc/mw_games 0.36.1 → 0.38.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 +58 -0
- package/README.md +32 -2
- package/api-surface.json +384 -0
- package/dist/assets/paths.d.ts.map +1 -1
- package/dist/assets/paths.js +3 -0
- package/dist/assets/paths.js.map +1 -1
- package/dist/audio/Playable.d.ts.map +1 -1
- package/dist/audio/Playable.js +14 -1
- package/dist/audio/Playable.js.map +1 -1
- package/dist/audio/Synth.d.ts +14 -0
- package/dist/audio/Synth.d.ts.map +1 -1
- package/dist/audio/Synth.js +16 -0
- package/dist/audio/Synth.js.map +1 -1
- package/dist/audio/index.d.ts +1 -1
- package/dist/audio/index.d.ts.map +1 -1
- package/dist/audio/index.js +1 -1
- package/dist/audio/index.js.map +1 -1
- package/dist/core/FixedStepper.d.ts +38 -0
- package/dist/core/FixedStepper.d.ts.map +1 -0
- package/dist/core/FixedStepper.js +56 -0
- package/dist/core/FixedStepper.js.map +1 -0
- package/dist/core/Input.d.ts +61 -0
- package/dist/core/Input.d.ts.map +1 -1
- package/dist/core/Input.js +101 -0
- package/dist/core/Input.js.map +1 -1
- package/dist/core/StoredValue.d.ts +11 -2
- package/dist/core/StoredValue.d.ts.map +1 -1
- package/dist/core/StoredValue.js +11 -2
- package/dist/core/StoredValue.js.map +1 -1
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -1
- package/dist/core/index.js.map +1 -1
- package/dist/mw_games.global.js +121 -121
- package/dist/mwl/presentation.d.ts +70 -0
- package/dist/mwl/presentation.d.ts.map +1 -0
- package/dist/mwl/presentation.js +412 -0
- package/dist/mwl/presentation.js.map +1 -0
- package/dist/physics/PhysicsWorld.d.ts +123 -0
- package/dist/physics/PhysicsWorld.d.ts.map +1 -0
- package/dist/physics/PhysicsWorld.js +137 -0
- package/dist/physics/PhysicsWorld.js.map +1 -0
- package/dist/physics/XpbdWorld.d.ts +139 -0
- package/dist/physics/XpbdWorld.d.ts.map +1 -0
- package/dist/physics/XpbdWorld.js +207 -0
- package/dist/physics/XpbdWorld.js.map +1 -0
- package/dist/physics/index.d.ts +3 -0
- package/dist/physics/index.d.ts.map +1 -0
- package/dist/physics/index.js +2 -0
- package/dist/physics/index.js.map +1 -0
- package/dist/physics/xpbd.d.ts +3 -0
- package/dist/physics/xpbd.d.ts.map +1 -0
- package/dist/physics/xpbd.js +2 -0
- package/dist/physics/xpbd.js.map +1 -0
- package/dist/two-d/presentation/MwlPresentation.d.ts +98 -0
- package/dist/two-d/presentation/MwlPresentation.d.ts.map +1 -0
- package/dist/two-d/presentation/MwlPresentation.js +704 -0
- package/dist/two-d/presentation/MwlPresentation.js.map +1 -0
- package/dist/two-d/presentation/index.d.ts +3 -0
- package/dist/two-d/presentation/index.d.ts.map +1 -0
- package/dist/two-d/presentation/index.js +2 -0
- package/dist/two-d/presentation/index.js.map +1 -0
- package/dist/two-d/render/Camera.d.ts +6 -0
- package/dist/two-d/render/Camera.d.ts.map +1 -1
- package/dist/two-d/render/Camera.js +9 -0
- package/dist/two-d/render/Camera.js.map +1 -1
- package/dist/two-d/render/Particles.d.ts +10 -1
- package/dist/two-d/render/Particles.d.ts.map +1 -1
- package/dist/two-d/render/Particles.js +14 -7
- package/dist/two-d/render/Particles.js.map +1 -1
- package/dist/two-d/render/Shape2D.d.ts +7 -0
- package/dist/two-d/render/Shape2D.d.ts.map +1 -1
- package/dist/two-d/render/Shape2D.js +18 -0
- package/dist/two-d/render/Shape2D.js.map +1 -1
- package/dist/two-d/render/SpriteSheet.d.ts +5 -0
- package/dist/two-d/render/SpriteSheet.d.ts.map +1 -1
- package/dist/two-d/render/SpriteSheet.js +15 -0
- package/dist/two-d/render/SpriteSheet.js.map +1 -1
- package/dist/two-d/ui/Bar.d.ts +3 -1
- package/dist/two-d/ui/Bar.d.ts.map +1 -1
- package/dist/two-d/ui/Bar.js +5 -0
- package/dist/two-d/ui/Bar.js.map +1 -1
- package/dist/two-d/ui/Button.d.ts +19 -0
- package/dist/two-d/ui/Button.d.ts.map +1 -1
- package/dist/two-d/ui/Button.js +30 -4
- package/dist/two-d/ui/Button.js.map +1 -1
- package/dist/two-d/ui/Dropdown.d.ts +3 -0
- package/dist/two-d/ui/Dropdown.d.ts.map +1 -1
- package/dist/two-d/ui/Dropdown.js +3 -0
- package/dist/two-d/ui/Dropdown.js.map +1 -1
- package/dist/two-d/ui/DropdownView.d.ts +40 -0
- package/dist/two-d/ui/DropdownView.d.ts.map +1 -0
- package/dist/two-d/ui/DropdownView.js +75 -0
- package/dist/two-d/ui/DropdownView.js.map +1 -0
- package/dist/two-d/ui/Label.d.ts +2 -0
- package/dist/two-d/ui/Label.d.ts.map +1 -1
- package/dist/two-d/ui/Label.js +7 -0
- package/dist/two-d/ui/Label.js.map +1 -1
- package/dist/two-d/ui/Layout.d.ts +34 -0
- package/dist/two-d/ui/Layout.d.ts.map +1 -1
- package/dist/two-d/ui/Layout.js +47 -0
- package/dist/two-d/ui/Layout.js.map +1 -1
- package/dist/two-d/ui/Window.d.ts +17 -0
- package/dist/two-d/ui/Window.d.ts.map +1 -1
- package/dist/two-d/ui/Window.js +28 -4
- package/dist/two-d/ui/Window.js.map +1 -1
- package/dist/two-d/ui/index.d.ts +6 -2
- package/dist/two-d/ui/index.d.ts.map +1 -1
- package/dist/two-d/ui/index.js +2 -1
- package/dist/two-d/ui/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +36 -2
- package/sbom.cdx.json +58 -6
- package/tools/browser-driver.d.mts +14 -0
- package/tools/browser-driver.mjs +62 -0
- package/tools/build-tablebases.mjs +6 -2
- package/tools/classic-html.mjs +9 -1
- package/tools/compile-openings.mjs +6 -2
- package/tools/compile-resources.d.mts +5 -0
- package/tools/compile-resources.mjs +45 -4
- package/tools/compress-dist.mjs +15 -3
- package/tools/emit-page.mjs +5 -1
- package/tools/init.d.mts +5 -0
- package/tools/init.mjs +145 -0
- package/tools/play-uci.mjs +6 -3
- package/tools/sbom.mjs +5 -1
- package/tools/single-file.mjs +9 -1
- package/tools/verify-scenarios.d.mts +15 -0
- package/tools/verify-scenarios.mjs +129 -0
- package/tools/vite.mjs +5 -0
- package/tools/webp-convert.mjs +9 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
1
3
|
import { readdir, readFile, mkdir, writeFile, rm } from 'node:fs/promises';
|
|
2
|
-
import { join, extname, relative, sep, dirname
|
|
4
|
+
import { join, extname, relative, sep, dirname } from 'node:path';
|
|
3
5
|
import { WEBP_CONVERTIBLE_EXTENSIONS } from './webp-convert.mjs';
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -68,6 +70,11 @@ export async function* walk(dir) {
|
|
|
68
70
|
* lossy re-encoding for photographic source art)
|
|
69
71
|
* @param options.webpQuality 0-100, used only when `webpLossless` is false (default 90)
|
|
70
72
|
* @returns the group names, in the order the page should load them
|
|
73
|
+
*
|
|
74
|
+
* An `assets/credits.json` (an array, or `{ "credits": [...] }`, of `{ name, license, author?, url? }`)
|
|
75
|
+
* is not embedded as an asset: it is turned into `CREDITS.txt` beside the compiled scripts, for
|
|
76
|
+
* the attribution that free art packs require. An entry without a `name` and a `license` is an
|
|
77
|
+
* error, since a credits file that silently omits a pack defeats its purpose.
|
|
71
78
|
*/
|
|
72
79
|
export async function compileResources({
|
|
73
80
|
from,
|
|
@@ -96,7 +103,14 @@ export async function compileResources({
|
|
|
96
103
|
// (or requires installing) sharp
|
|
97
104
|
const toWebp = convertToWebp ? (await import('./webp-convert.mjs')).toWebp : null;
|
|
98
105
|
|
|
106
|
+
const creditsFile = join(from, 'credits.json');
|
|
107
|
+
let credits = null;
|
|
108
|
+
|
|
99
109
|
for await (const file of walk(from)) {
|
|
110
|
+
if (file === creditsFile) {
|
|
111
|
+
credits = creditsText(JSON.parse(await readFile(file, 'utf8')));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
100
114
|
const ext = extname(file).toLowerCase();
|
|
101
115
|
const type = types[ext];
|
|
102
116
|
if (!type) {
|
|
@@ -136,14 +150,33 @@ export async function compileResources({
|
|
|
136
150
|
written.push({ name, count: Object.keys(assets).length, size: js.length });
|
|
137
151
|
}
|
|
138
152
|
|
|
139
|
-
|
|
153
|
+
if (credits) await writeFile(join(to, 'CREDITS.txt'), credits, 'utf8');
|
|
154
|
+
|
|
155
|
+
return { groups: written, rawBytes, embeddedBytes, webpConverted, skipped, credits };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** the plain-text credits for a manifest, one block per entry; throws on an entry that cannot be attributed */
|
|
159
|
+
export function creditsText(manifest) {
|
|
160
|
+
const entries = Array.isArray(manifest) ? manifest : manifest?.credits;
|
|
161
|
+
if (!Array.isArray(entries)) throw new Error('credits.json must be an array or { "credits": [...] }');
|
|
162
|
+
const blocks = entries.map((entry, index) => {
|
|
163
|
+
if (typeof entry?.name !== 'string' || typeof entry?.license !== 'string') {
|
|
164
|
+
throw new Error(`credits.json entry ${index} needs a "name" and a "license"`);
|
|
165
|
+
}
|
|
166
|
+
const lines = [entry.name + (entry.author ? ` by ${entry.author}` : ''), `License: ${entry.license}`];
|
|
167
|
+
if (entry.url) lines.push(entry.url);
|
|
168
|
+
return lines.join('\n');
|
|
169
|
+
});
|
|
170
|
+
return blocks.join('\n\n') + '\n';
|
|
140
171
|
}
|
|
141
172
|
|
|
142
173
|
// ---------------------------------------------------------------- CLI
|
|
143
174
|
|
|
144
|
-
|
|
175
|
+
//the real path, since an npm `bin` shim reaches this file through a symlink; a suffix test on the
|
|
176
|
+
//name is not enough (`compress-dist.mjs` ends with `t.mjs`)
|
|
177
|
+
const isMain = process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
145
178
|
|
|
146
|
-
|
|
179
|
+
async function runCli() {
|
|
147
180
|
const args = process.argv.slice(2);
|
|
148
181
|
const [from, to] = args.filter((a) => !a.startsWith('--'));
|
|
149
182
|
if (!from || !to) {
|
|
@@ -180,4 +213,12 @@ if (isMain) {
|
|
|
180
213
|
}
|
|
181
214
|
}
|
|
182
215
|
|
|
216
|
+
//no top-level await: a vite config loaded as CommonJS has to be able to require() this module
|
|
217
|
+
if (isMain) {
|
|
218
|
+
runCli().catch((error) => {
|
|
219
|
+
console.error(error);
|
|
220
|
+
process.exitCode = 1;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
183
224
|
export { dirname };
|
package/tools/compress-dist.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
1
3
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { relative, extname
|
|
4
|
+
import { relative, extname } from 'node:path';
|
|
3
5
|
import { gzip, brotliCompress, constants } from 'node:zlib';
|
|
4
6
|
import { promisify } from 'node:util';
|
|
5
7
|
import { availableParallelism } from 'node:os';
|
|
@@ -153,9 +155,11 @@ async function compressOne(dir, file, { gzip, brotli, wantXz }) {
|
|
|
153
155
|
return row;
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
|
|
158
|
+
//the real path, since an npm `bin` shim reaches this file through a symlink; a suffix test on the
|
|
159
|
+
//name is not enough (`compress-dist.mjs` ends with `t.mjs`)
|
|
160
|
+
const isMain = process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
157
161
|
|
|
158
|
-
|
|
162
|
+
async function runCli() {
|
|
159
163
|
const [dir, ...flags] = process.argv.slice(2);
|
|
160
164
|
if (!dir) {
|
|
161
165
|
console.error('usage: node tools/compress-dist.mjs <dist folder> [--xz] [--no-gzip] [--no-brotli]');
|
|
@@ -179,3 +183,11 @@ if (isMain) {
|
|
|
179
183
|
}
|
|
180
184
|
console.log(` ${rows.length} file(s) considered in ${relative(process.cwd(), dir)}`);
|
|
181
185
|
}
|
|
186
|
+
|
|
187
|
+
//no top-level await: a vite config loaded as CommonJS has to be able to require() this module
|
|
188
|
+
if (isMain) {
|
|
189
|
+
runCli().catch((error) => {
|
|
190
|
+
console.error(error);
|
|
191
|
+
process.exitCode = 1;
|
|
192
|
+
});
|
|
193
|
+
}
|
package/tools/emit-page.mjs
CHANGED
|
@@ -184,5 +184,9 @@ async function main(argv) {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
//realpath, since an npm `bin` shim reaches this file through a symlink
|
|
187
|
+
//no top-level await: a vite config loaded as CommonJS has to be able to require() this module
|
|
187
188
|
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url))
|
|
188
|
-
|
|
189
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
190
|
+
console.error(error);
|
|
191
|
+
process.exitCode = 1;
|
|
192
|
+
});
|
package/tools/init.d.mts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** the starter project's files as `{ path: content }`, for a package called `name` */
|
|
2
|
+
export function initFiles(name: string): Record<string, string>;
|
|
3
|
+
|
|
4
|
+
/** writes the starter project into an empty or missing `folder`; throws if it already holds files */
|
|
5
|
+
export function initProject(folder: string): { folder: string; files: string[] };
|
package/tools/init.mjs
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `mwg-init <folder>`: the smallest game that builds into one double-clickable page.
|
|
8
|
+
*
|
|
9
|
+
* It writes a vite project that uses `mwgPage()`: one scene, a moving square, a vite config
|
|
10
|
+
* that loads without `"type": "module"` in the way `.mts` files do, and the scripts to run
|
|
11
|
+
* and build it. The point is that the nearest example to copy is already on disk, in the
|
|
12
|
+
* shape the published package supports, rather than a folder of this repository's own
|
|
13
|
+
* examples that import their sources by relative path.
|
|
14
|
+
*/
|
|
15
|
+
const PACKAGE_VERSION = JSON.parse(
|
|
16
|
+
readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'),
|
|
17
|
+
).version;
|
|
18
|
+
|
|
19
|
+
/** the project's files as `{ path: content }`, named after `name` */
|
|
20
|
+
export function initFiles(name) {
|
|
21
|
+
return {
|
|
22
|
+
'package.json':
|
|
23
|
+
JSON.stringify(
|
|
24
|
+
{
|
|
25
|
+
name,
|
|
26
|
+
private: true,
|
|
27
|
+
version: '0.0.0',
|
|
28
|
+
type: 'module',
|
|
29
|
+
scripts: { dev: 'vite', build: 'vite build', check: 'tsc --noEmit' },
|
|
30
|
+
dependencies: { '@datamoc/mw_games': `^${PACKAGE_VERSION}`, 'pixi.js': '^8.20.1' },
|
|
31
|
+
devDependencies: { typescript: '^7.0.2', vite: '^8.2.2' },
|
|
32
|
+
},
|
|
33
|
+
null,
|
|
34
|
+
2,
|
|
35
|
+
) + '\n',
|
|
36
|
+
'vite.config.mts': [
|
|
37
|
+
"import { defineConfig } from 'vite';",
|
|
38
|
+
"import { mwgPage } from '@datamoc/mw_games/tools/vite';",
|
|
39
|
+
'',
|
|
40
|
+
'//`singleFile: true` also writes dist/standalone.html, the whole game in one file',
|
|
41
|
+
'export default defineConfig({ plugins: [mwgPage({ singleFile: true })] });',
|
|
42
|
+
'',
|
|
43
|
+
].join('\n'),
|
|
44
|
+
'tsconfig.json':
|
|
45
|
+
JSON.stringify(
|
|
46
|
+
{
|
|
47
|
+
compilerOptions: {
|
|
48
|
+
target: 'ES2022',
|
|
49
|
+
module: 'ESNext',
|
|
50
|
+
moduleResolution: 'bundler',
|
|
51
|
+
lib: ['ES2022', 'DOM'],
|
|
52
|
+
strict: true,
|
|
53
|
+
noEmit: true,
|
|
54
|
+
skipLibCheck: true,
|
|
55
|
+
},
|
|
56
|
+
include: ['src'],
|
|
57
|
+
},
|
|
58
|
+
null,
|
|
59
|
+
2,
|
|
60
|
+
) + '\n',
|
|
61
|
+
'index.html': [
|
|
62
|
+
'<!DOCTYPE html>',
|
|
63
|
+
'<html lang="en">',
|
|
64
|
+
'<head>',
|
|
65
|
+
'\t<meta charset="utf-8">',
|
|
66
|
+
'\t<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
67
|
+
`\t<title>${name}</title>`,
|
|
68
|
+
'\t<style>html, body { margin: 0; height: 100%; background: #101018; overflow: hidden; } #game { display: block; width: 100vw; height: 100vh; }</style>',
|
|
69
|
+
'</head>',
|
|
70
|
+
'<body>',
|
|
71
|
+
'\t<canvas id="game"></canvas>',
|
|
72
|
+
'\t<script type="module" src="./src/main.ts"></script>',
|
|
73
|
+
'</body>',
|
|
74
|
+
'</html>',
|
|
75
|
+
'',
|
|
76
|
+
].join('\n'),
|
|
77
|
+
'src/main.ts': [
|
|
78
|
+
"import { Game, Scene2D } from '@datamoc/mw_games/two-d';",
|
|
79
|
+
"import { Shape2D, Text2D } from '@datamoc/mw_games/two-d/render';",
|
|
80
|
+
'',
|
|
81
|
+
'class MainScene extends Scene2D {',
|
|
82
|
+
'\tprivate square!: Shape2D;',
|
|
83
|
+
'\tprivate elapsed = 0;',
|
|
84
|
+
'',
|
|
85
|
+
'\toverride create(): void {',
|
|
86
|
+
"\t\tconst label = new Text2D({ text: 'Hello, mwg!', style: { fill: 0xd8dae6, fontFamily: 'monospace', fontSize: 16 } });",
|
|
87
|
+
'\t\tlabel.position.set(24, 24);',
|
|
88
|
+
'\t\tthis.stage.addChild(label);',
|
|
89
|
+
'\t\tthis.square = new Shape2D().rect(-20, -20, 40, 40).fill(0x6fb1ff);',
|
|
90
|
+
'\t\tthis.stage.addChild(this.square);',
|
|
91
|
+
'\t}',
|
|
92
|
+
'',
|
|
93
|
+
'\toverride update(dt: number): void {',
|
|
94
|
+
'\t\tthis.elapsed += dt;',
|
|
95
|
+
'\t\tthis.square.position.set(Game.current.width / 2 + Math.sin(this.elapsed) * 120, Game.current.height / 2);',
|
|
96
|
+
'\t}',
|
|
97
|
+
'}',
|
|
98
|
+
'',
|
|
99
|
+
"const game = new Game({ canvas: document.getElementById('game') as HTMLCanvasElement, background: 0x101018 });",
|
|
100
|
+
'game.start(MainScene).catch((error) => console.error(error));',
|
|
101
|
+
'',
|
|
102
|
+
].join('\n'),
|
|
103
|
+
'assets/.gitkeep': '',
|
|
104
|
+
'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`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** writes the project into `folder`; refuses a folder that already holds files, so nothing is overwritten */
|
|
109
|
+
export function initProject(folder) {
|
|
110
|
+
const target = resolve(folder);
|
|
111
|
+
if (existsSync(target) && readdirSync(target).length > 0) {
|
|
112
|
+
throw new Error(`${target} is not empty; run mwg-init in a new or empty folder`);
|
|
113
|
+
}
|
|
114
|
+
const written = [];
|
|
115
|
+
for (const [path, content] of Object.entries(
|
|
116
|
+
initFiles(
|
|
117
|
+
basename(target)
|
|
118
|
+
.toLowerCase()
|
|
119
|
+
.replace(/[^a-z0-9-]/g, '-'),
|
|
120
|
+
),
|
|
121
|
+
)) {
|
|
122
|
+
const file = join(target, path);
|
|
123
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
124
|
+
writeFileSync(file, content, 'utf8');
|
|
125
|
+
written.push(path);
|
|
126
|
+
}
|
|
127
|
+
return { folder: target, files: written };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//no top-level await: this module is also imported, and the real path covers an npm bin symlink
|
|
131
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
132
|
+
const folder = process.argv[2];
|
|
133
|
+
if (!folder) {
|
|
134
|
+
console.error('usage: mwg-init <new folder>');
|
|
135
|
+
process.exitCode = 2;
|
|
136
|
+
} else {
|
|
137
|
+
try {
|
|
138
|
+
const { folder: target, files } = initProject(folder);
|
|
139
|
+
console.log(`wrote ${files.length} files to ${target}\nnext: cd ${folder} && npm install && npm run dev`);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error(error.message);
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
package/tools/play-uci.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { join, extname } from 'node:path';
|
|
4
5
|
import { createInterface } from 'node:readline';
|
|
5
6
|
import { applyMove, cloneChess, legalMoves, parseFen, squareName, startingChess } from '../src/board/chess.ts';
|
|
6
7
|
import { parseUciMove, probeBook } from '../src/board/openings.ts';
|
|
@@ -164,7 +165,9 @@ export function createUciSession(options = {}) {
|
|
|
164
165
|
};
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
|
|
168
|
+
//the real path, since an npm `bin` shim reaches this file through a symlink; a suffix test on the
|
|
169
|
+
//name is not enough (`compress-dist.mjs` ends with `t.mjs`)
|
|
170
|
+
const isMain = process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
168
171
|
|
|
169
172
|
if (isMain) {
|
|
170
173
|
const args = process.argv.slice(2);
|
package/tools/sbom.mjs
CHANGED
|
@@ -328,5 +328,9 @@ async function main(argv) {
|
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
//realpath, since an npm `bin` shim reaches this file through a symlink
|
|
331
|
+
//no top-level await: a vite config loaded as CommonJS has to be able to require() this module
|
|
331
332
|
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url))
|
|
332
|
-
|
|
333
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
334
|
+
console.error(error);
|
|
335
|
+
process.exitCode = 1;
|
|
336
|
+
});
|
package/tools/single-file.mjs
CHANGED
|
@@ -200,7 +200,7 @@ function renderBootstrap(payloads, algorithm) {
|
|
|
200
200
|
|
|
201
201
|
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
async function runCli() {
|
|
204
204
|
const args = process.argv.slice(2);
|
|
205
205
|
const dist = args[0];
|
|
206
206
|
if (!dist || dist.startsWith('--')) {
|
|
@@ -229,3 +229,11 @@ if (isMain) {
|
|
|
229
229
|
);
|
|
230
230
|
console.log(' one file, no server, no sibling script - open it directly');
|
|
231
231
|
}
|
|
232
|
+
|
|
233
|
+
//no top-level await: a vite config loaded as CommonJS has to be able to require() this module
|
|
234
|
+
if (isMain) {
|
|
235
|
+
runCli().catch((error) => {
|
|
236
|
+
console.error(error);
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ScenarioPlanEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
enter: string;
|
|
4
|
+
/** Steps that play all sides and advance dialogue for one game-owned turn. */
|
|
5
|
+
turn: string[];
|
|
6
|
+
/** A step that throws when game-owned map, units, outcome or campaign checks fail. */
|
|
7
|
+
verify: string;
|
|
8
|
+
leave?: string;
|
|
9
|
+
}
|
|
10
|
+
export function validateScenarioPlan(plan: unknown): ScenarioPlanEntry[];
|
|
11
|
+
export function verifyScenarios(options: { file: string; turns?: number; timeoutMs?: number; only?: string }): Promise<{
|
|
12
|
+
ok: boolean;
|
|
13
|
+
scenarios: { id: string; turns: number; ok: boolean }[];
|
|
14
|
+
failures: string[];
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { launchChrome } from './find-chrome.mjs';
|
|
6
|
+
|
|
7
|
+
/** Validate the serialisable, game-owned plan before running any scene steps. */
|
|
8
|
+
export function validateScenarioPlan(plan) {
|
|
9
|
+
if (!Array.isArray(plan) || plan.length === 0) throw new Error('scenario plan must be a nonempty array');
|
|
10
|
+
const ids = new Set();
|
|
11
|
+
for (const scenario of plan) {
|
|
12
|
+
if (!scenario || typeof scenario.id !== 'string' || !scenario.id || ids.has(scenario.id))
|
|
13
|
+
throw new Error('scenario IDs must be nonempty and unique');
|
|
14
|
+
ids.add(scenario.id);
|
|
15
|
+
for (const field of ['enter', 'verify'])
|
|
16
|
+
if (typeof scenario[field] !== 'string' || !scenario[field])
|
|
17
|
+
throw new Error(`${scenario.id}: ${field} needs a scene control name`);
|
|
18
|
+
if (
|
|
19
|
+
!Array.isArray(scenario.turn) ||
|
|
20
|
+
scenario.turn.length === 0 ||
|
|
21
|
+
scenario.turn.some((step) => typeof step !== 'string' || !step)
|
|
22
|
+
)
|
|
23
|
+
throw new Error(`${scenario.id}: turn needs a nonempty list of scene control names`);
|
|
24
|
+
if (scenario.leave !== undefined && (typeof scenario.leave !== 'string' || !scenario.leave))
|
|
25
|
+
throw new Error(`${scenario.id}: leave needs a scene control name`);
|
|
26
|
+
}
|
|
27
|
+
return plan;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Opens one local page and drives every scenario through its registered SceneControls. */
|
|
31
|
+
export async function verifyScenarios({ file, turns = 3, timeoutMs = 120000, only = '' }) {
|
|
32
|
+
if (!Number.isSafeInteger(turns) || turns < 1) throw new Error('turns must be a positive integer');
|
|
33
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) throw new Error('timeoutMs must be a positive integer');
|
|
34
|
+
const browser = await launchChrome({ argsEnv: 'MWG_VISUAL_CHROME_ARGS' });
|
|
35
|
+
const failures = [];
|
|
36
|
+
const scenarios = [];
|
|
37
|
+
let expired = false;
|
|
38
|
+
const bounded = async (operation) => {
|
|
39
|
+
let timer;
|
|
40
|
+
try {
|
|
41
|
+
return await Promise.race([
|
|
42
|
+
operation,
|
|
43
|
+
new Promise((_, reject) => {
|
|
44
|
+
timer = setTimeout(() => {
|
|
45
|
+
expired = true;
|
|
46
|
+
reject(new Error(`scene step exceeded ${timeoutMs} ms`));
|
|
47
|
+
}, timeoutMs);
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
} finally {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
|
56
|
+
let current = 'startup';
|
|
57
|
+
page.on('pageerror', (error) => failures.push(`${current}: ${error.message}`));
|
|
58
|
+
page.on('console', (message) => {
|
|
59
|
+
if (message.type() === 'error') failures.push(`${current}: ${message.text()}`);
|
|
60
|
+
});
|
|
61
|
+
await page.goto(pathToFileURL(resolve(file)).href, { waitUntil: 'load', timeout: timeoutMs });
|
|
62
|
+
await page.waitForFunction(() => Boolean(window.__MWG_SCENARIOS__), undefined, { timeout: timeoutMs });
|
|
63
|
+
const plan = validateScenarioPlan(await page.evaluate(() => window.__MWG_SCENARIOS__.scenarios));
|
|
64
|
+
const selected = plan.filter((scenario) => scenario.id.startsWith(only));
|
|
65
|
+
if (!selected.length) throw new Error(`no scenarios match ${JSON.stringify(only)}`);
|
|
66
|
+
// Refuse a missing step before entering the first scenario.
|
|
67
|
+
await page.evaluate((plan) => {
|
|
68
|
+
const controls = window.__MWG_SCENARIOS__.controls;
|
|
69
|
+
for (const scenario of plan)
|
|
70
|
+
for (const step of [scenario.enter, ...scenario.turn, scenario.verify, scenario.leave].filter(Boolean))
|
|
71
|
+
if (!controls.has(step)) throw new Error(`scene control step is not registered: ${step}`);
|
|
72
|
+
}, selected);
|
|
73
|
+
for (const scenario of selected) {
|
|
74
|
+
current = scenario.id;
|
|
75
|
+
const before = failures.length;
|
|
76
|
+
let played = 0;
|
|
77
|
+
const run = (step) => bounded(page.evaluate((name) => window.__MWG_SCENARIOS__.controls.run(name), step));
|
|
78
|
+
try {
|
|
79
|
+
await run(scenario.enter);
|
|
80
|
+
for (; played < turns; played++) {
|
|
81
|
+
for (const step of scenario.turn) await run(step);
|
|
82
|
+
await run(scenario.verify);
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
failures.push(`${current}: ${error.message}`);
|
|
86
|
+
} finally {
|
|
87
|
+
if (scenario.leave && !expired) {
|
|
88
|
+
try {
|
|
89
|
+
await run(scenario.leave);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
failures.push(`${current}: cleanup: ${error.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
scenarios.push({ id: scenario.id, turns: played, ok: failures.length === before });
|
|
96
|
+
// A timed-out promise is still live in the page. Do not overlap another scenario with it.
|
|
97
|
+
if (expired) break;
|
|
98
|
+
}
|
|
99
|
+
return { ok: failures.length === 0, scenarios, failures };
|
|
100
|
+
} finally {
|
|
101
|
+
await browser.close();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
106
|
+
const [file, ...args] = process.argv.slice(2);
|
|
107
|
+
const options = { file };
|
|
108
|
+
try {
|
|
109
|
+
if (!file)
|
|
110
|
+
throw new Error('usage: mwg-scenarios <built index.html> [--turns N] [--timeout-ms N] [--only prefix]');
|
|
111
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
112
|
+
const name = { '--turns': 'turns', '--timeout-ms': 'timeoutMs', '--only': 'only' }[args[i]];
|
|
113
|
+
if (!name || args[i + 1] === undefined) throw new Error(`invalid option: ${args[i]}`);
|
|
114
|
+
options[name] = name === 'only' ? args[i + 1] : Number(args[i + 1]);
|
|
115
|
+
}
|
|
116
|
+
verifyScenarios(options)
|
|
117
|
+
.then((report) => {
|
|
118
|
+
console.log(JSON.stringify(report, null, 2));
|
|
119
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
120
|
+
})
|
|
121
|
+
.catch((error) => {
|
|
122
|
+
console.error(error.message);
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(error.message);
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
package/tools/vite.mjs
CHANGED
|
@@ -50,6 +50,11 @@ export function mwgPage(options = {}) {
|
|
|
50
50
|
reportCompressedSize: false,
|
|
51
51
|
rollupOptions: {
|
|
52
52
|
output: { format: 'iife', entryFileNames: 'game.js', assetFileNames: '[name][extname]' },
|
|
53
|
+
//an iife has no import.meta, and the bundler already swaps it for {} (vite's own
|
|
54
|
+
//preload helper reads it); the warning on every build is noise, not information
|
|
55
|
+
onLog(level, log, defaultHandler) {
|
|
56
|
+
if (log.code !== 'EMPTY_IMPORT_META') defaultHandler(level, log);
|
|
57
|
+
},
|
|
53
58
|
},
|
|
54
59
|
},
|
|
55
60
|
};
|
package/tools/webp-convert.mjs
CHANGED
|
@@ -58,7 +58,7 @@ export const WEBP_CONVERTIBLE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg']);
|
|
|
58
58
|
|
|
59
59
|
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
async function runCli() {
|
|
62
62
|
const args = process.argv.slice(2);
|
|
63
63
|
const input = args.find((a) => !a.startsWith('--'));
|
|
64
64
|
if (!input) {
|
|
@@ -88,3 +88,11 @@ if (isMain) {
|
|
|
88
88
|
(lossless ? ' [lossless]' : ` [lossy, quality ${quality}]`),
|
|
89
89
|
);
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
//no top-level await: a vite config loaded as CommonJS has to be able to require() this module
|
|
93
|
+
if (isMain) {
|
|
94
|
+
runCli().catch((error) => {
|
|
95
|
+
console.error(error);
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
});
|
|
98
|
+
}
|