Battle Game History
Change log for the battle game prototype
2026-02-13
Sea/Islands Map Mode
Added
- › MapGeneratorSea (`map_generator_sea.gd`) — new sea/islands map generator: plans island positions with minimum spacing, grows islands from seeds with 1-hex sea gap enforcement, connects each island to 3 nearest neighbors by road
- › Map mode toggle — "Land" / "Sea" cycle button on main menu next to sliders; stored in `GlobalState.map_mode`; grouped with Width/Height sliders, button spans both rows
- › Island planning — seed positions spread across left half with min 3-hex spacing and 2-hex border margin; count scales with map area (`scale * 12`, range 10-50 total); mirrored for symmetry
- › HQ islands — 20-32 hex GRASS islands centered on HQ positions; compact spawn zones (5×10) for sea mode prevent massive grass strips on large maps
- › Non-HQ islands — 10-30 hexes each, hard-capped at 32; natural BFS growth from seeds
- › Island diversity — 25% FOREST, 10% MOUNTAIN conversion; 60% beach sand on coastlines
- › Ocean bridges — A* road routing with SEA cost = 5; each island connects to 3 nearest neighbors; HQ-to-HQ road always placed as safety net
- › Cleanup passes — trim oversized land patches >32 hexes (outside-in edge removal); remove fragments <10 hexes; all land types treated as one connected component
- › Map mode in save/load — `map_mode` persisted to save file and restored on Continue
Changed
- › hex_grid_data.gd — `_generate_map()` selects generator based on `GlobalState.map_mode`; `get_spawn_zone_rects()` uses compact 5×10 zones centered on HQ for sea mode
- › save_game.gd — includes `map_mode` in save data
Feature-Based Map Generation
Added
- › MapGeneratorLand (`map_generator_land.gd`) — standalone land-mode generator extracted from hex_grid_data; creates coherent geographic features instead of random noise
- › Coastal strips — irregular sea strips along top/bottom edges (50% chance per edge), depth scales with map height; skipped on small maps (<500 area)
- › Inland lakes — 1-6 lakes grown via BFS flood-fill from random seeds, size scales with map area (`area / 15`)
- › Mountain ranges — 1-8 random-walk ridges with N-S directional bias for chokepoints; 50% chance each step widens to 2-3 hexes; ridge length = `max(w,h) / 2`
- › Rivers — 1-5 connected rivers crossing the full map top-to-bottom with horizontal drift and 30% meander chance; detour around protected spawn zones
- › Forest clusters — 3-25 clusters grown near features (70%) or randomly (30%); max size `area / 20`; distance-based probability falloff
- › Beaches — 60% chance for GRASS adjacent to SEA; plus scaled inland sand patches
- › Semi-mirror symmetry — features placed in left half, mirrored to right for competitive fairness
- › Patch cleanup — removes isolated sea patches < 6 hexes and mountain patches < 4 hexes (flood-fill connected component check)
- › A* road routing — roads placed via A* with terrain-weighted costs (GRASS:1, FOREST:3, RIVER:4, MOUNTAIN:10, SEA:999); existing roads reused for free
- › Bridges — DIRT_ROAD placed on RIVER hexes where roads cross rivers; works with existing pathfinding
- › Multi-road network — main road (HQ to HQ), secondary road (offset vertically), third road on large maps (scale_factor > 1.5)
- › Scale factor — all feature counts scale with `map_area / 768.0` for proportional features at any map size
Allow Units on Friendly HQ
Added
- › Move onto friendly HQ — clicking own HQ with a unit selected now moves the unit there instead of inspecting the building
- › `_try_move_to()` — extracted movement logic from `handle_grid_click()` into reusable function
Changed
- › `get_occupied_hexes()` — friendly buildings no longer block pathfinding (matches AI behavior)
- › Click priority — unit on hex → attack/select; enemy building → attack; friendly building → move onto; empty → move; no selection → inspect/deselect
Changed
- › hex_grid_data.gd — removed random noise generation, cellular automata smoothing, `_smooth_grid()`, `smooth_iterations`; now delegates to MapGeneratorLand
- › `get_spawn_zone_rects()` — renamed from private `_get_spawn_zone_rects()` for generator access
- › `_verify_connectivity()` — now recognizes bridges (roads on river hexes) as passable in BFS
- › `_place_hqs()` — extracted from inline code into named function
- › Unit spawn positions — now sorted by distance to HQ; units deploy close to their HQ instead of scattered across spawn zone
- › Max map size — reduced from 64×64 to 48×48
2026-02-12
Extract PauseMenu from UiRoot
Added
- › PauseMenu (`pause_menu.gd`) — dedicated pause lifecycle node that owns pause overlay, main/options sub-menus, save/load from pause; signal-less delegation pattern (same as InputHandler/TurnManager)
- › UiRoot public API — `set_hud_visible()`, `back_to_menu()` made public for PauseMenu access
Changed
- › UiRoot — reduced from ~878 to ~670 lines; no longer owns pause state, overlay, or menu building; creates PauseMenu as child node in `_ready()`
Procedural Sound Effects
Added
- › Sound generator — `tools/generate_sounds.ps1` creates 6 synthesized WAV files (22050 Hz, 16-bit mono) matching the existing asset generation pattern
- › SoundManager autoload — lightweight singleton with a pool of 4 AudioStreamPlayers for overlapping playback; public `play_*()` functions for each sound
- › Combat sounds — attack hit (sharp metallic impact), counter-attack hit (softer impact), explosion (unit/building destroyed); plays for both human and AI attacks
- › UI sounds — unit selection click, movement start sweep, button click on every button (main menu, in-game HUD, pause menu, game over)
- › Sound on/off toggle — in pause Options menu; persists across scenes via `GlobalState.sound_enabled`
AI Retreat & Defensive Positioning
Added
- › Wounded unit retreat — AI units below 40% HP (50% for 0-DEF units like artillery) fall back toward their HQ for healing; capped at 50% of living army to prevent total collapse; retreating units avoid hexes adjacent to enemies
- › HQ defense — when enemies approach within 6 hexes of the AI's HQ, nearby AI units (within 8 hexes) switch to intercept the closest threat; scoring bonus for interposition hexes (between enemy and HQ, within attack range)
- › Retreat priority sorting — non-retreating units act first so aggressive units attack before retreaters move, preventing path blocking
- › Retreating units still attack — Phase 1 attack runs before retreat movement, so wounded units take kill shots before falling back
Extract TurnManager from GameController
Added
- › TurnManager (`turn_manager.gd`) — dedicated turn lifecycle node that owns turn state (current faction, round number, AI controllers), HQ healing, turn switching, AI orchestration; signal-less delegation pattern (same as InputHandler)
- › GameController delegations — `get_current_faction()`, `get_current_round()`, `is_ai_turn()`, `get_player_name()` delegate to TurnManager; `spawn_floating_text()` and `update_unit_highlights()` made public for TurnManager access
Changed
- › GameController — reduced from ~840 to ~730 lines; no longer owns turn state, AI controllers, or turn lifecycle methods; creates TurnManager as child node in `_ready()`
- › Save/load — faction and round read from/written to TurnManager via `set_state()` / `get_current_faction()` + `get_current_round()`
Extract InputHandler from GameController
Added
- › InputHandler (`input_handler.gd`) — dedicated input translation node that owns all input state (zoom, panning, touch) and translates raw events into GameController method calls; signal-less delegation pattern avoids signal spaghetti for synchronous request-response input
- › GameController public API — `get_selected_unit()`, `get_current_faction()`, `is_unit_moving()`, `is_paused()` getters; `select_unit()`, `deselect_unit()`, `get_occupied_hexes()` renamed from private; `handle_grid_click(grid_pos)` extracted as new public method
Changed
- › GameController — reduced from ~1050 to ~840 lines; no longer owns input state or handles raw input events; creates InputHandler as child node in `_ready()`
- › Save/load — zoom level read from/written to InputHandler instead of GameController
Priority 1 Technical Debt Refactoring
Added
- › EntityIndex (`entity_index.gd`) — O(1) Dictionary-based spatial lookup for units and buildings by grid position; replaces O(n) linear scans in `_process()`, combat, input handling, and AI scoring
- › CombatResolver (`combat_resolver.gd`) — pure combat calculation functions (terrain bonuses, flanking, pincer, damage preview, format helpers); no side effects, takes `hex_grid_data` + `EntityIndex` in constructor
- › Shared faction colors — `FACTION_COLORS` dict and `get_hp_bar_color()` moved to `GlobalEnums`; deleted duplicates from `UnitBase` and `BuildingBase`
Changed
- › Public API renames — `_update_label()` → `update_label()`, `_get_hp_bar_color()` → `get_hp_bar_color()`, `_try_undo_move()` → `try_undo_move()`, `_is_thinking` → `is_thinking`, `_is_ai_turn()` → `is_ai_turn()`, `_get_player_name()` → `get_player_name()`
- › BuildingBase — added `set_label_visible()` to replace direct `_label.visible` access
- › UiRoot — deleted divergent `_is_ai_turn()` / `_get_player_name()` duplicates; delegates to `GameController`
- › AI controller — uses `CombatResolver` instead of local terrain bonus duplicates and GameController private access; updates `EntityIndex` on AI moves
- › GameController — owns `EntityIndex` + `CombatResolver`; registers/unregisters entities at spawn/death/move; ~80 lines of duplicate code removed
AI Threat Assessment & Convoy Movement
Added
- › Threat assessment — AI units now evaluate danger before moving; each enemy that can attack a candidate hex applies a penalty based on expected damage (`THREAT_BASE_PENALTY + damage × 2.0`); wounded units (< 50% HP) become increasingly cautious (up to 2× penalty); penalty halved if the unit can counter-attack from that hex
- › Convoy/formation bonus — AI units prefer moving adjacent to friendly units (+8 per neighbor, capped at 3); fragile units (DEF ≤ 1, e.g. artillery/buggy) get an extra +12 escort bonus when near tanky units (DEF ≥ 4, e.g. heavy tank)
Changed
- › AI movement scoring — `_score_move_hex()` now includes threat penalty and convoy bonus between retreat safety and HQ defense interposition checks; buggy no longer suicidally charges into artillery range, artillery sticks near tank escorts
Architectural Review & Technical Debt Roadmap
Added
- › Technical debt section in TODO — full architectural review of all 11 core files; identified GameController as god object (1109 lines, 8+ responsibilities), O(n) entity lookups, 6 pairs of duplicated code, hardcoded combat logic, and procedural UI without layout helpers
- › Prioritized refactoring plan — P1: extract CombatResolver + entity spatial index + shared constants + public AI APIs; P2: extract InputHandler, TurnManager, PauseMenu (before fog of war/economy); P3: input actions, data-driven combat, grid position base class, UI layout helpers
- › Map generator modes planned — land (current), sea/islands (mostly ocean with island roads), mountain (limited land with narrow passages)
Spawn Terrain Fix
Fixed
- › Units no longer spawn on impassable terrain — heavy tanks could spawn on forest/mountain, light tanks and artillery on mountain; now swaps spawn positions with compatible units (e.g. infantry) or converts hex to grass as fallback
Phone-Specific UI Adaptations
Added
- › Phone detection — `GlobalState.is_phone()` combines touchscreen detection with narrow viewport width (< 500px in `canvas_items` stretch mode) to reliably identify small portrait-mode phones; lazy-cached for performance
- › Next Unit button (phone only) — 48px "Next" button right of End Turn, replaces Space key which doesn't exist on phones; hidden during AI turns
- › Map toggle button (phone only) — 48px "Map" button at top-right, replaces M key for toggling minimap; minimap starts hidden on phones to save screen space
- › Version label mode indicator — shows "v0.1.xx - Phone" or "v0.1.xx - Desk" to confirm which UI mode is active
Changed
- › Starting unit composition — each faction now spawns 2 infantry + 2 light tanks + 1 heavy tank + 1 artillery + 1 buggy (was 3 infantry + 1 light tank + 1 heavy tank + 1 artillery + 1 buggy)
- › Bottom-anchored buttons on phones — End Turn, Undo, and Next Unit buttons positioned 24px from bottom edge (thumb zone) instead of 100px; Pause button moved from top-left to bottom-left
- › Larger phone buttons — all buttons 48px tall (was 36-40px) with larger font sizes for comfortable tap targets
- › Higher default zoom on phones — 1.5× default (was 1.0×) with 1.0× minimum (was 0.75×) so hexes are large enough to tap accurately in portrait mode
- › Larger floating text on phones — 24px font with 5px outline (was 16px/3px) and wider label area (160×40 vs 120×30) for readability on small screens
- › Main menu phone cleanup — keyboard hints stripped from button labels ("vs AI (S)" → "You vs AI", etc.); fullscreen button hidden (doesn't work on mobile browsers, especially iOS Safari)
- › Undo button label on phones — shows "Undo" instead of "Undo (Z)" since Z key doesn't exist on phones
- › Phone detection via JavaScript — web exports use `navigator.maxTouchPoints + window.innerWidth < 768` for reliable phone detection (Godot's DisplayServer and viewport size are unreliable in web `_ready()`)
- › 2× larger phone buttons — all in-game buttons (End Turn 200×80, Undo/Next 140×80, Pause/Map 90×90) and main menu buttons (height 90) doubled in size for comfortable tapping
- › Phone UI test mode — press P on desktop to toggle phone/desktop UI for testing
- › Pause menu vertical centering — calculates total content height and centers menu on screen (prevents Resume button being cut off on phones)
- › Map button repositions below minimap — on phones, Map toggle button moves below the minimap when it's open to avoid overlap
- › Auto-sized button widths — all main menu buttons auto-fit text width with 10px extra padding; History/TODO right-aligned
- › Phone title 40% smaller — main menu title font reduced from 80px to 48px on phones to avoid overlap with sliders
- › Sliders 25% smaller on phones — width/height slider rows scaled down (font 21, slider 225×30) to prevent overlapping the title
- › Shorter round label on phones — shows "Round X — Player" without unit count to avoid overlapping unit info
- › "You vs AI" button rename — start button text changed from "vs AI" to "You vs AI"
- › Version string separator — changed em dash (—) to simple hyphen (-) between version and date
2026-02-11
Idle Unit Animation
Added
- › Idle bob animation — active faction units with AP remaining gently bob up and down (sine wave, ±1.5px at ~0.4Hz); each unit has a random phase offset so they move independently; units stop bobbing when out of AP or when the turn switches
- › Damage preview on hover — hovering an enemy unit or building while a friendly unit is selected shows estimated damage in the bottom-right info panel (e.g. "Est: 5 dmg / 2 back"); uses the same formula as actual combat including terrain bonuses
- › Undo move — press Z or click "Undo (Z)" button to revert the last move before attacking; refunds AP and snaps unit back to previous position; cleared on attack, deselect, or turn end
- › Attacker initiative bonus — attackers get +1 ATK for initiating combat; counter-attacks do not receive this bonus
- › Flanking bonus — melee attackers get +1 ATK per friendly unit adjacent to the defender (excluding the attacker); shown in damage preview as "(+N flank)"
- › Pincer bonus — extra +2 ATK when a friendly unit is directly behind the target (opposite side from attacker); stacks with regular flanking
- › AI focus fire — AI tracks damage dealt per target each turn; prioritizes attacking already-wounded enemies (+30 score) and securing coordinated kills (+80 score); movement also steers toward damaged targets
Changed
- › Ranged attacks have no counter-attack — counter-attacks now only occur on melee (adjacent) attacks; ranged units like artillery attack safely from distance
Changed
- › Selected unit coloring — selected units now stay in their faction color (brightened when has AP, dimmed when spent) instead of shifting to yellow-green/orange
- › Touch-friendly zoom limit — minimum zoom raised from 0.5× to 0.75× on touch devices so hexes stay large enough to tap accurately
Horizontal Menu Buttons & AI vs AI Mode
Added
- › AI vs AI game mode — new "AI vs AI (A)" button on main menu starts a spectator game where both factions are computer-controlled; player can still zoom, pan, and pause but cannot interact with units
- › Horizontal start buttons — "vs AI (S)", "2 Players (T)", and "AI vs AI (A)" displayed side-by-side in the main menu instead of stacked vertically
Changed
- › AI controller parameterized — `ai_controller.gd` now accepts `ai_faction` parameter instead of hardcoded faction 1, enabling multiple AI instances
- › GameController supports multiple AIs — replaced single `_ai_controller` with `_ai_controllers` array; each AI controls its assigned faction
- › Zoom/pan available during AI turns — mouse wheel, pinch-to-zoom, arrow keys, and right-click panning now work while AI is playing (previously blocked)
- › Building label hidden in compact mode — HQ name and HP text hidden when unit overlay is set to compact (I key toggle)
Fixed
- › Hex distance operator precedence — `>> 1` shift had lower precedence than subtraction in offset-to-cube conversion, causing wrong attack range checks on certain column parities (tanks could attack at range 2)
HUD Polish & Auto-Sizing Panels
Added
- › Hover info on cursor — bottom-right HUD shows unit/building name + HP for the hex under the cursor without clicking; terrain type shown below (e.g. "Road on Grass"); buildings replace terrain line entirely
- › Auto-sizing HUD panels — all HUD background panels (terrain, unit info, turn indicator, combat log, center messages) dynamically fit their text content instead of fixed-size boxes
- › Center message fade-out — "Welcome!", turn announcements, and other center messages fade out over 0.5s instead of disappearing instantly; larger font (32px)
- › Multi-line combat log — attack/counter-attack/destroyed shown on separate lines for readability
Changed
- › Bottom-left panel hidden when empty — unit/building info panel only appears when something is selected; no more permanent "No unit selected" box
- › Road display format — roads now show as "Road on Grass", "Road on Mountain" etc. instead of "Grass (Road)"
- › Building info simplified — no faction/player name shown in bottom-left selection panel or bottom-right hover
- › End Turn button repositioned — moved higher to better separate from turn indicator
- › Public lookup API — `get_unit_at()` and `get_building_at()` made public on GameController (renamed from private `_get_unit_at`/`_get_building_at`)
Code Decoupling & OOP Cleanup
Changed
- › Public API for GameController — added `is_game_over()`, `set_unit_moving()` getters/setters; renamed `_try_attack`/`_try_attack_building`/`_toggle_unit_overlay_mode` to public (dropped underscore prefix)
- › AI uses public API — `ai_controller.gd` now calls public methods instead of accessing private fields/methods on GameController
- › UiRoot dependency injection — `ui_root.gd` receives references via `setup()` method from GameController instead of hard-coded `get_node("../../...")` paths
- › Minimap uses public getter — `mini_map.gd` calls `get_terrain_colors()` instead of accessing private `_per_hex_type_colors` dictionary
Pause Menu & HUD Polish
Added
- › Clean pause overlay — all game HUD elements (turn info, terrain info, unit info, end turn button, minimap, combat log, pause button, messages) are hidden when the pause menu is open for a distraction-free overlay
Changed
- › Pause menu button order — reorganized to: Save Game, (Load Game), Options, gap, Quit to Menu, gap, Resume
- › Combat log hidden on unpause — combat log panel no longer force-shows when exiting pause; only appears when triggered by actual combat
Fixed
- › Cursor outside map bounds — cursor could be placed on hexes outside the map grid; now validated against map dimensions
- › Welcome message blocks clicks — center message box consumed mouse events, preventing unit selection underneath; now click-through via `MOUSE_FILTER_IGNORE`
Combat Visual Polish
Added
- › Building hit flash — buildings flash white and tween back to faction color when taking damage, matching unit hit flash behavior
- › Attack/counter-attack delay — 0.3s pause between initial attack and counter-attack for better readability
- › Death animation — units fade out + shrink over 0.4s instead of disappearing instantly; buildings fade out over 0.4s before triggering game over screen
Unit Overlay Improvements
Added
- › Compact/detailed unit info toggle (I key) — compact mode (default) shows only AP for active units with AP remaining; detailed mode shows full name + HP + AP; toggle via `I` key or Options menu
- › Dark text outlines — unit labels, building labels, and floating combat text all have dark outlines for readability on light tile sets (e.g. sketch theme)
- › HP bar outlines — unit and building HP bars have dark border outlines
- › HP bar + label z-ordering — HP bars and text labels render above neighboring unit sprites via dedicated child nodes with elevated z_index
Changed
- › HP bar moved closer to unit — reduced gap between sprite and HP bar by 10px
- › Unit Info toggle in Options menu — pause menu Options sub-menu now has "Unit Info: compact/detailed" button alongside Hex Style
In-Game UI Improvements
Added
- › Pause button (top-left) — always-visible "II" button for touch devices that can't press Escape; opens the pause menu including during AI turns
- › Options sub-menu in pause menu — "Hex Style" toggle button cycles through tile set themes; "Back" returns to main pause menu
Changed
- › Turn info moved to bottom-center — relocated from top-left to below the End Turn button for better screen layout
- › Pause allowed during AI turns — Escape key and pause button now work at any time, not just during the player's turn
- › True game pause — opening the pause menu freezes the entire scene tree (`get_tree().paused`), stopping AI coroutines, animations, and timers; UI layer continues processing via `PROCESS_MODE_ALWAYS`
Image-Based Hex Tiles
Added
- › Textured hex tiles — replaced flat solid-color terrain fills with 64×64 hex-shaped PNG textures; each terrain type has distinct pixel art detail (grass tufts, forest canopies, sandy grain, rocky mountains, wavy water)
- › Tile variants — multiple visual variants per terrain type (grass ×3, forest ×2, sand ×2, mountain ×2, river ×1, sea ×1, empty ×1) randomly assigned per hex using seeded RNG for visual variety while keeping the same map seed deterministic
- › Tile set themes — 2 artistic styles: `default` (bold comic colors) and `sketch` (black pen on white paper); switchable via `GlobalState.tile_set`; persisted in save files; architecture supports adding more styles
- › Tile set toggle (V key) — press `V` during gameplay to cycle between tile set themes; works even while paused; shows brief message with current theme name
- › Fallback rendering — gracefully falls back to solid colors if textures are missing, ensuring safe incremental rollout
- › Generator script — `tools/generate_hex_tiles.ps1` creates all tile PNGs using PowerShell System.Drawing with `-Theme` parameter
Changed
- › Renderer uses `draw_texture_rect()` instead of `draw_polygon()` for terrain base layer; comic outlines (`draw_polyline()`) still render on top
- › Minimap unchanged — continues using color palette (textures too small at minimap scale)
2026-02-10
Attack Visual Feedback
Added
- › Screen shake on attack — camera shakes briefly (6px, 0.2s) when a unit or building takes damage; counter-attacks trigger a lighter shake (4px, 0.15s)
- › Unit hit flash — units briefly flash white when taking damage, then tween back to their faction color over 0.15s; works with all visual states (selected, highlighted, base)
Touch Panning
Added
- › Single-finger drag to pan — drag the map with one finger on touch screens; distinguishes taps (< 10px) from pans using a threshold so unit selection still works cleanly
Main Menu Improvements
Added
- › "About Mavwarf" button — bottom-left of main menu, opens mavwarf.netlify.app in the default browser
- › Scrolling hex grid background — subtle blue-gray hex line art scrolls diagonally behind the menu for visual interest
- › Fullscreen toggle — F11 key (main menu + gameplay) and "Fullscreen (F11)" button in top-right of main menu; works in web builds and standalone exports
Responsive UI Scaling
Changed
- › Game title renamed to "Mavwarf's classic Battle Game"
- › Responsive UI scaling — added `canvas_items` stretch mode with `expand` aspect in project settings; all UI elements (main menu, in-game HUD) now automatically scale to match window size with crisp text rendering at native resolution
- › Larger default window — window opens at 1920×1080 instead of the default 1152×648; base design resolution remains 1152×648 for proportional scaling
- › Web export copy script (`copy_export_to_webpage.bat`) — fixed to use `robocopy` for reliable recursive copying with `/E /PURGE` flags
Web Export: Show History & TODO
Changed
- › History and TODO buttons visible in web builds — were hidden unnecessarily; only the Exit button remains hidden (can't quit a browser tab)
- › `.md` files included in web export — added `*.md` to `include_filter` in `export_presets.cfg` so `HISTORY.md` and `TODO.md` are packed into the `.pck`
Pinch-to-Zoom (Mobile)
Added
- › Pinch-to-zoom — two-finger pinch gesture on touch screens zooms the camera in/out, matching the mouse wheel behavior (0.5×–2.0× range)
- › Tap-to-select/move — single-finger taps work as left-click via Godot's default touch-to-mouse emulation; no code changes needed
Changed
- › Left-click blocked during pinch — prevents accidental unit selection/movement when zooming with two fingers
Distinct Unit Sprites
Added
- › Per-unit-type sprites — each of the 5 unit types now has a unique 64×64 top-down pixel art silhouette instead of sharing the same tank sprite
- › Infantry (`infantry.png`) — soldier with helmet, torso, legs, rifle, and backpack
- › Light Tank (`light_tank.png`) — medium tank with round turret and moderate barrel
- › Heavy Tank (`heavy_tank.png`) — wider hull, thick tracks, large turret with heavy barrel and muzzle brake
- › Artillery (`artillery.png`) — long cannon on wheeled platform with stabilizer legs and ammo box
- › Buggy (`buggy.png`) — small open vehicle with 4 exposed wheels, roll cage, and windshield
- › Sprite generator (`tools/generate_sprites.ps1`) — PowerShell script using System.Drawing to generate all 5 pixel art sprites; kept for reference/regeneration
Changed
- › Bright gray base sprites — sprites use bright neutral gray palette instead of green; multiplicative faction tinting now produces clean blue/red instead of muddy teal/olive
- › Stronger faction colors — blue `(0.3, 0.55, 1.0)` and red `(1.0, 0.45, 0.45)` replace the old light pastel tints for better visual distinction
- › Faction color preserved in all unit states — highlighted (has AP), selected, and exhausted units all show their faction color; highlighted units get a brighter tint instead of plain white, selected units get a yellow-shifted faction color instead of flat yellow
- › All `.tres` files updated with per-unit texture references
- › `tank_green.png` removed — replaced by consistently named `light_tank.png`
3 New Unit Types (Artillery, Heavy Tank, Buggy)
Added
- › Artillery — ranged unit (range 2-3) with min_attack_range 2; cannot attack adjacent enemies, slow (3 AP), fragile (8 HP), high attack (8 ATK); forest movement costs 3 AP, blocked by mountains
- › Heavy Tank — powerhouse unit with 20 HP, 10 ATK, 6 DEF; blocked by forests and mountains; 4 AP, slow but devastating
- › Buggy — fast scout with 8 AP, 5 HP, 2 ATK, 1 DEF; grass costs only 1 AP, can traverse mountains (cost 3); unique among vehicles
- › `min_attack_range` field on UnitTypeData — default 1 (melee); artillery sets it to 2, creating a dead zone that infantry can exploit
- › 7 units per faction — spawn composition changed from 3 infantry + 2 light tanks to 3 infantry + 1 light tank + 1 heavy tank + 1 artillery + 1 buggy (14 total)
- › AI min_attack_range awareness — AI respects dead zone in attack checks, avoids positioning artillery too close to enemies (-100 penalty), and skips counter-damage estimation for out-of-range defenders
Changed
- › Attack target highlights — changed from semi-transparent red fill to thick red hex outline (3px, 85% opacity) for better visibility without obscuring units
- › UnitType enum extended with ARTILLERY, HEAVY_TANK, BUGGY (appended, existing values unchanged)
- › Spawn positions increased from 5 to 7 per faction to accommodate new unit types; relaxed clustering fallback so all 7 positions are always found
- › Red attack highlights respect min_attack_range — adjacent enemies won't show red highlight for artillery
- › Counter-attack respects min_attack_range — artillery can't counter-attack when hit from adjacent hex
- › Unit info panel shows range as "2-3" for artillery instead of just "3"
- › SaveGame updated with 3 new unit type resource entries (backward compatible)
Web Export Prep
Changed
- › Project config name updated to "Mav Battle" in project.godot
- › Web mode hides irrelevant buttons — Exit, History, and TODO buttons are hidden when running in a browser (`OS.has_feature("web")`); Escape-to-quit also suppressed in web mode
- › `.gitignore` — added `.export/` directory
AI Opponent
Added
- › AI controller (`scripts/ingame/ai/ai_controller.gd`) — standalone Node that plays faction 1 automatically in vs_ai mode; iterates through AI units with visible delays (~0.5s) so the player can follow the action
- › Two start buttons on main menu — "Start vs AI (S)" for single-player and "Start 2 Players (T)" for hot-seat; `game_mode` stored in `GlobalState` ("vs_ai" or "two_player")
- › AI turn logic — each unit attacks, moves toward nearest enemy/HQ, then attacks again; camera follows AI units during their turn
- › Target scoring — AI prioritizes kill shots (+100 units, +200 HQ), low-HP targets, high-value targets, and avoids costly counter-attacks
- › Movement scoring — AI evaluates reachable hexes by distance to target, attack opportunity (+50), terrain defense bonus, and road penalty
- › AI turn display — turn label shows "AI" instead of "Player 2"; End Turn button shows "AI Turn..." and is disabled during AI thinking
- › Victory text — shows "You Win!" or "AI Wins!" in vs_ai mode instead of "Player X Wins!"
- › Game mode in save data — `game_mode` saved and restored so Continue/Load preserves the correct mode; loading a save where it was AI's turn triggers AI play automatically
Changed
- › Input blocked during AI turn — all clicks, End Turn, and pause (Escape) are disabled while AI is thinking
- › Main menu layout — Start Game button replaced with two buttons; Continue button placed below both
- › `T` keyboard shortcut added for "Start 2 Players"
Save/Load Game
Added
- › Save Game — pause menu (Escape) now includes a "Save Game" button that writes the full game state to `user://savegame.json` (terrain, roads, units with HP/AP, buildings with HP, turn/round, viewport position, zoom level)
- › Load Game — pause menu shows a "Load Game" button (only when a save file exists) that re-enters the game scene and restores all saved state
- › Continue from main menu — a "Continue (C)" button appears below "Start Game" when a save file exists; press `C` or click to resume the saved game
- › SaveGame utility class (`scripts/globals/save_game.gd`) — static `class_name SaveGame` with `save_exists()`, `read_save_file()`, `build_save_data()`, `write_save_file()`, and `get_unit_type_resource()` methods
Changed
- › GameController `_ready()` refactored — unit/building spawn extracted to `_spawn_initial_units_and_buildings()`; `_ready()` branches on `GlobalState.load_from_save` to either generate fresh or restore from save
- › HexGridDataNode `_ready()` branches — loads terrain/road arrays from save data when `GlobalState.load_from_save` is true instead of running the map generator
- › GlobalState extended — new `load_from_save` flag and `_save_data` dictionary for passing parsed save data to the game scene
- › Start Game clears load flag — ensures starting a new game never accidentally loads stale save data
Animated Unit Movement
Added
- › Hex-by-hex walk animation — units visually travel along the A* computed path at ~200 px/sec (~4 hexes/sec) instead of teleporting to the destination
- › `movement_finished` signal on UnitBase — emitted when the walk animation completes; GameController uses `CONNECT_ONE_SHOT` to refresh highlights and UI
- › Camera follows walking unit — viewport tracks the unit's visual grid position during animation for smooth scrolling
Changed
- › Logical position updates instantly — destination hex is blocked from frame 1 (pathfinding and occupied-hex checks see the final position immediately), only the visual position animates
- › Input blocked during animation — all clicks, End Turn, and unit cycling are disabled while a unit is walking; right-click panning and edge-scroll remain active
- › Moving units exempt from snap — `_process()` in GameController skips `place_on_grid()` for units mid-animation so lerp isn't overridden
- › Visibility check includes visual position — moving units remain visible if either their logical destination or current visual position is within the renderer viewport
Fixed
- › Unit flicker during movement — segment transition used stale `from_pos`/`to_pos` after advancing `_move_index`, causing a one-frame jump back to the previous segment start; now snaps to `to_pos` and returns immediately
Camera Zoom
Added
- › Mouse wheel zoom — scroll wheel up to zoom in (up to 2.0×), scroll wheel down to zoom out (down to 0.5×); viewport `canvas_transform` scales visuals while the renderer dynamically adjusts the visible hex count to fill the screen
- › CanvasLayer for UI — changed the UI node from Node to CanvasLayer so HUD, minimap, messages, and pause overlay stay fixed on screen regardless of zoom level
- › Dynamic render area — `RENDERER_WIDTH`/`RENDERER_HEIGHT` are now getter properties backed by `_render_cols`/`_render_rows` vars that grow when zoomed out and shrink when zoomed in; `set_zoom()` method on renderer adjusts them
- › Screen-edge scrolling — edge scroll now uses screen-space mouse position (20px margin from any screen edge) instead of grid cursor position, working correctly at all zoom levels
- › Edge padding — viewport position allows -1/+1 overshoot past map bounds so border hexes are fully visible when zoomed in
Changed
- › `grid_to_local()` uses absolute coordinates — hex positions are now fixed in world space; canvas transform handles both zoom and scroll (replaces old relative coordinate system)
- › Renderer owns canvas transform — `_update_canvas_transform()` in renderer combines zoom scaling + scroll panning into a single viewport transform; GameController delegates zoom to renderer via `set_zoom()`
- › Canvas transform reset on scene exit — returning to main menu resets viewport transform to identity so menus render at normal scale
- › Minimap viewport rectangle automatically reflects current zoom level (larger rect when zoomed out)
- › Click-to-select/move/attack works at any zoom — canvas transform inversion accounts for zoom + scroll
Pause Menu
Added
- › Pause menu on Escape — pressing Escape during gameplay shows a dark overlay with "Paused" title, Resume button, and Quit to Menu button (replaces instant scene change)
- › Resume with Escape or button — pressing Escape again or clicking Resume closes the overlay and resumes gameplay
- › `game_paused` signal in GlobalEvents — emitted when pause state changes; GameController blocks all input and unit updates while paused
Cached Node Reference Safety Audit
Changed
- › `is_instance_valid()` guards added to all cached references to freeable nodes: `_selected_unit` in GameController (`_process`, `_select_next_unit_with_ap`, `_update_highlights`) and UiRoot (`_update_terrain_label`, `_on_unit_selection_changed`), `_selected_building` in UiRoot (`_on_building_selected`)
- › Auto-clear stale reference — GameController's `_process()` nulls `_selected_unit` if validity check fails
Lose When All Units Destroyed
Added
- › Elimination win condition — destroying all of a faction's units triggers game over, reusing the existing victory overlay ("Player X Wins!" + Back to Menu)
HP Bars on Units and Buildings
Added
- › HP bar below units — green→yellow→red bar drawn via `_draw()` between sprite and text label; updates on damage, healing, and AP changes
- › HP bar below buildings — same color scheme, drawn below the diamond shape in BuildingBase's existing `_draw()`
- › `queue_redraw()` on state change — HP bar redraws automatically whenever `_update_label()` is called (damage, healing, turn reset)
Dynamic Unit Spawning
Changed
- › Units created dynamically at runtime — all 10 unit nodes (5 per faction) removed from `hex_game_root.tscn`; GameController now spawns units in code using `_create_unit()` helper, matching the existing BuildingBase dynamic creation pattern
- › UnitBase creates its own children — Sprite2D and Label child nodes are now created dynamically in `init_from_type()` instead of relying on `@onready` references to scene-configured children; `@export` vars (`unit_type_resource`, `start_grid_position`, `start_faction`) and `_ready()` auto-init removed
- › Simplified scene tree — `hex_game_root.tscn` reduced from 8 to 5 ext_resources and 30+ nodes to 7 nodes; `load_steps` updated accordingly
- › Prepares for factory-based unit production — dynamic spawning allows changing unit counts/types without editing the scene file
File & Autoload Rename Cleanup
Changed
- › `Events` autoload → `GlobalEvents` — renamed to match sibling naming convention (`GlobalEnums`, `GlobalState`, `GlobalEvents`); all `Events.` references updated across 3 script files
- › `hex_grid_data_node.gd` → `hex_grid_data.gd` — removed redundant `_node` suffix
- › `hex_grid_renderer_node_2d.gd` → `hex_grid_renderer.gd` — removed redundant `_node_2d` suffix
- › `ui_root_node_2d.gd` → `ui_root.gd` — removed redundant `_node_2d` suffix
- › Updated all references in `project.godot`, `hex_game_root.tscn`, and `CLAUDE.md`
Floating Text & Combat Log
Added
- › Floating text above units/buildings — short animated numbers float upward and fade out: red "-X HP" for damage, orange for counter-attack damage, bright red "Destroyed!", green "+X HP" for healing
- › FloatingText class (`scripts/ingame/ui/floating_text.gd`) — self-cleaning Node2D with tween animation (floats 40px up, fades over 1.5s, then queue_free)
- › Combat log panel (bottom-center) — shows ATK/DEF calculation details for 3 seconds after each attack, e.g. "Infantry attacks Light Tank: ATK 4+1 - DEF 4 = 1 HP damage"
- › `combat_log` signal in Events autoload — emitted by combat methods, received by UI combat log panel
Changed
- › Combat popups replaced — attack/counter-attack/building damage no longer use center-screen popup messages; replaced with floating text (quick visual feedback) + combat log (detailed math)
- › Turn announcements unchanged — "Your turn, Player X!" and "Welcome!" still use center popup since they're not tied to a specific hex
- › Healing shows floating text — HQ self-repair and unit-on-HQ repair now display green "+X HP" above the healed entity
HQ Buildings & Win Condition
Added
- › HQ building per faction — each player gets one HQ placed at their spawn zone center (column 3 and column width-4), rendered as a faction-colored diamond shape with "HQ\n30/30 HP" label
- › BuildingBase class (`scripts/ingame/buildings/building_base.gd`) — new Node2D class for buildings with HP, faction, grid positioning, diamond rendering, and damage handling
- › BuildingType enum in GlobalEnums — `HQ` (extensible for future building types)
- › HQ on minimap — diamond markers in faction colors appear on the minimap for each HQ
- › Attack HQ — units adjacent to an enemy HQ see a red attack highlight; clicking attacks the HQ using the standard damage formula (ATK vs HQ_DEFENSE=5) with no counter-attack
- › HQ self-repair — each HQ regenerates +3 HP at the start of its owner's turn (capped at max 30 HP)
- › HQ unit repair — units standing on their own HQ at turn start gain +3 HP (capped at max_hp)
- › HQ selectable — clicking an HQ shows its info in the bottom-left panel (name, owner, HP, DEF, repair rate)
- › `building_selected` signal in Events autoload — emitted when a building is clicked for inspection
- › Game over screen — destroying an enemy HQ shows a dark overlay with "Player X Wins!" (48pt) and a "Back to Menu" button
- › `hq_destroyed` signal in Events autoload — emitted when an HQ reaches 0 HP
- › Pathfinding respects buildings — enemy HQs block movement (treated as occupied); friendly HQ is walkable
Changed
- › HQ hex forced to GRASS — map generator ensures HQ positions have walkable terrain
- › Spawn positions exclude HQ hex — units won't spawn on the same hex as their faction's HQ
- › Building labels hide when unit on top — prevents text overlap when a unit moves onto its own HQ
- › All input blocked after game over — movement, attacks, and end-turn disabled once an HQ is destroyed
- › Building info refreshes on turn change — bottom-left panel updates HQ HP automatically when healing occurs
- › Dangling reference safety — `_selected_building` cleared on HQ destruction and guarded with `is_instance_valid()`
Warning Cleanup
Changed
- › Zero Godot warnings — fixed all GDScript warnings across the codebase: integer division (`/ 2.0` or `>> 1`), narrowing conversions, unused signal annotations (`@warning_ignore` per-statement), shadowed variables renamed (`global_position` → `pos`, `show` → `mm_visible`), unused parameters prefixed with `_`
2026-02-09
Hide AP for Inactive Player
Changed
- › On-map unit labels hide AP for inactive player — unit labels below sprites show "HP" only for the non-active player's units; active player's units show "HP - AP"
- › Unit info panel hides AP for enemy units — bottom-left HUD panel omits the AP line when clicking an enemy unit
Expanded Unit Info Panel
Changed
- › Unit info shows full stats — selected unit panel now displays ATK, DEF, attack range, and attack AP cost on a third line (e.g. "ATK: 4 DEF: 3 Rng: 1 Cost: 1 AP")
- › Panel widened — unit info background expanded from 258×74 to 330×94 to fit the additional stats line
- › "No unit selected" — bottom-left panel shows placeholder text when no unit is selected (was blank)
Per-Unit Attack AP Cost
Changed
- › Attack AP cost per unit type — replaced hardcoded `ATTACK_AP_COST = 1` constant in GameController with per-unit `attack_ap_cost` field on `UnitTypeData`; infantry costs 1 AP to attack, light tanks cost 2 AP
Code Documentation
Added
- › Function comments — one-line comments above all non-trivial functions across all GDScript files
- › Variable comments — inline comments on non-obvious variables (state, constants, cached references)
Toggleable Minimap
Added
- › Minimap overlay (top-right) — shows the full hex grid at a glance with terrain colors, road dots, unit markers (blue/red), and a white viewport rectangle tracking the visible 20×12 window
- › Click and drag to pan — click or drag on the minimap to jump/scroll the viewport to that area
- › `M` key toggle — press `M` to show/hide the minimap (`toggle_minimap` input action)
- › `set_viewport_position()` on hex grid renderer — sets absolute viewport position with bounds clamping (used by minimap click-to-pan)
- › Mouse-over-minimap suppresses edge-scroll — renderer stops cursor tracking and auto-scrolling when mouse is over the minimap
One Unit Per Hex
Changed
- › Pathfinding respects occupied hexes — `get_reachable_hexes()` and `find_path()` now accept an `occupied` parameter; hexes with units on them are excluded from movement highlights and path calculations
- › Units pathfind around occupied hexes — A* treats occupied hexes as impassable (except the mover's own position), so units route around friendly and enemy units
Unit Highlighting & Round Counter
Added
- › Active unit highlighting — all units of the current player that still have AP are tinted white, making it easy to see who can still act. Units with no AP remaining show their normal faction color (light blue / light red).
- › Round counter — HUD (top-left) now shows "Round 1 — Player 1 — 5 units". Round increments when Player 1 becomes active again.
Changed
- › Selected unit color — selected units show a yellow tint at the same brightness as highlighted units; dimmer yellow-brown when out of AP
- › Unit color priority — selected (yellow) > highlighted/has AP (white) > faction color (blue/red)
Fixed
- › Turn indicator missing on game start — `_emit_turn_changed` was called before UI connected to the signal; now uses `call_deferred()` so all nodes are ready first
- › Message text grey — MessageLabel drew behind semi-transparent background; fixed child draw order with `move_child()`
Terrain Combat Bonuses
Added
- › Terrain defense bonus — units on defensive terrain take less damage. Infantry: Forest +3, Mountain +2, Sand +1. Light Tank: Forest +1.
- › Terrain attack bonus — units on certain terrain deal more damage. Infantry: Mountain +2, Forest +1. Light Tank: Sand +1.
- › `terrain_defense` and `terrain_attack` dictionaries on `UnitTypeData` — map `HexType` to integer bonuses, with `get_terrain_defense()` / `get_terrain_attack()` accessors
- › Roads negate all terrain bonuses — units on road hexes get no attack or defense bonus
- › Combined combat message — single popup shows attack math, counter-attack, and destruction in one view, e.g. "Infantry attacks Light Tank / ATK 4+1 - DEF 4 = 1 HP damage / Light Tank counters: ATK 7 - DEF 3+3 = 1 HP"
- › Terrain HUD shows both bonuses — e.g. "Forest (Atk +1, Def +3)" when a unit is selected
Changed
- › Damage formula updated — `max(1, (attack + terrain_atk_bonus) - (defense + terrain_def_bonus))`; applies to both attack and counter-attack
Fixed
- › Message text was grey — MessageLabel drew behind the semi-transparent background panel; fixed draw order so label renders on top
Configurable Map Size
Added
- › Map size sliders on main menu — width (20–64) and height (12–64) sliders with step 4, placed above the seed row
- › `map_width` / `map_height` in GlobalState — default 20×12; persisted across scene changes
Changed
- › Map generation uses `GlobalState` dimensions — replaced hardcoded `_DEFAULT_HEX_GRID_WIDTH` / `_DEFAULT_HEX_GRID_HEIGHT` constants
Main Menu Layout & Version Display
Added
- › Version display (bottom-left) — shows version and build date from `Version` class (`scripts/globals/version.gd`), e.g. "v0.1.1 — 2026-02-09 14:59"
- › `Version` class (`scripts/globals/version.gd`) — `MAJOR`, `MINOR`, `PATCH` constants + `BUILD_DATE`; `PATCH` increments with each commit
Changed
- › Title renamed to "Mavs Battle Game" and moved to top of screen (outside center container)
- › Seed row moved above Start Game button for better visual flow
- › Exit button moved to bottom-center of screen, separated from the center menu
- › History/TODO buttons stacked vertically in bottom-right corner (was horizontal)
- › Increased margins on bottom elements for breathing room from screen edges
Map Seed on Main Menu
Added
- › Seed display on main menu — shows the current map seed between Start Game and Exit buttons
- › Randomize button (R) — generates a new random seed (1–99999); keyboard shortcut `R`
- › `GlobalState` autoload (`global_state.gd`) — new singleton for game settings that persist across scene changes; stores `map_seed` (default 42)
Changed
- › Map generation uses `GlobalState.map_seed` — removed `@export var seed` from HexGridDataNode; seed is now controlled from the main menu
Bigger Terrain Clusters, More Units & Clustered Spawns
Changed
- › Larger mountain and sea clusters — increased SEA probability from 12% to 17%, MOUNTAIN from 19% to 28%; smoothing increased from 2 to 3 iterations for bigger terrain formations
- › 5 units per faction — each player now has 3 infantry + 2 tanks (up from 1 infantry + 1 tank); spawn positions increased from 2 to 5 per faction
- › Units spawn clustered together — first position is random, subsequent picks prefer distance 2–3 from the cluster, producing tight deployment groups
- › Unit nodes renamed for clarity — `Tank1Node2D`, `Infantry1Node2D`, etc. instead of `PlayerNode2D`/`InfantryNode2D`
Procedural Map Generator with Fair Spawns
Added
- › Spawn zone cleanup — after terrain smoothing, left (columns 1–5) and right (columns width-6 to width-2) zones are forced to walkable terrain (GRASS replaces SEA/RIVER/EMPTY), guaranteeing both factions start on land
- › Connectivity verification — BFS checks that a walkable path exists between spawn zones; if not, a land bridge is carved (greedy walk converting impassable hexes to GRASS)
- › Auto-placed units — GameController queries `HexGridDataNode.get_spawn_positions(faction)` at startup and assigns 2 non-adjacent walkable hexes per faction from their spawn zone
- › Spawn position storage — `_spawn_positions` dictionary on HexGridDataNode stores generated positions per faction
Changed
- › Road generation now connects spawn zone centers (left center → right center) instead of top-left → bottom-right corner
- › Unit start positions removed from scene file — `start_grid_position` defaults to `(-1, -1)` sentinel; `UnitBase._ready()` skips positioning when sentinel is set
- › Map generation pipeline expanded: random terrain → smoothing → spawn zone cleanup → connectivity check → road generation → spawn position picking
Removed
- › Hardcoded unit positions — `start_grid_position` values removed from all 4 unit nodes in `hex_game_root.tscn`
Centered Pop-Up Messages
Changed
- › Pop-up messages centered on screen — "Welcome!", "Your turn" and other messages now display as large white text (36pt) centered on screen with a dark semi-transparent background, replacing the small bottom-center text
Fixed
- › Message crash on startup — `show_message("Welcome!")` was called before `_message_bg` was created in `_ready()`; moved to end of initialization
Roads as Overlay Layer
Added
- › Road overlay system — roads are now a separate data layer on top of terrain instead of replacing it. A hex can be both "Grass" and have a road on it.
- › `RoadType` enum in GlobalEnums — `NONE`, `DIRT_ROAD` (extensible for future types like paved roads or bridges)
- › Road data layer in `HexGridDataNode` — `_road_grid` array with `get_road_type()`, `set_road_type()`, `has_road()` accessors
- › Road generation — 4 random-walk paths of 8–15 steps placed after terrain generation
- › Road rendering — gray-tan center dots with spoke lines connecting to neighboring road hexes, drawn as a layer between terrain and highlights
- › Terrain label shows road — hovering a road hex displays e.g. "Grass (Road)" in the HUD
Changed
- › ROAD removed from HexType enum — terrain types are now EMPTY, GRASS, FOREST, SAND, MOUNTAIN, RIVER, SEA (no ROAD)
- › Grass movement cost increased from 1 to 2 AP for all unit types, making roads (cost 1) meaningful
- › Pathfinding uses road overlay — both A* and Dijkstra check for roads first; road hexes always cost 1 AP regardless of terrain underneath, acting as bridges over rivers/sea
- › Renderer restructured into layers — base terrain → road overlay → movement/attack highlights → cursor
- › `hex_cursor_changed` signal now includes `has_road: bool` parameter
Removed
- › ROAD as a terrain type — replaced by the overlay system
Two-Player Turn System
Added
- › Alternating turns — Player 1 and Player 2 take turns. End Turn switches the active faction and resets AP only for the incoming player's units.
- › Turn indicator HUD (top-left) — shows "Player X Turn — N units" with semi-transparent background, updates on turn change and unit death.
- › Turn-start message — "Your turn, Player X!" displayed at each turn transition and game start.
- › `turn_changed` signal in Events autoload — emits `(faction, unit_count)` for UI updates.
Changed
- › Faction-aware input — all click, move, attack, Space-cycle, and highlight logic now checks `_current_faction` instead of hardcoded faction 0. Only the active player's units can be commanded.
- › End Turn only resets AP for the new active faction's units (not all units).
- › End Turn blink only considers the current faction's units when checking exhaustion.
Combat System
Added
- › Attack action — click an enemy unit adjacent to your selected unit to attack. Costs 1 AP.
- › Damage calculation — `max(1, attacker.attack_value - defender.defense_value)`. Minimum 1 damage guaranteed.
- › Counter-attack — if the defender survives and the attacker is within the defender's attack range, the defender fires back using the same damage formula.
- › Unit destruction — units at 0 HP are removed from the map. If the attacker dies from a counter-attack, selection is cleared.
- › Attack range — each unit type defines `attack_range` (currently 1 for both infantry and light tank). Attack highlights show which enemies are in range.
- › Red attack highlights — when a friendly unit is selected with AP, enemy units within attack range get a red semi-transparent overlay (alongside blue movement highlights).
- › Enemy units — 2 enemy faction units placed on the map (infantry at 10,5 and light tank at 8,7).
- › Faction coloring — friendly units tinted light blue, enemy units tinted light red. Selected units still show yellow.
- › `unit_died` signal in Events autoload for future listeners.
- › `take_damage()` on UnitBase — subtracts HP, updates label, returns true if unit died.
Changed
- › Click logic is now faction-aware: clicking an enemy with a friendly unit selected attempts attack (if in range and have AP), otherwise selects the enemy for inspection.
- › Space key only cycles through friendly (faction 0) units with AP remaining.
- › End Turn blink only considers friendly units when checking if all are exhausted.
- › `_hex_distance()` renamed to `hex_distance()` (public) for external use by GameController.
- › `_update_movement_targets()` renamed to `_update_highlights()` — now computes both movement and attack targets.
- › Movement and attack on non-friendly units is blocked (can only move/attack with faction 0 units).
Space Key Cycles to Next Unit with AP
Added
- › Space key cycles units — pressing Space selects the next unit that still has action points remaining and centers the viewport on it. Wraps around the unit list. Does nothing when all units are exhausted.
- › `select_next_unit` input action mapped to Space key in project.godot
Fixed
- › End Turn button no longer steals Space key — button had default `FOCUS_ALL` focus mode, so clicking it grabbed keyboard focus and Space triggered `ui_accept` on the button instead of cycling units. Set to `FOCUS_NONE`.
Edge-Scroll on Cursor Near Display Edge
Added
- › Mouse edge-scrolling — when the cursor hovers on a hex at the edge of the visible 20×12 renderer window, the map automatically scrolls in that direction (~6 hexes/second). Diagonal scrolling works at corners. Existing arrow-key and right-click panning still work.
Architecture Simplification
Removed
- › `MoveDirection` enum — unused since arrow-key unit movement was removed
- › `HexTileData` class — over-abstraction wrapping a single enum value; grid now stores `HexType` values directly, eliminating 768 `RefCounted` objects
- › `PlayerName` node renamed to `UnitLabel` in scene and script — leftover from when there was a dedicated player unit
Changed
- › `grid_to_world()` → `grid_to_local()` in hex renderer — the function returns local coordinates relative to the renderer, not world positions; name now matches behavior
- › CLAUDE.md updated to reflect removed classes and renamed APIs
Further Code Cleanup
Removed
- › Unused `move_range` from `UnitTypeData` and both `.tres` files — movement is governed by AP and terrain costs, not a flat range
- › Unused `_grid_position` from `HexTileData` — tile position is implicit from its array index; simplified constructor to take only hex type
- › Unused `has_acted` from `UnitBase` — AP system tracks unit exhaustion instead
- › Redundant `hex_map_set_cursor_to_mouse` signal — cursor already updates via `_input()` in the renderer
Fixed
- › Hex smoothing uses proper neighbors — `_smooth_grid()` now uses `get_hex_neighbors()` (6 hex neighbors) instead of a rectangular 3×3 loop (8 square neighbors), producing more natural terrain clusters
Code Cleanup & Bugfix
Removed
- › Dead code in UnitBase — removed ~110 lines of unused arrow-key movement code (`start_moving_to_neighbour_grid`, `update_grid_movement`, `snap_to_next_grid_pos`, orientation/direction helpers, `_speed`, `_prev_grid` vars)
- › `HexTileData.is_walkable()` — superseded by `UnitTypeData.get_movement_cost()`
- › `MathTools` autoload — replaced all `MathTools.limit_value()` calls with built-in `clampi()`; deleted `global_math_tools.gd`
Changed
- › GameController decoupled from UI — replaced direct `ui_root.set_blinking()` call with `Events.all_units_exhausted_changed` signal; GameController no longer holds a cross-tree reference to the UI
Fixed
- › Map jump when selecting units near left edge — `update_renderer_position` applied bitmask `& 0xfffe` to negative values, wrapping to huge positive numbers and jumping the viewport to the far right
Auto-Scroll, End Turn Button & UI Polish
Added
- › End Turn button (bottom center) — resets AP on all units; blinks yellow when all units are exhausted
- › `end_turn` signal in Events autoload for decoupled turn handling
- › Semi-transparent backgrounds on unit info (bottom-left) and terrain info (bottom-right) HUD panels
- › Auto-scroll margin — map viewport scrolls when selected unit is within 2 hexes of the edge
Changed
- › Welcome message moved from bottom-left to bottom-center
- › Click empty hex to deselect — clicking a non-reachable hex deselects the unit; Space key deselect removed
Removed
- › R key AP reset — replaced by End Turn button
- › Space key deselect — replaced by click-to-deselect
Reachable Hex Highlights & Arrow Key Simplification
Added
- › Reachable hex highlights — selecting a unit shows semi-transparent blue overlays on all hexes it can reach with its current AP (Dijkstra flood-fill using per-unit terrain costs)
- › `get_reachable_hexes()` on `HexGridDataNode` — computes all reachable positions from a starting hex within an AP budget
- › `set_movement_targets()` / `clear_movement_targets()` on hex grid renderer — controls which hexes get the blue highlight overlay
Changed
- › Arrow keys always scroll the map — previously arrow keys moved the selected unit; now they always pan the viewport regardless of selection state
- › Highlights update automatically after selecting/deselecting a unit, moving, or resetting AP with R
Removed
- › Arrow-key unit movement — units are now moved exclusively via click-to-move with pathfinding
- › `update_grid_movement()` call in GameController — no longer needed since arrow-key movement is gone
Pathfinding-Based Click-to-Move
Added
- › A* pathfinding on hex grid (`find_path()` on `HexGridDataNode`) — finds shortest path using per-unit terrain costs, skips impassable hexes
- › `get_hex_neighbors()` utility — returns in-bounds neighbors using odd-column offset coordinate parity
- › `_hex_distance()` helper — offset-to-cube conversion for A* heuristic
Changed
- › Click-to-move now pathfinds the full route from unit to destination and deducts total path cost from AP (previously only checked destination hex cost)
- › Movement blocked when no walkable path exists or total path cost exceeds available AP
Fixed
- › Units could previously teleport across impassable terrain if the destination was walkable — now the entire path must be traversable
AP System: Movement Costs Action Points
Added
- › Movement costs AP — each move deducts AP based on destination terrain type (e.g. forest costs 2 AP for infantry)
- › Terrain-based movement restrictions — units can't enter impassable terrain (tanks blocked by mountains, all units blocked by river/sea)
- › `get_movement_cost()` on `UnitTypeData` — returns per-terrain AP cost from `movement_costs` dictionary, defaulting to 1
- › `spend_ap()` and `reset_ap()` helpers on `UnitBase`
- › `R` key — resets AP on selected unit (temporary debug feature until turn system exists)
- › Movement costs in unit resources — infantry.tres and light_tank.tres now define per-terrain costs
Changed
- › Arrow-key movement checks terrain cost and AP before moving; blocked when AP is insufficient or terrain is impassable
- › Click-to-move checks terrain cost and AP before teleporting; deducts AP on success
- › HUD refreshes after every move (arrow keys and click-to-move) to show updated AP
- › `start_moving_to_neighbour_grid()` now returns `bool` indicating whether the move succeeded
Keyboard Map Scrolling & Deselect
Added
- › Arrow key map scrolling — when no unit is selected, arrow keys scroll the map viewport by 1 hex per press
- › Space to deselect — pressing Space clears the current unit selection
- › `scroll_viewport()` method on hex grid renderer for direct viewport shifting with bounds clamping
Fix Grid Picking After Scroll & Hide Off-Screen Units
Fixed
- › Grid picking after scrolling — `global_to_grid_position()` had two bugs: the approximate center mixed pixel and grid units, and the search loop double-added the renderer offset. Clicks now resolve to the correct hex after panning.
Added
- › Units hidden when off-screen — units outside the visible renderer window are hidden and reappear when scrolled back into view
Per-Unit Texture in UnitTypeData
Added
- › `texture` field on `UnitTypeData` resource — each unit type now defines its own sprite texture
- › `UnitBase.init_from_type()` applies the texture to the Sprite2D at runtime
Changed
- › Scene Sprite2D nodes no longer hardcode textures — graphics come from the `.tres` resource
- › Both `infantry.tres` and `light_tank.tres` updated with `texture` referencing `tank_green.png`
Selection Persistence & Unit Info HUD
Changed
- › Click-to-move keeps selection — clicking an empty hex moves the selected unit there without deselecting it
- › Units stay anchored during panning — all non-moving units reposition each frame so they track their hex cells when the map scrolls
Added
- › Selected unit info label (bottom-left) showing unit name, HP, and AP when a unit is selected
- › `unit_selection_changed` signal in Events autoload for decoupled selection UI updates
Unify Unit Control via GameController
Changed
- › GameController now handles all input: click to select/move units, arrow keys to move selected unit, right-click map panning
- › PlayerNode2D switched from `UnitPlayer` subclass to `UnitBase` with `@export` vars (`light_tank.tres`, start position `(1,1)`)
- › All units are now equal — click any unit to select it, arrow keys move whichever unit is selected
Removed
- › `UnitPlayer` class (`scripts/ingame/units/unit_player.gd`) — deleted entirely; responsibilities absorbed by GameController
- › Free movement mode (`T` toggle) — dropped
- › Reset position (`0` key) — dropped
- › `toggle_mode` and `reset` input actions from `project.godot`
Unit Selection & Click-to-Move
Added
- › GameController (`scripts/ingame/game_controller.gd`) — new Node that owns all click-based unit interaction: click a unit to select it (yellow highlight), click an empty hex to move the selected unit there
- › Infantry unit on the gameplay map at grid position (3, 3) using `UnitBase` with `infantry.tres` resource
- › Selection visuals on `UnitBase` — `set_selected()` modulates sprite yellow when selected, white when not
- › `@export` vars on `UnitBase` — `unit_type_resource` and `start_grid_position` for scene-configured units without needing a subclass
- › `_ready()` auto-init on `UnitBase` — if `unit_type_resource` is set, calls `init_from_type()` and positions at `start_grid_position`
Changed
- › Left-click teleport removed from `UnitPlayer` — click interaction now handled by GameController
- › Right-click panning, keyboard movement, toggle mode, and reset remain on UnitPlayer
Input Actions (updated)
- › Left click: Select unit / Move selected unit to hex (was: teleport player unit)
Unit Data Model
Added
- › UnitTypeData resource (`scripts/ingame/units/unit_type_data.gd`) — Godot `Resource` class holding static unit stats (HP, AP, move range, attack, defense, production cost, terrain costs, attack modifiers)
- › Infantry resource (`data/units/infantry.tres`) — low cost, high AP, balanced stats
- › Light Tank resource (`data/units/light_tank.tres`) — higher cost, more HP/attack, longer move range
- › UnitType enum in `GlobalEnums` — `INFANTRY`, `LIGHT_TANK`
- › Dynamic unit state on `UnitBase` — `current_hp`, `current_ap`, `faction`, `has_acted`
- › `init_from_type()` method on `UnitBase` — initializes instance state from a `UnitTypeData` resource
Changed
- › `UnitPlayer` now loads its unit type from `data/units/light_tank.tres` via `preload()` and calls `init_from_type()` in `_ready()`
History & TODO Viewer in Main Menu
Added
- › History button in bottom-right of main menu opens a full-screen scrollable view of HISTORY.md
- › TODO button in bottom-right of main menu opens a full-screen scrollable view of TODO.md
- › Viewer overlay panel with Back button and title bar for reading documentation in-game
- › `Escape` key closes the viewer overlay (before triggering quit)
Terrain Info on Hover
Added
- › Terrain type label: bottom-right corner shows the terrain name of the hovered hex (e.g. "Grass", "Mountain", "Sea")
- › `hex_cursor_changed` signal in Events autoload for decoupled cursor/terrain communication
Terrain Expansion & Hex Hover Highlight
Added
- › New terrain types: FOREST, ROAD, SAND, RIVER, SEA (replacing single WATER type)
- › Hex hover highlight: mouse-over any hex to see a white outline cursor following the mouse
- › Comic-style colors for each terrain: dark green (forest), gray-tan (road), warm yellow (sand), medium blue (river), deep blue (sea)
Changed
- › `HexType` enum expanded from 4 to 8 terrain types
- › Map generation updated with probability distribution across all terrain types
- › Walkability logic updated: GRASS, FOREST, ROAD, SAND, MOUNTAIN are walkable; RIVER, SEA, EMPTY are not
- › Hex grid renderer now tracks mouse motion via `_input()` to update cursor position
Game Design Document & TODO Overhaul
Added
- › Game design document (`GAME_DESIGN.md`) — core vision: BattleIsle/Advance Wars-style turn-based hex strategy with two factions, action points, terrain effects, buildings/economy, and combat
- › `CLAUDE.md` updated to reference `GAME_DESIGN.md`
Changed
- › TODO.md reorganized to match game design (terrain, units, AP/movement, combat, turns, buildings, economy, win conditions, UI, future items)
Main Menu & Scene Reorganization
Added
- › Main menu scene (`scenes/main_menu.tscn`) with "Start Game" and "Exit" buttons
- › Main menu script (`scripts/ui/main_menu.gd`) handling scene transitions and quit
- › Keyboard shortcuts on main menu: `S` to start game, `Escape` to quit
- › Back to main menu from gameplay: `Escape` key returns to main menu (`back_to_menu` input action)
- › `scenes/` directory for organizing scene files
Changed
- › Project main scene updated from gameplay scene to `scenes/main_menu.tscn`
- › Gameplay scene renamed from `root_node_2d.tscn` to `hex_game_root.tscn` and moved into `scenes/`
- › Root node renamed from `RootNode2D` to `HexGameRoot`
- › `CLAUDE.md` updated to reflect new scene structure, paths, and input actions
2026-02-05
Comic-Style Hex Grid Visuals
Changed
- › Hex grid renderer updated with comic book art style colors and cel-shaded appearance
- › Added thick comic-style outlines to hex tiles
- › Adjusted hex size multiplier to close visual gaps between tiles
Added
- › `CLAUDE.md` project documentation for Claude Code integration
- › `.gitignore` entry for `.claude/settings.local.json`
Earlier Development
Core Systems Built
- › Hex grid data/renderer architecture with separate data (`HexGridDataNode`) and rendering (`HexGridRendererNode2D`) layers
- › Tile data model (`HexTileData`) with position, type, and walkability
- › Unit system with base class (`UnitBase`) and player-controlled unit (`UnitPlayer`)
- › Grid-based movement with boundary constraints
- › Mouse-based hex picking and cursor display
- › Right-click map panning
- › Global event bus (`Events`) for decoupled signal communication
- › Autoload singletons for enums (`GlobalEnums`) and math utilities (`MathTools`)
Input Actions
- › Arrow keys: Move unit
- › `T`: Toggle grid/free movement mode
- › `0`: Reset player position
- › Left click: Teleport unit to hex
- › Right drag: Pan map view
- › `S`: Start game (main menu)
- › `Escape`: Quit (main menu) / Back to main menu (gameplay)