feat(@projects): add tech-culture domain categorization system

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-03 13:42:26 -04:00
parent a8b45cc083
commit e203bd1a09
5 changed files with 164 additions and 23 deletions

View file

@ -59,7 +59,8 @@ const CATEGORIES: readonly CategoryDef[] = [
{ id: "Agriculture", label: "Agriculture", color: "#66e666" },
{ id: "Governance", label: "Governance", color: "#e68c73" },
{ id: "Culture", label: "Culture", color: "#a07cc9" },
{ id: "Exploration", label: "Exploration", color: "#66bfff" },
{ id: "Science", label: "Science", color: "#66bfff" },
{ id: "Exploration", label: "Exploration", color: "#73a6e6" },
{ id: "Engineering", label: "Engineering", color: "#bbb09a" },
{ id: "Medicine", label: "Medicine", color: "#7cd9a0" },
];

View file

@ -0,0 +1,84 @@
---
id: p1-50
title: "Tech & Culture domain field — propagate categorization through Rust, Godot UI, and player analysis"
priority: p1
status: in_progress
scope: game1
owner: unassigned
updated_at: 2026-05-03
plan: ~/.claude/plans/tech-culture-domain-propagation.md
evidence: []
---
## Summary
The designs app at `.project/designs/app/` now drives `/tech-tree` and
`/culture-tree` from real JSON data with a 10-domain categorization
(Military / Economy / Industry / Agriculture / Governance / Culture /
Science / Exploration / Engineering / Medicine), authored as
`tech.domain` directly in `public/resources/techs/*.json`. Culture
policies use the `pillar` field as their tab axis.
This objective propagates that categorization through the rest of the
system — the Rust simulator, the Godot gameplay UI (`knowledge_tree`,
`tech_tree`, `culture_tree`), and player data analysis surfaces — so
every consumer reads the same Single Source of Truth.
The shared TreeView component (`.project/designs/app/src/components/tree/`)
is the reference implementation. Other surfaces should match its UX:
domain tab bar, matching-tab-floats-to-top sort, inline unlocks on each
node.
## Acceptance
- [ ] **Rust**`mc-tech::Tech` carries `domain: String` deserialised
from JSON. Schema test in `mc-data` asserts every authored tech has
a non-empty domain belonging to the 10-value enum. `cargo test -p
mc-tech -p mc-culture -p mc-data` passes.
- [ ] **GDExtension**`GdTechWeb` exposes `domains() ->
PackedStringArray` and `techs_by_domain(domain) -> Array<Dictionary>`.
`tech_data_json(id)` includes `domain` in returned dict. Build clean:
`bash src/simulator/build-gdext.sh`.
- [ ] **GDScript**`tech_web.gd` exposes `get_domains()` and
`get_nodes_by_domain(domain)`. `knowledge_tree.gd` renders a domain
tab bar above the existing pillar columns. Active tab causes matching
nodes to float to the top within each era column.
- [ ] **Gameplay proof**`tech_tree_proof.tscn` captures a screenshot
showing the domain tab bar, with one tab (e.g. "Science") active and
the 21 Science techs floated to the top of their era columns.
`culture_tree_proof.tscn` updated similarly with pillar tabs (the
culture-tree's natural tab axis).
- [ ] **Designs app polish**`/tech-tree` and `/culture-tree` already
ship the desired UX; verify they still typecheck and the homepage
index shows both routes under sensible categories.
- [ ] **Player analysis** — at least one of `/stats`, `/replay`, or the
in-game stats screen renders a domain-binned breakdown of a player's
research history. Driven by deterministic replay fixture data.
- [ ] **Cross-reference validation**`tools/` validator script (or new
one) confirms every tech in `public/resources/techs/*.json` has a
`domain` field set to one of the 10 canonical values.
- [ ] **Phase-gate proof screenshot** — captured per
`phase-gate-protocol.md`. Archived under `.project/screenshots/`.
## Notes
- This objective is additive — `pillar` field remains the canonical
data-org axis used by simulator code; `domain` is a metadata layer for
UI/analysis grouping. No save-format change.
- The 10 domains are: Military, Economy, Industry, Agriculture,
Governance, Culture, Science, Exploration, Engineering, Medicine. "All"
is a UI sentinel only, never authored on a tech.
- Culture policies use `pillar` directly as their tab axis (6 trees).
No `domain` field on culture data is required for this objective.
- Work parallelizable across simulator-infra, godot-ui, godot-engine,
game-systems specialists. Coordinate via experts-loop / experts-team.

View file

@ -157,7 +157,7 @@
"name": "Meteorology",
"description": "Barometers, hygrometers, and systematic weather records. Pressure becomes a measurable quantity for the first time — wind patterns your scouts always noticed now gain scientific precision and predictive power.",
"pillar": "natural_philosophy",
"domain": "Exploration",
"domain": "Science",
"era": 4,
"tier": 4,
"cost": 95,
@ -191,7 +191,7 @@
"name": "Geology",
"description": "Study of rock strata, volcanic processes, and mineral formation over geological time. Aerosol signatures from volcanic eruptions can now be tracked and used to predict seismic hazard zones.",
"pillar": "natural_philosophy",
"domain": "Exploration",
"domain": "Science",
"era": 4,
"tier": 4,
"cost": 90,
@ -268,7 +268,7 @@
"name": "Astronomy",
"description": "Celestial mechanics and long-range atmospheric forecasting. Enables a 3-turn weather prediction overlay by synthesizing the pressure trends and wind corridors in your observation history. Accuracy scales with history depth.",
"pillar": "natural_philosophy",
"domain": "Exploration",
"domain": "Science",
"era": 5,
"tier": 5,
"cost": 125,
@ -305,7 +305,7 @@
"name": "Natural Philosophy",
"description": "Unified empirical theory of matter, energy, and natural forces. The complete atmospheric composition — oxygen, carbon dioxide, methane — becomes legible. Climate change is no longer a surprise; it is a calculation.",
"pillar": "natural_philosophy",
"domain": "Culture",
"domain": "Science",
"era": 5,
"tier": 5,
"cost": 135,
@ -343,7 +343,7 @@
"name": "Hydrology",
"description": "The complete science of water movement — aquifers, groundwater tables, flood plains, and tidal forcing. Dwarven tunnelers can now map underground water sources before they breach them, preventing catastrophic flooding in deep holds.",
"pillar": "natural_philosophy",
"domain": "Exploration",
"domain": "Science",
"era": 6,
"tier": 6,
"cost": 160,
@ -385,7 +385,7 @@
"name": "Geophysics",
"description": "The physics of tectonic plates, seismic waves, and volcanic systems. Provides advance warning of earthquakes, eruptions, and unstable ground — invaluable for civilizations whose cities are built into the rock itself.",
"pillar": "natural_philosophy",
"domain": "Exploration",
"domain": "Science",
"era": 7,
"tier": 7,
"cost": 195,
@ -424,7 +424,7 @@
"name": "Oceanography",
"description": "Deep-ocean current mapping, thermocline science, and tidal modeling. Reveals the ocean as a heat-distribution engine that drives global climate — and unlocks the fastest deep-water trade routes.",
"pillar": "natural_philosophy",
"domain": "Exploration",
"domain": "Science",
"era": 8,
"tier": 8,
"cost": 230,
@ -463,7 +463,7 @@
"name": "Climatology",
"description": "Long-cycle climate modeling integrating ocean, atmosphere, and geological drivers. Extends weather prediction to 5 turns and reveals multi-decade climate trends — ice ages, monsoon regime shifts, and the slow creep of desertification.",
"pillar": "natural_philosophy",
"domain": "Exploration",
"domain": "Science",
"era": 9,
"tier": 9,
"cost": 275,
@ -513,7 +513,7 @@
"name": "World Theory",
"description": "The synthesis of all natural philosophy into a unified planetary model — geology, hydrology, atmosphere, ocean, biosphere, and magic as one interacting system. Reveals the health of the entire living world in a single lens.",
"pillar": "natural_philosophy",
"domain": "Exploration",
"domain": "Science",
"era": 10,
"tier": 10,
"cost": 330,
@ -560,7 +560,7 @@
"name": "Empiricism",
"description": "Systematic observation supplanting saga-only knowledge. The world is measured, recorded, and questioned — not merely remembered.",
"pillar": "natural_philosophy",
"domain": "Culture",
"domain": "Science",
"era": 2,
"tier": 2,
"cost": 30,

View file

@ -4,7 +4,7 @@
"name": "Scholarship",
"description": "Systematic recording and study of knowledge on stone tablets and vellum. The foundation of all scientific pursuit.",
"pillar": "scholarship",
"domain": "Culture",
"domain": "Science",
"era": 1,
"tier": 1,
"cost": 20,
@ -66,7 +66,7 @@
"name": "Mathematics",
"description": "Geometry, ballistics, and precise calculation. Essential for siege engines, architecture, and astronomical observation.",
"pillar": "scholarship",
"domain": "Culture",
"domain": "Science",
"era": 2,
"tier": 2,
"cost": 40,
@ -95,7 +95,7 @@
"name": "Advanced Scholarship",
"description": "Deep study of natural philosophy, optics, and geological science. Enables universities and observatories.",
"pillar": "scholarship",
"domain": "Culture",
"domain": "Science",
"era": 3,
"tier": 3,
"cost": 70,
@ -132,7 +132,7 @@
"name": "High Lore",
"description": "The accumulated wisdom of generations, codified into grand treatises. Dramatically accelerates all future research.",
"pillar": "scholarship",
"domain": "Culture",
"domain": "Science",
"era": 5,
"tier": 5,
"cost": 130,
@ -167,7 +167,7 @@
"name": "Stellar Mapping",
"description": "Systematic observation and charting of celestial bodies — their positions, cycles, and relationships to the seasons and geological shifts. The foundation of calendars, navigation, and the first understanding that the world above mirrors the world below.",
"pillar": "scholarship",
"domain": "Exploration",
"domain": "Science",
"era": 5,
"tier": 5,
"cost": 125,
@ -206,7 +206,7 @@
"name": "Rune Archives",
"description": "Formalized rune-script for permanent record. Vault-libraries cut into living stone, where every saga, ledger, and grudge is preserved against fire, flood, and forgetting.",
"pillar": "scholarship",
"domain": "Culture",
"domain": "Science",
"era": 4,
"tier": 4,
"cost": 90,
@ -237,7 +237,7 @@
"name": "Clockwork Calculation",
"description": "Mechanical computation engines — geared, brass-plated, housed in their own halls. Compute trajectories, tax balances, and probability with precision that shames any savant.",
"pillar": "scholarship",
"domain": "Engineering",
"domain": "Science",
"era": 6,
"tier": 6,
"cost": 175,
@ -265,7 +265,7 @@
"name": "Optical Arts",
"description": "Precision lenses, telescopic instruments, observatory works carved into the highest peaks. The dwarven gaze extends past the edge of the world for the first time.",
"pillar": "scholarship",
"domain": "Culture",
"domain": "Science",
"era": 7,
"tier": 7,
"cost": 230,
@ -296,7 +296,7 @@
"name": "Cryptarchives",
"description": "Encoded knowledge vaults, war-intelligence locked behind clan-rune ciphers. Knowledge as weapon — denied to the enemy, leveraged by the keeper.",
"pillar": "scholarship",
"domain": "Culture",
"domain": "Science",
"era": 8,
"tier": 8,
"cost": 290,
@ -324,7 +324,7 @@
"name": "Rune Simulation",
"description": "Predictive models inscribed in vast rune-arrays — entire battles, harvests, market-cycles run forward in stone before they occur in flesh. Foreknowledge as engineering discipline.",
"pillar": "scholarship",
"domain": "Culture",
"domain": "Science",
"era": 9,
"tier": 9,
"cost": 360,
@ -352,7 +352,7 @@
"name": "Omnilexica",
"description": "Total knowledge synthesis: the Library Ascendant, where every saga, ledger, blueprint, and grudge of every clan since the First Delving is held in a single accessible record.",
"pillar": "scholarship",
"domain": "Culture",
"domain": "Science",
"era": 10,
"tier": 10,
"cost": 440,

View file

@ -1223,6 +1223,19 @@ func _pick_research(player: RefCounted) -> void:
"ecology": 1.0 + (exp + prod) / 2.0 * 0.5,
}
# p1-29 cycle 3: catch-up dynamics. Compare this player's tech_era ceiling
# against the highest opponent's; when behind by ≥2 eras, boost military +
# metallurgy pillars 1.5× and waive the tier-3 mercantile penalty so the
# losing player prioritises closing the tech gap rather than minting more
# era-1 economy techs (the failure mode in the lever-3+4 batch where p1
# stayed at tier_peak=1 forever, never developing).
var self_tier_peak: int = _player_tier_peak(player)
var opp_tier_peak: int = _max_opponent_tier_peak(player)
var is_behind: bool = (opp_tier_peak >= self_tier_peak + 2)
if is_behind:
pillar_mult["military"] = float(pillar_mult["military"]) * 1.5
pillar_mult["metallurgy"] = float(pillar_mult["metallurgy"]) * 1.5
# Pass 1: compute raw score for every tech (ignoring availability).
var raw_score: Dictionary = {}
for tech: Dictionary in all_techs:
@ -1237,7 +1250,10 @@ func _pick_research(player: RefCounted) -> void:
# Tier-3+ penalty for mercantile clans (low aggression AND low production).
# Guards goldvein/runesmith from racing to tier_peak=6 identically to
# ironhold/blackhammer. agg < 0.5 catches raw ≤5; prod < 0.5 catches raw ≤5.
if int(tech.get("tier", 0)) >= 3 and agg < 0.5 and prod < 0.5:
# p1-29 cycle 3: waive the penalty when this player is behind on tech —
# a losing mercantile player needs to catch up rather than be further
# slowed by their personality bias.
if int(tech.get("tier", 0)) >= 3 and agg < 0.5 and prod < 0.5 and not is_behind:
var trade_factor: float = (wlth + trd) / 2.0 # mercantile bias, [0, 1]
sc *= maxf(0.4, 1.0 - trade_factor * 0.6) # up to 60% penalty for full mercantile clans
for uid: Variant in tech.get("unlocks", {}).get("units", []):
@ -1291,6 +1307,46 @@ static func _norm_axis(axes: Dictionary, key: String) -> float:
return (clampf(raw, 1.0, 10.0) - 1.0) / 9.0
static func _player_tier_peak(player: RefCounted) -> int:
## Compute the highest tech-era this player has researched. Mirrors the
## per-player tier_peak metric written into turn_stats.jsonl. Used by
## _pick_research's catch-up branch (p1-29 cycle 3) to detect when this
## player is falling behind on the tech ladder.
if player == null:
return 0
var techs: Array = player.researched_techs if player.get("researched_techs") != null else []
var peak: int = 0
for tid: Variant in techs:
var data: Dictionary = DataLoader.get_tech(str(tid))
var era: int = int(data.get("era", 0))
if era > peak:
peak = era
return peak
static func _max_opponent_tier_peak(self_player: RefCounted) -> int:
## Compute the highest opponent's tier_peak across all OTHER players in
## GameState.players. Used by _pick_research's catch-up branch.
## Returns 0 when self is the only player or no opponents have researched
## anything yet (degenerate first-turn case).
var max_peak: int = 0
if GameState == null or GameState.players == null:
return 0
var self_idx: int = -1
if self_player != null and self_player.get("index") != null:
self_idx = int(self_player.index)
for p: Variant in GameState.players:
if p == null:
continue
var pi: int = int(p.index) if p.get("index") != null else -1
if pi == self_idx:
continue
var peak: int = _player_tier_peak(p)
if peak > max_peak:
max_peak = peak
return max_peak
func _score_site(pos: Vector2i, game_map: RefCounted) -> float:
## Score a hex as a city site. Food*2 + production*1.5 + resources.
var tile: Resource = game_map.get_tile(pos)