@datamoc/mw_games 0.37.0 → 0.39.1

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 (113) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +26 -0
  3. package/api-surface.json +650 -2
  4. package/dist/assets/SvgParts.d.ts +31 -0
  5. package/dist/assets/SvgParts.d.ts.map +1 -0
  6. package/dist/assets/SvgParts.js +54 -0
  7. package/dist/assets/SvgParts.js.map +1 -0
  8. package/dist/assets/SvgSource.d.ts +3 -0
  9. package/dist/assets/SvgSource.d.ts.map +1 -0
  10. package/dist/assets/SvgSource.js +22 -0
  11. package/dist/assets/SvgSource.js.map +1 -0
  12. package/dist/assets/index.d.ts +2 -0
  13. package/dist/assets/index.d.ts.map +1 -1
  14. package/dist/assets/index.js +1 -0
  15. package/dist/assets/index.js.map +1 -1
  16. package/dist/assets/svg.d.ts.map +1 -1
  17. package/dist/assets/svg.js +9 -5
  18. package/dist/assets/svg.js.map +1 -1
  19. package/dist/mw_games.global.js +110 -110
  20. package/dist/mwl/presentation.d.ts +70 -0
  21. package/dist/mwl/presentation.d.ts.map +1 -0
  22. package/dist/mwl/presentation.js +419 -0
  23. package/dist/mwl/presentation.js.map +1 -0
  24. package/dist/physics/PhysicsWorld.d.ts +49 -17
  25. package/dist/physics/PhysicsWorld.d.ts.map +1 -1
  26. package/dist/physics/PhysicsWorld.js +297 -59
  27. package/dist/physics/PhysicsWorld.js.map +1 -1
  28. package/dist/physics/XpbdWorld.d.ts +1 -1
  29. package/dist/physics/XpbdWorld.js +1 -1
  30. package/dist/physics/cloth/index.d.ts +98 -0
  31. package/dist/physics/cloth/index.d.ts.map +1 -0
  32. package/dist/physics/cloth/index.js +207 -0
  33. package/dist/physics/cloth/index.js.map +1 -0
  34. package/dist/physics/index.d.ts +4 -0
  35. package/dist/physics/index.d.ts.map +1 -1
  36. package/dist/physics/index.js +2 -0
  37. package/dist/physics/index.js.map +1 -1
  38. package/dist/physics/spring/index.d.ts +90 -0
  39. package/dist/physics/spring/index.d.ts.map +1 -0
  40. package/dist/physics/spring/index.js +97 -0
  41. package/dist/physics/spring/index.js.map +1 -0
  42. package/dist/skeleton/index.d.ts +94 -13
  43. package/dist/skeleton/index.d.ts.map +1 -1
  44. package/dist/skeleton/index.js +192 -73
  45. package/dist/skeleton/index.js.map +1 -1
  46. package/dist/two-d/presentation/MwlPresentation.d.ts +110 -0
  47. package/dist/two-d/presentation/MwlPresentation.d.ts.map +1 -0
  48. package/dist/two-d/presentation/MwlPresentation.js +755 -0
  49. package/dist/two-d/presentation/MwlPresentation.js.map +1 -0
  50. package/dist/two-d/presentation/index.d.ts +3 -0
  51. package/dist/two-d/presentation/index.d.ts.map +1 -0
  52. package/dist/two-d/presentation/index.js +2 -0
  53. package/dist/two-d/presentation/index.js.map +1 -0
  54. package/dist/two-d/render/ClothSprite.d.ts +83 -0
  55. package/dist/two-d/render/ClothSprite.d.ts.map +1 -0
  56. package/dist/two-d/render/ClothSprite.js +105 -0
  57. package/dist/two-d/render/ClothSprite.js.map +1 -0
  58. package/dist/two-d/render/HazeLayer.d.ts +112 -0
  59. package/dist/two-d/render/HazeLayer.d.ts.map +1 -0
  60. package/dist/two-d/render/HazeLayer.js +185 -0
  61. package/dist/two-d/render/HazeLayer.js.map +1 -0
  62. package/dist/two-d/render/NeonGlow.d.ts +100 -0
  63. package/dist/two-d/render/NeonGlow.d.ts.map +1 -0
  64. package/dist/two-d/render/NeonGlow.js +183 -0
  65. package/dist/two-d/render/NeonGlow.js.map +1 -0
  66. package/dist/two-d/render/SkeletalSprite.d.ts +5 -2
  67. package/dist/two-d/render/SkeletalSprite.d.ts.map +1 -1
  68. package/dist/two-d/render/SkeletalSprite.js +27 -3
  69. package/dist/two-d/render/SkeletalSprite.js.map +1 -1
  70. package/dist/two-d/render/SpringSprite.d.ts +40 -0
  71. package/dist/two-d/render/SpringSprite.d.ts.map +1 -0
  72. package/dist/two-d/render/SpringSprite.js +39 -0
  73. package/dist/two-d/render/SpringSprite.js.map +1 -0
  74. package/dist/two-d/render/SpriteDeform.d.ts +156 -0
  75. package/dist/two-d/render/SpriteDeform.d.ts.map +1 -0
  76. package/dist/two-d/render/SpriteDeform.js +278 -0
  77. package/dist/two-d/render/SpriteDeform.js.map +1 -0
  78. package/dist/two-d/render/SvgSkeleton.d.ts +54 -0
  79. package/dist/two-d/render/SvgSkeleton.d.ts.map +1 -0
  80. package/dist/two-d/render/SvgSkeleton.js +112 -0
  81. package/dist/two-d/render/SvgSkeleton.js.map +1 -0
  82. package/dist/two-d/render/index.d.ts +12 -0
  83. package/dist/two-d/render/index.d.ts.map +1 -1
  84. package/dist/two-d/render/index.js +6 -0
  85. package/dist/two-d/render/index.js.map +1 -1
  86. package/dist/two-d/ui/Bar.d.ts +3 -1
  87. package/dist/two-d/ui/Bar.d.ts.map +1 -1
  88. package/dist/two-d/ui/Bar.js +5 -0
  89. package/dist/two-d/ui/Bar.js.map +1 -1
  90. package/dist/two-d/ui/Button.d.ts +12 -0
  91. package/dist/two-d/ui/Button.d.ts.map +1 -1
  92. package/dist/two-d/ui/Button.js +16 -4
  93. package/dist/two-d/ui/Button.js.map +1 -1
  94. package/dist/two-d/ui/Window.d.ts +10 -0
  95. package/dist/two-d/ui/Window.d.ts.map +1 -1
  96. package/dist/two-d/ui/Window.js +17 -6
  97. package/dist/two-d/ui/Window.js.map +1 -1
  98. package/dist/two-d/ui/index.d.ts +2 -0
  99. package/dist/two-d/ui/index.d.ts.map +1 -1
  100. package/dist/two-d/ui/index.js.map +1 -1
  101. package/dist/version.d.ts +1 -1
  102. package/dist/version.js +1 -1
  103. package/package.json +24 -7
  104. package/sbom.cdx.json +6 -58
  105. package/tools/asset-report.d.mts +37 -0
  106. package/tools/asset-report.mjs +157 -0
  107. package/tools/compile-resources.d.mts +2 -0
  108. package/tools/compile-resources.mjs +23 -2
  109. package/tools/emit-page.d.mts +2 -0
  110. package/tools/emit-page.mjs +11 -1
  111. package/tools/verify-scenarios.d.mts +15 -0
  112. package/tools/verify-scenarios.mjs +129 -0
  113. package/tools/vite.mjs +4 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datamoc/mw_games",
3
- "version": "0.37.0",
3
+ "version": "0.39.1",
4
4
  "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.",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {
@@ -35,6 +35,7 @@
35
35
  ],
36
36
  "bin": {
37
37
  "mwg-bench": "tools/benchmark-browser.mjs",
38
+ "mwg-assets": "tools/asset-report.mjs",
38
39
  "mwg-audit-adoptions": "tools/audit-adoptions.mjs",
39
40
  "mwg-emit": "tools/emit-page.mjs",
40
41
  "mwg-extract-rgssad": "tools/extract-rgssad.mjs",
@@ -45,6 +46,7 @@
45
46
  "mwg-openings": "tools/compile-openings.mjs",
46
47
  "mwg-placeholder-assets": "tools/make-example-assets.mjs",
47
48
  "mwg-sbom": "tools/sbom.mjs",
49
+ "mwg-scenarios": "tools/verify-scenarios.mjs",
48
50
  "mwg-size": "tools/bundle-size.mjs",
49
51
  "mwg-smoke": "tools/browser-smoke.mjs",
50
52
  "mwg-tablebases": "tools/build-tablebases.mjs",
@@ -57,6 +59,8 @@
57
59
  "dist",
58
60
  "tools/compile-resources.mjs",
59
61
  "tools/compile-resources.d.mts",
62
+ "tools/asset-report.mjs",
63
+ "tools/asset-report.d.mts",
60
64
  "tools/webp-convert.mjs",
61
65
  "tools/webp-convert.d.mts",
62
66
  "tools/extract-html.mjs",
@@ -71,6 +75,8 @@
71
75
  "tools/play-uci.d.mts",
72
76
  "tools/init.mjs",
73
77
  "tools/init.d.mts",
78
+ "tools/verify-scenarios.mjs",
79
+ "tools/verify-scenarios.d.mts",
74
80
  "tools/classic-html.mjs",
75
81
  "tools/classic-html.d.mts",
76
82
  "tools/sbom.mjs",
@@ -116,6 +122,14 @@
116
122
  "MWG - Best Practices.md"
117
123
  ],
118
124
  "exports": {
125
+ "./mwl/presentation": {
126
+ "types": "./dist/mwl/presentation.d.ts",
127
+ "import": "./dist/mwl/presentation.js"
128
+ },
129
+ "./two-d/presentation": {
130
+ "types": "./dist/two-d/presentation/index.d.ts",
131
+ "import": "./dist/two-d/presentation/index.js"
132
+ },
119
133
  ".": {
120
134
  "types": "./dist/index.d.ts",
121
135
  "import": "./dist/index.js"
@@ -214,6 +228,7 @@
214
228
  },
215
229
  "./tools/benchmark-browser": "./tools/benchmark-browser.mjs",
216
230
  "./tools/audit-adoptions": "./tools/audit-adoptions.mjs",
231
+ "./tools/asset-report": "./tools/asset-report.mjs",
217
232
  "./tools/browser-driver": "./tools/browser-driver.mjs",
218
233
  "./tools/benchmark-history": "./tools/benchmark-history.mjs",
219
234
  "./tools/browser-smoke": "./tools/browser-smoke.mjs",
@@ -223,6 +238,7 @@
223
238
  "./tools/compile-openings": "./tools/compile-openings.mjs",
224
239
  "./tools/build-tablebases": "./tools/build-tablebases.mjs",
225
240
  "./tools/init": "./tools/init.mjs",
241
+ "./tools/verify-scenarios": "./tools/verify-scenarios.mjs",
226
242
  "./tools/play-uci": "./tools/play-uci.mjs",
227
243
  "./tools/compress-dist": "./tools/compress-dist.mjs",
228
244
  "./tools/csp": "./tools/csp.mjs",
@@ -298,6 +314,9 @@
298
314
  "i18n:edit": "node tools/i18n-edit.mjs",
299
315
  "i18n:check": "node tools/i18n-edit.mjs --check",
300
316
  "example:ui": "vite --config examples/interface/vite.config.ts",
317
+ "example:mwl-presentation": "npm run build:pkg && node examples/mwl-presentation/generate.mjs && vite --config examples/mwl-presentation/vite.config.ts",
318
+ "example:mwl-presentation:build": "npm run build:pkg && node examples/mwl-presentation/generate.mjs && vite build --config examples/mwl-presentation/vite.config.ts && node tools/emit-page.mjs examples/mwl-presentation --assets=examples/mwl-presentation/assets",
319
+ "visual:smoke:presentation": "npm run example:mwl-presentation:build && node tools/presentation-smoke.mjs",
301
320
  "example:ui:build": "vite build --config examples/interface/vite.config.ts && node tools/emit-page.mjs examples/interface --assets=examples/assets",
302
321
  "example:dialogue": "vite --config examples/dialogue/vite.config.ts",
303
322
  "example:dialogue:build": "vite build --config examples/dialogue/vite.config.ts && node tools/emit-page.mjs examples/dialogue --assets=examples/assets",
@@ -313,6 +332,10 @@
313
332
  "example:multi-turn-beam:build": "vite build --config examples/multi-turn-beam/vite.config.ts && node tools/emit-page.mjs examples/multi-turn-beam --assets=examples/assets",
314
333
  "example:chess": "vite --config examples/chess/vite.config.ts",
315
334
  "example:chess:build": "vite build --config examples/chess/vite.config.ts && node tools/emit-page.mjs examples/chess --assets=examples/assets",
335
+ "example:cloth": "vite --config examples/cloth/vite.config.ts",
336
+ "example:cloth:build": "vite build --config examples/cloth/vite.config.ts && node tools/emit-page.mjs examples/cloth --assets=examples/assets",
337
+ "example:spring": "vite --config examples/spring/vite.config.ts",
338
+ "example:spring:build": "vite build --config examples/spring/vite.config.ts && node tools/emit-page.mjs examples/spring --assets=examples/assets",
316
339
  "example:tower-defense": "vite --config examples/tower-defense/vite.config.ts",
317
340
  "example:tower-defense:build": "vite build --config examples/tower-defense/vite.config.ts && node tools/emit-page.mjs examples/tower-defense --assets=examples/assets",
318
341
  "example:3d": "vite --config examples/three-d/vite.config.ts",
@@ -384,7 +407,6 @@
384
407
  "@babylonjs/core": "^9.25.0",
385
408
  "@babylonjs/loaders": "^9.25.0",
386
409
  "@capacitor/core": "^8.5.2",
387
- "matter-js": "^0.20.0",
388
410
  "pixi.js": "^8.20.1",
389
411
  "playwright-core": "^1.62.1",
390
412
  "ws": "^8.21.3"
@@ -407,9 +429,6 @@
407
429
  },
408
430
  "ws": {
409
431
  "optional": true
410
- },
411
- "matter-js": {
412
- "optional": true
413
432
  }
414
433
  },
415
434
  "devDependencies": {
@@ -419,10 +438,8 @@
419
438
  "@capacitor/cli": "^8.5.2",
420
439
  "@capacitor/core": "^8.5.2",
421
440
  "@capacitor/ios": "^8.5.2",
422
- "@types/matter-js": "^0.20.2",
423
441
  "@types/node": "^26.4.1",
424
442
  "@types/ws": "^8.18.1",
425
- "matter-js": "^0.20.0",
426
443
  "multithreading": "^0.3.52",
427
444
  "pixi.js": "^8.20.1",
428
445
  "playwright-core": "^1.62.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:fc255c4f-4827-5c1a-9b7d-2851ddb9d40e",
4
+ "serialNumber": "urn:uuid:87d5da55-61f5-526f-9055-cdcdd6680517",
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.37.0"
13
+ "version": "0.39.1"
14
14
  }
15
15
  ]
16
16
  },
17
17
  "component": {
18
18
  "type": "library",
19
- "bom-ref": "pkg:npm/%40datamoc/mw_games@0.37.0",
19
+ "bom-ref": "pkg:npm/%40datamoc/mw_games@0.39.1",
20
20
  "name": "@datamoc/mw_games",
21
- "version": "0.37.0",
22
- "purl": "pkg:npm/%40datamoc/mw_games@0.37.0",
21
+ "version": "0.39.1",
22
+ "purl": "pkg:npm/%40datamoc/mw_games@0.39.1",
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,27 +1375,6 @@
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
- },
1399
1378
  {
1400
1379
  "type": "library",
1401
1380
  "bom-ref": "pkg:npm/%40types/node@26.6.5",
@@ -3181,27 +3160,6 @@
3181
3160
  }
3182
3161
  ]
3183
3162
  },
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
- },
3205
3163
  {
3206
3164
  "type": "library",
3207
3165
  "bom-ref": "pkg:npm/minimatch@10.2.6",
@@ -4643,7 +4601,7 @@
4643
4601
  ]
4644
4602
  },
4645
4603
  {
4646
- "ref": "pkg:npm/%40datamoc/mw_games@0.37.0",
4604
+ "ref": "pkg:npm/%40datamoc/mw_games@0.39.1",
4647
4605
  "dependsOn": [
4648
4606
  "pkg:npm/%40babylonjs/core@9.30.0",
4649
4607
  "pkg:npm/%40babylonjs/loaders@9.30.0",
@@ -4652,11 +4610,9 @@
4652
4610
  "pkg:npm/%40capacitor/core@8.5.3",
4653
4611
  "pkg:npm/%40capacitor/ios@8.5.3",
4654
4612
  "pkg:npm/%40datamoc/pyroeis@0.9.0",
4655
- "pkg:npm/%40types/matter-js@0.20.2",
4656
4613
  "pkg:npm/%40types/node@26.6.5",
4657
4614
  "pkg:npm/%40types/ws@8.18.2",
4658
4615
  "pkg:npm/json5@2.2.3",
4659
- "pkg:npm/matter-js@0.20.0",
4660
4616
  "pkg:npm/multithreading@0.3.52",
4661
4617
  "pkg:npm/pixi.js@8.22.0",
4662
4618
  "pkg:npm/playwright-core@1.64.0",
@@ -4980,10 +4936,6 @@
4980
4936
  "pkg:npm/%40types/node@26.6.5"
4981
4937
  ]
4982
4938
  },
4983
- {
4984
- "ref": "pkg:npm/%40types/matter-js@0.20.2",
4985
- "dependsOn": []
4986
- },
4987
4939
  {
4988
4940
  "ref": "pkg:npm/%40types/node@26.6.5",
4989
4941
  "dependsOn": [
@@ -5385,10 +5337,6 @@
5385
5337
  "ref": "pkg:npm/lru-cache@11.5.3",
5386
5338
  "dependsOn": []
5387
5339
  },
5388
- {
5389
- "ref": "pkg:npm/matter-js@0.20.0",
5390
- "dependsOn": []
5391
- },
5392
5340
  {
5393
5341
  "ref": "pkg:npm/minimatch@10.2.6",
5394
5342
  "dependsOn": [
@@ -0,0 +1,37 @@
1
+ export interface AssetReportFile {
2
+ path: string;
3
+ raw: number;
4
+ gzip: number;
5
+ brotli: number;
6
+ sha256: string;
7
+ /** This file's gzip bytes over the page's total gzip bytes, 0 when the page is empty. */
8
+ share: number;
9
+ }
10
+ export interface AssetReportSource {
11
+ path: string;
12
+ raw: number;
13
+ sha256: string;
14
+ }
15
+ export interface AssetReportTotals {
16
+ files: number;
17
+ raw: number;
18
+ gzip: number;
19
+ brotli: number;
20
+ assetFiles: number;
21
+ assetRaw: number;
22
+ }
23
+ export interface AssetReport {
24
+ dist: string;
25
+ files: AssetReportFile[];
26
+ assets: AssetReportSource[];
27
+ /** Groups of source paths with byte-identical content, sorted; empty when none exist. */
28
+ duplicates: string[][];
29
+ totals: AssetReportTotals;
30
+ }
31
+ export function fingerprint(data: Uint8Array): string;
32
+ export function compressedSizes(data: Uint8Array): { gzip: number; brotli: number };
33
+ export function assetReport(options: {
34
+ dist: string;
35
+ assets?: string;
36
+ mime?: Record<string, string>;
37
+ }): Promise<AssetReport>;
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto';
3
+ import { gzipSync, brotliCompressSync, constants } from 'node:zlib';
4
+ import { realpathSync, statSync } from 'node:fs';
5
+ import { readFile, writeFile } from 'node:fs/promises';
6
+ import { join, relative, sep, extname } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { walk, DEFAULT_MIME } from './compile-resources.mjs';
9
+
10
+ /**
11
+ * Reports what a built page actually ships, so a game can see where its bytes go.
12
+ *
13
+ * Two halves, both deterministic (sorted paths, fixed compression settings, hashes):
14
+ *
15
+ * - `files`: every file under `dist` (precompressed `.gz`/`.br`/`.xz` siblings
16
+ * excluded - they are derived artifacts, and the gzip/brotli columns already count
17
+ * them) with raw, gzip (level 9) and brotli (quality 11) weights plus its share of
18
+ * the compressed page, the same settings `tools/compress-dist.mjs` writes with;
19
+ * - `assets`: every source file the asset folder embeds (the root `credits.json`
20
+ * and unknown extensions excluded, exactly the files `compileResources` skips),
21
+ * with duplicates grouped by SHA-256 so an asset embedded twice is visible.
22
+ *
23
+ * Shipped as `@datamoc/mw_games/tools/asset-report` (`assetReport(options)`, what the
24
+ * flags below map onto) and as the `mwg-assets` command.
25
+ *
26
+ * usage: mwg-assets <dist folder> [--assets=<dir>] [--json=<report file>]
27
+ */
28
+
29
+ const DERIVED_SIBLINGS = new Set(['.gz', '.br', '.xz']);
30
+
31
+ export function fingerprint(data) {
32
+ return createHash('sha256').update(data).digest('hex');
33
+ }
34
+
35
+ export function compressedSizes(data) {
36
+ return {
37
+ gzip: gzipSync(data, { level: 9 }).length,
38
+ brotli: brotliCompressSync(data, { params: { [constants.BROTLI_PARAM_QUALITY]: 11 } }).length,
39
+ };
40
+ }
41
+
42
+ function isDirectory(path) {
43
+ try {
44
+ return statSync(path).isDirectory();
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * @param options.dist the built page folder to measure
52
+ * @param options.assets the asset source folder to inventory (skipped when absent)
53
+ * @param options.mime extension to MIME type, merged over the compile defaults, so a
54
+ * game embedding extra types reports the same set the build embeds
55
+ * @returns the sorted files, sources, duplicate groups and totals described above
56
+ */
57
+ export async function assetReport({ dist, assets, mime = {} } = {}) {
58
+ if (!dist || !isDirectory(dist))
59
+ throw new Error(`assetReport needs the dist folder of a built page, got "${dist}"`);
60
+ if (assets !== undefined && !isDirectory(assets))
61
+ throw new Error(`assetReport needs a readable assets folder, got "${assets}"`);
62
+
63
+ const files = [];
64
+ for await (const file of walk(dist)) {
65
+ if (DERIVED_SIBLINGS.has(extname(file).toLowerCase())) continue;
66
+ const data = await readFile(file);
67
+ const { gzip, brotli } = compressedSizes(data);
68
+ files.push({
69
+ path: relative(dist, file).split(sep).join('/'),
70
+ raw: data.length,
71
+ gzip,
72
+ brotli,
73
+ sha256: fingerprint(data),
74
+ share: 0,
75
+ });
76
+ }
77
+ files.sort((a, b) => (a.path < b.path ? -1 : 1));
78
+ const totalGzip = files.reduce((sum, file) => sum + file.gzip, 0);
79
+ for (const file of files) file.share = totalGzip ? file.gzip / totalGzip : 0;
80
+
81
+ const sources = [];
82
+ const duplicates = [];
83
+ if (assets !== undefined) {
84
+ const types = { ...DEFAULT_MIME, ...mime };
85
+ const creditsFile = join(assets, 'credits.json');
86
+ const byHash = new Map();
87
+ for await (const file of walk(assets)) {
88
+ if (file === creditsFile) continue;
89
+ if (!types[extname(file).toLowerCase()]) continue;
90
+ const data = await readFile(file);
91
+ const sha256 = fingerprint(data);
92
+ const key = relative(assets, file).split(sep).join('/');
93
+ sources.push({ path: key, raw: data.length, sha256 });
94
+ const group = byHash.get(sha256) ?? [];
95
+ group.push(key);
96
+ byHash.set(sha256, group);
97
+ }
98
+ sources.sort((a, b) => (a.path < b.path ? -1 : 1));
99
+ for (const group of byHash.values()) if (group.length > 1) duplicates.push([...group].sort());
100
+ duplicates.sort((a, b) => (a[0] < b[0] ? -1 : 1));
101
+ }
102
+
103
+ return {
104
+ dist,
105
+ files,
106
+ assets: sources,
107
+ duplicates,
108
+ totals: {
109
+ files: files.length,
110
+ raw: files.reduce((sum, file) => sum + file.raw, 0),
111
+ gzip: totalGzip,
112
+ brotli: files.reduce((sum, file) => sum + file.brotli, 0),
113
+ assetFiles: sources.length,
114
+ assetRaw: sources.reduce((sum, source) => sum + source.raw, 0),
115
+ },
116
+ };
117
+ }
118
+
119
+ const kb = (n) => (n / 1024).toFixed(1) + ' KB';
120
+ const percent = (share) => (share * 100).toFixed(1) + '%';
121
+
122
+ async function main(argv) {
123
+ const folder = argv.find((arg) => !arg.startsWith('--'));
124
+ if (!folder) {
125
+ console.error('usage: mwg-assets <dist folder> [--assets=<dir>] [--json=<report file>]');
126
+ process.exit(1);
127
+ }
128
+ const value = (name) => argv.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
129
+ const report = await assetReport({ dist: folder, assets: value('assets') });
130
+
131
+ console.log(`\n ${report.dist}`);
132
+ for (const file of report.files) {
133
+ console.log(
134
+ ` ${file.path.padEnd(40)}${kb(file.raw).padStart(10)} gz ${kb(file.gzip).padStart(10)} br ${kb(file.brotli).padStart(10)} ${percent(file.share).padStart(6)}`,
135
+ );
136
+ }
137
+ console.log(
138
+ ` ${report.totals.files} file(s), ${kb(report.totals.raw)} raw -> ${kb(report.totals.gzip)} gz / ${kb(report.totals.brotli)} br`,
139
+ );
140
+ if (report.assets.length) {
141
+ console.log(` ${report.totals.assetFiles} embedded asset(s), ${kb(report.totals.assetRaw)} raw`);
142
+ for (const group of report.duplicates) console.log(` duplicate: ${group.join(' == ')}`);
143
+ }
144
+ const json = value('json');
145
+ if (json) {
146
+ await writeFile(json, JSON.stringify(report, null, 2) + '\n', 'utf8');
147
+ console.log(` report written to ${json}`);
148
+ }
149
+ }
150
+
151
+ //realpath, since an npm `bin` shim reaches this file through a symlink
152
+ //no top-level await: a vite config loaded as CommonJS has to be able to require() this module
153
+ if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url))
154
+ main(process.argv.slice(2)).catch((error) => {
155
+ console.error(error);
156
+ process.exitCode = 1;
157
+ });
@@ -7,6 +7,8 @@ export interface CompileResourcesOptions {
7
7
  to: string;
8
8
  mime?: Record<string, string>;
9
9
  groupBy?: (key: string) => string;
10
+ /** Exact paths relative to the asset folder to embed. Omit to include all; [] includes none. Unknown entries fail by name. */
11
+ assetFiles?: readonly string[];
10
12
  toWebp?: boolean;
11
13
  webpLossless?: boolean;
12
14
  webpQuality?: number;
@@ -58,6 +58,9 @@ export async function* walk(dir) {
58
58
  * @param options.from folder holding the assets
59
59
  * @param options.to folder to write the generated scripts into
60
60
  * @param options.mime extension to MIME type, merged over the defaults
61
+ * @param options.assetFiles exact paths relative to `from` to embed; omit for all, [] for none.
62
+ * Unknown paths and explicitly selected unsupported formats fail before replacing output.
63
+ * credits.json is converted to CREDITS.txt only when selected (or when no list is given).
61
64
  * @param options.groupBy how to name the script an asset lands in; defaults to its top folder
62
65
  * @param options.toWebp convert `.png`/`.jpg`/`.jpeg` assets to WebP before embedding
63
66
  * (default false - see `tools/webp-convert.mjs`). The asset's *key* never changes (a game
@@ -81,6 +84,7 @@ export async function compileResources({
81
84
  to,
82
85
  mime = {},
83
86
  groupBy,
87
+ assetFiles,
84
88
  toWebp: convertToWebp = false,
85
89
  webpLossless = true,
86
90
  webpQuality = 90,
@@ -90,6 +94,22 @@ export async function compileResources({
90
94
  const types = { ...DEFAULT_MIME, ...mime };
91
95
  const group = groupBy ?? ((key) => (key.includes('/') ? key.slice(0, key.indexOf('/')) : 'assets'));
92
96
 
97
+ const files = [];
98
+ for await (const file of walk(from)) files.push(file);
99
+ const keyOf = (file) => relative(from, file).split(sep).join('/');
100
+ const selected =
101
+ assetFiles === undefined
102
+ ? undefined
103
+ : new Set(assetFiles.map((key) => key.replaceAll('\\', '/').replace(/^\.\//, '')));
104
+ if (selected) {
105
+ const known = new Set(files.map(keyOf));
106
+ for (const key of selected) {
107
+ if (!known.has(key)) throw new Error(`unknown asset file ${key}`);
108
+ if (key !== 'credits.json' && !types[extname(key).toLowerCase()])
109
+ throw new Error(`unsupported asset file ${key}`);
110
+ }
111
+ }
112
+
93
113
  await rm(to, { recursive: true, force: true });
94
114
  await mkdir(to, { recursive: true });
95
115
 
@@ -106,7 +126,8 @@ export async function compileResources({
106
126
  const creditsFile = join(from, 'credits.json');
107
127
  let credits = null;
108
128
 
109
- for await (const file of walk(from)) {
129
+ for (const file of files) {
130
+ if (selected && !selected.has(keyOf(file))) continue;
110
131
  if (file === creditsFile) {
111
132
  credits = creditsText(JSON.parse(await readFile(file, 'utf8')));
112
133
  continue;
@@ -118,7 +139,7 @@ export async function compileResources({
118
139
  continue;
119
140
  }
120
141
 
121
- const key = relative(from, file).split(sep).join('/');
142
+ const key = keyOf(file);
122
143
  const data = await readFile(file);
123
144
 
124
145
  let mimeType = type;
@@ -9,6 +9,8 @@ export interface EmitPageOptions {
9
9
  assets?: string;
10
10
  /** maps an asset path to its script's name; defaults to one script per top-level folder */
11
11
  groupBy?: (key: string) => string;
12
+ /** Exact paths relative to the asset folder to embed. Omit to include all; [] includes none. Unknown entries fail by name. */
13
+ assetFiles?: readonly string[];
12
14
  /** write `.gz`/`.br` siblings for a server to negotiate; default true */
13
15
  compress?: boolean;
14
16
  /** also write `.xz`, where the system `xz` binary exists */
@@ -66,6 +66,7 @@ export async function emitPage({
66
66
  dist,
67
67
  assets,
68
68
  groupBy,
69
+ assetFiles,
69
70
  compress = true,
70
71
  xz = false,
71
72
  singleFile = false,
@@ -75,8 +76,17 @@ export async function emitPage({
75
76
  csp = true,
76
77
  }) {
77
78
  if (!dist) throw new Error('emitPage needs the dist folder of a vite build');
79
+ if (!assets && assetFiles?.length) throw new Error(`unknown asset file ${assetFiles[0]}: no assets folder`);
78
80
  const compiled = assets
79
- ? await compileResources({ from: assets, to: join(dist, 'assets'), groupBy, toWebp, webpLossless, webpQuality })
81
+ ? await compileResources({
82
+ from: assets,
83
+ to: join(dist, 'assets'),
84
+ groupBy,
85
+ assetFiles,
86
+ toWebp,
87
+ webpLossless,
88
+ webpQuality,
89
+ })
80
90
  : { groups: [], rawBytes: 0, embeddedBytes: 0, webpConverted: 0 };
81
91
 
82
92
  const html = await readFile(join(dist, 'index.html'), 'utf8');
@@ -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
+ }>;