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