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

15 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 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
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 seedprocess_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:

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

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.

./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-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