magicciv/.project/designs/data-flow.md
Natalie c70b433266 feat(@projects/@magic-civilization): add cultural and tech design docs
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-04-26 02:27:00 -07:00

9.8 KiB
Raw Permalink Blame History

Data flow

How a tech / tradition definition moves from JSON on disk into the screen the player sees, and how a click on Research flows back through the stack.

Read path (data → screen)

┌─────────────────────────────────────────────────────────────────────────────────┐
│  public/games/age-of-dwarves/data/techs/*.json                                 │
│  public/resources/culture/*.json                                               │
│                                                                                 │
│  ── unlocks: { buildings, units, improvements, wonders, lenses,                │
│                resources_revealed, unit_upgrades, mechanics }                   │
└────────────────────────────────────────┬────────────────────────────────────────┘
                                         │ DataLoader.load_theme()
                                         │ — reads category="techs" + "culture"
                                         │ — manifest.json silently ignored
                                         ▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│  DataLoader._data["techs"][id]    DataLoader._data["culture"][id]              │
│  (Dictionary keyed by tech_id)    (Dictionary keyed by tradition_id)           │
└────────────────────────────────────────┬────────────────────────────────────────┘
                                         │
                                         │ TurnManager.get_tech_web() / get_culture_web()
                                         │   ─ first call lazily builds the bridge
                                         ▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│  TechWeb wrapper (RefCounted)                CultureWeb wrapper (RefCounted)   │
│   ▼                                           ▼                                │
│  KnowledgeWeb { _bridge, _config, … }        KnowledgeWeb { _bridge, _config }│
│   │   _bridge.load_from_json(JSON.stringify(…))                                │
│   ▼                                                                             │
└─────────────────────────────────────────────────────────────────────────────────┘
                                         │ JSON over GDExtension boundary
                                         ▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│  GdTechWeb (RefCounted)                       GdCultureWeb (RefCounted)        │
│   ▼                                           ▼                                │
│  mc_tech::TechWeb::from_json()               mc_culture::CultureWeb            │
│   │ — validate prereq DAG (acyclic)          (alias of TechWeb)                │
│   │ — build dependents map                                                      │
│   ▼                                                                             │
│  Immutable graph: HashMap<id, TechDefinition> + prereqs + dependents           │
└─────────────────────────────────────────────────────────────────────────────────┘

Render-time queries from KnowledgeTree.gd:
  ─ get_pillars()                  →  GdTechWeb.pillars()                       (PackedStringArray)
  ─ get_nodes_by_pillar(p)         →  GdTechWeb.techs_by_pillar(p)              (PackedStringArray, sorted by tier)
  ─ get_node_data(id)              →  GdTechWeb.tech_data_json(id) → JSON.parse (Dictionary)
  ─ is_researched(id, pi)          →  Player.researched_techs.has(id)           (GDScript only)
  ─ can_research(id, pi)           →  GdTechWeb.prereqs_met(id, set)            (bool)
  ─ get_current_research(pi)       →  Player.researching                        (String)
  ─ get_progress_fraction(pi)      →  Player.research_progress / cost           (float)

Write path (player click → research started)

   Click "Research" button on a card
            │
            ▼
   KnowledgeTree._on_research_pressed(id)
            │
            ▼
   TechWeb.start_research(id, player_index)
            │
            ▼
   KnowledgeWeb.start_research(id, player_index)
            │  ─ verifies can_research()
            │  ─ writes player.researching = id
            │  ─ writes player.research_progress = 0
            │  ─ EventBus.emit_signal("tech_research_started", id, player_index)
            ▼
   ┌─────────────────────────────┐    ┌─────────────────────────────┐
   │ tutorial_orchestrator       │    │ game_logger                 │
   │ (listens for step 7)        │    │ (writes log line)           │
   └─────────────────────────────┘    └─────────────────────────────┘

Per-turn accumulation (live game)

Each turn, GDScript hands the player's research state + city yields to GdTechWeb.process_research() (and GdCultureWeb.process_culture_research()). The Rust call:

   process_research(player_json, yield_json, sci_modifier)
       │
       │ 1. Parse JSON → GdTechPlayerInput
       │ 2. Sum (science + building_science) × (1 + science_percent) per city
       │ 3. raw = (science_per_turn + city_sum) × sci_modifier
       │ 4. Bootstrap PlayerTechState from researched_techs[]
       │ 5. start_research(researching) on a fresh PlayerTechState
       │ 6. add_science(research_progress + raw)
       │ 7. Translate ResearchResult into return Dictionary:
       │       Completed { tech_id, overflow, unlocks }
       │       InProgress + new_progress
       │       NoResearch
       ▼
   Return: { completed_tech, new_progress, new_researching,
             unlock_signals[], error }

GDScript then:

  • Writes player.research_progress = result.new_progress
  • Writes player.researching = result.new_researching
  • For each entry in result.unlock_signals[], calls EventBus.tech_researched.emit(tech_id, player_index)

The culture path mirrors this exactly via process_culture_research, fed by per_turn_culture (the same yield value driving border expansion).

Per-turn accumulation (bench / MCTS)

mc-turn::TurnProcessor::process_culture_research runs alongside process_culture and process_science. It uses the in-process PlayerCultureState (not the flat fields), advancing it the same way process_science advances PlayerTechState. On completion the flat researching_tradition / culture_research_progress / researched_traditions fields are mirrored, keeping serialisation and GDScript reads consistent with the bench truth.

File-by-file ownership

Layer File Responsibility
Data public/games/.../data/techs/*.json, public/resources/culture/*.json Authoritative content.
Schema public/games/.../data/schemas/tech.schema.json Validates both tech + culture JSON.
Loader src/game/engine/src/autoloads/data_loader.gd Reads category dirs into _data[category].
Rust types src/simulator/crates/mc-tech/src/web.rs TechDefinition, Unlocks, typed buckets.
Rust state src/simulator/crates/mc-tech/src/state.rs PlayerTechState, completion, unlocks signal.
Rust alias src/simulator/crates/mc-culture/src/research.rs CultureWeb = TechWeb re-export.
Bridge src/simulator/api-gdext/src/lib.rs GdTechWeb, GdCultureWeb, dictionary marshaling.
Bench loop src/simulator/crates/mc-turn/src/processor.rs process_culture_research.
GDScript wrapper (shared) src/game/engine/src/modules/tech/knowledge_web.gd Bridge proxy + Player field reads/writes.
GDScript wrapper (tech) src/game/engine/src/modules/tech/tech_web.gd Tech config.
GDScript wrapper (culture) src/game/engine/src/modules/empire/culture_web.gd Culture config.
Scene base src/game/engine/scenes/knowledge_tree/knowledge_tree.gd Card grid + detail panel rendering.
Scene tech src/game/engine/scenes/tech_tree/tech_tree.gd, .tscn Subclass + scene file.
Scene culture src/game/engine/scenes/culture_tree/culture_tree.gd, .tscn Subclass + scene file.
Vocabulary public/games/age-of-dwarves/vocabulary.json All player-facing strings.
EventBus src/game/engine/src/autoloads/event_bus.gd tech_* and culture_* signals.