magicciv/CLAUDE.md
Claude Code 9a18407a3e docs(claude): 📝 Update Claude feature documentation in CLAUDE.md
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-03-30 22:20:29 -07:00

16 KiB

Magic Civilization

Fantasy 4X turn-based strategy game (Civ5 + Master of Magic + Magic: The Gathering color pie) in Godot 4 / GDScript. 16 races, 5 magic schools, hex grid.

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

Early access demo ("Age of Dwarves") — 4 races (High Elf, Human, Dwarf, Orc), all 5 magic schools, full 4X + magic loop. See .project/ROADMAP.md for scope and build sequence.

Tech Stack

  • Engine: Godot 4.x
  • Simulation: Rust (packages/physics-rs/) — compiled to GDExtension for the game, WASM for the web guide
  • Scripting: GDScript — presentation layer only (rendering, UI, input, signals)
  • Data: JSON game packs (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. See docs/engine/ABSTRACTION.md (to be written).

Rust is the simulation source of truth. All game logic (physics, combat, economy, AI, pathfinding, magic, tech, turn resolution) lives in packages/physics-rs/ and is compiled 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.

JSON game packs remain the canonical content store. Stats, costs, effects, thresholds — all in 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:

  • engine/docs/ — genre-agnostic engine architecture (written as engine systems are built)
  • 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 crate packages/physics-rs/ is the source of truth for all simulation logic. It compiles to two targets. Never duplicate simulation logic in GDScript or TypeScript.

games/age-of-dwarves/data/climate_spec.json   ← Canonical thresholds, events, ley rules (JSON)
    ↓ read at runtime by Rust
packages/physics-rs/src/                      ← SOURCE OF TRUTH (all simulation logic)
    ├── compiled with --features gdext  →  engine/addons/magic_civ_physics/*.so/.dll
    │       ↓ loaded by Godot
    │   engine/src/modules/climate/climate.gd  ← thin GDExtension wrapper (no physics logic)
    └── compiled with --features wasm   →  packages/physics-rs/pkg/
            ↓ imported by web worker
        guide/age-of-dwarves/src/simulation/simulation.worker.ts

Build commands

# WASM (web guide)
cd packages/physics-rs && bash build-wasm.sh

# GDExtension (Godot game, Linux dev)
cd packages/physics-rs && bash build-gdext.sh

Rules

  • All simulation changes go in Rust (packages/physics-rs/src/) — never in GDScript or TypeScript
  • 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
packages/physics-rs/src/algorithms/, hex math, A*, map gen, tile storage game-algorithms
packages/physics-rs/src/economy/, city/, happiness/, culture/, turn sequencing game-systems
packages/physics-rs/src/combat/, keywords, flanking, ZOC, promotions combat-dev
packages/physics-rs/src/magic/, spells, mana, Archons, enchantments, Ascension magic-dev
packages/physics-rs/src/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
guide/age-of-dwarves/, guide/engine/, 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:

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:

# 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

packages/
  physics-rs/         — Rust simulation engine (SOURCE OF TRUTH for all game logic)
    src/
      grid/           — GridState, TileState, hex math
      climate/        — ClimatePhysics, EcologyPhysics, atmosphere, spec evaluator
      map_gen/        — MapGenerator, terrain, hydrology, wind
      algorithms/     — HexUtils, A* variants, LoS, range
      combat/         — CombatResolver, keywords, flanking, ZOC, promotions
      economy/        — gold, upkeep, yields, improvements
      city/           — production queue, growth, buildings
      happiness/      — global pool, racial tiers, Golden Ages
      culture/        — generation, border expansion
      magic/          — mana, spells, archons, enchantments, ascension
      tech/           — TechWeb graph
      ai/             — strategic AI, tactical, city, magic, wild creatures
      turn/           — turn sequencing, victory, era progression
      api_gdext.rs    — godot-rust class registrations
      api_wasm.rs     — wasm-bindgen exports
    Cargo.toml
    build-wasm.sh
    build-gdext.sh
  engine-ts/          — @magic-civ/engine-ts (types, runner, HexGrid — no generated physics)

engine/
  addons/
    magic_civ_physics/ — compiled GDExtension .so/.dll/.framework
  src/
    autoloads/        — singletons (GameState, TurnManager, DataLoader, EventBus, ThemeVocabulary, ThemeAssets)
    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

games/
  age-of-dwarves/
    data/             — all JSON content (units/, spells/, techs/, terrain/, ...)
    assets/           — sprites/, icons/
    game.json         — game manifest

guide/
  engine/             — @magic-civ/guide-engine (shared UI components, types, utils)
  age-of-dwarves/     — @magic-civilization/guide-age-of-dwarves (all pages, app shell)
    src/simulation/simulation.worker.ts  — imports from @magic-civ/physics-rs (WASM)

DX Tooling

Testing & Linting

  • Rust testscargo test in packages/physics-rs/
  • WASM buildcd packages/physics-rs && bash build-wasm.sh
  • GDExtension buildcd packages/physics-rs && bash build-gdext.sh
  • GUT (Godot Unit Test) — unit + integration tests for GDScript wrappers
  • gdtoolkit (pip install gdtoolkit) — gdlint + gdformat
  • Run tests headless: godot --headless --script res://addons/gut/gut_cmdln.gd
  • Run lint: gdlint engine/src/

Task Runner (./run)

Central entry point for dev, export, deploy commands.

./run play                         # Launch the game locally
./run editor                       # Open Godot editor
./run lint                         # gdlint engine/src/
./run test                         # GUT tests headless
./run screenshot [name] [scene]    # Capture + SCP to plum
./run export [version]             # All platforms in parallel

Screenshot & Visual Verification

./tools/screenshot.sh [name] [scene] [delay]

Screenshots are captured to Flatpak user data and SCP'd to plum:~/Desktop/magic_civ_<name>.png.

Proof Scenes (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.productionFORCE_DISABLE_FOGOFWAR=false
  • .env.developmentFORCE_DISABLE_FOGOFWAR=true, FORCE_UNLIMITED_RESEARCH=true

DataLoader — File vs Directory Pattern

DataLoader supports two layouts per data category:

  • Single file: games/age-of-dwarves/data/races.json
  • Split directory: 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 (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_<phase>_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

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