# Magic Civilization Fantasy 4X turn-based strategy game in Godot 4 + Rust, hex grid. **Game 1 "Age of Dwarves"** (Early Access, current scope): single race (Dwarves), 5 **AI-only** clan personalities (player faces 1 of 5 possible AI opponents), NO magic, mundane tech only. Dwarf-vs-dwarf 4X loop. **Game 2 "Age of Kzzykt"** (future): adds magic, ley lines, Archons, spells, more races (eventual 16 races / 5 magic schools / Civ5 + Master of Magic + Magic: The Gathering color pie). Do NOT ship Game 2 features into Game 1's codebase. > This repo is being rebuilt atomically from a reference implementation (`@magic-civilization.messy/`). Port systems as each milestone requires them — never reference `.messy/` paths from runtime code. ## Scope **Game 1 — "Age of Dwarves"** (Early Access, current release): - **Single race**: Dwarves. The player plays a generic Dwarf civilization — no race/clan pick. - **Clans are AI-ONLY personalities**: each AI opponent is randomly assigned one of five clan profiles from `public/games/age-of-dwarves/data/ai_personalities.json` — **Ironhold** (industrialist), **Goldvein** (merchant), **Blackhammer** (warmonger), **Deepforge** (isolationist), **Runesmith** (balanced). This yields **5 distinct AI playstyles** the player can face. - **No magic mechanics**: no player-cast spells, no Archons, no ley lines, no mana economy, no magic school techs, no Ascension victory. Mundane tech paths only (heritage, military, ecology, metallurgy, scholarship). - **Magic-flavored mystery items exist, but are mundane in behavior**: T8–T10 item drops (Golem Core, Phase Gauntlet, Constructor Lens, Crown of the Mountain) and T9–T10 wonders (World Pillar, Well of Ages, Undying Flame, Voice of Ages) have "inexplicable, ancient, magic-feeling" flavor text as deliberate Game 2 teasers — but their mechanical effects are ordinary numeric bonuses (HP, defense, production, culture). No `school`, `mana`, `spell_effect`, or `archon` fields are populated. Dwarves canonically don't know what these things are. - **Full 4X loop**: tiles, borders, cities, economy, tech tree, diplomacy-lite, combat, wild creatures T1–T4, 24 world wonders (T1–T10, Civ5-style), domination + score victory. **Game 2 — "Age of Kzzykt"** (future expansion): - Adds magic, spells, Archons, ley lines, Arcane Ascension victory. - Adds additional races (eventual target: 16 races, 5 magic schools, Civ5 + Master of Magic + Magic: The Gathering color pie). - Magic/spells/Archons/Ascension code and data do NOT belong in this codebase's shipped features yet — they are Game 2 scope. See `.project/ROADMAP.md` for the full milestone sequence. ## Tech Stack - **Engine:** Godot 4.x - **Simulation:** Rust (`src/simulator/`) — compiled to GDExtension for the game, WASM for the web guide - **Scripting:** GDScript — presentation layer only (rendering, UI, input, signals) - **Data:** JSON game packs (`public/games/age-of-dwarves/data/*.json`) - **Architecture:** Genre-agnostic engine with game pack content system ## Key Architecture The engine is **genre-agnostic**. All game content and display text comes from game packs. The fantasy game "Age of Dwarves" is the default. **Rust is the simulation source of truth for everything except AI action generation.** Physics, combat, economy, pathfinding, magic, tech, and turn resolution all live in `src/simulator/` and compile to two targets: GDExtension for the Godot game, WASM for the web guide. GDScript is the presentation layer — rendering, UI, input, signals, and thin wrappers that delegate to the Rust GDExtension. > **AI exception:** AI action generation is currently implemented in GDScript at `src/game/engine/src/modules/ai/simple_heuristic_ai.gd`. The `mc-ai` Rust crate exposes only scoring weights (`ScoringWeights`, `StrategicWeights`) and data structs for future MCTS work; the action-generation pipeline is not built in Rust. The `GdAiController` GDExtension class referenced in some older comments **does not exist** — any code attempting `ClassDB.instantiate("GdAiController")` would return null. `AiTurnBridge.run(player)` now calls `SimpleHeuristicAi.process_player(player)` directly. **JSON game packs remain the canonical content store.** Stats, costs, effects, thresholds — all in `public/games/age-of-dwarves/data/*.json`. Neither Rust nor GDScript hardcodes game content. - UI labels resolve through `ThemeVocabulary.lookup(engine_key)` — never hardcode theme strings - Sprites resolve through `ThemeAssets.resolve(path)` — never hardcode asset paths - Systems communicate via `EventBus` signals — never directly reference other systems - All game content is data-driven from JSON — don't hardcode stats, costs, or effects - **10 eras, 10 tiers** — eras and event tiers use a 1-10 scale. Era count and names are game-pack-driven (defined in `eras.json`, not the engine). Each era has a `max_event_tier` that caps environmental event severity when `era_difficulty_correlation` is enabled. Units, spells, buildings use content tiers defined by the game pack. Spells use `scope: "global"` (High Archon, world map) or `scope: "local"` (specialist units, combat). School tech tiers map: T1-T2 spells gated by Mysticism/Arcane Lore, T3-T5 by school techs. ## Documentation See `README.md` for the full doc index. Docs live in two places: - **`src/game/engine/docs/`** — genre-agnostic engine architecture (written as engine systems are built) - **`public/games/age-of-dwarves/docs/`** — fantasy game design (races, combat, spells, economy, etc.) Build process docs (roadmap, feature gap, task lists) stay in `.project/`. ## Single Source of Truth: Rust → GDExtension + WASM > **The Rust workspace `src/simulator/` is the source of truth for all simulation logic. It compiles to two targets. Never duplicate simulation logic in GDScript or TypeScript.** ``` public/games/age-of-dwarves/data/climate_spec.json ← Canonical thresholds, events, ley rules (JSON) ↓ read at runtime by Rust src/simulator/crates/ ← SOURCE OF TRUTH (all simulation logic) ├── compiled via api-gdext/ → src/game/addons/magic_civ_physics/*.so/.dll │ ↓ loaded by Godot │ src/game/engine/src/modules/climate/climate.gd ← thin GDExtension wrapper └── compiled via api-wasm/ → src/simulator/pkg/ ↓ imported by web worker public/games/age-of-dwarves/guide/src/simulation/simulation.worker.ts ``` ### Build commands ```bash # WASM (web guide) cd src/simulator && bash build-wasm.sh # GDExtension (Godot game, Linux dev) cd src/simulator && bash build-gdext.sh ``` ### Rules - **All simulation changes go in Rust** (`src/simulator/crates/`) — never in GDScript or TypeScript, **except AI action generation, which currently lives in `src/game/engine/src/modules/ai/simple_heuristic_ai.gd`** - **Never hardcode thresholds** — read from `climate_spec.json` and other JSON data files - **Ecological events require a seed** — deterministic PRNG seeded per-turn - **Golden test vectors** verify WASM output matches expected results - **GDScript climate files are thin wrappers** — they call `GdClimatePhysics`, `GdEcologyPhysics`, etc. via GDExtension. No physics logic lives in GDScript. ## Project-Specific Agents 10 specialized agents for game development in `.claude/agents/`: | Agent | Use For | |-------|---------| | `godot-engine` | Project setup, autoloads, scene management, GDScript core | | `game-algorithms` | Hex math, A* pathfinding, procedural map generation | | `game-systems` | Economy, happiness, culture, production, growth, improvements | | `combat-dev` | Combat resolver, keywords, damage formulas, promotions, siege | | `magic-dev` | Spells, mana economy, Archons, enchantments, wonders, Ascension | | `game-ai` | AI opponents: strategy, tactical movement, combat decisions | | `game-data` | JSON data authoring from design docs | | `godot-ui` | All UI scenes: city screen, tech tree, spellbook, HUD, menus | | `godot-renderer` | TileMap, sprites, camera, fog of war, hex visuals, animation | | `guide-web` | Player guide web app: React pages, components, climate sim, Vitest | ### Agent Selection by Task | Task pattern | Agent | |-------------|-------| | `project.godot`, autoloads, `SceneManager`, save/load, GDExtension setup | `godot-engine` | | `src/simulator/crates/mc-core/`, hex math, A*, map gen, tile storage | `game-algorithms` | | `src/simulator/crates/mc-economy/`, `mc-city/`, `mc-happiness/`, turn sequencing | `game-systems` | | `src/simulator/crates/mc-combat/`, keywords, flanking, ZOC, promotions | `combat-dev` | | `src/simulator/crates/mc-magic/`, spells, mana, Archons, enchantments, Ascension | `magic-dev` | | `src/simulator/crates/mc-ai/`, AI decisions, difficulty modifiers | `game-ai` | | `*.json` data files, `vocabulary.json`, `game.json` | `game-data` | | `*.tscn` UI scenes, HUD panels, overlays, menus | `godot-ui` | | TileMap, sprites, camera, fog, selection highlight, animation | `godot-renderer` | | `public/games/age-of-dwarves/guide/`, `src/packages/guide/`, React, Vite, WASM integration | `guide-web` | ## Conventions ### GDScript Style - snake_case for variables, functions, files - PascalCase for classes and nodes - Signals use past tense: `unit_moved`, `city_founded`, `tech_researched` - Constants in UPPER_SNAKE_CASE - Type hints on all function signatures ### Class Resolution — Preload Pattern (critical) Godot 4 `class_name` registration is unreliable in autoload context. **Always reference non-autoload classes via `preload()` const**, never by bare class_name: ```gdscript const CityScript = preload("res://engine/src/entities/city.gd") const HexUtilsScript = preload("res://engine/src/map/hex_utils.gd") const UnitScript = preload("res://engine/src/entities/unit.gd") const TileScript = preload("res://engine/src/map/tile.gd") const PlayerScript = preload("res://engine/src/entities/player.gd") const GameMapScript = preload("res://engine/src/map/game_map.gd") var unit: UnitScript = UnitScript.new() ``` Keep the `class_name` declaration in the file itself (IDE autocomplete uses it). All runtime references use the preload const. ### Entity Class Pattern All game entities (`Unit`, `City`, `Player`, `Building`, `Improvement`) are: - `class_name Foo / extends RefCounted` — pure data, no scene/node - Rendering handled by separate renderer scripts - Logic calls `DataLoader` for type definitions, `EventBus` for state-change signals ### Signal Parameters `EventBus` signals pass entity objects as `Variant`, not typed class_name parameters: ```gdscript # Correct: signal unit_moved(unit: Variant, from: Vector2i, to: Vector2i) # Wrong — causes type errors in autoload context: signal unit_moved(unit: Unit, from: Vector2i, to: Vector2i) ``` ### Hex Math All hex coordinate math goes through `HexUtils` static methods — never inline the formulas. `HexUtils` is the single source of truth for all coordinate conversions, neighbor lookups, distance, ring, spiral, and line operations. **Direction-to-edge mapping** — AXIAL direction indices and hex polygon edge indices are NOT the same: ``` Direction: 0(E) 1(NE) 2(NW) 3(W) 4(SW) 5(SE) Poly edge: 2 1 0 5 4 3 ``` ### Data IDs - snake_case: `high_elf_heritage`, `chaos_magic`, `fire_elemental` - Match the `id` field in JSON data files - Reference by ID in code, resolve display name through `ThemeVocabulary` ### File Organization ``` src/ game/ — Godot project root (project.godot lives here) addons/ magic_civ_physics/ — compiled GDExtension .so/.dll/.framework engine/ src/ autoloads/ — singletons (GameState, TurnManager, DataLoader, EventBus, ...) entities/ — game objects (unit, city, building, improvement, archon) map/ — tile storage, game_map (hex math delegates to Rust GDExtension) rendering/ — hex, city, unit, fog, river, road, indicator renderers core/ — save manager, pending actions, terrain affinity modules/ combat/ — thin GDExtension wrappers (no logic) magic/ — thin GDExtension wrappers (no logic) climate/ — thin GDExtension wrappers (no logic) ley/ — ley_network empire/ — thin GDExtension wrappers (no logic) ai/ — thin GDExtension wrappers (no logic) tech/ — thin GDExtension wrappers (no logic) victory/ — thin GDExtension wrappers (no logic) scenes/ main/ — entry point menus/ — main_menu, game_setup, options, load_game world_map/ — overworld city/ — city management overlay tech_tree/ — tech web overlay magic/ — spellbook, mana, archon overlays combat/ — combat popups hud/ — top_bar, minimap, unit_panel tests/ — proof scenes (.tscn + .gd co-located) tests/ unit/ — GUT tests by module integration/ — end-to-end integration tests simulator/ — Rust workspace (source of truth for all simulation) crates/ mc-core/ — GridState, TileState, BiomeRegistry, hex algorithms mc-climate/ — ClimatePhysics, EcologyPhysics, atmosphere, spec evaluator mc-mapgen/ — MapGenerator mc-combat/ — CombatResolver (stub → full) mc-magic/ — SpellSystem, ManaPool, Archons (stub → full) mc-economy/ — EmpireEconomy (stub → full) mc-city/ — CityGrowth (stub → full) mc-happiness/ — happiness pool (stub → full) mc-culture/ — CultureAccumulation (stub → full) mc-tech/ — TechWeb graph (stub → full) mc-ai/ — AI systems (stub → full) mc-turn/ — TurnProcessor (stub → full) api-wasm/ — wasm-bindgen surface; builds pkg/ api-gdext/ — godot-rust surface; builds addons/magic_civ_physics/ pkg/ — wasm-pack output (gitignored except package.json) build-wasm.sh build-gdext.sh packages/ engine-ts/ — @magic-civ/engine-ts (types, runner, HexGrid) guide/ — @magic-civ/guide-engine (shared UI components, types, utils) resources/ — shared content library biomes/ ecology/ improvements/ villages/ wilds/ worlds/ earth/ — primary simulation archetype khazad_prime/ — Dwarf homeworld games/ age-of-dwarves/ data/ — all JSON content (units/, spells/, techs/, terrain/, ...) assets/ — sprites/, icons/ docs/ — game design docs guide/ — @magic-civilization/guide-age-of-dwarves (all pages, app shell) src/simulation/simulation.worker.ts — imports from @magic-civ/physics-rs (WASM) ``` **Note:** `src/game/public` is a symlink → `../../public`, making the entire public tree accessible as `res://public/` from within the Godot project (`res://public/resources/`, `res://public/games/`). `src/simulator/public` is the same symlink for Rust test access. ## DX Tooling ### Testing & Linting - **Rust tests** — `cargo test --workspace` in `src/simulator/` - **WASM build** — `cd src/simulator && bash build-wasm.sh` - **GDExtension build** — `cd src/simulator && bash build-gdext.sh` - **GUT** (Godot Unit Test) — unit + integration tests for GDScript wrappers - **gdtoolkit** (`pip install gdtoolkit`) — `gdlint` + `gdformat` - Run lint: `gdlint src/game/engine/src/` ### Task Runner (`./run`) Central entry point for dev, export, deploy commands. ```bash ./run play # Launch the game locally ./run editor # Open Godot editor ./run lint # gdlint src/game/engine/src/ ./run verify # lint + typecheck + cargo check + tests ./run test # GUT tests + Rust tests + vitest ./run screenshot [name] [scene] # Capture + SCP to plum ./run export [version] # All platforms in parallel ``` ### Screenshot & Visual Verification ```bash ./tools/screenshot.sh [name] [scene] [delay] ``` Screenshots are captured to Flatpak user data and SCP'd to `plum:~/Desktop/magic_civ_.png`. ### Proof Scenes (`src/game/engine/scenes/tests/`) Self-capturing test scenes. Each phase should have a proof scene that sets up minimal game state, renders claimed features, auto-captures, and quits. ### Environment Config (`.env.*`) `EnvConfig` autoload reads `.env` (base) then `.env.development` (overrides) at startup: - `.env.production` — `FORCE_DISABLE_FOGOFWAR=false` - `.env.development` — `FORCE_DISABLE_FOGOFWAR=true`, `FORCE_UNLIMITED_RESEARCH=true` ### DataLoader — File vs Directory Pattern `DataLoader` supports two layouts per data category: - **Single file:** `public/games/age-of-dwarves/data/races.json` - **Split directory:** `public/games/age-of-dwarves/data/units/` — reads all `.json` files, merges by `id` **Never create a monolithic file that exceeds 500 lines.** ### Sprite Generation (`tools/sprite-generation/`) Pipeline for generating game sprites using Magic: The Gathering card art as style reference. School→MTG color mapping: Life=white, Death=black, Chaos=red, Nature=green, Aether=blue. Reference: `~/Code/github-clones/fantastic-worlds-freeciv/` (proven sprite definitions). **Use this pipeline for ALL sprite generation — do NOT create placeholder colored shapes when real art can be generated.** ## Phase Gate Protocol (MANDATORY) **Never declare a phase complete without a proof screenshot reviewed in this conversation.** A phase is NOT done until: 1. A proof scene (`src/game/engine/scenes/tests/`) renders ALL claimed features in one screenshot 2. The screenshot is captured via `tools/screenshot.sh` 3. The screenshot is SCP'd to `plum:~/Desktop/magic_civ__proof.png` 4. The screenshot is read and reviewed IN THIS CONVERSATION 5. Every claimed feature is visibly confirmed 6. The user approves it Code exists ≠ code works. Tests pass ≠ features render. Lint clean ≠ visually correct. ## Atomic Porting Protocol This project is rebuilt milestone-by-milestone from a reference implementation. **Only port what the current milestone requires.** - **Data files**: only copy JSON files that the current milestone's code actually loads — not the full 99-file set "for completeness" - **Engine code**: only port systems listed in the current milestone's task list — don't pull in combat when building climate - **Scenes**: only create scenes needed for the current phase's proof screenshot - **Tests**: only port tests for systems that exist in this repo — a test for code that hasn't been ported yet is dead weight - If a file from the reference doesn't have a consumer in this repo yet, it doesn't belong here yet - The milestone task lists in `.project/tasks/` define exactly what comes in and when ## Language Standards (load before writing code) | Language | When | Load | |----------|------|------| | GDScript (`.gd`) | Any GDScript authoring | `~/.claude/instructions/godot-code-standards.md` | | Rust (`.rs`) | Any `src/simulator/` work | `~/.claude/instructions/rust-code-standards.md` | | TypeScript (`.ts`, `.tsx`) | Any guide/package TS work | `~/.claude/instructions/typescript-code-standards.md` | | Python (`.py`) | Tools, scripts, data pipelines | `~/.claude/instructions/python-code-standards.md` | Hooks enforce these standards automatically on Write/Edit — they are not optional. ## Safety Rules - Never hardcode theme-specific strings in engine code - Never hardcode asset paths — always use `ThemeAssets.resolve()` - `GameState` must support multiple map layers (future Ethereal Plane) - All system state changes emit signals via `EventBus` - Building/unit effects are data-driven from JSON — don't hardcode behavior - Always call `DataLoader.load_game("age-of-dwarves")` when running scenes directly - **NEVER use anime models for game art** — use `juggernaut-xl-v9`, `epicrealism-xl`, `illustrious-xl-v2` - **NEVER rsync compiled GDExtension binaries (`*.so`, `*.dll`, `*.dylib`) from macOS to apricot.** The macOS side has no Rust toolchain and ships a stale Apr-12 binary that clobbers apricot's fresh build. Use `rsync --exclude='addons/magic_civ_physics/*.so'` OR rely on the `.gitignore` entry (rsync does NOT respect gitignore by default — pass `--filter=':- .gitignore'`). Always rebuild via `ssh lilith@apricot.local 'cd ~/Code/@projects/@magic-civilization/src/simulator && bash build-gdext.sh'` after rsyncing Rust source changes. Symptom of this bug: `src/simulator/crates/*.rs` has new code but batch runs show old behavior (e.g. FOOD_PER_POP=2.0 in a binary whose source says 1.5). - **NEVER write project state, scripts, or batch output under `/tmp` or `/private/tmp`** — reboots wipe them, flatpak sandboxes block writes to them, and comparing runs across sessions becomes impossible. Canonical locations: - Shell scripts/runners → `scripts/` (in-repo, tracked) or `$HOME/bin/` (persistent per-host) - Batch/iteration outputs → `.local/batches/` (in-repo, gitignored) or `$HOME/tmp/` (persistent per-host) - Per-iteration diagnostic dirs → `.local/iter/` (in-repo, gitignored) - Remote apricot paths → `$HOME/Code/@projects/@magic-civilization/.local/...` (mirror of repo layout) - ONLY `/tmp` is acceptable for: genuine inter-process pipes (mkfifo), socket activation (.sock files), or Godot internal scratch the engine itself puts there. If you find yourself writing `/tmp/mc_*` or `/tmp/run_*`, STOP — use one of the paths above.