@datamoc/mw_games 0.7.1 → 0.7.2

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 (111) hide show
  1. package/CHANGELOG.md +964 -0
  2. package/README.md +27 -2
  3. package/dist/actors/Affix.d.ts +7 -1
  4. package/dist/actors/Affix.d.ts.map +1 -1
  5. package/dist/actors/Affix.js +4 -2
  6. package/dist/actors/Affix.js.map +1 -1
  7. package/dist/actors/StatBlock.d.ts +20 -0
  8. package/dist/actors/StatBlock.d.ts.map +1 -1
  9. package/dist/actors/StatBlock.js +14 -3
  10. package/dist/actors/StatBlock.js.map +1 -1
  11. package/dist/actors/index.d.ts +2 -2
  12. package/dist/actors/index.d.ts.map +1 -1
  13. package/dist/actors/index.js +1 -1
  14. package/dist/actors/index.js.map +1 -1
  15. package/dist/ai/index.d.ts +118 -0
  16. package/dist/ai/index.d.ts.map +1 -0
  17. package/dist/ai/index.js +203 -0
  18. package/dist/ai/index.js.map +1 -0
  19. package/dist/ai/lua.d.ts +47 -0
  20. package/dist/ai/lua.d.ts.map +1 -0
  21. package/dist/ai/lua.js +195 -0
  22. package/dist/ai/lua.js.map +1 -0
  23. package/dist/ai/search.d.ts +59 -0
  24. package/dist/ai/search.d.ts.map +1 -0
  25. package/dist/ai/search.js +100 -0
  26. package/dist/ai/search.js.map +1 -0
  27. package/dist/board/Engine.d.ts +14 -0
  28. package/dist/board/Engine.d.ts.map +1 -1
  29. package/dist/board/Engine.js +33 -45
  30. package/dist/board/Engine.js.map +1 -1
  31. package/dist/board/index.d.ts +1 -1
  32. package/dist/board/index.d.ts.map +1 -1
  33. package/dist/board/index.js +1 -1
  34. package/dist/board/index.js.map +1 -1
  35. package/dist/core/Csv.d.ts +10 -0
  36. package/dist/core/Csv.d.ts.map +1 -1
  37. package/dist/core/Csv.js +34 -3
  38. package/dist/core/Csv.js.map +1 -1
  39. package/dist/core/Entity.d.ts +2 -2
  40. package/dist/core/Entity.d.ts.map +1 -1
  41. package/dist/core/Entity.js +18 -4
  42. package/dist/core/Entity.js.map +1 -1
  43. package/dist/index.d.ts +1 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +1 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/mw_games.global.js +96 -96
  48. package/dist/mwl/actors.d.ts +3 -1
  49. package/dist/mwl/actors.d.ts.map +1 -1
  50. package/dist/mwl/actors.js +5 -0
  51. package/dist/mwl/actors.js.map +1 -1
  52. package/dist/mwl/catalog.d.ts.map +1 -1
  53. package/dist/mwl/catalog.js +2 -10
  54. package/dist/mwl/catalog.js.map +1 -1
  55. package/dist/mwl/compiler.d.ts +14 -0
  56. package/dist/mwl/compiler.d.ts.map +1 -1
  57. package/dist/mwl/compiler.js +23 -0
  58. package/dist/mwl/compiler.js.map +1 -1
  59. package/dist/mwl/conditions.d.ts +9 -0
  60. package/dist/mwl/conditions.d.ts.map +1 -0
  61. package/dist/mwl/conditions.js +162 -0
  62. package/dist/mwl/conditions.js.map +1 -0
  63. package/dist/mwl/content.d.ts +25 -0
  64. package/dist/mwl/content.d.ts.map +1 -1
  65. package/dist/mwl/content.js +46 -34
  66. package/dist/mwl/content.js.map +1 -1
  67. package/dist/mwl/fengari.d.ts +8 -0
  68. package/dist/mwl/fengari.d.ts.map +1 -0
  69. package/dist/mwl/fengari.js +136 -0
  70. package/dist/mwl/fengari.js.map +1 -0
  71. package/dist/mwl/hooks.d.ts +1 -1
  72. package/dist/mwl/index.d.ts +16 -7
  73. package/dist/mwl/index.d.ts.map +1 -1
  74. package/dist/mwl/index.js +7 -3
  75. package/dist/mwl/index.js.map +1 -1
  76. package/dist/mwl/readers.d.ts +18 -0
  77. package/dist/mwl/readers.d.ts.map +1 -0
  78. package/dist/mwl/readers.js +58 -0
  79. package/dist/mwl/readers.js.map +1 -0
  80. package/dist/mwl/report.d.ts +9 -0
  81. package/dist/mwl/report.d.ts.map +1 -0
  82. package/dist/mwl/report.js +36 -0
  83. package/dist/mwl/report.js.map +1 -0
  84. package/dist/mwl/runtime.d.ts +37 -1
  85. package/dist/mwl/runtime.d.ts.map +1 -1
  86. package/dist/mwl/runtime.js +170 -19
  87. package/dist/mwl/runtime.js.map +1 -1
  88. package/dist/mwl/schema.d.ts +13 -1
  89. package/dist/mwl/schema.d.ts.map +1 -1
  90. package/dist/mwl/schema.js +195 -8
  91. package/dist/mwl/schema.js.map +1 -1
  92. package/dist/mwl/scripts.d.ts +15 -0
  93. package/dist/mwl/scripts.d.ts.map +1 -0
  94. package/dist/mwl/scripts.js +23 -0
  95. package/dist/mwl/scripts.js.map +1 -0
  96. package/dist/mwl/utils.d.ts +8 -0
  97. package/dist/mwl/utils.d.ts.map +1 -0
  98. package/dist/mwl/utils.js +37 -0
  99. package/dist/mwl/utils.js.map +1 -0
  100. package/dist/two-d/render/Types2D.d.ts +19 -4
  101. package/dist/two-d/render/Types2D.d.ts.map +1 -1
  102. package/dist/two-d/render/Types2D.js +25 -0
  103. package/dist/two-d/render/Types2D.js.map +1 -1
  104. package/dist/two-d/render/index.d.ts +2 -2
  105. package/dist/two-d/render/index.d.ts.map +1 -1
  106. package/dist/two-d/render/index.js +1 -1
  107. package/dist/two-d/render/index.js.map +1 -1
  108. package/dist/version.d.ts +1 -1
  109. package/dist/version.js +1 -1
  110. package/package.json +19 -3
  111. package/tools/mwl.mjs +32 -6
package/CHANGELOG.md ADDED
@@ -0,0 +1,964 @@
1
+ # Changelog
2
+
3
+ All notable changes to `mwg` are documented here. Format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versioning follows
5
+ [Semantic Versioning](https://semver.org/) as of this first release - a 0.y.z version means
6
+ the public API may still change between minor versions.
7
+
8
+ ## [0.7.2] - 2026-09-11
9
+
10
+ ### Added
11
+
12
+ - Renderer-free JavaScript and optional Lua AI modules, including bounded alpha-beta search for
13
+ local actors and top-level controllers.
14
+ - MWL typed tables, references, campaign metadata, deterministic artifact emission, and richer
15
+ event, dialogue, expression, and persistence support.
16
+ - Stable caller-chosen entity ids, typed 2D value exports, affix pool filtering, and shared
17
+ CSV/MWL cell coercion.
18
+
19
+ ### Changed
20
+
21
+ - The chess example now uses the shared MWG alpha-beta primitive.
22
+ - The standalone bundle baseline, API report, documentation, and roadmap were updated.
23
+ - Release checks now isolate npm audit and package-smoke caches from user-level npm settings.
24
+
25
+ ## [Unreleased]
26
+
27
+ ## [0.7.1] - 2026-09-10
28
+
29
+ ### Added
30
+
31
+ - MWL content-directory builds now generate game data, i18n, and asset manifests, with the
32
+ battle example consuming generated MWL content.
33
+ - `extract:html` extracts inline scripts, styles, data URLs, CSS data URLs, and `srcset`
34
+ resources from a self-contained HTML file.
35
+
36
+ ### Changed
37
+
38
+ - MWL generated-source diagnostics now use deterministic relative paths.
39
+
40
+ ## [0.7.0] - 2026-09-10
41
+
42
+ ### Added
43
+
44
+ - MWL build-time compilation with one command producing compiled data, i18n, and asset manifests.
45
+ - Generic MWL item adapters for inventory, equipment slots, and stat modifiers.
46
+ - Catalog validation for duplicate ids, slots, effects, and hook references, with source locations.
47
+ - Game-provided numeric expressions and versioned MWL save migrations.
48
+ - Wesnoth CFG asset fields including images, profiles, icons, and sounds.
49
+
50
+ ## [0.6.0] - 2026-09-10
51
+
52
+ This is a pre-alpha, not an "early release" any more, and the project says so in README. The
53
+ same note records the naming plan for future major versions: 1.0 takes a name beginning with A,
54
+ 2.0 one beginning with B, and so on, none of them chosen yet. Code-wise this release is the
55
+ reduced-motion cluster from WebKit's "Responsive Design for Motion", done properly rather than
56
+ as a single media query.
57
+
58
+ ### Added
59
+ - Reduced motion beyond the media feature (roadmap items 199-202): `MotionIntent` and
60
+ `motionDuration` let a caller say whether a motion is decorative or meaningful, so a trigger
61
+ can be replaced by a fade rather than deleted; `Tweener.tween` takes `{ intent, alternate }`;
62
+ camera follow and `FloatingText`'s rise now respect the preference (and `AnimatedSprite`
63
+ documents why it deliberately does not); `watchReducedMotion` reports a change while a game is
64
+ running, so an in-flight tween, burst or fade stops instead of finishing; and the interface
65
+ example gained a motion demo that `npm run motion:smoke` opens three ways (no preference,
66
+ `prefers-reduced-motion: reduce` before load, and a flip mid-session) in a real browser.
67
+
68
+ ## [0.5.5] - 2026-09-10
69
+
70
+ Accessibility reaches the framework floor: a reduced-motion switch the tween, particle, camera
71
+ and screen-effect paths already consult, WCAG contrast helpers for a `theme` palette, and a
72
+ screen-reader bridge `Window` and `MessageBox` announce through. The verification story catches
73
+ up as well - a published-package `file://` smoke, a bundle-size budget, a coverage floor,
74
+ cross-version save fixtures and a build of every example, all gated in CI. `Blob` moves to
75
+ `core` (a breaking import change) with a per-cell `clear`, `assets.load` can rasterize a vector
76
+ source at a chosen resolution, and a new benchmark settles the CSS/SVG-animation question in
77
+ Pixi's favour.
78
+
79
+ ### Added
80
+ - `assets.load(paths, { resolution })` (roadmap item 198): rasterizes a vector source (SVG)
81
+ at a multiple of its intrinsic size instead of only at intrinsic size, so a game that zooms
82
+ into an icon asks for `2` or `3` rather than shipping a soft bitmap. Passing `onProgress`
83
+ alone still works. The colour-transform example loads its gem SVG at 2x.
84
+ - `npm run benchmark:animation`: measures CSS/SVG element animation against Pixi sprites (and
85
+ both at once) at increasing counts, sampling rAF intervals in headless Chrome. On an RTX
86
+ 3070, 4000 composer-animated SVG elements fell to ~14 fps while 4000 Pixi sprites held 60.
87
+ - Accessibility (roadmap item 192): `core.reducedMotion`/`setReducedMotion`/
88
+ `prefersReducedMotion` (the OS `prefers-reduced-motion` preference with a per-game override,
89
+ consulted by `Tweener`, `Camera`, `ScreenEffects` and `ParticleEmitter`),
90
+ `ui.contrastRatio`/`meetsContrast`/`relativeLuminance` for WCAG-checking a palette, and
91
+ `ui.screenReader`, a hidden `aria-live` mirror that `Window` (its title) and `MessageBox`
92
+ (each page and its choices) already announce through.
93
+ - `npm run package:smoke` (roadmap item 193): packs the tarball, installs it into a scratch
94
+ directory outside the repo, builds the tutorial's own tiny game and opens both that page and
95
+ the standalone `mw_games.global.js` from `file://` in headless Chrome.
96
+ - `npm run size:check` / `size:update` (194): the global bundle and `dist` against the
97
+ committed `tools/bundle-size.json`, failing past a 2% growth.
98
+ - `npm run coverage:check` (195): the existing suite under Node's line/branch/function floors.
99
+ - Cross-version save fixtures (196): committed `tests/fixtures/saves/` blobs for a floor,
100
+ a `GameState` and a `Blob`, loaded by `tests/save-compat.test.ts`.
101
+ - `npm run examples:build` (197): runs every `example:*:build` script, so a vite or
102
+ `emit-page` regression in any example is caught, not just the benchmarked ones.
103
+ - `npm run coverage` and `npm run visual:smoke:ui`. Coverage reuses the existing Node test
104
+ suite through `--experimental-test-coverage`, so measuring it costs no dependency. The
105
+ visual smoke builds the interface example, opens it from `file://` in headless Chrome, and
106
+ fails when the page errors or the canvas paints nothing, attaching the screenshot as a
107
+ GitHub artifact; CI runs it on every pull request.
108
+ - `core.Blob` gained a per-cell `clear(x, y)` (roadmap item 191): it zeroes one cell's
109
+ volume and leaves its neighbours alone, so a game can douse a single tile, seal a breach,
110
+ or put out one patch of fire without rebuilding the field.
111
+ - `npm run api:report` / `npm run api:check`, backed by the new committed `API_REPORT.md`:
112
+ a generated index of every public export and its declaration, so API renames, removals
113
+ and signature changes are visible in review instead of discovered by consumers.
114
+ `tests/api-surface.test.ts` emits declarations with `tsc --emitDeclarationOnly` and fails
115
+ when the committed report drifts; CI also runs `api:check` after the build.
116
+ - `tests/barrel-collisions.test.ts`: a guard that fails when two `export *` sources in the
117
+ same barrel export the same name, which TypeScript would otherwise resolve by silently
118
+ exporting neither.
119
+ - `@datamoc/mw_games/two-d/pixi-interop` is now a real published subpath, matching the
120
+ escape hatch ADR-001 already documented.
121
+
122
+ ### Changed
123
+ - CI now runs the coverage floor, the bundle-size budget, `examples:build` and the
124
+ published-package smoke alongside its existing check/test/build/api gates, so neither
125
+ coverage, bundle growth, an unbuildable example nor a broken packed install can land
126
+ unnoticed.
127
+ - Formatting is now gated: Prettier over `src`, `tests`, `examples` and `tools`
128
+ (`.prettierrc.json`, `npm run format`/`format:check`, run in CI). No ESLint rule set comes
129
+ with it, and the reformat changes no behaviour (line wrapping, trailing commas and the
130
+ like); `barrel-collisions.test.ts` now tolerates Prettier wrapping a long
131
+ `export { ... } from './...'` across lines.
132
+ - ROADMAP.md: five verification gaps are recorded as the open backlog at the tail (193, a
133
+ published-package `file://` smoke; 194, a bundle-size budget; 195, a coverage floor; 196,
134
+ cross-version save fixtures; 197, building every example in CI), each with the concrete
135
+ number or path it should assert against. The 1.0 checklist's API line widens from
136
+ `DEPRECATED` alone to a full stability-marker contract (`@experimental` plus `DEPRECATED`).
137
+ - ROADMAP.md: item 192 records the accessibility gaps the list had never named (screen-reader
138
+ text for `ui` widgets, `prefers-reduced-motion`, palette contrast) as one open backlog item
139
+ at the tail; the roadmap-progress test now guards that open items sit after every shipped
140
+ one rather than forbidding them, so a recorded idea needs no test edit. The 1.0 exit
141
+ checklist now decides the published `dist` map files explicitly instead of leaving them to
142
+ drift.
143
+ - **Breaking: `Blob` moved from `mwg/roguelike` to `mwg/core`.** A grid of volumes is not a
144
+ dungeon-crawl concern, so the field now lives in the renderer-free `core` module (still
145
+ reachable at the top level from `mw_games`) and imports nothing. Import it from
146
+ `@datamoc/mw_games/core` instead of `@datamoc/mw_games/roguelike`. `spread`'s predicate
147
+ parameter is now named `open` rather than `passable`; positional callers are unaffected.
148
+ - Documentation page is now the `REFERENCE.md` module guide (with a sidebar TOC), and
149
+ the TypeDoc symbol dump lives under `documentation/api/` instead of being the front
150
+ door.
151
+ - ROADMAP.md: the last open item (175, the `pixi.js` dependency-shape question) is closed
152
+ by decision - `pixi.js` stays a direct dependency for the 0.x line and moves to an
153
+ optional peer dependency with 1.0. The stale priority prose is replaced with a current
154
+ statement, and the new "Parked decisions" and "1.0 exit checklist" sections record what
155
+ actually remains before 1.0.
156
+ - ADR-001's known gap is narrowed: `Sprite2D` closed the constructible-sprite half; the
157
+ remaining value-level exception is the one documented pixi-interop file.
158
+
159
+ ## [0.5.4] - 2026-09-10
160
+
161
+ Inline sound markers now travel with translated dialogue strings and fire at their visible
162
+ position during `MessageBox` reveals. The reference docs, webpage features, and string-editor
163
+ example now document and demonstrate `{sound:path}` markers.
164
+
165
+ ## [0.5.3] - 2026-09-10
166
+
167
+ Closes ROADMAP items 184-190, the string stack requested directly across one session:
168
+ translation editing (CLI and web), sound cues, f-string specs, markdown, and progressive
169
+ display.
170
+
171
+ ### Added
172
+ - `examples/string-editor` (roadmap item 190) - the string editor as a playable page:
173
+ an English reference beside an editable French translation, rendered live through
174
+ `RichLabel` with an adjustable `{HP_loose}` variable, placeholder-drift warnings, and
175
+ a blip/hit/pickup cue per string that fires on the progressive reveal. Listed on the
176
+ website's examples page with its own generated diagram.
177
+ - `i18n:edit` cue preview (roadmap item 189) - `Ctrl+P` plays the current row's sound
178
+ cue through the OS player (`afplay`, `aplay`/`paplay`/`ffplay`, Windows WAV via
179
+ SoundPlayer; `MWG_SFX_PLAYER` names a replacement), falling through only when the
180
+ command itself is missing. Plain `p` stays unbound so previews never start by accident.
181
+ - Shared progressive display (roadmap item 188) - `ui.startReveal`/`advanceReveal`/
182
+ `completeReveal`/`revealComplete` behind `Label.showProgressive`/`updateReveal` and
183
+ `RichLabel`'s markdown-aware counterpart (markers never count or leak, via
184
+ `ui.sliceSpans`); `MessageBox` now runs on the same primitive with its
185
+ confirm-completes-then-advances rule unchanged.
186
+ - `i18n:edit` placeholder and markdown rendering (roadmap item 187) - panes preview
187
+ markdown as terminal bold/italic (`v` toggles raw source), flag placeholder drift
188
+ with a `≠` marker and per-row detail, hint the needed tokens in the edit prompt, and
189
+ report both in `--check` mode. Backed by `i18n.tokenizeMessage`/`diffPlaceholders`,
190
+ the same grammar `t()` interpolates with, so the check cannot disagree with runtime.
191
+ - `ui.RichLabel`/`parseMarkdown`/`stripMarkdown` (roadmap item 186) - basic inline
192
+ markdown for UI text (`**bold**`, `*italic*`, combined `***both***`, backslash
193
+ escapes) through Pixi `HTMLText`. The parser is pure and unit-tested; unmatched
194
+ markers stay literal. Heavier than `Label`, so meant for descriptions and help
195
+ bodies, not per-frame numbers or typewriter reveal.
196
+ - `i18n.formatSpec` (roadmap item 185) - Python f-string-style value formatting for
197
+ catalog placeholders (`{dmg:03d}`, `{hp:.1%}`, `{name:>12}`), with `!s`/`!r`/`!a`
198
+ conversions and `=` debugging, wired into `t()`/`tRaw` and FTL parsing. Verified case
199
+ by case against CPython output; an ill-fitting spec leaves its placeholder untouched.
200
+ The one divergence nothing can close: JavaScript numbers carry no int/float
201
+ distinction, so integer-valued floats take the integer path.
202
+ - `i18n` semantic `audio` channel (roadmap item 184) - `format(message, 'audio')`
203
+ resolves `<type>.audio` to a sound path a game plays through `mwg/audio`'s `Sound`,
204
+ one more presentation of the same simulation event. Resolves raw via the new `tRaw`
205
+ (no typographic spacing, no RTL wrapping), returns `''` when no cue is declared, and
206
+ never falls back to a bare type holding a sentence. `validateMessageAudio` flags empty
207
+ and non-string audio entries.
208
+ - `tools/i18n-edit.mjs` (roadmap item 184) - a split-screen terminal translation editor:
209
+ reference language left, translation right, either side any language (`x` swaps them),
210
+ per-string cue editing (`s`, one cue per semantic family), JSON/FTL auto-detection,
211
+ and a `--check` mode for CI. Pure session logic lives in `i18n/EditSession.ts`.
212
+
213
+ ## [0.5.2] - 2026-09-09
214
+
215
+ Closes ROADMAP items 178-182, five ideas raised against a reference game whose `main.ts` had
216
+ grown to roughly 7,800 lines. 178 and 181 turned out to describe the same mechanism and were
217
+ built as one primitive rather than two.
218
+
219
+ ### Added
220
+ - `core.Registry` (roadmap item 180) - a named lookup registered once and read back by name,
221
+ the dispatch primitive underneath a catalog of factories or handlers, in place of a
222
+ hand-written `if`/`switch` cascade. No auto-discovery: a game still imports and registers
223
+ each entry itself.
224
+ - `core.SceneComponentHost`/`SceneComponent` (roadmap items 178, 181) - composes a scene out
225
+ of named sections (map logic, encounter logic, UI wiring), each its own module with
226
+ whichever `Scene` lifecycle hooks it needs, instead of one scene class accreting every
227
+ responsibility. Composition, not a base class, so it works whether a scene extends `Scene`
228
+ or `two-d.Scene2D`; uses `core.Registry` internally for its by-name lookup.
229
+ - `rpg.questsFromRows`/`QuestStageRow` (roadmap item 179) - groups a flat table of stage rows
230
+ (typically a `core.parseCSV` result, one row per stage sharing a `questId`) into
231
+ `QuestDefinition`s a `QuestLog` can `define`, the same relational shape `core.parseCSV`
232
+ already models elsewhere, in place of a hand-written `.ts` object literal nesting each
233
+ quest's stage array.
234
+ - `actors.toEntitySaveState`/`fromEntitySaveState`/`EntitySaveState` (roadmap item 182) - a
235
+ `BuiltEntity`'s mutable state (base stat values, level/experience, the carried item) in the
236
+ same "definitions supplied fresh on load" shape `StatBlock`/`Progression` already draw;
237
+ `fromEntitySaveState` rebuilds via `buildEntity` from the same row and catalog, then
238
+ overlays the saved state on top.
239
+
240
+ ## [0.5.1] - 2026-09-09
241
+
242
+ Closes ROADMAP item 148 (the last item open before this pass) and corrects an overstated
243
+ renderer-boundary claim found while auditing the roadmap for anything still open.
244
+
245
+ ### Added
246
+ - `board.HexSkirmish` (roadmap item 148) - `startingSkirmish`/`setSkirmishTerrain`/
247
+ `canPlaceSkirmishUnit`/`addSkirmishUnit`/`skirmishMoves`/`moveSkirmishUnit`/
248
+ `skirmishAttack`/`skirmishIncome`/`endSkirmishTurn`, a Wesnoth-style hex army-game rules
249
+ layer distinct from `board.Tactics`: per-terrain movement cost and defence, adjacency-only
250
+ attacks where a surviving defender always strikes back, capturable villages that grant
251
+ income and heal their owner's units, no zone of control.
252
+ - `two-d/render.Sprite2D` - a bare, untinted, MWG-named re-export of Pixi's `Sprite`,
253
+ alongside the existing `Node2D`/`Shape2D`/`Text2D`. Closes half the gap ADR-001 and
254
+ ROADMAP item 167 had overstated as fully closed (see Fixed below); prefer
255
+ `render.TintedSprite` the moment a colour transform is needed.
256
+ - `core.parseCSV` (roadmap item 176) - a header-row CSV string into an array of typed row
257
+ objects, so a table shaped like `actors.AffixTable` can be authored as a spreadsheet
258
+ instead of a hand-written `.ts` object literal. `columns` coerces named fields to
259
+ `'number'`/`'boolean'`/`'list'`/`'map'`; an empty cell omits that field entirely, the same
260
+ as an optional property never set. Takes a raw string with no opinion on how it was
261
+ loaded, the same boundary `i18n.parseFTL` already draws.
262
+ - `actors.buildEntity`/`buildEntities` (roadmap item 177) - one hero/monster file row
263
+ (typically a `core.parseCSV` result) into a fully wired entity: a `StatBlock` of base
264
+ stats (every non-reserved column), an optional `Progression` against a named growth
265
+ curve, a starting item carrying a named starting affix, and a `core.ReactionTable`
266
+ already holding a low-HP rule if the row names a threshold. Every named reference
267
+ resolves through a game-supplied `EntityTemplateCatalog` - `mwg` never invents what a
268
+ name means, the same boundary `AffixDef.id` already draws.
269
+
270
+ ### Fixed
271
+ - `ADR.md`'s ADR-001 and ROADMAP item 167 claimed the renderer boundary was fully closed
272
+ ("nothing left... not a gap"). Corrected (ROADMAP item 173): every *type* position across
273
+ the public API genuinely no longer names a `pixi.js` type, but `two-d/pixi-interop.ts` is
274
+ itself a literal re-export from `pixi.js`, and `package.json`'s `dependencies` still lists
275
+ `pixi.js` directly - a real, still-open gap at the value level, tracked as ROADMAP item
276
+ 175 rather than rushed through as a breaking dependency-shape change to an
277
+ already-published version.
278
+
279
+ ## [0.5.0] - 2026-09-09
280
+
281
+ A declarative reactions primitive, closing the "branch cascade of `if HP < n`" gap named
282
+ directly, plus a new website page collecting the framework's original, non-obvious
283
+ mechanisms for a game-developer audience.
284
+
285
+ ### Added
286
+ - `core.ReactionTable`/`ReactionRule` (roadmap item 172) - named `when`/`action` rules
287
+ checked against any state shape (a `StatBlock`'s values, or a plain object's own fields
288
+ such as an item's durability), firing `action` the moment `when` turns true and staying
289
+ quiet while it remains true - edge-triggered the same way `roguelike.BossPhases` tracks
290
+ entered phases. A `once` rule retires after firing; the default re-fires on the next
291
+ rising edge. Lives in `core`, not `actors`, so the same table watches a character or an
292
+ inanimate object without either module depending on the other.
293
+ - `webpage/features/index.html` - ten specific, original mwg mechanisms (declarative
294
+ reactions, the per-sprite multiply-and-add colour transform, `Barrier`'s outermost-first
295
+ layered absorption, `Advancement`'s unified points/branch/capstone track, `Affix`'s
296
+ trigger-routed curse-derived enchantments, `TriggerTracker`'s turn-window streaks, i18n's
297
+ bidi-isolate/typographic rules, `StatStages`'s bounded ladder as one modifier, `Scramble`'s
298
+ honestly-scoped obfuscation, and the deliberate `Scheduler`/`TurnClock` split), linked from
299
+ every hand-authored page's nav.
300
+
301
+ ## [0.4.3] - 2026-09-08
302
+
303
+ A simplification pass across storage, selection, and text options, one pathfinding
304
+ allocation fix, a new opt-in build tool, and an interactive architecture diagram on the
305
+ website.
306
+
307
+ ### Added
308
+ - `tools/compress-dist.mjs` (`@datamoc/mw_games/tools/compress-dist`) - writes precompressed
309
+ `.gz`/`.br` siblings (optionally `.xz` via a system `xz` binary) next to a build's text
310
+ files, for a server that can negotiate `Content-Encoding`; the originals are untouched, so
311
+ a `file://` page keeps working unchanged. Wired into `tools/emit-page.mjs`'s example build
312
+ step (`--no-compress`/`MWG_NO_COMPRESS=1` to skip, `--xz` to also archive).
313
+ - `webpage/design/architecture-explorer.html` - an interactive, pannable/searchable diagram
314
+ of `mwg`'s real module dependency graph (which modules reach `core`, which reach a
315
+ renderer), generated from `src/`'s own import graph rather than hand-drawn, linked from
316
+ the design page's "Module boundaries" section.
317
+
318
+ ### Changed
319
+ - `core.StoredValue` - the read-with-fallback/write/remove trio `PlayerStats`, `RunHistory`
320
+ and `NewsSeenTracker` each hand-rolled slightly differently now share one implementation.
321
+ - `core.NewsClient` is rebuilt on the shared `HttpTransport` every other injectable HTTPS
322
+ client here already used, rather than its own hand-rolled timeout/abort handling.
323
+ - `two-d/ui.SelectionModel` - the highlight/select/confirm/wrap-at-both-ends contract
324
+ `ListView` and `IconGrid` had begun to implement slightly differently now lives in one
325
+ place; each widget keeps only what actually differs (1-D rows vs. 2-D cells).
326
+ - `two-d/ui`'s `Label`/`BitmapLabel` share their text-option shape and RTL-aware default
327
+ alignment through a new internal `themedText` helper instead of two copies.
328
+ - `rpg`'s `FreeMover`/`GridMover` share their "play this animation if the sprite has it"
329
+ guard; `actors`'s `applyStatusEffect`/`applyItemStatusEffect` share their clock-lease/
330
+ cancel logic. Both were identical code living in two places.
331
+ - `vite.lib.config.ts` and the examples' shared Vite config turn off `reportCompressedSize`:
332
+ the estimate cost a full in-memory gzip of the bundle for a number `tools/compress-dist.mjs`
333
+ reports for real now.
334
+ - `roguelike.Level` gained `forEachNeighbor` (visits a cell's neighbours via callback instead
335
+ of allocating an array), used by `Pathfinder`'s flood fills and `Placement.cellsNear` - the
336
+ hottest per-cell allocation in map generation, now gone.
337
+
338
+ ## [0.4.2] - 2026-09-08
339
+
340
+ A roadmap-management pass alongside three real capability gaps closed: deterministic dungeon
341
+ content placement, i18n catalog content tooling, and 2D/3D asset loading unified where it
342
+ correctly can be.
343
+
344
+ ### Added
345
+ - `tools/roadmap-progress.mjs`/`roadmapProgress.js` gained an item-management panel: search,
346
+ "only open items", and local (never committed to `ROADMAP.md`) priority/status/assignee
347
+ triage per item, with export/import.
348
+ - `roguelike.candidateCells`/`cellsNear`/`selectDistinctCells` - deterministic content
349
+ placement over a `Level`: terrain/occupancy/region filters, radius-based clustering, and
350
+ bounded distinct-cell selection with a JSON-safe roll trace, composing for neighbouring
351
+ items, scattered decorations, and single room/branch rewards.
352
+ - `i18n.messageText`/`levenshteinDistance`/`findSimilarMessages` - near-duplicate catalog
353
+ message detection by edit distance.
354
+ - `i18n.catalogUsage`/`catalogCompleteness`/`pluralFormCoverage` - catalog statistics: used
355
+ vs. unused keys against a caller-supplied reference list, translated-fraction completion,
356
+ and CLDR plural-category coverage.
357
+ - `i18n.mergeCatalogKeys` - drops a merged key from a catalog, keeping the surviving key's
358
+ text; rewriting call sites stays the caller's own job.
359
+ - `assets.loadBinary`/`getBinary`/`isBinaryLoaded`/`releaseBinary` (also its own
360
+ `@datamoc/mw_games/assets/binary` entry point) - a renderer-free raw-byte cache, the
361
+ direct fix for `three-d`'s `Vox.parseVox` needing a game to fetch bytes itself.
362
+ - `3d/models`' `loadModelContainer3D`/`isModelContainerLoaded`/`releaseModelContainer` -
363
+ loads a glTF/GLB source once, caching the Babylon `AssetContainer`; a game calls the
364
+ container's own `instantiateModelsToScene()` for each independent, unaliased copy it
365
+ wants placed, rather than importing (and re-fetching) the same model every time.
366
+
367
+ ### Fixed
368
+ - `two-d/ui`'s right-to-left mirroring was incomplete: `Bar`'s fill always grew from the
369
+ left, `IconGrid` always laid its columns left-to-right and never swapped what the
370
+ `'left'`/`'right'` actions meant, and `HelpScreen` always put its topic list on the left.
371
+ All three now mirror under `theme().direction === 'rtl'`, matching `Window`/`ListView`/
372
+ `Button`/`Label`, which already did.
373
+ - `ROADMAP.md` had two items both numbered 161, a genuine duplicate; the misplaced one was
374
+ renumbered and moved to the true end of the list before being implemented.
375
+ - `tools/build-webpage-docs.mjs`'s nested `npm install` could fail when the caller's own
376
+ global `~/.npmrc` set an `allow-scripts` value npm rejects for a project-scoped install;
377
+ that env var is now explicitly cleared for the nested call.
378
+
379
+ ## [0.4.1] - 2026-09-07
380
+
381
+ A study of `mwg-pixel-dungeon` (a reference game, not in this repo) recommended closing the
382
+ PixiJS boundary the rest of the way, a formal simulation runtime, stable entity identity, and
383
+ semantic messaging as framework primitives. All of it landed this release, plus the eight
384
+ architecture decisions it proposed, recorded in the new `ADR.md`.
385
+
386
+ ### Added
387
+ - `core.EntityRegistry`/`EntityId` - assigns and looks up stable string ids for live objects,
388
+ so events, saves, AI targets and buffs can name a creature or item without holding it.
389
+ `idOf` is what a game passes as `Scheduler.toJSON`'s or `SimulationRuntime.snapshot`'s
390
+ `actorId`.
391
+ - `core.PresentationQueue` - plays a batch of simulation events one at a time, each waiting on
392
+ its own `play()`-returned duration, freed of any renderer - the presentation-side
393
+ counterpart to a `SimulationRuntime.dispatch()`/`runScenario()` result.
394
+ - `simulation.SimulationRuntime`/`SimulationContext`/`SimulationOutcome`/
395
+ `SimulationRuntimeRule`/`SimulationSnapshot` - a facade over one state + one
396
+ `roguelike.Scheduler` + one `core.Generator` for the interactive half of a turn-based
397
+ simulation: `dispatch` runs a single command, threading `random`/`scheduler` as context and
398
+ charging any returned cost; `snapshot`/`restore` capture and rebuild the whole triple.
399
+ Composes with the existing `advanceToInput`/`runScenario` rather than replacing them.
400
+ `simulation` also re-exports `Scheduler`/`Actor`/`SchedulerSnapshot` from `roguelike`.
401
+ - `roguelike.Scheduler` gained `postpone` (delaying an actor other than the current one) and
402
+ `toJSON`/`restore` (a deterministic snapshot keyed by a caller-supplied actor id), making it
403
+ the sole, serialisable authority on logical time.
404
+ - `i18n.SemanticMessage`/`MessageChannel`/`MessageFormatter`/`createCatalogFormatter` - a
405
+ typed `{ type, params }` communication intent rendered differently per channel (log/
406
+ compact/accessibility/debug) from the same catalog, built on the existing `t()`/plural/
407
+ Fluent machinery rather than a parallel implementation.
408
+ - `i18n.formatNumber`/`formatDate`/`formatList` - locale-aware `Intl` formatting for a message
409
+ that needs more than `{token}` substitution.
410
+ - `i18n.diffCatalogKeys`/`validateCatalog` - catalog consistency checks: two catalogs' key
411
+ sets compared, and one catalog's own empty messages/incomplete plural forms flagged.
412
+ - `two-d.render.Container2D`/`Texture2D`/`Rect`/`TextureRegion`/`rectOf` - plain,
413
+ renderer-free public types in place of `pixi.js`'s own `Container`/`Texture`/`Rectangle`;
414
+ `Scene2D.stage` is now typed `Container2D`, `SpriteSheet.region` returns a `TextureRegion`.
415
+ - `two-d.render.Node2D`/`Shape2D`/`Text2D`/`TiledSprite`/`Gradient` - bare, MWG-named
416
+ re-exports of Pixi's `Container`/`Graphics`/`Text`/`TilingSprite`/`FillGradient`, so a game
417
+ never has to name `pixi.js` for a plain layer, vector drawing, one-off text, a scrolling
418
+ tile, or a gradient fill.
419
+ - `two-d.pixi-interop` - the one sanctioned, explicit escape hatch for the rare Pixi-specific
420
+ need the facade above does not cover.
421
+ - `TintedSprite` (and everything built on it - `AnimatedSprite`, `TileMap`, `DialogueStage`)
422
+ now registers its own colour-transform Pixi pipe automatically on import; a game no longer
423
+ passes `{ extensions: [registerColorTransform] }` to `Game`.
424
+ - `ADR.md` - the eight architecture decisions from the `mwg-pixel-dungeon` study, each
425
+ recorded against the code and tests that enforce it.
426
+
427
+ ### Fixed
428
+ - `ButtonSkin.texture`, `ButtonOptions.icon`, `IconGridItem.icon`, `ListItem.icon`, and
429
+ `Theme.panel` were still typed as raw `pixi.js` `Texture`/`Container` in public interfaces;
430
+ all retyped to `Texture2D`/`Container2D` (the same underlying type, not a breaking change).
431
+
432
+ ## [0.4.0] - 2026-09-06
433
+
434
+ **The reshaping release, and intended to be the last one.**
435
+
436
+ `mwg` grew fast, and grew by accretion: the renderer ended up inside `mwg/core`, an event
437
+ interpreter ended up importing a widget library, the same idea got solved three or four
438
+ different ways in different modules, and several names meant a different thing depending on
439
+ which file you were reading. None of that was going to get cheaper to fix. A rename costs a
440
+ line in this file today; after 1.0 it costs somebody else's build.
441
+
442
+ So this release spends the breakage budget deliberately, all at once, while the version
443
+ number still says it is allowed:
444
+
445
+ - **The renderer is no longer part of the core.** `mwg/core` now holds the scene lifecycle,
446
+ input, saves, RNG and signals, and pulls in no renderer at all. `Game` and the display half
447
+ of `Scene` moved to the new `mwg/two-d`, which is where `render`, `ui` and `stage` now live
448
+ as well. `mwg/3d` is its Babylon counterpart, and neither costs a game anything if it is
449
+ not used.
450
+ - **Game logic is renderer-free too.** `mwg/rpg` was the last module reaching a renderer by
451
+ accident; its event interpreter now takes an injected dialogue presenter. So dungeon
452
+ generation, map events, quests, stats, turn order, inventory and battles all run under a 2D
453
+ game, a 3D game, or a headless test, unchanged. `tests/renderer-isolation.test.ts` walks the
454
+ real import graph and enforces exactly which modules may reach PixiJS or Babylon.
455
+ - **One idea, one spelling.** `advance(turns)` is turns and `update(dt)` is real seconds.
456
+ `toJSON`/`fromJSON` is how a class saves. "Nothing to pick" is `null`, not `-1` or
457
+ `undefined`. Two identical hook registries became one.
458
+ - **The character sheet can be saved.** `Inventory`, `StatBlock`, `EquipmentSlots`,
459
+ `Progression`, `SkillPoints` and `Charges` gained serialization, so a game stops hand-rolling
460
+ the most important half of its own save file.
461
+
462
+ The hope, stated plainly: **1.0 should be this shape.** What remains before it is coverage and
463
+ confidence, not another reshuffle. Additions rather than renames from here.
464
+
465
+ ### Added
466
+ - `two-d.ParticleEmitter` - a pooled, seeded particle emitter (sparks, dust, rain). The pool
467
+ is allocated once at `max` and never grows, and every draw goes through `core.Random`, so a
468
+ replayed run reproduces the same spray. Given no `texture` it runs the simulation and draws
469
+ nothing, which is how it is tested without a DOM.
470
+ - `two-d.ScreenEffects` - a full-screen colour wash: `fadeOut`/`fadeIn`/`flash`/`setTint`,
471
+ driven by `update(dt)` returning true on the frame an effect completes.
472
+ - `two-d.Tooltip` - a hover explanation over a themed `Window`, with a frame-driven delay and
473
+ edge-aware placement that flips rather than running off screen.
474
+ - `roguelike.RoomBuilder`/`hallBuilder`/`eligibleBuilders`/`pickBuilder`, and
475
+ `DungeonOptions.builders` - composable room interiors: the generator places and joins rooms,
476
+ a builder carves each one. A room no builder fits falls back to a plain hall.
477
+ - `roguelike.generateDungeonGraph` - the same pipeline as `generateDungeon`, also returning the
478
+ room graph, a rejected-placement retry count, and which builder carved each room.
479
+ `DungeonOptions.hooks` fires `onRoomPlaced`/`onCorridorCarved` as the pipeline runs.
480
+ - `roguelike.FeatureLayer` - a floor feature/event layer: named kinds with
481
+ `inspect`/`interact`/`consequence`/`persistent` rules attached to generated cells.
482
+ - `roguelike.rollRoster` - a variant-spawn/content-roll API: rare additions, per-entry
483
+ alternative swaps, then a shuffle, with every roll traced (including an explicitly deferred
484
+ one) for parity tests.
485
+ - `roguelike.compareDungeonArtifacts`/`checkDeterminism` - a dungeon parity harness that tags
486
+ every mismatch `'graph'` or `'paint'`.
487
+ - **Save/load for the character sheet.** `Inventory`, `StatBlock`, `EquipmentSlots`,
488
+ `Progression`, `SkillPoints` and `Charges` all gain `toJSON`/`fromJSON`. `Inventory` saves
489
+ per-item instance state (level, wear, affix, `instanceId`) while taking kind-level fields
490
+ from the game's own item table on load, so rebalancing reaches old saves. `StatBlock` saves
491
+ base values and deliberately no modifiers, since whatever applied them reapplies them.
492
+ `Charges` saves banked regeneration progress, so reloading cannot shorten a recharge.
493
+ - `core.HookRegistry` - the named-event registry `battle.BattleHooks` and
494
+ `roguelike.CombatHooks` are now both built on.
495
+ - `two-d/ui.messageBoxPresenter` - wires `rpg.EventRunner`'s dialogue to a `MessageBox` in one
496
+ argument.
497
+ - `rpg.MovableSprite`, `rpg.AutomapTarget`, `rpg.DialoguePresenter` - the structural shapes
498
+ that replaced concrete renderer types in `mwg/rpg`'s signatures.
499
+ - `core.Input` gains touch input: `touchCode`/`bindTouch`/`pressTouch`/`releaseTouch` follow
500
+ the same synthetic-code pattern `bindButton`/`pollGamepads` already use for a gamepad, so
501
+ `isDown`/`justPressed`/`justReleased` work unmodified for a touch-driven action. `attachSwipe`
502
+ resolves a one-finger drag into one of 8 directional action pulses by angle, or a `tap`
503
+ action for a short drag. `core.PlayerInput` gains matching `bindTouch`/`pressTouch`/
504
+ `releaseTouch` wrappers.
505
+ - `ui.Button` gains `onPress`/`onRelease` signals (pointerdown, and pointerup/pointerupoutside),
506
+ the hook a virtual on-screen d-pad button needs to feed `pressTouch`/`releaseTouch` for
507
+ hold-to-repeat movement, independent of `onClick`.
508
+ - `mwg/3d` gains `Collision3D.ts` (`buildHeightIndex`, `cellAt`, `heightAt`,
509
+ `resolveCapsuleAgainstGrid`): horizontal collision against `createTileGrid3D`'s stepped
510
+ grid, stopping a moving capsule/AABB at a raised column's edge instead of clipping through
511
+ it. `Character3D` takes an optional third constructor argument, `collideXZ`, to plug this
512
+ in; omitted, movement behaves exactly as before.
513
+ - `assets.fetchWithByteProgress` reports real cumulative bytes transferred (and the known
514
+ total, from `Content-Length`) for a real origin - a dev server, or a desktop host serving
515
+ through a virtual origin rather than `file://`. `desktop/MwgDesktopHost` now serves the
516
+ built game through `CoreWebView2.SetVirtualHostNameToFolderMapping` (a real `https://`
517
+ origin) instead of a plain `file://` navigation, which is what makes this - and any other
518
+ real network call - actually usable inside it.
519
+
520
+ ### Changed
521
+
522
+ - **Breaking: `mwg/render`, `mwg/ui` and `mwg/stage` moved under `mwg/two-d`.** The Pixi half
523
+ of the framework is now one module, symmetric with `mwg/3d`. Import from
524
+ `@datamoc/mw_games/two-d` (or the granular `two-d/render`, `two-d/ui`, `two-d/stage`). No
525
+ legacy aliases were kept.
526
+ - **Breaking: `Game` moved from `mwg/core` to `mwg/two-d`, and `Scene` split.** `core.Scene` is
527
+ now the renderer-free lifecycle (`create`/`update`/`resize`/`onSuspend`/`onResume`/`destroy`);
528
+ `two-d.Scene2D` adds the Pixi container and is what a 2D game extends. `SceneStack` is generic
529
+ over the scene type, so a Babylon game gets scene management, suspend/resume and minigame
530
+ stacking. **`mwg/core` now pulls in no renderer at all**, and neither does `mwg/rpg`.
531
+ - **Breaking: `rpg.EventRunner` takes `present: DialoguePresenter` instead of `windows`.** The
532
+ event interpreter no longer builds a `MessageBox`; use
533
+ `present: messageBoxPresenter(this.windows)` for the previous behaviour. `boxWidth`,
534
+ `boxHeight` and `speed` moved onto the presenter.
535
+ - **Breaking: `loadTiledMap` moved from `mwg/rpg` to `mwg/two-d/render`**, where the `TileMap`
536
+ and `SpriteSheet` it builds already lived.
537
+ - **Breaking: every "pick one" now returns `null`.** `Random.weighted` returned `-1` and
538
+ `Random.element`/`Random.weightedKey` returned `undefined`; all three now match
539
+ `rollEncounter`/`rollLoot`.
540
+ - **Breaking: one meaning per name.** `advance(turns)` is turns or rounds and `update(dt)` is
541
+ real seconds, so `EnvironmentClock.advance(seconds)` became `update(dt)`,
542
+ `Barrier.decay(ticks)` and `AbilityCycle.tick()` became `advance(turns)`,
543
+ `BossPhases.update(hpFraction)` became `check(hpFraction)`, and `QuestLog.advance` became
544
+ `advanceStage`. `EnvironmentClock.snapshot`/`restore` and `SupportLedger.save`/`restore`
545
+ became `toJSON`/`fromJSON`.
546
+ - **Breaking: `GridMover`/`FreeMover` take a `MovableSprite`** rather than an `AnimatedSprite`,
547
+ so a static sprite (or anything with `x`/`y`) works; the animation members are optional.
548
+ - `mwg/assets` split into renderer-free path resolution (`assets/paths`, its own export
549
+ subpath) and the Pixi-backed loader, which is what took Pixi out of `mwg/3d` and `mwg/audio`.
550
+ - Per-module export subpaths now ship TypeScript types; previously only the package root did,
551
+ so `@datamoc/mw_games/core` resolved to `any` under `moduleResolution: bundler`/`node16`.
552
+ - `sideEffects` narrowed from a blanket `false` to the two `mwg/3d` files with real
553
+ side-effect imports, so a bundler can no longer drop Babylon's glTF and thin-instance
554
+ registrations.
555
+
556
+ ### Fixed
557
+ - `examples/three-d`'s hero now actually collides with the raised ridge its demo path crosses,
558
+ rather than the path being hand-routed around it.
559
+
560
+ ## [0.3.1] - 2026-09-05
561
+
562
+ ### Added
563
+ - `audio.Music.playTracks(paths, fadeDuration)` plays a playlist rather than one looping
564
+ track: each track plays once, `HTMLAudioElement.onended` (when the backend exposes it)
565
+ advances to the next, and the sequence cycles once it reaches the end. `play()`/`stop()`
566
+ cancel an active playlist so a stale `onended` from a track already replaced cannot advance
567
+ a newer one.
568
+
569
+ ## [0.3.0] - 2026-09-05
570
+
571
+ ### Added
572
+ - `core.PlayerStats` (a lifetime total, folded per run - deliberately not derived from
573
+ `RunHistory`, which can drop old entries), `ui.StatsScreen` (a `label: value` display
574
+ recipe), and `core.TelemetryClient` (automatic structured events, off by default until a
575
+ game calls `setConsent(true)`).
576
+ - `tools/benchmark-browser.mjs` now records each run's fps/p95/memory to a per-page history
577
+ file (`benchmark-results/`, gitignored) and fails on a regression against the best-seen
578
+ prior run, not only its fixed gate. `npm run benchmark:tower-defense`/`benchmark:ui` cover
579
+ those examples the same way `benchmark:browser`/`benchmark:3d` already did.
580
+ `tools/benchmark-simulation.mjs` (`npm run benchmark:simulation`) measures headless
581
+ `runScenario`/`advanceToInput` throughput, with the same history tracking.
582
+ - `core.LockstepClient` and a reference server, `tools/multiplayer-server.mjs` (real-time
583
+ multiplayer's one exception to "`mwg` ships no backend"): lockstep tick authority over a
584
+ plain `WebSocket`, server-assigned identity, and a `?room=` matchmaking string. Run it with
585
+ `npm run multiplayer:server`.
586
+ - `core.SaveSyncClient.list()` returns every save-profile slot name an endpoint holds, so a
587
+ player's multiple named profiles (`SaveSystem` already supports as many as a game creates)
588
+ can each sync independently against one endpoint.
589
+ - `desktop/MwgDesktopHost`, a minimal WebView2 (WinForms, net8.0-windows) reference host that
590
+ loads a built example's own `index.html` via `file://` unmodified. `npm run desktop:build`/
591
+ `desktop:run` mirror the existing `cap:*` scripts' naming and target the same `tower-defense`
592
+ reference build. Verified with a real `dotnet build`/`dotnet run` and a screenshot of the
593
+ running window showing the game actually rendered.
594
+ - `assets.load` and `assets.AssetStream`'s `preload`/`preloadLikely` take an optional
595
+ `onProgress` (asset-count fraction, not literal bytes), threaded through `examples/loading`'s
596
+ own tasks into `LoadQueue.report`. Eviction under a tight budget was verified in a real
597
+ browser across 300 preload/evict cycles: resident texture count stayed pinned to the budget
598
+ throughout.
599
+ - `render.StatusVisuals` maps active status-effect names to a tint on a sprite (priority by
600
+ declaration order, an optional pulsing strength), bridging `actors.applyStatusEffect`'s
601
+ game-side data to what a character looks like without either module importing the other.
602
+ - `mwg/simulation` (`Simulation` on the standalone build): `advanceToInput` runs a
603
+ scheduler's automatic actions up to a budget until an actor needs input, and
604
+ `runScenario` applies a finite command sequence against caller-supplied rules. Both
605
+ are headless, with no rendering or browser imports, and take random state explicitly
606
+ for reproducibility.
607
+ - Button accepts a per-instance nine-patch skin with state tints and caption styling, including captions created later with setText.
608
+ - Label accepts stroke, resolution and roundPixels options. Existing defaults are unchanged. Textures remain owned by the caller.
609
+
610
+ ### Changed
611
+ - **Breaking:** `core.SaveSyncClient.upload`/`download` now take a `slot` name -
612
+ `upload(payload)`/`download()` are `upload(slot, payload)`/`download(slot)` - so one
613
+ endpoint can hold more than one player profile.
614
+
615
+
616
+ ## [0.2.1] - 2026-09-05
617
+
618
+ ### Fixed
619
+ - `Sound.play()`, `Music.play()` and `Synth.playTone()` discarded the `Promise` that
620
+ `HTMLAudioElement.play()` returns with no rejection handler. Reusing a pooled `Sound`
621
+ instance before its prior `play()` settled, or `Music`'s crossfade pausing the previous
622
+ track while the incoming one was still loading, rejects that promise ("The fetching
623
+ process for the media resource was aborted..."), which surfaced as an unhandled
624
+ rejection instead of being silently absorbed, the correct handling for an interruption
625
+ that is expected, not an error.
626
+ - `Camera.update()` computed its deadzone size through the `view` getter, which also
627
+ clamps to bounds and allocates a rectangle object - two needless allocations every
628
+ frame for values the deadzone calculation never used.
629
+
630
+ ## [0.2.0] - 2026-09-05
631
+
632
+ ### Added
633
+
634
+ **Core**
635
+ - `Tweener`/`Easing`, `UndoHistory`, `PlayerInput` (rebindable action-to-key mapping),
636
+ `RunHistory` (per-run reports and rankings, no online component), `News` (`NewsClient`,
637
+ `NewsSeenTracker`), `Sanitize` (`checkSize`, `checkNoControlCharacters`,
638
+ `sanitizeInboundText`, `validateSchema` - a 10MB default size cap and no embedded control
639
+ characters on any inbound save/news/sync data), `Scramble`, `SaveSync` (`SaveSyncClient`),
640
+ `Save.exportSlot`/`importSlot`, `Loading` (`LoadQueue`), `Feedback` (`FeedbackClient`),
641
+ `Input.onWheel` (wheel scroll as a first-class input signal alongside keys, not just a
642
+ `ListView` feature - horizontal-scroll and zoom modifier keys resolve to a
643
+ `WheelAction`/`WheelInput` the game interprets, distinct from the browser's own scroll/zoom).
644
+ - `version`: `mwg`'s own version string, exported from `mwg/core` and kept in sync with
645
+ `package.json` by a dedicated test.
646
+
647
+ **Render**
648
+ - `Viewport`/`splitScreenHalves` (local split-screen multiplayer), `Camera.setViewport`
649
+ screen offset, `ColorBlindness` (`createColorBlindnessFilter`, accessible colour modes),
650
+ `Capabilities.inspectGraphicsCapabilities`/`detectWebGpu` (real WebGPU shader-compile
651
+ probe, not just a `navigator.gpu` existence check), `Streaming.AssetStream`.
652
+
653
+ **UI**
654
+ - `BitmapLabel`, `Toast`, `theme.highContrastTheme`, `LoadingScreen`, pointer and wheel
655
+ support on `ListView`/`IconGrid`, `Bar` fill/background textures with pixel rounding.
656
+
657
+ **Audio**
658
+ - `Synth` (`synthesizeTone`, `playTone`), `Midi` (`parseMidi`, `scheduleMidi`,
659
+ `MidiPlayer`, `noteToFrequency`), `Captions` (`onCaption` - subtitles for accessibility).
660
+
661
+ **Actors / World / Board / RPG**
662
+ - `Support` (`SupportLedger`), `ItemStatusEffect.applyItemStatusEffect` (temporary item
663
+ status), `Environment` (`EnvironmentClock`), `FogOfWar.FactionFog`, `Collision`
664
+ (`aabbOverlap`, `circleOverlap`, `circleAabbOverlap`, `resolveAabbAgainstTiles`).
665
+
666
+ **i18n / Stage**
667
+ - `Fluent.parseFTL`, typographic apostrophes, `StageScript.history`/`showLast`/`skipSeen`,
668
+ `MessageBox` `nvl` mode and `autoAdvance`.
669
+
670
+ **3D, mobile and desktop**
671
+ - `Character3D.playAnimation`/`stopAnimation`/`currentAnimation`,
672
+ `Heightmap.createHeightmapTerrain3D`, an Ionic Capacitor integration for native
673
+ iOS/Android packaging, and a WebView2 desktop host reference under `desktop/`.
674
+
675
+ **Examples**
676
+ - `examples/loading` (`LoadQueue`/`LoadingScreen`/`AssetStream` end to end, including a
677
+ deliberately-failing-once task exercising retry), `examples/three-d`,
678
+ `examples/tower-defense`.
679
+
680
+ ### Changed
681
+ - `graphics-capabilities`'s `wgsl` capability now defaults to `false` unless a real
682
+ WebGPU shader-compile probe supplies it, instead of asserting true from
683
+ `navigator.gpu`'s mere existence.
684
+ - `tools/find-chrome.mjs` extracted from the byte-identical `findChrome()` duplicated in
685
+ `benchmark-browser.mjs` and `graphics-capabilities.mjs`.
686
+
687
+ ## [0.1.2] - 2026-09-03
688
+
689
+ ### Added
690
+ - `Session`: counts how many times a game has launched, persisted across page loads (the
691
+ same `SaveStorage` abstraction `SaveSystem` uses) - the signal a native wrapper needs to
692
+ decide whether to ask for a store rating. `mwg` counts; it never prompts, and bundles no
693
+ store/ads/IAP SDK of any kind.
694
+
695
+ ### Changed
696
+ - **Breaking:** `Game` no longer registers `mwg/render`'s colour-transform batcher
697
+ automatically. `mwg/core` imported from `mwg/render` to do this, meaning a game that
698
+ only imported `mwg/core` still compiled in the whole render module. `GameOptions` gained
699
+ `extensions`, a list of Pixi-extension registration functions the caller supplies
700
+ instead: a game using `TintedSprite` (directly, or via `TileMap`/`DialogueStage`/
701
+ `AnimatedSprite`) now passes `{ extensions: [registerColorTransform] }` explicitly.
702
+ `mwg/core` now imports from no other module.
703
+
704
+ ## [0.1.1] - 2026-09-03
705
+
706
+ First tagged release. Everything below shipped on `main` before this tag existed; nothing
707
+ in it is new as of the tag, only now given a version number to refer to.
708
+
709
+ ### Added
710
+
711
+ **Core**
712
+ - `Game` (owns the Pixi `Application`, the frame loop, current `Scene`), `Scene`, `Signal`
713
+ (typed event emitter, stack-mode listener order), seeded `Random` (save/restorable state,
714
+ `withSeed` for scoped determinism), keyboard `Input` with rebinding.
715
+ - `Achievements`: named milestones unlocked by counters crossing a target - unlocking is
716
+ derived, never stored; the increment that earns one reports it, `drainNew()` queues
717
+ announcements, and loaded counts announce nothing.
718
+ - `Game.step(dt)`, to drive a frame by hand - needed because Chrome throttles
719
+ `requestAnimationFrame` in a background tab.
720
+ - `SaveSystem`: named, versioned save slots over `localStorage`, with an in-memory fallback.
721
+ - `Recorder`/`Player`: action recording and replay for testing - every `Input.onAction`
722
+ stamped against the `Game.onFrame` count, re-dispatched at the same counts while the loop
723
+ is driven by hand with `Game.step(dt)`; `serializeReplay`/`deserializeReplay` round-trip
724
+ the log with validation.
725
+ - `Collection`: named record collections over `localStorage` (quest logs, bestiaries,
726
+ achievements) - `all`/`get`/`put`/`remove`/`where`/`clear` reading storage directly, so
727
+ there is no cached copy to go stale; `SaveSystem`'s memory fallback is now shared.
728
+ - `SceneStack` with `Game.pushScene`/`popScene`: a scene suspends underneath another and
729
+ resumes with a result via `Scene.onSuspend`/`onResume` - only the top scene updates, all
730
+ render, so overlays and opaque scenes both work.
731
+ - `Logger`: categories and severity over bare `console.log` - four levels, a filter, and a
732
+ sink tests capture instead of the console.
733
+ - `Hex`: cube-coordinate hex grid math (`hexNeighbors`, `hexDistance`, `hexLine`, `hexRange`,
734
+ `hexToPixel`/`pixelToHex`), orientation-agnostic and shared by `roguelike` and `render`.
735
+
736
+ **Render**
737
+ - `Camera`, `TileMap` (chunked and culled against the camera; `square`, `hex`, `isometric`, and
738
+ `staggered` projections, all culled through one shape-agnostic bounding-box method), `SpriteSheet`, `AnimatedSprite`,
739
+ `TintedSprite`, and `ColorTransformBatcher` - a per-sprite multiply-and-add colour transform
740
+ in the batch shader, which Pixi's built-in tint cannot do.
741
+ - `LayeredSprite`, for characters built from swappable parts (skin, hair, garment).
742
+ - `ActorAnimator`: the idle/move/action animation-state convention `AnimatedSprite` and
743
+ `GridMover` never had on their own, with the one rule that makes them coherent - an action
744
+ interrupts idle/move, but is not itself interruptible while it plays.
745
+ - `Projectile`: tweens a sprite's position in a straight line, the render-side half of aiming
746
+ a thrown item or a wand bolt.
747
+ - `blobIndex`/`autotileFrames`/`BLOB_SHAPES`: the classic 256-combination-to-47-shape blob-tile
748
+ reduction for auto-tiling a terrain edge or corner from many small pieces.
749
+ - Verified: SVG textures load correctly through the compiled `data:` URI path; the render path
750
+ holds 60fps at 4000 tinted sprites and over a 160,000-cell synthetic map, with no silent
751
+ fallback off WebGL.
752
+ - `TileMap` reads one sheet per tileset (`tileFrame` packs naming the sheet; plain indices
753
+ still read as sheet 0), and cells carry an elevation: `setCellHeight` lifts the top tile
754
+ by `heightStep` per level, growing two shaded side faces per level on the diamond
755
+ projections, with `tileCenter` riding along so occupants stand on top.
756
+
757
+ **Assets**
758
+ - `load`/`texture`/`get`/`resolve`, synchronous after one `load()` call per scene.
759
+ - `tools/compile-resources.mjs`: turns every asset into a `data:` URI script, so a compiled
760
+ build never needs `fetch`/XHR (both blocked from `file://`).
761
+
762
+ **UI**
763
+ - `Window`, `WindowStack` (keyboard focus to the top window only), `ListView`, `MessageBox`,
764
+ `Label`, `NinePatch`, `VerticalLabel` (for vertical writing systems), `theme`.
765
+ - `IconGrid`: a multi-column icon grid alongside `ListView`, for SPD-sized item counts. Tap-
766
+ then-tap reordering and a frame-driven long-press-to-quickslot timer, both plain methods a
767
+ test can drive without simulating pointer events.
768
+
769
+ **i18n**
770
+ - Message tables, `Intl.PluralRules`-backed plurals, `{token}` interpolation, and
771
+ `direction()` (ltr/rtl), which `ui`'s `Theme.direction` reads from a game's own glue code.
772
+
773
+ **Actors**
774
+ - `StatBlock` (base + derived stats through a fixed add→multiply→set modifier order),
775
+ `EquipmentSlots` (ties an item's modifiers to a stat block on equip/unequip),
776
+ `Progression`/`powerCurve` (levels and experience over a replaceable growth curve),
777
+ `Inventory` (stacking, weight, containers), `skillCheck`.
778
+ - `SkillPoints`: a per-stat spendable ledger over a `StatBlock`, with a rising cost-per-rank
779
+ callback and an optional cap - deliberately not a wrapper around `Progression`, just bridged
780
+ to it by the points a level-up grants.
781
+ - `craft(inventory, recipe)`: consumes every ingredient and adds the result atomically - an
782
+ ingredient shortfall or a result that doesn't fit rolls back everything already removed.
783
+ - `applyStatusEffect`: a temporary buff/debuff - applies `StatBlock` modifiers and registers
784
+ their removal against any clock shaped like `TurnClock`'s `add`/`remove`, returning a handle
785
+ whose `cancel()` removes them early (a cure, a remove-curse scroll).
786
+ - `identify`/`enchant`/`damageItem`/`repairItem`: item-depth functions over a plain
787
+ `InventoryItem`, which gained `level`, `durability` and `maxDurability` fields - durability
788
+ is opt-in per item, a no-op without `maxDurability` set.
789
+ - `Charges`: a resource that regenerates on its own as turns pass (a wand's limited charges),
790
+ distinct from a `StatBlock` stat spent per use and restored only by an explicit event.
791
+ - `rollLoot`: a weighted drop table, the same shape as `world.rollEncounter` - roll whether
792
+ anything drops, then weight-pick which.
793
+ - `buy`/`sell`: a shop transaction between two `Inventory`s paid from a `StatBlock` currency
794
+ stat, all-or-nothing like `craft()` - a full stock/buyer-capacity rollback on failure.
795
+ - `Advancement`: tiered specialization (level-gated point tiers, one permanent branch choice,
796
+ one capstone) with a point ledger - never the spend rule, which stays game-side.
797
+ - `rollAffix`/`applyAffix`/`removeAffix`/`affixOf`: named item affixes carrying only trigger,
798
+ weight and curse flag; `InventoryItem` gained an optional `affix` field.
799
+ - `scaledModifiers`: `{stat, op, base, perLevel}` templates resolved to plain `Modifier`s at
800
+ a given item level.
801
+ - `Appearances`: per-run seeded shuffling of which look each unidentified kind wears, with
802
+ `toJSON`/`fromJSON`.
803
+ - `canAfford`/`spend`: a spendable per-use resource (mana, stamina) over a `StatBlock`
804
+ pool - one cost or several, all-or-nothing `craft()`-shaped, with a negative cost
805
+ refused as an authoring error rather than a refund.
806
+
807
+ **Roguelike**
808
+ - `FieldOfView` (visible/explored/remembered), `Pathfinder` (A*, a Dijkstra distance map,
809
+ `autoExplore`), `Scheduler` (energy-cost turn order), `generateDungeon` (seeded, with an
810
+ optional `kinds` list for a game's own terrain ids alongside wall/floor).
811
+ - `decideMonsterAI`: a wander/hunt/flee behaviour loop over a monster's own `FieldOfView`
812
+ (not the player's), built entirely from already-shipped primitives.
813
+ - `Secrets`: discoverable/hidden tile state (secret doors, undiscovered traps). A concealed
814
+ cell is disguised straight in `Level`'s own terrain, so `FieldOfView`/`Pathfinder` need no
815
+ changes to support it.
816
+ - `chebyshevDistance`/`traceLine`/`hasLineOfSight`/`canTarget`/`resolveArea`: targeting via a
817
+ Bresenham line trace (a different, cheaper question than `FieldOfView`'s shadowcast), with
818
+ `single`/`line`/`burst` area resolution.
819
+ - `decideMonsterAI` gained a `disposition` option (`hostile`/`neutral`/`peaceful`, defaulting
820
+ to `hostile`, unchanged) and a `provoked` flag - a peaceful/neutral monster always wanders
821
+ until either overrides it back to hunting.
822
+ - `Doors`: open/closed/locked door state, `Secrets`' own shape reused - the state is just
823
+ terrain, swapped between a door's own open/closed kinds; locking is a separate flag that
824
+ keeps a door closed and unopenable until `unlock` is called.
825
+ - `BossPhases`/`AbilityCycle`: HP-fraction thresholds reporting newly entered phases in
826
+ order, plus named ability cooldowns with `tick`/`ready`/`use` - both JSON round-trippable.
827
+ - `Blob`: per-cell volumes diffused into passable neighbours and decayed per step, with
828
+ `cellsAbove` for the game to apply its own effects on.
829
+ - `Level`, `Secrets` and `Doors` gained `toJSON`/`fromJSON` - the game's own `kinds` table
830
+ (or live level) is supplied fresh on load, `QuestLog`'s own convention.
831
+ - `Elevation`: a discrete height per cell, in the `Secrets`/`Doors` sidecar shape.
832
+ `FieldOfView.update` takes it for asymmetric cliff sight (a cell blocks exactly when
833
+ above viewer and target alike), and `Pathfinder` takes it with a climb limit (ascent
834
+ capped, descent free).
835
+
836
+ **World**
837
+ - `World` (many maps, each created once and kept alive, with an explicit non-persistent mode
838
+ for SPD-shaped floors that rebuild fresh every visit), `Overworld`, `TurnClock`,
839
+ `rollEncounter`.
840
+
841
+ **RPG**
842
+ - `loadTiledMap` (orthogonal, single embedded tileset, uncompressed CSV layers; isometric and
843
+ staggered orientations too, each mapped to the matching `TileMap` shape), `GameState`
844
+ (switches/variables), `MapEvent`/`activePage` (last matching page wins), `EventRunner`,
845
+ `GridMover` (tweened tile movement, a walk cycle, and `turnTo` for facing without moving).
846
+ - `QuestLog`: staged quest/mission definitions (`canStart`/`start`/`advance`/`status`), with
847
+ `toJSON`/`fromJSON` for `SaveSystem`.
848
+ - `loadTiledMap` reads any number of tilesets - embedded or external (fetching an external
849
+ `.tsx`/JSON stays the caller's asset loading) - resolving each gid by Tiled's own
850
+ greatest-firstgid-at-or-below rule.
851
+ - `automap`: Tiled-style automapping rules - `input` patterns matched anywhere (`EMPTY`
852
+ constrains nothing), one `output` variant written per match (`EMPTY` leaves the cell
853
+ alone), rules applied in order with each rule's matches collected before any write.
854
+
855
+ **Stage**
856
+ - `DialogueStage` (backdrop, characters standing in front of it, expressions, speaker focus)
857
+ and `StageScript`, a small command interpreter run as one awaited call.
858
+ - `StageScript.runStory`: a story as a graph of named passages - `{goto}` commands and
859
+ `StageChoice` jumps between them, falling off a passage ending the story.
860
+ - `importTwee`: Twine's Twee notation (plain text, no DOM needed) into a `StoryScript`:
861
+ text lines become `say`, `[[links]]` in all three forms become one closing `ask`, and
862
+ dangling links, doubled passages and setter links are refused rather than half-read.
863
+
864
+ **Battle**
865
+ - `Creature` (wraps `actors.StatBlock`/`Progression`), `TypeMatrix`, `Party`, `battleOrder`,
866
+ `checkEvolution` (last-match-wins, so a multi-stage chain reaches its final form).
867
+ - `StatStages`: a bounded, symmetric stage ladder over a `StatBlock` stat (Swords Dance's own
868
+ shape) - replaces rather than stacks a stat's current modifier, and `resetAll()` clears
869
+ every stage in one call for the switch-out rule.
870
+ - `chooseMove`/`chooseSwitch`: battle AI built on `TypeMatrix.multiplierFor` - a move scored
871
+ by type effectiveness (or a custom `score`), and a switch suggested only when the bench has
872
+ a genuinely better defensive matchup than staying in.
873
+ - `BattleHooks`: a battle-scoped event/hook system for passive abilities and held items,
874
+ keyed by a plain event-name string a game's own battle loop defines; `offSource` removes
875
+ every hook tied to one source (an ability leaving the field on faint).
876
+ - `Field`: named, optionally-timed battle-wide conditions (weather, terrain, a screen) a game
877
+ reads directly (`field.has('rain')`) - distinct from any one creature's own `StatBlock`.
878
+
879
+ **Board**
880
+ - `mwg/board`: a generic `BoardGrid` (owned pieces on a cell grid, move/capture) plus four
881
+ traditional games built on it - chess (`startingChess`/`legalMoves`/`applyMove`, FEN
882
+ parsing, check/mate/stalemate/draw detection, and a material-evaluation alpha-beta
883
+ `chooseMove`/`search` engine), checkers (forced captures, multi-jump chains, king
884
+ promotion), go (placement, capture-by-surrounding, ko, area scoring after two passes),
885
+ and backgammon (points, bar/off, dice, hitting a blot).
886
+ - `startingTactics`/`tacticalMoves`/`tacticalAttack`/`setTacticalOverwatch`: a small
887
+ grid-tactics layer (move/shoot/overwatch, one action budget per unit per turn) for an
888
+ XCOM-shaped turn.
889
+ - `createDeck`/`shuffleDeck`/`deal`: a standard 52-card deck (with optional jokers), shuffled
890
+ and dealt into equal hands; `trickWinner` resolves one trick - highest trump, or highest of
891
+ the lead suit with no trump played - leaving bidding and stakes to the game.
892
+ - `dealSolitaire`/`drawSolitaire`/`moveSolitaireTableau`/`moveSolitaireToFoundation`/
893
+ `solitaireWon`: a seeded Klondike deal and its four moves - stock/waste draw with
894
+ waste-recycling, alternating-colour descending tableau moves, ace-up same-suit foundation
895
+ moves, and the all-four-full win check.
896
+ - `rollDice`/`rollExpression`: `NdM` and `NdM±K` dice notation over `Random`. `DiceCup`: a
897
+ kept-die reroll cup (`keep`/`reRoll`/`clearKept`). `scoreDice`: all thirteen standard
898
+ Yahtzee-shaped categories over a five-die hand.
899
+
900
+ **Roguelike**
901
+ - `CombatHooks`: named-event listeners for a grid actor's combat lifecycle
902
+ (`beforeAttack`/`beforeDamage`/`afterDamage`/`onKill`, or any game-defined event string),
903
+ optionally tagged by source for bulk removal; `modifyDamage` runs the `beforeDamage` seam
904
+ and clamps/reports whether the hit was fully prevented. HP, formulas and when each event
905
+ fires stay game-side.
906
+
907
+ **Audio**
908
+ - `Sound` (pooled, round-robin) and `Music` (crossfade via `update(dt)`), both taking an
909
+ injectable `create()` in place of `new Audio()` for testing.
910
+
911
+ **Examples**
912
+ - `colour-transform`, `interface`, `dialogue`, `village` (an NPC with switch-selected
913
+ conversation pages and an autorun cutscene), `battle`, `dungeon` - an SPD-shaped
914
+ mockup wiring together most of the above: generated floors, three-state fog of war,
915
+ bump-to-attack, wander/hunt/flee monsters, secret doors and hidden traps, a thrown flask
916
+ of oil, a dense icon-grid inventory, autosave-on-descend with permadeath - and `chess`, a
917
+ playable board against `mwg/board`'s search engine, moved by click-to-select-then-move or
918
+ a held/repeating arrow-key cursor.
919
+
920
+ **Project**
921
+ - A project website with a live colour-transform demo, the capability-spec table, and the
922
+ roadmap, deployed to GitHub Pages.
923
+ - TypeDoc-generated API documentation, built through an isolated toolchain (TypeDoc needs
924
+ TypeScript's classic compiler API, which TS7 no longer exposes through its main package).
925
+
926
+ ### Changed
927
+ - `generateDungeon` gained an optional `kinds` list, so a game can add its own terrain ids
928
+ (a trap kind, say) alongside the generator's wall/floor without forking it.
929
+ - `Targeting`'s `AreaShape` gained `cone`, and `coneCells`/`chainTargets`/`knockbackPath`
930
+ cover sprays, arcs and shoves alongside the existing `single`/`line`/`burst` resolution.
931
+ - `EquipmentSlots` gained an optional `locked` predicate - a locked slot reports `isLocked()`
932
+ and refuses both `unequip` and swaps; without it every slot behaves exactly as before.
933
+ - `examples/dungeon`'s inventory screen is an `IconGrid` now, not a `ListView`.
934
+ - `TurnClock`'s `TimedEffect` gained an optional `onExpire` callback, fired once right before
935
+ an effect whose duration has run out is removed - what `applyStatusEffect` needed to tie a
936
+ status effect's expiry to removing the `StatBlock` modifiers it applied.
937
+ - `Game`'s scenes run as a stack: `switchScene` replaces everything, `pushScene`/`popScene`
938
+ suspend and resume; only the top scene updates, all render, and resize reaches the whole
939
+ stack.
940
+ - `Pathfinder`'s search loops check the step (both ends on the map, climb within limit)
941
+ rather than the destination cell alone.
942
+
943
+ ### Fixed
944
+ - `checkEvolution` was first-match-wins; a multi-stage evolution chain could get stuck on an
945
+ earlier form instead of reaching its final one. Now last-match-wins, matching
946
+ `Event.activePage`'s own convention.
947
+ - `GridMover` never updated `facing` on a *blocked* move, so approaching an NPC from certain
948
+ angles left a player unable to turn to face it. Added `turnTo`.
949
+ - A vault's floor and treasure (behind a secret door) were rendering through the "solid" wall
950
+ before the door was ever found, because the vault cell was carved eagerly instead of
951
+ staying genuinely undiscovered rock until the door opened.
952
+ - Sprites looked blurry under `pixelArt: true` on a hidpi display, even though nearest-neighbour
953
+ texture sampling was already on. The browser does a second, separate resize compositing the
954
+ canvas element onto the page (its backing buffer is rarely the same size as its CSS display
955
+ size), and that step defaults to smoothing regardless of anything Pixi does. Fixed by adding
956
+ a `.mwg-pixel-art { image-rendering: pixelated }` class to the canvas - a class survives
957
+ `autoDensity`/`resizeTo` rewriting the canvas's `style` attribute wholesale, where an inline
958
+ style did not.
959
+ - Off-map neighbours could alias real cells through `Level.index` (which does not
960
+ bounds-check), writing distances into the wrong cell once a search looked at the
961
+ step's origin rather than only its destination. Steps now refuse either end off
962
+ the map before any index is touched.
963
+
964
+