Changelog
[0.12.3] — 2026-06-12
Changed
- The app's WASM is now content-hashed, so a deploy can't serve a stale or broken mix. The loader and binary used to live at fixed filenames refreshed by revalidation, which occasionally handed a browser a new loader with an old binary (or vice versa) after a release — the "Failed to load" screen. Each build now emits uniquely-named files plus a tiny
manifest.jsonthe app reads to find them; the named files are cached permanently and only the manifest is ever re-checked, so every load gets a matching, current pair. The "Clear Cache & Reload" button is correspondingly simpler. - One build pipeline for the WASM. The build/optimize/verify steps, previously duplicated (and drifting) across CI jobs, are now single
justrecipes, and CI builds the WASM once and shares it between the smoke-test and deploy jobs instead of compiling it twice.
[0.12.2] — 2026-06-12
Fixed
- Weapon stats on the datacard were blank. A unit's weapons listed their range but showed
A:- BS:- S:- AP:- D:-for everything else, both on the datacard and in the shooting tool's weapon table. Same cause as the unit stat row a release earlier: the weapon profile stores its stats under uppercase keys (A/BS/WS/S/AP/D) while the code read lowercase, and the open index signature on the stats object hid the mistake from the type-checker. Range was the only field that worked because it uses the correct key. The Datacard story now renders a real loadout and asserts the weapon stats show. - "Clear Cache & Reload" didn't reliably refresh the app. The WASM loader and binary are served from fixed URLs marked
no-cache, and there's no service worker — so the button's cache-clearing did nothing and an ordinary reload only revalidated the files, letting a stale or mismatched pair from a previous deploy stick around. The button now unregisters service workers, clears the Cache Storage API, and forces the next load to re-fetch both the loader and the binary from scratch (the binary is requested explicitly, since the loader otherwise drops the cache-busting marker when locating it).
[0.12.1] — 2026-06-12
Fixed
- The datacard showed a dash for every unit stat. Movement, Toughness, Save, Wounds, Leadership, and Objective Control all rendered as
-, whether you opened a unit's card in-game, previewed it while building an army, or imported a list from ListForge. The card read the stat profile with lowercase keys (m,t,sv, …) while the dataset stores them uppercase (M,T,Sv, …); the profile type's open index signature let the wrong keys slip past the type-checker, so each stat came back empty and fell through to a dash. The one column that worked — the invuln save — was the only one already using the right key. Weapons, abilities, and keywords were never affected. A new Datacard story renders the real component against the embedded dataset and asserts the stats and abilities show.
[0.12.0] — 2026-06-10
Added
Edit a tournament pack's army list in place — your deployment plans follow. A pack used to freeze its army at creation: changing the list meant rebuilding the pack and losing every saved deployment. Now an "Edit list" button on the pack opens the same text/builder editor (it reuses the army-library editor wholesale), and saving writes the edited list back into the pack. Saved placements re-key to the new units by identity — a renamed-but-same-datasheet unit keeps its spots, while placements for a removed unit or models past a shrunk squad's new size are dropped with a toast naming them. The pack format is unchanged. (pending commit)
The list builder, rebuilt to feel like a real list builder. It's now a three-column workspace: the unit picker on the left groups units by battlefield role (Characters, Battleline, Dedicated Transports, …) with Infantry/Vehicle/… type filters and search; the center shows your list grouped the same way with per-section points subtotals; and a dedicated panel on the right edits the selected unit — model count, wargear, enhancement, and warlord, with the unit's live datacard below. Duplicate a configured unit in one click. Roster rows show the unit's equipped weapons at a glance, and un-roled units split into Infantry / Beast / Vehicle / … sections instead of one "Other" lump. (pending commit)
Edit wargear in the text army editor too, not just the builder. Units imported by pasting a list now get the same per-model wargear editor (a "Wargear" twist-down on each unit card in the library editor). Your loadout edits are saved with the list and survive re-importing an edited version — matched by unit identity, like nicknames and assignments — and flow through to the datacard and shoot tool on the board. (pending commit)
Changed
- A list's Force Disposition is now its locked spine, not a free dropdown. In the builder, disposition is presented as the list-level choice that fixes which missions you'll play; it auto-derives from the chosen detachment once that mapping lands in the data, with a manual pick as the fallback until then. (pending commit)
- Switching faction in the builder no longer wipes your list. Only the detachment (and detachment-scoped enhancements) reset; your units stay, so you can field allied / agent units from another faction. (pending commit)
- Bumped the
wh40kdcpath dependency 0.5.6 → 0.5.7, which lands the canonical roster-json import adapter on the dataset's main line (the builder's save→re-import round-trip depends on it) and restores the missing Beacon secondary card — the 11e secondary deck is now 18 cards (deck-size assertions updated). (pending commit) - Bumped the
wh40kdcpath dependency 0.5.7 → 0.5.11 (the build re-locks to the local40kdc-datacheckout): cruncher target-profiles + fleet comparison matrix (0.5.8), loadout ranking/auto-enumeration (0.5.9), 11e cover modelled as −1 to hit rather than a save bonus (0.5.10), and ability range-scope honoured, e.g. Furious Onslaught's 18" (0.5.11). All existing tests stay green against it. (pending commit)
Removed
- "Copy settings" dropped from the army library. The per-row action that merged another saved list's per-unit settings (nicknames, reserve/leader/transport assignments, splits) onto a list no longer appears in the library list view — it didn't fit there. (The underlying merge logic is retained for now, just no longer surfaced.) (pending commit)
Fixed
- Wargear stuck "below min", impossible to fix. Symptom: an imported unit (e.g. T'au Kroot, with the always-on Kroot pistol) flagged a weapon as below its minimum, but the count had no stepper to raise it. Root cause: a weapon every model carries has a fixed count (min == max == model count) and renders as a static value, so an import that under-recorded it left it permanently flagged. Imported loadouts are now reconciled against the unit's bounds — fixed weapons are set to their required count, the rest clamped to range. (pending commit)
- Builder buttons floated mid-panel and the header scrolled away. The builder ran inside the modal's own scroll region, so its footer detached and the title scrolled off. The builder now owns its scrolling (its three columns scroll; header and Save/Cancel footer stay pinned). (pending commit)
- Cancel in the list builder dropped you on the library list, not your config. Symptom: opening a saved list with "Edit in Builder" and then pressing Cancel (or Escape) landed on the army-library list instead of the config you were editing. Root cause: cancel unconditionally re-opened the library, discarding the still-open edit; it now only does that for a from-scratch "Build New" (which has a scratch config to drop) and otherwise returns to the open config. (pending commit)
- "← All Packs" did nothing in the Tournament Packs modal. Symptom: while viewing an open pack, clicking "← All Packs" stayed on that pack's detail instead of returning to the pack list. Root cause: the button re-sent
OpenTournamentPacks, but the modal's detail-vs-list view is driven by the edit session's open pack (PackData.active), which that command intentionally doesn't clear — and clearing it Rust-side would also drop the on-board plan bar that reads the same session. Fixed with a modal-local list-view override, so "← All Packs" navigates back to the list without disturbing the session. (pending commit)
Unreleased (11e) — 2026-06-05
Added
- Native list builder. Build an army from scratch inside the app — pick a faction and detachment, search the unit roster, add units, set model counts and wargear, choose an enhancement and warlord, with a live points total. Or open any saved list in the builder to tweak it. Lists you build save to the library like any other and play on the board immediately. Soft validation only: over-points and illegal-loadout warnings show as chips, but nothing is blocked — you're still the rules arbiter. Built entirely on the embedded 40K dataset, so there's no data entry. (pending commit)
- Drag-and-drop army setup. In the army editor, drag a leader (or 11e support unit — now recognized with a SUP tag) onto a unit to attach it, drag a unit onto a transport to embark, or drop onto the Board / Strategic / Deep Strike zones to reassign. Units the game data says the leader can join glow green while dragging — a hint only, any drop is accepted. The dropdowns remain for keyboard use. (pending commit)
- Copy settings between saved lists. A new "copy settings" action on each library entry pulls nicknames, reserve/deep-strike assignments, leader attachments, transport assignments, and splits from another saved list onto this one — matched by unit identity, so it works across list edits and even different list-builder formats. Attachments whose partner unit isn't in the new list are skipped with a note. The fastest path from "tweaked my list" to "configured and ready to deploy". (pending commit)
- Saved army lists remember their Force Disposition. Saving a list to the library (from pregame or the editor) now stores the disposition you declared; loading it back pre-fills your side of the matchup picker and resolves the mission exactly like picking it by hand. Saved per-unit settings also record the resolved datasheet identity, strengthening cross-list matching. Older saved lists load unchanged. (pending commit)
- Re-importing an edited list keeps your unit settings. Nicknames, reserve assignments, leader attachments, and transport assignments now follow units across a re-import by stable identity (resolved datasheet + occurrence) instead of silently re-attaching to whatever unit landed at the old list position. Settings whose unit — or whose attachment target — left the list are dropped with a toast naming them. (pending commit)
- Primary awards get "suggested" chips and round-window greying. The board evaluator now also reads the shared primary mission: awards whose conditions look achievable for the player whose turn it is show the same amber "suggested" hint secondaries have (advisory only — the player still ticks). And any award — primary or secondary — whose trigger window excludes the current battle round dims with a "Triggers in rounds 1–2" tooltip; it stays clickable, because the player is the rules arbiter. (pending commit)
- Tournament packs (Phase 5). Pre-plan a deployment for every opponent Force Disposition: a pack bundles your army (embedded from the army library, so an exported pack is one self-contained JSON file) with any number of named deployment plans per matchup cell — e.g. one per terrain layout, or going first vs second. Building a plan uses the real board: pick the cell (the matchup matrix resolves the mission automatically), choose map and deployment pattern with the normal landing pickers, deploy with the normal tools, and “Save plan to pack” captures the layout — positions, rotations, floors, and reserve assignments, keyed by unit identity so plans survive list re-imports (splits included). At the table: import the opponent’s army, open your pack, click the plan in their disposition’s cell, and both armies come up with yours already placed. Packs live in the new Tournament Packs modal (landing footer) with rename/export/import; loading a plan mid-game is rejected (plans are pre-deployment layouts). Spawning is now idempotent per unit, so re-running setup after a plan load never duplicates models.
- Expected-damage preview (Phase 5). Picking a shoot-tool target now floats an advisory chip next to it showing the expected attack math for the selected weapon — one row per weapon profile (frag vs krak side by side) with the full chain (attacks → hits → wounds → unsaved → damage → models killed), a ½-range badge when Rapid Fire/Melta fire, and quick toggles in the shoot panel for the common modifiers (re-roll 1s, +1 to hit/wound, target in cover). The math is the 40kdc closed-form engine running client-side, conformance-pinned to the Rust implementation; the chip tracks the target through camera pans and zooms. Advisory only — nothing is enforced.
- 11e roll-first charge flow (Phase 4). Charging is now: select the charger (selecting implies declaring) → roll the 2d6 — enter your physical dice or hit "Roll for me" (animated; the in-app roll is generated Rust-side so the display always matches the record) → click targets within the rolled reach → place models → finish. The charge ring shows the rolled total post-roll (12" advisory before). Every roll is logged in the analysis timeline (
🎲 Charge 7" (3+4)), including failed and abandoned charges and command re-rolls — the coaching data point. Rolls persist in the v14 save (additive), and undo/redo and branch switching handle them. Out-of-range target picks warn but never block (charge modifiers exist; the app stays the annotation layer). - Overrun replaces Pile In/Consolidate (11e fight-phase movement, Phase 4). One Overrun tool with the 3" move cap (snap-back kept; the cap is parameterized for future ability-modified distances), plus a new advisory: dragging an Overrun move that ends out of 2" engagement range of every enemy tints the model orange — soft guidance only. Old dev-window saves containing Pile In/Consolidate moves load as Overrun.
- 11e primary missions are crate-driven with tick-based scoring (Phase 3 of the migration). The active primary is a
PrimaryMissionId(String)resolving bothDataset.missions(per-round/per-game VP caps) and the mission's primary card (award DSL — the same schema secondaries use; mission id == card id, 1:1 across all 25). The score flyout gains a shared primary panel mirroring the secondary tick UX: check the awards you achieved, step counted awards, and the crate engine derives the VP (score_primary_event, clamped to the 15/round cap); "Score N VP → Attacker/Defender" commits the derived total to that player and clears the ticks (un-committed ticks also clear at turn rollover — they're the current turn's assertion scratch). Manual ±VP stays as the override, and both paths log the samePrimaryAwardtimeline action. Persisted in save v14 (lenient: a pre-tickprimaryshape degrades to "none selected", never failing the load). (pending commit) - Force-Disposition matchup picker: the landing modal's mission select is now a 5×5 disposition pair (yours × opponent) resolved through the crate's
mission_matchupsmatrix (full 25-pair coverage pinned by test) via a pure, reusableresolve_matchup_missionhelper — the Phase-5 tournament pack walks the same helper per matchup cell. A manual primary pick still works and clears the pair. Disposition dropdown options now sourced fromDataset.force_dispositions.all()(wh40kdc ≥ 0.5.3). All landing-modal<select>elements converted to Svelte 5value={}controlled pattern. Detachment→disposition auto-set wired (auto_set_disposition_from_detachment, no-op until GW publishes the mapping and40kdc-databackfills it). (pending commit) - 10e mission enums removed with Phase 3: the 10-variant
PrimaryMission(+ static award tables) and the 20-variantTournamentMission(A–T), plusActiveTournamentMission, the deadObjectiveOverrideHidden-Supplies override (no reader since objectives became terrain footprints), theSetTournamentMission/SetPrimaryMissionConfigcommands, and the tournament dropdown. Recommended-layout ★s re-key off the active deployment pattern'srecommended_terrain_layout_ids(empty upstream in 0.4.18 — none render until the dataset backfills). (pending commit) - Id-only bridge: the Svelte frontend now resolves all static display data from the embedded 40kdc TS dataset (
@alpaca-software/40kdc-data, file-linked like the Rust path dep; singleton insite/src/lib/data/dataset.ts). The bridge carries only ids + dynamic state:DatacardDatashrank todatasheet_id+ loadout-filtered weapon ids (+ raw-name fallback so an unresolved import still renders a card),WeaponOptionJsto aweapon_id, andDatacard.svelte/ShootToolPanel.svelteresolve profiles, stats, keywords, and multi-profile weapon rows client-side. Ability tooltips and detachment rules text now show the generated effect-DSL approximation (describeAbility/describe_ability, conformance-pinned across both ports in wh40kdc 0.4.18) — display content improves automatically as upstream DSL authoring fills in. Newjust data-syncrecipe builds the TS twin, refreshes the Rust dep, and asserts the two embedded dataset versions match. - One canonical ordered weapon list (
base_shape::loadout_ranged_weapons/loadout_melee_weapons, loadout-filtered by resolved weapon id with raw-name fallback and full-list fallback) now backs the shoot-panel list,check_weapon_range,sync_shoot_rings, andsync_auto_rings. 5 new tests pin the ordering/fallback contract. - 11e objectives are terrain footprints. An objective is a terrain area (
is_objectivepiece, optionalobjective_rolehome/center/expansion); its footprint — spanning the link group — is the OC-control boundary.sync_objective_oc_tallynow sums a unit's OC into an objective when any of its models' bases touch the footprint (model_in_areaover the link-group union), and control is auto-derived from the tally (higher total OC controls; equal-nonzero = contested) with a persisted player override (board-click cycles auto → Attacker → Defender → Contested → Neutral → clear).ObjectiveControlStateis re-keyed by anchor piece id ({auto, overrides}); markers spawn at objective-area centroids on terrain load. Thecontrols-objectivesuggestion predicate gained its qualifiers —exclude:home,objective_role(reconciling the card DSL'scentralto terrain'scenter),objective:opponent-home,scope:enemy-territory— backed by per-objectiveObjectiveFacts, no longer Unknown. Newarmy_list::terrain_layoutbridge resolves crate layouts to the app's polygon model and rehydrates objective metadata. 13 new tests. (pending commit) - Card-runtime board substrate + suggestion engine (Track B): the board-facing half of the 11e secondary system. Terrain tags are now real board state — a card action (
BeginCardAction→ApplyCardAction) marks a terrain area, storing aTerrainTag {tag, source, clears_on, by_player, set_round, set_seq}on the link-group anchor entity (TerrainAreaCardState); tags clear on their declared schedule at turn rollover (clears_on: neverpersists, e.g. Plunder). On top, a pure ECS-free evaluator (src/card_eval/) reads aBoardEvalContextsnapshot and reports which awardwhenconditions look achievable, surfaced as a non-blocking "suggested" hint in the score flyout (AwardOptionJs.suggested/suggested_count, keyed off a newAwardSuggestionsresource) — advisory only, never auto-credited; the player still ticks. Backed predicates:terrain-has-tag(incl.friendly_units_min/enemy_units_max/last_marked),units-destroyed(+-comparison),controls-objective(plain +exclude:home),objective-majority,destroyed-in-tagged-terrain(kill-time and turn-start snapshot — the membership-timing nuance, via a per-turnDestroyedLog+TurnStartMembership),destroyed-while-on-objective, andengagement-fronts(table-quarter occupancy, presence = a unit wholly within a quarter). Predicates the app's data model can't yet back (objective-has-tag,unit-has-tag, …) return Unknown and never suggest. Reusesunit_on_footprint/point_in_shape/base_fully_in_zonefor membership and consumes territory fromDataset.deployment_patterns[].territories. Eligible areas highlight and tagged areas show a tint + badge via pre-spawned, visibility-toggled pools. Terrain tags + per-game action-use counters persist in the v14 save (additive, serde-default; old v14 saves load unchanged). 40 new unit tests. (pending commit) - Card-runtime data layer (Track A): 11e secondary-mission scoring is now player-asserted and crate-driven. The hardcoded 19-variant
SecondaryCardenum + static award tables are gone; cards areSecondaryCardId(String)resolved againstwh40kdc'sDataset.mission_cards, and an active card stores onlyticked: BTreeMap<award_index, count>— VP is derived on demand through the crate engine (score_secondary_event, exclusive-group resolution, per-card cap). The score flyout becomes a tick list: check an award you achieved (ToggleAward), step the count onper/per_maxawards (SetAwardCount), and the engine computes VP — nothing is auto-credited. Award labels and caps come from the crate (describe_award/score_cap); the bridge emits real kebab card ids and degrades gracefully (no panic) on an unknown id. (pending commit) - Card-runtime board substrate (B1):
TerrainPiece.is_objective(serde-default, omitted when false) marks an 11e objective area, pluslink_group_pieces()/link_group_anchor()helpers insrc/types/terrain.rsthat expand a piece to its full LOS link group (or a singleton) and pick the smallest-id anchor — the canonical "a tag / objective spans the whole linked footprint, scored once" unit that the card runtime, footprint membership, and OC scoring build on. 4 unit tests. (pending commit) src/army_list/base_shape.rs:base_shape_for(&Unit) -> BaseShapederives a model's base from thewh40kdccrate — round/oval/flying/hull straight fromUnit.base_size_mm, with precise compound-hull (Falcon) polygons and measured rectangular footprints kept in an in-branch table re-keyed from fuzzy display names to stableunit.idslugs (the id-keyed overrides run ahead of the crate lookup; the crate's flatHull/FlyingBaseshapes are the fallback). Plusstat_value_inches(&StatValue)for movement (integer inches; dice-valued stats →None). First consumer-side increment of the 11e data-pipeline migration (Phase 1). (pending commit)
Changed
Bumped the
wh40kdcpath dependency 0.5.5 → 0.5.6: the canonical roster-json export now round-trips on import (RosterJsonAdapter, registered ahead of all other format matchers) — the persistence contract the upcoming native list builder saves lists on. (pending commit)Live LOS while dragging. Dragging a unit with the LOS tool active now re-runs the analysis continuously — triggers that arrive mid-flight queue and coalesce into one re-run from the latest position (previously they were silently dropped), and the overlay mesh is swapped in place on the existing entity instead of despawn/respawn. Rasterization (the dominant cost, ~20ms for a 10-model unit) moved off the main thread into the async analysis task. If a cycle still blows the live budget (35ms on WASM, where the async pool shares the main thread), the overlay instead fades to signal staleness and redraws on release; moving a unit no longer blanks the overlay while the new region computes. The analysis pipeline systems now run in deterministic order (poll → clear → trigger → spawn) — previously a same-frame Clear+Trigger pair could race, dropping the rerun and double-despawning task entities. (6cc3675f)
Bumped the
wh40kdcpath dependency 0.4.5 → 0.4.18: the official 11e launch mission deck (17 secondaries — the provisional 18-card deck is gone, Plunder is action-driven, Find and Deny is out), ability-DSL enrichment for the remaining 14 factions, thewas-hit-by-attackcombat-reactive condition, and the cross-impl effect-DSL describer. Deck-size tests and the card-eval tests that exercised pre-launch card shapes now pin those DSL shapes inline instead of via since-removed cards. (pending commit)The landing-modal detachment panel resolves from the crate (
Dataset.detachments→ stratagem/enhancement/rule ids) by the detachment id captured at import. The oldDetachmentDatabaselookup was keyed by display name + legacy faction code, which the crate-importer migration had silently broken — the panel now populates again, with names, CP/point costs, phases, and generated rules text where a DSL ability is linked. (pending commit)Bumped the
wh40kdcpath dependency 0.3.3 → 0.4.0, which ships thebase_size_mmbackfill (993/1183 units populated) and renames the base-size API (UnitBaseSizeMm/UnitBaseSizeMmShape→BaseSize/BaseSizeShape, now withFlyingBase/Hull/Uniquevariants). Round/oval derivation is no longer blocked; theround_base_from_datasettest is enabled. (pending commit)Rewrote army-list import (
import_list) to consume the crate'stry_import_rosterinstead of the hand-rolled Listforge/NewRecruit parsers. Each roster unit maps to oneArmyUnitcarrying its resolveddatasheet_id+ a structuredVec<LoadoutEntry>(resolved weapon ids + counts); identity is resolved once at import so runtime readers stop fuzzy-matching by name. Unresolved units stay in the list with candidate suggestions instead of being silently dropped, and multi-profile datasheets emit a fidelity toast. (pending commit)Migrated the save-load model rebuild (
persistence.rs) offBaseDatabase: OC, movement, and the Monster/Vehicle flag now re-derive from the crate via the model's storeddatasheet_id(falling back to a name lookup for older saves). Added shared dataset-query helpers inarmy_list::base_shape(resolve_unit, profile/keyword/weapon accessors) used across readers. (pending commit)Bumped the
wh40kdcpath dependency 0.4.0 → 0.4.3, which lands the 11e card-runtime data: 0.4.1 shipsDataset.deployment_patterns[].territories+ per-piece objective roles + terrain layouts; 0.4.2 adds the authoritative scoring-DSL$commentspec, redefinesengagement-frontsas table quarters, reconciles the terrain-tag enum docs, and adds awhen_drawn.battle_roundfirst-round-redraw signal; 0.4.3 adds the headerless plain-text import adapter (below). (pending commit)11e victory-point caps:
PlayerScore::total_cappednow caps primary ≤ 45 and secondary ≤ 45 (was 50/40), with the combined ≤ 90, painted +10, and grand-total ≤ 100 rules unchanged. (pending commit)Save format → v14, a clean 10e/11e break. 11e saves use a new, incompatible format; the 10e migration ladder (v1–v13 chain + its version-specific helpers and tests) is removed from this build. Loading a 10e save now fails fast with a message pointing at the
10e-finalbuild instead of silently mis-parsing the reshaped secondary data. (pending commit)TimelineAction::label()resolves secondary-card names live from the dataset (threaded through the bridge'spush_state_to_js→ timeline builders) rather than from a baked-in enum, so the analysis-tree labels stay correct as card data evolves. (pending commit)The app now consumes the
wh40kdccrate's 11e terrain layouts and deployment patterns directly:TerrainLayoutsis built fromDataset.terrain_layouts(viaDataset::resolve_terrain+ objective-flag rehydration) and deployment zones fromDataset.deployment_patterns, replacing the in-repo assetinclude_str!s. Layouts with no objective area render as "(draft)". The default board istake-and-hold-mirror-2. (pending commit)Crate-converted terrain pieces now carry a render
category("area"/"wall"), so areas draw with the translucent blue-gray 11e tint and wall features dark-opaque (previously the empty category fell through to the brown 10e fallback once areas became blocking). The layout conversion also bails with a warning if the resolver ever returns more pieces than the layout declares (template-composed features), instead of silently mispairing geometry through the positional zip. (0eb0baf3)
Removed
- Phase 2 of the data-pipeline migration: the double-embed is gone. Deleted
BaseDatabase/DetachmentDatabase(src/army_list/base_lookup.rswholesale, including the fuzzy name-resolution helpersnormalize_punct/resolve_model_name/strip_epic_hero_suffixand the hull-geometry tables superseded bybase_shape.rs), the eight embedded JSON databases (assets/Datasheets*.json,Stratagems.json,Enhancements.json,Detachment_abilities.json), theirinclude_str!blocks, and the bridge'sfilter_weapons_by_loadout. Also removed the orphanedassets/base-sizes.json/assets/terrain-presets.json(no readers since the terrain-editor removal).assets/is now just the icon and the blank sandbox board — every datasheet read flows through the single embeddedwh40kdcdataset. (pending commit) - In-app terrain editor removed (
terrain_editor.rs, its events/resources, theActiveTool::TerrainEdittool, bridge command parsing +TerrainEditToolDataexport, and the SvelteTerrainEditToolPanel+ types) — superseded by the standalone 40kdc-data layout applet. Shared LOS/terrain helpers (TerrainPieceMarker,local_to_world,link_group_*, terrain types) are preserved. (pending commit) - In-repo 10e GW terrain layouts (
assets/terrain-layouts/gw/**), the early gw-11e assets (gw-11e/**), two sandbox layouts, andassets/deployment-patterns.jsondeleted — all layout + pattern data now comes from the crate (11e-only; 10e lives on the10e-finalbuild).ObjectiveRangeRingdeleted: the terrain footprint outline is the control boundary. (pending commit)
Fixed
- LOS rays passed straight through L-shaped ruin walls from the convex side. Symptom: the visibility overlay lit regions behind corner-ruin walls (a unit could "see through" the wall in one direction). Root cause:
extract_footprint_edgesderived each edge's outward normal from a vertex-mean-center test, which is only valid for convex polygons — on a concave L footprint the mean sits in the notch, flipping the normals of the concave-side edges so the one-way exit-blocking test never fired. Normals now derive from polygon winding (shoelace sign), correct for any simple polygon; regression-pinned with the realcorner-ruin-rightgeometry. (0eb0baf3) - Terrain areas did not block LOS at all — sight lines threaded between a ruin's walls straight through its footprint. Symptom: LOS analysis on take-and-hold-mirror showed targets beyond a large ruin area as visible. Root cause: the crate-layout conversion left areas
blocking: false(a "per-template blocking fidelity is a follow-up" placeholder), so only the thin wall features occluded. Per 11e §13.10 an area is obscuring iff it hosts at least one blocking feature — in the competitive dataset that's everything except barricades (catwalk/gantry/pipedefault_blockingcorrected totruein 40kdc-data; their non-blocking rules are narrative-play only), so every area in the GW layouts blocks through its footprint, keeping see-into/occupant-sees-out semantics via the existing one-way footprint edges; integration test pins the through-the-footprint case on the real layout. (0eb0baf3) - Barricades cast bogus LOS shadows. Symptom: 2" non-blocking scenery occluded sight lines. Root cause: the conversion hardcoded
blocking: truefor every feature instead of honoring the template'sdefault_blocking(barricades are the one non-blocking competitive feature; catwalk/gantry/pipe block — their dataset values were corrected alongside). (0eb0baf3) - Objective OC control recomputed every frame for every objective. Symptom: an ungated per-frame system iterated all units against every objective each tick. Root cause:
sync_objective_oc_tallyhad norun_if(the original center-distance version ran unconditionally); the footprint-membership rewrite adds anobjective_tally_dirtygate (units moved/changed, an objective marker added, or the active layout changed). (pending commit) - The GW app's headerless plain-text export (the format both built-in sample lists —
world-eaters.txt,demons.txt— use) failed to import, so every army-list import in that format yielded zero units (96 integration tests panicked indexing an empty unit list). Root cause: the migrated importer routes through the crate'stry_import_roster, whose adapters all require a framing header — thegwadapter needs the+ FACTION KEYWORD:fence,newrecruit-simpleneeds# ++ Army Roster ++with[N pts]brackets — so headerless(N Points)/(N pts)paren lists matched no adapter. Fix (upstream, wh40kdc 0.4.3): newgw-headerlessFormatAdapterthat parses the GW-app export, NewRecruit copy-text, and## Section (N pts)dialects with a unified model-vs-wargear rule (a top-level bullet is a model group when it has: wargearor deeper child bullets, else wargear), declining fenced/++-header/WTC input so framed adapters still win. The app'sgw_headerless_text_export_is_currently_unsupportedtest is converted togw_headerless_text_export_imports(assertsworld-eaters.txtresolves units + datasheet ids). (pending commit)
0.11.0 — 2026-04-30
Added
- Variant-accent pip on unit bases: models in a multi-variant squad (e.g. Stealth team with both burst-cannon and fusion-blaster models, or Pathfinders with carbine / ion rifle / rail rifle loadouts) now show a small colored pip at the bottom-right of the base, deterministically hashed from
(model_name, loadout)into a 6-color palette. Single-variant squads render unchanged. Newvariant_keyandloadoutfields threaded throughSpawnUnit→UnitBase, recomputed on save/load so pips stay stable across sessions. - Shooting tool weapon list is now per-model: the picker filters
weapons_for_unitby the selected shooter'sloadouttext, so the ion-rifle Pathfinder only shows ion rifle + shared weapons instead of the whole squad's grab bag. Falls back to the unfiltered list when loadout is absent or matches nothing in the DB, so the picker is never left empty. Matching reusesnormalize_punct(nowpub) for punctuation/case-insensitive bidirectional contains. - Straight-line pencil tool (
ActiveTool::StraightLine, default hotkeyI, labeled "Line"): two primary clicks commit aPencilSegmentbetween the two clicked points. Reuses the existingSavedAnnotation::PencilSegmentserialization so persistence schema is unchanged. Gizmo preview follows the cursor between the first and second clicks. - Drop-diagnostic toast on army-list import: when the datasheet-DB lookup silently drops any parsed model (partial drop or total fallback), the import emits an actionable
ToastNotificationnaming the unit and the dropped model names. Wired into both live import (UiCommand::ImportArmyList) and session-restore (sync_army_list_text). Upgrades the existing warn-log to fire on partial drops too, not just total fallback. - Pregame "Save to Library" button now confirms the save with a toast (
"Defender — 4/28/2026" saved to library) and toasts a clear "import an army list first" message when the slot is empty. PreviouslySavePregameAsConfigran silently while the symmetricSaveArmyConfig(library-edit modal footer) already toasted, so users were never sure the button did anything.save_pregame_as_confignow returnsOption<String>so dispatch can branch on success. - Camera reset buttons in
HeaderBar: a "Reset rotation" button (returns the camera to identity rotation and re-centers on board center, preserving zoom) and a "Reset view" button (full home — rotation, position, and zoom). Gives laptop users a click-driven escape hatch from accidental Q/E rotation without needing theHomekey. NewUiCommand::ResetCameraRotationandUiCommand::ResetCameraViewvariants plumbed through the bridge; newreset_camera_rotationhelper alongside the existingreset_camera_to_defaultinsrc/plugins/camera.rs. - Per-side "Clear army" button in the pregame
ArmyEditor(Stage 2 of the landing modal). Two-step confirm (Clear army → Confirm clear / Cancel) reuses the existingUnloadArmyConfigForPlayerdispatch path, so attacker and defender can be cleared independently. Wired only via the new optionalonclearprop, so the button stays out of contexts (e.g.ArmyLibraryModaledit view) where clearing isn't meaningful.
Changed
- Unit name labels render at 85% of the previous size (new
NAME_LABEL_SCALE_FACTORconstant) so dense squads overlap less. Applied uniformly to the individual-model name card and the blob-label card, with card width / height scaled proportionally so the dark background stays snug around the text.
Fixed
- Nurglings (and other swarm-style units exported by NR/LF JSON without nested
type: "model"rows) imported as a single base on the board regardless of squad size. Root cause:parse_newrecruit_jsoninsrc/army_list/newrecruit.rs:122had a fallback that hardcodedcount=1when atype: "unit"selection had notype: "model"children, ignoring the unit's ownnumberfield. Fix: usesel.number.max(1)so a{"type":"unit","name":"Nurglings","number":3}exports as 3 models. Two regression tests pin both the parser-level (unit_with_no_models_uses_outer_number) and end-to-end import (nurglings_nr_json_resolves_full_squad) contracts. (11f383ea) - World Eaters sample list dropped
Dishonouredfrom Jakhals and displayedChaos Spawnwith the wrong faction's datacard (CSMM=8"OC=0instead of WEM=10"OC=1). (e4d90183) Four coordinated fixes so this regression class can't recur silently: (1) Upstream extractor (army-assist/scripts/extract_from_game_datacards.py): split multi-model composition strings like"1 Jakhal Pack Leader, 1 Dishonoured and 8 Jakhals"on,/andbefore applying the count-prefix regex, so each role becomes its own model row inDatasheets_models.json. Re-ran extraction; broadly safer for any squad with a single combined-composition entry. (2) Resolver hard-fail (BaseDatabase::ids_for_factioninsrc/army_list/base_lookup.rs): no longer falls back to all-faction matches when a faction hint is given but no datasheet matches. With 5+ same-named datasheets across CSM/DG/EC/TS/WE for names like "Chaos Spawn", the silent fallback was the engine that turned every faction-extraction bug into a wrong-stats bug; it now surfaces as adrop_warningstoast naming the unit and faction hint. (3) Faction-id threading throughArmyUnit→SpawnUnit→UnitBase→BoardUnit: import was already faction-aware (correct stats wrote intoArmyUnit) but the runtime datacard / weapons / abilities / keywords queries insrc/platform/bridge.rsandsrc/plugins/units.rspassedNoneas the faction hint, so the displayed datasheet picked whichever same-named row appeared first in JSON (CSM for Chaos Spawn). All eight call sites now pass the unit's storedfaction_id. (4) Snapshot test guard (world_eaters_sample_list_imports_cleaninsrc/plugins/ui/left_panel.rs): pins the contract end-to-end (Chaos SpawnM=10"OC=1, three distinct Jakhals model rows, no concatenated composition strings, everyArmyUnit.faction_idpopulated), so future data refreshes / parser tweaks / resolver changes / new construction sites that re-break this fail atcargo test. - Eldar Corsair Voidreavers imported as only the Felarch (4 of 5 models silently dropped). Root cause:
resolve_model_nameinsrc/army_list/base_lookup.rsstripped the" w/"loadout suffix but not the verbose" with "form, soVoidreaver with Shuriken riflenever matched the canonicalCorsair Voidreaver.left_panel.rs:81then filtered it out with no log. Added" with "to the loadout-suffix boundary set. - Warlock Conclave imported as a single placeholder model regardless of squad size. Root cause: same
" with "suffix-stripping gap —Warlock with Singing Speardid not resolve to the DB'sWarlockscanonical, so all parsed models failed validation and the total-fallback path spawned(unit.name, 1). Fixed by the" with "strip plus existing pluralize lookup. - Twin Lance imported as one model instead of
Ri'Lantar+Ri'Locai. Root cause: two compounding issues — the army-list exporter uses ASCII apostrophes (', 0x27) whileDatasheets_models.jsonis typeset with U+2019 ('), sostr::to_lowercase()comparisons failed; and the DB namesRi'Lantarwith a" – EPIC HERO"en-dash suffix that the word-boundary prefix match couldn't bridge. Addednormalize_punct(ASCII-folds U+2018/U+2019/U+2010/U+2011/U+2013/U+2014) applied at both index-build and lookup sites, plusstrip_epic_hero_suffixrun at index time soRi'Lantar – EPIC HEROindexes asri'lantar. - T'au Stealth Battlesuits imported as a single placeholder regardless of squad size. Root cause: same apostrophe-encoding mismatch —
Stealth Shas'ui/Stealth Shas'vrein the list (ASCII') did not match the DB's curly', so all three model variants failed validation. Resolved by the samenormalize_punctfix.
0.10.3 — 2026-04-28
Added
- Three new wall feature templates in the terrain editor palette:
Warzone Wall Lg(9"),Warzone Wall Md(6"),Warzone Wall Sm(5") — match the GW Chapter Approved 2025 Warzone-branded terrain set. Wired intoFEATURE_TEMPLATE_NAMES,TEMPLATE_NAMES, andcreate_templateinsrc/plugins/terrain_editor.rs; auto-surfaces in the Features palette via the existing bridge. All other wall properties match existing walls (height 5", thickness 0.25, blocking, category "wall"). assets/terrain-layouts/gw/warzone-layout-4.json— GW Chapter Approved 2025 Leviathan terrain layout 4, fully mirrored with paired wall and ruin features across both deployment zones. Registered insrc/lib.rsso it appears in the layout dropdown.
0.10.2 — 2026-04-24
Fixed
- Shoot tool: no weapon range rings rendered, and every enemy click toasted "Target is out of weapon range" even from point-blank. Root cause:
BaseDatabase::weapon_range_inchescalled.parse::<f32>()on the raw datasheet string, but ranges are stored with a trailing inch mark ("12\""), so every parse returnedNone.sync_shoot_ringsskipped all weapons (empty pool → no rings) andcheck_weapon_rangetreated every shot as out-of-range. Strip non-digit suffixes before parsing. Also fixed the panel's duplicate inch mark (12""→12").
0.10.1 — 2026-04-24
Fixed
- Movement range rings did not reappear after a unit was dragged past its movement distance and then dragged back within range. Root cause:
sync_ring_positionsintimeline.rshad asymmetric visibility logic — it setVisibility::Hiddenwhen the radius collapsed but never restored visibility when the radius recovered. Rewrote as the single authoritative writer for movement-ring visibility, gated on Selected unit + movement-appropriate tool/phase + timeline locked. Visibility is now a pure function of current state, evaluated every frame. - Shoot-tool weapon range rings appeared blank after selecting a friendly unit. Root cause: a race between
handle_shoot_click(which blanket-hid allShooterRangeRingentities on friendly click) andsync_shoot_rings(which re-shows them based on the new shooter). Both systems ran in the same Update tuple with undefined ordering; whichever's commands flushed last won. Removed the blanket-hide fromhandle_shoot_click;sync_shoot_ringsis now the sole visibility writer.
Added
- RangeRing tool panel: clicking a unit while the RangeRing tool is active now surfaces an edit panel with four ring-category toggles (Move, Advance, Threat, Weapons) and a Clear button.
sync_auto_ringswas a no-op previously becauseactive_countnever incremented; rewrote to spawn/hide rings for Move (green), Advance (gold), Threat = Move+6+12 (pink), and per-weapon ranges (red) — lazily pooled, never despawned. NewRangeRingToolDatabridge type,SetRangeRingVisibility/ClearRangeRingUnitUI commands,RangeRingToolPanel.svelteregistered inGameOverlay.svelte. - CI
smoke-testjob on PRs: builds WASM + SvelteKit, servessite/build/statically via Python'shttp.server, runsjust smoke-test(Playwright WASM boot). Blocksdeploy.site/**removed frompaths-ignoreinci-deploy.ymlso Svelte PRs also re-run the wasm-bindgen sync verification.playwright.config.tsswitches itswebServercommand based onCIso local dev still usesbevy run web. (d6198a9)
Changed
- DOM-focus keyboard gating centralized behind a
KeyboardGatedSystemSetinsrc/lib.rs, configured once withrun_if(|dom: Res<DomHasFocus>| !dom.0). Fifteen keyboard-input systems acrosscamera/selection/terrain_editor/units/ui/keyboard/ui/save_load/ui/headernow use.in_set(crate::KeyboardGated); their per-functiondom_focus: Res<DomHasFocus>params andif dom_focus.0 { return; }early returns are gone. Same runtime behavior, but adding a new keyboard-responsive system is now a one-line.in_set()at registration rather than a scattered in-body check. (d6198a9) DetachmentPanel.sveltedrops the "Core Stratagems" section. Core strats dominated the panel by volume; the detachment-specific stratagems are now the only ones rendered. Serialization still includes core strats for other surfaces. (#55)
0.10.0 — 2026-04-23
Added
site/src/lib/actions/textFieldFocus.ts: Svelte action that setsDomHasFocuswhile a text field owns keyboard focus, so Bevy hotkeys (tool switches, phase jumps, WASD camera) don't fire on keystrokes. Applied to every<input>/<textarea>across game modals and panels.- Undo/Redo buttons in the RightPanel header (↶ / ↷), visible after lock. Previously undo was keyboard-only (Cmd+Z / Cmd+Shift+Z).
- Mid-game reserves deployment:
UiCommand::SelectReserveUnitevent, bridge parser, and dispatch handler. Each unit in LeftPanel's "Reserves" section now has a Deploy button — clicking it switches to the DeployReserves tool, selects the unit, and toasts "Click the board to place". Fires the existingDeployFromReservesflow. - Detachment data: embed
Detachment_abilities.json,Stratagems.json, andEnhancements.jsonfrom game-datacards. NewDetachmentDatabaseresource exposes per-detachment ability/stratagem/enhancement lookups keyed on(faction_id, detachment_name), case-insensitive on the name. Core Rules stratagems merge into every lookup so universal strats remain visible regardless of detachment. - Detachment panel in the landing modal: on army import, each player's side now shows the detachment's abilities, enhancements, stratagems (faction + Core Rules), collapsible sections with phase tags and CP-cost chips. Bridges
DetachmentInfoJsthroughLandingDatausingextract_detachment()on list headers plusDetachmentDatabaselookups.
Changed
drain_js_commandsinbridge.rsnow logslog::warn!for malformed JSON, unknown command types, and known types with bad payloads — previously all three were silently dropped.sync_dom_focusnow gated withrun_if(dom_focus_dirty)— a new atomic flag flipped by JS calls toset_dom_has_focus. Complies with the CLAUDE.md rule that everysync_system must have a run_if gate.extract_from_game_datacards.pynow readsbaseSizefrom upstream datasheets (was hardcoded to empty) and synthesizes per-model rows from thecompositionfield so squad leaders and named characters (Infernus Sergeant, Aggressor Sergeant, Twistbray, etc.) resolve during list import. Datasheet ids shared across multiple Chaos factions are now suffixed with the faction (e.g....-TS) so Bevy'sid_to_factionmap stays unique.- Reverse-prefix model name resolution now tolerates singular→plural differences between the parsed name and the DB model name (e.g. parsed
"Death Company Marine"resolves to DB"Death Company Marines with Jump Packs"). - Refreshed all datasheet JSONs from game-datacards (2026-04-21): 1,170 datasheets, 1,682 models with base sizes, post-Jan 2026 balance dataslate changes and detachment updates for all major faction packs.
Removed
- Dead outbound state fields from
UiStateSnapshot:pencil_eraser_active,active_tool_label,live_action_count. No Svelte component read them; they were dead weight on every bridge JSON push.
Fixed
- Typing in the landing-modal army-list paste box (and every other modal text input) silently fired Bevy hotkeys for each keystroke — tool switches on K/M/S/C, phase jumps on F1–F5, camera pan on WASD. Root cause:
setDomFocus(true)was only called in 4 tool panels; every other<input>/<textarea>leftDomHasFocus=false, so Bevy keyboard systems ran unguarded against the typed keys. Fixed by thetextFieldFocusaction applied to all text fields undersite/src/lib/components/game/**. - Killed unit overlay rendered as a rectangle instead of a meaningful icon. Root cause:
Text2d("☠")used Bevy's embedded FiraSans-Bold font, which lacks U+2620; the text renderer drew a missing-glyph rectangle. Replaced with two crossedRectanglemesh children forming a red X, sized proportionally to each base. - Dead units blocked all other units from moving through their position. Root cause: the collision snapshot in
sync_drag_blockedandon_drag_end_commitincluded killed units (and even retired/hidden ones) because theunit_snapshotbuilder had nois_killedfilter. Added.filter()to exclude killed units from both snapshots.
0.9.8 — 2026-04-17
Changed
- Accessibility contrast bumps:
text-muted#8b8b96→#a8a8b2(AAA 7.9:1),text-dim#838390→#8a8a94(AA 5.1:1). New type scale tokens (13px floor) and layered shadow tokens for dark backgrounds.
Added
- Seven new faction themes in
site/src/lib/palettes.ts: Imperial Fists, White Scars, Sisters of Battle, Aeldari, Drukhari, World Eaters, Death Guard. - Design brief captured at
site/design-brief.md.
0.9.7 — 2026-04-16
Added
- App icon (native window/taskbar) and web favicon now use the Shadowboxing logo. Native: startup system embeds
assets/icon.icoat compile time viainclude_bytes!and sets it on all winit windows. Web:site/static/favicon.icoserved at/favicon.ico, referenced inapp.html.
0.9.6 — 2026-04-14
Changed
- Charge tool: complete UX overhaul.
- Either-order selection: clicking an enemy unit before selecting a charger no longer shows an error. The target is queued and the panel lists eligible friendly units to charge with. Clicking any of those units (on the board or in the panel) sets the charger with the target pre-filled.
- Unit-level charger: the charger is now the whole unit (by
unit_id), not a single clicked model. All alive models in the unit can be dragged during the placement subphase. - Placement subphase: after "Declare Charge", the panel shows each model's placement progress (X / Y models placed). Every model must be dragged to a charge position before "Finish Charge" unlocks. This ensures all models get a
has_charged = truetimeline entry, keeping undo/redo consistent.
Fixed
- Death Company Marines with Jump Packs (and any unit whose JSON export uses a shorter model name than the DB canonical name) now spawn the correct model count instead of 1. Root cause: step 5 of
resolve_model_name()only checked if the DB name was a prefix of the parsed name, never the reverse — so"Death Company Marine"could not match"Death Company Marine with Jump Packs". Added a reverse word-boundary prefix arm to cover this case.
0.9.5 — 2026-04-13
Added
- T'au Empire faction data: 52 datasheets, 53 models (with base sizes), 243 weapons, 291 abilities, 295 keywords. Tau army lists now import with correct stats, base shapes, and unit abilities.
Fixed
- Space Marine chapter subfactions (Blood Angels, Dark Angels, Space Wolves, Black Templars, Salamanders, Ultramarines, Deathwatch, Iron Hands, Raven Guard, White Scars, Crimson Fists, Imperial Fists) now correctly map to the SM faction in text-format army list headers. Previously only "Space Marines" and "Adeptus Astartes" were recognized.
- Multi-model vehicle/walker units (e.g. Crisis Battlesuits, Krootox Rampagers) now spawn the correct number of models. The vehicle model-count cap, intended to prevent weapon-bullet accumulation on single-model vehicles, was too aggressive — it capped all vehicles at 1 model regardless of actual unit composition. Now only caps when the parsed model name differs from the canonical database name, indicating a weapon-derived count.
- Hardened
w/loadout-suffix stripping in model name resolution to also match"Marine w/Eviscerator"(no trailing space after slash), not just"Marine w/ Eviscerator".
0.9.4 — 2026-04-12
Fixed
- Declare Charge / Cancel Charge / Remove Charge Target buttons in the charge tool panel were unclickable. Root cause:
drain_js_commands()inbridge.rshad no match arms for these three command strings, so they were silently dropped before reaching the dispatch handler.
0.9.3 — 2026-04-11
Fixed
- Charge tool was completely non-functional: clicking a friendly unit and enemy targets had no effect on dragging because
charge_declaredwas never set toSome(true). Root cause: the declaration confirmation step (bridge command + Svelte panel) was not implemented. AddedDeclareCharge/CancelCharge/RemoveChargeTargetUiCommands,ChargeToolDatabridge type, andChargeToolPanel.svelteto complete the flow. (opkmylnl) - Units in the World Eaters sample list resolved to wrong factions (CSM Chaos Spawn/Forgefiend, DG Helbrute instead of WE). Root cause:
extract_faction/extract_detachmentused a case-sensitivepts_re_likecheck; newer Listforge exports use "Points" (capital P), so the army name line was not filtered and was returned as the faction. With no faction hint, all DB lookups fell back to cross-faction, picking whichever datasheet appeared first. Fix:s.to_lowercase().contains(...)in both functions. - Helbrute (and other vehicles) spawned with 2 models when the list had 1. Root cause: Listforge exports a dual-fist loadout as two separate
• 1x Helbrute fistbullets; the text parser accumulatedmodel_counts["Helbrute fist"] = 2, which prefix-matched to canonical "Helbrute" and producedArmyUnit { count: 2 }. Fix: capeffective_countto 1 inimport_listwhenis_monster_or_vehicleis true — monsters/vehicles are always single-model per unit slot.
0.9.2 — 2026-04-10
Fixed
- Army library configs now load at startup, so the "load from library" dropdown in the landing modal is populated immediately instead of requiring the user to open the Army Library first.
- Movement range rings (green/orange) no longer show during non-movement tools (e.g. shooting phase). They now only appear when using movement tools or the Select tool during Movement/PreGame phases.
- Movement range rings now follow the model's current position and shrink by cumulative distance traveled, instead of staying anchored to the phase-start position with static radii.
Added
- Weapon range rings during ShootAnnotate tool: selecting a shooter shows one blue ring per ranged weapon at the correct edge-to-edge range. Hovering a weapon row in the panel highlights that ring and fades the others.
- Linked terrain areas: two terrain area footprints can now be marked as linked so they behave as one piece for LOS. A unit inside either linked area can see out of the combined footprint, and external rays pass through the shared internal edge to hit the combined outer boundary. Shift-click a second area + press L to link; U to unlink. Crucible of Battle center trapezoids are pre-linked.
0.9.1 — 2026-04-09
Added
- Multi-floor ruin support: models can now be assigned to different floors of a ruin (0=ground, 1+). Per-model
floor: u8field onUnitBase, tracked through snapshots, save/load, branch switching, rewind, and undo/redo SetFloorevent +UiCommand::SetFloor+ bridge parser — floor can be set from Svelte UI or keyboard (shortcuts coming next phase)- Floor changes add vertical distance (
|delta| * 3") to cumulative movement tracking, so the ADV pill correctly fires when combined horizontal + vertical distance exceeds M TimelineAction::SetFloor/SavedActionData::SetFloor— floor transitions recorded in the action log and persisted in save filesUndoCommand::SetFloorwith full undo/redo support including distance reversalFLOOR_HEIGHT_INCHESconstant (3.0") inconstants.rsfloorfield added toBoardModelbridge type for Svelte consumption- Floor badge pill ("F1"/"F2"/"F3") in the unit indicator stack, hidden when on ground floor; dynamic text updates when floor changes
- Z-stacking: models on higher floors render on top (+0.05 z per floor)
ViewingFloorresource +sync_floor_filtersystem: viewport floor filter dims non-matching models to 30% opacity, controlled viaUiCommand::SetViewingFloor[/]keyboard shortcuts to decrement/increment floor on selected models (clamped 0-5)- Measure tool shows floor height difference when both endpoints snap to units (e.g.,
6.2" | 3" height) - Plunging fire soft guidance: shooting annotations append
[Plunging]when shooter has 3"+ height advantage over target - Save version bumped to v12 (v11 migrates automatically via
#[serde(default)]) - Floor state preserved through: deployment lock snapshots, end-of-turn snapshots, branch parking/restore, rewind/live-return, truncate-to-deployment, truncate-to-turn
- 8 new integration tests for floor system (set/get, undo/redo, save/load, timeline snapshot, distance cost)
Fixed
floor_persists_through_save_loadtest failed on CI: assertion checkedunits[0]by array index, but Bevy query iteration order differs across save/load boundaries due to archetype reordering (IsLeadersplits entities into separate archetypes). Fixed by asserting any attacker unit preserved floor=2 after roundtrip- Floor segment terrain templates: Floor 4x4, Floor 3x4, Floor 2.5x4, Floor Trapezoid — non-blocking platforms with category "floor", height 3.0", default floor 1
floor: u8field onTerrainPiece— configurable per-piece floor number (0 = ground), carried through template swaps and save/load- Floor control in terrain micro-adjustment panel: ±1 nudge buttons + direct input, visible when selected piece has floor > 0
- Semi-transparent blue rendering for floor segments, distinct from walls and areas
- Viewport floor filter overlay (
FloorFilter.svelte): floating pill-bar with All/G/1/2 buttons, positioned bottom-right of canvas, sendsSetViewingFloorcommand available_floorsandviewing_floorin top-level game state bridge for Svelte consumptionUpdateTerrainPiecenow acceptsfloorfield for terrain piece floor number changes
0.9.0 — 2026-04-08
Added
- 11th edition terrain support: terrain areas and terrain features are now separate piece types with parent-child relationships (features move with their parent area when dragged)
- 5 new 11e terrain area templates: Large (11.5×7), Triangle (8×11.5), Medium (6×4), Long Line (10×2.5), Short Line (6×2)
- 3 new wall feature templates at 0.25" thickness: Short (4"), Medium (6"), Long (10")
- Terrain presets: save area+feature combos as named presets and stamp them onto the board
TerrainEditToolPanel.svelte: grouped template dropdown (11e Areas / Features / 10e), add/delete/export/save-preset buttons, keyboard hints- Terrain editor bridge wiring:
AddTerrainPiece,RemoveTerrainPiece,ExportTerrainLayout,SaveTerrainPreset,PlaceTerrainPresetcommands now parsed from JS - Stub layout
gw-11e/crucible-of-battle.jsonfor building 11e layouts - Polygon (triangle) centroid label positioning — labels render inside triangular footprints instead of at a vertex
- Distinct render colors for terrain areas (blue-gray) vs wall features (dark opaque) vs legacy 10e pieces
Fixed
- Vite 8 build failure:
/wasm/bevy-deploy-helper.jsunresolved import. Vite 8'svite:import-analysisplugin and Rolldown reject staticimport()of files in the public directory even with@vite-ignore; moved the URL into a variable to make the import non-static
0.8.9 — 2026-04-07
Added
- Unit splitting (combat squadding): split a unit into two equal halves during pregame so each half can be independently assigned to different transports, reserves, or board deployment. Supports multi-variant units (e.g., Sergeant + Marines), odd model counts (ceil/floor), and full save/load + army library round-trip.
- Mobile/tablet device warning: detects touch-primary devices and shows a dismissible "Desktop Required" modal before loading the WASM bundle, saving bandwidth on devices that can't meaningfully use the app
- 10 new integration tests for split/unsplit behavior, transport assignment, and edge cases (total tests: 355 → 365)
TestHarnessextended withsplit_unit,unsplit_unit,unit_splits,army_unitsmethodsUnitSplitsresource tracking split relationships between unit halves- Svelte ArmyEditor: split/unsplit buttons on unit cards with SPLIT badge and (1/2)/(2/2) labels
Fixed
- Disembark tool: clicking a transport now shows a sidebar panel listing all passengers with individual Disembark buttons and an emergency disembark toggle (was broken — passenger selection had no UI)
- Embark and Disembark tools are now available in Shooting, Charge, and Fight phases (were Movement-only; factions like Drukhari need transport access in every phase)
BoardUnitbridge type now exposestransport_idso the frontend knows which transport a unit is embarked in
0.8.8 — 2026-03-30
Added
- 44 new integration tests across 4 modules: missions (20), persistence round-trip (10), transport (6), leaders (8) — total test count 311 → 355
TestHarnessextended with 25 new methods: mission DSL (award_primary_vp,set_painted,set_objective_control,set_primary_mission,secondary_deck_action,primary_vp,secondary_vp,painted,primary_mission,secondary_active_cards,secondary_draw_pile_len,secondary_achieved_cards,secondary_discarded_cards,objective_controls), transport DSL (embark_unit,disembark_unit,assign_transport,remove_from_transport,is_in_transport,transport_of), leader DSL (attach_leader,detach_leader,detach_leader_pregame,is_attached,attached_to), persistence (export_save_json)just coverageandjust coverage-lcovrecipes forcargo-llvm-covcode coverage reporting- Pencil eraser sub-mode: press P again while in pencil tool to toggle between draw and erase; drag over strokes to remove them with a 0.5-unit radius eraser circle
- Measure tool live preview: line, end dot, and distance label now follow the cursor in real time after the start point is set, without requiring a button hold
- Measure tool ghost base: when measuring from a selected unit, a semi-transparent copy of the unit's base shape appears at the cursor position so players can verify the model fits at the destination
- Exhaustive integration test suite: 11 new test files with 55 tests covering timeline state machine, undo/redo, branch switching, kills/wounds, movement tracking, rewind/truncate, pregame, persistence reset, annotations, action deletion, and property-based invariants
TestHarnessextended with 22 new methods: query DSL (active_player,cursor_round,cursor_view,is_killed,wound_state,is_battleshocked,has_advanced,has_charged,remained_stationary,undo_stack_len,redo_stack_len,active_branch,cumulative_distance,find_notes) and action DSL (confirm_action,confirm_battleshock,confirm_shot,adjust_wounds,end_pregame_turn,switch_branch,delete_timeline_action,reset_game,import_save,create_note,clear_annotations)- Property-based tests with proptest: undo/redo cycle identity, kill toggle involution, rewind/live identity, end turn player swap
- Sample army lists (Chaos Daemons, World Eaters) now appear as built-in entries in the Army Library with a "sample" badge; loadable for either player and copyable to user configs
Changed
- Army library configs now carry a
builtinflag distinguishing built-in samples from user-created configs - Save format bumped to v11 (backward-compatible: v10 saves load with defaults)
Fixed
- Autosave now round-trips pre-lock game state: restoring from an autosave taken during army management or deployment correctly reopens the landing modal, preserves setup stage, submitted flags, nicknames, and pregame assignments (leaders, transports, reserves)
- Branch switching now correctly parks and restores reserves state (InReserves/ReserveType components were not being inserted/removed during branch restore)
- Branch 1 (Defender First) snapshot at deployment lock now captures actual reserves state instead of hardcoding
in_reserves=false
Removed
- Dedicated "Load Sample" button in ArmyEditor (replaced by built-in library entries in the "Load from Library" dropdown)
0.8.7 — 2026-03-28
Added
- Reserve deployment system: assign units to Strategic Reserves or Deep Strike in army management, deploy during game with passenger disembark, full undo/redo support
- Deployment zone validation: units placed outside their deployment zone receive a DragBlocked indicator; Scout and Infiltrate abilities bypass zone restrictions
- Label collision avoidance for movement arrow distance labels (Pass 4): arrow labels dodge unit labels and each other with iterative push-apart
- Integration tests for reserve deployment (
tests/reserves.rs) and deployment zones (tests/deployment_zones.rs) TestHarnessDSL additions:assign_reserve(),is_in_reserves()
Changed
- Timeline snapshots now capture reserves state for branch switching and historical view restoration
- Undo system tracks reserve deployments with full passenger restoration data (
PassengerUndoData) - Deployment pattern data updated with zone geometry
- Bump
proptest1.10→1.11 (f653727) - Bump
dirs5→6 (510948c) - Bump
ts-rs11→12 (d104be0) - Bump
geo0.28→0.32; fixSimplifyAPI change (a1862c7) - Bump
@types/node22→25,typescript5.9→6.0 in site (7d21adf, e8860e4) - Bump 9 minor/patch npm deps in site: svelte, tailwindcss, vitest, storybook addons, prettier-plugin-tailwindcss (d39aef7)
- Bump GitHub Actions: checkout v6, setup-node v6, upload-artifact v7, download-artifact v8 (6fe9264)
Fixed
- Eliminated all compiler warnings (52 native + 5 WASM) via cfg-gated imports, dead-code suppression for WASM bridge types, and unused variable cleanup
Removed
- Remove unused
ehttpdependency (a1862c7)
0.8.6 — 2026-03-27
Added
- Headless test harness: lib/bin split (
src/lib.rs) withregister_game()for shared app setup (8cd5da3) build_headless_app(): windowless Bevy app using DefaultPlugins minus WinitPlugin for off-main-thread testing (8cd5da3)TestHarnessstruct with frame stepping (tick,tick_n,tick_until,execute) and game DSL (import_army,spawn_all,deploy_all,lock_deployment,begin_game,set_phase,move_unit,confirm_kill,end_turn) (8cd5da3)- LOS DSL on TestHarness:
select_unit,trigger_los_analysis,wait_for_analysis,danger_region,danger_area_sq_in(8cd5da3) UiCommand::MoveUnit+MoveUnitCommandevent for programmatic unit movement without drag simulation (8cd5da3)on_move_unit_commandsystem: delegates tocommit_position()directly, no drag validation (annotation-first) (8cd5da3)MoveUnitJSON parsing in WASM bridge (drain_js_commands) (8cd5da3)- Integration test suite: 4 smoke tests (
tests/smoke.rs) and 2 LOS pipeline tests (tests/los.rs) (8cd5da3) - Escape key handling on modal backdrops (Save, Load, Changelog, Settings) (8cd5da3)
Changed
main.rsreduced from 375 to 80 lines; all game registration moved toregister_game()inlib.rs(8cd5da3)- Dispatch
SystemParambundles (UnitWriters,TimelineWriters, etc.) widened frompub(crate)topubfor lib/bin visibility (8cd5da3) UiStatewidened frompub(crate)topubfor same reason (8cd5da3)commit_positionandactive_tool_to_move_typeinunits.rswidened topub(crate)(8cd5da3)
Fixed
- Svelte a11y lint warnings suppressed on modal overlay divs and tooltip triggers (8cd5da3)
- ArmyLibraryModal:
<input>moved inside<label>for proper nesting (8cd5da3)
0.8.5 — 2026-03-27
Added
- Notes tool: text alignment options (left, center, right)
- Notes tool: text wrapping width slider to constrain note width
- Notes tool: click existing notes to select for editing (text, font size, alignment, width)
- Notes tool: drag selected notes to reposition them
- Notes tool: delete individual notes via Delete/Backspace key or panel button
- Notes tool: hover and selection bounding box outlines (Word-style text box feel)
- Notes tool: grey border gizmo on preview showing current text bounds while composing
- Notes tool: edit mode panel with Update/Delete/Done actions
- Save/load: annotations (notes + pencil strokes) now persist across save/load cycles
- Save/load: backward-compatible new fields on
SavedAnnotation::Note(alignment,width) - Army Library: atk/def load buttons on each config row for direct import into army management
Fixed
- Annotations now included in saves (removed "not yet supported" toast)
0.8.4 — 2026-03-27
Added
- Army Library: save, load, rename, copy, and delete reusable army configurations independently of game saves
ArmyEditor.svelte: extracted reusable swimlane army editor component from LandingModal (nicknames, leader attachment, transport/reserve assignment, text import)ArmyLibraryModal.svelte: list view with CRUD actions, edit view with full ArmyEditor, inline rename, delete confirmation- Army config persistence: localStorage (WASM) and gzip files (native) via
list/save/load/delete_army_configplatform functions SavedArmyConfig/SavedUnitConfigdata model with name+occurrence keying for stable configs across re-importsLibraryEditStateembedded inUiStatefor library editing without exceeding Bevy's 16-param system limit- 17 new
UiCommandvariants for library CRUD and editing, with bridge parsing and dispatch - Pregame integration: "Load from Library" dropdown and "Save to Library" button per player in Army Management stage
- "Army Library" button in GameSetup footer
- Escape key navigation in library modal (edit → list → close)
Changed
- LandingModal reduced from 745 to 321 lines via ArmyEditor extraction
- Army config display names loaded from stored JSON instead of derived from storage keys
- Storage keys use opaque timestamps (
army_{epoch}) instead of sanitized names
Fixed
- Library rename had no visible effect:
list_army_configs()was deriving display names from storage keys (which include timestamps) instead of loading thenamefield from the JSON config - Cancel from library edit view closed the entire modal instead of returning to list view
0.8.3 — 2026-03-27
Added
- Delete action button (✕) on live turn timeline items: surgically removes a model's action and cascades to all subsequent actions for that model in the turn, bypassing the LIFO undo stack
- Unit datacard panel in the right sidebar: shows stat lines, weapons, abilities, and keywords for the selected unit (
Datacard.svelte,AbilityTooltip.svelte) BaseShape::perimeter_sample_points(): rotation-aware 9-point sampling for zone/terrain checks, replacing inline cardinal+diagonal math information.rsandselection.rs- Delete confirmation prompt in Save modal (inline "Delete? Yes/No")
- Reserve staging label backgrounds (dark card behind unit name in staging area)
GamePhase::ordinal()for phase ordering comparisonsUndoCommand::entity()andUndoCommand::phase()helpers- Pregame assignment UI: reserve type and transport assignment dropdowns in landing modal
Changed
- Kill, Battleshock, and PerformAction tools fire events immediately on click instead of setting a pending target state
- Landing modal unit labels show all attached leaders (plural) instead of only the first
ActionDetailJsnow carriesoriginal_indexandentity_bitsfor UI→Rust action identification- Datacard types exported via ts-rs (
DatacardStatLine,DatacardWeapon,DatacardAbility,DatacardData) - Branch switching now parks and restores mission state (primary VP, secondary deck, objective control)
Fixed
- Autosave not running during deployment:
mark_autosave_dirtyandtick_autosaveboth guarded ontimeline.locked, so in-progress deployments were never captured - Undeployed units dropped on save/load:
placedcounts reset to 0 when army text was re-parsed on load; addedreconcile_placed_countssystem that patches ArmyUnit.placed from spawned ECS entities after load - Nicknames lost on load:
reconcile_placed_countsalso restores nicknames from ECS entity data - Indicator pill ordering:
sync_indicator_pillsnow runs.after()kill/action/battleshock/shot/wound systems so pills reflect current-frame state sync_unit_strengthordering: now runs.after(confirm_kills)to avoid one-frame stale below-half-strength badge
Removed
- Stale scaffold files (
missions.md,reserve-zones.md,undo-redo.md)
0.8.2 — 2026-03-26
Added
- Left panel groups units by location post-lock: On Board, In Transport, Reserves sections replace inline badges
- Card draw: "Pick" button opens inline card picker from draw pile; "Return" button puts active card back in deck (reuses existing
Reshuffleaction) - Per-save download button in Save and Load modals (
ExportSpecificSaveevent) - Delete confirmation prompt in Load modal (inline "Delete? Yes/No")
- Rotation discoverability: one-time toast hint when a non-circular model is first selected
- Movement distance labels now have dark semi-transparent backgrounds for readability
Changed
- Branch labels: "Go First" / "Go Second" renamed to "Attacker First" / "Defender First"
- Draw button works in all deck modes (DeckMode guard removed)
Fixed
- Label card background drifting from text on rotation —
LabelCardBackgroundwas missing fromsync_unit_rotationcounter-rotation filter - Model turning yellow-orange during pregame:
ProximityWarningtint persisted after drag end; now removed on drag commit and onOnExit(Deployment) - Movement distance labels doubled after dropping unit:
cleanup_drag_previewnow checksdrag_state.target(cleared immediately) alongside deferredIsBeingDraggedremoval - Reserve staging labels duplicated:
RemovedComponentsevents not fully drained caused repeated layout rebuilds; changed.next().is_some()to.count() > 0 - Save list not refreshing after saving:
list_saves()now called after successful save; delete also updates save dialog list
0.8.1 — 2026-03-26
Fixed
- Line of Sight overlay rendered overlapping semi-opaque triangles that accumulated opacity into an opaque blob — replaced earcut triangulation with scanline rasterization to a 0.05" boolean grid, emitting RLE horizontal strip quads; overlap is deduplicated by construction (
rasterize_to_mesh)
0.8.0 — 2026-03-26
Added
- Go First / Go Second timeline branches created at deployment lock; each branch maintains independent turn history, cursor position, live actions, annotations, phase, mission state, and undo/redo stacks; switch via button row in timeline panel header (
43e1bc3) - NewRecruit/Listforge JSON army list parser (
newrecruit.rs): imports BattleScribe roster schema JSON in addition to existing Listforge text format; auto-detects JSON vs text on paste (43e1bc3) - Branch-scoped annotations: notes and pencil strokes tagged with
BranchTagso they show/hide when switching branches (43e1bc3) - Reserve label visibility: on-unit name labels hidden when unit enters staging area (staging provides its own), restored on exit via
sync_reserve_label_visibility(43e1bc3)
Changed
- Save format v10: branches array replaces single timeline; phase and mission state moved from top-level to per-branch; v9 saves auto-migrate by wrapping legacy timeline into a single "Go First" branch (
43e1bc3)
0.7.1 — 2026-03-26
Added
- Screenshot header bar showing game phase, round, and active player; hidden during normal play, rendered in world space above the board during F9 capture and screenshot sequences (
65efd9d)
0.7.0 — 2026-03-25
Added
- Undo/redo for all live-turn actions: movements, kills, battleshock, action toggles, and shoot annotations (
b278b06) UndoPluginwithapply_undoandapply_redosystems, gated onon_event+timeline.locked && cursor.is_live()(b278b06)UndoCommandenum with 6 variants (MoveUnit,KillUnit,UnkillUnit,ToggleBattleshock,ToggleAction,ShootAnnotate), each carrying enough state to reverse the action (b278b06)- Undo stack push sites in
on_unit_moved,confirm_kills,confirm_action_flag,confirm_battleshock_flag,confirm_shot_flag(b278b06) - Unit tests for
UndoStack: push/pop/clear semantics (4d3c1f5)
Fixed
undo_stack.clear()inon_truncateandon_begin_gameran before thetimeline.lockedguard, silently destroying undo history on rejected events (4d3c1f5)remove_last_action_for_unitreturn value was ignored inMoveUnit,KillUnit,ShootAnnotate, andUnkillUnitundo paths; now logs a warning on missing entries (4d3c1f5)
0.6.7 — 2026-03-25
Added
- TypeScript type definitions extracted into dedicated per-type files under
site/src/lib/types/(21 modules:ArmyUnitJs,BoardModel,BoardUnit,CursorViewJs,PhaseActionsJs,UiStateSnapshot, and others); mirrored intobindings/site/src/lib/types/for use by type-generation tooling src/platform/bridge_types.rs: new module consolidating Rust-side bridge type definitions previously scattered acrossbridge.rs- Hookify rule
update-changelog: stop-event hook reminding to updateCHANGELOG.mdas the last step after completing any task
Changed
- Killed units are now hidden (
Visibility::Hidden) at turn end instead of despawned; entity references inActionLog,PositionSnapshot, andTimelineEntityMapremain valid for the session, eliminating stale-entity bugs in timeline rewind and undo (c1068ab)
Fixed
eslint-plugin-storybookpeer dependency conflict: bumped from^9.0.0to^10.3.3to match[email protected], fixingnpm cifailure in CI deploy step (be55e09)
0.6.6 — 2026-03-24
Added
verify-wasmjustfile recipe: useswasm-objdumpto check that every__wbg_import in the WASM binary is present in the JS glue file; fails fast with a list of missing symbols- CI deploy job now runs
just verify-wasmafterBuild WASMto catch WASM/JS sync failures before they reach Cloudflare Pages
Changed
commands.ts: replacedwindow as anycasts with adeclare global { interface Window { ... } }augmentation so__shadowboxing_send_commandand__shadowboxing_set_dom_focusare properly typed throughout the codebaseLandingModal.svelte:unitLabelparameter typed asArmyUnit[](wasany[]); five(u: any)iterator annotations removed — inferred from array element type+page.svelte:catch (e: any)narrowed tocatch (e: unknown)withinstanceof Errorguard on.messageaccess
0.6.5 — 2026-03-24
Fixed
- Neutral dark palette had three WCAG AA contrast violations:
text-dim(#5c5c66, ~2.9:1 on dark surfaces) lightened to #838390 (≥4.5:1 everywhere);accent-foregroundchanged from #ffffff to #0a1f1c (dark teal-black) so white-on-teal buttons are no longer a 2.49:1 failure ToolButtonhotkey<kbd>used Tailwind/50and/60opacity modifiers which composited to failing contrast even when the base tokens were valid — removed opacity suffixesPaletteSamplerstory used hardcodedcolor: whiteon three accent buttons and anopacity: 0.5span, bypassing the token system — switched tovar(--color-accent-foreground)and removed opacity- Docs download buttons used
text-accent-foreground/70on file size labels inside teal<a>elements (~3.9:1) — dropped the opacity modifier
0.6.4 — 2026-03-24
Fixed
- Landing modal was functionally broken:
SetFirstPlayer,SetPrimaryMissionConfig,SetSecondaryMode, andSetDeckModecommands were neither parsed inbridge.rsnor implemented indispatch.rs— all four now fully wired end-to-end GameStateTypeScript interface was missing thelanding: LandingData | nullfield; Rust was serializing it but the type gap broke IDE support and risked unreliable reactivity in Svelte 5's$state()proxy — addedLandingDataandNamedOptioninterfaces and seededdefaultStateon_click_objectiveran every frame without arun_ifgate, inconsistent with all other systems in the plugin — now gated onmouse.just_pressed(MouseButton::Left)UiDrawSetdoc comment still referenced egui internals after the egui removal — updated to reflect current behavior
0.6.3 — 2026-03-24
Added
- Storybook accessibility audit: WCAG contrast ratio grid and color-blind (deuteranopia, protanopia, tritanopia) simulation stories under "Design Language/Accessibility"
contrast.tsutility for computing WCAG 2.1 contrast ratios and gradesContrastAudit.sveltecomponent rendering all text-on-background pairs with numeric ratios and AAA/AA/Fail badges
Fixed
- Deployment zone and objective ring opacity sliders now work — added
sync_zone_opacityandsync_ring_opacitysystems gated onresource_changed::<OpacitySettings>; reserve edge strips and exclusion rings also respond to zone opacity slider - Deployment zones, objective markers, objective outlines, and range rings spawned with
AlphaMode2d::Opaquedespite having alpha < 1.0 — switched to explicitAlphaMode2d::Blend - New Game now resets camera zoom, pan, and rotation to default centered view — extracted
reset_camera_to_defaultand wired it intoon_reset_gameviaCameraCleanupSystemParam - Screenshot tool produced blank images in Chrome — canvas now pre-creates WebGL2 context with
preserveDrawingBuffer: truebefore Bevy init so the framebuffer survives past present - Screenshot key changed from F12 to F9 to avoid collision with Chrome DevTools inspector
0.6.2 — 2026-03-24
Added
- Score flyout panel: two-column ATK/DEF scoring with primary VP buttons, secondary card draw/achieve/discard/award, painted army toggle
- Note tool panel: floating textarea with font-size slider, enter-submits toggle, live canvas preview
- Keybinding editor in settings modal: click-to-rebind, clear, reset-to-defaults
BoardThemeresource: board background and grid colors respond to dark/light mode- Storybook stories for all 16 game UI components with shared
_fixtures.tsmock data - Prettier config with
prettier-plugin-svelteandprettier-plugin-tailwindcss
Changed
- Settings modal fully wired: theme selector, gameplay toggles, team color pickers, overlay/opacity sliders, keybindings —
UpdateSettingdispatch handles all 16 keys (was a TODO stub) SetPhasedispatch blocked during PreGame, Deployment, and GameOver phases- Board grid and background driven by theme instead of hardcoded tan/black constants
- Header bar gains clickable score display and review-mode indicator
- Hotbar refactored to two-tier layout with phase flyout tool rows
- Left panel wound controls: ±buttons for multi-wound models, kill toggle for single-wound, leader wounds in purple
- Right panel timeline: round headers show VP totals, player turns have collapsible phase action details
- Landing modal army management widens for pregame config (leader attachment, transport/reserve assignment dropdowns)
Fixed
- Grid opacity used alpha-threshold heuristic to tell thick from thin lines — replaced with explicit
GridLineThickcomponent marker - Exiting chromakey mode restored hardcoded colors instead of current board theme
0.6.1 — 2026-03-24
Added
- Faction-based theming system with 7+ palettes (
palettes.ts), dark/light mode toggle, localStorage persistence <Kbd>component for keyboard shortcut display- Storybook v10 with palette sampler story and dark/light toolbar addon
- Vitest + Playwright browser-mode testing config
Changed
- Color tokens overhauled: neutral gray → teal accent, semantic colors (success/warning/danger), tiered panel surfaces
- Game UI panels use CSS custom properties instead of hardcoded colors
- Docs pages use
<Kbd>component instead of inline<kbd>elements - Header bar gains dark/light mode toggle button
Removed
- Hardcoded color values from game overlay components
0.6.0 — 2026-03-24
Added
- Svelte web overlay: all UI migrated from egui to Svelte 5 components rendered as DOM overlay on the Bevy canvas
- WASM↔JS bridge: Rust pushes game state as JSON to Svelte reactive stores; Svelte sends commands back via
UiCommanddispatch UiCommandenum (40+ variants) as single chokepoint for all UI-originated actions- Tailwind v4 theme with game UI tokens (panels, phases, move types, teams)
- Google Fonts: Barlow Condensed (headings), Barlow (body), JetBrains Mono (data)
DomHasFocusresource replaces egui keyboard focus detection- Svelte Toast with client-side 3-second latch timer (Rust pushes raw message, Svelte owns display timing)
- Svelte header bar with phase/round/player display, score button, pause button
- Svelte hotbar with tool buttons, hotkey badges, phase flyout (Svelte-owned open/close state)
- Svelte left panel with unit tree, per-model wound +/- controls, collapsible multi-model units
- Svelte right panel with timeline tree, view/revert buttons, end-turn/begin-battle/new-game buttons
- Svelte modals: landing (game setup + army management), pause, settings, save/load, changelog
- Army import in landing modal: textarea + import, parsed unit list with weapon/loadout text
- Pregame configuration: leader attachment, transport passenger assignment, reserve/deep strike per unit
- Deployment tree in left panel: per-unit Deploy buttons, Deploy All, three-section layout (Deployable / Transports / Reserves)
- Nicknames configurable per unit in landing modal pregame stage
- Timeline action details: collapsible phase sections with per-model action attribution
- Model suffixes (a), (b), (c) for duplicate model names — cross-references between left panel wounds and right panel actions
- Eye icon on completed turns for quick view/rewind
- Screenshot flash as CSS animation
- Standalone
web/index.htmlforbevy run web - Feature request and bug report buttons on docs site with GitHub issue forms
Changed
src/plugins/ui.rs(5,011 lines) split into module tree undersrc/plugins/ui/- "Begin Setup" closes landing modal without spawning — units deploy from left panel tree
- Unit tree: collapsible multi-model units with ▸/▾ indicators, single-model units get inline wound controls
- Attached leaders grouped with bodyguard as combined row (e.g., "Khârn + Berzerkers")
- Transport passengers shown indented under transport with → prefix in deployment tree
- Army management modal width increased (700→900px) for pregame configuration
- Canvas element gets
tabindex="-1"for programmatic focus return after overlay clicks - Docs/Discord links moved from landing modal to page footer
Fixed
- Transport assignment dropdown empty due to broken dedup filter comparing indices across different arrays
- Leader attachment not persisting across modal transitions (assignment field missing bodyguard UID)
- Leaders blocked from reserves/deep strike by overly broad guard
- Attached leaders and transport passengers deployable independently (now prevented in dispatch + UI)
- Bridge not detecting PreGameAssignments changes (missing
is_changedcheck) - Keyboard focus lost after clicking Svelte overlay buttons (canvas refocus via
requestAnimationFrame) wasm-opt --enable-allreplaced with individual feature flags for binaryen v128 compatibility
Removed
bevy_eguidependency removed entirely — WASM binary 11% smaller (27MB → 24MB)- All egui draw systems deregistered and dead code deleted
- Nav header removed from game page
0.4.0 — 2026-03-22
Added
- Tournament mission selector (CA 2025-26): single dropdown in Game Setup selects letter A-T, auto-fills deployment pattern and primary mission
- Recommended terrain layouts highlighted with
*when a tournament mission is selected - Hidden Supplies missions (F, I, N) render 6 objectives with the center split into two offset positions
- Objective positions added to all 6 deployment patterns (previously only Tipping Point had them)
- Native release packaging: macOS .app bundle, Windows portable .zip, Linux .tar.gz + .deb — attached to GitHub Releases on tag push
- Website download links: docs page fetches latest release from GitHub API with per-platform download buttons
- Discord release notifications now include download link
- Unit nicknames: assign custom names per unit in pregame to distinguish identical squads (e.g. "Left Pack", "Midfield Hounds")
- Weapon loadout display: Listforge weapon text preserved from army list and shown per model variant when "Loadouts" toggle is checked in pregame
- Nicknames display everywhere: board labels, tool confirmations, datacard, wound tree, transport panel, action log
Changed
UnitBase::display_name()andUnitBase::label()helpers for nickname-aware display across all UI
Fixed
- Parser now handles nested
•sub-bullets (Demons format) and◦sub-bullets (World Eaters format), capturing weapon loadouts from sub-items - Flat weapon-only lists (Chaos Knights / dogspam) now aggregate all bullet items as the fallback loadout string
- Nickname editor moved from left deployment panel to pregame modal (Stage 2) where it belongs
0.3.3 — 2026-03-21
Added
- Header bar with phase info (left) and centered score summary (clickable)
- Score flyout window for detailed scoring (primary VP, secondaries, objectives); open via click or O key
- O key toggles score flyout
Changed
- End Turn button moved to bottom-right of timeline panel (large, pinned, Civ-style)
- Score editing removed from timeline panel sidebar; now lives in the flyout
0.3.2 — 2026-03-21
Fixed
- Annotations silently lost on save/load: save now shows a toast warning when annotations exist
- "Clear All Annotations" button mislabeled — renamed to "Clear Annotations" since it only clears current turn
- Stale comments in timeline.rs referencing old "half-turns" and "current phase" terminology
0.3.1 — 2026-03-20
Added
- Notes and pencil strokes are now tracked per player turn: annotations created during a turn are frozen on turn end, shown/hidden on timeline rewind, and restored on truncate
0.3.0 — 2026-03-20
Added
- Scoring actions logged to timeline: primary VP awards, secondary card draws, awards, achieves, and discards appear in the timeline tree under the phase where they occurred
- Historical score display: viewing a completed player turn shows that turn's score snapshot in the Score section instead of live scores
Changed
- WH40K terminology aligned:
TurnNode→BattleRoundNode,PlayerHalf→PlayerTurnRecord,halves→player_turns,turn_number→round_number PhaseState+TimelineState.current_viewconsolidated into singleTimelineCursorresource withhead: TimelinePosition,view: CursorView,active_player: PlayerEndTurnevent renamed toEndPlayerTurn; "End Turn" button relabeled to "End Player Turn"- Scores now captured per player turn (on
PlayerTurnRecord) instead of per battle round; prevents second player overwriting first player's snapshot SnapshotScoresnow includesSecondaryDeckStatefor both players — secondary deck state restored on truncateon_truncaterestoresMissionState,ObjectiveControlState, andSecondaryStatefrom the reverted player turn's snapshot; deployment revert resets all scores to default- Timeline tree always shows all 5 battle rounds (future rounds grayed out) and "pending" label for the waiting player
EndPlayerTurnguarded against firing during PreGame or non-live view- Right panel header redesigned: shows Battle Round, player's turn, and current phase as label; PreGame shows player's scout moves; GameOver shows final scores
- PreGame buttons reordered: "Next Player" first, "Begin Battle" second (was "Begin Turn 1")
Fixed
- Scores not restored on truncate: reverting to an earlier turn left VP totals from the later state intact
- Score snapshots overwritten per battle round: first player's scores were replaced by second player's because
TurnNode.scoreswas set on everyEndTurn EndTurncould fire from PreGame, corrupting timeline withturn_number.max(1)pushing into wrong node- "Attacker (pending)" shown in timeline tree after Attacker already completed pregame; condition checked player turn count instead of whether the specific player had a record
0.2.1 — 2026-03-20
Added
- Mission management: primary missions (10 types), secondary cards (19 types), VP scoring with round caps, per-round score snapshots
- Objective control: per-objective state cycling (Neutral/Attacker/Defender/Contested), OC tally bars on objective markers, 3" range rings
- OC stat on unit bases from datasheet lookup
- Demo production script (
demo/script.md)
Changed
- Timeline panel redesigned: flat snapshot list replaced with Turn → Player → Phase → Action tree hierarchy
- Timeline data model restructured from
Vec<TimelineSnapshot>toTurnNode/PlayerHalf/PositionSnapshottree - Turn separators show inline scores (ATK: N | DEF: N) when available
- Live section has warm background tint; current phase highlighted with gold marker
- Save format bumped to v8 with tree-structured timeline; v1–v7 saves auto-migrate on load
RewindToSnapshot(usize)/TruncateToSnapshot(usize)replaced with typedRewindTo(TimelineView)/TruncateTo(TimelineView)events
Fixed
- Faction-blind datasheet lookup returned wrong stats for cross-faction units (e.g. Chaos Spawn got CSM M8"/OC0 instead of WE M10"/OC1); army list faction is now extracted and threaded through all lookups
- Save/load round-trip lost faction-aware stats:
spawn_saved_unitspassedNonefor all faction hints, so pre-fix saves with wrong baked-in movement/OC were never corrected on reload; now extracts faction from saved army list text and re-derives movement + OC from database - Blob member labels re-appeared after turn advance because
sync_killed_overlayunconditionally set non-killed labels toInherited, overriding blob-inducedHidden - Mid-turn save snapshots no longer produce indistinguishable "Live — Command" labels on reload; live state saved as separate
live_actions/live_positionsfields
0.2.0 — 2026-03-18
Added
- Off-board staging area: reserve and embarked units now visible in labeled columns beside the board (Defender left, Attacker right) instead of hidden at origin
- Click-to-deploy: clicking a staging unit auto-switches to Deploy Reserves tool with that unit pre-selected
- Reserve arrival zone overlay: 6" edge strips (Strategic Reserves) and 9"/6" exclusion rings (Deep Strike) around enemy models, toggled in settings or auto-shown with Deploy Reserves tool
- Transport system:
IsTransportandInTransportcomponents for embarking/disembarking units - Embark tool: two-click flow (click unit → click transport) to embark a unit into a transport
- Disembark tool: click transport → auto-place passengers in 3" ring using formation spawning, with emergency disembark fallback (6" ring, models destroyed if unplaceable)
- Deploy Reserves tool: select a reserves unit from sidebar, click board to place; transports deploy passengers alongside
- Enter Reserves tool: click a board unit to send it to Strategic Reserves or Deep Strike
- Exclusion range toggle (9"/6") in the Deploy Reserves hotbar context for Rapid Ingress scenarios
- Save/load support for reserves and transport state (save version 6, backward-compatible with v5)
- Two-stage landing modal: Stage 1 (Game Setup) for terrain/deployment/first player, Stage 2 (Army Management) for import/transports/reserves
- Pre-game transport assignment: transports auto-detected from Datasheets.json, embark passengers via ComboBox in Stage 2
- Pre-game reserves assignment: per-unit Board/Strategic/Deep Strike selector in Stage 2, applied at lock time
- Transport detection from datasheets:
unit_has_transport()method on BaseDatabase,IsTransportcomponent auto-inserted on spawn - Comprehensive 10e core rules reference update: Transports, Deep Strike, Scouts, Infiltrators, Stratagems, Terrain, Aircraft, Attached Units, and more
Fixed
- Staging area section labels ("In Rhino", "Strategic Reserves", etc.) were oversized and overlapped unit models; reduced label size and increased spacing
0.1.0 — 2026-03-17
Added
- Selection-driven LOS: selecting units scopes the danger region to those units; per-player team-colored overlays replace the single red region
- Pill visibility toggles in Settings: per-indicator-type checkboxes (ADV, FLB, ACT, SHOCK, FP, COH, SCT, INF, STA, CHG, SHT, LOW, LDR)
- Drag cancel: Escape during a drag snaps units back to pre-drag position without deselecting
- Tool-to-phase auto-sync: switching tools via keybind auto-advances GamePhase when the tool requires a different phase
- Async folder picker on native (no longer freezes the app while the OS dialog is open)
- Unit datacard embedded in right panel with click-frame gate to avoid wgpu deadlock
- Wound tree click-to-focus: clicking a unit name targets that unit in the active tool
- Blob grouping: clustered same-unit models (within coherency distance) share a single centroid label and merged badge row; individual labels hidden while in a blob
Changed
- Camera default view expanded to include staging columns on both sides of the board
- Embarked/reserve units no longer hidden — they're visible in their staging column, making army state transparent at a glance
- LOS system simplified: removed source dot markers, candidate markers, and staged analysis flow in favor of direct async task per player
- Wound tree redesigned: right-anchored +/- controls, clickable truncating names, single-model units render flat without collapsing header, group header uses bodyguard name
- Weapons table in datacard switched to compact vertical layout (one weapon per separator block)
- Left/right panels use fixed-width with non-resizable dividers and solid scrollbars
- Ring entities (Shoot, Charge, Measure) pre-spawned as hidden; toggled on click instead of spawned/despawned
- LDR badge only shown post-lock when leader is actually attached
- Snapshot labels no longer include phase icon; Live label omits phase name
- Collapsing headers in wound tree, snapshot list, and action log default to collapsed
Fixed
- Post-lock tool click hang:
sync_focused_unitwritingFocusedUniton the click frame triggered wgpuTexture::dropdeadlock. Removed the resource; datacard computes focus inline with a click-frame gate - Wound tree click hang: same deadlock via direct
FocusedUnitwrite. Wound tree now writes tool state instead - Escape during drag deselected units instead of cancelling the drag
- Phase didn't advance when switching tools via keybind (e.g. pressing K for Kill didn't switch to Fight phase)
- Blocking
rfd::FileDialogfroze the native app while OS folder picker was open - Rotation (R key, scroll wheel) blocked during deployment unless in a movement tool; now allowed in any tool when timeline is unlocked
Removed
FocusedUnitresource,sync_focused_unitsystem,FocusedTriggersSystemParam,focused_unit_needs_updaterun conditiondraw_unit_datacardstandalone system (inlined intodraw_right_panel)warm_egui_atlasstartup systemSelectedUnitForAnalysis,SelectedSourceEntity,SelectedCandidate,UnitAnalysisStateresources- Source dot markers, candidate dot markers, staged analysis flow,
apply_unit_fade,handle_source_clicksystems handle_analysis_clicksystem (LOS now reacts toSelectedcomponent changes)show_source_pointssetting from VisibilityOverlaySettingsscaffolds/blob-grouping.mdplanning doc (implemented)
2026-03-15
Added
- Per-unit facing/rotation for Hull and non-circular Oval bases
- R key: 15-degree clockwise snap (Shift+R for CCW)
- Scroll wheel: 1-degree fine rotation when non-circular unit selected
- Facing indicator triangle on front edge of non-circular bases
- Pivot cost charged once per phase: 2" for Monster/Vehicle, 1" for others (matches 10e rules)
- Rotation persisted in saves (v4), timeline snapshots, and rewind/truncate
- Formation commands reset rotation to 0
- Monster/Vehicle keyword detection via
Datasheets_keywords.json— drives pivot cost tier - Note tool: font size slider (8–48, default 12) and Enter-submits toggle (Shift+Enter for newline)
- RangeRing tool hotkey removed (R freed for rotation; rebindable in settings)
- Leader attachment: leaders attach to bodyguard units with dashed gold connector and datacard UI (
0cf2d64) - Double-click to select all living models in a unit (
eef06a5) - Formation tools: keyboard shortcuts (1/2/3/4) for grid formations, C for coherency pack (
b6a4993) - Manual click/drag replacing all
Pointer<*>events on unit entities (b6a4993)
Fixed
- Deployment click hang:
MeshPickingPluginraycasting unit bases caused wgpu deadlock — setPickingBehavior::IGNOREon all unit base entities (b6a4993) - All post-lock click and drag handlers converted to manual hit testing via CPU-side point-in-shape (
b6a4993)
Removed
- All
Pointer<Click>/Pointer<Drag>/Pointer<DragEnd>usage for unit entities (b6a4993)
2026-03-14
Added
- Below half strength tracking with red "LOW" badge and datacard model count
- Dev style panel for live-tuning visual constants without recompiling
- Formation-aware spawn: units spawn in compact 2-line formations instead of grid scan
- Note tool (N) and Pencil tool (P) for board annotations
- Shooting/stationary/charged annotations with STA, CHG, SHT badges
- Fall back badge ("FLB") on units that fell back this turn
Fixed
- Click hang on dense formations: all click handlers now process only first valid event per frame and drain the rest
- Coherency check and objective ring color gated on change events instead of running every frame
- Mixed-variant units (e.g. Bloodletters + Bloodreaper) no longer spawn as separate clusters
2026-03-13
Added
- Getting started guide with sample army lists (Chaos Daemons + World Eaters) (
73e2a98) - Configurable keybindings, team colors, file locations, streamer mode (F11) in Settings (
1a2d9f1) - Egui baseline style system with larger text, wider spacing, rounded corners (
73e2a98)
Changed
- Landing modal redesigned with centered layout (
73e2a98) - Status toast centered and auto-dismisses after 3 seconds (
1a2d9f1) - Killed unit overlay: skull glyph at 15% alpha, ring/label hidden (
1a2d9f1) - Analysis tab removed from left panel — LOS tool replaces it (
1a2d9f1) - Enforce max movement setting moved to Settings tab (
1a2d9f1)
Fixed
- Alpha rendering: 15
ColorMaterialinstances with alpha < 1.0 defaulted toAlphaMode2d::Opaque— switched to explicitBlend(1a2d9f1) - F12 key conflict (screenshot vs streamer mode) — streamer mode moved to F11 (
73e2a98) - C key conflict (coherency vs charge) — charge hotkey changed to G (
73e2a98)
2026-03-12
Added
- Save/load persistence: full game state with gzip compression, autosave, JSON export/import (
62e1b9f) - Landing modal with terrain/deployment config, army import, autosave restore (
7177bd8) - Pause menu (Escape) with save, load, settings, new game (
7177bd8) - Wound tracking with per-model +/- controls in post-lock Army tab tree view (
777ee78) - Dead unit behavior: immovable, untargetable, skull overlay, no OC/pills, kill toggles unkill (
a42001b) - SvelteKit + TailwindCSS frontend replacing static HTML docs (
25b93e9) - CI workflow:
cargo test+ WASM check gate deploys (fd397ed) - Playwright WASM smoke test (
a340368) - Cloudflare Pages deployment replacing GitHub Pages (
4503379) - Live drag preview arrow with distance label during movement (
62e1b9f) - DevMode resource (backtick toggle) gating terrain editor (
62e1b9f) - PNG screenshot capture on F12 — native saves to disk, WASM triggers download (
2c5e232) - Settings promoted to top-level tab (
92f8eab) - Changelog page on docs site (
4dae660) - Hull base dimensions: per-vehicle rectangular sizes, save migration v3 (
45cbc4d) - README with getting started and architecture overview (
d51183d)
Changed
- Save format stores layout/pattern IDs instead of full structs (
62e1b9f) - CI and deploy merged into single workflow (
afbca6e) - Duplicate UI removed: GameConfig tab, redundant army import, first-player toggle in right panel (
7177bd8)
Fixed
- Right-click hang on circular bases: child entities missing
PickingBehavior::IGNOREcaused duplicate drag events (8ea1fe0) - WASM startup crash:
std::env::set_varpanics on wasm32 since Rust 1.91 (a340368) - WASM control-flow exception displayed as load error — wasm-bindgen "throw to escape" is normal (
aeb1e33) - WASM build failure: missing
js-sysdependency in Cargo.toml (fd397ed) - Save/load didn't restore army list UI state — sync was one-directional (
fd397ed) - wasm-bindgen CLI version mismatch in CI — now extracted from cargo metadata (
83ce109) - WASM binary name mismatch: hyphens vs underscores in build paths (
afbca6e) - Terrain debug dots visible on startup: sync only ran on resource change, not on entity spawn (
3e6a147) - Scroll over egui panels leaked to camera zoom — system ordering race with
is_pointer_over_area()(fd397ed)
2026-03-11
Added
- Terrain layout editor: click to select, drag to reposition, R for rotation, Delete to remove, JSON export (
2e5ec18) - GW terrain layouts 2-8 for Chapter Approved 2025-26 (
ee9526d,8ecf0e5,4c9cf8e) - Scout moves with PreGame phase, 9" proximity warning, SCT badge (
1d41966) - Infiltrate with INF badge during deployment (
1d41966) - Spawn overlap prevention with drag visual feedback (red tint on invalid position) (
1d41966)
Fixed
- Pill badge overlap: layout now uses actual pill widths instead of fixed center-to-center spacing (
1d41966) - Pill badge flicker: visibility/transform writes guarded behind change checks (
1d41966) - Unit spawn overlap: positions now checked against already-spawned units (
1d41966)
2026-03-10
Added
- Multi-select: box select, shift-click, group drag, formation keys (1/2/3), coherency pack (C) (
fae9d6b) - Coherency indicator (COH badge) for out-of-coherency models (
fae9d6b) - Unit datacard panel showing stats and weapons table (
fae9d6b) - Footprint badge ("FP") for units standing on terrain (
fae9d6b) - Camera controls: zoom (scroll), pan (middle mouse), rotate (Q/E), reset (Home) (
fae9d6b) - Chromakey mode for OBS overlay, opacity sliders for grid/zones/rings (
fae9d6b) - Bowtie formation and unit group convex hull outlines (
fae9d6b)
Changed
- GamePhase converted to Bevy State with OnEnter/OnExit scheduling
- Phase/tool icons switched from Unicode symbols to 3-letter text (Cmd, Mov, Sht, etc.)
- Hotbar docked to bottom via TopBottomPanel instead of floating window
Fixed
- Selection rect and measure tool offset from cursor: viewport vs window coordinate mismatch (
fae9d6b) - Box select triggered when clicking large-base units — now uses actual base shape radius (
fae9d6b) - "Revert here" removed the wrong snapshot and didn't restore phase state (
fae9d6b) - Formation spacing too wide: was using coherency distance instead of touching distance (
fae9d6b)
2026-03-09
Added
- Pastebin URL support for army list import (
8564a74) - Pill badge indicators (ADV, ACT, SHOCK, FP) as colored capsules with unified stacking (
8564a74)
Fixed
- Parser rejected Listforge v1.48.0 format:
(XX points)vs(XX pts)(8564a74) - Duplicate-named units merged in deployment panel — now keyed by
unit_id(8564a74) - Left panel text overflow on long unit names (
8564a74)
2026-03-08
Added
- Battleshock and action tools with SHOCK/ACT badge indicators
- Clipboard paste working in WASM (
prevent_default_event_handling: false)
Fixed
- Army tab buttons (Add/Remove/Clear) available after deployment lock — now hidden (
timeline.locked)
2026-03-04
Added
- Tool palette: ActiveTool as Bevy States with per-phase tool sets and default tools
- MoveType enum driving arrow color and dash style
- PileIn/Consolidate 3" max distance enforcement
2026-03-03
Added
- Timeline system for game state snapshots
- Movement planner with drag-based unit movement
- Visibility/LOS analysis system
- Army list parsing (Listforge format + Datasheets.json)
2026-03-02
Added
- Unit spawning and drag-and-drop placement
- Range rings, deployment zones, board and terrain rendering
2026-02-26
Added
- Initial project setup