@datamoc/mw_games 0.36.1 → 0.37.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 (114) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +6 -2
  3. package/api-surface.json +234 -0
  4. package/dist/assets/paths.d.ts.map +1 -1
  5. package/dist/assets/paths.js +3 -0
  6. package/dist/assets/paths.js.map +1 -1
  7. package/dist/audio/Playable.d.ts.map +1 -1
  8. package/dist/audio/Playable.js +14 -1
  9. package/dist/audio/Playable.js.map +1 -1
  10. package/dist/audio/Synth.d.ts +14 -0
  11. package/dist/audio/Synth.d.ts.map +1 -1
  12. package/dist/audio/Synth.js +16 -0
  13. package/dist/audio/Synth.js.map +1 -1
  14. package/dist/audio/index.d.ts +1 -1
  15. package/dist/audio/index.d.ts.map +1 -1
  16. package/dist/audio/index.js +1 -1
  17. package/dist/audio/index.js.map +1 -1
  18. package/dist/core/FixedStepper.d.ts +38 -0
  19. package/dist/core/FixedStepper.d.ts.map +1 -0
  20. package/dist/core/FixedStepper.js +56 -0
  21. package/dist/core/FixedStepper.js.map +1 -0
  22. package/dist/core/Input.d.ts +61 -0
  23. package/dist/core/Input.d.ts.map +1 -1
  24. package/dist/core/Input.js +101 -0
  25. package/dist/core/Input.js.map +1 -1
  26. package/dist/core/StoredValue.d.ts +11 -2
  27. package/dist/core/StoredValue.d.ts.map +1 -1
  28. package/dist/core/StoredValue.js +11 -2
  29. package/dist/core/StoredValue.js.map +1 -1
  30. package/dist/core/index.d.ts +3 -1
  31. package/dist/core/index.d.ts.map +1 -1
  32. package/dist/core/index.js +3 -1
  33. package/dist/core/index.js.map +1 -1
  34. package/dist/mw_games.global.js +121 -121
  35. package/dist/physics/PhysicsWorld.d.ts +123 -0
  36. package/dist/physics/PhysicsWorld.d.ts.map +1 -0
  37. package/dist/physics/PhysicsWorld.js +137 -0
  38. package/dist/physics/PhysicsWorld.js.map +1 -0
  39. package/dist/physics/XpbdWorld.d.ts +139 -0
  40. package/dist/physics/XpbdWorld.d.ts.map +1 -0
  41. package/dist/physics/XpbdWorld.js +207 -0
  42. package/dist/physics/XpbdWorld.js.map +1 -0
  43. package/dist/physics/index.d.ts +3 -0
  44. package/dist/physics/index.d.ts.map +1 -0
  45. package/dist/physics/index.js +2 -0
  46. package/dist/physics/index.js.map +1 -0
  47. package/dist/physics/xpbd.d.ts +3 -0
  48. package/dist/physics/xpbd.d.ts.map +1 -0
  49. package/dist/physics/xpbd.js +2 -0
  50. package/dist/physics/xpbd.js.map +1 -0
  51. package/dist/two-d/render/Camera.d.ts +6 -0
  52. package/dist/two-d/render/Camera.d.ts.map +1 -1
  53. package/dist/two-d/render/Camera.js +9 -0
  54. package/dist/two-d/render/Camera.js.map +1 -1
  55. package/dist/two-d/render/Particles.d.ts +10 -1
  56. package/dist/two-d/render/Particles.d.ts.map +1 -1
  57. package/dist/two-d/render/Particles.js +14 -7
  58. package/dist/two-d/render/Particles.js.map +1 -1
  59. package/dist/two-d/render/Shape2D.d.ts +7 -0
  60. package/dist/two-d/render/Shape2D.d.ts.map +1 -1
  61. package/dist/two-d/render/Shape2D.js +18 -0
  62. package/dist/two-d/render/Shape2D.js.map +1 -1
  63. package/dist/two-d/render/SpriteSheet.d.ts +5 -0
  64. package/dist/two-d/render/SpriteSheet.d.ts.map +1 -1
  65. package/dist/two-d/render/SpriteSheet.js +15 -0
  66. package/dist/two-d/render/SpriteSheet.js.map +1 -1
  67. package/dist/two-d/ui/Button.d.ts +7 -0
  68. package/dist/two-d/ui/Button.d.ts.map +1 -1
  69. package/dist/two-d/ui/Button.js +16 -2
  70. package/dist/two-d/ui/Button.js.map +1 -1
  71. package/dist/two-d/ui/Dropdown.d.ts +3 -0
  72. package/dist/two-d/ui/Dropdown.d.ts.map +1 -1
  73. package/dist/two-d/ui/Dropdown.js +3 -0
  74. package/dist/two-d/ui/Dropdown.js.map +1 -1
  75. package/dist/two-d/ui/DropdownView.d.ts +40 -0
  76. package/dist/two-d/ui/DropdownView.d.ts.map +1 -0
  77. package/dist/two-d/ui/DropdownView.js +75 -0
  78. package/dist/two-d/ui/DropdownView.js.map +1 -0
  79. package/dist/two-d/ui/Label.d.ts +2 -0
  80. package/dist/two-d/ui/Label.d.ts.map +1 -1
  81. package/dist/two-d/ui/Label.js +7 -0
  82. package/dist/two-d/ui/Label.js.map +1 -1
  83. package/dist/two-d/ui/Layout.d.ts +34 -0
  84. package/dist/two-d/ui/Layout.d.ts.map +1 -1
  85. package/dist/two-d/ui/Layout.js +47 -0
  86. package/dist/two-d/ui/Layout.js.map +1 -1
  87. package/dist/two-d/ui/Window.d.ts +7 -0
  88. package/dist/two-d/ui/Window.d.ts.map +1 -1
  89. package/dist/two-d/ui/Window.js +13 -0
  90. package/dist/two-d/ui/Window.js.map +1 -1
  91. package/dist/two-d/ui/index.d.ts +4 -2
  92. package/dist/two-d/ui/index.d.ts.map +1 -1
  93. package/dist/two-d/ui/index.js +2 -1
  94. package/dist/two-d/ui/index.js.map +1 -1
  95. package/dist/version.d.ts +1 -1
  96. package/dist/version.js +1 -1
  97. package/package.json +21 -2
  98. package/sbom.cdx.json +58 -6
  99. package/tools/browser-driver.d.mts +14 -0
  100. package/tools/browser-driver.mjs +62 -0
  101. package/tools/build-tablebases.mjs +6 -2
  102. package/tools/classic-html.mjs +9 -1
  103. package/tools/compile-openings.mjs +6 -2
  104. package/tools/compile-resources.d.mts +5 -0
  105. package/tools/compile-resources.mjs +45 -4
  106. package/tools/compress-dist.mjs +15 -3
  107. package/tools/emit-page.mjs +5 -1
  108. package/tools/init.d.mts +5 -0
  109. package/tools/init.mjs +145 -0
  110. package/tools/play-uci.mjs +6 -3
  111. package/tools/sbom.mjs +5 -1
  112. package/tools/single-file.mjs +9 -1
  113. package/tools/vite.mjs +5 -0
  114. package/tools/webp-convert.mjs +9 -1
package/sbom.cdx.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.6",
4
- "serialNumber": "urn:uuid:17cd32d6-94ff-58fa-90e1-9bec6171c507",
4
+ "serialNumber": "urn:uuid:fc255c4f-4827-5c1a-9b7d-2851ddb9d40e",
5
5
  "version": 1,
6
6
  "metadata": {
7
7
  "tools": {
@@ -10,16 +10,16 @@
10
10
  "type": "application",
11
11
  "group": "datamoc",
12
12
  "name": "tools/sbom.mjs",
13
- "version": "0.36.1"
13
+ "version": "0.37.0"
14
14
  }
15
15
  ]
16
16
  },
17
17
  "component": {
18
18
  "type": "library",
19
- "bom-ref": "pkg:npm/%40datamoc/mw_games@0.36.1",
19
+ "bom-ref": "pkg:npm/%40datamoc/mw_games@0.37.0",
20
20
  "name": "@datamoc/mw_games",
21
- "version": "0.36.1",
22
- "purl": "pkg:npm/%40datamoc/mw_games@0.36.1",
21
+ "version": "0.37.0",
22
+ "purl": "pkg:npm/%40datamoc/mw_games@0.37.0",
23
23
  "description": "A local-file game framework: renderer-free game logic, drawn through PixiJS in 2D or Babylon.js in 3D, with mobile and desktop packaging.",
24
24
  "licenses": [
25
25
  {
@@ -1375,6 +1375,27 @@
1375
1375
  }
1376
1376
  ]
1377
1377
  },
1378
+ {
1379
+ "type": "library",
1380
+ "bom-ref": "pkg:npm/%40types/matter-js@0.20.2",
1381
+ "name": "@types/matter-js",
1382
+ "version": "0.20.2",
1383
+ "purl": "pkg:npm/%40types/matter-js@0.20.2",
1384
+ "scope": "excluded",
1385
+ "hashes": [
1386
+ {
1387
+ "alg": "SHA-512",
1388
+ "content": "dcf3cacb7431bd9f3d87dfb0741576e3cf08d492d5b3b0c4a4890fbe03bc242d6651d89548edfb648bd572d3930fb848abc3802f6809dee8064ae522a7a0e10b"
1389
+ }
1390
+ ],
1391
+ "licenses": [
1392
+ {
1393
+ "license": {
1394
+ "id": "MIT"
1395
+ }
1396
+ }
1397
+ ]
1398
+ },
1378
1399
  {
1379
1400
  "type": "library",
1380
1401
  "bom-ref": "pkg:npm/%40types/node@26.6.5",
@@ -3160,6 +3181,27 @@
3160
3181
  }
3161
3182
  ]
3162
3183
  },
3184
+ {
3185
+ "type": "library",
3186
+ "bom-ref": "pkg:npm/matter-js@0.20.0",
3187
+ "name": "matter-js",
3188
+ "version": "0.20.0",
3189
+ "purl": "pkg:npm/matter-js@0.20.0",
3190
+ "scope": "excluded",
3191
+ "hashes": [
3192
+ {
3193
+ "alg": "SHA-512",
3194
+ "content": "882f5f611ef3553dc7a69367b05b29f573a841d40ddad5327da2a0e021cb1fc6cdfa3e864f81b0ec81f6acfd2d7e501e6eb1c5c3bdf4451dc3915499457f21c0"
3195
+ }
3196
+ ],
3197
+ "licenses": [
3198
+ {
3199
+ "license": {
3200
+ "id": "MIT"
3201
+ }
3202
+ }
3203
+ ]
3204
+ },
3163
3205
  {
3164
3206
  "type": "library",
3165
3207
  "bom-ref": "pkg:npm/minimatch@10.2.6",
@@ -4601,7 +4643,7 @@
4601
4643
  ]
4602
4644
  },
4603
4645
  {
4604
- "ref": "pkg:npm/%40datamoc/mw_games@0.36.1",
4646
+ "ref": "pkg:npm/%40datamoc/mw_games@0.37.0",
4605
4647
  "dependsOn": [
4606
4648
  "pkg:npm/%40babylonjs/core@9.30.0",
4607
4649
  "pkg:npm/%40babylonjs/loaders@9.30.0",
@@ -4610,9 +4652,11 @@
4610
4652
  "pkg:npm/%40capacitor/core@8.5.3",
4611
4653
  "pkg:npm/%40capacitor/ios@8.5.3",
4612
4654
  "pkg:npm/%40datamoc/pyroeis@0.9.0",
4655
+ "pkg:npm/%40types/matter-js@0.20.2",
4613
4656
  "pkg:npm/%40types/node@26.6.5",
4614
4657
  "pkg:npm/%40types/ws@8.18.2",
4615
4658
  "pkg:npm/json5@2.2.3",
4659
+ "pkg:npm/matter-js@0.20.0",
4616
4660
  "pkg:npm/multithreading@0.3.52",
4617
4661
  "pkg:npm/pixi.js@8.22.0",
4618
4662
  "pkg:npm/playwright-core@1.64.0",
@@ -4936,6 +4980,10 @@
4936
4980
  "pkg:npm/%40types/node@26.6.5"
4937
4981
  ]
4938
4982
  },
4983
+ {
4984
+ "ref": "pkg:npm/%40types/matter-js@0.20.2",
4985
+ "dependsOn": []
4986
+ },
4939
4987
  {
4940
4988
  "ref": "pkg:npm/%40types/node@26.6.5",
4941
4989
  "dependsOn": [
@@ -5337,6 +5385,10 @@
5337
5385
  "ref": "pkg:npm/lru-cache@11.5.3",
5338
5386
  "dependsOn": []
5339
5387
  },
5388
+ {
5389
+ "ref": "pkg:npm/matter-js@0.20.0",
5390
+ "dependsOn": []
5391
+ },
5340
5392
  {
5341
5393
  "ref": "pkg:npm/minimatch@10.2.6",
5342
5394
  "dependsOn": [
@@ -38,6 +38,8 @@ export interface BrowserDriver {
38
38
  evaluate<T>(expression: string): Promise<T>;
39
39
  script<Args extends unknown[], T>(fn: (...args: Args) => T | Promise<T>, ...args: Args): Promise<T>;
40
40
  tap(xFraction: number, yFraction: number): Promise<void>;
41
+ /** presses at one canvas fraction, moves to another with the button held in `steps` moves (default 8), releases */
42
+ drag(fromX: number, fromY: number, toX: number, toY: number, steps?: number): Promise<void>;
41
43
  screenshot(): Promise<Buffer>;
42
44
  consoleErrors(): string[];
43
45
  close(): Promise<void>;
@@ -77,6 +79,18 @@ export function parseBidiEndpoint(stderr: string): string | null;
77
79
  /** the CDP Input.dispatchMouseEvent sequence a tap is, at CSS pixels */
78
80
  export function buildTapSequence(x: number, y: number): CdpMouseEvent[];
79
81
 
82
+ /** the CDP Input.dispatchMouseEvent sequence a drag is: press, held moves, release, at CSS pixels */
83
+ export function buildDragSequence(
84
+ x1: number,
85
+ y1: number,
86
+ x2: number,
87
+ y2: number,
88
+ steps?: number,
89
+ ): Record<string, unknown>[];
90
+
91
+ /** the in-page synthetic pointer drag, the Firefox path (see pageTapSource) */
92
+ export function pageDragSource(x1: number, y1: number, x2: number, y2: number, steps?: number): string;
93
+
80
94
  /** the BiDi input.performActions pointer half of a tap, at CSS pixels */
81
95
  export function buildPointerActions(x: number, y: number): BidiPointerAction[];
82
96
 
@@ -146,6 +146,47 @@ export function buildTapSequence(x, y) {
146
146
  ];
147
147
  }
148
148
 
149
+ /** `steps` evenly spaced points from (x1, y1) to (x2, y2), both ends included */
150
+ function dragPath(x1, y1, x2, y2, steps) {
151
+ const count = Math.max(1, Math.floor(steps));
152
+ return Array.from({ length: count + 1 }, (_, i) => ({
153
+ x: Math.round(x1 + ((x2 - x1) * i) / count),
154
+ y: Math.round(y1 + ((y2 - y1) * i) / count),
155
+ }));
156
+ }
157
+
158
+ /**
159
+ * The CDP `Input.dispatchMouseEvent` sequence a drag is: press at the start, move through
160
+ * `steps` intermediate points with the button held, release at the end - the down, move, up a
161
+ * pointer-drag handler (a bridge builder, a slider, a pan) needs and `tap` cannot give.
162
+ */
163
+ export function buildDragSequence(x1, y1, x2, y2, steps = 8) {
164
+ const path = dragPath(x1, y1, x2, y2, steps);
165
+ const last = path[path.length - 1];
166
+ return [
167
+ { type: 'mouseMoved', x: x1, y: y1, button: 'none', buttons: 0 },
168
+ { type: 'mousePressed', x: x1, y: y1, button: 'left', buttonValue: 0, clickCount: 1, buttons: 1 },
169
+ ...path.slice(1).map((p) => ({ type: 'mouseMoved', x: p.x, y: p.y, button: 'left', buttons: 1 })),
170
+ { type: 'mouseReleased', x: last.x, y: last.y, button: 'left', buttonValue: 0, clickCount: 1, buttons: 0 },
171
+ ];
172
+ }
173
+
174
+ /** the in-page counterpart of `buildDragSequence` for Firefox, for the reason `pageTapSource` exists */
175
+ export function pageDragSource(x1, y1, x2, y2, steps = 8) {
176
+ const path = dragPath(x1, y1, x2, y2, steps);
177
+ return `(() => {
178
+ const canvas = document.querySelector("canvas");
179
+ const base = { bubbles: true, cancelable: true, composed: true, pointerId: 1, pointerType: "mouse", isPrimary: true, button: 0 };
180
+ const fire = (type, x, y, buttons) => canvas.dispatchEvent(new PointerEvent(type, { ...base, clientX: x, clientY: y, buttons }));
181
+ const path = ${JSON.stringify(path)};
182
+ fire("pointermove", path[0].x, path[0].y, 0);
183
+ fire("pointerdown", path[0].x, path[0].y, 1);
184
+ for (const p of path.slice(1)) fire("pointermove", p.x, p.y, 1);
185
+ const end = path[path.length - 1];
186
+ fire("pointerup", end.x, end.y, 0);
187
+ })()`;
188
+ }
189
+
149
190
  /**
150
191
  * The named keys a game binds, with everything each input protocol needs: the
151
192
  * DOM `code`, the Windows virtual-key code CDP wants, the character the key types
@@ -520,6 +561,13 @@ async function waitForReadyPage(evaluate, consoleErrors, url, timeoutMs = READY_
520
561
  }
521
562
  }
522
563
 
564
+ /** the canvas rectangle in CSS pixels, which canvas fractions are measured against */
565
+ function canvasBox(evaluate) {
566
+ return evaluate(
567
+ '(() => { const box = document.querySelector("canvas").getBoundingClientRect(); return { x: box.x, y: box.y, w: box.width, h: box.height }; })()',
568
+ );
569
+ }
570
+
523
571
  /** runs a consumer function against the live page, awaiting it when it is async */
524
572
  function runPageScript(evaluate, fn, args) {
525
573
  return evaluate(`(${fn.toString()})(...${JSON.stringify(args)})`, true);
@@ -616,6 +664,14 @@ export async function driveChrome(options = {}) {
616
664
  await cdp.send('Input.dispatchMouseEvent', event);
617
665
  }
618
666
  },
667
+ /** press at one canvas fraction, move to another with the button held, release */
668
+ drag: async (fromX, fromY, toX, toY, steps) => {
669
+ const box = await canvasBox(evaluate);
670
+ const at = (fx, fy) => [Math.round(box.x + box.w * fx), Math.round(box.y + box.h * fy)];
671
+ for (const event of buildDragSequence(...at(fromX, fromY), ...at(toX, toY), steps)) {
672
+ await cdp.send('Input.dispatchMouseEvent', event);
673
+ }
674
+ },
619
675
  /** presses and releases one key through Chrome's trusted input channel (P41) */
620
676
  press: async (key, holdMs = 60) => {
621
677
  const sequence = cdpKeySequence(key);
@@ -769,6 +825,12 @@ export async function driveFirefox(options = {}) {
769
825
  //actions never reach Pixi 8's EventSystem, and this is the measured-working path.
770
826
  await evaluate(pageTapSource(x, y), true);
771
827
  },
828
+ /** press at one canvas fraction, move to another with the button held, release */
829
+ drag: async (fromX, fromY, toX, toY, steps) => {
830
+ const box = await canvasBox(evaluate);
831
+ const at = (fx, fy) => [Math.round(box.x + box.w * fx), Math.round(box.y + box.h * fy)];
832
+ await evaluate(pageDragSource(...at(fromX, fromY), ...at(toX, toY), steps), true);
833
+ },
772
834
  /** presses and releases one key through WebDriver's key actions (P41) */
773
835
  press: async (key, holdMs = 60) => {
774
836
  const actions = bidiKeyActions(key, holdMs);
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
2
4
  import { mkdirSync, writeFileSync } from 'node:fs';
3
- import { join, relative, basename } from 'node:path';
5
+ import { join, relative } from 'node:path';
4
6
  import { huffEncode } from '../src/board/tablebase.ts';
5
7
  import { spawn } from '../src/threads/spawn.ts';
6
8
 
@@ -451,7 +453,9 @@ function defaultJobs() {
451
453
  return 2;
452
454
  }
453
455
 
454
- const isMain = process.argv[1] && import.meta.url.endsWith(basename(process.argv[1]));
456
+ //the real path, since an npm `bin` shim reaches this file through a symlink; a suffix test on the
457
+ //name is not enough (`compress-dist.mjs` ends with `t.mjs`)
458
+ const isMain = process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
455
459
 
456
460
  if (isMain) {
457
461
  const args = process.argv.slice(2);
@@ -37,7 +37,7 @@ export function toClassicScript(html) {
37
37
  return { html: html.replace(scriptTag, `<script defer src="${match[1]}"></script>`), src: match[1] };
38
38
  }
39
39
 
40
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
40
+ async function runCli() {
41
41
  const input = process.argv[2];
42
42
  if (!input) {
43
43
  console.error('Usage: node tools/classic-html.mjs dist/index.html');
@@ -59,3 +59,11 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
59
59
  }
60
60
  }
61
61
  }
62
+
63
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
64
+ //no top-level await: a vite config loaded as CommonJS has to be able to require() this module
65
+ runCli().catch((error) => {
66
+ console.error(error);
67
+ process.exitCode = 1;
68
+ });
69
+ }
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
2
4
  import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
3
- import { basename, join, relative } from 'node:path';
5
+ import { join, relative } from 'node:path';
4
6
  import { applyMove, legalMoves, positionKey, sq, squareName, startingChess } from '../src/board/chess.ts';
5
7
  import { parseUciMove } from '../src/board/openings.ts';
6
8
 
@@ -115,7 +117,9 @@ export function compileBook(dir) {
115
117
  return { version: 1, positions };
116
118
  }
117
119
 
118
- const isMain = process.argv[1] && import.meta.url.endsWith(basename(process.argv[1]));
120
+ //the real path, since an npm `bin` shim reaches this file through a symlink; a suffix test on the
121
+ //name is not enough (`compress-dist.mjs` ends with `t.mjs`)
122
+ const isMain = process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
119
123
 
120
124
  if (isMain) {
121
125
  const [dir, out] = process.argv.slice(2);
@@ -24,6 +24,11 @@ export interface CompileResourcesResult {
24
24
  embeddedBytes: number;
25
25
  webpConverted: number;
26
26
  skipped: number;
27
+ /** the `CREDITS.txt` text when the asset folder holds a `credits.json`, else null */
28
+ credits: string | null;
27
29
  }
28
30
 
31
+ /** the plain-text credits for a manifest (an array, or `{ credits: [...] }`, of `{ name, license, author?, url? }`) */
32
+ export function creditsText(manifest: unknown): string;
33
+
29
34
  export function compileResources(options: CompileResourcesOptions): Promise<CompileResourcesResult>;
@@ -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, basename } from 'node:path';
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
- return { groups: written, rawBytes, embeddedBytes, webpConverted, skipped };
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
- const isMain = process.argv[1] && import.meta.url.endsWith(basename(process.argv[1]));
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
- if (isMain) {
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 };
@@ -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, basename } from 'node:path';
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
- const isMain = process.argv[1] && import.meta.url.endsWith(basename(process.argv[1]));
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
- if (isMain) {
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
+ }
@@ -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
- await main(process.argv.slice(2));
189
+ main(process.argv.slice(2)).catch((error) => {
190
+ console.error(error);
191
+ process.exitCode = 1;
192
+ });
@@ -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
+ }
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, readdirSync } from 'node:fs';
3
- import { join, basename, extname } from 'node:path';
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
- const isMain = process.argv[1] && import.meta.url.endsWith(basename(process.argv[1]));
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
- await main(process.argv.slice(2));
333
+ main(process.argv.slice(2)).catch((error) => {
334
+ console.error(error);
335
+ process.exitCode = 1;
336
+ });
@@ -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
- if (isMain) {
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
+ }