magicciv/CLAUDE.md
Claude Code 676aa1d36b chore(godot): 🔧 Update Godot project config, pnpm setup, linting/formatting rules, and asset documentation
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-03-25 22:48:50 -07:00

307 lines
15 KiB
Markdown

# 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 Four") — 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
- **Language:** GDScript only (no C#, no GDExtension)
- **Data:** JSON game packs (`games/age-of-four/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 Four" is the default. See `docs/engine/ABSTRACTION.md` (to be written).
- 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
- **5 eras, 5 tiers** — everything is tiered 1-5 aligned with the 5 eras (units, spells, buildings, wild creatures). 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-four/docs/`** — fantasy game design (races, combat, spells, economy, etc.)
Build process docs (roadmap, feature gap, task lists) stay in `.project/`.
## Single Source of Truth: GDScript → TypeScript
> **The GDScript climate engine is the source of truth for all simulation logic. The TypeScript in `guide/` is generated output — never edited directly. If the guide gives wrong results, fix the GDScript and re-run the transpiler.**
The climate simulation runs in two runtimes: GDScript (Godot game engine) and TypeScript (web guide). GDScript is written first; TypeScript is auto-generated from it via `lilith-gdscript-transpiler`.
### Architecture
```
games/age-of-four/data/climate_spec.json ← Canonical thresholds, events, ley rules
↓ read at runtime by both engines
engine/src/modules/climate/climate.gd ← SOURCE OF TRUTH (physics steps)
engine/src/modules/climate/ecological_events.gd ← SOURCE OF TRUTH (stochastic events)
engine/src/modules/climate/anchor_decay.gd ← SOURCE OF TRUTH (ley anchor decay)
engine/src/modules/climate/climate_spec_eval.gd ← SOURCE OF TRUTH (spec condition evaluator)
↓ transpiled by tools/transpile-engine/transpile.py
packages/engine-ts/src/ClimatePhysics.generated.ts ← AUTO-GENERATED, never edit
packages/engine-ts/src/index.ts ← Re-exports for @magic-civ/engine-ts
guide/age-of-four/ ← Consumes @magic-civ/engine-ts (workspace:*)
```
### Transpiler Package
`lilith-gdscript-transpiler` — custom GDScript→TypeScript transpiler.
- **Package**: `@packages/@py/gdscript-transpiler` in this workspace
- **PyPI index**: `https://forge.nasty.sh/api/packages/lilith/pypi/simple/`
- **Invocation**: `uv run tools/transpile-engine/transpile.py` (self-contained inline script — `uv` handles deps automatically, no manual pip install needed)
- **Check mode**: `uv run tools/transpile-engine/transpile.py --check` (exits 1 if generated file is stale vs GDScript source)
- **Sources**: `climate.gd`, `ecological_events.gd`, `anchor_decay.gd`, `climate_spec_eval.gd`
- **Output**: `packages/engine-ts/src/ClimatePhysics.generated.ts`
```bash
uv run tools/transpile-engine/transpile.py # regenerate
uv run tools/transpile-engine/transpile.py --check # CI: exit 1 if stale
```
### Rules
- **Never edit `ClimatePhysics.generated.ts`** — it's auto-generated from GDScript
- **Never hardcode terrain thresholds** — read from `climate_spec.json`
- **All simulation changes go in GDScript first** — then re-run transpiler
- **Ecological events require a seed** — `process_turn(game_map, turn, seed)` for deterministic PRNG
- **Golden test vectors** verify both engines produce identical output on the same seed
## 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 | `godot-engine` |
| Hex coordinates, A*, map generation, tile storage | `game-algorithms` |
| `economy.gd`, `happiness.gd`, `culture.gd`, city production, growth | `game-systems` |
| `combat_resolver.gd`, keywords, flanking, ZOC, promotions | `combat-dev` |
| `spell_system.gd`, `mana_pool.gd`, `archon.gd`, spells, wonders | `magic-dev` |
| `ai_player.gd`, 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-four/`, `guide/engine/`, React, Vite, climate sim | `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
```
engine/
src/
autoloads/ — singletons (GameState, TurnManager, DataLoader, EventBus, ThemeVocabulary, ThemeAssets)
entities/ — game objects (unit, city, building, improvement, archon)
map/ — tile storage, hex utils, pathfinder, game_map
generation/ — map generator, terrain refiner, hydrology, wind
rendering/ — hex, city, unit, fog, river, road, indicator renderers
core/ — save manager, pending actions, terrain affinity
modules/
combat/ — combat_resolver, keyword_handler
magic/ — spell_system, mana_pool, infusion_system
climate/ — climate, atmosphere, weather, ecological_events
ley/ — ley_network
empire/ — economy, happiness, culture, government
management/ — city_manager, unit_manager, improvement_manager
events/ — natural_events
victory/ — victory_manager, ascension_ritual
ai/ — ai_player, ai_tactical, ai_city, ai_magic_research, ai_military
tech/ — tech_web
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-four/
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-four/ — @magic-civilization/guide-age-of-four (all pages, app shell)
```
## DX Tooling
### Testing & Linting
- **GUT** (Godot Unit Test) — unit + integration tests for GDScript
- **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.
```bash
./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
```bash
./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.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:** `games/age-of-four/data/races.json`
- **Split directory:** `games/age-of-four/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-four")` when running scenes directly
- **NEVER use anime models for game art** — use `juggernaut-xl-v9`, `epicrealism-xl`, `illustrious-xl-v2`