RELEASE NOTES

What's changed, and what it means for you.

SEE WHAT'S NEXT →

v00.21.05

May 2026

A small feedback tab now lives in the bottom-right corner of the battle screen at all times. Click it to open a panel where you can flag a bug, suggest a change, or leave any note — without leaving the game. Submissions go straight to the admin review queue.

  • app/tactical/_components/feedback-tab.tsx: New Pure Fabrication component. Corner tab triggers a slide-up panel with a type selector (Bug / Suggestion / Other), textarea (1000 char limit, live counter), and Send button. Submits to POST /api/feedback with page: '/tactical'. Shows success confirmation and auto-closes after 1.8s. No store access.
  • app/tactical/page.tsx: FeedbackTab imported and rendered as the last child of <main>, after the post-match screen.
  • app/docs/roadmap/page.tsx: In-Game Feedback Button moved from HIGH to COMPLETE.
  • lib/version.ts + package.json: bumped to v00.21.05.

v00.21.04

April 2026

After every match, instead of a bare win/loss popup, you now see a full post-match screen with three tabs. The Battle Chronicle tells the story of what happened in each faction's voice — Vampires speak of blood-tithes while placeholder units get dry field reports. The Tactical Report shows MVP, first blood, turning points, and momentum shifts. The Squad Breakdown shows how each squad performed in kills, damage, and losses. Everything runs from the battle log with no AI token cost.

  • lib/tactical/post-match/match-analyzer.ts: New module. analyzeMatch() reads the battle log to compute per-squad kills, damage, and losses. Identifies MVP (kills×3 + damage/5 + objectives×4 − modelsLost), tanker (most damage absorbed), first blood, and turning points (momentum swings, squad wipes, VP swings, multi-kill rounds). Tracks kill attribution via a lastAttacker Map built from damage_dealt events.
  • lib/tactical/post-match/narrative-generator.ts: New module. Faction voice registry (VOICE_REGISTRY) with themed vocabulary for all 10 factions. generateNarrative() produces a multi-paragraph battle chronicle with title, verdict, and key insight.
  • lib/tactical/post-match/index.ts: Barrel export for analyzeMatch, generateNarrative, and their types.
  • app/tactical/_components/post-match-screen.tsx: Three-tab UI component — Battle Chronicle (narrative), Tactical Report (stats), Squad Breakdown (per-squad cards). Cinzel/parchment/gold styling consistent with existing game UI.
  • app/tactical/page.tsx: Old inline win/draw overlay (~80 lines) replaced with <PostMatchScreen> component.
  • lib/tactical/store.ts + ai-orchestrator.ts: battle_ended log event added when checkWinConditions() detects game over. Payload: winner, reason, finalVP, survivors, totalRounds. constlet fix in advanceRitePhase for log append.
  • lib/version.ts + package.json: bumped to v00.21.04.

v00.21.03

April 2026

When you buy a larger army than the AI opponent (more squads in total), the game was getting stuck after the AI ran out of squads to activate. The turn would flip to the AI with nothing for it to do, and instead of passing the turn back it left the game in a broken state where you could accidentally click and select the enemy's units. This is now fixed — when the AI has no squads left to activate, it correctly hands control back to you.

  • ai-orchestrator.ts: getActivatableSquads imported from engine. After takeTurn() produces an empty action list, the orchestrator now checks whether P1 still has activatable squads. If yes, it flips currentPlayer back to 1 and pushes a player-turn toast. If both sides have nothing left (defensive fallback), it clears isAITurn without freezing. Previously the empty-actions path fell through to the recursive terminus which correctly cleared isAITurn but left currentPlayer === 2, making enemy models selectable.
  • lib/version.ts + package.json: bumped to v00.21.03.

v00.21.02

April 2026

Legibility pass on the army builder screens. Secondary text in both the faction selector and the unit cards was too small and too dark to read comfortably. Font sizes and colours updated throughout.

  • army-builder.tsx (faction step): Faction name 0.75→0.9rem; resource label 0.45→0.6rem, colour lightened; tagline 0.5→0.62rem, colour lightened; “COMING SOON” badge made visible from near-invisible to #4a3a28.
  • army-builder.tsx (unit cards): Tier/class/MDL line 0.42→0.6rem, lightened; “PTS” label 0.38→0.55rem, lightened; stat label row 0.32→0.5rem, lightened; weapon line 0.42→0.6rem, lightened; trait badge 0.37→0.55rem, lightened; ability excerpt 0.42→0.6rem, lightened; “NOT IN ARMY” label lightened; footer and eyebrow labels also lifted to match.
  • lib/version.ts + package.json: bumped to v00.21.02.

v00.21.01

April 2026

Two fixes for the new army-building flow. First: after picking your army and edicts on the second (or later) game in a session, the edict selector was being skipped entirely — the game dropped straight into the old battle still loaded in memory. Second: if you bought more squads than the AI opponent had, you could only deploy as many squads as the AI before the deploy phase ended prematurely.

  • page.tsx: Pre-battle gate condition changed from !battle to pendingEdicts === null. The old condition routed correctly on the first game (battle was null) but skipped the EdictSelector on all subsequent games because the previous battle remained loaded in the Zustand store. The new condition gates purely on local React flow state and is unaffected by store contents.
  • store.ts: placeSquad()nextPlayer changed from const to let; if the natural next player has no squads remaining, it skips forward to the player who still has squads. Previously, when P2 ran out of squads first, activePlayer was set to P2 with selectedSquadId: null, leaving P1 with unplaced squads and no way to continue. The AI auto-deploy trigger is unchanged but now fires correctly for uneven army sizes.
  • lib/version.ts + package.json: bumped to v00.21.01.

v00.21.00

April 2026

The game now starts with a genuine choice. Before deploying your forces, you build your warband from scratch — pick your faction from the full roster, then spend your point budget on units from that faction's available lineup. Larger battles give you more points and a bigger army. Every game starts differently because you decide which squads to bring.

  • lib/tactical/army-builder.ts: New module — ARMY_POINT_LIMITS (400 / 650 / 1000 pts for Skirmish / Standard / Grand), ArmySquadEntry, ArmySelection types, FACTION_META (all 10 factions, only VAM available), FACTION_UNITS registry.
  • army-builder.tsx: New component — two-step full-screen flow. Step 1: faction selector (10 faction cards; VAM enabled, others Coming Soon). Step 2: unit picker grid with per-unit cards, point-meter progress bar, +/− squad controls, and sticky footer with point total. Matches pre-battle visual style (Cinzel, dark bg, gold accent).
  • dev-state.ts: buildDevState() gains optional p1Squads?: ArmySquadEntry[] param. When provided, P1's warband is built from the player's selection; otherwise falls back to the hardcoded dev setup (two VAM T1 squads). Squad IDs use sequential p1-squad-N prefix.
  • page.tsx: Pre-battle flow extended to three steps: BattleSizeSelector → ArmyBuilder → EdictSelector. pendingArmy: ArmySelection | null state added. PLAY AGAIN resets all three steps. Docstring updated to reflect live flow.
  • lib/version.ts + package.json: bumped to v00.21.00.

v00.20.30

April 2026

When a heavy unit successfully blocks a charge, the game now shows a clear CHARGE DENIED result in the combat log — no more wondering why the charge did not land. The denied result appears alongside First Strike and standard charge panels so all reactive outcomes have the same visible feedback.

  • store.ts: lastChargeDenied: boolean added to TacticalUIState; resolveChargeLanding() sets it true when applyChargeDenialIfEligible() fires a denial and false on all other charge resolutions.
  • page.tsx: CHARGE DENIED log panel added (red border, consistent with charge panel stack); reads lastChargeDenied from store subscription; renders when true. Panel clears on next interaction alongside other combat log panels.
  • lib/version.ts: bumped to v00.20.30.

v00.20.29

April 2026

Heavy, armoured units can now deny a charge entirely before it resolves — if a unit with the Charge Denial trait is waiting when an enemy tries to rush in, the charge is stopped cold and the attacker eats a free counter-attack. No wounds land, no retaliation fires; the charge simply does not happen. The placeholder Brute unit now carries this trait so the mechanic can be tested immediately.

  • types.ts: 'charge-denial' added to UnitTrait union; 'charge_denied' added to BattleEventType (after 'first_strike_triggered').
  • engine.ts: applyChargeDenialIfEligible() implemented — checks the defending squad for the charge-denial trait, fires a pooled melee counter-attack against the charging squad, logs a charge_denied event, and returns a chargeDenied flag.
  • actions.ts: applyChargeDenialIfEligible imported from engine; wired as the first check inside applyChargeWithRetaliation() — fires before the First Strike path and before charge attacks. If chargeDenied is true, the function returns early with empty results.
  • placeholder-units.ts: traits: ['charge-denial'] added to the T3 Brute (ph-brute) entry.

v00.20.28

April 2026

Thrall units now have a reason to stay in the fight — every kill they land this round charges up a bonus attack die that carries into their next swing. Stack kills and the Thrall becomes genuinely threatening; lose the round without landing a kill and the bonus resets. A ⚡ badge on the unit info panel shows the current bonus so you always know how many extra dice you're sitting on.

  • types.ts: killTriggerBonus?: number added to LiveSquad; kill-trigger trait comment updated to reference VAM Thrall.
  • engine.ts: applyChargeAttacks() consumes killTriggerBonus before swing and increments it on kill; resolveEndOfRound() resets killTriggerBonus: 0 for all squads.
  • actions.ts: executeAttack() consumes killTriggerBonus before resolveAttack() and increments it after spillover kills (ranged and melee). Bonus is capped at 3.
  • unit-info-panel.tsx: killTriggerBonus prop added; ⚡ +N ATK badge renders in gold when bonus is > 0.
  • page.tsx: killTriggerBonus passed to UnitInfoPanel from infoSquad state.

v00.20.27

April 2026

Nightshroud now punishes charging enemies with First Strike — if the defenders deal enough damage before the charge lands, the charge is suppressed entirely and the attacker never gets to swing. The combat log shows the First Strike results first, then the charge result (or “CHARGE SUPPRESSED” in red if the charge was stopped).

  • engine.ts: applyFirstStrikeIfEligible() implemented — checks the defending squad for the first-strike trait, fires pooled melee attacks against the charging squad, and returns results plus a chargeSuppressed flag if the charge point member was slain.
  • store.ts: resolveChargeLanding() now calls applyFirstStrikeIfEligible() before applyChargeAttacks(). If chargeSuppressed is true, both applyChargeAttacks and applyRetaliationIfEligible are skipped. lastFirstStrikeResults set on every charge resolution; cleared to [] on normal attacks. Fixed missing unitMoveDestinations: {} in initial state block.
  • page.tsx: First Strike combat log panel added (amber/gold border) above the charge panel. “CHARGE SUPPRESSED” text appears in the panel when first strike kills the point member. lastFirstStrikeResults selector added.
  • vam-units.ts: Nightshroud traits: ['first-strike'] (corrected from invalid firstStrike: true field).

v00.20.26

April 2026

No gameplay changes. This release finishes a clean-up pass on the main game file — moving the last two visual helper functions into their own dedicated module. The code that decides which hexes to warn you about during formation and which colours to show on unit-move ghosts now lives in a focused formation utilities file, keeping the top-level page code lean and readable.

  • formation-utils.ts (new — lib/tactical/): getFormationWarnHexes() (Extraction 6/7) and getSquadMoveGhostColors() (Extraction 7/7) extracted from page.tsx. getSquadMoveGhostColors() uses getFactionColors() from unit-codes.ts instead of a direct FACTION_COLORS lookup, matching the canonical faction-colour resolution path. Completes the 7-extraction decomposition pass.
  • page.tsx: Both IIFEs replaced with clean one-liner calls importing from formation-utils. hexDistance and FACTION_COLORS imports removed. No logic changes.
  • styles.ts (same session — Extraction 5/7): Style constants extracted from page.tsx into app/tactical/_components/styles.ts in the same decomposition pass. BattleSizeSelector import path corrected as part of that patch.

v00.20.25

April 2026

Forced March and Press The Line now move the entire squad reliably. Previously, some members could get stuck if a squad-mate that moved earlier happened to occupy a hex their forward path would have used. The fix makes front-most models advance first, clearing the way for those behind them.

  • edict-actions.ts: applyForcedMarch() and applyPressTheLine() now sort livingMembers front-to-back before iterating — descending by q for P1, ascending for P2. Front models advance first, vacating hexes that rear members need to find valid forward candidates. No change to movement rules, step count, or hasMoved handling. Resolves M-05.

v00.20.24

April 2026

When you hover a destination in Unit Move mode, you can now instantly tell which ghost is the point member leading the formation and which are followers snapping in behind. The leader shows a gold pip at its centre, a slightly larger token ring, and a solid gold outline — followers are smaller, dimmer, and dashed. No rules or logic changed.

  • tactical-map.tsx: Leader ghost (index squadMoveLeaderIndex, always 0) now renders with radius 11 (vs 9 for followers), solid gold stroke (#c8a840) at 1.5px (vs dashed 1px), opacity 0.75 (vs 0.55), and a filled gold center pip (r=3, fill="#c8a840"). Follower ghosts retain the existing dashed faction-colored style at reduced opacity. No store, engine, or types changes. Resolves P-17.

v00.20.23

April 2026

After you put a model on overwatch, the bottom bar no longer shows two competing pieces of text at once. The “OVERWATCH SET” confirmation now appears where the phase label normally sits, styled in the same gold so it reads as a status report rather than an instruction. The Advance button on the right remains visible — you can still step to the next phase immediately after declaring overwatch.

  • page.tsx: Phase label (· MOVE PHASE ·) is now suppressed when statusMessage is set and selectedModelId is null — the condition unique to overwatch declaration. In that state the status message renders in the phase-label slot: gold, 0.65rem, 0.14em letter-spacing. All other status messages (NO VALID TARGETS IN RANGE, SELECT ENEMY TARGET) fire while a model is still selected, so they continue to render as red warnings below the phase label. No store, engine, or types changes. Resolves P-20.

v00.20.22

April 2026

Objective scoring at the end of each round now gets a proper moment. The round-start toast now shows a third line in gold — “OBJECTIVES · P1 +2 VP · P2 +1 VP” — so both players can immediately see what the round was worth. If no objectives were scored, the line is omitted. When VP is shown, the toast stays on screen for 3 seconds instead of 2 to give you time to read it.

  • types.ts: Optional vpMessage?: string field added to BattleToast. Only populated on kind: 'round' toasts when at least one player scored VP this round.
  • store.ts: In advanceRitePhase(), VP delta computed as newBattle.vp[N] - battle.vp[N] after completeSquadActivation() returns. When either delta is non-zero, vpMessage is set to “OBJECTIVES · P1 +N VP · P2 +N VP” and durationMs extends from 2000 to 3000. No engine changes.
  • page.tsx: RiteToast renders a third gold line (#c8a840, 0.6rem Cinzel) when toast.vpMessage is set. Positioned below the initiative line. Resolves P-04.

v00.20.21

April 2026

The deploy screen now clearly shows which side of the map each player owns. The left zone is highlighted in blue for Player 1, the right in red for Player 2 — with matching labels on the map edges and inside the zones themselves. The squad panel also shows a short direction hint so you always know where to place your forces.

  • tactical-map.tsx: Edge labels conditioned on state.phase === 'deploy' — now only shown during deployment. Fill colors changed from near-black to P1-blue (#3a6aaa) and P2-red (#aa3a3a), text expanded to “PLAYER 1 DEPLOY ZONE” / “PLAYER 2 DEPLOY ZONE”. Two additional in-grid P1 / P2 labels added inside the zoom/pan group, x-positions derived from hexToPixel() column averages via new DEPLOY_ZONE_LABEL_X_P1 / DEPLOY_ZONE_LABEL_X_P2 constants — labels track the map at any zoom level.
  • deploy-panel.tsx: Zone direction hint added under the header divider: ← YOUR ZONE · LEFT SIDE for P1, YOUR ZONE · RIGHT SIDE → for P2. Color matches player accent. Display-only change. Resolves P-16.

v00.20.20

April 2026

New players now see a brief rules screen before their first activation. After the coin toss resolves and the first-player choice is made, the coin-flip modal advances to a second screen “The Law of the Rite” that explains the three core activation rules. The player dismisses it by clicking BEGIN BATTLE — it is never auto-dismissed.

  • coin-flip-modal.tsx: Two-step flow added via local step: 'flip' | 'rules' state and pendingFirstPlayer: 1 | 2 | null. Player choice buttons and the AI auto-choose path now set pendingFirstPlayer and advance step to 'rules' instead of calling onChoose directly. Rules screen shows three onboarding rules (One Squad Per Turn, Players Alternate, Hold the Ground) in the existing Cinzel / dark-gold visual style. onChoose fires only when BEGIN BATTLE is clicked. No store, types, engine, or other UI changes. Resolves P-14.

v00.20.19

April 2026

The bottom bar now gives first-time players clearer guidance when nothing is selected. During the battle phase it reads “Click a friendly member to begin your rite” with a sub-line reminding you that each squad gets one turn to move, shoot, or fight. During deployment it points you to the panel and tells you to click your zone to place.

  • page.tsx: Idle state fallback in the action bar centre replaced with a phase-aware two-line block. Battle idle: primary CLICK A FRIENDLY MEMBER TO BEGIN YOUR RITE, sub Move · Shoot · Fight — one squad per turn. Deploy idle: primary SELECT A SQUAD FROM THE PANEL, sub Then click your deployment zone to place it. Sub-line styled #8a7a5a, 0.55rem — consistent with the edict phase flavour text. Display-only change. Resolves P-12.

v00.20.18

April 2026

The AI no longer deploys in silence — after each enemy squad is placed during the deploy phase, a banner slides in from the top of the screen reading “ENEMY DEPLOYS: [UNIT NAME]” and fades out automatically. Previously the AI's tokens simply appeared on the map with no feedback.

  • store.ts: placeSquad() now pushes a 'phase' kind BattleToast (1500ms, auto-dismiss) when isAIDeploy === true. Message is ENEMY DEPLOYS: <DEF NAME>, derived from unitDefinitions[squad.definitionId].name. Uses the existing PhaseBanner component and popToast/dismissToast infrastructure — no new state, types, or UI components. Resolves P-06.

v00.20.17

April 2026

Edict cards on the battlefield now open a full-detail modal when clicked — showing the edict name, category, timing, and full description alongside the Spend Edict button. Previously clicking a card expanded it inline; now the cards stay compact and the modal handles all the detail, matching the behaviour of the edict selection screen.

  • edict-strip.tsx: Replaced inline expand/collapse with a EdictDetailModal component. Cards are now compact name chips with a ◈ hint icon. Clicking any card opens the modal. Modal matches the selector's visual style — category badge, name, timing pill, full description, and SPEND EDICT button. Backdrop click or ✕ closes it.

v00.20.16

April 2026

Portrait tokens no longer show a unit code label over the image — the portrait itself is the identity. Tokens without a portrait are unchanged.

  • tactical-model-token.tsx: Unit code text block is now suppressed when TOKEN_IMAGES[definitionId] is set. Flat-fill tokens render the label as before.

v00.20.15

April 2026

Nightshroud tokens now show a portrait on the hex map — a tight crop of the unit's face with glowing red eyes centered in the circle. The token system was also refactored: instead of a hardcoded check for a single unit, any unit can now get a portrait token by adding one line to a central registry.

  • tactical-model-token.tsx: Added TOKEN_IMAGES registry (Record<definitionId, imagePath>). Token body block now checks TOKEN_IMAGES[definitionId] instead of hardcoding definitionId === 'vam-thrall'. Adding portrait tokens for future units requires one registry entry — no render logic changes.
  • public/images/factions/vampires/units/nightshroud/token.jpg: New 259×259px face-centred crop of the Nightshroud portrait, centred on the glowing eyes at 48% x / 22% y of the original 576×1024 image.

v00.20.14

April 2026

Updated the design guide to accurately reflect the current token visual spec — token sizes, stroke weights, and ring measurements now match the code exactly.

  • app/docs/design-guide/page.tsx: Geometry spec table corrected — token body radius updated from r=11px (all tiers) to r=9–18px (tier-scaled) with the full T1–T6 breakdown; token stroke width updated from 1.5px to 0.75px (T1–T4) · 1px (T5–T6); member view faction ring updated from r=11 · 1.5px to tier radius · 0.75px.

v00.20.13

April 2026

Fixed a bug where only the lead model of a charging squad would reach the enemy — the rest of the unit was left behind because followers were only allowed their normal movement range during a charge. Now the whole squad commits to the charge together, using the same extended range as the charging leader.

  • Fix (engine.ts — getChargeDestinations): Charge followers now use chargeRange (MOV + 3) instead of MOV when computing reachable follower slots. Previously followers were evaluated at base MOV, causing them to be marked stranded whenever the charge destination was more than MOV hexes from their current position.

v00.20.12

April 2026

Vampire Thrall tokens on the hex map now show a portrait of the unit instead of a flat colour. A face-centred crop of the unit's oil painting portrait is displayed inside the circular token, giving each Thrall a distinct visual identity on the battlefield. A new dev tool for saving image crops to the public folder was also added to support future unit portrait work.

  • tactical-model-token.tsx: Thrall token body now renders a circular portrait image (token.jpg) via SVG <image> + <clipPath>, with a faction-coloured stroke ring. All other units unchanged.
  • public/images/factions/vampires/units/thrall/token.jpg: New 276×276px face-centred crop of the Thrall portrait, optimised for circular token rendering.
  • app/api/save-image/route.ts: Dev-only POST endpoint that accepts a base64 image payload and writes it to public/images/. Used by the canvas crop pipeline.
  • Fix (page.tsx): _abModel temporal dead zone crash resolved — declaration moved above _showMoveUnit which referenced it.

v00.20.11

April 2026

Fixed a loop where selecting a unit that had already moved would trap the player in an empty movement mode with no way to attack, charge, or advance — forcing a cancel and re-select that just repeated the same dead end. The unit now correctly falls through to show whatever actions are still available after its move is spent.

  • Fix (store.ts): initiateUnitMove() now guards on pointMember.hasMoved. Previously it entered unit-move mode with zero destinations when the point member had already moved, which set unitMoveMode: true and hid all ATTACK, CHARGE, and OVERWATCH buttons (all require !unitMoveMode). Now returns early so the player sees the correct remaining actions.
  • Fix (page.tsx): _showMoveUnit visibility flag now includes !_abModel?.hasMoved so the MOVE UNIT button does not appear as a dead option after movement is spent.

v00.20.10

April 2026

Edict cards are now significantly more readable — larger text and brighter colours throughout. A new “third click” Full View button opens a centred modal for each edict showing the full name, category, timing, and description at comfortable reading size, with a select/deselect button inside so you never have to close it to make a choice.

  • edict-selector.tsx: Edict name bumped from 0.6rem to 0.75rem; unselected colour lifted from #8a7a5a to #b0a080; selected name now #e8d090. Category label up from 0.45rem to 0.55rem; all five category colours brightened. Inline description up from 0.55rem #6a5a3a to 0.72rem #a09070.
  • edict-selector.tsx: ‘◈ FULL VIEW’ button added to each card alongside ‘▼ DETAILS’. Opens EdictModal — a z-80 overlay with edict name at 1.25rem, category badge, timing pill, full description at 0.95rem/1.75 line-height, and an add/remove toggle button. Backdrop click closes the modal.

v00.20.09

April 2026

Admins can now configure how P2 is set up before a battle starts. A new Battle Configuration page in the admin panel offers four modes: Baseline (always placeholder units), Random (AI picks from available factions each game), Specific Faction (admin pre-selects which faction P2 plays), and Player Selected (pending — requires faction selection screen). The P2 squad roster is now data-driven and will expand automatically as new factions are built.

  • Schema (prisma/schema.prisma): New GameConfig model — key/value singleton rows for admin-controlled runtime settings. Run prisma db push before deploying.
  • API (app/api/game-config/route.ts): GET returns current P2 config (public, read by tactical page on load). PATCH updates config (admin-only). Defaults to baseline if no row exists yet.
  • Admin page (app/admin/game-config/page.tsx): Four mode options with descriptions. Specific Faction mode reveals a faction picker (all available factions + baseline). Player Selected shown as PENDING badge. Active state summary. Save button with saving/saved feedback.
  • Admin layout (app/admin/layout.tsx): BATTLE CFG entry added to sidebar nav.
  • Admin hub (app/admin/page.tsx): Battle Configuration card added.
  • Tactical page (app/tactical/page.tsx): P2_FACTION_SQUADS registry maps faction IDs to squad definitions (placeholder + vam). resolveP2Faction() handles all four modes. buildDevState() accepts p2Mode and p2SpecificFaction params. Page fetches config on mount and passes it through. Hardcoded P2 placeholder squads replaced with data-driven loop.

v00.20.08

April 2026

Three new opening-game edicts give you powerful plays in rounds 1–2, before the lines engage. Forced March lets a squad rush forward without burning their move action. Scouting Report strips cover from an enemy squad for the rest of the round. Forward Scouts teleports a T1 squad to the board's midfield in a bold early gambit. A direction bug in Press The Line and Forced March was also fixed — both now correctly push squads along the board's horizontal axis toward the enemy.

  • Bug fix (edict-actions.ts): applyPressTheLine() was filtering and scoring movement along the r-axis (rows) instead of the q-axis (columns). P1 deploys in low-q columns; P2 in high-q. All three axis references corrected: filter conditions now check nb.q against pos.q, and the reducer picks the candidate with the largest q-direction delta.
  • Feature (edict-actions.ts): applyForcedMarch() added — identical to Press The Line's q-axis advance logic, but hasMoved is intentionally NOT set on moved models, preserving their full move action for later in their activation.
  • Feature (edict-actions.ts): applyScoutingReport() added — sets edictBuffs.scoutedSquadId on the target enemy squad. Cover is stripped in executeAttack() in actions.ts whenever the target squad's ID matches the scouted ID.
  • Feature (edict-actions.ts): applyForwardScouts() added — T1-only, hasAttacked guard; places squad within 3 hexes of board centre via buildDeployFormation(); iterates sorted anchor candidates until a full formation fits.
  • Types (types.ts): EdictId union extended with forced-march, scouting-report, forward-scouts. edictBuffs shape on BattleState gains scoutedSquadId: string | null.
  • Registry (edicts.ts): Three new EdictDefinition entries appended to UNIVERSAL_EDICTS. Forward Scouts carries tierLimit: 1.
  • Actions (actions.ts): executeAttack() — cover computation now checks edictBuffs.scoutedSquadId; if the target squad is scouted, cover is forced to 'none' before passing to resolveAttack().
  • Init (page.tsx): buildDevState()edictBuffs initialiser updated to include scoutedSquadId: null.
  • Reset (engine.ts): resolveEndOfRound()edictBuffs reset object updated to include scoutedSquadId: null.

v00.20.07

April 2026

Vampire tokens now show their correct unit labels and faction colours on the battlefield. The Thrall shows THRL and the Nightshroud shows NSHD instead of the fallback VAM– that appeared for both units. Tokens also render in true vampire crimson rather than grey — the faction colour palette now resolves correctly from short faction codes like 'vam'.

  • Fix (unit-codes.ts): UNIT_CODES added a new block of tactical-game entries in hyphen format (e.g. 'vam-thrall') matching the IDs used by vam-units.ts and placeholder-units.ts. Previous entries used underscore format from the retired game2 system, causing getUnitCode() to fall back to a truncated ID for every live tactical unit. Underscore entries retained in a separate deprecated block for safety. Placeholder unit codes added: ph-skirmisherSKMR, ph-lineLINE, ph-archerARCH, ph-bruteBRUE, ph-eliteELTE, and so on. VAM T1 codes: vam-thrallTHRL, vam-nightshroudNSHD.
  • Fix (unit-codes.ts): FACTION_SHORT_TO_PALETTE_KEY map added — bridges short faction codes ('vam', 'wol', 'gol', etc.) to the long-form keys used by FACTION_COLORS ('vampires', 'werewolves', 'golems', etc.). getFactionColors(faction) helper exported — resolves via the map first, then falls back to a direct key match, then falls back to the 'placeholder' palette. Root cause: vam-units.ts sets faction: 'vam'; FACTION_COLORS is keyed on 'vampires'; direct lookup returned undefined, causing grey tokens for all VAM units.
  • Fix (tactical-model-token.tsx, tactical-map.tsx): All direct FACTION_COLORS[faction] lookups replaced with getFactionColors(faction). Covers the token body fill in tactical-model-token.tsx and the squad-mate dashed ring colour in tactical-map.tsx.

v00.20.06

April 2026

A redundant toast notification that fired at the start of every round has been removed. The round banner already tells you who strikes first — the player-turn toast no longer repeats the same message a moment later at round boundaries.

  • Fix (store.ts): advanceRitePhase() player_turn toast now gated on roundAfter === roundBefore. At round boundaries the round toast already shows initiative via PLAYER X STRIKES FIRST; mid-round player switches (same round) still fire a player_turn toast as before.
  • Docs: UX-UI-AUDIT.md — P-18 moved to resolved archive.

v00.20.05

April 2026

The combat log panels no longer pile up and cover the action bar when multiple results are showing at once. A new Retaliation panel also appears after any melee counter-strike so you can see exactly what the defending squad hit back with.

  • Fix (page.tsx): Combat log stack given maxHeight calc(100% - 80px) and overflowY auto — panels no longer overflow the screen or cover the action bar when multiple results are showing simultaneously.
  • Feature (page.tsx): RETALIATION log panel added to the combat log stack (green border, υ arrow). Displays after attack/charge results when a retaliation counter-strike fires. Stack visibility condition updated to include lastRetaliationResults.length > 0.

v00.20.04

April 2026

Melee now bites back. Any squad attacked in melee — whether in the fight phase or on the receiving end of a charge — gets to counter-strike immediately if they have the models for it. Reckless or unskilled units can be tagged with a no-retaliation trait to opt out. Skirmishers will eventually gain first-strike to avoid the risk entirely.

  • Feature (engine.ts): applyRetaliationIfEligible() — universal melee retaliation engine function. Four gates: (1) defender has a melee weapon, (2) at least one in-formation defender is adjacent to a living attacker, (3) retaliation cap not reached (retaliationsUsedThisRound < maxRetaliationsPerRound, default 1 per round), (4) defender does not have the no-retaliation trait. Pools ATK from all in-formation defenders. Full spillover damage. Does not set hasAttacked on defenders.
  • Chore (engine.ts): resolveEndOfRound() now resets retaliationsUsedThisRound: 0 on all squads alongside the existing per-round reset.
  • Types (types.ts): Added UnitTrait union (no-retaliation | first-strike | flying | kill-trigger). Added traits?: UnitTrait[] and maxRetaliationsPerRound?: number to UnitDefinition. Added retaliationsUsedThisRound?: number to LiveSquad (optional for backwards compatibility). Added retaliation_strike to BattleEventType. Removed stale firstStrike?: boolean field superseded by traits.
  • Store (store.ts): resolveAttackOnTarget() calls applyRetaliationIfEligible() after every melee attack resolves. resolveChargeLanding() calls applyRetaliationIfEligible() after applyChargeAttacks(). Retaliation result stored in lastRetaliationResults: ChargeResult[]. Removed stale imports of applyChargeWithRetaliation and RetaliationResult from a previous incomplete session.

v00.20.03

April 2026

Vampire T1 units arrive — the player side of the battle is now a real faction. Thrall and Nightshroud replace the generic placeholder squads for Player 1. Thralls grind forward and hit harder than anything at their tier; Nightshrouds are glass cannons that will eventually kill anything that charges them.

  • Feature (vam-units.ts): New faction file for Vampire units. Thrall — T1, 3 models, HP 2, DMG 2, melee only, kill-trigger ability text. Nightshroud — T1, 3 models, HP 1, DMG 3, melee only, first-strike ability text. Both use MEL 3 / DFN 1 / MOV 3 / ARM 0.
  • Feat (page.tsx): buildDevState() swaps P1 squads from ph-skirmisher + ph-line to vam-thrall + vam-nightshroud (3 models each). P2 retains placeholder units as AI opponent and stat baseline. Unit registry now merges PLACEHOLDER_UNITS and VAM_UNITS.
  • Docs (roadmap): Three new HIGH priority items added — Retaliation (charge response layer), First Strike (unit ability, blocked on Retaliation), Kill-Trigger VAM Thrall Enhancement.
  • Design (05-DECISIONS-LOG-2026-04-06.md): Decision 7 — placeholder units retained permanently as stat baseline; Decision 8 — First Strike and Retaliation deferred with design intent locked.

v00.20.02

April 2026

Edict System arrives — before each battle you now pick a set of powerful one-use orders. During the Edict Phase you can spend one per round to reshape the battlefield: resurrect a fallen soldier, double melee damage, shield your squad, curse an enemy, or reposition across the map in an instant.

  • Feature (edicts.ts): New Universal Edicts registry — 12 named edicts each with category, timing, target type, and description. getEdictDefinition() helper for lookup by ID.
  • Feature (edict-actions.ts): All 12 edict effect implementations — applyEdictEffect() dispatcher routes to Iron Will, War Cry, Expose Weakness, Killing Blow, Smoke and Shadow, Press the Line, Suppressing Fire, Field Dressing, Armour of Contempt, Marked for Death, Fury of the Ancients, Ghost Step.
  • Engine (engine.ts): resolveEndOfRound() extracted as a dedicated function — single reset path for all per-round state (edict buffs cleared, edictsRemaining reset to 1). advanceRound() delegates entirely to it.
  • Engine (actions.ts): executeAttack() applies edict buffs — ATK bonus/halved, double melee DMG, SHRD bonus, DFN penalty/bonus, SHRD ignore, and +1 MEL/AIM for Marked for Death.
  • Engine (types.ts): Added EdictId, EdictTiming, EdictCategory, EdictDefinition, EdictBuff types. Extended BattleState with edictsRemaining, selectedEdicts, spentEdictIdThisRound, edictBuffs.
  • Store (store.ts): spendEdict() action — guards ritePhase, edictsRemaining, and selectedEdicts. Friendly-target edicts fire immediately; enemy-target edicts set pendingEdictId and await a map click. selectHex() handles the pending-edict early-exit to apply and clear the target.
  • UI (edict-selector.tsx): Pre-battle edict selection modal — grid of 12 cards, pick exactly N = maxRounds edicts, expandable descriptions, gold border on selected, MARCH TO WAR confirm.
  • UI (edict-strip.tsx): In-battle edict strip shown above the action bar during Edict Phase — collapsible cards, single row for Skirmish/Standard, two-row 4+4 for Grand, SPEND EDICT button inside expanded card, green checkmark on spent edict.
  • UI (page.tsx): Three-step pre-battle flow: BattleSizeSelector → EdictSelector → battle. buildDevState() accepts selectedEdicts and sets all edict state fields. P2 (AI) receives a random N-edict selection. EdictStrip wired above action bar.

v00.20.01

April 2026

Dead code removed — ActionType and pendingAction were vestigial types never used by any live logic. Deleting them shrinks the type surface and eliminates a misleading field from ActivationState.

  • Chore (types.ts): Removed ActionType type definition ('move' | 'shoot' | 'charge' | 'ability' | 'overwatch') and pendingAction: ActionType | null field from ActivationState. The field was hard-coded to null everywhere it appeared and never read.
  • Chore (engine.ts): Removed ActionType from the import list. Removed pendingAction: null from the activation reset object inside completeSquadActivation().
  • Chore (page.tsx): Removed pendingAction: null from the activation object in buildDevState().

v00.20.00

April 2026

Shoot and Fight phases now reachable — a critical bug caused squads to always end their rite after Move. Weapon range was being read from the wrong field, so the engine never detected ranged or melee weapons. Also includes a cleanup pass: internal naming aligned and a documentation value corrected.

  • Fix (store.ts, page.tsx — w.range bug): WeaponProfile has no top-level range field — range lives at weapon.stats.RANGE. All seven occurrences of w.range replaced with w.stats.RANGE: three in advanceRitePhase() and initiateCharge() in store.ts; four in page.tsx covering the _abHasMelee derivation, the ADVANCE button label IIFE, and the action bar charge guard. Before this fix, hasRanged and hasMelee always evaluated to false, so getNextRitePhase() skipped Shoot and Fight phases entirely.
  • Cleanup (types.ts, engine.ts): wound_inflicted renamed to damage_dealt in HitResult and both call sites in engine.ts. PWR JSDoc updated to reflect its role as flat damage before ARM reduction.
  • Docs (LOCAL-SETTINGS.md): Cover bonus values corrected — light cover was documented as +2 DFN; corrected to +1 DFN to match the locked stat system.

v00.19.99

April 2026

Combat system overhauled — the Wound and Save steps are gone. Every attack now resolves in two steps: a contested hit roll, then flat damage. The result is faster, more legible combat where every stat has a clear, direct function.

  • Engine (types.ts): HitResult updated; WoundResult and SaveResult removed. RNG stat renamed to AIM across all model stat interfaces. RANGE moved from a top-level model stat into WeaponStats, removing the naming collision with weapon range values.
  • Engine (engine.ts): resolveHit() replaced with a contested roll — both sides roll d6 and add their stat (AIM/MEL vs DFN); attacker wins ties. resolveAttack() updated to the two-step Hit → Damage chain. selectWeaponForRange() reads weapon.stats.RANGE. checkAndFireOverwatch() and applyChargeAttacks() both use AIM/MEL contested rolls.
  • Engine (actions.ts): executeAttack() updated to use AIM and weapon.stats.RANGE. No other action logic changed.
  • Data (placeholder-units.ts): Full rewrite — all unit stat profiles rebalanced around the new resolution chain. RNG fields replaced with AIM. RANGE moved into per-weapon stats blocks.
  • UI (page.tsx): Combat log panels updated to display contested roll pairs (attacker roll vs defender roll) and flat damage totals. Wound and Save rows removed from all log views.
  • Docs (user-guide/page.tsx): Resolution chain section, cover table, charging section, terrain table, and full stat glossary all updated to reflect the new system. ARM described as flat damage reduction; SHRD as flat ARM negation; cover as DFN bonus (light) or DFN + ARM bonus (heavy).

v00.19.98

April 2026

User Guide rewritten from the ground up — restructured around the journey of a new player, with eleven purpose-written sections covering the tactical game from first boot to VP scoring. All auto-battler era content archived in JSX comments.

  • Docs (User Guide — Full Rewrite): app/docs/user-guide/page.tsx restructured top-to-bottom. Eleven sections sequenced in new-player learning order: What Is Dark Wars, Starting a Match (battle sizes, deploy zone SVG), Reading the Screen (token anatomy SVG), The Rite (four-phase flow SVG), Moving (highlight colours, MOVE UNIT vs MOVE INDIVIDUAL), Attacking (pooled ATK, resolution chain table, cover, OOF), Charging (two-step flow, spillover), Overwatch (set and consume), Formation (kept verbatim from v00.19.97), Terrain (kept verbatim), Objectives and VP (DOM control, HUD dots, dot-states SVG), and Terminology (updated to Model/Squad/Warband/Rite/Round/Phase). Stale auto-battler sections (Getting Started, VS AI Mode, Factions, Commanders & Clans, Reading the Battlefield, Warband Building, Combat, Commander Abilities, Victory Conditions, Turn Structure) archived in a JSX comment block at the bottom of the file — not deleted.

v00.19.97

April 2026

Formation rules documented on the web — a new Formation section in the User Guide explains adjacency, out-of-formation consequences, attack pooling, and how to rejoin, with an inline SVG diagram.

  • Docs (User Guide — Formation): New section added to app/docs/user-guide/page.tsx covering the adjacency-based formation rule (distance 1), the in-formation vs out-of-formation stat comparison boxes, the ATK pool mechanic, and the rejoining rule. Includes an inline SVG diagram showing a 4-model squad in formation (left, all green) vs a 3-model chain with one detached model (right, amber). Section inserted before Victory Conditions.

v00.19.96

April 2026

Formation coherency tightened from 2 hexes to 1 — models must be strictly adjacent to at least one squadmate to be considered in-formation. This makes positioning matter more: only models touching the chain contribute to the attack pool.

  • Design Decision — Formation Coherency Distance (Locked): checkFormation() and checkFormationSilent() in engine.ts changed from hexDistance <= 2 to hexDistance <= 1. A model is now out-of-formation if no squadmate occupies an adjacent hex. This cascades to ATK pool eligibility in executeAttack() and applyChargeAttacks() (both filter by !m.outOfFormation), the OOF damage penalty, and the formation warning overlay on the map. Also corrects a doc/code inconsistency — checkFormation()'s docstring already said “adjacent (1 hex)” but the implementation used 2.

v00.19.95

April 2026

P-19 resolved — both combat log panels now show how many attackers contributed to a pooled roll, making the unit-action system legible at a glance.

  • Polish (P-19 — Pooled Attacker Count): attackerCount?: number added to AttackResult in types.ts. executeAttack() in actions.ts stamps the field with eligibleMembers.length; applyChargeAttacks() in engine.ts stamps it with inFormationMembers.length. The “LAST COMBAT RESULT” panel in page.tsx shows “N ATTACKERS POOLED” below the attacker line when attackerCount > 1. The “CHARGE COMBAT” header appends “· N ATTACKERS POOLED” from lastChargeResults[0].attackResult.attackerCount, replacing the old lastChargeResults.length > 1 check which never fired.

v00.19.94

April 2026

Edict phase map cleanup — move highlights and formation warnings no longer appear during edict phase, where movement isn't possible anyway. The map now reads clean during the order phase.

  • Polish (Edict Phase Move Highlights): TacticalMap now receives an empty moveHighlights array when ritePhase === 'edict', suppressing the Dijkstra highlight overlay during a phase where clicking a hex has no effect. The formationWarnHexes guard in page.tsx also short-circuits on edict phase, preventing the amber formation-warning overlay from appearing. Both changes are display-only — no store, engine, or action logic altered.

v00.19.93

April 2026

Action bar grouping — buttons are now split into three visually distinct groups with subtle dividers, making it easier to read your options at a glance. The advance/end-rite button reads clearly as the primary action. Plus a store hotfix preventing a stuck-selection edge case mid-rite.

  • Polish (P-07 — Action Bar Grouping): The flat button row in page.tsx is replaced with three labelled groups: Escape (CANCEL, MOVE INDIVIDUAL — ghost style), Action (MOVE UNIT, ATTACK, CHARGE, OVERWATCH — existing color coding), and Advance (ADVANCE TO MOVE/SHOOT/FIGHT, END RITE — gold primary). Groups are separated by a 1px #2a2318 vertical divider that only renders when both adjacent groups have visible buttons. The advance/end-rite button gets padding: 4px 16px (wider than the base 4px 10px) to distinguish it as the primary CTA without changing its color or style constant. Button logic, visibility conditions, and store actions are unchanged. Pre-render visibility flags (_escapeGroupHasButtons, _actionGroupHasButtons, _advanceGroupHasButtons) computed before JSX to drive conditional divider rendering.
  • Hotfix (clearSelection ritePhase reset): clearSelection() in store.ts now resets UI ritePhase to null. Previously, clicking an empty hex mid-rite left the rite phase active in UI state, causing the F-08 squad-lock guard to block all subsequent model selection until the player reloaded. The fix was in the working tree since v00.19.92 but not previously committed.

v00.19.92

April 2026

Five UX polish items closed — charge step indicators, ghost preview hold, deploy panel text contrast, and coin flip text contrast all addressed.

  • Polish (P-03 — Charge Step Indicators): Bottom bar messages for the two-step charge targeting flow (pick target squad, then pick landing hex) updated in page.tsx to be more prominent and clearly signpost each step to the player.
  • Polish (P-05 — Ghost Preview Hold): Unit-move ghost preview now holds the last valid formation on cursor when moving off a valid hex, rather than disappearing. Implemented in store.tsunitMoveHoverKey only clears on explicit cancel or commit, not on hover-miss.
  • Polish (P-09 / P-10 — Deploy Panel Text Contrast): “DEPLOY A SQUAD” header and the tier label / model count text under each squad card in deploy-panel.tsx lightened to meet readable contrast against the panel background.
  • Polish (P-11 — Coin Flip Text Contrast): “ROUNDS 2+ ALTERNATE AUTOMATICALLY” footer in coin-flip-modal.tsx color changed from #3a3020 to #5a5040 — now legible against the dark modal background.

v00.19.91

April 2026

P-02 resolved — the AI turn is now visible at two levels: a pulsing orange “◆ AI TURN” label in the HUD strip, and an orange dashed ring that tracks whichever model the AI is currently moving or attacking on the map.

  • Polish (P-02 — AI Turn Visibility): New aiActiveModelId: string | null field added to TacticalUIState in store.ts. ai-orchestrator.ts sets it before every move and attack action and clears it on endActivation. tactical-map.tsx accepts a new optional aiActiveModelId prop and renders an orange dashed ring (#e06020, r+9, dash 6 2) that tracks the acting model through its tween animation — using the same tween-position logic as the existing gold active ring. page.tsx reads the field from the store, passes it to TacticalMap, and replaces the old small red “· AI THINKING…” label with a larger pulsing “◆ AI TURN” in orange matching the ring color. CSS keyframe animation injected via a scoped <style> tag. Design guide updated: new Overlay Rings reference table documents all six ring types with color, radius offset, dash pattern, and trigger conditions.

v00.19.90

April 2026

Move Unit is now the default — clicking a multi-model squad enters formation-move mode immediately. Individual model moves are an explicit opt-out via a new MOVE INDIVIDUAL button.

  • Feature (unit-move as default): selectModel() in store.ts now calls initiateUnitMove() unconditionally after setting selection state. initiateUnitMove() self-guards on livingCount < 2 and phase legality, so solo models and single-survivor squads silently fall through to individual move highlights as before. advanceRitePhase() also calls initiateUnitMove() when advancing to the Move phase, catching the edict → move transition where a model is already selected. No engine changes.
  • Feature (MOVE INDIVIDUAL escape hatch): New switchToIndividualMove() action added to store.ts. Exits unit-move mode and restores per-model Dijkstra highlights for the selected model. Wired to a new MOVE INDIVIDUAL ghost button in the action bar, visible only while in unit-move mode with a multi-member squad selected. The existing MOVE UNIT button is retained as a re-entry path after opting into individual mode.

v00.19.89

April 2026

P-01 resolved — squads that have finished their activation now show a grey fog overlay on every token, giving the player a clear at-a-glance read of which squads are done for the round.

  • Polish (P-01 — Activated Squad Indicator): When a squad completes its rite, all of its tokens now display a semi-transparent grey overlay (rgba(160,160,160,0.28)) inside the token body. The overlay sits above the existing per-model spent hatch and below the HP arc and unit code text, so health and identity remain readable. Applied at both standard (LOD 1) and macro (LOD 0) zoom levels. Data source: LiveSquad.activatedThisRound, already set by completeSquadActivation() and reset by advanceRound(). squadActivatedThisRound added to the modelMeta lookup in tactical-map.tsx and threaded to TacticalModelToken as a new prop. No engine, store, types, or actions changes.

v00.19.88

April 2026

D-02 resolved — declaring Overwatch now shows a confirmation message in the bottom bar so the player knows the action registered.

  • Polish (D-02 — Overwatch confirmation): overwatchSelectedModel() in store.ts now sets statusMessage: 'OVERWATCH SET · MODEL WILL REACT TO ENEMY MOVES' after applying overwatch stance. The message appears in the bottom bar centre and clears on the next selection or mode change, consistent with the existing status message system. No engine, type, or UI file changes.

v00.19.87

April 2026

Charge spillover — excess damage from a charge attack now carries to the next nearest enemy model in the target squad until exhausted.

  • Feature (charge wound spillover): Previously, damage beyond a model's remaining HP was lost. Now, after the primary target is killed, any excess damage carries to the next nearest living member of the target squad as raw HP reduction. Wounds are already resolved — no additional dice are rolled. Spillover cascades through the target squad in distance order until damage is exhausted or the squad is wiped. Implemented in applyChargeAttacks() in engine.ts only. No changes to types, store, actions, ranged attacks, overwatch, or UI.

v00.19.86

April 2026

Fix — melee ATK pooling applied correctly for both charge and normal melee attacks. All in-formation squad members now contribute ATK as long as at least one member is adjacent to the target.

  • Fix (charge and melee ATK pooling): The pooling rule established in v00.19.49 states: for melee, if at least one squad member is adjacent to the target, all in-formation members contribute their ATK regardless of individual distance. Both applyChargeAttacks() in engine.ts and executeAttack() in actions.ts were instead filtering the pool to only members with dist === 1, meaning followers who landed behind the point member during a charge contributed nothing. Fix: split eligibility into a gate check (at least one adjacent member) and a pool computation (all in-formation members). Charge attacks now correctly fire the full squad's pooled ATK. Normal melee attacks receive the same correction. No changes to ranged pooling, engine types, store, or UI.

v00.19.85

April 2026

P-13 — Unit Info Panel. Selecting a model now slides a stat panel into the bottom-left corner showing HP, all model stats, weapon profiles, and active status flags.

  • Feature (P-13 — Unit Info Panel): New component app/tactical/_components/unit-info-panel.tsx. Pure Fabrication — receives model, definition, and player as props from page.tsx; no store access. Renders null when nothing is selected so the panel is fully absent (not hidden). When present it slides in from the left over 200 ms via CSS translateX transform. Content: unit name and faction/tier header, HP bar (green → amber → red), 3×2 stat grid (MOV/MEL/RNG/DFN/ARM/DOM), weapon section (ATK/PWR/SHRD/DMG + MELEE or NHEX range per weapon), and a conditional status flags row (OVERWATCH, MOVED, SPENT). Left-border accent matches player faction colour (P1 blue, P2 red). page.tsx changes limited to: import, three-line data derivation before return, and one <UnitInfoPanel> render inside the map container. No engine, store, types, or action file changes.

v00.19.84

April 2026

F-07 resolved — the version string is now visible on the Battle Size Selector screen, not only inside the game HUD.

  • Fix (F-07 — version not visible on Battle Size Selector): Added a small version label below the ← RETURN TO HOME link in BattleSizeSelector. Renders APP_VERSION (already imported) in Cinzel, extremely muted (#2a2218), 0.4rem, centred. No visual weight — present for dev reference without drawing attention. Display-only change in page.tsx; no engine, store, or action file changes.

v00.19.83

April 2026

F-06 resolved — the Battle Size Selector now has a ghost “Return to Home” link so players can back out before committing to a battle.

  • Fix (F-06 — no exit from Battle Size Selector): The selector previously had no way to leave the screen once entered — the only action was DEPLOY FORCES. Added a ghost ← RETURN TO HOME anchor link directly below the DEPLOY FORCES button, pointing to /. Styled in Cinzel, very muted gold-on-dark, brightens subtly on hover. Display-only change in page.tsx; no engine, store, or action file changes.

v00.19.82

April 2026

F-05 resolved — the “RITE X OF Y” counter now shows a stable denominator that cannot shrink mid-round as models die.

  • Fix (F-05 — unstable rite counter): totalRites was recomputed live each render by counting squads with at least one living model. If a squad was wiped out mid-round, the denominator would drop — turning “RITE 2 OF 4” into “RITE 2 OF 3” unexpectedly. Fix: added roundTotalRites: number | null to TacticalUIState in store.ts. The count is snapshotted in initBattle (round 1) and at every round boundary in advanceRitePhase (rounds 2+), using the post-activation newBattle state so dead squads are already excluded from the next round's total. The HUD reads roundTotalRites directly; the live count is retained only as a null-fallback. No changes to engine, actions, or AI files.

v00.19.81

April 2026

Charge cancel fix — pressing CANCEL during charge-target or charge-dest mode now fully clears charge state instead of leaving the player locked.

  • Fix (charge mode cancel lock): clearSelection() was resetting attackMode and unitMoveMode but not the four charge state fields (chargeTargetMode, chargeDestMode, chargeTargetSquadId, chargeDestinations). When the player pressed CANCEL during charge targeting, selectedModelId cleared (hiding the CANCEL button) while chargeTargetMode stayed true, routing all subsequent hex clicks into resolveChargeOnTarget and making the interface unresponsive. Fix: all four charge state fields added to the clearSelection() reset. Additionally, the CANCEL button visibility condition in page.tsx now includes chargeTargetMode and chargeDestMode as explicit guards so the button is always visible while any charge mode is active.

v00.19.80

April 2026

F-04 resolved — pressing ATTACK when no valid targets exist now shows a bottom-bar message instead of silently doing nothing.

  • Fix (F-04 — silent no-op on ATTACK): When initiateAttack() computed zero attackable hexes, the store previously returned silently with no player feedback. Now sets a statusMessage UI state field to “NO VALID TARGETS IN RANGE”. The bottom bar centre displays this message in red, overriding the normal mode text, until the player changes selection or cancels. The statusMessage field is cleared on any selection change, mode transition, or successful attack-mode entry. No changes to engine, actions, or AI files.

v00.19.79

April 2026

F-03 resolved — combat log panels consolidated into a single stacked column so overwatch, attack, and charge results never overlap.

  • Fix (F-03 — log panel overlap): Overwatch, attack, and charge log panels previously rendered as three independent absolutely-positioned elements — the overwatch panel at right-80 and attack/charge at right-4. When both overwatch and attack results existed simultaneously (e.g. overwatch fires during a move, then the player attacks), the panels could visually clash or overlap at narrow viewports. All three panels now live inside a single flex-column wrapper anchored at bottom-16 right-4, stacking vertically with an 8px gap. Each panel retains its own conditional visibility and styling. Display-only change in page.tsx.

v00.19.78

April 2026

F-02 resolved — active squad display in bottom HUD now shows the unit definition name instead of the raw store key.

  • Fix (F-02 — active squad display): The bottom HUD bar previously rendered the raw activeSquadId store key (e.g. “P1-SKM”). Now resolves through battle.squads[id].definitionIdbattle.unitDefinitions[defId].name and displays the uppercased unit definition name (e.g. “SKIRMISHER”). Falls back to the raw ID if the lookup fails.

v00.19.77

April 2026

F-01 resolved — models auto-re-select after move or attack so the player sees remaining action options.

  • Fix (F-01 — post-action selection): moveSelectedModel() and resolveAttackOnTarget() in store.ts now re-select the acting model after resolution instead of clearing to null. After a move, the player immediately sees attack / charge / overwatch buttons. After an attack, the player sees the ADVANCE button and current phase state. Guarded for model death — if the model was slain by overwatch or cursed terrain during the move, selection correctly clears.

v00.19.76

April 2026

AI token rendering fix — per-model independent tween system replaces single shared RAF. P2 model tokens now animate smoothly during AI turns.

  • Fix (move animation): The old tween system used a single shared RAF loop that was cancelled and restarted whenever any model moved. With 350ms AI move delays vs 1000ms tween duration, earlier models' animations were interrupted mid-flight, leaving tokens visually displaced from their hexes. Replaced with a per-model independent tween system: each model tracks its own animation start time in a persistent activeTweensRef map, and a single self-terminating RAF loop processes all active tweens concurrently. New moves add entries without cancelling in-progress animations. Both P1 and P2 tokens now animate smoothly regardless of move timing.

v00.19.75

April 2026

F-08 resolved — only one squad can be active at a time during the battle phase.

  • Fix (F-08 — multi-squad activation): Added a guard in selectModel() that blocks selection of models from a different squad while a rite is already in progress (ritePhase !== null). The active squad now locks out all others until END RITE is pressed. Re-selecting models within the active squad (F-09 fix) is preserved.

v00.19.74

April 2026

F-11 resolved — hex cells render as regular hexagons and mouse-to-hex mapping is accurate across the full board.

  • Fix (F-11 — hex distortion): Restored preserveAspectRatio="xMidYMid meet" in tactical-map.tsx. The v00.19.71 switch to "none" fixed click alignment but stretched hexes non-uniformly when the browser was wider than the SVG's natural aspect ratio.
  • Fix (F-11 — click alignment): All three mouse-to-SVG coordinate conversions (hover, wheel zoom, pan) now use svg.getScreenCTM().inverse() via DOMPoint.matrixTransform() instead of manual scaleX/scaleY math. This correctly accounts for the centering offset (letterboxing) that "xMidYMid meet" introduces, making hover and click registration accurate across the full board width including P2's high-q columns.
  • Simplified initial camera fit: With "meet", the browser handles fitting the viewBox to the container. Initial zoom seeded to 1.0 instead of computing width / SVG_W.

v00.19.73

April 2026

Tech debt — code path unification (TD-01 Phase 2 complete). Human and AI now share the same move and attack functions.

  • Unified move pipeline: New executeMoveWithEffects() in actions.ts composes applyMove → checkAndFireOverwatch → applyCursedEntryIfNeeded into a single call. Replaces 4 duplicated call sites (individual move, unit move, charge landing, AI move). Human and AI token movement now uses identical mechanical code.
  • Unified attack pipeline: New executeAttack() in actions.ts handles weapon selection, pooled ATK, cover, cursed ARM penalty, OOF bonus, damage, and prune in one call. Replaces 2 duplicated call sites (player attack, AI attack). Human and AI attack resolution now uses identical mechanical code.
  • Bug fix (AI cover): AI attacks previously hardcoded cover: 'none', ignoring terrain cover bonuses on the target hex. The shared executeAttack() now correctly computes cover from terrain for both human and AI attacks.
  • TD-01 resolved: No parallel code paths remain for P1 vs P2 movement or attack. Both actors submit decisions through the same shared functions. store.ts reduced to ~420 lines. actions.ts expanded to ~170 lines. ai-orchestrator.ts reduced to ~140 lines.

v00.19.72

April 2026

Tech debt — store.ts mechanical decomposition (TD-02 Phase 1 complete).

  • Refactor (TD-02): store.ts decomposed from ~780 lines to ~500 lines. Four pure helper functions extracted to lib/tactical/actions.ts (getTerrainLookup, applyCursedEntryIfNeeded, computeMoveHighlights, buildDeployFormation). AI turn orchestrator and AI deploy scheduler extracted to lib/tactical/ai-orchestrator.ts (runAITurnOrchestrator, scheduleAIDeploy). Store keeps thin wrappers. Zero logic changes — pure mechanical extraction. All 23 features validated intact.

v00.19.71

April 2026

P2 hex alignment fix — mouse-to-hex mapping now correct across the full battlefield width (pending play-test).

  • Fix candidate (F-11 root cause): tactical-map.tsx switched from preserveAspectRatio="xMidYMid meet" to preserveAspectRatio="none". The previous value caused the browser to apply a hidden horizontal centering offset when fitting the SVG into its container. This offset was not accounted for in the mouse-to-hex inverse calculation, causing clicks and hover events on P2's high-q side to register on the wrong hex. With none, the viewBox maps directly to the container and the existing zoom/pan inverse math is correct across the full width.

v00.19.70

April 2026

Initial camera — mount-time zoom seeding so the full battlefield width fits on load (pending play-test).

  • Fix candidate (F-11): tactical-map.tsx now seeds initial zoom via a mount-time useEffect that reads the SVG element's rendered width with getBoundingClientRect() and sets zoom = svgWidth / SVG_W. At zoom 1.0, only P1's side of the map was visible on ~1270px screens; P2 tokens appeared off-screen. No ResizeObserver, no window.innerWidth, no preserveAspectRatio changes — one effect, runs once on mount. Awaiting play-test confirmation.

v00.19.69

April 2026

Rite phase preservation — re-selecting a model mid-rite no longer resets the squad back to the edict phase.

  • Fix (F-09): selectModel in store.ts now preserves the current ritePhase when the player re-selects a model belonging to the squad whose rite is already in progress. Previously, clicking any model mid-rite reset ritePhase to 'edict', stranding the player in the shoot phase with no actionable buttons.

v00.19.68

April 2026

Token alignment fix — tokens now always render centered in their hex cells.

  • Fix: tactical-map.tsx model tokens, squad-mate rings, and active model indicator now fall back to hexToPixel(model.position) when no tween is active, instead of using potentially stale visualPositions values. Tokens are now always aligned with their hex cells when not animating.

v00.19.67

April 2026

AI round 2+ fix — AI now correctly takes its turn every round, including when it has initiative at the start of a new round.

  • Fix: runAITurn endActivation branch in store.ts now detects when a new round starts with currentPlayer === 2 and re-triggers the AI instead of clearing isAITurn. Previously the AI would only run when triggered by the player ending their rite, causing it to skip its turn entirely when it had initiative at round start.
  • Fix: buildDevState in page.tsx was hardcoding maxRounds: 4 regardless of the battle size chosen. Standard (6 rounds) and Grand (8 rounds) games now use the correct round count.

v00.19.66

April 2026

AI & Coin Flip Fixes — AI now moves and attacks; coin flip correctly handled for single-player.

  • Fix: Removed bestMoveDist >= currentDist guard in ai.ts that prevented the AI from moving when already near an enemy.
  • Fix: Removed !model.hasMoved guard in runAITurn (store.ts) that blocked follower moves after the point member moved.
  • Fix: coin-flip-modal.tsx now correctly handles single-player vs AI. If Player 1 wins the toss they see choice buttons (I GO FIRST / OPPONENT GOES FIRST). If the AI wins the toss, it auto-chooses after a short delay and the player sees the result.

v00.19.65

April 2026

Move Animation Polish — squad-mate rings and active model indicator now track the token body throughout the move animation.

  • Fix: squadMateRings and activeModelIndicator memos in tactical-map.tsx now read visualPositions[id] (the live tween pixel position) instead of hexToPixel(logical). Both rings track the token body throughout the animation rather than snapping to the destination hex while the token is still mid-tween. Dep arrays updated accordingly.
  • Chore: File header comment in tactical-map.tsx updated to reflect current state. Roadmap housekeeping from v00.19.64: Battle Size Selector added to COMPLETE array; Hex-Grid Core Engine detail updated.

v00.19.64

April 2026

Battle Size Selector — choose Skirmish, Standard, or Grand before deploying forces.

  • Feature: BattleSizeSelector modal renders on mount before initBattle() is called. Three options: Skirmish (4 rounds), Standard (6 rounds), Grand (8 rounds). Skirmish pre-selected. Confirm button labelled DEPLOY FORCES.
  • Feature: buildDevState() now accepts a maxRounds parameter (default 4) instead of hardcoding it. The chosen size is passed through handleSizeConfirm on the page.
  • Fix: PLAY AGAIN now returns to the size picker instead of auto-restarting with the previous size.
  • Chore: Removed stale "Win Condition — Per-Round VP Scoring" entry from roadmap HIGHEST array (completed in v00.19.63).

v00.19.63

April 2026

VP Scoring — game now runs to 4 rounds (Skirmish) and the HUD tracks objective control in real time.

  • Feature: maxRounds set to 4 (Skirmish) in buildDevState(). The game now ends by round limit, surfacing the existing VP and DOM tiebreak logic in checkWinConditions() that was previously unreachable at 99 rounds.
  • Feature: Objective control indicators in the HUD centre strip. One dot per objective, coloured blue (P1), red (P2), or dim grey (uncontrolled). Updates after every round via scoreObjectives(). Dot label shows the first letter of the objective name. Hidden during deploy phase.

v00.19.62

April 2026

Command Phase — pre-battle coin flip, round initiative system, and HUD initiative indicator.

  • Feature: advanceRound() now wires initiativePlayer correctly. Whoever went second last round goes first next round (strict alternation). currentPlayer at round start is set from initiativePlayer, not the previous player's flip.
  • Feature: Added coinFlipPending: boolean to BattleState. Set true at deploy→battle transition. Cleared by resolveCoinFlip() after player choice.
  • Feature: CoinFlipModal component. Appears once, pre-battle, after all squads are deployed. Auto-resolves a coin flip on mount, reveals the winner, and lets the winner choose who activates first in Round 1. Cannot be skipped.
  • Feature: Initiative HUD indicator. · INITIATIVE P1 or · INITIATIVE P2 displayed in the HUD centre strip during battle, coloured to the player's theme colour.
  • Feature: Round toasts now include a PLAYER X STRIKES FIRST line coloured to the initiative player, so every round transition communicates who goes first.

v00.19.61

April 2026

Deploy → battle transition fix — player can no longer get stuck in attack mode with no valid targets at battle start.

  • Fix: initiateAttack() now guards against entering attack mode when attackHighlights is empty. Previously clicking ATTACK with no enemies in range set attackMode = true with an empty highlight array, leaving the player stuck with no red targets and only CANCEL as an escape.
  • Fix: placeSquad() now calls clearedUI() when transitioning to phase: 'battle'. Ensures all deploy-phase UI state (ghost positions, selection flags) is fully reset the moment battle begins.

v00.19.60

April 2026

Deploy token snap fix — placed units now appear correctly on their hex instead of animating in from off-screen.

  • Fix: seededRef block in tactical-map.tsx now skips off-grid models (q < 0) when initialising visualPosRef. Previously hexToPixel(-1,-1) produced an off-screen pixel position which became the tween start point.
  • Fix: Move animation effect now detects off-grid → on-grid transitions (prev.q < 0) and snaps directly to the real hex pixel position instead of tweening from the invalid off-screen origin.

v00.19.59

April 2026

AI deploy recursion fix — P1 squads no longer place in P2's zone due to activePlayer being wrong when the player clicks.

  • Fix: placeSquad() was calling itself recursively via the AI auto-deploy callback without a guard, causing activePlayer to flip to P2 before P1's second placement. P1's second click then landed in P2's zone.
  • Fix: Added isAIDeploy?: boolean parameter to placeSquad(). When true, the AI auto-deploy callback is suppressed. The AI always passes true; human clicks always use the default false.

v00.19.58

April 2026

Deploy formation placement fix — squads now place correctly in the deploy zone instead of drifting to the centre of the map.

  • Fix: buildDeployFormation() in store.ts was using hand-rolled odd-r offset neighbour direction tables (DIRS_EVEN / DIRS_ODD) which produced incorrect neighbours for this coordinate system. Replaced with hexNeighbors() + withinGridBounds() from hex-utils.ts, which use the correct cube-coordinate conversion.
  • Chore: hexNeighbors and withinGridBounds added to the hex-utils import in store.ts.

v00.19.57

April 2026

AI Deploy — P2 automatically places its squads after each P1 placement, mirroring the alternating deploy sequence.

  • Feat: After P1 places a squad, placeSquad() schedules a 400ms delayed callback that reads fresh state, picks the first entry in unplacedP2, finds a valid formation anchor in P2's zone (sorted toward centre row for variety), and calls placeSquad() on P2's behalf.
  • Feat: AI deploy respects all the same guards as human placement — zone keys, occupied hexes, formation availability. Falls back silently if no valid anchor exists.
  • Feat: When P2's auto-placement completes, the panel and activePlayer flip back to P1 for the next squad, matching the intended alternating deployment sequence.

v00.19.56

April 2026

Deployment Zones Slices 3–4 — hover ghost preview and squad placement. Players can now fully deploy their warbands before battle begins.

  • Feat: placeSquad(q, r) store action — places the selected squad in formation starting at the clicked hex (BFS fills adjacent zone hexes), removes it from unplacedP1/P2, flips activePlayer, and auto-selects the next squad for the incoming player. When all squads are placed, transitions to phase: 'battle'.
  • Feat: hoverDeployHex(q, r) store action — computes formation ghost positions when hovering a valid deploy zone hex with a squad selected. Stored in UI-only deployGhostPositions state.
  • Feat: buildDeployFormation() pure helper in store.ts — BFS from anchor hex, finds N adjacent free hexes within the deploy zone for formation placement.
  • Feat: selectHex dispatch updated — during deploy phase with a squad selected, routes clicks to placeSquad instead of model selection.
  • Feat: page.tsx wired — onHexHover routes to hoverDeployHex during deploy; deployGhostPositions passed to TacticalMap as squadMoveGhosts, reusing the existing ghost rendering pipeline.
  • Feat: After each placement, the panel automatically selects the next squad for the new active player — no manual re-selection needed.

v00.19.55

April 2026

Deployment Zone Orientation Fix — zones now render as left/right columns matching the horizontal battlefield layout.

  • Fix: buildDeploymentZoneKeys() in maps.ts was iterating over rows (r), producing top/bottom bands. Corrected to iterate over columns (q): P1 gets cols 0–3, P2 gets cols 18–21, all rows full height.
  • Fix: maps.ts file header comment updated — now correctly documents the horizontal battlefield orientation (P1 left / P2 right).
  • Fix: Shattered Crossroads map comment updated to reflect left/right deployment columns instead of top/bottom rows.

v00.19.54

April 2026

Deployment Zones Slice 2 — Squad Selection Panel. The active player sees their unplaced squads and can click one to select it for placement.

  • Feat: New DeployPanel component (app/tactical/_components/deploy-panel.tsx) — left-edge overlay panel showing the active player's unplaced squads as clickable cards.
  • Feat: selectSquadForDeploy(squadId | null) action added to store.ts — sets or toggles deployment.selectedSquadId. No-op outside deploy phase.
  • Feat: buildDevState() updated: models start at off-grid sentinel (q=-1, r=-1) instead of pre-placed positions. deployment.unplacedP1 / unplacedP2 populated from all squad IDs. Two squads per side (Skirmisher + Line Infantry).
  • Feat: tactical-map.tsx filters off-grid models (position.q < 0) from token rendering so unplaced squads are invisible on the map until placed.
  • Visual: Panel shows player colour accent, squad name, tier label, model count, “NEXT TO PLACE” badge on the first card, and “&check; SELECTED” indicator. Clicking the selected card again deselects it.

v00.19.53

April 2026

Deployment Zones Slice 1 — zone highlight. P1 and P2 deployment bands are visually tinted on the map whenever the game is in the deploy phase.

  • Feat: deployment?: DeploymentState added to BattleState in types.ts (optional field — safe for battle-phase harness).
  • Feat: buildDeploymentZoneKeys() imported into tactical-map.tsx. Two memoised zone sets (P1 / P2) are computed from mapConfig whenever state.phase === 'deploy'; empty sets otherwise.
  • Visual: Each hex in a deployment zone receives a semi-transparent tint overlay polygon — blue (rgba(42,106,170,0.18)) for P1 rows 0–3, red (rgba(170,42,42,0.18)) for P2 rows 14–17. No-man's-land (rows 4–13) receives no tint.
  • Dev harness: buildDevState() in page.tsx now sets phase: 'deploy' so the zones are immediately visible at localhost. Battle logic and AI turn are untouched.

v00.19.52

April 2026

Out-of-Formation Penalty — OOF models restricted to Move only; isolated models take increased damage on every unsaved wound.

  • Feat: isActionLegalForModel() pure helper added to engine.ts — blocks attack, charge, and overwatch for any model with outOfFormation === true. Move is always permitted so the model can rejoin.
  • Feat: resolveAttack() gains targetOutOfFormation: boolean parameter (default false). When true, damage per unsaved wound becomes weapon.DMG + 1 instead of weapon.DMG.
  • Feat: OOF damage penalty wired across all four attack paths: player attack (resolveAttackOnTarget), overwatch (checkAndFireOverwatch), charge (applyChargeAttacks), and AI attack (runAITurn).
  • Feat: Action gate wired in store: initiateAttack, overwatchSelectedModel, and initiateCharge each call isActionLegalForModel() before proceeding. OOF models silently fail these actions.
  • Visual: Dashed red ring on OOF tokens was already implemented in tactical-model-token.tsx — no visual changes required.

v00.19.51

April 2026

hasMoved Flag — per-model one-move-per-activation guard layered on top of rite phase gating.

  • Feat: hasMoved: boolean added to LiveModel in types.ts. Initialised false in buildDevState().
  • Feat: applyMove() in engine.ts sets hasMoved: true on the moved model at move time. Covers individual moves, unit moves (point member & all followers), and charge landing moves.
  • Feat: getMovableHexes() returns an empty Map immediately when model.hasMoved === true. Both this guard and isActionLegalForPhase() must pass for a move to be legal.
  • Feat: advanceRound() resets hasMoved: false on all models alongside hasAttacked, hasCharged, and onOverwatch.
  • Feat: AI move path in runAITurn() skips emitting a move action for any model where hasMoved === true. AI still bypasses rite phase gating.

v00.19.50

April 2026

Phase System Hard Enforcement — store-layer phase gating. Illegal actions are silent no-ops per rite phase.

  • Feat: isActionLegalForPhase() pure helper added to engine.ts — maps 4 action types (move / attack / charge / overwatch) against ritePhase; returns bool.
  • Feat: moveSelectedModel, initiateUnitMove, resolveUnitMove, hoverHex guarded to Move phase only (pass when ritePhase === null || 'move').
  • Feat: overwatchSelectedModel guarded to Move phase only (same condition).
  • Feat: initiateAttack, resolveAttackOnTarget guarded to Shoot + Fight phases (pass when ritePhase === null || 'shoot' || 'fight').
  • Feat: initiateCharge, resolveChargeOnTarget, resolveChargeLanding guarded to Fight phase only (pass when ritePhase === null || 'fight').
  • Unchanged: selectModel, selectHex, advanceRitePhase never phase-gated. AI path (runAITurn) unchanged — bypasses rite system entirely.

v00.19.49

April 2026

Pooled Unit Action — attacks now resolve as a squad action. ATK dice pooled from all eligible in-formation members.

  • Feat: resolveAttack() accepts an explicit atkPool parameter (default = weapon.stats.ATK) — fully backward-compatible; all existing callers with no pooling argument unchanged.
  • Feat: Eligibility rules — in-formation members only; ranged attack requires at least one member in range; melee attack requires at least one adjacent member. Out-of-formation members excluded from pool.
  • Feat: resolveAttackOnTarget() in store computes eligibleMembers, sums ATK into atkPool, passes pool to resolveAttack(), and marks hasAttacked on all squad members after resolution.
  • Feat: AI attack path in runAITurn() updated with the same pooling logic and hasAttacked sweep.
  • Feat: Charge attacks pooled via applyChargeAttacks() — ATK pooled from all eligible adjacent in-formation members; all squad members marked hasAttacked.
  • Unchanged: Overwatch fires per single model using the default atkPool path — no pooling on reaction shots.

v00.19.48

April 2026

Soft Phase Gating — action buttons are now phase-aware. Only contextually valid actions appear per rite phase. No engine changes.

  • Feat: MOVE UNIT button gated to ritePhase === null || ritePhase === 'move' — hidden in Shoot, Fight, and Edict phases.
  • Feat: ATTACK button gated to ritePhase === null || ritePhase === 'shoot' || ritePhase === 'fight' — hidden in Move and Edict phases.
  • Feat: CHARGE button gated to ritePhase === null || ritePhase === 'fight' — visible in Fight phase only.
  • Feat: OVERWATCH button gated to ritePhase === null || ritePhase === 'move' — visible in Move phase only.
  • Feat: Edict phase early-return at top of action IIFE — all three action buttons (ATTACK, CHARGE, OVERWATCH) hidden when ritePhase === 'edict'.
  • Unchanged: CANCEL always visible when selectedModelId || attackMode; ADVANCE TO [NEXT] / END RITE always visible when rite is active. No engine changes — soft enforcement only.

v00.19.47

April 2026

Phase Transition Banner — narrow amber sweep banner fires at the top of the map on each phase advance within a rite, 1s auto-dismiss, click-to-dismiss.

  • Feat: Phase toast pushed in advanceRitePhase() on next !== null branch — fires on every phase advance within a rite; does not fire on END RITE (rite-complete branch unchanged). Kind: 'phase', message: ‘▸ MOVE PHASE’ etc., duration: 1000ms.
  • Feat: PhaseBanner component added to page.tsx. Positioned absolute at top edge of map div, full width, 28px height, z-index 35. Slides in/out from the top via translateY(-100%) → translateY(0), 200ms transitions. Subdued amber (#c8a840) Cinzel text, dark semi-transparent background.
  • Feat: Toast routing in page.tsx updated — front toast with kind === 'phase' renders PhaseBanner; kind === 'round' or 'player_turn' renders RiteToast. Mutually exclusive; only the front toast renders.

v00.19.46

April 2026

Player Turn Announcement Toast — centre-screen toast fires when the active player changes, 2s auto-dismiss, click-to-dismiss.

  • Feat: player_turn toast pushed in advanceRitePhase() on player flip — captures playerBefore prior to completeSquadActivation(), compares against newBattle.currentPlayer, pushes into existing newToasts[] array.
  • Feat: player_turn toast pushed in runAITurn() endActivation branch using the same pattern — fires when AI finishes and control returns to Player 1. Pushed into existing aiNewToasts[] array.
  • Feat: Toast message is PLAYER N with sub-message YOUR ORDERS, COMMANDER. player field set correctly for future colour differentiation (deferred). Duration: 2000ms.
  • Feat: RiteToast component (from Slice 2) handles rendering — no new component needed. kind === 'player_turn' already included in the front-toast render condition.
  • Note: Round toast and player-turn toast can both fire on the same END RITE if the round also advanced — both enter newToasts[], queue drains one by one.

v00.19.45

April 2026

Round Announcement Toast — centre-screen toast fires at the start of each new round, gold styling, 2s auto-dismiss, click-to-dismiss, atmospheric sub-message rotates by round.

  • Feat: BattleToast interface added to types.ts with id, kind ('round' | 'player_turn' | 'phase'), message, subMessage, player, and durationMs fields.
  • Feat: toasts: BattleToast[] queue added to store UI state. UI-only — not stored in BattleState. Cleared on clearedUI() and initial state.
  • Feat: popToast() and dismissToast(id) actions added to store. popToast removes the front item; dismissToast filters by id for click-to-dismiss.
  • Feat: Round toast pushed in advanceRitePhase() and runAITurn() on round advance detection (roundAfter > roundBefore). Message: ROUND N · OF M. Sub-message rotates by round (R1–R4 atmospheric lines; R5+ “The reckoning continues.”). Duration: 2000ms.
  • Feat: RiteToast component added to page.tsx. Centre-screen absolute, fades in 150ms, holds, fades out 150ms, calls popToast() on exit. Click anywhere on toast calls dismissToast(id). Only the front toast renders; queue drains one by one. Positioned inside map div (now position: relative).

v00.19.44

April 2026

Rite Phase System — Slice 1 & AI Unit Move — Rite phases live and visible. Each squad activation progresses through Edict → Move → Shoot → Fight. Version string and AI unit move upgraded.

  • Feat: RitePhase type added to types.ts: 'edict' | 'move' | 'shoot' | 'fight'. ritePhase: RitePhase | null added to BattleState and initialised to null in buildDevState(). getNextRitePhase() pure helper added to engine.ts — returns the next phase or null when the rite is complete, with skip logic for units without ranged or melee weapons. advanceRound() reset block clears ritePhase to null.
  • Feat: advanceRitePhase() action replaces endActivation() entirely. Advances through Edict → Move → Shoot → Fight; calls completeSquadActivation() when phase returns null. selectModel() sets ritePhase: 'edict' when a new squad begins its rite.
  • Feat: HUD rite counter — RITE X OF Y displayed in top strip (squads activated this round vs total living squads). Hidden between rites.
  • Feat: Phase label shown in action bar (e.g. · MOVE PHASE ·). Edict phase shows atmospheric flavour text. ADVANCE TO [NEXT PHASE] / END RITE button replaces END ACTIVATION, label updates correctly per phase and weapon profile.
  • Fix: HUD version string replaced hardcoded value with APP_VERSION imported from lib/version.ts.
  • Feat: AI upgraded to move full squads. takeTurn() in ai.ts now calls getUnitMoveDestinations() — point member picks the destination closest to the nearest enemy; all non-stranded followers emit move actions to their formation slots. Replaces old point-member-only approach.

v00.19.43

April 2026

Map System Cleanup — terrain cover correction, follower terrain cost, and roadmap housekeeping.

  • Fix: TERRAIN_COVER['high_ground'] corrected from 'light' to 'none' in lib/tactical/types.ts. High ground grants a ranged attack bonus (deferred), not a defence save. The incorrect value was causing attackers targeting units on high ground to face a spurious +1 ARM modifier.
  • Fix: Follower reachability in getUnitMoveDestinations() upgraded from flat BFS to reachableHexesCost() when a terrainLookup is present. Followers now pay 2 movement points to enter forest/ruins and cannot be placed on water hexes. Followers that cannot reach any valid slot are stranded as before.
  • Fix: Same follower terrain cost fix applied to getChargeDestinations(). Function signature updated to accept an optional terrainLookup parameter. resolveChargeOnTarget() in store.ts now passes the terrain lookup through.
  • Docs: Map System — v1 roadmap entry moved from HIGHEST (Not Started) to COMPLETE. All four Map System phases are done.
  • Feat: Cursed terrain now mechanically active. Entry: 1 flat HP on stepping onto a cursed hex (no save, bypasses ARM). Ongoing: −1 ARM on all saves (melee and ranged) while occupying a cursed hex. Applies to all units including vehicles. Both values are PLACEHOLDER — balance pass required. Constants CURSED_ENTRY_DAMAGE and CURSED_ARM_PENALTY in lib/tactical/types.ts. applyCursedEntryDamage() and getCursedArmPenalty() added to engine.ts. Entry wired in all three move paths (individual, unit-move, charge). ARM penalty wired in player attack, overwatch (engine), and AI attack paths.

v00.19.42

April 2026

Grid Resize — battlefield expanded from 17×15 to 22×18 for better pacing and flanking room.

  • Feat: GRID_COLS 17→22, GRID_ROWS 15→18 in lib/tactical/hex-utils.ts. SVG dimensions auto-recalculate from the constants — no other rendering changes required.
  • Feat: All three maps updated to 22×18 with deploy zones bumped from 3 to 4 rows each (10 open rows between deployment zones, up from 9). All terrain coords and objectives rescaled proportionally.
  • Feat: Dev scenario model positions updated to sit within the new deployment zones (P1 rows 0–3, P2 rows 14–17).
  • Design: Sized against WH40K reference — standard 44×60” table, Space Marine MOV 6”, contact turn 2–3. At T2 MOV 4 with 10 open rows, both sides close in 3–4 turns, which matches the intended pacing.

v00.19.41

April 2026

Terrain Movement & Cover — map terrain now affects movement cost, line of sight, and cover saves.

  • Feat: getMovableHexes() in engine.ts accepts an optional terrainLookup. When provided, switches from flat BFS to Dijkstra (reachableHexesCost): water = impassable (hard-blocked), forest/ruins = 2 movement points, open/high_ground/cursed = 1. Falls back to flat BFS when no lookup is supplied.
  • Feat: getAttackableHexes() accepts an optional terrainLookup and performs an LOS check via hexLineDraw(). Intermediate hexes with ruins or water terrain block line of sight, removing the target from the attackable set. Melee (distance = 1) skips LOS entirely.
  • Feat: checkAndFireOverwatch() accepts an optional terrainLookup and looks up the target's hex terrain → TERRAIN_COVER[terrain] → passes the result to resolveAttack() instead of hardcoded 'none'.
  • Feat: resolveAttackOnTarget() in store.ts replaced const cover = 'none' with a live terrain lookup against the target's hex. Forest = light cover (+1 ARM), ruins = heavy cover (+2 ARM), all others = none.
  • Feat: getUnitMoveDestinations() wired to pass terrainLookup through to the point-member getMovableHexes() call, so formation moves respect terrain costs.
  • Arch: store.ts gains a getTerrainLookup(battle) helper that builds and returns the lookup from battle.mapId via getMap() + buildTerrainLookup(). All terrain-aware call sites use this helper. Graceful degradation: if mapId is unset or unknown, helper returns undefined and every function falls back to its pre-terrain behaviour.
  • Arch: applyChargeAttacks() cover is intentionally left as 'none' — cover does not apply to melee per design.

v00.19.40

April 2026

AI Turn Step Visibility — AI actions now play out one at a time with animated delays, making the opponent's turn readable.

  • Feat: runAITurn() in lib/tactical/store.ts replaced its synchronous for loop with a recursive applyNextAction(index) closure. takeTurn() still produces the full BattleAction[] plan in one pass; actions are then applied one at a time with per-type delays: 350 ms for move, 500 ms for attack, 0 ms for endActivation.
  • Feat: Each step commits to the Zustand store immediately after resolving, triggering a React re-render so the map updates live between actions. lastAttackResult is set after each attack so the combat log panel refreshes in real time.
  • Feat: isAITurn remains true throughout the sequence, blocking player input until endActivation fires as the final action. Slain-model guards added to both move and attack branches — if a model is removed by a previous overwatch shot mid-sequence, that action is skipped safely.
  • Arch: No changes to ai.ts, engine.ts, or page.tsx. No async/await introduced — the store remains synchronous between state commits; only setTimeout is used for the inter-action delay.

v00.19.39

April 2026

Charge crash fix — withinGridBounds called with a single coord instead of an array in getChargeDestinations.

  • Fix: TypeError: hexes.filter is not a function thrown whenever charge landing hexes were computed. Root cause: two call sites in getChargeDestinations() passed a single HexCoord to withinGridBounds(), which expects HexCoord[]. Fixed by replacing both calls with inline bounds guards (nb.q >= 0 && nb.q < GRID_COLS && ...), consistent with every other bounds check in the engine.

v00.19.38

April 2026

First Playable AI Battle — greedy AI opponent completes a full T1 vs T1 combat loop end-to-end.

  • Feat: Greedy AI engine (lib/tactical/ai.ts) — pure module, no React, no store import. Exports takeTurn(battle): BattleAction[]. Strategy per activation: find nearest living enemy model by hex distance, move point member toward it, attack any enemy in range, end activation. No charge or overwatch in v1.
  • Feat: AI turn wired into the store. After the player ends activation and the turn flips to Player 2, isAITurn is set and runAITurn() fires after a 300 ms delay. Player input is blocked during AI execution via early return in selectHex(). HUD shows “AI THINKING…” indicator while active.
  • Feat: Draw outcome added. WinResult union extended with { winner: null; reason: 'max_rounds' }. True VP + DOM tie at round limit now surfaces a DRAW overlay instead of defaulting to Player 1.
  • Feat: Win/Draw overlay upgraded — shows final VP for each player, survivor count per side, and round reached. Draw variant uses a neutral heading and border color.
  • Chore: Dev scenario cleaned to a pure T1 vs T1 mirror match: p1-skm (5 ph-skirmisher, q=1–2) vs p2-skm (5 ph-skirmisher, q=13–14). Removed p1-line, p2-brute, p2-archer, and all test visual states. maxRounds set to 4 (Skirmish).

v00.19.37

April 2026

Charge Action — melee squads can charge into close combat, displacing movement with a bonus-range assault and full Hit → Wound → Save → Damage resolution.

  • Feat: Charge Action fully implemented. initiateCharge() enters charge-targeting mode; resolveChargeOnTarget() moves the charging squad into contact and fires the resolution chain; resolveChargeLanding() handles per-model placement and post-charge state cleanup.
  • Feat: CHARGE button added to action bar. Visible for melee-type squads when the selected model has not yet charged, has not yet attacked, and has not yet activated this round.
  • Feat: Charge log panel renders bottom-right after a charge resolves. Shows per-attacker breakdown: hits / wounds / saves / damage. Mutually exclusive with the attack log panel — each clears the other on open.
  • Arch: hasCharged: false added to all model entries in buildDevState(). hasCharged is set to true on the charging model after resolution and is reset with all other per-round flags at round end.

v00.19.36

April 2026

Overwatch reaction firing — enemy moves trigger immediate automatic attacks from overwatch models.

  • Feat: Overwatch reaction firing fully implemented. When a model moves and lands within range of an enemy model on overwatch, that overwatch model fires immediately using the full Hit → Wound → Save → Damage resolution chain. Overwatch is consumed (onOverwatch → false) on any fire attempt, hit or miss. Multiple overwatch models fire in sequence; if a target is slain mid-sequence, remaining overwatch shots are cancelled. Wired in both individual moves and MOVE UNIT (per-model check as each member lands).
  • Feat: OVERWATCH button added to action bar (purple). Visible when a model is selected, its squad has not yet activated, and the model is not already on overwatch. Calls overwatchSelectedModel() in the store.
  • Feat: Overwatch log panel appears bottom-right when overwatch fires. Shows attacker → target, hits/wounds/saves, and damage for each shot. Supports multiple simultaneous overwatch firings.
  • Fix: getOverwatchTriggers() replaced range-6 placeholder with actual weapon range lookup via selectWeaponForRange() per overwatch model.
  • Arch: Option A — applyMove() stays pure (state-in, state-out unchanged). New checkAndFireOverwatch(state, movedModelId, owningPlayer) engine function is the post-move layer: returns { state, results[] }. Store calls it after every applyMove() call. OverwatchResult interface exported from engine for store and UI consumption.

v00.19.35

April 2026

Formation fix — stable arc on every move, not just the first.

  • Fix: Squad formation collapsed back into a line on the second and subsequent moves. Root cause: the candidate pool was built from neighbors of the growing placed cluster, so the scoring algorithm was always offered a “direct-rear extension” hex that beat the flanks on row-delta. The fix replaces the cluster-expansion approach with a pre-computed slot list derived purely from pointDest geometry: BFS outward from the PM’s destination, collect all non-forward hexes (q ≤ pointDest.q for P1, allowing same-column side hexes), sort by ring distance then row delta, and assign followers greedily to the first unclaimed reachable slot. Slot order is identical on every move because it never references follower current positions — the formation is now stable.
  • Arch: On odd-r offset pointy-top hex grids, only one hex is strictly behind the PM at d=1 on odd rows. The two natural flanks share the PM’s q column (side-adjacent at the same q). Allowing q ≤ pointDest.q (not strict q <) captures these slots, giving a 3-wide arc for a 4-member squad: F1 directly behind, F2 and F3 flanking the PM’s sides.

v00.19.34

April 2026

Formation arc fix — squad move preview now fans correctly behind the leader.

  • Fix: Followers were still collapsing into a diagonal column behind the point member during MOVE UNIT preview. Root cause: the arc scoring in getUnitMoveDestinations() was measuring rowDelta from placed.r (the last-placed squad member) rather than from pointDest.r (the leader's destination row). Scoring from placed.r caused each follower to optimise relative to the previous follower rather than relative to the leader, producing a chain that drifted off-axis. Fix: rowDelta = |neighbor.r − pointDest.r| — always scored against the leader's destination row. Followers now fill directly behind the leader first (score 0), then fan to ±1 flanks (score 1), producing the correct wedge shape.
  • Chore: Removed all window.__unitMoveDebug instrumentation from engine.ts.

v00.19.33

April 2026

Formation scoring attempt — seen set added, debug instrumentation removed.

  • Fix (partial): Added a seen set to getUnitMoveDestinations() to prevent double-scoring of hexes adjacent to multiple placed members. Attempted to measure rowDelta from the placed member rather than the leader — this was incorrect and superseded by v00.19.34.

v00.19.32

April 2026

Model move animation — smooth tween from old hex to new on commit.

  • Feat: When a model moves, its token now tweens smoothly from the old hex center to the new one over 180ms using an easeOut curve, instead of snapping instantly. Newly spawned or deployed units snap to position without tweening.
  • Arch: All animation logic lives in hex-grid.tsx only — engine and store are untouched. Three refs track state: visualPosRef (animated pixel position), prevLogicalRef (last known hex coord per unit, for change detection), tweenRafRef (active RAF handle). A useEffect on units diffs old vs new positions, builds a tween map for movers, and drives a RAF loop that writes to visualPosRef and flushes to visualPositions state each frame. UnitToken accepts optional visualX/visualY props, falling back to hexToPixel when absent. Hit targets (hover circles) remain anchored to logical hex position throughout. Pattern mirrors the RAF tween in use-tower-rush-tick.ts.

v00.19.31

April 2026

Attack range indicator — melee vs ranged badge on hover during targeting.

  • Feat: When hovering an attack target during targeting mode, a small pill badge now appears below the target token indicating which weapon type will fire: ⚔ MELEE (amber, distance ≤ 1) or ↗ RANGED (blue, distance > 1). Badge is purely visual — no game logic changed.
  • Arch: weaponHintMap memo in hex-grid.tsx computes melee/ranged per attack target using hexDistance from the selected unit. Passed as attackWeaponHint prop to UnitToken — only set when the unit is both an attack target and the hovered unit. Single Responsibility: hint derivation is isolated in one memo; UnitToken only renders what it receives.

v00.19.30

April 2026

Unit move UX — first-hex ghost fix.

  • Fix: Ghost tokens now render immediately on unit-move mode entry, even if the cursor has not moved since clicking MOVE UNIT. Root cause: hoverHex fires only on hex change, so the first hex the cursor was already over produced no ghost. Fix: initiateUnitMove in store.ts now seeds unitMoveHoverKey to the first valid destination key instead of null. The seeded preview persists until the cursor moves to the actual hover hex on the first mousemove.
  • Roadmap: All four next-workstream candidates (first-hex ghost fix, model move animation, overwatch reaction firing, attack range indicator) added to the HIGH PRIORITY section of the roadmap.

v00.19.29

April 2026

Unit move ghost preview — faction-colored formation ghosts on hover.

  • Fix: Corrected the root bug preventing ghost tokens from rendering. unitMoveDestinations[key] is a UnitMoveCandidate { positions, stranded }page.tsx was passing the whole object as squadMoveGhosts. Now correctly reads .positions from the candidate.
  • Feat: Ghost tokens now render as faction-colored circles, not generic blue. page.tsx builds a squadMoveGhostColors parallel array keyed to squad member order (point member first, followers in modelIds order) using FACTION_COLORS[faction].fill. Passed to TacticalMap via new squadMoveGhostColors prop.
  • Feat: Ghost rendering in tactical-map.tsx upgraded. Leader ghost: radius 11, full faction fill at 0.75 opacity, gold pip. Follower ghosts: radius 9, faction fill at 0.55 opacity. Hex outline reduced to 0.8px to stay subordinate to the token circles. All ghosts remain pointer-events none.
  • Arch: TacticalMapProps gains squadMoveGhostColors?: string[] — parallel array to squadMoveGhosts, falls back to neutral blue per-ghost if absent or index out of range. Pure Fabrication pattern respected: no faction lookup logic in the map component.

v00.19.28

April 2026

Unit move (point-member formation) — complete.

  • Feat: MOVE UNIT button triggers formation move mode. Point member destinations computed by getUnitMoveDestinations() in engine.ts; followers pack into adjacent hexes around each candidate. Valid destinations shown as blue move highlights.
  • Feat: Clicking a valid destination commits the formation move: applyMove() threaded through point member then each non-stranded follower. Stranded followers (no coherent hex available) are skipped and flagged in the action bar.

v00.19.27

April 2026

Homepage auth gating — session-aware home page and nav.

  • Feat: app/page.tsx converted to an async server component. Calls getServerSession(authOptions) at the top level and renders one of two branches with no client-side flash. Logged-out view retains the full marketing layout (hero, feature grid, lore band, footer) with a “SIGN IN TO PLAY” CTA pointing to /auth/signin. Logged-in view shows a reduced hero with a gold “ENTER THE BATTLEFIELD” CTA pointing to /tactical and secondary links for Lore, Docs, and Profile.
  • Feat: components/SiteNav.tsx — PLAY nav link now conditionally included only when session exists (via existing useSession hook), and href corrected from /game2 to /tactical.
  • Arch: Shared layout elements (hex background, lore band, footer) extracted as pure server components (HexBackground, LoreBand, SiteFooter) and composed into LoggedOutHome and LoggedInHome sub-trees. No 'use client' directive or useSession on the page — zero hydration flash.

v00.19.26

April 2026

Hex-grid tactical game — pointy-top hex orientation.

  • Feat: Hex grid rotated from flat-top to pointy-top orientation. Hexes now have vertices at the top and bottom, and flat faces on the left and right sides. On a horizontal battlefield (P1 left, P2 right), this means the face a unit advances through toward the enemy is perfectly perpendicular — a clean forward face for the firing arc system to build on.
  • Arch: All changes contained in lib/tactical/hex-utils.ts. getHexPoints now uses a 30° (Math.PI/6) angle offset so the first vertex points up. hexToPixel swaps to the pointy-top odd-r offset formula: x-spacing driven by SQRT3 * HEX_SIZE, y-spacing by HEX_SIZE * 1.5, stagger on odd rows. SVG_W and SVG_H updated to match. Cube coordinate conversions updated to odd-r. Hover inverse math in tactical-map.tsx updated to match.
  • Scope: Zero changes to game logic, map configs, terrain data, or token rendering. All hex math consumers (distance, pathfinding, neighbours, coherency) are orientation-agnostic via cube coordinates and are unaffected.

v00.19.25

April 2026

Hex-grid tactical game — horizontal battlefield orientation.

  • Feat: The battlefield is now oriented horizontally — Player 1 deploys along the left edge, Player 2 along the right edge. This fills landscape monitors far more naturally and matches how wargames are traditionally laid out side-to-side.
  • Map: Dev harness deployment zones updated in page.tsx. P1 units now spawn across q=0..1 columns; P2 units across q=15..16 columns. Row spread covers the full vertical range so both sides feel present across the board height.
  • UI: Edge labels in tactical-map.tsx moved from top/bottom positions to left/right sides, rotated ±90° to read vertically alongside their respective edges. No hex-math changes were required — hex-utils.ts is orientation-agnostic.
  • Scope: tactical-map.tsx and page.tsx only. Map configs and terrain data are unaffected.

v00.19.24

April 2026

Hex-grid tactical game — token polish: death animation.

  • Feat: Models no longer vanish instantly on death. When a model is removed from state (HP hits 0), it plays a 500ms two-phase collapse animation: a fast opacity drop to 0.2 over the first 30% of the duration, followed by a scale-to-zero collapse with full fade-out. Combat is now legible — kills register visually before the token disappears.
  • Arch: Dying models are tracked in a local Map<modelId, DyingEntry> in tactical-map.tsx. A useEffect diffs state.models keys each render, snapshots last-known position from a lastKnownPositions ref, and registers departing model IDs as dying. A second useEffect clears the map after DEATH_ANIMATION_MS + 50ms. Dying tokens are rendered as ghost LiveModel shells alongside live tokens, with dying=true passed to TacticalModelToken. The token component stays Pure Fabrication — no hooks or store access added.
  • Roadmap: Token Polish section fully complete. Death Animation moved to COMPLETE.

v00.19.23

March 2026

Hex-grid tactical game — token polish: spent model texture.

  • Feat: Spent models (where model.activated === true) now display a 45° diagonal hatch overlay in addition to the existing opacity: 0.35 dim. The pattern is a white line hatch at opacity: 0.18, applied as a secondary circle filled via a scoped SVG pattern element. LOD 1 only — macro view is unaffected.
  • Arch: Pattern is keyed by model.id to prevent SVG ID collisions when multiple tokens render on the same map. Defined in a conditional <defs> block inside the token root, only mounted when the model is activated.

v00.19.22

March 2026

Hex-grid tactical game — token polish: overwatch pulse animation.

  • Feat: Overwatch crosshair marker now breathes — CSS @keyframes overwatch-pulse fades the marker from opacity: 1 to 0.3 and back over a 2s ease-in-out loop. Token body is unaffected; only the crosshair <g> animates.
  • Arch: Implemented as a scoped <defs><style> block co-located with the overwatch marker in tactical-model-token.tsx. No hooks, no RAF loop — preserves the Pure Fabrication architecture of the token component.

v00.19.21

March 2026

Hex-grid tactical game — token polish: unit selection ring + hex hover state.

  • Feat: squadMateRings layer — when a model is selected, all other models in the same squad receive a subtle faction-colored dashed ring (strokeDasharray="2 4", opacity=0.45, radius r+4). Distinct from the gold active-model indicator so the active model stays visually dominant.
  • Feat: Hex hover state — hoveredHex: HexCoord | null local state in tactical-map.tsx. Each hex <g> wires onMouseEnter/onMouseLeave. A soft parchment ring (stroke="#c8b89a", opacity=0.25, no dash) is rendered above tokens on the hovered hex. Ring is skipped when the active model occupies that hex (gold indicator dominates). Ring radius sizes to the occupant's tier if a model is present, falls back to T2 radius for empty hexes.
  • Arch: Both rings are useMemo-derived in tactical-map.tsx only — no store changes, no token prop changes. Render order: tokens → hover ring → squad-mate rings → active-model ring.
  • Roadmap: Unit Selection Ring moved to COMPLETE. Hover State marked In Progress. Token polish items (Overwatch Pulse, Spent Model Texture, Death Animation) remain queued.

v00.19.20

March 2026

Hex-grid tactical game — coherency indicators.

  • Feat: lib/tactical/engine.tsapplyMove() now runs a silent coherency pass after every model move via the new private checkCoherencySilent() helper. The outOfFormation flag on every squad member is refreshed immediately after each move, not just at activation end. Solo models (only member in squad) are never flagged OOF.
  • Feat: app/tactical/_components/tactical-map.tsx — new coherencyWarnHexes prop. Move-destination hexes that would put the selected model out of formation are rendered amber instead of green (dark amber fill #1f1500, amber stroke #b87020), with a matching amber inner ring. Standard green highlights are unaffected.
  • Feat: app/tactical/page.tsx — derives coherencyWarnHexes from moveHighlights by checking whether each destination keeps the selected model within 2 hexes of at least one squad-mate. Passed as a new prop to TacticalMap.
  • Visual: Out-of-formation dashed red ring (#cc1818) already present in tactical-model-token.tsx now activates live during moves (not just at activation end), driven by the live coherency flag.

v00.19.19

March 2026

Hex-grid tactical game — squad move / formation ghost.

  • Feat: app/tactical/_components/tactical-map.tsx — ghost hover preview for squad-move mode. handleSvgMouseMove tracks the cursor hex in lastHoveredKey and fires onHexHover to the store on each hex change. A dedicated ghost render pass draws translucent formation-ghost tokens at squadMoveGhosts positions; the ghost at squadMoveLeaderIndex renders with a distinct leader ring to distinguish it from member ghosts.
  • Feat: app/tactical/page.tsx — wires squad-move mode into the page. Subscribes to squadMoveMode, squadMoveDestinations, squadMoveHoverKey, initiateSquadMove, resolveSquadMove, and hoverHex from the Zustand store. Derives squadMoveGhosts and squadMoveLeaderIndex and passes them to TacticalMap. Passes onHexHover={hoverHex} to the map.
  • Feat: MOVE SQUAD button added to action bar. Visible when a model is selected, squad-move mode is off, attack mode is off, and the selected model's squad has 2+ living members. Styled in blue (BTN_STYLE_SQUAD) to distinguish from the gold END ACTIVATION and red ATTACK buttons.
  • Feat: lib/tactical/store.ts — three new actions: initiateSquadMove() computes all valid formation destinations via getSquadMoveDestinations() and enters squad-move mode; resolveSquadMove(leaderDest) threads applyMove() through each squad member and exits the mode; hoverHex(q, r) updates squadMoveHoverKey during hover.
  • UX: Status bar shows SQUAD MOVE · HOVER TO PREVIEW · CLICK TO COMMIT while squad-move mode is active, replacing the normal selection prompt.

v00.19.18

March 2026

Hex-grid tactical game — combat / attack flow.

  • Feat: lib/tactical/store.tsinitiateAttack() enters attack-targeting mode: computes valid enemy targets via getAttackableHexes(), sets attackMode: true, and shows red hex highlights. resolveAttackOnTarget() runs the full Hit→Wound→Save→Damage resolution chain, applies the result, prunes slain models, and stores lastAttackResult in UI state. Clicking outside a valid target cancels attack mode and restores move highlights.
  • Feat: lib/tactical/types.tshasAttacked: boolean added to LiveModel. Enforces one-attack-per-activation; reserved for multi-attack special units in future.
  • Feat: app/tactical/page.tsx — ATTACK button appears in action bar when a model is selected and hasAttacked is false. Status bar updates to reflect attack-targeting mode. Battle log panel (bottom-right overlay) shows roll-by-roll results after each combat: hit rolls, wound rolls, save rolls, and total damage.
  • Weapon selection: selectWeaponForRange() auto-picks best weapon for range — melee if adjacent (distance 1), ranged otherwise, ranked by ATK then PWR.
  • Cover: Cover tier defaults to none for all hexes; terrain-aware cover lookup is a future follow-up.

v00.19.17

March 2026

Hex-grid tactical game — activation fix.

  • Fix: selectModel in lib/tactical/store.ts now sets activation.activeSquadId and activation.activeModelId on BattleState when a model is selected. END ACTIVATION button now appears as soon as a model is clicked — no longer requires a prior explicit squad selection step.

v00.19.16

March 2026

Hex-grid tactical game — Zustand game store.

  • New: lib/tactical/store.ts — Zustand store bridging the UI and the game engine. Holds live BattleState (game layer) and selection/highlight state (UI layer) separately. Actions: initBattle, selectModel, clearSelection, selectHex, moveSelectedModel, overwatchSelectedModel, endActivation.
  • Updated: app/tactical/page.tsx — replaced useMemo fake state with store. Battle loads via initBattle() on mount. Hex clicks route through selectHex. Added action bar (player turn indicator, selected model status, CANCEL and END ACTIVATION buttons) and win overlay.
  • Live: Clicking a friendly model now shows BFS move highlights. Clicking a highlighted hex moves the model and updates the map. The gold active-model ring follows the last moved model.

v00.19.15

March 2026

Hex-grid tactical game — TacticalModelToken fix.

  • Fix: Added placeholder entry to FACTION_COLORS in lib/game/unit-codes.ts. Placeholder units now render with a neutral grey palette instead of falling through to the ?? fallback. Resolves blank tokens on the tactical dev harness.

v00.19.14

March 2026

Hex-grid tactical game — TacticalMap component (Layer 4).

  • New: app/tactical/_components/tactical-map.tsx — SVG hex-grid map component. Renders the full 17×15 grid using odd-q hexToPixel offset coordinates. Terrain visuals (forest, ruins, water, cursed, high_ground), decorations, and cover tier badges (LT / HV) on each non-open terrain hex. Elevated hexes show a secondary inner ring.
  • New: One circular token per LiveModel, colored by player (blue / red). Each token shows a HP arc, activated dimming (opacity 0.35 when spent), an out-of-formation dashed red ring, and a crosshair overwatch marker at top-right.
  • New: Objective hexes render a labelled DOM meter bar showing P1 vs P2 dominance weight derived from live model stats within 1 hex. Diamond objective marker included.
  • New: Active model indicated by a gold dashed ring (driven by activation.activeModelId in BattleState).
  • New: Move-highlight and attack-highlight hex sets accepted as props; rendered as green / red tinted hexes with inner ring accents.
  • New: Zoom / pan system matching hex-grid.tsx pattern: passive wheel listener with cursor-anchored zoom, mouse-drag pan, deny-list for interactive elements.
  • Updated: app/tactical/page.tsx — dev harness replaced stub. Renders TacticalMap with a seeded BattleState (Shattered Crossroads map, 4 placeholder squads, visual test cases for overwatch / out-of-formation / activated / low-HP states).

v00.19.13

March 2026

Hex-grid tactical game — engine scaffold.

  • New: lib/tactical/types.ts — full type system for the new hex-grid tactical game. Covers model stats (MOV/MEL/RNG/DFN/HP/ARM/DOM), weapon stats (ATK/PWR/SHRD/DMG), live model & squad state, game phases, activation state, objective state, battle state, and roll results.
  • New: lib/tactical/hex-utils.ts — hex geometry for 17×15 grid. Includes hexToPixel, hexDistance (cube coord correct), hexNeighbors, hexesInRange, hexRing, BFS reachableHexes, and BFS shortestPath.
  • New: lib/tactical/engine.ts — pure game engine. Implements full Hit → Wound → Save → Damage resolution chain, movement with BFS blocking, overwatch stance & trigger detection, formation coherency checks, objective DOM scoring, VP accumulation, win condition checks (elimination / VP / DOM tiebreak), and round advancement.
  • New: lib/tactical/maps.ts — map config system with registry pattern. Ships three v1 maps: Shattered Crossroads, Ash Fields, Ironvault Breach. Each map defines terrain, deployment zones, and named objectives. Helpers: getMap(), getAllMaps(), buildDeploymentZoneKeys(), buildTerrainLookup().
  • New: lib/tactical/placeholder-units.ts — generic unit definitions across all six tiers (T1–T6) for engine testing prior to faction wiring.
  • New: app/tactical/page.tsx — route scaffold at /tactical confirming engine layer exists.

v00.19.12

March 2026

Feedback system — admin review panel.

  • New: app/admin/feedback/page.tsx — full admin review panel. Fetches all submissions from GET /api/admin/feedback, supports filter by All / Open / Reviewed / Closed, and displays date, type badge, body (with expand/collapse for long entries), user, and page path per row.
  • New: Inline status change dropdown per row (optimistic UI, PATCHes immediately). Internal note textarea with SAVE button; disabled until note content differs from saved value.
  • Wired: Feedback card added to admin hub (app/admin/page.tsx). FEEDBACK link added to admin sidebar (app/admin/layout.tsx).

v00.19.11

March 2026

Tower Rush — deploy queue bug fixes.

  • Fix: CP-purchased units were showing “Occupied” even when the wall slot was visually empty. A spurious check was testing whether any live unit occupied the lane gate position (pos 1), which fodder almost always does. Removed — the wall slot logic already handles all real conflict cases.
  • Fix: Auto-wave fodder was entering the lane ahead of CP-purchased units. CP units now displace fodder from the wall slot; the displaced fodder is demoted to the queue. Fodder already in the lane is unaffected.
  • Cleanup: Removed unused p1SpawnPos / p2SpawnPos helpers from use-tower-rush-tick.ts.

v00.19.10

March 2026

Tower Rush — 5-wide formation render pass.

  • Lane width: Both maps widened from 90 px to 116 px. The 5-file spread (±40 px from centre) now fits comfortably with 9 px of padding each side.
  • File offset: FILE_SPACING_PX = 20 defined in both tower-rush-road.tsx and use-tower-rush-tick.ts. fileToPixelY(file, centreY) converts a −2–+2 file index to an SVG y-coordinate.
  • Unit token y: Tokens now render at their actual file position. Tweened vp.y is used when available; fileToPixelY(tr.file, centreY) is the static fallback.
  • Tween: trPosToPixel() in the tick hook now includes file in its y-target. File changes tween smoothly with no additional logic changes.
  • Unaffected: Wall and queue tokens render via wallYBase offsets and are unaffected by the file system.

v00.19.09

March 2026

Tower Rush — 5-wide formation system (engine/types pass).

  • New field: TRPosition gains a file field (−2 to +2). Three supporting constants added: TR_FILE_MAX = 2 (5-wide open road), TR_FILE_CHOKE_MAX = 1 (3-wide near gates), TR_CHOKE_ZONE_POS = 2 (compression threshold).
  • Stub hex encoding: Lane spacing widened from ×3 to ×6 rows; file is now encoded in the r-axis offset (r = TR_ROAD_ROW + lane×6 + file). Adjacent files within a lane are hex-adjacent and can fight; cross-lane units remain isolated.
  • Formation march: Units spread to fill 5 files in open road (centre-first: 0, −1, +1, −2, +2) and compress to 3-wide within 2 pos of the enemy gate. File is reassigned on every march step via pickFile().
  • Combat adjacency: areAdjacent() updated — ranged units (depth 2) engage any enemy within pos range regardless of file; melee requires |fileDiff| ≤ 1.
  • Spawn seeding: All trPosition construction paths (wall slot, gate step, deploy, fallback) now include file: 0. Units spread on their first march step.
  • Render deferred: tower-rush-road.tsx still renders tokens at lane centre-Y. File-aware y-offsets and wider lane strips are the next session.

v00.19.08

March 2026

Tower Rush — bug fixes, Soulfire fortress attack, speed rebalance.

  • Fix: Auto-battler win condition was firing every pulse in Tower Rush when no units were on the field (between waves). System-log events from runPulse are now suppressed in the TR engine — TR has its own win condition keyed on fortress HP.
  • Fix: Units were halting one step before the enemy fortress due to an off-by-one in the atFortress march guard (>= changed to >). Units now correctly advance to the fortress threshold and deal damage.
  • Fix: Win condition was re-firing every pulse after the match ended. Added an early-return guard: if ms.isRunning is already false, the engine returns immediately without re-evaluating win conditions.
  • Feature: Fortress Soulfire attack — each fortress independently fires on a T2-equivalent cooldown, dealing 500 flat Soulfire damage (bypasses DFN, ARM, and all mitigation) to the most-advanced enemy unit within range 2 of its gate.
  • Balance: x1 speed rebalanced to speedMult 0.1 for a ~5-minute match. Speed table updated: 0.5× / 1× / 2× / 4× / 8×.
  • Docs: DEBT-011 added to TECHNICAL-DEBT.md and roadmap — two-pass plan to lock all dmgPerMember values and remove ×100 stat scaling. Soulfire concept and Fortress Attack System documented in TOWER-RUSH.md.

v00.19.07

March 2026

Tower Rush — Option B Linear Position Model, multi-lane maps, map select step.

  • New system: Tower Rush mode rewritten on a Linear Position Model. Units have a TRPosition (lane + pos 0–1) replacing the old step-based approach; pixel coordinates derived from posToPixelX / laneToPixelY for smooth, map-aware rendering.
  • Multi-lane maps: TRLaneConfig, TRTerrainZone, TowerRushMap, and TOWER_RUSH_MAPS constant added in tower-rush-types.ts. Each map defines lane count, terrain zone bands, and a display name.
  • Map select step: Setup flow gains a ‘map’ phase between clan and match selection, rendered in tower-rush-setup.tsx. Players choose a named map before the match begins.
  • Lane tab strip: tower-rush-deploy-panel.tsx gains a lane tab UI; active lane state is shared across both deploy panels so both players target the same lane on multi-lane maps.
  • Engine & road: tower-rush-engine.ts and tower-rush-road.tsx fully rewritten — lane-scoped march logic, depth bands, terrain zone rendering, and map param threading throughout.
  • Compat: trPosition added as an optional field on WatchUnit in battle-types.ts; auto-battler units leave it undefined. battle-engine.ts untouched.

v00.18.47

March 2026

Tombstone comment tidy pass — DEBT-010 fully closed.

  • Chore: All post-extraction tombstone comments removed from game2-client.tsx. Import statements are the authoritative record of where symbols live; pointer comments repeating that information add no value and drift over time.
  • Docs: Comment discipline rule added to DEVELOPER-DOCUMENTATION.md under Design Philosophy — no tombstone comments, imports are documentation, tracking state belongs in the session not the file.
  • DEBT-010 closed: All 8 extractions complete and in production. game2-client.tsx is now in its intended end state.

v00.18.46

March 2026

DEBT-010 cleanup — orphaned BattleMap body removed from game2-client.tsx.

  • Fix: A previous session left ~211 lines of the old inline BattleMap JSX stranded at module scope in game2-client.tsx after the extraction to battle-map.tsx. The orphaned block has been removed.
  • No behaviour change: BattleMap body now lives exclusively in battle-map.tsx; game2-client.tsx retains only the import and <BattleMap> usage.

v00.18.45

March 2026

DEBT-010 Extraction #8 — battle cinematic extracted to dedicated module.

  • New file: use-battle-cinematic.ts contains the async runCinematic function — the full win-cinematic sequence (zoom to last unit, HP drain, fade, zoom out, battle summary finalisation) extracted from game2-client.tsx.
  • game2-client.tsx: Inline ~80-line async block replaced with a single runCinematic(deps, capturedLastStandId, ev.winner) call via the CinematicDeps interface.
  • No behaviour change: Pure refactor — the cinematic sequence is identical; only the file boundary moved.

v00.18.44

March 2026

DEBT-010 Extraction #7 — clan ability activators moved to dedicated module.

  • New file: use-clan-abilities.ts created with all activate* functions, getActiveAbilityConfig, and the three clan constants — extracted from game2-client.tsx into a focused module via the ClanAbilityDeps interface.
  • game2-client.tsx: Local function bodies replaced with thin wrappers that build a deps object and delegate to the imported functions. activateOvercharged call path via onActivateOvercharged prop preserved unchanged.
  • No behaviour change: Pure refactor — all ability logic is identical; only the file boundary moved.

v00.18.43

March 2026

DEBT-010 Extraction #6 — BattleMap component moved to dedicated module.

  • New file: battle-map.tsx created with the BattleMapProps interface and BattleMap function — the full hex grid renderer, hover tooltip, cinematic overlay, and Deliberate Design radial picker.
  • No behaviour change: Pure refactor — all map rendering is identical; only the file boundary moved.

v00.18.42

March 2026

DEBT-010 Extraction #5 — dead function bodies removed from game2-client.tsx.

  • Dead code removed: REMOVED_BattleUnitToken and DEAD_AttackAnimLayer function bodies deleted from game2-client.tsx — these stubs remained after Extraction #5 and have been fully excised.
  • No behaviour change: Pure cleanup — all rendering logic lives in battle-map-token.tsx and is imported from there unchanged.

v00.18.41

March 2026

DEBT-010 Extraction #4 — hex terrain and fortress decorations moved to battle-map-hex.tsx.

  • Extraction complete: TerrainDecoration and FortressHexDecoration local definitions removed from game2-client.tsx — both are now exclusively in battle-map-hex.tsx and imported from there.
  • No behaviour change: Pure refactor — all terrain and fortress hex visuals are identical; only the file boundary moved.

v00.18.40

March 2026

DEBT-010 Extraction #3 — animation type definitions moved to dedicated module.

  • New file: battle-map-types.ts created with AnimState, AttackAnim, UnitFadeAnim, and UnitStateFlash — all exported from one focused types module.
  • Also consolidated: BattleSummary local definition removed from game2-client.tsx; it is now exclusively in battle-panels.tsx (exported) and imported from there.
  • No behaviour change: Pure refactor — all animation and summary logic is identical; only the file boundary moved.

v00.18.39

March 2026

DEBT-010 Extraction #2 — panel components moved out of game2-client.tsx.

  • Extraction complete: BattleRightPanel, BattleOutcomeSummary, MECHANIC_TOOLTIPS, BattleBottomBarProps, and BattleBottomBar duplicate definitions removed from game2-client.tsx — all are now exclusively in battle-panels.tsx and imported from there.
  • No behaviour change: Pure refactor — all panel UI is identical; only the file boundary moved.

v00.18.38

March 2026

DEBT-010 Extraction #1 — overlay components moved out of game2-client.tsx.

  • Extraction complete: Seven duplicate blocks removed from game2-client.tsxGLOSSARY_SEEN_KEY, SEEN_TOOLTIPS_KEY, getSeenTooltips, markTooltipSeen, MechanicTooltip interface, MechanicTooltipBanner, GlossaryOverlay, TerrainLegend, and BriefingOverlay are now exclusively in battle-overlays.tsx and imported from there.
  • No behaviour change: Pure refactor — all overlay UI is identical; only the file boundary moved.

v00.18.37

March 2026

Code cleanup — a 540-line duplicate overlay block removed from game2-client.tsx.

  • Duplicate block removed: A previous partial write had left ~540 lines of overlay component code duplicated inside game2-client.tsx alongside a corrupted comment line. The duplicate is gone; overlays are cleanly imported from battle-overlays.tsx.
  • Interface rename: BattleMapProps2 renamed to BattleMapProps — the ‘2’ suffix was an artifact of the corruption.

v00.18.36

March 2026

Resend verification email — users can now request a new verification link from the banner.

  • Resend link: The unverified email banner now includes a “resend it” inline link. Clicking sends a fresh 24-hour verification email via Resend and updates the banner to confirm.
  • API route: POST /api/auth/send-verification — requires an active session, checks current verified state from the database, generates a new token (replacing any existing one), and sends the verification email.

v00.18.35

March 2026

Build fix — Vercel production deploy restored.

  • SiteNav JSX fragment: SiteNav.tsx was returning two sibling elements (<header> and <UnverifiedBanner />) without a wrapping fragment, causing a parse error at build time. Wrapped in <>...</>.
  • Version files: lib/version.ts and package.json were not bumped during v00.18.33 or v00.18.34 — both corrected to v00.18.35 now.

v00.18.34

March 2026

Admin UI panel — user and role management now available at /admin.

  • User table: Paginated list of all accounts with search by name or email. Shows display name, email, roles, ban status, match count, and join date.
  • Email verified indicator: A green ✓ or amber ! next to each email address shows verification status at a glance.
  • Inline edit: Hover any row and click EDIT to update display name, toggle DEVELOPER/ADMIN roles, and flip ban status. PLAYER role is always preserved.
  • Delete account: Hard-delete any account except your own, with a two-step confirm prompt that auto-cancels after 4 seconds.
  • Invites: /admin/invite panel for issuing, rescinding, and resending invite links is unchanged but now fully documented.

v00.18.33

March 2026

Password reset — players can now recover access to their account via email.

  • Forgot password flow: /auth/forgot-password accepts an email address and sends a time-limited reset link via Resend. Always returns success — no email enumeration.
  • Reset page: /auth/reset-password?token=... validates the token, accepts a new password (8+ characters, confirmed), and updates the account. Expired or invalid tokens show a clear error with a re-request link.
  • Token model: Reuses VerificationToken with a reset: prefix on the identifier — no schema migration required. Tokens expire in 1 hour (tighter than the 24h verification window). Single-use.
  • Sign-in page: “Forgot password?” link added below the password field.
  • Dev mode: When RESEND_API_KEY is absent, the reset URL logs to the console so the flow can be tested locally.

v00.18.32

March 2026

Email verification — new accounts must verify their email address before full access is granted.

  • Verification email: After signing up, a verification email is sent via Resend with a 24-hour link. The signup flow now redirects to a “Check your inbox” holding page instead of auto-signing in.
  • Verify link: /auth/verify-email?token=... validates the token, marks the account as verified, and shows a success state with a sign-in button. Expired or invalid tokens show a clear error.
  • Unverified banner: Signed-in users who haven't verified yet see an amber warning strip below the nav on all pages. Dismissible for the session.
  • Schema: emailVerified DateTime? added to the User model. Run npx prisma migrate dev --name add-email-verified to apply.
  • Dev mode: When RESEND_API_KEY is absent, emails log to the server console instead of sending — the full verification URL is printed so the flow can be tested locally.

v00.18.31

March 2026

Admin invite management — rescind and resend controls.

  • Rescind invite: Admins can now delete a pending or expired invite from the Invites panel. A two-step confirmation (RESCIND → CONFIRM?, with a 4-second auto-cancel) prevents accidental removal. The row disappears from the list immediately after deletion.
  • Resend invite: Admins can refresh any pending or expired invite — generating a new token and a new 7-day expiry. The updated invite URL appears inline below the row for manual copying. Email stub logs the new URL to the server console.
  • ACTIONS column: The issued invites table now has a fifth column. USED invites show a dash; all others show RESCIND and RESEND buttons. Buttons are mutually disabled during in-flight requests.
  • API: DELETE /api/admin/invite removes a record by id. PATCH /api/admin/invite refreshes token, expiry, and clears usedAt — both admin-only.

v00.18.30

March 2026

Briefing phase bug fixes — BEGIN BATTLE now correctly starts the engine.

  • BEGIN BATTLE fix: Clicking BEGIN BATTLE during the pre-battle Briefing phase now correctly transitions to a live battle. The root cause was React reusing the same BattleScreen component instance across BRIEFING and BATTLE phases (no key prop), so isRunning stayed false. Fixed by adding key="briefing" and key="battle" to ensure a clean remount.
  • BriefingOverlay restored: The overlay component (amber top bar, “The lines are drawn. Command awaits.”, wax-seal BEGIN BATTLE button) was referenced in the codebase but missing from this copy of the file. Now properly defined.
  • BattleScreen prop signature: isBriefing and onBeginBattle were used inside BattleScreen but missing from its prop destructuring and type annotation. Both are now correctly declared as optional props.

v00.18.29

March 2026

Faction crests now appear on the lore faction page and on macro-zoom battle tokens.

  • Lore faction page (Surface 4): Each faction's full-size crest (72px) now appears beside the faction name in the hero header on /lore/factions/[faction]. Color matches the faction's accent stroke.
  • Macro-zoom token (Surface 5): When zoomed far out (<0.65×), unit tokens now show a small faction-colored diamond glyph centered inside the circle. The SVG polygon approach was chosen over the React FactionCrest component, which cannot render inside an SVG context.
  • UI-19a complete: All 5 faction crest surfaces are now wired — lore hub, battle left panel, battle right panel, lore faction hero, and macro-zoom token.

v00.18.28

March 2026

Warband builder: wizard panel no longer covers the START BATTLE button.

  • Layout fix: When the onboarding wizard panel is open, the warband builder's scrollable content area now reserves 320px of right padding, keeping the Starting Field, Reserve, and START BATTLE column fully visible and clickable.

v00.18.27

March 2026

Pre-battle Briefing phase — review your army before the battle begins.

  • Briefing overlay: After army setup, a full-screen overlay presents your deployed army — unit names, tiers, counts, and stats — before the battle starts. You confirm when ready.
  • BRIEFING phase: A new BRIEFING setup phase sits between ARMY_P1 and battle start. The game engine is fully initialized but paused, giving you time to review without the clock running.

v00.18.26

March 2026

Battle speed controls — pause/resume button and persistent speed preference.

  • Pause/Resume button: A ⏸/▶ toggle now appears before the speed buttons during an active battle. Pause freezes the engine entirely — the interval is cleared, all pulses stop, and ability buttons remain interactive. The button turns amber when paused. Clicking ▶ resumes from where the battle left off.
  • Pause safety: Pause is blocked once the battle is over — the button disappears after a winner is declared. The NEW BATTLE button is also hidden while paused (resume or battle must end first).
  • Speed persistence: Your last-used speed setting is now saved to localStorage (darkwars_battle_speed) and restored automatically when you start a new battle. Defaults to 1× if no preference is stored.

v00.18.25

March 2026

Password reset flow — forgot password and reset password pages added.

  • Forgot password page: New /auth/forgot-password route with email form; calls the forgot-password API and shows a confirmation message on success (no user enumeration — same message regardless of whether the email exists).
  • Reset password page: New /auth/reset-password route; reads ?token= from the URL, validates and confirms matching passwords (min 8 chars), calls the reset-password API, and auto-redirects to sign in on success.
  • Sign in link: "Forgot password?" link added below the sign-in button, above the "No account? Create one" link.

v00.18.24

March 2026

ON FIELD unit card layout polish — all 5 issues resolved.

  • Button overflow fixed: Stance and priority selects now use w-full min-w-0 on their container and minWidth: 0 on each select, preventing the right select from clipping outside the panel boundary.
  • Button alignment fixed: Both selects share a fixed height: 28 and items-stretch on the row container, ensuring they sit at the same vertical height.
  • Header padding alignment: The status dot, archetype icon, and unit name now share the same px-3 container as the stat content below, so all left edges align.
  • HP row restructured: HP 2/2 and 100% are now on a single flex justify-between row; the bar follows on its own row below. The old separate right-aligned float is gone.
  • Stat label hierarchy: ATK and DFN labels are now text-parchment/55 uppercase tracking-wide; values are text-parchment/90 font-medium — consistent with the commander panel treatment.

v00.18.23

March 2026

Wizard overlap fix extended to Clan Select and Warband Builder screens.

  • Complete wizard padding coverage: The 316px right-padding fix applied in v00.18.21 only covered Match Settings and Faction Select. Clan Select and Warband Builder had no wizardOpen prop, so their footer bars were still covered by the wizard panel. All four setup screens now receive wizardOpen and apply the padding whenever the wizard is visible.

v00.18.22

March 2026

Onboarding wizard now persists across all setup screens instead of disappearing on phase advance.

  • Wizard persistence fix: The wizard was rendered inside each per-phase return block, so advancing from Faction Select to Clan Select (or any subsequent step) unmounted it mid-read. The fix hoists both wizards to the Game2Client root level and replaces the per-phase early returns with a single renderPhaseContent() helper. The wizard now survives every phase transition and is only removed when the battle starts.
  • Reopen link follows the player: The Tier 2 reopen link (“Show Dark Wars Guide”) now appears on all setup screens beyond Match Settings, not just Faction Select, so dismissed players can recall it at any point in the flow.

v00.18.21

March 2026

Onboarding wizard no longer blocks the Confirm button on faction and settings screens.

  • Wizard overlap fix: The onboarding panel is fixed to the right edge at 300px wide. On screens where the wizard is open, the bottom action bar now reserves 316px of right padding — keeping the Confirm button and footer controls clear of the panel at all times. Affects the Match Settings footer and the Faction Select confirm bar.

v00.18.20

March 2026

Admin nav version number brightness corrected — now matches the footer version.

  • SiteNav version label: The version string shown in the top-right admin bar was text-parchment/20 — visibly dimmer than the text-parchment/50 footer version. Both now use the same opacity for visual consistency.

v00.18.19

March 2026

The /lore/units page now has a sticky faction filter bar — click any faction to show only that faction's units, or ALL to reset.

  • Faction filter bar (UI-12): A sticky bar sits just below the page hero band and above the unit list. It shows an ALL button plus one button for each of the 10 factions. Clicking a faction hides all other faction sections and shows only the selected one. ALL resets the view.
  • Faction accent colours: The active filter button highlights using that faction's accent colour — crimson for Vampires, amber for Werewolves, sea blue for Atlanteans, and so on. The ALL button highlights in gold-warm when active.
  • Client component conversion: The page was converted from a server component to a client component to hold the filter state locally. No URL params, no server round-trips — purely local React state.

v00.18.18

March 2026

One-line docs cross-link — the roadmap now links back to release notes.

  • Roadmap header cross-link (UI-16): A ‘← WHAT'S CHANGED’ link now sits right-aligned next to the roadmap subtitle, mirroring the ‘SEE WHAT'S NEXT →’ link that already existed in the release notes header. The two docs pages are now symmetrically cross-linked in both directions.

v00.18.17

March 2026

Two UX polish items: the clan select screen now tells you when more options are below, and the clan ability button finally shows you how long until it recharges.

  • Clan select scroll indicator: A faint gradient and ‘↓ MORE’ label appear at the bottom of the clan card grid when there are clans below the visible area. The indicator disappears once you've scrolled to the end — it only shows when the list is actually clipped.
  • Ability recharge fill-bar: When a round-based clan ability has been used, the button now shows an animated amber fill-bar that fills from left to right as the round progresses, with a percentage label. You can see at a glance how close you are to the next activation window.
  • Exhausted treatment: Match-limited abilities that have been used display ‘EXHAUSTED’ in dim text instead of the generic ‘USED’ label — a more thematic and readable state for a permanent spend.
  • Ability ready pulse: When a round-based ability finishes recharging, the button briefly glows amber once to signal availability — a peripheral-vision readable cue that doesn't demand your full attention mid-battle.

v00.18.16

March 2026

New player onboarding wizard is now fully wired — a dismissible field guide appears on first visit at setup screens.

  • Tier 1 wizard (genre newcomer): Appears on the Match Settings screen for first-time visitors. Explains what an auto-battler is, what a warband is, how continuous time works, and what a Commander does. Key framing: you build the army, then command it — you do not control individual units.
  • Tier 2 wizard (Dark Wars newcomer): Appears on the Faction Select screen for players who haven't yet seen the Dark Wars overview. Covers faction identity, faction resources, how clans differentiate Commanders, and the win condition.
  • Manuscript panel aesthetic: Both wizards render as a scrollable parchment panel alongside (not covering) the setup screen. Styled as pages from a field manual — consistent with the Old Book by Candlelight aesthetic.
  • Dismissible and re-openable: Each wizard can be skipped or closed at any time. Once dismissed, a small ‘Show Field Guide’ or ‘Show Dark Wars Guide’ link appears in the screen's footer bar so it can be recalled. State persisted in localStorage via darkwars_wizard.
  • Never appears during battle: The wizard is wired exclusively to MATCH_SETTINGS and FACTION_P1 setup phases. It is never rendered during live combat.

v00.18.15

March 2026

Unit tokens now scale with zoom — three distinct tiers of detail as you move from full battlefield view down to individual members.

  • Macro view (zoomed out): At extreme zoom-out, unit tokens show faction colour, HP arc, and identity dots only — no text labels. Broken units display a red dashed ring. The map reads cleanly at a glance without illegible text cluttering the tokens.
  • Standard view (default): At normal zoom levels, each token shows its unit code abbreviation, member count in Roman numerals, HP arc, and cohesion arc. Unchanged from before.
  • Member view (zoomed in): Past 1.6× zoom, individual member dots appear in their ring layout. All detail layers are visible. Unchanged from before.
  • Smooth transitions: The tier boundary sits at 0.65× scale — just above the 0.5× speed setting. Switching speeds or manually zooming crosses the boundary naturally. All animations, state flashes, hover rings, and last-stand pulses are preserved across all three tiers.

v00.18.14

March 2026

The battle now explains itself — first-time annotations appear the moment you encounter a new mechanic.

  • First-time mechanic tooltips: A thin amber annotation strip appears above the combat log the first time you encounter each of four core mechanics: a unit being destroyed, a cohesion break, a clan ability triggering, and your faction resource hitting zero.
  • Non-blocking: The strip occupies passive screen real estate — it never covers the map, the command panel, or any interactive element. The battle continues uninterrupted.
  • Auto-dismissing: Each tooltip fades in and clears automatically after 7 seconds. Click it to dismiss immediately.
  • Once only: Each tooltip fires exactly once per player, stored in your browser. Veterans who have seen all four will never see them again.
  • Speed-aware: Tooltips are suppressed at 2× speed and above — at fast speeds you want outcomes, not annotations.

v00.18.13

March 2026

Every unit on your warband now has a stance and a target priority — set them before the battle, or adjust them live while it unfolds.

  • 10 stances: Each unit can be assigned one of ten stances that govern how it moves and engages. Advance (default) closes distance and attacks. Hold never moves — attacks anything in range. Hold Ground stays put but chases an enemy that steps adjacent, up to 2 hexes. Stand Ground never moves under any circumstance. Burn moves every engine tick — no cooldown gating. Retreat to Range backs away if an enemy closes to 1 hex, holds and fires at 2–3. Guard Hex anchors to a designated hex and always returns to it. Ranged Stance only fires at range 2, retreats if melee contact is made. Assault rushes without movement cooldown. Cease Fire stops attacking entirely.
  • 12 target priorities: Beyond the existing Closest, Lowest HP, and Highest ATK, units can now be set to target Most Armor, Least Armor, Fastest, Slowest, Highest Tier, Lowest Tier, Commander First, Ranged First, or Flying First.
  • Set before battle: Every unit in your warband builder now shows compact stance and target priority selectors in the Starting Field and Reserve rosters. Assign your strategy before the first blow is struck.
  • Adjust mid-battle: Each unit card in the On Field panel of the battle screen shows live stance and priority selectors. Change them at any time — free, no resource cost. Switching to Guard Hex or Hold Ground captures the unit's current position as the anchor automatically.

v00.18.12

March 2026

The battlefield is alive — units now fade in on arrival, dissolve on death, and glow when their state changes.

  • Unit death fade: When a unit's last member falls, the token fades out over 200ms instead of vanishing instantly. The moment of death is now visible.
  • Deploy arrival: Every new unit — player-deployed, AI-deployed, or Revenant-resurrected — fades in and drops onto the hex over 300ms.
  • HP arc tween: The arc around each token now smoothly animates toward its true value each frame. Large hits cause a visible drain instead of a jump.
  • State-change flash: When a unit enters Entombed (green ring), Overcharged (amber ring), or Rage Surged (red ring) state, a brief flash ring pulses around the token to mark the transition.
  • Revenant resurrection: The Vampire Revenant's once-per-match resurrection is now animated — it fades out on death, then rises back in on the fortress hex 2 seconds later.

v00.18.11

March 2026

The Faith Theocracies enter the field — a new faction built around faith, hierarchy, and sacrifice.

  • Faith Theocracies faction: Fully playable with 11 unit types and The Hierophant Commander. The Devotion resource (Rage/Biomass equivalent) earns a flat baseline each round plus a bonus per living unit on the field, capped at 1200.
  • Sacred Hierarchy aura: While the Commander is alive, all friendly units within 3 hexes gain +100 DFN passively every pulse. If The Reliquary is alive, the aura extends to 5 hexes. Losing the Commander drops all units to base DFN.
  • Temple Guard / The Inquisitor: Temple Guard gains an additional +100 DFN within 2 hexes of the Commander. The Inquisitor debuffs all enemy units within 1 hex with –100 DFN while it lives.
  • The Flagellant: When this unit takes damage, it gains +100 ATK until the round ends. Resets each round boundary.
  • Sacred Executioner: After a kill, the next hit this unit lands strips 100 ARM from the target (or 100 DFN if no ARM). The strip fires once per chain.
  • The Penitent / Martyrdom: The Penitent survives its first destruction at 100 HP (once per match). When any FTH unit dies within 2 hexes of the Commander, the player gains +100 Devotion.
  • High Priest deploy heal: Deploying the High Priest from Reserve immediately heals all friendly units 200 HP per member (once per match).
  • Sects (Egyptian active): Opened Mouth resets all ally attack cooldowns on Commander deploy; Eternal Cartouche passively protects the Commander from stat strips; Forty-Two Assessors permanently reduces the enemy Commander's ATK by 100 on the first kill (once per match).

v00.18.10

March 2026

The battlefield now has a built-in terrain key — tap the TERRAIN button to see what every hex color means.

  • Terrain legend (UI-20): A collapsible TERRAIN button now sits in the bottom-left corner of the map, mirroring the ‘?’ visual glossary button on the right. Click it to expand a compact legend showing all six terrain types — Open Ground, Forest, Ruins, Cursed Ground, High Ground, and Fortress Hex — each with a live hex swatch using the exact in-game colors, plus the terrain name and its Cohesion effect. Collapses back to a small labeled button when you're done.

v00.18.09

March 2026

The left panel is now a modular command centre — Reserve and On Field sections can be collapsed to focus on what matters.

  • Collapsible command modules (UI-25): The battle screen left panel now uses a slot/panel architecture. Reserve units and your On Field list are each housed in a collapsible module with a label bar — click to expand or collapse. The Commander & Abilities section remains always-visible as the primary tactical interface. No visual or behavioural changes; this is a structural foundation for Stance Control, Target Priority, and Resource Command modules coming in future updates.

v00.18.08

March 2026

Unit cards now show a small archetype icon so you can read what a unit does at a glance.

  • Unit archetype icons (UI-19): A small Unicode glyph now appears before each unit's name in the warband builder and both battle screen panels. ⚔ for melee, ⌖ for ranged, ♛ for Commanders, ◈ for armoured brutes, ↔ for Reach units, and ◆ for First Strike units. Icons are monochrome and sized to sit quietly alongside the name without competing with it.

v00.18.07

March 2026

Setup screens are sharper, high-tier units are less mysterious, and battle now leaves a record.

  • Post-battle field report (UI-11): When the battle ends, a FIELD RECORD panel appears above the combat log showing units destroyed per side, total damage dealt, round count, and match duration. The winning faction's name is highlighted in gold with a ✦ mark. Stats are displayed side-by-side for easy comparison.
  • Faction card accent bar (UI-13): Each faction card on the faction select screen now has a 3px left border in that faction's accent colour, making it faster to scan and identify factions at a glance.
  • CONFIRM helper label (UI-09): On the faction select and clan select screens, a soft italic label now appears near the CONFIRM button when nothing is selected — “Select a faction to continue” / “Select a clan to continue.” It fades once a selection is made.
  • Disabled field button tooltip (UI-08): In the warband builder, hovering a greyed-out “+ FIELD” button on a high-tier unit now shows a tooltip: “Over starting field budget — add to Reserve instead.” Previously, the button was silent about why it was disabled.

v00.18.06

March 2026

Your faction follows you everywhere — crest icons now appear in the lore hub, battle panels, and warband builder.

  • Faction crests in the lore hub: Every faction card on the /lore page now shows a small crest icon alongside the resource type in the top-right corner. Lore slugs (hyphenated) are converted to the underscore faction ID format before passing to FactionCrest — no routing changes needed.
  • Faction crests in the battle screen: Both the left panel (your faction) and right panel (AI faction) now show a size-18 crest icon inline with the faction name at the top of each panel. Icons use the faction's existing underscore-format ID — no conversion required.
  • Warband builder (v00.18.05): The ArmyBuilderScreen header already received a size-22 FactionCrest alongside the warband title in the previous patch.

v00.18.04

March 2026

Melee units now fight back. First Strike units strike before their target can respond.

  • Universal melee counter-attack: Every non-ranged unit can now counter when struck in melee. Previously, counter-attacks required an explicit counter_attack property that no unit had — making every engagement strictly one-sided. Now, when a melee attacker swings at an adjacent melee defender, the defender immediately swings back. Ranged units do not counter (they aren't equipped for close-contact response). Reach units do counter — they're classified melee and the adjacency check handles it. Tier gating still applies: a unit cannot counter an attacker outside its allowed tier range. The combat log now reads "[Attacker] attacks [Defender]: X dmg | [Defender] counters: Y dmg" when a counter fires.
  • First Strike initiative burst: First Strike units (currently: Vampire Commander, Alpha Stalker) now fire a free opening attack the moment they close within range of a new target. This fires outside the normal attack cooldown — it's an additional strike that happens once per new engagement. If the target dies, the First Strike unit's engagement tracker resets and it will burst again against the next new target it closes with. Combined with universal counter-attack, First Strike now has a clear identity: you skip the counter that would otherwise punish you for charging, and you front-load damage before the enemy can respond.

v00.18.03

March 2026

Battle screen visual glossary overlay.

  • Visual glossary overlay (UI-18): A new '?' button in the bottom-right corner of the battle map opens a two-tab field manual overlay. The UNIT TOKENS tab annotates 11 visual elements, each with a miniature live SVG preview that exactly matches what you see on the battlefield: HP arc (green / yellow / red thresholds), Cohesion arc (slate-blue outer ring), Broken state (dashed red ring), P1 white dot, Commander gold dot, member dots at zoom, Entombed (green border), Overcharged (amber fill), and Rage Surged (red border) special states. The TERRAIN tab covers all 9 hex types with exact color swatches and Cohesion effects: Open Ground, Forest, Ruins, Cursed Ground, High Ground, Fortress Hex (yours and the enemy's), your deploy column, and the enemy deploy column. Dismiss by clicking outside the panel or pressing the × button — battle continues unpaused. A small amber notification dot on the '?' button clears on first open, marking the feature as seen.

v00.18.01

March 2026

Setup flow now shows your place in the ritual.

  • Step progress indicator: A sticky bar now appears at the top of every setup screen — Settings · Faction · Clan · Warband. Wax-seal dots mark each step: hollow for steps ahead, gold-filled and glowing for the active step, dim for completed steps. A thin connector line runs between them, filling in as you advance.
  • Stays visible while scrolling: The bar is sticky — it remains anchored at the top as you scroll through clan cards or the warband builder, so you always know where you are in the preparation sequence.

v00.18.00

March 2026

Vampire Revenant redesigned — death is a delay, not an ending.

  • Revenant resurrection: The Revenant no longer survives at 100 HP. Instead, when destroyed for the first time it is removed from the field, then returns 2 seconds later at the nearest unoccupied hex to the Bloodcrypt — 1 member at 50% HP/mbr (1,000 HP base). HP/mbr scales with any buffs active at the time of return. Once per match.
  • Fortress return: The respawn hex is determined by BFS outward from the Bloodcrypt. The fortress hex itself is used if unoccupied; otherwise the nearest free adjacent hex is chosen. Fortress control does not matter — the Revenant always returns to its own player’s fortress anchor.
  • Combat log: Two new log entries — “REVENANT falls — but death holds no claim here.” on destruction, and “REVENANT rises at the Bloodcrypt — 1 member, 1000 HP.” on return.

v00.17.99

March 2026

Normal difficulty targeting: 2-strike patience window before switching to penetrable targets.

  • Normal difficulty — 2-strike rule: Units on normal difficulty will attempt up to 2 attacks on their current target before the penetration-preference logic kicks in. After 2 consecutive 0-damage hits on the same target, the unit switches to a penetrable enemy if one exists. Counter resets immediately when the target changes or when any damage is dealt.
  • Hard difficulty unchanged: Hard AI still prefers penetrable targets immediately on every targeting decision — no patience window.
  • Fields added: consecutiveZeroDmgHits and lastTargetId on WatchUnit to track the per-unit counter without any state outside the unit itself.

v00.17.98

March 2026

Smart targeting — units at normal and hard difficulty prefer enemies they can actually damage.

  • Penetration-preferred targeting: At normal and hard difficulty, units now prefer targets whose DFN they can exceed (ATK > DFN) before applying their normal priority (closest / lowest HP / highest ATK). If all enemies are impenetrable, the unit falls back to its normal logic rather than becoming inert.
  • Easy difficulty unchanged: Easy AI continues to pick random targets with no penetration awareness, preserving the intentionally underpowered feel.
  • Applies to both sides: P1 player units use the same logic so your own units make smarter decisions on normal/hard. P2 AI units likewise seek penetrable targets within their hard/normal pools.

v00.17.97

March 2026

App-wide muted text brightness pass — lore, docs, and homepage.

  • Opacity remap: All parchment opacity values below /75 lifted across every public-facing lore and docs page. Remap: /15→/30, /20→/35, /25→/45, /30→/55, /40→/60, /50→/70, /60→/80, /70→/85. Borders, dates, secondary labels, and body text all affected.
  • Pages updated: lore/world, lore/timeline, lore/page (hub), lore/factions/[faction], lore/factions/[faction]/[clan], lore/units, docs/page, docs/release-notes, docs/user-guide, and app/page (homepage).

v00.17.96

March 2026

Terrain reference SVG added to user guide and design guide.

  • User guide — new Terrain section: Added between Combat and Victory Conditions. Contains a player-facing SVG reference table covering all six terrain types with move cost, DFN, ATK, and LOS columns plus per-terrain notes. Followed by a brief description of The Contested Ridge layout.
  • Design guide — terrain table replaced: The sparse plain HTML terrain table under “TERRAIN — MECHANICAL QUICK REFERENCE” has been replaced with the full SVG diagram. Now includes a Cohesion column (Forest +20, Ruins +40, Cursed −15 per second), plus the Fortress Hex regen block (on-hex +200, adjacent +100) with all five faction fortress names.

v00.17.95

March 2026

Homepage How to Play rewrite, docs access gating, SiteNav rollout complete.

  • How to Play — step accuracy: Added Match Settings as step 1. Removed false "1,200 point budget" (budget is configurable). Removed "pause" (no pause button — only speed controls). Added explicit mention of 0.1× and 4× speeds. Steps renumbered 1–6.
  • Feature grid — accuracy fixes: "12 Clans" → "12 Clans Each" (each faction has 12, not 12 total). Removed "3 Objectives" (Domination Victory not implemented in auto-battler). Replaced with "Adversity Builds" — describes the resource economy identity.
  • Homepage footer: Replaced ROADMAP link (now dev-only) with USER GUIDE. Shortened "DOCUMENTATION" to "DOCS".
  • Docs access gating: Roadmap, Design Principles, and Design Guide now redirect non-dev players to /. All three require Developer or Admin role via server-side session check.
  • Public docs hub (/docs): Now shows only User Guide and Release Notes to all players. Dev-only docs removed from the card list.
  • Dev hub (/dev): Added Design Principles card. Reordered: Roadmap, Design Principles, Design Guide, Release Notes, Prisma Studio.
  • SiteNav rollout complete: Design Principles page now uses SiteNav (was missed in the previous session). All public-facing pages now share the unified nav bar.

v00.17.94

March 2026

Audit closure — confirmed /lore/[faction] routes are fully built (UI-02 resolved).

  • Lore faction routes: Verified all 10 faction pages at /lore/factions/[slug] render correctly with History, Unit Roster, Clans, and Timeline. No code changes needed — routes were already built.

v00.17.93

March 2026

Systemic muted text brightness pass — battle screen panels, roadmap, PVP coming soon block.

  • PVP coming soon block: Border /10 → /20, label /20 → /40, "coming soon" caption /25 → /50 — was nearly invisible.
  • Battle screen left panel: Commander title, CLAN ABILITY label, PASSIVE label, Reserve/On field section labels, CMD cost text all lifted from /35–40 to /55. HP label and member count lifted to /55–75.
  • Battle screen right panel: Clan commander title /35 → /55. Defeated empty state /25 → /50.
  • Battle log: Timestamp /30 → /50. COMBAT LOG label /50 → /65. Tier label in info strip /35 → /55.
  • Roadmap page: Completed item detail /40 → /60. Overflow count /30 → /50. All section body text lifted from /40–50 to /60–70. Faction row labels /20–30 → /35–50. LOW DEFERRED heading /40 → /60. Status tags /30 → /50.
  • Warband builder: Mobile expand chevron /30 → /50. Remove X button /30 → /50.

v00.17.92

March 2026

Match Settings legibility patch — slider labels, caption, subtitle, and back link.

  • Match Settings screen: MATCH SETTINGS subtitle /40 → /55. Range end labels (300, 3000) /30 → /50. Starting field / Reserve caption /40 → /55. ← HOME back link /30 → /45. Slider unfilled track opacity 0.15 → 0.25.

v00.17.91

March 2026

Muted text brightness pass — warband builder and battle screen (UI-07 complete).

  • Warband builder legibility: Tier/cost labels, stat labels, properties, ability text, roles, description, and hover-panel placeholder all lifted from parchment/35–50 to parchment/55–70. Roster row tier labels lifted from /40 to /55. "None" empty-state text lifted from /45 to /55.
  • Clan select legibility: Commander italic line lifted from /60 to /70. Ability description body text lifted from /70 to /80.
  • Battle screen stat bars: ATK/DFN labels in the left and right unit panels lifted from /40 to /55. All stat labels in the bottom info strip (ATK, DFN, ARM, HP, MBR, WIL) lifted from /40 to /55.

v00.17.90

March 2026

Custom 404 page — Old Book aesthetic, full navigation recovery.

  • Custom 404 page: Replaced the bare Next.js default with a fully branded not-found page. Shows the Dark Wars wordmark, a hex icon, lore-consistent messaging ("UNCHARTED · These coordinates lie beyond the known maps. The war continues elsewhere."), and three navigation links — Return Home, New Battle, The Factions. Includes the hex background pattern and torn-page SVG border treatment from the homepage. Consistent with the Old Book aesthetic throughout.

v00.17.89

March 2026

Three Werewolf pack Commanders unlocked — Striped Veil, Laughing Dark, Midnight Road.

  • Striped Veil — Ghost of the Canopy: The Pack Commander phases freely through enemy-occupied hexes. On passing through, it appears on the opposite side. If that hex is occupied, it finds the nearest unoccupied adjacent hex. Friendly hexes still block. Pathfinding updated to treat enemy positions as passable for this Commander.
  • Laughing Dark — Unnerving Presence: Enemies within 2 hexes of the Pack Commander attack 30% slower — their attack cooldown resets at 1.3× the normal rate while in range. The aura applies and removes automatically as units move in and out of range each pulse.
  • Midnight Road — Between Worlds: Once per match, when this Commander would be destroyed, they slip Between Worlds instead — surviving at 1 HP and becoming untargetable for 2 full rounds. They return diminished but present. No heal on emergence.
  • WOL roadmap: 11 of 12 Commander clans now active. River Crossing (objective-gated) remains deferred until objectives are implemented in the auto-battler.

v00.17.88

March 2026

Muted text brightness pass — app-wide legibility lift.

  • All parchment/25, /30, and /45 instances across every /lore, /docs, and /game2 setup page audited and lifted. Mapping: /25 → /45, /30 → /50, /45 → /60. Affected: lore hub, world, timeline, units hub, faction pages, clan pages, unit pages, design guide stat scaling section, and the warband builder detail panel.

v00.17.87

March 2026

Terrain decoration icons and fortress glyphs on the battle map.

  • Forest, Ruins, Water, and Cursed hexes now render inline SVG art — tree silhouettes, crumbling columns, wave lines, and a dashed sigil respectively — ported from the legacy turn-based renderer.
  • Fortress hexes now display a crenellated tower glyph (silver for P1, red for P2) replacing the plain "BASE" text label. High Ground hexes keep their amber tint with no additional icon.

v00.17.86

March 2026

Unit hover tooltip now shows portrait art.

  • Hovering a unit on the battle map now shows its portrait at the top of the details tooltip — name and tier overlay the bottom of the image. Falls back gracefully to text-only for factions without portraits yet.

v00.17.85

March 2026

Left panel HP bar label — on-field unit cards clarified.

  • HP bar label: On-field unit cards in the left panel now show HP 2/2 on the left and a health-colored percentage on the right, directly above the bar. Previously the label and member count were on opposite ends of a justify-between row with no percentage, making the bar ambiguous. The bar immediately follows the label row with a small gap to reinforce the connection.

v00.17.84

March 2026

Flying unit targeting rules live in the auto-battler.

  • Flying counter targeting: Ground melee units can no longer target Flying units. The filter is applied in pickTarget() (used by all P1 units) and the equivalent P2 AI targeting function. Only Ranged, Reach, or Flying units can engage a Flying target. Duskwing and Winged Killer are the two live flying units; they were previously trivially killable by any melee unit.
  • Flying units targeting: Flying units can target anything — they see all ground units from the air. No change needed to their target selection, which was already unrestricted.

v00.17.83

March 2026

Terrain combat modifiers, move cost, and visual layer.

  • Combat modifiers (Step 2): Terrain now affects every attack. Defenders in Forest gain +100 DFN, Ruins +200, Cursed Ground −100. Attackers in Water suffer −100 ATK. High Ground elevation gives the elevated unit +100 ATK when attacking downhill, and +100 DFN when defending against uphill attacks. Flying units bypass all of these modifiers.
  • Move cost (Step 3): Ground units entering Forest pay double movement cooldown (+100 extra), Water pays triple (+200), and ascending to High Ground adds +100. This is applied as extra cooldown time after each move, making rough terrain meaningfully slower. Flying units are unaffected.
  • Visual layer (Step 4): Terrain is now visible on the battle board. Forest hexes render dark green, Ruins dark stone, Water deep blue, Cursed Ground deep purple. High Ground hexes get a subtle amber tint overlay. Each non-open hex displays a small terrain label. Fortress Hex positions (col 1 r4 and col 9 r4) show a BASE marker for both sides.

v00.17.82

March 2026

Terrain Cohesion tick live; roadmap audit.

  • Terrain Cohesion tick: The auto-battler now reads real hex terrain data on every Cohesion tick. Units standing in Forest hexes gain +20 Cohesion/sec, Ruins hexes grant +40/sec, and Cursed Ground drains −15/sec. Previously the engine had a hardcoded stub returning 'open' for every hex regardless of terrain.
  • Roadmap audit: Three COMPLETE entries (Flying, Terrain System, Domination Victory) were implemented in the turn-based engine only and have been annotated accordingly. Three new HIGH PRIORITY items added for their auto-battler equivalents, with step-by-step implementation specs.

v00.17.81

March 2026

AI reserve deployment pacing fix.

  • AI starting field fix: The AI warband builder was using deployCost (in-battle CMD cost) instead of pointCost (warband build cost) when deciding which units start on the field. Since CMD costs are small numbers, nearly every unit passed the 300pt budget check and deployed at the start. Now correctly uses pointCost — the AI fields only what fits in the 300pt starting budget and holds the rest in reserve.
  • AI deploy pacing: Reserve units were being auto-deployed too aggressively (every 1s at normal difficulty). Cooldown increased to 3s (normal), 5s (easy), 1.5s (hard). The AI now also requires at least 500 CMD before deploying from reserve, mirroring the resource pressure the player faces.

v00.17.80

March 2026

Cohesion arc on battle tokens.

  • Cohesion arc: Each unit token now shows a muted slate-blue arc outside the HP arc, tracking Will (cohesion). The arc is hidden at full cohesion to keep clean tokens, and disappears entirely when a unit is Broken. Rendered in both standard and member-zoom view.
  • WIL in hover info: The hover tooltip and bottom info strip now show a WIL stat — percentage remaining in blue, or BROKEN in red when the unit has broken.

v00.17.79

March 2026

Warband builder unit detail panel.

  • Unit detail panel (desktop): Hovering a unit card in the warband builder now populates a sticky detail panel to the right of the card grid. Shows name, tier, point cost, deploy cost, ATK, DFN, ARM, HP per member, size, move range, properties, role labels, special ability, and description. The layout expanded from a 5-column to 7-column grid to fit the panel without touching the card grid or roster.
  • Unit detail (mobile): A small chevron button appears on each card. Tapping it expands the card inline with the same stats. Tap again to collapse. No hover required.
  • Hovered card highlight: The card being hovered gets a subtle background tint so it's clear which unit the detail panel is showing.

v00.17.78

March 2026

Unit info on hover, left panel improvements.

  • Hover tooltip: Hovering a unit token on the battlefield now shows a floating info card after 280ms. Shows name, tier, HP%, member count, ATK, DFN, ARM, properties, and special ability text. The card flips left/right and up/down to stay on screen near the edges. A dashed ring highlights the hovered token.
  • Bottom info strip: When any unit is hovered, the COMBAT LOG label row is replaced by a stat strip showing the same unit info in a wider single-line format. The combat log scrolls underneath. The strip persists until another unit is hovered, so you can read it after moving the cursor away.
  • Left panel unit cards: The green HP bar in the On Field list now has an HP label above it (previously unlabeled). The member count moved above the bar as part of the HP row. The footer row now shows ATK and DFN side by side instead of member count and ATK.

v00.17.77

March 2026

Vampire Blood income wired.

  • Vampire Blood income: Vampires now earn Blood (bonus Command) from dealing damage. Each 100 damage dealt generates +100 Command, capped at +300 per round. The value was already calculated inside resolveCombat() but never consumed. Fix: added VampireBattleState with a per-round Blood accumulator to PlayerBattleState; wired bloodGenerated into p1Cmd (P1 Vampires) and p2Res (P2 Vampires) after each attack. The Blood cap resets at each round boundary.

v00.17.76

March 2026

AI army builder bug fixed — AI now starts with a full field.

  • AI starting field fix: The AI could begin a battle with zero or near-zero units on the field. Root cause: the Commander (deployCost 600) was conditionally checked against the 300-pt starting budget — a check that could never pass — leaving the starting field empty. Commander is now always placed in reserve (isStarting: false), freeing the full starting-field budget for T1–T3 units. Reserve budget guard also corrected: was comparing unit cost against total reserveLimit rather than remaining reserve capacity.

v00.17.75

March 2026

Battle crash fixed, UI scroll issues resolved, vault archive pass complete.

  • Battle crash fix: ReferenceError: lastStandEmitted is not defined — crashed on battle start for all factions. Variable was declared in BattleEngineInput but missing from the runPulse() destructuring. One-line fix; no logic changes.
  • Army builder scroll: Unit grid had a fixed max-h-96 (384px) cap — with 11 VAM units (or any large roster) the grid clipped and required scrolling to reach the Clan Lord at the bottom. Grid now uses calc(100vh - 220px) and fills available vertical space, scrolling internally only when needed.
  • Clan picker scroll: The clan grid on the Select Clan screen was unconstrained — on factions with many clans (VAM has 9) the list pushed the Commander preview card and Confirm button off-screen. Grid now capped at calc(100vh - 320px); Confirm bar is always visible.
  • Vault archive: Full review of all vault .md files. Archive/ folder created. Five items archived: 09-PROTOTYPE-ROADMAP.md, AUTO-BATTLER-HANDOFF.md, wiz_v14_sim.py, Updates.md, and the legacy Decision-Logs/ folder. Content verified before archiving — Engine Migration plan confirmed present in TECHNICAL-DEBT.md DEBT-008.

v00.17.70

March 2026

Lore section, unit portrait pages, Admin and Dev hubs — the website gains its world.

  • /lore section: Full world and faction lore now live on the site. Hub, world overview, cross-faction timeline, faction pages (all 10), clan pages (120), unit pages, and a cross-faction units browser.
  • Vampire unit pages: All 11 Vampire units have full written descriptions and design intent sections. Other 9 factions stubbed with the interface ready.
  • Unit portrait images: Portrait slot wired on unit pages (3:4 aspect ratio, Rembrandt oil painting style). Thrall portrait confirmed live. Subfolder structure per unit for multiple image types.
  • Homepage lore band: Option C lore band added between hero and How To Play — TEN FACTIONS. ANCIENT EARTH. with four quick-jump links: Factions, Units, World, Timeline.
  • Admin hub: /admin now has a proper hub page with cards for Command Ledger, Invites, and Prisma Studio. LEDGER renamed from nav, ADMIN replaces it.
  • Dev hub: New /dev section (gated to DEVELOPER + ADMIN) with Art Generator, Release Notes, Roadmap, Design Guide, and Prisma Studio links.
  • Generate gated: /generate now requires DEVELOPER or ADMIN role. Unauthenticated users redirected to sign-in.
  • Nav: ADMIN and DEV links show side by side when user holds both roles. Amber for Admin, emerald for Dev.

v00.17.69

March 2026

Cohesion System integrated — Will Damage, terrain regen, and Fortress Hex now active in the battle engine.

  • CohesionRole type: Eight roles (Fodder, Skirmisher, Ranged, Line, Brute, Elite, Champion, Commander) now determine each unit's Cohesion pool. All 43 unit definitions across all 10 factions have been assigned a role.
  • Dynamic maxCohesion: The factory now computes maxCohesion from role × tier multiplier (T1 ×1.0 through T5 ×2.0) instead of a hardcoded 1000. A T3 Line unit now starts with 1500 Cohesion; a T5 Commander with 4000.
  • Will Damage — attack drain: Every physical attack deals 10% of post-mitigation HP damage to the defender's Cohesion bar. High-damage attacks rattle morale; chip damage barely registers.
  • Will Damage — member death spike: Each member death adds a flat +250 Cohesion spike to the surviving group. Rapid member loss can collapse a unit's will before its HP is gone.
  • Break state: A unit at 0 Cohesion becomes Broken (isBroken). Break clears once Cohesion recovers to 300 (the Unbreak threshold).
  • Fortress Hex regen: Units on or adjacent to their faction's Fortress Hex regenerate Cohesion each tick — +200/sec on-tile, +100/sec adjacent. P1 Fortress is at col 1 row 4; P2 at col 9 row 4.
  • Terrain regen: Forest grants +20 Cohesion/sec; Ruins +40/sec; Cursed Ground drains −15/sec. Open ground provides no passive regen. Flying units are exempt from all terrain and Fortress effects. (Terrain lookup is a stub returning 'open' until the terrain layer ships.)
  • will_immune property: Units with the 'will_immune' property skip all Will Damage (design space for Golems and similar constructs).

v00.17.68

March 2026

Vampire clan abilities rebuilt for the auto-battler — 5 abilities updated across 4 clans.

  • Jade Coffin — Deathless Vigil: The Commander enters dormancy for ~3 seconds (immune to damage, cannot act). On emergence, all friendly units within 3 hexes restore 20% of their max HP. Previously restored only attacker cooldowns, which had no meaning in continuous time.
  • Hungry Moon — Endless Hunger: Now fully passive. Each Commander kill grants permanent +100 ATK and +50 DMG/member, stacking up to 3 times. Previously reset the Commander's attack cooldown — a turn-based mechanic with no equivalent in the auto-battler.
  • Stopped Clock — Temporal Drain: Now fully passive. Each Commander hit applies a -150 ATK debuff to the target for 4 seconds. The debuff is reapplied on each hit, does not stack, and expires naturally. Previously froze the target for one turn.
  • Amber Flame — Bioluminescent Lure: Now fully passive and automatic. Every ~8 seconds, the nearest enemy within 3 hexes is pulled 1 hex toward the Commander. The pull pathfinds around occupied hexes and does nothing if no valid hex closer to the Commander exists. Previously a player-activated ability with a per-round cooldown.
  • Lord's Decree (Iron Throne clan): Minor language fix — "without becoming Exhausted" removed from the ability description. The ability now correctly reads as granting an immediate free attack, with no legacy turn-based framing.

v00.17.67

March 2026

WOL unit ability audit — four auto-battler fixes across the full roster.

  • Pack Runner — Pack Fury: new ability wired. When any friendly Werewolf group within 2 hexes takes a damage instance, nearby Pack Runners gain +50 ATK and +25 DMG/member until end of round (Volatile, stacks, max +200 ATK). Rewards tight pack positioning.
  • Howling Dead trigger cap changed from once-per-round to once per match. A worthy death only happens once. The round-boundary reset has been removed.
  • Flank Hunter confirmed dynamic — ATK bonus recalculates on every attack tick as the enemy count changes. No code change; design intent is now explicit.
  • The Raging and all other Volatile bonuses — language standardised to "resets end of round" throughout (was "end of turn" in the design doc, a turn-based legacy term).
  • use-battle-engine.ts marked @ts-nocheck and flagged as deprecated dead code — superseded by battle-engine.ts since v00.17.49.

v00.17.66

March 2026

WOL clan redesigns — Fifth Sun and Broken Chain rebuilt for the auto-battler.

  • Fifth Sun reworked: trigger is now HP-based (Commander drops below 50% HP, not member count). On trigger: +200 ATK and +50 DMG/member permanently. Upgrade hysteresis tracked for future upgrade system. Once per match. Fixed a bug where the old member-count check never fired on size-1 Commanders.
  • Broken Chain reworked: the +300 Rage is now banked and paid out at the next round boundary, bypassing the volatile wipe. Restores the original design intent — a guaranteed spend window that turns damage timing into a strategic decision.

v00.17.65

March 2026

Iron Maw — Controlled Detonation wired. WOL Commander clan #6 of 12 now active.

  • Iron Maw pack ability live: once per round, press DETONATION to arm the banking flag — the next Rage intake from incoming damage is intercepted and held outside the volatile pool.
  • Banked Rage is paid out at the round boundary, before the volatile wipe, so it carries into the new round as spendable Rage.
  • Wired in both the turn-based engine (game-engine.ts) and the auto-battler engine (battle-engine.ts).
  • New ironMawPendingRage and ironMawBankingActive fields added to PlayerState (types.ts) and WolBattleState (battle-types.ts).

v00.17.64

March 2026

Command Ledger — admin panel for managing players, roles, and invites.

  • New /admin panel (Command Ledger) — visible only to admins via a LEDGER link in the nav.
  • Users tab: paginated player table with name, email, roles, match count, join date, and inline editing.
  • Inline editor: update display name, toggle DEVELOPER and ADMIN roles, and ban or unban any account without leaving the page.
  • Invites tab: issue 7-day invite links by email, view all pending, used, and expired invites.
  • Prisma schema extended with banned / bannedAt fields on User and a new Invite model.

v00.17.63

March 2026

AI difficulty selection is now live — choose your challenge before the battle begins.

  • Match Settings screen now shows Game Mode (VS AI selected by default; PvP coming soon) and AI Difficulty (Easy / Normal / Hard).
  • Easy: the AI targets and moves randomly, and attacks 33% slower.
  • Normal: the AI advances on the nearest enemy — the current default behavior.
  • Hard: the AI prioritises killing the lowest-HP group, contests objectives, and attacks 33% faster.
  • Deploy throttle by difficulty: Easy deploys reserves no faster than once every 3 seconds; Normal once per second; Hard once every half-second.

v00.17.61

March 2026

Command point budget slider now spans the full intended range.

  • Budget slider range changed from 600–3300 to 300–3000. The displayed value is now the true total command point budget.
  • Default budget updated from 1800 to 1500.

v00.17.60

March 2026

The auto-battler is now the only game in town.

  • The Classic Mode link has been removed from the home page. The continuous-time auto-battler is now the primary and only entry point.
  • Visiting /game now redirects automatically to /game2.

v00.17.59

March 2026

The last unit on the battlefield now dies with drama.

  • When one unit remains alive on a side, it enters Last Stand — a pulsing red halo rings the token and battle speed drops to 0.1× automatically.
  • When the killing blow lands, the camera slowly zooms in on the dying unit, its HP arc drains fully to zero, then the token fades out.
  • After the fade, the camera pulls back to full view before the victory overlay appears on the map.

v00.17.56

March 2026

AI warband builder was still broken — old version replaced with correct one.

  • Root cause: the updated buildAIArmy logic (v00.17.54) was written in ai-brain.ts but the actual call in setup-screens.tsx used a completely separate old function that was never updated.
  • Fix: replaced the setup-screens.tsx buildAIArmy with the correct four-step logic (Commander → T3–T1 fill → T4 if room → T1/T2 backfill).
  • AI reserve units now respect the match's reserve limit — the builder skips any unit whose point cost exceeds the limit when assigning it to reserve.

v00.17.55

March 2026

Reserve limit slider now actually gates in-battle deployment.

  • Fixed: the reserve limit set on the Match Settings screen was validated in the warband builder but never passed to the battle. Units over the limit could always be deployed during a match regardless of the slider value.
  • Reserve limit is now threaded through to BattleScreen and BattleLeftPanel. Units whose point cost exceeds the limit are grayed out in the reserve panel and show · OVER LIMIT, and cannot be selected or deployed.

v00.17.54

March 2026

AI warband builder now spends its full 1,200 pt budget.

  • Fixed: AI was buying at most 2–3 units due to strict per-tier count caps that collided with the budget ceiling. Now fields a full warband every game.
  • New build order: Commander first, then fill with Elites and Soldiers until budget is nearly spent, then backfill with T1/T2 duplicates for any remaining points.
  • T4 Champions are now added opportunistically (only if 500+ pts remain after mid-tier fill) rather than always forcing one in.

v00.17.53

March 2026

Version badge added for Developer and Admin roles.

  • Developer and Admin users now see a version badge (e.g. v00.17.53) in the nav bar and on the profile page
  • Badge is dimmed and unobtrusive — visible to privileged roles only, invisible to regular players

v00.17.52

March 2026

Match history now linked to logged-in player.

  • Completed games are now saved to the logged-in player's match history. The /profile page will show real win/loss/match counts after playing while signed in. Guest play (not signed in) still works — matches are recorded anonymously.

v00.17.51

March 2026

Internal refactor — no gameplay changes.

  • Renamed all internal codebase symbols to match locked terminology: GroupTypeUnitType, GROUP_TYPESUNIT_TYPES, getGroupTypesForFaction()getUnitTypesForFaction(), GameUnit.groupTypeIdunitTypeId, GameUnit.groupTypeunitType, PlayerState.kithDestroyedGroupskithDestroyedUnits. These were the last internal inconsistencies from Item 25 of the terminology overhaul. No player-facing changes.

v00.17.50

March 2026

Commander mechanical differentiation — all 10 faction Commanders now have distinct stats and properties.

  • Added 5 missing Commanders to factions.ts: Atlanteans (Tide Warden — highest DFN, AtkSpd 0.75), Lemurians (The Decay — ATK 1100 / HP 1800 / AtkSpd 2.0, highest DPS and most fragile), Sorcerers (The Ascendant — ATK 1050 / AtkSpd 1.5), Faith Theocracies (The Hierophant — Move 1 / DFN 600 / HP 2600), Hive Mind (The Chorus — ATK 800 / AtkSpd 0.5, pure enabler).
  • Updated Vampire Commander: ATK 1100, Move 3, AtkSpd 1.5, first_strike property added — fastest Commander on the board.
  • Updated Werewolf Commander: ATK 1050, DFN 400, HP 2600, AtkSpd 1.5 — low DFN is intentional to absorb hits and generate Rage.
  • Kith Commander "never attacks" rule is now code-enforced: added never_attacks property to kith_commander; canAttack() in combat.ts returns false for any unit with this property. Previously the rule existed only as a description note.

v00.17.49

March 2026

Internal refactor — no gameplay changes.

  • Battle engine extracted to its own pure module (battle-engine.ts): the tick function now takes a plain input object and returns a plain output object with no side effects. All animations, log entries, and win detection are communicated back to the component via a typed event list. game2-client.tsx is now a thin shell — it owns rendering and player ability handlers, nothing more.

v00.17.48

March 2026

Internal refactor — no gameplay changes.

  • AI logic extracted to its own module (battle-ai.ts): target selection and movement pathfinding now live in a dedicated, pure file with no side effects. Battle types (WatchUnit, PlayerBattleState, faction state interfaces) moved to battle-types.ts. game2-client.tsx is now solely battle rendering and ability handlers.

v00.17.47

March 2026

Internal refactor — no gameplay changes.

  • Setup screens extracted to their own file (setup-screens.tsx): faction picker, clan picker, warband builder, match settings, and the root component now live separately from battle machinery. game2-client.tsx is now exclusively the battle engine and its sub-components.

v00.17.46

March 2026

Internal refactor — no gameplay changes.

  • Faction-specific battle state extracted into dedicated types: WolBattleState, GolBattleState, KithBattleState, and WizardBattleState. These compose into PlayerBattleState as named sub-objects rather than a flat list of fields. No player-facing changes.

v00.17.45

March 2026

Internal refactor — no gameplay changes.

  • Faction data fully self-describing: Every faction now carries its own resource visibility flag, grouping vocabulary (Clan / Pack / Order / Institute / Colony / etc.), and resource cap. The UI reads these directly instead of branching on faction names — so new factions wire in automatically with no extra UI code.

v00.17.44

March 2026

Internal refactor — no gameplay changes.

  • Resource color and label now live in faction data: The UI values for each faction's secondary resource bar (color and short label) are now defined directly in the faction registry, so new factions automatically wire in without any extra UI code.

v00.17.43

March 2026

Every battle now starts with a shared agreement on how large your reserve can be.

  • Match Settings screen: A new screen appears at the start of every match, before either player picks a faction. It sets the terms of the battle — currently just the reserve limit, with more settings to follow as the game grows.
  • Reserve limit slider: A slider from 300 to 3000 (steps of 100) lets you decide how large a reserve warband each side can bring. You can also type the number directly. Default is 1500. If you're happy with the default, just hit Begin Battle and you're through in one click.
  • Enforced in the warband builder: The Reserve zone in the warband builder now shows your current reserve cost against the agreed limit. If you exceed it, the zone turns red and the Start Battle button locks until you remove units.

v00.17.42

March 2026

Higher-tier units now feel genuinely dangerous to lower-tier enemies — and genuinely hard to scratch from below.

  • Tier gap bonuses: When a unit attacks or is attacked by a unit of a different tier, the higher-tier unit receives stat bonuses that scale with the gap. A Champion fighting a Soldier hits harder, is harder to hit, and shrugs off more damage than a same-tier exchange would suggest.
  • Four bonus stats: ATK (hit gate), DFN (hit resistance), ARM (damage reduction), and DMG/member — all four stats receive bonuses proportional to the tier gap (1 through 4 tiers difference). The larger the gap, the steeper the advantage.
  • Replaces hard tier walls: The old system blocked lower tiers from dealing any damage beyond their ceiling. The new system allows upward attacks but imposes a compounding stat penalty — a desperate T1 unit can still scratch a T3, but it will barely land and deal almost nothing in return.
  • Nightshroud exception: The Nightshroud's unusually high ATK (400) still lets it nick a T2 Soldier even with bonuses active — by design. High-ATK specialists retain a narrow upward threat.

v00.17.41

March 2026

You can now create an account, sign in, and see your match history.

  • User accounts: Sign up at /auth/signup with an email and password. Everyone starts as Player. Roles (Developer, Admin) are assigned by an administrator.
  • Sign in / sign out: Your name and role badge appear in the top-right corner of the home page. Click OUT to sign out, or click your name to go to your profile.
  • Profile page: /profile shows your match record — total matches, wins, and losses — plus a history of your last 20 completed games. Protected route; redirects to sign-in if you're not logged in.
  • Database: Backed by a Neon Postgres database connected through Vercel. Match history accumulates automatically as you play.

v00.17.40

March 2026

Three Werewolf packs unlocked — they were already working, just not visible.

  • Broken Chain, Bone Throne, Fifth Sun now selectable: These three Werewolf packs were fully implemented in the combat engine but had an internal flag that kept them hidden from the pack selection screen. That flag is now corrected. All three work as designed: Broken Chain banks +300 Rage on Commander damage once per match; Bone Throne stacks +100 ATK per kill (max +300); Fifth Sun triggers a permanent +200 ATK when the Commander drops below half strength.

v00.17.39

March 2026

Units now wear their size. Larger groups occupy more of the hex.

  • Tiered token sizes: Unit tokens on the battlefield are now sized by how many members the unit fields. A two-member Wizard pair sits small and precise in the hex. A seven-strong congregation fills it. A ten-member Kith swarm dominates it. Four tiers — Standard, Large, XL, and XXL — scale automatically based on group size.
  • Size at a glance: You can now read the rough scale of a unit immediately without hovering. A small circle means a handful of elite fighters; a large one means a mass of bodies. The HP arc scales with the token, so it stays proportional at every size.

v00.17.38

March 2026

The words "unit" and "warband" are now locked as the official terms across all player-facing text.

  • Terminology locked — Unit & Warband: Two terms are now standardised everywhere you see them. A unit is the hex entity you command on the battlefield. A warband is your complete fighting force. These replace the older "group" and "army" language that had accumulated across the game over earlier versions.
  • Where the change appears: The warband builder heading, the home page hero and how-to-play copy, these release notes, and all game design documentation have been updated. Internal code variable names (used by the engine, not visible to players) are unchanged — that's a separate decision.

v00.17.22

March 2026

The /game2 battle screen gets finer speed controls, smarter reserve deployment, and a full WOL faction pass.

  • Slow-motion speeds — 0.1× and 0.25×: Two new speed options sit below the existing 0.5× button. At 0.1× every movement and attack plays out at a tenth of normal pace — useful for watching exactly how combat resolves, especially when multiple abilities trigger in sequence.
  • Reserve deploy — column-full detection: Reserve cards now correctly disable when your deploy column is fully occupied. Instead of silently doing nothing when clicked, a card shows 'NO SPACE' in amber so you know the column is blocked rather than thinking you're short on Command.
  • Reach — new engine property: Units with the Reach property engage enemies at distance 2 (same as ranged) but remain classified as melee — no ranged targeting adjustments, no anti-ranged effects. Stops advancing one hex short and strikes from there.
  • Alpha Stalker (WOL T3 Elite): First Strike + Reach. Closes to within two hexes and swings before the enemy can respond. The REACH badge is shown on any unit carrying the property.
  • WOL pack abilities — full pass: Rage Surge (spend 100 Rage for +200 ATK until round end), Bone Gnawer (strips 100 ARM or DFN on hit, once per target per match), The Unbroken (survives death at 100 HP once per match), The Exposed (strips 100 DFN per attack, stacks, min 0), Bone Throne, Sacred Wound, and Broken Chain pack abilities all wired.

v00.16.01

March 2026

Tell your units what to hunt. Stance and target priority are now live in the main game.

  • Target priority per unit: Select any of your units during the Action phase and you'll see a three-button row — CLOSE, LOW HP, HI ATK. Set it to LOW HP and that unit's attack list sorts by the most wounded enemy first. HI ATK surfaces the most dangerous threat. CLOSE is the default, picking the nearest valid target. The choice is yours every time.
  • Stance per unit: Units now have a stance toggle — ADVANCE or HOLD. A unit on Hold won't move from its position but will still attack anything in range. Use it to anchor a flank, defend an objective, or keep a fragile unit out of danger while still contributing to the fight.
  • Both settings survive the turn: Stance and priority are stored on each unit and persist across rounds. You set them once and they stay until you change them — no need to re-apply every turn.

v00.13.00

March 2026

The Spectate mode gets a real engine. Units move and attack on independent timers — no more turns.

  • Continuous time simulation: The /watch prototype now runs on a fixed 100ms heartbeat. Every unit has its own move timer and attack timer that drain independently. A fast unit closes ground quickly while a slow one plods behind — and neither waits for the other to act.
  • Movement speed is real: moveRange 3 means one hex per second at 1× speed. moveRange 4 gets there faster. moveRange 1 takes three full seconds per hex. The speed buttons scale all timers simultaneously — the heartbeat itself never changes.
  • Attack speed is its own stat: attackSpeed is now a separate stat, fully independent of movement. Without an explicit value, every unit attacks once per second at 1× speed — regardless of how fast or slow it moves. A Slag Titan can swing every second while crawling one hex every three.
  • Smooth unit movement: Units glide between hexes using a requestAnimationFrame lerp with ease-out easing. Each token arrives at its destination exactly as the next step fires, so motion is continuous and never stutters.
  • Matchup picker: The Spectate mode now lets you pick any T1 unit for each side from a dropdown. All 11 T1 units across the four playable factions are available. Dropdowns lock during a battle and reset cleanly for the next one.

v00.12.01

March 2026

Two bugs in the new Spectate mode — both fixed.

  • Spectate — combat was dealing 0 damage: The 5×5 watch grid was using unit positions that happened to fall on Water terrain in the main game's 11×9 map. Water applies a -100 ATK penalty to attackers, which dragged Thrall's ATK from 300 to 200 — exactly matching the Worker Swarm's DFN of 200, so every attack failed the hit gate. Fixed by stripping positions before combat resolution so no terrain lookups fire on the watch grid.
  • Spectate — HP arc didn't start as a full circle: The ring around each unit token uses a dash-offset trick to rotate its start point to 12 o'clock. When the arc is shorter than a full circumference (i.e. any HP less than full), the offset overshot the end of the dash and wrapped unpredictably, making the arc look broken at full health. Fixed by replacing the offset with a CSS rotation on the element itself, which rotates the geometry before drawing and is independent of arc length.

v00.12.00

March 2026

A new combat engine. ATK opens the door — DMG does the damage. Plus: watch two AIs fight on a 5×5 map.

  • New damage formula: ATK is now a gate only — it determines whether an attack lands, not how hard it hits. Once a hit lands, damage comes from a separate DMG/member stat. This makes ATK bonuses tactically meaningful (they let you punch through high-DFN targets) without inflating raw damage output.
  • DMG/member values locked for 11 units: VAM Thrall, VAM Nightshroud, WOL Whelp, WOL Feral Scout, WOL Howling Dead, GOL Iron Thrall, GOL Clay Ward, KITH Worker Swarm, KITH Biting Cloud, WIZ Apprentice Pair, and WIZ Warding Circle all have individually reviewed DMG values. All other units default to ATK÷6 until reviewed.
  • Kill-trigger system (VAM Thrall): The Thrall now gains +50 ATK per kill (max +300) and +5 DMG/member per kill (max +50). ATK stacks let it climb through higher-DFN targets over time; DMG stacks escalate its output against the same targets. Stacks are visible in the UI and tracked live in combat.
  • Data corrections: Vampires baseline Command corrected from 600 → 500. KITH Worker Swarm HP per member corrected from 400 → 200 (sacrifice unit; dies fast, feeds Biomass).
  • Spectate AI Battle mode: A new route at /watch puts VAM Thrall against KITH Worker Swarm on a 5×5 hex map with both sides fully AI-controlled. Use the speed control to watch at 0.5×, 1×, 2×, or 4×. The kill-trigger fires live — watch the Thrall's ATK and DMG climb as it racks up kills. Accessible from the main menu.

v00.11.01

March 2026

Combat is more precise. A swing that doesn't land is now a miss — not a graze.

  • DFN is now a hit threshold: A unit's Defence stat no longer reduces damage — it determines whether an attack connects at all. If the attacker's power doesn't strictly exceed the defender's DFN, the attack fails entirely. This is a miss, not a zero-damage graze, and future ability logic will treat it differently.
  • ARM still reduces damage: Armour applies only after a hit lands. The two stats now do distinctly different things — DFN is a gate, ARM is a soak.
  • No change at baseline: For standard T1 vs T1 combat (ATK 300, DFN 200, ARM 0), the result is identical to before. The difference only surfaces at edge cases where ATK exactly meets or falls below DFN.

v00.11.00

March 2026

Zoom out and your units tell you how they're doing. No numbers — Roman numerals and a status ring.

  • Roman numeral member count: The small "x3" label on unit tokens has been replaced with a Roman numeral — III, V, II, and so on. It sits below the unit code and reads instantly at a glance, even at smaller zoom levels.
  • Status ring reacts to health: The HP arc around each token now doubles as a health signal. Full strength shows in parchment. As a unit takes casualties, the numeral and ring shift to amber, then red. You don't need to hover to know when something is dwindling.
  • Two-mode token display: Zoom in past the threshold and individual member circles take over — the Slice 2 view unchanged. Zoom back out and the Roman numeral macro view returns. The switch is seamless and automatic.

v00.09.09

March 2026

Zoom in and see who's who. Individual members now appear inside each hex at close range.

  • Individual member tokens: When you zoom in far enough, each unit hex stops showing a single token and instead shows one mini-circle per member — numbered, faction-coloured, and laid out using a geometry-aware packing algorithm. A unit of 3 shows a triangle. A unit of 5 shows a pentagon ring. Up to 10 members fit cleanly inside any hex.
  • Seamless threshold switch: The switch happens automatically based on screen size — when a member token would be at least 8px wide on your screen, the zoomed view activates. Zoom back out and the single-token macro view returns instantly.
  • Wall-safe layout engine: Every layout is solved by a binary-search algorithm that maximises token spread while guaranteeing no token ever clips the hex wall. The geometry is exact — based on the actual SVG hex shape.

v00.09.08

March 2026

The battlefield now scrolls and zooms. Get a closer look at anything.

  • Scroll to zoom: Use your mouse wheel anywhere on the board to zoom in and out. The view scales around your cursor — whatever you're pointing at stays centred. Zoom range is 0.4× to 4×.
  • Click and drag to pan: Hold and drag anywhere on the board — open hexes, terrain, empty space — to move the view. Dragging works from any hex surface, not just blank background.
  • Units and clicks still work: Clicking to select a unit, move, or attack behaves exactly as before. Zooming and panning don't interfere with gameplay — a short drag never accidentally fires a click.

v00.09.05

March 2026

All 12 Vampire clan abilities are now engine-wired and playable.

  • Clan Commander Abilities — Vampires complete: Every Vampire clan now has a functional Commander ability. Selecting a clan changes how your Commander plays in combat.
  • Active abilities (require player action): Entombed (Jade Coffin), Bioluminescent Lure (Amber Flame), Usurper's Voice (Hollow Crown), Spoken Name (Thousand Names), Lord's Decree (Iron Throne), and Deliberate Design (Bloodwright) are all triggered from the Clan Ability panel.
  • Passive abilities (auto-trigger): Endless Hunger, Temporal Drain, Studied Weakness, Funeral Rite, Denied Passage, and Undying Vanity all fire automatically on their trigger conditions.
  • Usurper's Voice: Full enemy unit possession is implemented. The possessed unit changes ownership for one complete turn. It returns Exhausted to its owner at round end.

v00.09.04

March 2026

The Kith are fully playable — all 12 colonies selectable, all core mechanics live.

  • Kith faction — fully implemented: Biomass economy, death dividend income, Redeploy system, and all unit abilities are wired into the engine.
  • Death dividend income: Every time a friendly Kith unit is destroyed, the colony earns +200 Biomass at end of turn. Sacrifice is a resource.
  • Redeploy system: Deaths earn Redeploy slots (max 3 per match). Spend 400 Biomass to return your cheapest destroyed unit to reserve at full strength.
  • All 12 Colonies selectable.

v00.09.02

March 2026

Command Phase is now a real phase — the board locks until you've made your choices.

  • Command Phase formalised: The engine tracks it as COMMAND → MOVE → ACTION and enforces it throughout. The board locks while the Command Options panel is open.
  • Round 1 skips Command Phase. From Round 2 onward, every turn begins with Command Phase.

v00.09.01

March 2026

Movement is now its own phase — free, deliberate, and separate from action.

  • Move phase: All your units may move freely at the start of your turn — no Command cost. When done, click "Done Moving" to advance to Action phase.
  • Terrain still matters: Difficult terrain costs more movement hexes. Flying units ignore penalties.

v00.09.00

March 2026

The Command Options system is live — every turn starts with a choice.

  • Command Options: Starting Round 2, draw 3 cards per slot — Unit Upgrade, Battlefield Order, War Council. Pick one per slot or pass.
  • 18 Unit Upgrades, 10 Battlefield Orders, 10 War Council options. Draw without replacement within a match.

v00.08.04

March 2026

You can now win by controlling the entire battlefield.

  • Domination victory: Hold all three objectives uncontested at end of any round to win immediately.

v00.08.03

March 2026

The battlefield is no longer flat. Where you fight matters as much as how you fight.

  • Six terrain types: Forest, ruins, water, cursed earth, and high ground — each with movement costs, combat modifiers, and LOS rules.
  • Ruins can be destroyed: Take enough cumulative damage and they collapse permanently. Cover disappears, sight lines open.

v00.08.02 — v00.05.00

March 2026
  • v00.08.02 — QoL: sticky confirm button, objective tooltips, deploy limit enforcement
  • v00.08.01 — Fixed crash after difficulty selection; unfinished factions hidden
  • v00.08.00 — Complete tactical experience; faction-specific Commander death events
  • v00.07.00 — AI difficulty: Easy / Normal / Hard
  • v00.06.02 — Roadmap page added
  • v00.06.01 — How to Play rewritten for current VS AI flow
  • v00.06.00 — VS AI mode live; victory and defeat screens
  • v00.05.21 — Fixed Werewolf Rage resetting too early
  • v00.05.20 — Drag-and-drop warband builder
  • v00.05.19 — v00.05.00 — Unit selection, movement, banners, keyboard shortcuts, Golems, combat preview

v00.19.04 — Token bounce fix & unit queuing

Mar 2026
  • Fixed token bounce: road tokens now render at a fixed Y (flat road centre) — hex odd-column vertical offset no longer applied on the road strip
  • Fixed unit queuing: replaced two-pass march+stacking approach with a single priority-ordered pass; lead unit moves first and registers its column, followers check before stepping so collisions never occur
  • Units now visibly queue behind the front unit rather than oscillating

v00.19.03 — Unit movement & animation

Mar 2026
  • March pass: units with no nearby enemy now advance one column per pulse toward the enemy fortress — fixes units stalling after winning a fight
  • Fortress damage now triggers correctly as units reach the far column
  • Per-pulse tween: unit tokens glide smoothly between columns at 60fps via a single batched rAF loop per tick
  • Ease-out curve applied to token movement; newly spawned units appear at target position immediately

v00.19.02 — Lane-pass enforcement & Fortress Rush entry point

Mar 2026
  • Fortress Rush: advancing units now stack behind friendlies — no more ghosting through the lane
  • Broken (retreating) units still pass through friendlies freely
  • Homepage secondary CTA row: FORTRESS RUSH link added alongside SPECTATE
  • tower-rush-client.tsx display title updated from "Tower Rush" to "Fortress Rush"

v00.19.00 — Tower Rush prototype

Mar 2026
  • New game mode: Tower Rush at /tower-rush — faction picker, clan picker, match
  • Seven-file SOLID architecture: types, engine, fortress, road, deploy panel, tick hook, client shell
  • Auto-wave fodder spawning every 5s; units fight at midfield via shared battle-engine
  • Fortress HP tracker with 5 damage stage resource bursts
  • Morale Shatter (30s temporary debuff on Commander death, stacking)
  • Full faction deploy panel — all tiers, CP gating, Commander lock
  • Added cohesionImmune flag to UnitType; tr_fodder (Rabble) never Breaks or routes
  • battle-engine.ts: one-line cohesionImmune guard on Will Damage block

v00.19.05

Mar 2026
  • Fortress Rush: speed controls (0.1×, 0.25×, 0.5×, 1×, 2×, 4×) added inline with Pause/Resume
  • use-tower-rush-tick: SPEEDS constant + speedIdx state/ref; speedMult parameter removed from hook signature
  • Tick loop reads SPEEDS[speedIdxRef.current].mult per pulse; speedMult removed from useCallback dep array
  • TowerRushTickState interface extended: speedIdx, setSpeedIdx, speeds

v00.19.06

Mar 2026
  • Design: Option B linear position model locked for Tower Rush map variety architecture
  • TRPosition type defined: lane, pos (longitudinal), depth (melee/ranged bands)
  • TowerRushMap, TRLaneConfig, TRTerrainZone types specified; all map data will live in tower-rush-types.ts
  • Two v1 maps designed: The Road (1 lane) and Twin Passes (2 lanes, forest midfield)
  • Lane select UI spec: tab strip above deploy panel, faction-colored active tab, implicit on single-lane maps
  • Map select flow: third step in faction → clan picker, map prop passed through client → hook → engine
  • TOWER-RUSH.md updated: Option B section added, roadmap trimmed of now-resolved items

v00.04.00 — v00.01.00

Jan – Mar 2026
  • v00.04.00 — Commanders, clans, Morale Shatter, and the Calculated Burn ability
  • v00.03.00 — Full visual overhaul — the Old Book by Candlelight look
  • v00.02.00 — Vampires, Wizards, and Werewolves added with their faction economies
  • v00.01.00 — The foundation: hex grid, tier gating, and objective scoring