31 KiB
| id | title | priority | status | scope | updated_at | evidence | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| p1-43 | Building stacking — per-category upgrade chains (military / science / culture / production / etc.) | p1 | done | game1 | 2026-05-14 |
|
2026-05-03 verification
Data layer substantially shipped; engine consumption still pending.
- 178/181 buildings under
public/resources/buildings/carry thestack_modefield (parallel/amplify/single) per the 2026-04-30 hybrid decision. Spot-checks:barracks.json,infantry.json,library.json,scriptorium.json,harbor.json,deep_harbor.json,academy_of_sciences.json,alloy_furnace.json,grand_armory.json,forge_chant_hall.json,hospital.json. - 38 buildings declare
requires_existing: <id>ladder pointers (e.g.infantry.json::requires_existing="barracks",scriptorium.json::requires_existing="library",hospital.json::requires_existing="clinic",deep_harbor.json::requires_existing="harbor",alloy_furnace.json::requires_existing="mithril_forge"). - 71 buildings declare
produces: [unit_id, ...]rosters (e.g.barracks.produces,library.produces,harbor.produces,infantry.produces=[pikeman, defender, shield_bearer, plated_warrior, pike_guard],scriptorium.produces=[dwarf_deep_scout, dwarf_grand_scout, dwarf_engineer]). - New ladder-fill buildings authored:
infantry.json,scriptorium.json,iron_forge.json,barber.json,clinic.json,hospital.json— all underpublic/resources/buildings/.
Engine remains unwired:
grep "requires_existing\|consumes_existing"acrosssrc/simulator/crates/andsrc/game/engine/src/returns zero matches.mc-city::can_build,mc-city::production, and the GDScript dispatch path do not honour the prerequisite gate or consume-on-upgrade semantics.- No validator support in
tools/for cross-referencingrequires_existingids. - AI catalog scoring unchanged; UI does not surface "Can be upgraded to: X".
Promoted missing → partial.
Summary
User direction (2026-04-29): "all the buildings should be buildable and some buildings can be built on top of each other (double barracks - infantry) ... what about comboing other buildings ... science stack, culture stack".
Today every building is binary: a city either has it or doesn't. The mechanic the user wants: queueing a building on top of an existing one upgrades the slot in place — barracks + another barracks build = infantry (a stronger military producer). The same primitive applies to every category: science stacks (library → scriptorium → academy), culture stacks (monument → bardic_circle → great_hall), production stacks (forge → iron_forge → grand_forge), etc. This is distinct from the BUILDINGS.md "Hybrid Merged Structures" mechanic (which combines TWO different buildings + Synthesis tech into a hybrid). Stacking is the simpler primitive: same-category Lv1 → Lv2 → Lv3 chains within one slot.
The existing data already implies category-tier chains via the tier + category fields:
| Category | Lv1 (no tech) | Lv2 (mid tech) | Lv3+ (late tech) |
|---|---|---|---|
| Production | forge t1 |
(gap — iron_forge doesn't exist) |
dwarf_deep_forge t3, tempering_forge t6, steam_forge t7, adamantine_foundry t10 |
| Science | library t1 |
university t3, observatory t3 |
academy_of_sciences t5, climate_institute t9 |
| Culture | monument t1 |
great_hall t3, gathering_hall t2 |
ancestor_hall t10 |
| Military | barracks t1 |
(gap — infantry doesn't exist) |
armory t3, military_academy t6, command_citadel t10 |
| Food | granary t1 |
mill t2, brewery t2, watermill t2 |
great_granary t2 (wonder) |
| Defense | walls t1 |
watchtower t1 |
castle t3 |
| Wealth | marketplace t2, market t2 (DUPLICATE) |
guild_hall t4 |
(none) |
| Religion | temple t2 |
temple_of_the_ancestor t5 (wonder) |
(none) |
The stacking schema makes these chains explicit and queryable. Where a Lv2 successor doesn't exist yet (e.g. infantry, iron_forge, scriptorium), this objective authors the missing intermediates.
Three design questions need user sign-off before authoring:
- Successor identity: is
infantrya NEW building (needs authoring) or an existing one (e.g. reusearmoryas the "barracks Lv2" slot)? - Mechanic shape:
- (a) Replacement: building barracks twice consumes both, slot becomes
infantry. Original gone. - (b) Levelled: building stays "barracks" but carries a
level: 2field with stacked effects. - (c) Per-tile: two barracks on same tile merge (only relevant if
placement_tile_required: true).
- (a) Replacement: building barracks twice consumes both, slot becomes
- Schema: declare on the lower tier (
barracks.json::stacks_into: "infantry") or on the upper (infantry.json::requires_existing: "barracks"+consumes_existing: true)? The latter keeps the relationship bidirectional readable.
Recommendation: option (a) Replacement with declaration on the upper tier (requires_existing + consumes_existing). Matches civ-style upgrade slots, reads naturally in the city UI ("Upgrade Barracks → Infantry"), avoids per-tile placement complexity for a v1.
Acceptance
- ✓ Design pass + sign-off from user on the three questions above. (Q1–Q6 locked 2026-05-05; decisions recorded in §"p1-43a close-out / Q1–Q6 decisions" below.)
- ✓
building.schema.jsonextends with:requires_existing: <id|null>,consumes_existing: <bool>. Both default null/false. Validator confirmsrequires_existingresolves to a real building id. (public/games/age-of-dwarves/data/schemas/building.schema.json:42-45;tools/validate-game-data.py::validate_building_requires_existingcross-refs 185+ ids.) - ✓ For each declared stack-pair: the upper-tier building is NEW data (or repurposed existing) authored under
resources/buildings/<id>.jsonwith the new fields populated. (cycle 27 / 2026-05-05 viap1-43b: 14 new high-tier buildings closing the food/wealth/culture/production/medical short chains; manifest at 199 entries; allrequires_existingcross-refs validate.) - ✓ Initial 3-step ladders (civilian + economic + 14 military producer chains):
produces:arrays filled across 159 producer buildings; 9 documented asproduces_yield_only: true; 0 unaccounted. Closed viap1-43c(done 2026-05-14). User clarifications 2026-04-29: no religion, medical chain replaces it, military decomposes into many weapon-class chains (not one).
Civilian + economic chains (one per category)
- Science:
library→scriptorium(NEW) →university→ produces sages / cartographers / engineers - Culture:
monument→gathering_hall→great_hall→ produces bards / loremasters - Production:
forge→iron_forge(NEW) →dwarf_deep_forge→ produces smiths / engineers - Defense:
walls→watchtower→castle→ produces garrison/sentry units - Food:
granary→mill→watermill→ yield-only, no unit - Wealth:
marketplace→market(reconcile duplicate) →guild_hall→ produces merchants - Medical (NEW):
barber(NEW) →clinic(NEW) →hospital(NEW) → produces battle medics / field surgeons
Military — fourteen parallel weapon-class chains
Military is NOT one ladder. Each weapon class is its own stack with its own producer building(s). The existing data already supplies most of the building IDs — they just need the produces: list and explicit stack relationships. Counts in parens are the units that exist today in resources/units/ for that class:
| Weapon class | Producer chain | Notable units |
|---|---|---|
| Melee infantry (12 units) | barracks → drill_yard (existing t2) → armory (existing t3) |
warrior → pikeman → defender → ironwarden → hammerguard → mithril_vanguard → mountain_king |
| Heavy melee (4 units) | sword_hall (existing t2) → dwarf_deep_forge |
berserker → hearth_raider → goretooth → war_ram |
| Bow / Crossbow (3+4 dwarf) | bolt_range (existing t1) → ? (Lv2 needed) |
quarrelman → archer → bolt_thrower_crew |
| Marksman / Sniper (3 units) | marksman_lodge (existing t6) → ? |
marksman → anti_tank_rifleman → deep_eye |
| Riflery (7 units) | rifle_range (existing t6) → gun_works (existing t7) → coil_foundry (existing t9) |
rifleman → light_field_gun → machine_gunner → storm_trooper → steam_howitzer |
| Explosive / Grenade (4 units) | powder_works (existing t5) → assault_school (existing t8) |
cannon_crew → powder_sapper → trench_raider → bombard |
| Cavalry / Mounted (4 units) | stable (existing t1) → boar_pen (existing t2) |
boar_scout → ram_rider → cavalry → tusker_knight |
| Siege (5 units) | siege_workshop (existing t2) → siege_works (existing t8) |
ballista_crew → catapult_crew → trebuchet_crew |
| Mechanized / Walker (8 units) | walker_yard (existing t7) → tank_yard (existing t9) → armour_yard (existing t8) |
steam_walker → iron_strider → riveted_trooper → adamantine_tank |
| Artillery (6 units) | rocket_pad (existing t9) |
motorized_artillery → rocket_battery → rail_cannon → apex_artillery |
| Stealth / Special (3 units) | shadow_school (existing t9) |
commando → soulbolt → doomsoul |
| Magical / Runic (6 units) | runesmith_hall (existing t6) |
runesmith → rune_spear → emp_trooper → stormbolt_trooper |
| Air (6 units) | airfield → zeppelin_dock (existing t8) |
gyrocopter → iron_hawk → war_zeppelin → mithril_hawk → sky_fortress |
| Naval (13 units) | harbor → deep_harbor (existing t5) → naval_fortress (existing t8) |
river_galley → war_galley → dreadnought → fortress_ship |
Implications
- ~14 producer building chains for military alone, plus 7 civilian/economic chains. Total ~21 producer slots.
- Most chains already have building IDs; only a handful of NEW buildings needed to fill ladder gaps (
infantryLv2 melee,iron_forgeLv2 production,scriptoriumLv2 science,barber/clinic/hospitalfor medical). - Each producer building gets a
produces: [unit_id, ...]field declaring its unit roster. Stacking the building expands the roster to higher-tier units. - The dwarf-prefixed units (
dwarf_warrior,dwarf_river_galley, etc., tier=null) are race-flavored variants — should reconcile with the generic-tier roster as a separate audit. - ✓ Engine (Rust SSoT, cycle 5 / 2026-05-04):
City::can_build(&BuildingDef) -> Result<(), StackingError>returnsErr(RequiresExistingMissing { .. })whendef.requires_existingis set and the predecessor is absent (src/simulator/crates/mc-city/src/city.rs:404-413+stacking::check_requires_existingsrc/simulator/crates/mc-city/src/stacking.rs:65-80).City::finalize_build(&BuildingDef) -> Option<BuildingId>removes the predecessor fromcity.buildingswhenconsumes_existing: true(src/simulator/crates/mc-city/src/city.rs:419-428+stacking::apply_consumes_existingstacking.rs:104-119). NewStackMode { Parallel, Amplify, Single }enum atsrc/simulator/crates/mc-core/src/ids.rs:62-92; newrequires_existing: Option<BuildingId>,consumes_existing: bool,stack_mode: StackModefields onBuildingDefatsrc/simulator/crates/mc-city/src/building.rs:240-262. GDScript dispatch path (ai_turn_bridge_dispatch.gd, encyclopedia, build menu) is intentionally untouched in this cycle — it remains a thin pass-through; bridge wiring of the typed errors and "Can be upgraded to: X" UI is the remaining UI bullet. - ✓ Rust unit tests cover the three required cases:
test_requires_existing_blocks_build(src/simulator/crates/mc-city/src/city.rs:1188-1202),test_consumes_existing_removes_predecessor(city.rs:1204-1219),test_stack_mode_single_blocks_second_instance(city.rs:1221-1239). Plus 10 module-level tests insrc/simulator/crates/mc-city/src/stacking.rs::tests. The GDScript GUT test that calls throughGdCity::can_build/GdCity::finalize_build(the bridge wiring) remains a follow-up under the bridge bullet — Rust source-of-truth coverage is in place. - ✓
python3 tools/validate-game-data.pyextended to validaterequires_existingcross-refs. (tools/validate-game-data.py::validate_building_requires_existing; self-test fixture raises hard error on broken refs; full run zero failures across 185 ids.)
Stack-rate model — DECIDED 2026-04-30
User direction: hybrid by category, not single-mode.
stack_mode: "amplify"— single producer queue, rate scales with stack depth. Used for research / culture / production / infrastructure / wealth / medical / food / diplomacy / wonder. Rationale: these categories produce one logical output stream (research, culture points, smithing throughput, healing). Rate amplification feels right — Lv1 makes 1 sage, Lv2 makes a sage twice as fast.stack_mode: "parallel"— multiple parallel queues, one per stack tier. Used for military / naval / defense. Rationale: combat-unit producers benefit from training mixed armies in parallel — barracks (warriors) + infantry (pikemen) + armory (heavy infantry) running three queues simultaneously is what makes military stacks meaningfully different from non-military.
Schema field stack_mode: "amplify" | "parallel" already populated on every building (163 entries) by category-derived rule:
| Category | Mode |
|---|---|
| military, naval, defense | parallel |
| research, culture, production, infrastructure, wealth, food, diplomacy, wonder | amplify |
Wonders override to amplify regardless of category.
Open questions for user
- Successor identity per category: confirm the 8 ladders above (or revise). For each, pick existing building or sign off on authoring a new Lv2 intermediate.
- Cost: do stack-upgrades cost the FULL upper-tier
cost, or a discounted "upgrade cost" (e.g.cost - prerequisite_cost)? - Effects inheritance: when the prerequisite is consumed, do its effects vanish entirely, or does the upper-tier inherit them additively (so an
armorycity has barracks+infantry+armory effects compounded)? - Multiple categories per city: can a city have ONE stack ladder per category (8 ladders × 3 deep = 24 total slots) or share slots across categories?
- Reconcile duplicates surfaced by the audit:
marketplacevsmarket,monumentvsbardic_circle— fold one into the other or keep both? p1-59relationship: is the simpler "double X → Y" stacking enough for EA, with merged-structures (X+Y → hybrid) deferred? Or merge p1-59 into this objective?
Out of scope
- The full Hybrid Merged Structures mechanic (different building combinations + Synthesis tech) — that's
p1-59. - Per-tile placement / co-location math from BUILDINGS.md "Building Synergy" tier 5+ — separate post-EA feature.
- Master/Grandmaster aura system.
- AI catalog scoring of stack upgrades — tracked by
p1-42-ai-full-building-catalog.md(mc-ai evaluator walkingrequires_existingchains + combined costs). Re-homed from this objective 2026-05-14. - City UI upgrade surface ("Can be upgraded to: X") — requires the
GdBuildingRegistry::get_upgrade_targetapi-gdext bridge that does not yet exist. Filed asp1-43c-gdext-upgrade-target.md(stub). godot-ui specialist correctly stopped 2026-05-13 rather than degrade to a GDScript inverse scan (Rail-3 violation). - GUT bridge test
test_building_stacking.gd— downstream of the bridge above; will land alongsidep1-43c-gdext-upgrade-target.md(which carries its own bridge testtest_building_upgrade_target_bridge.gd).
Remaining work (2026-05-03)
Bullet: Design pass + sign-off from user on the three questions above
- Files to touch: this objective doc — record decisions inline before authoring further.
- Dependencies: blocks every engine bullet below.
- Acceptance gate: user comment in the objective answering Q1 (successor identity), Q2 (mechanic shape), Q3 (schema declaration site). Recommendation already on record: option (a) Replacement + upper-tier
requires_existing+consumes_existing. - SOLID/DRY/SSoT rails: keep open-questions section trimmed once decided; do not duplicate rationale across files.
Bullet: building.schema.json extends with requires_existing/consumes_existing (validator-enforced)
- Files to touch:
- Schema:
public/games/age-of-dwarves/data/schemas/building.schema.json— add the two fields withdefault: null/false. - Validator:
tools/validate-game-data.py— cross-refrequires_existingagainst authored building IDs inpublic/resources/buildings/.
- Schema:
- Dependencies: design sign-off above.
- Acceptance gate:
python3 tools/validate-game-data.pyreports zero warnings on current 178 buildings; intentionally-broken fixture (requires_existing: "nonexistent") raises a hard error. - SOLID/DRY/SSoT rails: schema lives ONLY in
data/schemas/; no duplicated field-list in Rust or GDScript. Extend in place — no v2 schema.
Bullet: For each declared stack-pair, upper-tier building authored as JSON with new fields populated
- Files to touch:
public/resources/buildings/infantry.json,scriptorium.json,iron_forge.json,barber.json,clinic.json,hospital.json— verifyrequires_existing+consumes_existingpopulated post-schema-extension.- Existing late-tier buildings (academy_of_sciences, dwarf_deep_forge, etc.) — add
requires_existingpointing at their Lv2.
- Dependencies: schema extension above.
- Acceptance gate: every chain in the table has each tier carrying
requires_existingto its predecessor; validator green. - SOLID/DRY/SSoT rails: data only in
public/resources/buildings/; nodata/buildings/overrides (per p1-40).
Bullet: Initial 3-step ladders (civilian + economic + 14 military)
- Files to touch:
- Civilian/economic chains: per the table — author/edit
library.json,scriptorium.json,university.json,monument.json,gathering_hall.json,great_hall.json,forge.json,iron_forge.json,dwarf_deep_forge.json,walls.json,watchtower.json,castle.json,granary.json,mill.json,watermill.json,marketplace.json,market.json,guild_hall.json,barber.json,clinic.json,hospital.json. - Military: 14 weapon-class chains as listed — populate
produces: [unit_id, ...]on each producer. - Reconcile
marketplacevsmarket(Q5 in open questions).
- Civilian/economic chains: per the table — author/edit
- Dependencies: design sign-off (Q1 + Q5).
- Acceptance gate:
tools/validate-game-data.pyconfirms everyproducesentry resolves to an authored unit ID; spot-checkinfantry.producesincludespikeman,defender,shield_bearer,plated_warrior,pike_guard. - SOLID/DRY/SSoT rails: rosters live on producer JSON only; no duplicated lists in Rust or GDScript.
Bullet: Engine — city.can_build(bid) honours requires_existing; dispatch consumes prerequisite on consumes_existing — CLOSED 2026-05-04 (cycle 5)
- ✓ Rust:
City::can_build(&BuildingDef)atsrc/simulator/crates/mc-city/src/city.rs:404-413;City::finalize_build(&BuildingDef)atcity.rs:419-428. Backed by pure functions in new modulesrc/simulator/crates/mc-city/src/stacking.rs(check_requires_existing,check_stack_mode,check_can_build,apply_consumes_existing). - ✓ Typed enum:
mc-core::StackMode { Parallel, Amplify, Single }atsrc/simulator/crates/mc-core/src/ids.rs:62-92— closes the "no stringly-typed" rail. - ✓ Typed errors:
mc-city::StackingError::{ RequiresExistingMissing, SingleInstanceAlreadyBuilt }atstacking.rs:18-39— surfaced verbatim to GDScript by the bridge (no parallel rule check on the GDScript side). - ✓ Tests: required-by-name unit tests at
src/simulator/crates/mc-city/src/city.rs:1188-1239; 10 module-level tests atsrc/simulator/crates/mc-city/src/stacking.rs::tests.cargo test -p mc-core -p mc-city -p mc-turnandcargo check --workspacegreen. - Outstanding (under separate bullets above): the GDScript dispatch (
ai_turn_bridge_dispatch.gd::dispatch_set_production) and theGdCitybridge methods that translateStackingError→EventBus.building_requires_existing_unmetare NOT wired in this cycle — Rust SSoT is in place; the bridge wrapper is a follow-up. Thestack_mode: amplifyrate-scalar runtime hook (yields × stack-depth) is NOT implemented;Amplifycurrently only relaxes the single-instance gate. Tracked under the AI catalog scoring + UI surface bullets.
Bullet: AI integration — catalog scoring treats stack upgrades as multi-step path
- Files to touch:
- Rust:
src/simulator/crates/mc-ai/src/evaluator.rs— extend building scorer to walkrequires_existingchains and combine costs. - Depends on
p1-42AI catalog scoring landing first.
- Rust:
- Dependencies:
p1-42. - Acceptance gate:
cargo test -p mc-ai test_stack_upgrade_combined_costgreen; scoringinfantryfrom a city withoutbarracksreturns barracks_cost + infantry_cost. - SOLID/DRY/SSoT rails: scorer in
mc-ai; no GDScript heuristic shadow.
Bullet: City UI surfaces stack relationships ("Can be upgraded to: X")
- Files to touch:
- GDScript:
src/game/engine/src/scenes/city/encyclopedia_building_panel.gd(or equivalent),src/game/engine/src/scenes/city/build_menu.gd. - Bridge:
GdBuildingRegistry::get_upgrade_target(building_id) -> Stringinsrc/simulator/api-gdext/src/lib.rs.
- GDScript:
- Dependencies: data bullet above.
- Acceptance gate: in-game encyclopedia for
barracksshows "Can be upgraded to: Infantry"; reverse lookup confirmed. - SOLID/DRY/SSoT rails: lookup in Rust registry; GDScript only renders.
Bullet: GUT test — barracks-built city can queue infantry; barracks-less cannot; building infantry removes barracks
- Files to touch:
src/game/engine/src/tests/test_building_stacking.gd(NEW). - Dependencies: engine bullet above.
- Acceptance gate:
godot --headless --test test_building_stacking.gdgreen. - SOLID/DRY/SSoT rails: assertions go through GdCity bridge, not direct GDScript shadow logic.
Bullet: tools/validate-game-data.py validates requires_existing cross-refs
- Files to touch:
tools/validate-game-data.py. - Dependencies: schema bullet above.
- Acceptance gate:
python3 tools/validate-game-data.pygreen; broken fixture errors hard. - SOLID/DRY/SSoT rails: validator reads schema + manifest only; no hardcoded ID list.
Bullet (sub-task, schema reconciliation): stack_mode: "single" value — RESOLVED 2026-05-04 by widening the enum
- ✓ Decision: option (a) — widen the enum to
parallel | amplify | single. Eight authored buildings carrysingle(not the two cited in the prior audit):council_of_runesmiths,deep_atelier,echoing_conduit,grand_clanmoot,saga_chronicle,stonelore_academy,throne_of_ages,vault_of_seals— allwonder_type: "national".singleis a meaningful per-city semantic distinct from amplify/parallel: it strictly rejects a second instance viaStackingError::SingleInstanceAlreadyBuilt. Re-tagging would have lost information. - ✓ Rust enum:
StackMode { Parallel, Amplify, Single }atsrc/simulator/crates/mc-core/src/ids.rs:62-92, defaultSingle(matches pre-p1-43"binary" semantics for buildings that omit the field). - ✓ JSON schema:
public/games/age-of-dwarves/data/schemas/building.schema.json:42-44—stack_modeenum widened to["parallel", "amplify", "single"];requires_existing(string|null),consumes_existing(bool),produces(string[]) declared explicitly. - ✓ Documentation:
public/games/age-of-dwarves/docs/BUILDING_SCHEMA.md§Stacking & Upgrade Chains (p1-43) — full table of the three modes + engine wiring + worked examples. - ✓ Verification: all 184 building JSONs deserialize via
mc-city::BuildingDef(testtest_all_authored_buildings_deserialize,cargo test -p mc-citypasses).
Bullets remaining: 6 (data ladder authoring, AI catalog scoring, GDScript bridge wiring + UI surface, GUT bridge test, validator cross-ref). Engine + schema reconciliation closed.
p1-43a close-out (2026-05-05)
All six locked design questions implemented; the engine + schema layer of the
stacking objective is now complete. Bulk authoring of the high-tier
buildings deferred to the follow-up p1-43b — closed 2026-05-05 (cycle
27): 14 buildings across the 5 short chains (food, wealth, culture,
production, medical) shipped under public/resources/buildings/, manifest
extended 185 → 199 entries, mid-chain insertions (clan_atelier T5,
industrial_smelter T8) handled with re-pointed requires_existing and
re-summed effects to preserve the Q3 additive invariant.
Q1–Q6 decisions (locked)
- Q1 — Chain coverage. Drafted 8-category table kept. Short chains (food, wealth, culture, production, medical) extended via NEW high-tier buildings. Science + defense already had 6–8 tiers, no new authoring there.
- Q2 — Cost. Both paths allowed. Direct build pays full
cost. Upgrade from prerequisite payscost − prereq.cost + upgrade_fee. Newupgrade_feefield on the building schema; defaultnull= 15% ofcostrounded. - Q3 — Effects inheritance (additive at author-time). Each tier's
effectsarray is authored as the SUM of all ancestor effects + the marginal contribution. The simulator does NOT re-sum predecessors at runtime (that would double-count parallel ladders). - Q4 — No slot pool. Constraint is natural economy: tiles, population,
production capacity, treasury. No
slot_capacityschema field. - Q5 — Fold
marketintomarketplace.market.jsondeleted; its distinguishing content (tech_required: trade_routes,produces: caravan_master,gpp_trade +1,tradeswrightspecialist slot) merged intomarketplace.json. All five external"market"references rewritten:guild_hall.requires_existing,vault_of_seals.requires_buildings_all_cities,specialists/specialists.json::merchant.employed_in,techs/agriculture.json::trade_routes.unlocks.buildings,ai_personalities.json(4 occurrences acrossgoldvein+runepriest),manifests/buildings.json. - Q6 — Vertical-only. Hybrid merged structures (cross-category fusion)
remains
p1-59and is out of scope forp1-43.
Schema + validator + engine
public/games/age-of-dwarves/data/schemas/building.schema.json: newupgrade_fee: number|nullfield (>= 0). Other three stacking fields (requires_existing,consumes_existing,stack_mode) already present from cycle 5.tools/validate-game-data.py::validate_building_requires_existing: cross-references everyrequires_existing: <id>againstpublic/resources/buildings/*.json. New self-test fixture (requires_existing: "nonexistent") raises a hard error. Self-test passes; full run reports zero failures in the new section across 185 known building ids.mc-city::BuildingDef::upgrade_fee: Option<u32>(src/simulator/crates/mc-city/src/building.rs).mc-city::stacking::compute_construction_cost(def, lvn_cost, prereq_cost, has_prereq) -> u32(src/simulator/crates/mc-city/src/stacking.rs). Saturating arithmetic; default fee =(lvn_cost * 0.15).round()whendef.upgrade_feeisNone.BuildingDef::costis intentionally NOT a Rust field (cost lives in JSON; the function takes it as a parameter so callers thread the cost lookup themselves).
5 chain-extension proof buildings
Authored under public/resources/buildings/, each carrying
requires_existing to its predecessor and upgrade_fee set, effects array
sums all ancestor tiers per Q3:
| ID | Tier | Category | requires_existing |
consumes_existing |
|---|---|---|---|---|
hydroponic_farm |
5 | food | watermill |
true |
bazaar |
5 | infrastructure (wealth) | guild_hall |
false |
grand_chronicle |
7 | culture | bardic_circle |
false |
gravity_press |
9 | production | mithril_forge |
false |
apothecarium |
5 | infrastructure (medical) | hospital |
true |
All five round-trip via BuildingDef (test_all_authored_buildings_deserialize
passes). Manifest public/games/age-of-dwarves/data/manifests/buildings.json
updated.
Tests (cycle 24)
test_direct_build_pays_full_cost,test_upgrade_pays_delta_plus_fee,test_upgrade_cost_saturates_when_prereq_costlier— new inmc-city::stacking::tests.consumes_existing_removes_predecessor,upgrade_chain_consumes_predecessor_at_completion— pre-existing, still green.cargo test -p mc-city --lib— 146/146 pass (incl. new cost tests +test_all_authored_buildings_deserializecovering 185 building JSONs).cargo test -p mc-core --lib— 211/211 pass.python3 tools/validate-game-data.py --self-test— all 4 golden bad-data tests pass, including the newrequires_existingcross-ref check.
Authoring rule documented
public/games/age-of-dwarves/docs/BUILDING_SCHEMA.md extended with:
upgrade_feefield row in the Stacking table.- "Cost calculation" subsection (Q2 — full vs delta+fee).
- "Effects inheritance authoring rule" subsection (Q3 — author-time additive inheritance, with worked food-chain example).
- "Slot constraint" subsection (Q4 — no artificial pool).
Follow-up filed
p1-43b-deep-chain-authoring.md (status: stub, scope: game1) covers the
remaining ~35 high-tier building JSONs across food, wealth, culture,
production, and medical chains — see that objective for the full slate.
Status
p1-43 remains partial. Engine + schema + chain-extension proof complete;
bulk authoring (p1-43b, done cycle 27) extended 185 → 199 buildings, now
209 total with 92 carrying produces: arrays. Remaining bullets tracked in
p1-43c (stub, 2026-05-07): chain ladder produces: fill (~117 remaining
producer buildings), AI stack scoring (blocked on p1-42), city UI upgrade
surface, GUT bridge test.