@datamoc/mw_games 0.42.0 → 0.43.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 (139) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +62 -1
  3. package/REFERENCE.md +57 -5
  4. package/api-surface.json +672 -0
  5. package/data/microgm-essential.PROVENANCE.md +13 -0
  6. package/data/microgm-essential.sf2 +0 -0
  7. package/data/soundfont-catalog.json +8 -0
  8. package/dist/audio/AudioLoader.d.ts +28 -0
  9. package/dist/audio/AudioLoader.d.ts.map +1 -0
  10. package/dist/audio/AudioLoader.js +53 -0
  11. package/dist/audio/AudioLoader.js.map +1 -0
  12. package/dist/audio/Channels.d.ts +1 -0
  13. package/dist/audio/Channels.d.ts.map +1 -1
  14. package/dist/audio/Channels.js +2 -1
  15. package/dist/audio/Channels.js.map +1 -1
  16. package/dist/audio/MidiRender.d.ts +16 -0
  17. package/dist/audio/MidiRender.d.ts.map +1 -1
  18. package/dist/audio/MidiRender.js +21 -0
  19. package/dist/audio/MidiRender.js.map +1 -1
  20. package/dist/audio/MusicPlayer.d.ts +80 -0
  21. package/dist/audio/MusicPlayer.d.ts.map +1 -0
  22. package/dist/audio/MusicPlayer.js +147 -0
  23. package/dist/audio/MusicPlayer.js.map +1 -0
  24. package/dist/audio/SampleDecoder.d.ts +16 -2
  25. package/dist/audio/SampleDecoder.d.ts.map +1 -1
  26. package/dist/audio/SampleDecoder.js +10 -6
  27. package/dist/audio/SampleDecoder.js.map +1 -1
  28. package/dist/audio/Sound.d.ts +2 -0
  29. package/dist/audio/Sound.d.ts.map +1 -1
  30. package/dist/audio/Sound.js +3 -0
  31. package/dist/audio/Sound.js.map +1 -1
  32. package/dist/audio/SoundFont.d.ts +5 -3
  33. package/dist/audio/SoundFont.d.ts.map +1 -1
  34. package/dist/audio/SoundFont.js +10 -8
  35. package/dist/audio/SoundFont.js.map +1 -1
  36. package/dist/audio/Synth.d.ts +1 -1
  37. package/dist/audio/Synth.d.ts.map +1 -1
  38. package/dist/audio/Synth.js +4 -0
  39. package/dist/audio/Synth.js.map +1 -1
  40. package/dist/audio/index.d.ts +6 -2
  41. package/dist/audio/index.d.ts.map +1 -1
  42. package/dist/audio/index.js +3 -1
  43. package/dist/audio/index.js.map +1 -1
  44. package/dist/mw_games.global.js +22 -22
  45. package/dist/mwl/index.d.ts +2 -0
  46. package/dist/mwl/index.d.ts.map +1 -1
  47. package/dist/mwl/index.js +1 -0
  48. package/dist/mwl/index.js.map +1 -1
  49. package/dist/mwl/objects.d.ts +31 -0
  50. package/dist/mwl/objects.d.ts.map +1 -0
  51. package/dist/mwl/objects.js +83 -0
  52. package/dist/mwl/objects.js.map +1 -0
  53. package/dist/mwl/schema.d.ts +1 -1
  54. package/dist/mwl/schema.d.ts.map +1 -1
  55. package/dist/mwl/tables.d.ts +2 -0
  56. package/dist/mwl/tables.d.ts.map +1 -1
  57. package/dist/mwl/tables.js +13 -0
  58. package/dist/mwl/tables.js.map +1 -1
  59. package/dist/physics/XpbdWorld.d.ts +23 -5
  60. package/dist/physics/XpbdWorld.d.ts.map +1 -1
  61. package/dist/physics/XpbdWorld.js +90 -6
  62. package/dist/physics/XpbdWorld.js.map +1 -1
  63. package/dist/testing/index.d.ts +16 -0
  64. package/dist/testing/index.d.ts.map +1 -1
  65. package/dist/testing/index.js +21 -0
  66. package/dist/testing/index.js.map +1 -1
  67. package/dist/three-d/Collision3D.d.ts +46 -2
  68. package/dist/three-d/Collision3D.d.ts.map +1 -1
  69. package/dist/three-d/Collision3D.js +46 -2
  70. package/dist/three-d/Collision3D.js.map +1 -1
  71. package/dist/three-d/Grid.d.ts +12 -1
  72. package/dist/three-d/Grid.d.ts.map +1 -1
  73. package/dist/three-d/Grid.js +12 -1
  74. package/dist/three-d/Grid.js.map +1 -1
  75. package/dist/three-d/Vox.d.ts +13 -1
  76. package/dist/three-d/Vox.d.ts.map +1 -1
  77. package/dist/three-d/Vox.js +13 -1
  78. package/dist/three-d/Vox.js.map +1 -1
  79. package/dist/two-d/Game.d.ts +21 -1
  80. package/dist/two-d/Game.d.ts.map +1 -1
  81. package/dist/two-d/Game.js +73 -6
  82. package/dist/two-d/Game.js.map +1 -1
  83. package/dist/two-d/Scene2D.d.ts +24 -0
  84. package/dist/two-d/Scene2D.d.ts.map +1 -1
  85. package/dist/two-d/Scene2D.js +30 -0
  86. package/dist/two-d/Scene2D.js.map +1 -1
  87. package/dist/two-d/render/SceneTransition.d.ts +272 -0
  88. package/dist/two-d/render/SceneTransition.d.ts.map +1 -0
  89. package/dist/two-d/render/SceneTransition.js +431 -0
  90. package/dist/two-d/render/SceneTransition.js.map +1 -0
  91. package/dist/two-d/render/index.d.ts +2 -0
  92. package/dist/two-d/render/index.d.ts.map +1 -1
  93. package/dist/two-d/render/index.js +1 -0
  94. package/dist/two-d/render/index.js.map +1 -1
  95. package/dist/two-d/ui/Button.d.ts +23 -0
  96. package/dist/two-d/ui/Button.d.ts.map +1 -1
  97. package/dist/two-d/ui/Button.js +37 -0
  98. package/dist/two-d/ui/Button.js.map +1 -1
  99. package/dist/two-d/ui/DropdownView.d.ts +49 -3
  100. package/dist/two-d/ui/DropdownView.d.ts.map +1 -1
  101. package/dist/two-d/ui/DropdownView.js +150 -9
  102. package/dist/two-d/ui/DropdownView.js.map +1 -1
  103. package/dist/two-d/ui/ScrollBox.d.ts +90 -0
  104. package/dist/two-d/ui/ScrollBox.d.ts.map +1 -1
  105. package/dist/two-d/ui/ScrollBox.js +206 -4
  106. package/dist/two-d/ui/ScrollBox.js.map +1 -1
  107. package/dist/two-d/ui/ScrollText.d.ts +122 -0
  108. package/dist/two-d/ui/ScrollText.d.ts.map +1 -0
  109. package/dist/two-d/ui/ScrollText.js +202 -0
  110. package/dist/two-d/ui/ScrollText.js.map +1 -0
  111. package/dist/two-d/ui/Tooltip.d.ts +70 -1
  112. package/dist/two-d/ui/Tooltip.d.ts.map +1 -1
  113. package/dist/two-d/ui/Tooltip.js +115 -3
  114. package/dist/two-d/ui/Tooltip.js.map +1 -1
  115. package/dist/two-d/ui/Window.d.ts +35 -2
  116. package/dist/two-d/ui/Window.d.ts.map +1 -1
  117. package/dist/two-d/ui/Window.js +58 -3
  118. package/dist/two-d/ui/Window.js.map +1 -1
  119. package/dist/two-d/ui/index.d.ts +7 -5
  120. package/dist/two-d/ui/index.d.ts.map +1 -1
  121. package/dist/two-d/ui/index.js +3 -2
  122. package/dist/two-d/ui/index.js.map +1 -1
  123. package/dist/version.d.ts +1 -1
  124. package/dist/version.js +1 -1
  125. package/package.json +7 -3
  126. package/sbom.cdx.json +19 -260
  127. package/tools/browser-driver.mjs +3 -3
  128. package/tools/browser-smoke.mjs +19 -2
  129. package/tools/build-tablebases.mjs +7 -3
  130. package/tools/compile-openings.mjs +7 -3
  131. package/tools/compile-resources.d.mts +2 -0
  132. package/tools/compile-resources.mjs +13 -1
  133. package/tools/emit-page.d.mts +6 -0
  134. package/tools/emit-page.mjs +43 -3
  135. package/tools/find-chrome.mjs +4 -2
  136. package/tools/init.mjs +11 -3
  137. package/tools/play-uci.mjs +9 -5
  138. package/tools/vite.mjs +9 -1
  139. package/tools.md +1 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,65 @@ the public API may still change between minor versions.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.43.0] - 2026-10-17
11
+
12
+ Scrolling, tooltips and screen transitions for interface-heavy games, a MIDI music player and
13
+ SoundFont tooling, and the shipped command line tools made to work from an installed package on
14
+ Node 20.19. `@datamoc/pyroeis` moves to 0.9.1, which ends the last moderate audit advisory.
15
+
16
+ ### Added
17
+
18
+ - Scrolling and transitions asked for by a bridge game (Pontifex feedback 45-50):
19
+ - `ScrollBox`: `touch` (drag to scroll, coasting on release, `ScrollDrag`), `handleKey` for line, page and
20
+ ends, a draggable scrollbar thumb and a clickable track, and `autoHeight`. A drag no longer clicks the row
21
+ it ends on.
22
+ - `DropdownView`: `maxVisibleRows` (the list scrolls), an opaque backdrop, `autoWidth`, opening with the chosen
23
+ row in view, `handleKey`, `handleOutsideClick` and `fitBelow`.
24
+ - `ScrollText` (a wrapped label that sizes itself and scrolls), `MarqueeLabel` (a long line sliding inside
25
+ its width, still under reduced motion), `ellipsize` and `truncateLines`.
26
+ - `TooltipHost`: tooltips on a touch long-press and on disabled buttons, from rectangles instead of pointer
27
+ events. `Button` gains `tooltip`, `setIcon` and a second icon for the selected state (`iconSelected`).
28
+ - `Game.switchScene(Next, { transition, duration, easing, pause })`, `Game.transition(swap, options)` and
29
+ `Scene2D.transitionTo`: `tileShrink`, `shrink`, `pop`, `grow`, `slide`, `iris` and `dissolve` over a snapshot of
30
+ the outgoing scene, skipped under reduced motion, with `Game.transitioning` and `Game.active`. `Window.open`
31
+ and `Window.close` take an animation (`pop`, `grow`, `slide`, `fade`). The bridge example switches level
32
+ with `tileShrink`.
33
+ - The dist grew 3.8% with them; the size baseline is moved to match.
34
+ - `limits` (`SoundFontLimits`: `sampleFrames`, `totalFrames`) on `decodeSoundFont`, `loadSoundFont` and
35
+ `webAudioSampleDecoder` raises or lowers what a SoundFont 3 may decode to, instead of the fixed 16 M
36
+ frames per sample and 128 M per font. The sample-rate bounds stay: the format itself stops at 50 kHz.
37
+ - `@example`s for the `3d` functions that need no renderer (`gridPoint3D`, `cellAt`, `heightAt`,
38
+ `buildHeightIndex`, `resolveCapsuleAgainstGrid`, `parseVox`). The generated API reference now covers
39
+ the subpaths the root barrel does not re-export (`3d`, `physics`, `ai/lua`, `mwl/pyroeis`, `testing`,
40
+ `threads`, `headless`, `assets/binary`), so the examples index links 803 of 813 symbols to a page.
41
+ - `npm run build:bridge-tutorial` regenerates the bridge tutorial from `tools/templates` and the example's
42
+ own source. `npm run bins:smoke` packs the project, installs it in an empty folder and runs every `mwg-*`
43
+ bin with `--help`; CI runs it, with the library build, on Node 20.19.0, the floor `engines` claims.
44
+ - `.gitattributes` (`* text=auto eol=lf`) ends the CRLF warnings on Windows checkouts.
45
+ - An optional, reproducible 314 KB SoundFont subset covering six essential presets,
46
+ with provenance and synthetic fallback for the instruments outside it.
47
+ - `AudioBus` loads embedded audio and MIDI by default through `createAudioLoader`, with
48
+ shared decoding, worker MIDI rendering and optional SoundFont settings across lifetimes.
49
+ - `MusicPlayer` controls rendered MIDI or decoded buffers with pause, resume, seek,
50
+ volume and a track-completion signal; `toAudioBuffer` copies rendered stereo samples
51
+ into a Web Audio buffer. The README shows the supplied SoundFont and worker playback path.
52
+ - Page byte budgets and potentially unused asset reports in the standalone build pipeline.
53
+ - XPBD link payloads, explicit rupture, geometric queries and state comparison helpers.
54
+ - Schema-directed `nodesToObjects` conversion and optional literal unions generated from
55
+ MWL table columns. Optional attributes are documented alongside the schema.
56
+ - Generated starter projects include a pre-commit hook for typecheck and tests, an activation
57
+ command and ignores for local agent state.
58
+ - Sawtooth sound synthesis, playback attempt counts and a shared browser test exposure hook.
59
+
60
+ ### Fixed
61
+
62
+ - `mwg-uci`, `mwg-openings` and `mwg-tablebases` imported `../src/...`, which the package does not ship, so
63
+ they failed in every installed copy; they load `dist` there, as the other tools already did.
64
+ - The bridge example keeps a saved best only when the new crossing costs less, or costs the same and sags
65
+ less, instead of replacing it by any cheaper one.
66
+ - Fixed waits that failed on a loaded machine: the browser smoke waits for drawn frames instead of 250 ms,
67
+ the driver test polls for its tap instead of sleeping 300 ms, and the WebP noise test is seeded.
68
+
10
69
  ## [0.42.0] - 2026-10-16
11
70
 
12
71
  The documentation and asset follow-ups to 0.41.0: SVG minification, a policy for the instruments a
package/README.md CHANGED
@@ -8,6 +8,10 @@ events, quests, stats and turn order underneath are the same either way.
8
8
  Licensed under the [MPL-2.0](LICENSE): games built with `mwg` carry no licence obligation
9
9
  of their own; improvements to `mwg`'s own files are shared back.
10
10
 
11
+ The canonical npm package is `@datamoc/mw_games`; use that name for installation, imports,
12
+ tools and bundled data such as `data/microgm-opt.sf2`. `@datamoc/mwg` remains a compatible
13
+ re-export package for existing projects. The recipes here consistently use the canonical name.
14
+
11
15
  **[Live examples and API docs](https://datamoc.github.io/mwg/)**: every example below,
12
16
  playable in the browser with no download, plus the generated API reference.
13
17
 
@@ -15,7 +19,7 @@ playable in the browser with no download, plus the generated API reference.
15
19
  planning or porting a demanding game with mwg, with links to the [mwg reference](https://github.com/datamoc/mwg/blob/main/REFERENCE.md)
16
20
  and the [mwg Pixel Dungeon study](https://datamoc.github.io/mwg-pixel-dungeon/).
17
21
 
18
- > **Status: pre-alpha (v0.36).** Every module in the shared floor below, plus optional
22
+ > **Status: pre-alpha.** Every module in the shared floor below, plus optional
19
23
  > 3D, mobile (Capacitor) and desktop (WebView2) packaging, is built and tested - see
20
24
  > [ROADMAP.md](https://github.com/datamoc/mwg/blob/main/ROADMAP.md) for open work, and
21
25
  > [CLOSED.md](https://github.com/datamoc/mwg/blob/main/CLOSED.md) for the full, numbered shipped history.
@@ -184,6 +188,63 @@ private or trustworthy, since the page that rewrites a save can recompute any ch
184
188
  never put a secret in a save or in settings (an account token, a password, a licence key), and
185
189
  treat a save as the player's own editable file, which it is.
186
190
 
191
+ ## Loading assets and input
192
+
193
+ `await load(['vehicles/car.svg'], { resolution: 2 })` rasterizes SVG assets at twice
194
+ their logical resolution, keeping them sharp when a camera zooms in. Import `load` from
195
+ `@datamoc/mw_games/assets`. `loadSvgParts(path, { resolution: 2 })` loads top-level SVG
196
+ groups as textures in their authored stacking order.
197
+
198
+ Input events live under the `Input` namespace: import `{ Input }` from
199
+ `@datamoc/mw_games/core` and subscribe with `Input.onAction.add(...)`. `onAction` is not a
200
+ direct core export. `Input.attachPointer` supplies pointer and drag events for mouse and touch.
201
+
202
+ ## Audio
203
+
204
+ Use `AudioBus` for one playback API across ordinary audio and MIDI assets. Its default
205
+ loader reads embedded assets, detects MIDI by its header and renders it on a worker.
206
+ `playBgm`/`playBgs` loop, while `playMe`/`playSe` play once; all take the same
207
+ `{ name, volume, pitch, pan }` fields (volume 0-100, pitch 100 unchanged, pan -100 to 100).
208
+ Pass `load: createAudioLoader(context, { midi: { soundFontBytes, missingProgram: 'synth' } })`
209
+ to use a SoundFont. `Sound` remains useful for pooled HTML audio effects and `MusicPlayer`
210
+ for a seekable jukebox.
211
+
212
+ `Sound` pools short effects. `sound.play(0.7, 1.1)` sets gain and pitch for that play;
213
+ `synthesizeSound({ waveform: 'sawtooth', frequency: 110, duration: 0.2 })` makes a pooled
214
+ effect without an asset file. `Sound.plays` counts started playback attempts for browser tests.
215
+
216
+ For MIDI music, copy the supplied `data/microgm-opt.sf2` from the installed package into
217
+ your assets folder alongside the `.mid` files. `mwgPage({ assets: 'assets' })` automatically
218
+ reduces the embedded SoundFont to the programs those MIDI files use. Load both through
219
+ `loadBinary`, then render once on a worker and play the resulting buffer:
220
+
221
+ For a smaller starting bank, `data/microgm-essential.sf2` supplies piano, guitar, violin,
222
+ trumpet, flute and standard drums in 314 KB. Use it instead of the full bank below, with
223
+ `missingProgram: 'synth'` for the remaining instruments. Both banks retain their WTFPL licence.
224
+
225
+ ```ts
226
+ import { loadBinary, getBinary } from '@datamoc/mw_games/assets';
227
+ import { MusicPlayer, renderMidiToBufferAsync } from '@datamoc/mw_games/audio';
228
+
229
+ const player = new MusicPlayer(new AudioContext(), { volume: 0.7 });
230
+ await loadBinary(['music/theme.mid', 'soundfonts/microgm-opt.sf2']);
231
+ player.load(await renderMidiToBufferAsync(getBinary('music/theme.mid'), {
232
+ soundFontBytes: getBinary('soundfonts/microgm-opt.sf2'),
233
+ missingProgram: 'synth',
234
+ }));
235
+ player.play({ repeat: true });
236
+ ```
237
+
238
+ Start playback from a player gesture. Call `player.update()` each frame to dispatch
239
+ `onEnded` for a nonrepeating track; that signal lets the game choose its next track.
240
+ `pause()`, `resume()`, `position()` and `seek(seconds)` share the same playhead.
241
+ `toAudioBuffer(context, rendered)` also converts a render for direct `Channel` use.
242
+ `missingProgram: 'synth'` voices instruments absent from the font with the built-in synth.
243
+
244
+ To catch page growth, set `mwgPage({ maxPageBytes: 1_500_000 })`. Its build report lists
245
+ embedded paths not found literally in the generated game script as potentially unused;
246
+ paths assembled at runtime need manual review. `assetFiles` selects exact asset paths to embed.
247
+
187
248
  ## Capability spec
188
249
 
189
250
  The definition of done for 1.0, drawn from what each of the nine references actually
package/REFERENCE.md CHANGED
@@ -530,6 +530,12 @@ batcher/high-shader internals are confined to `ColorTransformBatcher.ts`.
530
530
  `sequence(steps)` (item 302) chains fade/hold/flash steps end to end as one call, for the
531
531
  hold-then-fade-back transition the four individual methods cannot express alone; `update`
532
532
  keeps returning false at every step boundary, true only once the whole sequence finishes.
533
+ - `SceneTransition`/`TransitionOptions`/`TransitionRunner` + `tileShrink`, `shrink`, `pop`, `grow`, `slide`, `iris`
534
+ and `dissolve` - screen transitions: `Game.switchScene(Next, { transition, duration, easing })` photographs the
535
+ outgoing scene and animates it away (or grows the new one over it), `Scene2D.transitionTo(swap, options)` does
536
+ the same for a scene changing its own content, `Game.transitioning` tells a scene to ignore input, and
537
+ reduced motion skips it. `tileDelays`, `tileProgress`, `popScale`, `openAnimationFrame` and `resolveEasing` are the
538
+ timing as plain functions.
533
539
  - `ActorAnimator` - an idle/move/action animation state machine with one interruption rule.
534
540
  - `StatusVisuals` - composes every active status effect's colour additively onto a sprite's
535
541
  `colorAdd` (item 301), instead of one status winning by declaration order; each channel clips
@@ -663,7 +669,9 @@ Windows, lists, message boxes, HUD widgets - all themed from one live-swappable
663
669
  - `BitmapLabel`/`bitmapLabelStyle` - bitmap-font-backed text, for a HUD value redrawn
664
670
  every frame where `Label`'s per-string texture re-render would be wasteful.
665
671
  - `NinePatch` - a resizable nine-slice panel.
666
- - `Window` - a themed panel container. `close()` frees the window and its contents and is
672
+ - `Window` - a themed panel container. `open({ animation, from })` plays `pop`, `grow`, `slide` or `fade`
673
+ (`WindowAnimation`) and `close({ animation })` plays it backwards before closing, both driven by
674
+ `update(dt)` and skipped under reduced motion. `close()` frees the window and its contents and is
667
675
  idempotent; `closed` is the guard for the caller's reference afterwards (input and `place` on a
668
676
  closed window are no-ops), and `WindowStack.push` refuses one with a named error. A window's own
669
677
  area is not clickable, so a click travels to whatever is under it; `blocker: true` adds the
@@ -695,7 +703,11 @@ Windows, lists, message boxes, HUD widgets - all themed from one live-swappable
695
703
  - `Spinner`/`SpinnerOptions` + `spinValue` - an up/down numeric stepper; `spinValue` is the rule
696
704
  (snap to the step, then clamp at the ends or wrap past them).
697
705
  - `DropdownView`/`DropdownViewOptions` - the widget over a `Dropdown` model: a button showing the choice
698
- that opens a column of `Button`s, the chosen one marked with `Button.setSelected`.
706
+ that opens a column of `Button`s on an opaque backdrop, the highlighted one marked with
707
+ `Button.setSelected`. `maxVisibleRows` makes a long list scroll (wheel, touch, keys, scrollbar) and it opens with
708
+ the chosen row in view; `autoWidth` fits the longest label; `handleKey` (arrows, Page, Home/End, Enter,
709
+ Escape) and `handleOutsideClick` are what the scene forwards from its own input; `fitBelow` limits the
710
+ rows to what the viewport leaves under the button.
699
711
  - `Dropdown`/`DropdownOption`/`DropdownOptions` - a renderer-free option button: a selected entry,
700
712
  an open list, a highlight that starts on the selection and skips disabled options.
701
713
  - `TextModel`/`TextModelOptions` - the editing state behind a text field, fed by `core.Input`'s
@@ -727,8 +739,19 @@ Windows, lists, message boxes, HUD widgets - all themed from one live-swappable
727
739
  highlight.
728
740
  - `TreeView`/`TreeNode`/`TreeRow`/`TreeViewOptions` - a renderer-free collapsible tree, flattened to
729
741
  the rows on screen; collapsing the branch the highlight is inside lands it on the branch.
730
- - `ScrollBox`/`ScrollBoxOptions` + `scrollOffset` - a clipped viewport with a themed scrollbar;
731
- `scrollOffset` is the clamping rule the wheel, `scrollBy` and `scrollIntoView` all share.
742
+ - `ScrollBox`/`ScrollBoxOptions`/`ScrollKey` + `scrollOffset` - a clipped viewport with a themed scrollbar;
743
+ `scrollOffset` is the clamping rule the wheel, `scrollBy` and `scrollIntoView` all share. `touch: true` lets a
744
+ finger drag the content (a drag cancels the tap on the row under it) and coast on release, `handleKey`
745
+ scrolls by line, page and to either end, the thumb drags and the track jumps, and `autoHeight` sizes the
746
+ viewport to the content up to `height`. `update(dt)` drives the coasting.
747
+ - `ScrollDrag` - touch scrolling as plain numbers (`start`/`move`/`end`/`step`): how far the content follows a
748
+ finger and how it coasts after release, with no pointer events or clock of its own.
749
+ - `ScrollText`/`ScrollTextOptions` - a wrapped `Label` in a self-sizing `ScrollBox`: as tall as the text up to
750
+ `maxHeight`, scrolling beyond it, re-measured by `setText`.
751
+ - `MarqueeLabel`/`MarqueeLabelOptions` + `marqueeOffset` - one line that stays on one line: a long text slides
752
+ back and forth in a clipped window (and holds still, cut short, under reduced motion).
753
+ - `ellipsize` and `truncateLines` - cut a text to a width or a number of lines, ending in an ellipsis, with the
754
+ caller supplying the measuring function.
732
755
  - `flowLayout(items, width, { gap, align })` - left-to-right rows that wrap, returning each item's
733
756
  position and the total height: a toolbar that takes two rows on a phone.
734
757
  - `Grid`/`GridSpec`/`GridTrack` + `resolveAnchor`/`anchorAlign` with `Anchor`/`AnchorSpec`/
@@ -774,6 +797,10 @@ Windows, lists, message boxes, HUD widgets - all themed from one live-swappable
774
797
  - `Toast` - a queued, timed pop-up notification (fade in, hold, fade out).
775
798
  - `Tooltip` - a hover explanation over a themed `Window`: a frame-driven hover delay, and
776
799
  edge-aware placement that flips rather than letting the panel run off screen.
800
+ - `TooltipHost`/`TooltipHostOptions`/`TooltipTarget` - drives one `Tooltip` from rectangles: a press that rests
801
+ for `longPress` seconds shows the text on a touch screen, and a disabled `Button` (which gets no pointer
802
+ events) is still explained from its bounds and its `tooltip`. `attach(pointer)` wires an
803
+ `Input.attachPointer` tracker.
777
804
  - `HelpScreen` - a topic-list-plus-body help/controls screen.
778
805
  - `StatsScreen` - a player-stats display screen.
779
806
  - `LoadingScreen` - a progress-bar loading screen wired to `core.LoadQueue`.
@@ -895,11 +922,17 @@ reach the compiled asset map without it.
895
922
  (`force`, smoothed `ratio`, `peakRatio`) and `onBreak` when a link passes its strength; `ground` (a height or a function of x) holds nodes
896
923
  on a surface with `groundFriction`. The engine of
897
924
  Pontifex, generalised: bridges, trusses, cranes, anything where the question is which beam fails.
925
+ `XpbdWorld<T>`/`XpbdLink<T>`/`XpbdLinkSpec<T>` carry game-owned link `data` without
926
+ affecting the solver. `breakLink(index)` dispatches rupture once. `linksInBox` includes
927
+ segment crossings; `linksNear` orders nearby unbroken segments by distance. `snapshot()`
928
+ returns positions, velocities, load state and queued forces as detached arrays; `hash()`
929
+ compares their exact numeric bits for runs with the same configuration. Payloads and ground
930
+ callbacks are excluded. The bridge example applies a moving vehicle's weight with `applyForce`.
898
931
  - `PhysicsWorld` (`@datamoc/mw_games/physics`) - rigid bodies and constraints in plain TypeScript, no engine
899
932
  dependency: `addBox`/`addCircle`/`addConstraint`/`remove`, `update(dt)` from
900
933
  `Scene.update`, `onContact`, `alpha` for interpolation. A constraint with `maxStretch` breaks when pulled
901
934
  that far past its rest length (`onBreak`, `stretchOf`). Bodies never spin; `angle` is a display offset.
902
- - `synthesizeTone`/`synthesizeSound`/`playTone`/`Waveform` - a runtime square/triangle/sine/noise
935
+ - `synthesizeTone`/`synthesizeSound`/`playTone`/`Waveform` - a runtime square/triangle/sine/noise/sawtooth
903
936
  waveform synth for a procedural tone or SFX, no sample library; `synthesizeSound` returns a
904
937
  pooled `Sound` over the tone.
905
938
  - `parseMidi`/`scheduleMidi`/`midiLoopStart`/`noteToFrequency`/`MidiPlayer` - a small `.mid` file player
@@ -964,6 +997,15 @@ reach the compiled asset map without it.
964
997
  the page (FluidR3's piano, guitar and drums: 2.7 MB, against 20 MB decoded to a SoundFont 2),
965
998
  and `loadSoundFont` decodes it at load, so prefer it over a SoundFont 2 for anything but the
966
999
  smallest banks.
1000
+ - `createAudioLoader`/`AudioLoaderOptions` - a cached loader for embedded audio and MIDI,
1001
+ shared by all `AudioBus` lifetimes. MIDI is detected by header and rendered on a worker;
1002
+ other files use `decodeAudioData`. Options select SoundFont/render settings or a byte source.
1003
+ `AudioBus` uses this loader by default, so `playBgm` and `playSe` accept asset paths with
1004
+ the same volume, pitch and pan fields. BGM/BGS loop; ME/SE play once.
1005
+ - `MusicPlayer`/`MusicPlayerOptions`/`MusicPlayOptions` - one loaded `AudioBuffer` or
1006
+ `RenderedMidi`, with play, pause, resume, seek, position, volume and an `onEnded` signal.
1007
+ Call `update()` each frame to dispatch track completion and choose the next track.
1008
+ `toAudioBuffer(context, rendered)` copies rendered stereo samples into an `AudioBuffer`.
967
1009
  - `Channel`/`AudioBus` - buffered WebAudio music: a `Channel` plays one looping track with
968
1010
  volume, pitch, pan, a loop region and a seekable playhead, and an `AudioBus` wires two of
969
1011
  them into independent BGM/BGS loops, ME jingles that suspend and resume the BGM,
@@ -1528,6 +1570,8 @@ nothing here reaches a shipped game unless a test imports it.
1528
1570
  - `memoryStorage` - a `SaveStorage` held in memory, for `SaveSystem`, `Settings`, `Collection`,
1529
1571
  `PlayerStats` and the other stored values; the same class `core` falls back to where
1530
1572
  `localStorage` is missing.
1573
+ - `exposeForTests(name, object)` - publishes inspectable game state under
1574
+ `window.__mwg_test__[name]` for browser tests; does nothing outside a browser.
1531
1575
  - `fakeAudio`/`fakeAudioWithEnded`/`FakePlayable` - a `Playable` for `Sound`/`Music`'s `create`
1532
1576
  that counts its plays; the second also has the `onended` slot a backend reporting track ends
1533
1577
  has.
@@ -1747,6 +1791,11 @@ consumes generated data and does not parse `.mwl` source files in the browser.
1747
1791
  `id=rebels` validates) keys `world.sides` and `world.gold`, marks a map keep (`rebels Kh`), and
1748
1792
  is what `unit.side`, `MwlMessage.side` and every `side`/`side_filter` filter hold. `MwlSideRef`
1749
1793
  is just that id.
1794
+ - `nodesToObjects`/`MwlObject` - converts compiled nodes to JSON objects using the tag
1795
+ schema: declared numbers and booleans become typed fields, child tags become ordered
1796
+ arrays, and a `cardinality.max` of 1 selects a single object. Missing optional fields stay
1797
+ absent. Check the returned diagnostics for conversion errors and field collisions.
1798
+ Attributes not listed in `MwlTagSchema.required` are optional; use `number`, not `number?`.
1750
1799
  - `readAttributes` / `readChildren` - shared typed readers for compiled nodes. They coerce
1751
1800
  scalars and lists and return source-located diagnostics instead of silently guessing.
1752
1801
  - `MwlNode.attributeLocations` / `valueLocations` - parsed JSON5 nodes retain the exact key and
@@ -1769,6 +1818,9 @@ consumes generated data and does not parse `.mwl` source files in the browser.
1769
1818
  absent cell omits the key), and `emitTableTypes` emits one named row interface plus one
1770
1819
  typed accessor per table for a game's build script to write and import, so a changed
1771
1820
  column type or a removed column fails type-checking at every use.
1821
+ `emitTableTypes(tables, { literalColumns: ['id', 'sprite'] })` also exports sorted,
1822
+ deduplicated readonly value lists and literal unions such as `VehiclesSprite` from the
1823
+ current string-column values. Regenerate them with the catalog to catch stale game ids.
1772
1824
  - `MwlValidationOptions.tableReferences`/`MwlTableReference` (item 362) - declarative
1773
1825
  cross-table checks, so no game hand-rolls one validator per table: a column declares
1774
1826
  `references: { table, column }` or `oneOf: [...]`, and `validateCatalog` checks every cell