magicciv/.project/objectives/p1-43-building-stacking-upgrade.md
Natalie 7093758d83 feat(@projects/@magic-civilization): update mcts and tech objectives with followups
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-14 20:16:32 -07:00

31 KiB
Raw Blame History

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
src/simulator/crates/mc-city/src/building.rs:262-269 — BuildingDef::upgrade_fee: Option<u32> (p1-43a Q2)
src/simulator/crates/mc-city/src/stacking.rs:101-135 — compute_construction_cost (Q2 cost calc) + DEFAULT_UPGRADE_FEE_FRACTION
src/simulator/crates/mc-city/src/stacking.rs::tests — test_direct_build_pays_full_cost / test_upgrade_pays_delta_plus_fee / test_upgrade_cost_saturates_when_prereq_costlier
public/games/age-of-dwarves/data/schemas/building.schema.json:42-45 — upgrade_fee field added
public/resources/buildings/{hydroponic_farm,bazaar,grand_chronicle,gravity_press,apothecarium}.json — 5 chain-extension proof buildings
public/resources/buildings/marketplace.json — Q5 fold: market.json deleted, distinguishing content merged here (tradeswright/caravan_master/gpp_trade/trade_routes)
tools/validate-game-data.py::validate_building_requires_existing — cross-ref check + self-test (185 known building ids)
public/games/age-of-dwarves/docs/BUILDING_SCHEMA.md §Stacking — Q2 cost calc, Q3 author-time inheritance rule, Q4 no-slot-pool documented
.project/objectives/p1-43b-deep-chain-authoring.md — DONE 2026-05-05 (cycle 27)
14 high-tier buildings authored across 5 short chains (food T6/T7/T9, wealth T6/T7/T9, culture T5/T8/T9, production T8/T10, medical T7/T9/T10); manifest 185 → 199 entries; mid-chain insertions re-pointed grand_chronicle and gravity_press

2026-05-03 verification

Data layer substantially shipped; engine consumption still pending.

  • 178/181 buildings under public/resources/buildings/ carry the stack_mode field (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 under public/resources/buildings/.

Engine remains unwired:

  • grep "requires_existing\|consumes_existing" across src/simulator/crates/ and src/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-referencing requires_existing ids.
  • AI catalog scoring unchanged; UI does not surface "Can be upgraded to: X".

Promoted missingpartial.

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:

  1. Successor identity: is infantry a NEW building (needs authoring) or an existing one (e.g. reuse armory as the "barracks Lv2" slot)?
  2. Mechanic shape:
    • (a) Replacement: building barracks twice consumes both, slot becomes infantry. Original gone.
    • (b) Levelled: building stays "barracks" but carries a level: 2 field with stacked effects.
    • (c) Per-tile: two barracks on same tile merge (only relevant if placement_tile_required: true).
  3. 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. (Q1Q6 locked 2026-05-05; decisions recorded in §"p1-43a close-out / Q1Q6 decisions" below.)
  • building.schema.json extends with: requires_existing: <id|null>, consumes_existing: <bool>. Both default null/false. Validator confirms requires_existing resolves 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_existing cross-refs 185+ ids.)
  • ✓ For each declared stack-pair: the upper-tier building is NEW data (or repurposed existing) authored under resources/buildings/<id>.json with the new fields populated. (cycle 27 / 2026-05-05 via p1-43b: 14 new high-tier buildings closing the food/wealth/culture/production/medical short chains; manifest at 199 entries; all requires_existing cross-refs validate.)
  • ✓ Initial 3-step ladders (civilian + economic + 14 military producer chains): produces: arrays filled across 159 producer buildings; 9 documented as produces_yield_only: true; 0 unaccounted. Closed via p1-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: libraryscriptorium (NEW) → university → produces sages / cartographers / engineers
  • Culture: monumentgathering_hallgreat_hall → produces bards / loremasters
  • Production: forgeiron_forge (NEW) → dwarf_deep_forge → produces smiths / engineers
  • Defense: wallswatchtowercastle → produces garrison/sentry units
  • Food: granarymillwatermill → yield-only, no unit
  • Wealth: marketplacemarket (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) barracksdrill_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) airfieldzeppelin_dock (existing t8) gyrocopter → iron_hawk → war_zeppelin → mithril_hawk → sky_fortress
Naval (13 units) harbordeep_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 (infantry Lv2 melee, iron_forge Lv2 production, scriptorium Lv2 science, barber/clinic/hospital for 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> returns Err(RequiresExistingMissing { .. }) when def.requires_existing is set and the predecessor is absent (src/simulator/crates/mc-city/src/city.rs:404-413 + stacking::check_requires_existing src/simulator/crates/mc-city/src/stacking.rs:65-80). City::finalize_build(&BuildingDef) -> Option<BuildingId> removes the predecessor from city.buildings when consumes_existing: true (src/simulator/crates/mc-city/src/city.rs:419-428 + stacking::apply_consumes_existing stacking.rs:104-119). New StackMode { Parallel, Amplify, Single } enum at src/simulator/crates/mc-core/src/ids.rs:62-92; new requires_existing: Option<BuildingId>, consumes_existing: bool, stack_mode: StackMode fields on BuildingDef at src/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 in src/simulator/crates/mc-city/src/stacking.rs::tests. The GDScript GUT test that calls through GdCity::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.py extended to validate requires_existing cross-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

  1. 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.
  2. Cost: do stack-upgrades cost the FULL upper-tier cost, or a discounted "upgrade cost" (e.g. cost - prerequisite_cost)?
  3. Effects inheritance: when the prerequisite is consumed, do its effects vanish entirely, or does the upper-tier inherit them additively (so an armory city has barracks+infantry+armory effects compounded)?
  4. 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?
  5. Reconcile duplicates surfaced by the audit: marketplace vs market, monument vs bardic_circle — fold one into the other or keep both?
  6. p1-59 relationship: 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 walking requires_existing chains + combined costs). Re-homed from this objective 2026-05-14.
  • City UI upgrade surface ("Can be upgraded to: X") — requires the GdBuildingRegistry::get_upgrade_target api-gdext bridge that does not yet exist. Filed as p1-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 alongside p1-43c-gdext-upgrade-target.md (which carries its own bridge test test_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 with default: null/false.
    • Validator: tools/validate-game-data.py — cross-ref requires_existing against authored building IDs in public/resources/buildings/.
  • Dependencies: design sign-off above.
  • Acceptance gate: python3 tools/validate-game-data.py reports 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 — verify requires_existing + consumes_existing populated post-schema-extension.
    • Existing late-tier buildings (academy_of_sciences, dwarf_deep_forge, etc.) — add requires_existing pointing at their Lv2.
  • Dependencies: schema extension above.
  • Acceptance gate: every chain in the table has each tier carrying requires_existing to its predecessor; validator green.
  • SOLID/DRY/SSoT rails: data only in public/resources/buildings/; no data/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 marketplace vs market (Q5 in open questions).
  • Dependencies: design sign-off (Q1 + Q5).
  • Acceptance gate: tools/validate-game-data.py confirms every produces entry resolves to an authored unit ID; spot-check infantry.produces includes pikeman, 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) at src/simulator/crates/mc-city/src/city.rs:404-413; City::finalize_build(&BuildingDef) at city.rs:419-428. Backed by pure functions in new module src/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 } at src/simulator/crates/mc-core/src/ids.rs:62-92 — closes the "no stringly-typed" rail.
  • ✓ Typed errors: mc-city::StackingError::{ RequiresExistingMissing, SingleInstanceAlreadyBuilt } at stacking.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 at src/simulator/crates/mc-city/src/stacking.rs::tests. cargo test -p mc-core -p mc-city -p mc-turn and cargo check --workspace green.
  • Outstanding (under separate bullets above): the GDScript dispatch (ai_turn_bridge_dispatch.gd::dispatch_set_production) and the GdCity bridge methods that translate StackingErrorEventBus.building_requires_existing_unmet are NOT wired in this cycle — Rust SSoT is in place; the bridge wrapper is a follow-up. The stack_mode: amplify rate-scalar runtime hook (yields × stack-depth) is NOT implemented; Amplify currently 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 walk requires_existing chains and combine costs.
    • Depends on p1-42 AI catalog scoring landing first.
  • Dependencies: p1-42.
  • Acceptance gate: cargo test -p mc-ai test_stack_upgrade_combined_cost green; scoring infantry from a city without barracks returns 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) -> String in src/simulator/api-gdext/src/lib.rs.
  • Dependencies: data bullet above.
  • Acceptance gate: in-game encyclopedia for barracks shows "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.gd green.
  • 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.py green; 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 carry single (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 — all wonder_type: "national". single is a meaningful per-city semantic distinct from amplify/parallel: it strictly rejects a second instance via StackingError::SingleInstanceAlreadyBuilt. Re-tagging would have lost information.
  • ✓ Rust enum: StackMode { Parallel, Amplify, Single } at src/simulator/crates/mc-core/src/ids.rs:62-92, default Single (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-44stack_mode enum 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 (test test_all_authored_buildings_deserialize, cargo test -p mc-city passes).

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-43bclosed 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.

Q1Q6 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 68 tiers, no new authoring there.
  • Q2 — Cost. Both paths allowed. Direct build pays full cost. Upgrade from prerequisite pays cost prereq.cost + upgrade_fee. New upgrade_fee field on the building schema; default null = 15% of cost rounded.
  • Q3 — Effects inheritance (additive at author-time). Each tier's effects array 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_capacity schema field.
  • Q5 — Fold market into marketplace. market.json deleted; its distinguishing content (tech_required: trade_routes, produces: caravan_master, gpp_trade +1, tradeswright specialist slot) merged into marketplace.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 across goldvein + runepriest), manifests/buildings.json.
  • Q6 — Vertical-only. Hybrid merged structures (cross-category fusion) remains p1-59 and is out of scope for p1-43.

Schema + validator + engine

  • public/games/age-of-dwarves/data/schemas/building.schema.json: new upgrade_fee: number|null field (>= 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 every requires_existing: <id> against public/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() when def.upgrade_fee is None. BuildingDef::cost is 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 in mc-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_deserialize covering 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 new requires_existing cross-ref check.

Authoring rule documented

public/games/age-of-dwarves/docs/BUILDING_SCHEMA.md extended with:

  • upgrade_fee field 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.