@datamoc/mw_games 0.38.0 → 0.40.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 (92) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/api-surface.json +500 -2
  3. package/data/microgm-opt.PROVENANCE.md +20 -0
  4. package/data/microgm-opt.sf2 +0 -0
  5. package/dist/assets/SvgParts.d.ts +31 -0
  6. package/dist/assets/SvgParts.d.ts.map +1 -0
  7. package/dist/assets/SvgParts.js +54 -0
  8. package/dist/assets/SvgParts.js.map +1 -0
  9. package/dist/assets/SvgSource.d.ts +3 -0
  10. package/dist/assets/SvgSource.d.ts.map +1 -0
  11. package/dist/assets/SvgSource.js +22 -0
  12. package/dist/assets/SvgSource.js.map +1 -0
  13. package/dist/assets/index.d.ts +2 -0
  14. package/dist/assets/index.d.ts.map +1 -1
  15. package/dist/assets/index.js +1 -0
  16. package/dist/assets/index.js.map +1 -1
  17. package/dist/assets/svg.d.ts.map +1 -1
  18. package/dist/assets/svg.js +9 -5
  19. package/dist/assets/svg.js.map +1 -1
  20. package/dist/mw_games.global.js +110 -110
  21. package/dist/mwl/presentation.d.ts.map +1 -1
  22. package/dist/mwl/presentation.js +25 -18
  23. package/dist/mwl/presentation.js.map +1 -1
  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 +12 -0
  47. package/dist/two-d/presentation/MwlPresentation.d.ts.map +1 -1
  48. package/dist/two-d/presentation/MwlPresentation.js +204 -153
  49. package/dist/two-d/presentation/MwlPresentation.js.map +1 -1
  50. package/dist/two-d/render/ClothSprite.d.ts +83 -0
  51. package/dist/two-d/render/ClothSprite.d.ts.map +1 -0
  52. package/dist/two-d/render/ClothSprite.js +105 -0
  53. package/dist/two-d/render/ClothSprite.js.map +1 -0
  54. package/dist/two-d/render/HazeLayer.d.ts +112 -0
  55. package/dist/two-d/render/HazeLayer.d.ts.map +1 -0
  56. package/dist/two-d/render/HazeLayer.js +185 -0
  57. package/dist/two-d/render/HazeLayer.js.map +1 -0
  58. package/dist/two-d/render/NeonGlow.d.ts +100 -0
  59. package/dist/two-d/render/NeonGlow.d.ts.map +1 -0
  60. package/dist/two-d/render/NeonGlow.js +183 -0
  61. package/dist/two-d/render/NeonGlow.js.map +1 -0
  62. package/dist/two-d/render/SkeletalSprite.d.ts +5 -2
  63. package/dist/two-d/render/SkeletalSprite.d.ts.map +1 -1
  64. package/dist/two-d/render/SkeletalSprite.js +27 -3
  65. package/dist/two-d/render/SkeletalSprite.js.map +1 -1
  66. package/dist/two-d/render/SpringSprite.d.ts +40 -0
  67. package/dist/two-d/render/SpringSprite.d.ts.map +1 -0
  68. package/dist/two-d/render/SpringSprite.js +39 -0
  69. package/dist/two-d/render/SpringSprite.js.map +1 -0
  70. package/dist/two-d/render/SpriteDeform.d.ts +156 -0
  71. package/dist/two-d/render/SpriteDeform.d.ts.map +1 -0
  72. package/dist/two-d/render/SpriteDeform.js +278 -0
  73. package/dist/two-d/render/SpriteDeform.js.map +1 -0
  74. package/dist/two-d/render/SvgSkeleton.d.ts +54 -0
  75. package/dist/two-d/render/SvgSkeleton.d.ts.map +1 -0
  76. package/dist/two-d/render/SvgSkeleton.js +112 -0
  77. package/dist/two-d/render/SvgSkeleton.js.map +1 -0
  78. package/dist/two-d/render/index.d.ts +12 -0
  79. package/dist/two-d/render/index.d.ts.map +1 -1
  80. package/dist/two-d/render/index.js +6 -0
  81. package/dist/two-d/render/index.js.map +1 -1
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/package.json +11 -7
  85. package/sbom.cdx.json +6 -58
  86. package/tools/asset-report.d.mts +37 -0
  87. package/tools/asset-report.mjs +157 -0
  88. package/tools/compile-resources.d.mts +24 -0
  89. package/tools/compile-resources.mjs +138 -5
  90. package/tools/emit-page.d.mts +9 -1
  91. package/tools/emit-page.mjs +31 -3
  92. package/tools/vite.mjs +6 -1
@@ -7,9 +7,15 @@ 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;
15
+ /** Subset an embedded `.sf2` to the programs the embedded `.mid`/`.midi` files use. Default true; `false` embeds every SoundFont whole. */
16
+ soundFont?: boolean;
17
+ /** Explicit `{ bank, program }` list to subset a SoundFont to, overriding the MIDI-derived list. */
18
+ soundFontUsage?: readonly SoundFontUsage[];
13
19
  }
14
20
 
15
21
  export interface CompileResourcesGroup {
@@ -26,9 +32,27 @@ export interface CompileResourcesResult {
26
32
  skipped: number;
27
33
  /** the `CREDITS.txt` text when the asset folder holds a `credits.json`, else null */
28
34
  credits: string | null;
35
+ /** one entry per embedded `.sf2`: the programs kept (null when embedded whole) and the byte saving */
36
+ soundFonts: SoundFontEmbed[];
29
37
  }
30
38
 
31
39
  /** the plain-text credits for a manifest (an array, or `{ credits: [...] }`, of `{ name, license, author?, url? }`) */
32
40
  export function creditsText(manifest: unknown): string;
33
41
 
42
+ export interface SoundFontUsage {
43
+ bank: number;
44
+ program: number;
45
+ }
46
+
47
+ export interface SoundFontEmbed {
48
+ key: string;
49
+ /** programs kept, or null when the font was embedded whole for lack of a usage list */
50
+ programs: number | null;
51
+ rawBytes: number;
52
+ embeddedBytes: number;
53
+ }
54
+
55
+ /** parses a `--soundfont-usage=bank:program,...` CLI value; undefined when the flag is absent */
56
+ export function soundFontUsageFromFlag(value: string | undefined): SoundFontUsage[] | undefined;
57
+
34
58
  export function compileResources(options: CompileResourcesOptions): Promise<CompileResourcesResult>;
@@ -38,8 +38,56 @@ export const DEFAULT_MIME = {
38
38
  '.woff2': 'font/woff2',
39
39
  '.json': 'application/json',
40
40
  '.txt': 'text/plain',
41
+ '.mid': 'audio/midi',
42
+ '.midi': 'audio/midi',
43
+ '.sf2': 'application/octet-stream',
41
44
  };
42
45
 
46
+ const isSoundFontKey = (key) => key.toLowerCase().endsWith('.sf2');
47
+ const isMidiKey = (key) => /\.(mid|midi)$/i.test(key);
48
+
49
+ /**
50
+ * The SoundFont/MIDI implementation lives in the library, not in this tool: in this
51
+ * repository it is the TypeScript source, in an installed package it is the built
52
+ * `dist/` the source compiles to. Only called when a build actually embeds a
53
+ * SoundFont it can subset, so a game without one never pays for the import.
54
+ */
55
+ async function loadSoundFontTools() {
56
+ try {
57
+ const [soundFont, midi] = await Promise.all([
58
+ import('../src/audio/SoundFont.ts'),
59
+ import('../src/audio/Midi.ts'),
60
+ ]);
61
+ return { ...soundFont, ...midi };
62
+ } catch {
63
+ return import('../dist/audio/index.js');
64
+ }
65
+ }
66
+
67
+ function checkedSoundFontUsage(soundFontUsage) {
68
+ if (soundFontUsage === undefined) return undefined;
69
+ if (!Array.isArray(soundFontUsage)) throw new Error('soundFontUsage must be an array of { bank, program }');
70
+ return soundFontUsage.map((entry, index) => {
71
+ const { bank, program } = entry ?? {};
72
+ if (!Number.isInteger(bank) || bank < 0 || !Number.isInteger(program) || program < 0 || program > 127) {
73
+ throw new Error(`soundFontUsage[${index}] needs an integer bank >= 0 and a program between 0 and 127`);
74
+ }
75
+ return { bank, program };
76
+ });
77
+ }
78
+
79
+ /** parses a `--soundfont-usage=bank:program,...` CLI value; undefined when the flag is absent */
80
+ export function soundFontUsageFromFlag(value) {
81
+ if (value === undefined) return undefined;
82
+ return value
83
+ .split(',')
84
+ .filter(Boolean)
85
+ .map((pair) => {
86
+ const [bank, program] = pair.split(':').map(Number);
87
+ return { bank, program };
88
+ });
89
+ }
90
+
43
91
  export async function* walk(dir) {
44
92
  let entries;
45
93
  try {
@@ -58,6 +106,9 @@ export async function* walk(dir) {
58
106
  * @param options.from folder holding the assets
59
107
  * @param options.to folder to write the generated scripts into
60
108
  * @param options.mime extension to MIME type, merged over the defaults
109
+ * @param options.assetFiles exact paths relative to `from` to embed; omit for all, [] for none.
110
+ * Unknown paths and explicitly selected unsupported formats fail before replacing output.
111
+ * credits.json is converted to CREDITS.txt only when selected (or when no list is given).
61
112
  * @param options.groupBy how to name the script an asset lands in; defaults to its top folder
62
113
  * @param options.toWebp convert `.png`/`.jpg`/`.jpeg` assets to WebP before embedding
63
114
  * (default false - see `tools/webp-convert.mjs`). The asset's *key* never changes (a game
@@ -69,6 +120,10 @@ export async function* walk(dir) {
69
120
  * pixel-art tileset's exact colours survive; set false only with `webpQuality` to opt into
70
121
  * lossy re-encoding for photographic source art)
71
122
  * @param options.webpQuality 0-100, used only when `webpLossless` is false (default 90)
123
+ * @param options.soundFont subset an embedded `.sf2` to the programs the embedded
124
+ * `.mid`/`.midi` files use (default true); `false` embeds every SoundFont whole
125
+ * @param options.soundFontUsage explicit `{ bank, program }` list to subset to, for a
126
+ * game that voices notes no shipped MIDI file requests; overrides the MIDI-derived list
72
127
  * @returns the group names, in the order the page should load them
73
128
  *
74
129
  * An `assets/credits.json` (an array, or `{ "credits": [...] }`, of `{ name, license, author?, url? }`)
@@ -81,15 +136,66 @@ export async function compileResources({
81
136
  to,
82
137
  mime = {},
83
138
  groupBy,
139
+ assetFiles,
84
140
  toWebp: convertToWebp = false,
85
141
  webpLossless = true,
86
142
  webpQuality = 90,
143
+ soundFont = true,
144
+ soundFontUsage,
87
145
  } = {}) {
88
146
  if (!from || !to) throw new Error('compileResources needs both `from` and `to`');
89
147
 
90
148
  const types = { ...DEFAULT_MIME, ...mime };
91
149
  const group = groupBy ?? ((key) => (key.includes('/') ? key.slice(0, key.indexOf('/')) : 'assets'));
92
150
 
151
+ const files = [];
152
+ for await (const file of walk(from)) files.push(file);
153
+ const keyOf = (file) => relative(from, file).split(sep).join('/');
154
+ const selected =
155
+ assetFiles === undefined
156
+ ? undefined
157
+ : new Set(assetFiles.map((key) => key.replaceAll('\\', '/').replace(/^\.\//, '')));
158
+ if (selected) {
159
+ const known = new Set(files.map(keyOf));
160
+ for (const key of selected) {
161
+ if (!known.has(key)) throw new Error(`unknown asset file ${key}`);
162
+ if (key !== 'credits.json' && !types[extname(key).toLowerCase()])
163
+ throw new Error(`unsupported asset file ${key}`);
164
+ }
165
+ }
166
+
167
+ // A shipped SoundFont carries only the programs the game's shipped MIDI files
168
+ // request: the full bank - even a small one - is mostly
169
+ // instruments no track plays. Usage is resolved before any output is replaced,
170
+ // so an unreadable MIDI or SoundFont fails the build with the old output intact.
171
+ // With no shipped MIDI and no explicit `soundFontUsage` the usage is unknowable
172
+ // (a game may voice notes it generates), so the font embeds whole.
173
+ const isSelected = (file) => !selected || selected.has(keyOf(file));
174
+ const explicitUsage = checkedSoundFontUsage(soundFontUsage);
175
+ const embedsSoundFont =
176
+ soundFont !== false && files.some((file) => isSelected(file) && isSoundFontKey(keyOf(file)));
177
+ let soundFontTools = null;
178
+ let usage = explicitUsage;
179
+ if (embedsSoundFont && usage === undefined) {
180
+ const midiFiles = files.filter((file) => isSelected(file) && isMidiKey(keyOf(file)));
181
+ if (midiFiles.length > 0) {
182
+ soundFontTools = await loadSoundFontTools();
183
+ const parsed = [];
184
+ for (const file of midiFiles) {
185
+ try {
186
+ parsed.push(soundFontTools.parseMidi(await readFile(file)));
187
+ } catch (error) {
188
+ throw new Error(
189
+ `cannot read MIDI asset ${keyOf(file)} to subset a SoundFont: ${error.message ?? error}`,
190
+ );
191
+ }
192
+ }
193
+ usage = soundFontTools.collectSoundFontUsage(parsed);
194
+ }
195
+ }
196
+ if (embedsSoundFont && usage !== undefined && !soundFontTools) soundFontTools = await loadSoundFontTools();
197
+ const soundFonts = [];
198
+
93
199
  await rm(to, { recursive: true, force: true });
94
200
  await mkdir(to, { recursive: true });
95
201
 
@@ -106,7 +212,8 @@ export async function compileResources({
106
212
  const creditsFile = join(from, 'credits.json');
107
213
  let credits = null;
108
214
 
109
- for await (const file of walk(from)) {
215
+ for (const file of files) {
216
+ if (selected && !selected.has(keyOf(file))) continue;
110
217
  if (file === creditsFile) {
111
218
  credits = creditsText(JSON.parse(await readFile(file, 'utf8')));
112
219
  continue;
@@ -118,7 +225,7 @@ export async function compileResources({
118
225
  continue;
119
226
  }
120
227
 
121
- const key = relative(from, file).split(sep).join('/');
228
+ const key = keyOf(file);
122
229
  const data = await readFile(file);
123
230
 
124
231
  let mimeType = type;
@@ -132,6 +239,19 @@ export async function compileResources({
132
239
  }
133
240
  }
134
241
 
242
+ if (isSoundFontKey(key)) {
243
+ if (usage !== undefined && soundFontTools) {
244
+ try {
245
+ bytes = Buffer.from(soundFontTools.subsetSoundFont(data, usage));
246
+ } catch (error) {
247
+ throw new Error(`cannot subset SoundFont asset ${key}: ${error.message ?? error}`);
248
+ }
249
+ soundFonts.push({ key, programs: usage.length, rawBytes: data.length, embeddedBytes: bytes.length });
250
+ } else {
251
+ soundFonts.push({ key, programs: null, rawBytes: data.length, embeddedBytes: data.length });
252
+ }
253
+ }
254
+
135
255
  const name = group(key);
136
256
  if (!groups.has(name)) groups.set(name, {});
137
257
  groups.get(name)[key] = `data:${mimeType};base64,${bytes.toString('base64')}`;
@@ -152,7 +272,7 @@ export async function compileResources({
152
272
 
153
273
  if (credits) await writeFile(join(to, 'CREDITS.txt'), credits, 'utf8');
154
274
 
155
- return { groups: written, rawBytes, embeddedBytes, webpConverted, skipped, credits };
275
+ return { groups: written, rawBytes, embeddedBytes, webpConverted, skipped, credits, soundFonts };
156
276
  }
157
277
 
158
278
  /** the plain-text credits for a manifest, one block per entry; throws on an entry that cannot be attributed */
@@ -181,7 +301,7 @@ async function runCli() {
181
301
  const [from, to] = args.filter((a) => !a.startsWith('--'));
182
302
  if (!from || !to) {
183
303
  console.error(
184
- 'usage: node compile-resources.mjs <asset folder> <output folder> [--to-webp] [--webp-lossy[=quality]]',
304
+ 'usage: node compile-resources.mjs <asset folder> <output folder> [--to-webp] [--webp-lossy[=quality]] [--no-soundfont-subset] [--soundfont-usage=bank:program,...]',
185
305
  );
186
306
  process.exit(1);
187
307
  }
@@ -190,7 +310,11 @@ async function runCli() {
190
310
  const webpLossless = !lossyFlag;
191
311
  const webpQuality = lossyFlag && lossyFlag.includes('=') ? Number(lossyFlag.split('=')[1]) : 90;
192
312
 
193
- const { groups, rawBytes, embeddedBytes, webpConverted, skipped } = await compileResources({
313
+ const soundFontUsage = soundFontUsageFromFlag(
314
+ args.find((a) => a.startsWith('--soundfont-usage='))?.slice('--soundfont-usage='.length),
315
+ );
316
+
317
+ const { groups, rawBytes, embeddedBytes, webpConverted, skipped, soundFonts } = await compileResources({
194
318
  from,
195
319
  to,
196
320
  //a flat folder has no subdirectories to group by, so everything lands in one file
@@ -198,6 +322,8 @@ async function runCli() {
198
322
  toWebp,
199
323
  webpLossless,
200
324
  webpQuality,
325
+ soundFont: !args.includes('--no-soundfont-subset'),
326
+ soundFontUsage,
201
327
  });
202
328
 
203
329
  const kb = (n) => (n / 1024).toFixed(1).padStart(8) + ' KB';
@@ -205,6 +331,13 @@ async function runCli() {
205
331
  console.log(` ${join(relative(process.cwd(), to), name + '.js').padEnd(40)}${kb(size)} (${count} files)`);
206
332
  }
207
333
  console.log(` ${groups.length} script(s), ${kb(rawBytes)} of assets` + (skipped ? `, ${skipped} skipped` : ''));
334
+ for (const font of soundFonts) {
335
+ console.log(
336
+ font.programs === null
337
+ ? ` ${font.key}: SoundFont embedded whole (${kb(font.rawBytes)}), no shipped MIDI to subset against`
338
+ : ` ${font.key}: SoundFont subset to ${font.programs} program(s), ${kb(font.rawBytes)} -> ${kb(font.embeddedBytes)}`,
339
+ );
340
+ }
208
341
  if (toWebp) {
209
342
  console.log(
210
343
  ` ${webpConverted} image(s) converted to WebP (${webpLossless ? 'lossless' : `lossy, quality ${webpQuality}`}), ` +
@@ -1,4 +1,4 @@
1
- import type { CompileResourcesGroup } from './compile-resources.mjs';
1
+ import type { CompileResourcesGroup, SoundFontEmbed, SoundFontUsage } from './compile-resources.mjs';
2
2
  import type { CompressRow } from './compress-dist.mjs';
3
3
  import type { SingleFileOptions, SingleFileResult } from './single-file.mjs';
4
4
 
@@ -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 */
@@ -18,6 +20,10 @@ export interface EmitPageOptions {
18
20
  toWebp?: boolean;
19
21
  webpLossless?: boolean;
20
22
  webpQuality?: number;
23
+ /** Subset an embedded `.sf2` to the programs the embedded `.mid`/`.midi` files use. Default true; `false` embeds every SoundFont whole. */
24
+ soundFont?: boolean;
25
+ /** Explicit `{ bank, program }` list to subset a SoundFont to, overriding the MIDI-derived list. */
26
+ soundFontUsage?: readonly SoundFontUsage[];
21
27
  /** the page's Content-Security-Policy: on by default, `false` to leave it out */
22
28
  csp?: boolean | CspOptions;
23
29
  }
@@ -36,6 +42,8 @@ export interface EmitPageResult {
36
42
  rawBytes: number;
37
43
  embeddedBytes: number;
38
44
  webpConverted: number;
45
+ /** one entry per embedded `.sf2`: the programs kept (null when embedded whole) and the byte saving */
46
+ soundFonts: SoundFontEmbed[];
39
47
  compressed: CompressRow[] | null;
40
48
  single: SingleFileResult | null;
41
49
  }
@@ -3,7 +3,7 @@ import { readFile, writeFile, rm, stat } from 'node:fs/promises';
3
3
  import { realpathSync } from 'node:fs';
4
4
  import { join, resolve as resolvePath } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { compileResources } from './compile-resources.mjs';
6
+ import { compileResources, soundFontUsageFromFlag } from './compile-resources.mjs';
7
7
  import { compressDist } from './compress-dist.mjs';
8
8
  import { toClassicScript } from './classic-html.mjs';
9
9
  import { buildSingleFile } from './single-file.mjs';
@@ -30,6 +30,7 @@ import { withContentSecurityPolicy } from './csp.mjs';
30
30
  * [--no-compress] [--xz]
31
31
  * [--single-file] [--single-file-compress[=level]]
32
32
  * [--single-file-brotli[=quality]] [--to-webp] [--webp-lossy[=quality]]
33
+ * [--no-soundfont-subset] [--soundfont-usage=bank:program,...]
33
34
  *
34
35
  * `--dist` defaults to `<project folder>/dist` and `--assets` to `<project folder>/assets`; with
35
36
  * no assets folder at all, the page is rewritten and nothing is compiled.
@@ -66,18 +67,32 @@ export async function emitPage({
66
67
  dist,
67
68
  assets,
68
69
  groupBy,
70
+ assetFiles,
69
71
  compress = true,
70
72
  xz = false,
71
73
  singleFile = false,
72
74
  toWebp = false,
73
75
  webpLossless = true,
74
76
  webpQuality = 90,
77
+ soundFont = true,
78
+ soundFontUsage,
75
79
  csp = true,
76
80
  }) {
77
81
  if (!dist) throw new Error('emitPage needs the dist folder of a vite build');
82
+ if (!assets && assetFiles?.length) throw new Error(`unknown asset file ${assetFiles[0]}: no assets folder`);
78
83
  const compiled = assets
79
- ? await compileResources({ from: assets, to: join(dist, 'assets'), groupBy, toWebp, webpLossless, webpQuality })
80
- : { groups: [], rawBytes: 0, embeddedBytes: 0, webpConverted: 0 };
84
+ ? await compileResources({
85
+ from: assets,
86
+ to: join(dist, 'assets'),
87
+ groupBy,
88
+ assetFiles,
89
+ toWebp,
90
+ webpLossless,
91
+ webpQuality,
92
+ soundFont,
93
+ soundFontUsage,
94
+ })
95
+ : { groups: [], rawBytes: 0, embeddedBytes: 0, webpConverted: 0, soundFonts: [] };
81
96
 
82
97
  const html = await readFile(join(dist, 'index.html'), 'utf8');
83
98
  const classic = toClassicScript(html);
@@ -132,6 +147,10 @@ async function main(argv) {
132
147
  level: levelFlag?.includes('=') ? Number(levelFlag.split('=')[1]) : undefined,
133
148
  };
134
149
 
150
+ const soundFontUsage = soundFontUsageFromFlag(
151
+ argv.find((a) => a.startsWith('--soundfont-usage='))?.slice('--soundfont-usage='.length),
152
+ );
153
+
135
154
  const result = await emitPage({
136
155
  dist,
137
156
  assets: hasAssets ? assetsDir : undefined,
@@ -143,6 +162,8 @@ async function main(argv) {
143
162
  toWebp: argv.includes('--to-webp'),
144
163
  webpLossless: !webpLossyFlag,
145
164
  webpQuality: webpLossyFlag?.includes('=') ? Number(webpLossyFlag.split('=')[1]) : 90,
165
+ soundFont: !argv.includes('--no-soundfont-subset'),
166
+ soundFontUsage,
146
167
  csp: argv.includes('--no-csp')
147
168
  ? false
148
169
  : { connect: argv.filter((a) => a.startsWith('--connect=')).map((a) => a.slice('--connect='.length)) },
@@ -158,6 +179,13 @@ async function main(argv) {
158
179
  `${kb(result.rawBytes)} -> ${kb(result.embeddedBytes)} embedded`,
159
180
  );
160
181
  }
182
+ for (const font of result.soundFonts ?? []) {
183
+ console.log(
184
+ font.programs === null
185
+ ? ` ${font.key}: SoundFont embedded whole (${kb(font.rawBytes)}), no shipped MIDI to subset against`
186
+ : ` ${font.key}: SoundFont subset to ${font.programs} program(s), ${kb(font.rawBytes)} -> ${kb(font.embeddedBytes)}`,
187
+ );
188
+ }
161
189
  console.log(' open index.html directly - no server needed');
162
190
  if (result.compressed) {
163
191
  for (const row of result.compressed) {
package/tools/vite.mjs CHANGED
@@ -11,7 +11,12 @@ import { buildArtifactSbom, serialize, shippedFiles } from './sbom.mjs';
11
11
  * It sets the configuration that target needs (a classic IIFE bundle named `game.js`, relative
12
12
  * urls through `base: './'`, no inlined or copied assets since they are compiled instead) and
13
13
  * serves `assets` (default `'assets'`, `false` for none) as the public folder, so an asset path reads the same in `vite` dev as in the
14
- * build. At the end of the build it runs `emitPage` over the output folder with the same
14
+ * build. `assetFiles: ['characters/hero.svg', 'music/theme.ogg']` embeds only those paths;
15
+ * omit it for all assets, or pass [] for none. An unknown entry fails by name. Filtering
16
+ * affects the build only, so the dev server still serves the asset folder normally.
17
+ * An embedded `.sf2` is subset at build time to the programs the embedded `.mid` files
18
+ * use (`soundFont: false` embeds it whole, `soundFontUsage` names the programs instead).
19
+ * At the end of the build it runs `emitPage` over the output folder with the same
15
20
  * options. A setting the game's own config gives explicitly still wins, since vite merges a
16
21
  * plugin's `config` result under the user's.
17
22
  *