Changelog

What's new in Shadowboxing

Launch App →

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.json the 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 just recipes, 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 wh40kdc path 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 wh40kdc path dependency 0.5.7 → 0.5.11 (the build re-locks to the local 40kdc-data checkout): 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 both Dataset.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 same PrimaryAward timeline action. Persisted in save v14 (lenient: a pre-tick primary shape 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_matchups matrix (full 25-pair coverage pinned by test) via a pure, reusable resolve_matchup_mission helper — 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 from Dataset.force_dispositions.all() (wh40kdc ≥ 0.5.3). All landing-modal <select> elements converted to Svelte 5 value={} controlled pattern. Detachment→disposition auto-set wired (auto_set_disposition_from_detachment, no-op until GW publishes the mapping and 40kdc-data backfills it). (pending commit)
  • 10e mission enums removed with Phase 3: the 10-variant PrimaryMission (+ static award tables) and the 20-variant TournamentMission (A–T), plus ActiveTournamentMission, the dead ObjectiveOverride Hidden-Supplies override (no reader since objectives became terrain footprints), the SetTournamentMission/SetPrimaryMissionConfig commands, and the tournament dropdown. Recommended-layout ★s re-key off the active deployment pattern's recommended_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 in site/src/lib/data/dataset.ts). The bridge carries only ids + dynamic state: DatacardData shrank to datasheet_id + loadout-filtered weapon ids (+ raw-name fallback so an unresolved import still renders a card), WeaponOptionJs to a weapon_id, and Datacard.svelte / ShootToolPanel.svelte resolve 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. New just data-sync recipe 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, and sync_auto_rings. 5 new tests pin the ordering/fallback contract.
  • 11e objectives are terrain footprints. An objective is a terrain area (is_objective piece, optional objective_role home/center/expansion); its footprint — spanning the link group — is the OC-control boundary. sync_objective_oc_tally now sums a unit's OC into an objective when any of its models' bases touch the footprint (model_in_area over 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). ObjectiveControlState is re-keyed by anchor piece id ({auto, overrides}); markers spawn at objective-area centroids on terrain load. The controls-objective suggestion predicate gained its qualifiers — exclude:home, objective_role (reconciling the card DSL's central to terrain's center), objective:opponent-home, scope:enemy-territory — backed by per-objective ObjectiveFacts, no longer Unknown. New army_list::terrain_layout bridge 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 (BeginCardActionApplyCardAction) marks a terrain area, storing a TerrainTag {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: never persists, e.g. Plunder). On top, a pure ECS-free evaluator (src/card_eval/) reads a BoardEvalContext snapshot and reports which award when conditions look achievable, surfaced as a non-blocking "suggested" hint in the score flyout (AwardOptionJs.suggested/suggested_count, keyed off a new AwardSuggestions resource) — 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-turn DestroyedLog + TurnStartMembership), destroyed-while-on-objective, and engagement-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. Reuses unit_on_footprint/point_in_shape/base_fully_in_zone for membership and consumes territory from Dataset.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 SecondaryCard enum + static award tables are gone; cards are SecondaryCardId(String) resolved against wh40kdc's Dataset.mission_cards, and an active card stores only ticked: 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 on per/per_max awards (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, plus link_group_pieces() / link_group_anchor() helpers in src/types/terrain.rs that 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) -> BaseShape derives a model's base from the wh40kdc crate — round/oval/flying/hull straight from Unit.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 stable unit.id slugs (the id-keyed overrides run ahead of the crate lookup; the crate's flat Hull/FlyingBase shapes are the fallback). Plus stat_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 wh40kdc path 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 wh40kdc path 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, the was-hit-by-attack combat-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 old DetachmentDatabase lookup 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 wh40kdc path dependency 0.3.3 → 0.4.0, which ships the base_size_mm backfill (993/1183 units populated) and renames the base-size API (UnitBaseSizeMm/UnitBaseSizeMmShapeBaseSize/BaseSizeShape, now with FlyingBase/Hull/Unique variants). Round/oval derivation is no longer blocked; the round_base_from_dataset test is enabled. (pending commit)

  • Rewrote army-list import (import_list) to consume the crate's try_import_roster instead of the hand-rolled Listforge/NewRecruit parsers. Each roster unit maps to one ArmyUnit carrying its resolved datasheet_id + a structured Vec<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) off BaseDatabase: OC, movement, and the Monster/Vehicle flag now re-derive from the crate via the model's stored datasheet_id (falling back to a name lookup for older saves). Added shared dataset-query helpers in army_list::base_shape (resolve_unit, profile/keyword/weapon accessors) used across readers. (pending commit)

  • Bumped the wh40kdc path dependency 0.4.0 → 0.4.3, which lands the 11e card-runtime data: 0.4.1 ships Dataset.deployment_patterns[].territories + per-piece objective roles + terrain layouts; 0.4.2 adds the authoritative scoring-DSL $comment spec, redefines engagement-fronts as table quarters, reconciles the terrain-tag enum docs, and adds a when_drawn.battle_round first-round-redraw signal; 0.4.3 adds the headerless plain-text import adapter (below). (pending commit)

  • 11e victory-point caps: PlayerScore::total_capped now 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-final build 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's push_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 wh40kdc crate's 11e terrain layouts and deployment patterns directly: TerrainLayouts is built from Dataset.terrain_layouts (via Dataset::resolve_terrain + objective-flag rehydration) and deployment zones from Dataset.deployment_patterns, replacing the in-repo asset include_str!s. Layouts with no objective area render as "(draft)". The default board is take-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.rs wholesale, including the fuzzy name-resolution helpers normalize_punct/resolve_model_name/strip_epic_hero_suffix and the hull-geometry tables superseded by base_shape.rs), the eight embedded JSON databases (assets/Datasheets*.json, Stratagems.json, Enhancements.json, Detachment_abilities.json), their include_str! blocks, and the bridge's filter_weapons_by_loadout. Also removed the orphaned assets/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 embedded wh40kdc dataset. (pending commit)
  • In-app terrain editor removed (terrain_editor.rs, its events/resources, the ActiveTool::TerrainEdit tool, bridge command parsing + TerrainEditToolData export, and the Svelte TerrainEditToolPanel + 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, and assets/deployment-patterns.json deleted — all layout + pattern data now comes from the crate (11e-only; 10e lives on the 10e-final build). ObjectiveRangeRing deleted: 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_edges derived 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 real corner-ruin-right geometry. (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/pipe default_blocking corrected to true in 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: true for every feature instead of honoring the template's default_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_tally had no run_if (the original center-distance version ran unconditionally); the footprint-membership rewrite adds an objective_tally_dirty gate (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's try_import_roster, whose adapters all require a framing header — the gw adapter needs the + FACTION KEYWORD: fence, newrecruit-simple needs # ++ Army Roster ++ with [N pts] brackets — so headerless (N Points)/(N pts) paren lists matched no adapter. Fix (upstream, wh40kdc 0.4.3): new gw-headerless FormatAdapter that 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 : wargear or deeper child bullets, else wargear), declining fenced/++-header/WTC input so framed adapters still win. The app's gw_headerless_text_export_is_currently_unsupported test is converted to gw_headerless_text_export_imports (asserts world-eaters.txt resolves 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. New variant_key and loadout fields threaded through SpawnUnitUnitBase, recomputed on save/load so pips stay stable across sessions.
  • Shooting tool weapon list is now per-model: the picker filters weapons_for_unit by the selected shooter's loadout text, 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 reuses normalize_punct (now pub) for punctuation/case-insensitive bidirectional contains.
  • Straight-line pencil tool (ActiveTool::StraightLine, default hotkey I, labeled "Line"): two primary clicks commit a PencilSegment between the two clicked points. Reuses the existing SavedAnnotation::PencilSegment serialization 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 ToastNotification naming 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. Previously SavePregameAsConfig ran silently while the symmetric SaveArmyConfig (library-edit modal footer) already toasted, so users were never sure the button did anything. save_pregame_as_config now returns Option<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 the Home key. New UiCommand::ResetCameraRotation and UiCommand::ResetCameraView variants plumbed through the bridge; new reset_camera_rotation helper alongside the existing reset_camera_to_default in src/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 existing UnloadArmyConfigForPlayer dispatch path, so attacker and defender can be cleared independently. Wired only via the new optional onclear prop, so the button stays out of contexts (e.g. ArmyLibraryModal edit view) where clearing isn't meaningful.

Changed

  • Unit name labels render at 85% of the previous size (new NAME_LABEL_SCALE_FACTOR constant) 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_json in src/army_list/newrecruit.rs:122 had a fallback that hardcoded count=1 when a type: "unit" selection had no type: "model" children, ignoring the unit's own number field. Fix: use sel.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 Dishonoured from Jakhals and displayed Chaos Spawn with the wrong faction's datacard (CSM M=8" OC=0 instead of WE M=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 , / and before applying the count-prefix regex, so each role becomes its own model row in Datasheets_models.json. Re-ran extraction; broadly safer for any squad with a single combined-composition entry. (2) Resolver hard-fail (BaseDatabase::ids_for_faction in src/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 a drop_warnings toast naming the unit and faction hint. (3) Faction-id threading through ArmyUnitSpawnUnitUnitBaseBoardUnit: import was already faction-aware (correct stats wrote into ArmyUnit) but the runtime datacard / weapons / abilities / keywords queries in src/platform/bridge.rs and src/plugins/units.rs passed None as 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 stored faction_id. (4) Snapshot test guard (world_eaters_sample_list_imports_clean in src/plugins/ui/left_panel.rs): pins the contract end-to-end (Chaos Spawn M=10" OC=1, three distinct Jakhals model rows, no concatenated composition strings, every ArmyUnit.faction_id populated), so future data refreshes / parser tweaks / resolver changes / new construction sites that re-break this fail at cargo test.
  • Eldar Corsair Voidreavers imported as only the Felarch (4 of 5 models silently dropped). Root cause: resolve_model_name in src/army_list/base_lookup.rs stripped the " w/" loadout suffix but not the verbose " with " form, so Voidreaver with Shuriken rifle never matched the canonical Corsair Voidreaver. left_panel.rs:81 then 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 Spear did not resolve to the DB's Warlocks canonical, 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) while Datasheets_models.json is typeset with U+2019 ('), so str::to_lowercase() comparisons failed; and the DB names Ri'Lantar with a " – EPIC HERO" en-dash suffix that the word-boundary prefix match couldn't bridge. Added normalize_punct (ASCII-folds U+2018/U+2019/U+2010/U+2011/U+2013/U+2014) applied at both index-build and lookup sites, plus strip_epic_hero_suffix run at index time so Ri'Lantar – EPIC HERO indexes as ri'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'vre in the list (ASCII ') did not match the DB's curly ', so all three model variants failed validation. Resolved by the same normalize_punct fix.

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 into FEATURE_TEMPLATE_NAMES, TEMPLATE_NAMES, and create_template in src/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 in src/lib.rs so 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_inches called .parse::<f32>() on the raw datasheet string, but ranges are stored with a trailing inch mark ("12\""), so every parse returned None. sync_shoot_rings skipped all weapons (empty pool → no rings) and check_weapon_range treated 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_positions in timeline.rs had asymmetric visibility logic — it set Visibility::Hidden when 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 all ShooterRangeRing entities on friendly click) and sync_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 from handle_shoot_click; sync_shoot_rings is 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_rings was a no-op previously because active_count never 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. New RangeRingToolData bridge type, SetRangeRingVisibility/ClearRangeRingUnit UI commands, RangeRingToolPanel.svelte registered in GameOverlay.svelte.
  • CI smoke-test job on PRs: builds WASM + SvelteKit, serves site/build/ statically via Python's http.server, runs just smoke-test (Playwright WASM boot). Blocks deploy. site/** removed from paths-ignore in ci-deploy.yml so Svelte PRs also re-run the wasm-bindgen sync verification. playwright.config.ts switches its webServer command based on CI so local dev still uses bevy run web. (d6198a9)

Changed

  • DOM-focus keyboard gating centralized behind a KeyboardGated SystemSet in src/lib.rs, configured once with run_if(|dom: Res<DomHasFocus>| !dom.0). Fifteen keyboard-input systems across camera/selection/terrain_editor/units/ui/keyboard/ui/save_load/ui/header now use .in_set(crate::KeyboardGated); their per-function dom_focus: Res<DomHasFocus> params and if 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.svelte drops 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 sets DomHasFocus while 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::SelectReserveUnit event, 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 existing DeployFromReserves flow.
  • Detachment data: embed Detachment_abilities.json, Stratagems.json, and Enhancements.json from game-datacards. New DetachmentDatabase resource 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 DetachmentInfoJs through LandingData using extract_detachment() on list headers plus DetachmentDatabase lookups.

Changed

  • drain_js_commands in bridge.rs now logs log::warn! for malformed JSON, unknown command types, and known types with bad payloads — previously all three were silently dropped.
  • sync_dom_focus now gated with run_if(dom_focus_dirty) — a new atomic flag flipped by JS calls to set_dom_has_focus. Complies with the CLAUDE.md rule that every sync_ system must have a run_if gate.
  • extract_from_game_datacards.py now reads baseSize from upstream datasheets (was hardcoded to empty) and synthesizes per-model rows from the composition field 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's id_to_faction map 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> left DomHasFocus=false, so Bevy keyboard systems ran unguarded against the typed keys. Fixed by the textFieldFocus action applied to all text fields under site/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 crossed Rectangle mesh 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_blocked and on_drag_end_commit included killed units (and even retired/hidden ones) because the unit_snapshot builder had no is_killed filter. 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.ico at compile time via include_bytes! and sets it on all winit windows. Web: site/static/favicon.ico served at /favicon.ico, referenced in app.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 = true timeline 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() in bridge.rs had 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_declared was never set to Some(true). Root cause: the declaration confirmation step (bridge command + Svelte panel) was not implemented. Added DeclareCharge/CancelCharge/RemoveChargeTarget UiCommands, ChargeToolData bridge type, and ChargeToolPanel.svelte to 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_detachment used a case-sensitive pts_re_like check; 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 fist bullets; the text parser accumulated model_counts["Helbrute fist"] = 2, which prefix-matched to canonical "Helbrute" and produced ArmyUnit { count: 2 }. Fix: cap effective_count to 1 in import_list when is_monster_or_vehicle is 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: u8 field on UnitBase, tracked through snapshots, save/load, branch switching, rewind, and undo/redo
  • SetFloor event + 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 files
  • UndoCommand::SetFloor with full undo/redo support including distance reversal
  • FLOOR_HEIGHT_INCHES constant (3.0") in constants.rs
  • floor field added to BoardModel bridge 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)
  • ViewingFloor resource + sync_floor_filter system: viewport floor filter dims non-matching models to 30% opacity, controlled via UiCommand::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_load test failed on CI: assertion checked units[0] by array index, but Bevy query iteration order differs across save/load boundaries due to archetype reordering (IsLeader splits 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: u8 field on TerrainPiece — 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, sends SetViewingFloor command
  • available_floors and viewing_floor in top-level game state bridge for Svelte consumption
  • UpdateTerrainPiece now accepts floor field 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, PlaceTerrainPreset commands now parsed from JS
  • Stub layout gw-11e/crucible-of-battle.json for 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.js unresolved import. Vite 8's vite:import-analysis plugin and Rolldown reject static import() 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)
  • TestHarness extended with split_unit, unsplit_unit, unit_splits, army_units methods
  • UnitSplits resource 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)
  • BoardUnit bridge type now exposes transport_id so 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
  • TestHarness extended 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 coverage and just coverage-lcov recipes for cargo-llvm-cov code 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
  • TestHarness extended 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 builtin flag 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)
  • TestHarness DSL 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 proptest 1.10→1.11 (f653727)
  • Bump dirs 5→6 (510948c)
  • Bump ts-rs 11→12 (d104be0)
  • Bump geo 0.28→0.32; fix Simplify API change (a1862c7)
  • Bump @types/node 22→25, typescript 5.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 ehttp dependency (a1862c7)

0.8.6 — 2026-03-27

Added

  • Headless test harness: lib/bin split (src/lib.rs) with register_game() for shared app setup (8cd5da3)
  • build_headless_app(): windowless Bevy app using DefaultPlugins minus WinitPlugin for off-main-thread testing (8cd5da3)
  • TestHarness struct 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 + MoveUnitCommand event for programmatic unit movement without drag simulation (8cd5da3)
  • on_move_unit_command system: delegates to commit_position() directly, no drag validation (annotation-first) (8cd5da3)
  • MoveUnit JSON 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.rs reduced from 375 to 80 lines; all game registration moved to register_game() in lib.rs (8cd5da3)
  • Dispatch SystemParam bundles (UnitWriters, TimelineWriters, etc.) widened from pub(crate) to pub for lib/bin visibility (8cd5da3)
  • UiState widened from pub(crate) to pub for same reason (8cd5da3)
  • commit_position and active_tool_to_move_type in units.rs widened to pub(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_config platform functions
  • SavedArmyConfig / SavedUnitConfig data model with name+occurrence keying for stable configs across re-imports
  • LibraryEditState embedded in UiState for library editing without exceeding Bevy's 16-param system limit
  • 17 new UiCommand variants 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 the name field 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 in formation.rs and selection.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 comparisons
  • UndoCommand::entity() and UndoCommand::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
  • ActionDetailJs now carries original_index and entity_bits for 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_dirty and tick_autosave both guarded on timeline.locked, so in-progress deployments were never captured
  • Undeployed units dropped on save/load: placed counts reset to 0 when army text was re-parsed on load; added reconcile_placed_counts system that patches ArmyUnit.placed from spawned ECS entities after load
  • Nicknames lost on load: reconcile_placed_counts also restores nicknames from ECS entity data
  • Indicator pill ordering: sync_indicator_pills now runs .after() kill/action/battleshock/shot/wound systems so pills reflect current-frame state
  • sync_unit_strength ordering: 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 Reshuffle action)
  • Per-save download button in Save and Load modals (ExportSpecificSave event)
  • 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 — LabelCardBackground was missing from sync_unit_rotation counter-rotation filter
  • Model turning yellow-orange during pregame: ProximityWarning tint persisted after drag end; now removed on drag commit and on OnExit(Deployment)
  • Movement distance labels doubled after dropping unit: cleanup_drag_preview now checks drag_state.target (cleared immediately) alongside deferred IsBeingDragged removal
  • Reserve staging labels duplicated: RemovedComponents events 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 BranchTag so 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)
  • UndoPlugin with apply_undo and apply_redo systems, gated on on_event + timeline.locked && cursor.is_live() (b278b06)
  • UndoCommand enum 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() in on_truncate and on_begin_game ran before the timeline.locked guard, silently destroying undo history on rejected events (4d3c1f5)
  • remove_last_action_for_unit return value was ignored in MoveUnit, KillUnit, ShootAnnotate, and UnkillUnit undo 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 into bindings/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 across bridge.rs
  • Hookify rule update-changelog: stop-event hook reminding to update CHANGELOG.md as the last step after completing any task

Changed

  • Killed units are now hidden (Visibility::Hidden) at turn end instead of despawned; entity references in ActionLog, PositionSnapshot, and TimelineEntityMap remain valid for the session, eliminating stale-entity bugs in timeline rewind and undo (c1068ab)

Fixed

  • eslint-plugin-storybook peer dependency conflict: bumped from ^9.0.0 to ^10.3.3 to match [email protected], fixing npm ci failure in CI deploy step (be55e09)

0.6.6 — 2026-03-24

Added

  • verify-wasm justfile recipe: uses wasm-objdump to 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-wasm after Build WASM to catch WASM/JS sync failures before they reach Cloudflare Pages

Changed

  • commands.ts: replaced window as any casts with a declare global { interface Window { ... } } augmentation so __shadowboxing_send_command and __shadowboxing_set_dom_focus are properly typed throughout the codebase
  • LandingModal.svelte: unitLabel parameter typed as ArmyUnit[] (was any[]); five (u: any) iterator annotations removed — inferred from array element type
  • +page.svelte: catch (e: any) narrowed to catch (e: unknown) with instanceof Error guard on .message access

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-foreground changed from #ffffff to #0a1f1c (dark teal-black) so white-on-teal buttons are no longer a 2.49:1 failure
  • ToolButton hotkey <kbd> used Tailwind /50 and /60 opacity modifiers which composited to failing contrast even when the base tokens were valid — removed opacity suffixes
  • PaletteSampler story used hardcoded color: white on three accent buttons and an opacity: 0.5 span, bypassing the token system — switched to var(--color-accent-foreground) and removed opacity
  • Docs download buttons used text-accent-foreground/70 on 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, and SetDeckMode commands were neither parsed in bridge.rs nor implemented in dispatch.rs — all four now fully wired end-to-end
  • GameState TypeScript interface was missing the landing: LandingData | null field; Rust was serializing it but the type gap broke IDE support and risked unreliable reactivity in Svelte 5's $state() proxy — added LandingData and NamedOption interfaces and seeded defaultState
  • on_click_objective ran every frame without a run_if gate, inconsistent with all other systems in the plugin — now gated on mouse.just_pressed(MouseButton::Left)
  • UiDrawSet doc 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.ts utility for computing WCAG 2.1 contrast ratios and grades
  • ContrastAudit.svelte component 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_opacity and sync_ring_opacity systems gated on resource_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::Opaque despite having alpha < 1.0 — switched to explicit AlphaMode2d::Blend
  • New Game now resets camera zoom, pan, and rotation to default centered view — extracted reset_camera_to_default and wired it into on_reset_game via CameraCleanup SystemParam
  • Screenshot tool produced blank images in Chrome — canvas now pre-creates WebGL2 context with preserveDrawingBuffer: true before 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
  • BoardTheme resource: board background and grid colors respond to dark/light mode
  • Storybook stories for all 16 game UI components with shared _fixtures.ts mock data
  • Prettier config with prettier-plugin-svelte and prettier-plugin-tailwindcss

Changed

  • Settings modal fully wired: theme selector, gameplay toggles, team color pickers, overlay/opacity sliders, keybindings — UpdateSetting dispatch handles all 16 keys (was a TODO stub)
  • SetPhase dispatch 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 GridLineThick component 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 UiCommand dispatch
  • UiCommand enum (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)
  • DomHasFocus resource 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.html for bevy 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 under src/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_changed check)
  • Keyboard focus lost after clicking Svelte overlay buttons (canvas refocus via requestAnimationFrame)
  • wasm-opt --enable-all replaced with individual feature flags for binaryen v128 compatibility

Removed

  • bevy_egui dependency 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() and UnitBase::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: TurnNodeBattleRoundNode, PlayerHalfPlayerTurnRecord, halvesplayer_turns, turn_numberround_number
  • PhaseState + TimelineState.current_view consolidated into single TimelineCursor resource with head: TimelinePosition, view: CursorView, active_player: Player
  • EndTurn event renamed to EndPlayerTurn; "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
  • SnapshotScores now includes SecondaryDeckState for both players — secondary deck state restored on truncate
  • on_truncate restores MissionState, ObjectiveControlState, and SecondaryState from 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
  • EndPlayerTurn guarded 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.scores was set on every EndTurn
  • EndTurn could fire from PreGame, corrupting timeline with turn_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> to TurnNode/PlayerHalf/PositionSnapshot tree
  • 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 typed RewindTo(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_units passed None for 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_overlay unconditionally set non-killed labels to Inherited, overriding blob-induced Hidden
  • Mid-turn save snapshots no longer produce indistinguishable "Live — Command" labels on reload; live state saved as separate live_actions/live_positions fields

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: IsTransport and InTransport components 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, IsTransport component 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_unit writing FocusedUnit on the click frame triggered wgpu Texture::drop deadlock. Removed the resource; datacard computes focus inline with a click-frame gate
  • Wound tree click hang: same deadlock via direct FocusedUnit write. 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::FileDialog froze 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

  • FocusedUnit resource, sync_focused_unit system, FocusedTriggers SystemParam, focused_unit_needs_update run condition
  • draw_unit_datacard standalone system (inlined into draw_right_panel)
  • warm_egui_atlas startup system
  • SelectedUnitForAnalysis, SelectedSourceEntity, SelectedCandidate, UnitAnalysisState resources
  • Source dot markers, candidate dot markers, staged analysis flow, apply_unit_fade, handle_source_click systems
  • handle_analysis_click system (LOS now reacts to Selected component changes)
  • show_source_points setting from VisibilityOverlaySettings
  • scaffolds/blob-grouping.md planning 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: MeshPickingPlugin raycasting unit bases caused wgpu deadlock — set PickingBehavior::IGNORE on 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 ColorMaterial instances with alpha < 1.0 defaulted to AlphaMode2d::Opaque — switched to explicit Blend (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::IGNORE caused duplicate drag events (8ea1fe0)
  • WASM startup crash: std::env::set_var panics 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-sys dependency 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