feat(@projects/@magic-civilization): finalize stacking objective design

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-05 11:10:06 -04:00
parent 7ba49aa59e
commit 4a994eb22c
3 changed files with 249 additions and 0 deletions

View file

@ -229,3 +229,105 @@ Wonders override to `amplify` regardless of category.
- ✓ 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 ~35 high-tier
buildings deferred to the follow-up `p1-43b`.
### 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`) and the still-open AI catalog scoring + GDScript
bridge + UI surface bullets keep `p1-43` from `done`.

View file

@ -0,0 +1,101 @@
---
id: p1-43b
title: Deep chain authoring — fill T6/T7/T8/T9/T10 building tiers across the 5 short chains
priority: p1
status: stub
scope: game1
updated_at: 2026-05-05
blocked_by:
- p1-43a (engine + schema + chain-extension proof landed inline in p1-43)
evidence: []
---
## Summary
`p1-43a` (closed inline in `p1-43-building-stacking-upgrade.md` on 2026-05-05)
shipped the engine + schema layer and **5 representative new high-tier
buildings** as a chain-extension proof:
| ID | Tier | Chain |
|---|---|---|
| `hydroponic_farm` | 5 | food |
| `bazaar` | 5 | wealth |
| `grand_chronicle` | 7 | culture |
| `gravity_press` | 9 | production |
| `apothecarium` | 5 | medical |
This objective (`p1-43b`) covers the **remaining ~35 high-tier buildings**
needed to extend each short chain to ~8 tiers, per Q1's locked design.
## Scope — buildings to author
Each tier carries `requires_existing` pointing at its predecessor and
`upgrade_fee` (or omits to use the 15% default). Per Q3, each tier's
`effects` array is authored as the cumulative sum of all ancestors plus the
marginal contribution.
### Food chain (T1=granary → T2=watermill → T5=hydroponic_farm shipped)
- T6 candidate: `terraced_irrigation` — additional yield + drought-resistance
- T7 candidate: `subterranean_pasture` — meat/dairy + livestock specialist
- T9 candidate: `synthetic_grange` — industrial-era food multiplier
### Wealth chain (T2=marketplace → T4=guild_hall → T5=bazaar shipped)
- T6 candidate: `mercantile_exchange` — trade route capacity + banking
- T7 candidate: `royal_bank` — gold percent multiplier + specialist
- T9 candidate: `world_clearinghouse` — empire-wide gold percent
### Culture chain (T1=monument → T2=gathering_hall → T3=great_hall → T4=bardic_circle → T7=grand_chronicle shipped)
- T5 candidate: `clan_atelier` (NB: distinct from existing wonder `deep_atelier`) — culture + great-work slots
- T8 candidate: `hall_of_voices` — happiness + great-person spawn rate
- T9 candidate: `eternal_record` — empire-wide culture multiplier
### Production chain (T1=forge → T2=iron_forge → T3=dwarf_deep_forge → T5=steam_forge → T6=tempering_forge → T7=mithril_forge → T9=gravity_press shipped)
- T8 candidate: `industrial_smelter` — production + wonder-build percent
- T10 candidate: `adamantine_press` — caps the chain alongside existing `adamantine_foundry`
### Medical chain (T1=barber → T2=clinic → T3=hospital → T5=apothecarium shipped)
- T7 candidate: `field_hospital_corps` — heal-in-city + global heal bonus
- T9 candidate: `royal_infirmary` — happiness + plague-resistance + great person
- T10 candidate: `eternal_sanctum` — empire-wide unit heal cap
### Optional military chain extensions
`p1-43a`'s 14 weapon-class tables (already on record in the parent objective)
identified Lv2 gaps for `bow/crossbow` (`bolt_range` → ?) and `marksman`
(`marksman_lodge` → ?). If `p1-43b` widens scope, fill those producer-chain
gaps too.
## Acceptance
- ✗ Each new building authored as its own JSON file under
`public/resources/buildings/<id>.json`.
- ✗ Manifest `public/games/age-of-dwarves/data/manifests/buildings.json`
extended (alphabetical sort).
- ✗ Each building carries: `tier`, `category`, `cost`, `upkeep`, `effects`
(cumulative per Q3), `requires_existing`, `upgrade_fee`,
`consumes_existing` (default `false` unless replacement is intended),
`stack_mode: "amplify"` (single-stream chains), `produces` (where the chain
trains units), `specialist_slots` (where the role exists), and `sprite`.
- ✗ `tools/validate-game-data.py` reports zero new failures.
- ✗ `cargo test -p mc-city --lib test_all_authored_buildings_deserialize`
remains green.
## Out of scope
- Engine logic — already shipped in `p1-43a`.
- AI catalog scoring (separate bullet under `p1-43`, blocked by `p1-42`).
- GDScript bridge / UI surface (separate bullets under `p1-43`).
- Sprite art generation — sprite paths are authored as filenames; bitmap
generation is a separate sprite-pipeline task.
- Hybrid merged structures (`p1-59`).
## Closing
When all ~35 buildings are authored, validator + Rust deserialize green, and
the manifest is up to date: flip status to `done` and `p1-43` itself can
flip to `done` once the open AI / UI bullets are also closed.

View file

@ -223,8 +223,54 @@ defaults preserve pre-`p1-43` "binary" semantics.
|---|---|---|---|
| `requires_existing` | string\|null | `null` | Predecessor building id that must already be present in the city before this building can be queued. Production gate enforced by `City::can_build` via `mc-city::stacking::check_requires_existing`. |
| `consumes_existing` | bool | `false` | When `true` AND `requires_existing` is set, completing this building removes the predecessor id from the city's building list (replacement upgrade). When `false`, both predecessor and successor coexist (parallel ladder). |
| `upgrade_fee` | number\|null | `null` | (`p1-43a` Q2) Additional gold cost charged on the upgrade-from-prerequisite path. `null` means "use the engine default": 15% of `cost`, rounded. Authors set a concrete number to override. Ignored when `requires_existing` is `null` (no upgrade path exists). |
| `stack_mode` | enum | `"single"` | One of `"parallel" \| "amplify" \| "single"`. See table below. |
### Cost calculation (`p1-43a` Q2)
A building reachable through a `requires_existing` ladder may be queued via two
paths:
| Path | Cost paid |
|---|---|
| **Direct build** (predecessor not in city) | full `cost` |
| **Upgrade from predecessor** (predecessor present, `consumes_existing` true) | `cost prereq.cost + upgrade_fee` (saturating) |
Engine entry point: `mc-city::stacking::compute_construction_cost(def, lvn_cost, prereq_cost, has_prereq)`.
Default fee constant: `mc-city::stacking::DEFAULT_UPGRADE_FEE_FRACTION = 0.15`.
### Effects inheritance authoring rule (`p1-43a` Q3)
**Effect inheritance is author-time, not runtime.** When a building declares
`requires_existing: <prereq>`, its `effects` array MUST be authored as the
SUM of all ancestor tiers' effects PLUS the marginal contribution at this
tier. This means a city holding only the upper tier (after `consumes_existing`
removed the predecessor) still benefits from the entire chain — because the
upper tier's authored numbers already absorb the chain.
Concrete example — food chain `granary` (T1) → `mill` (T2) → `watermill` (T2) → `hydroponic_farm` (T5):
```text
granary : food +1
mill : food +1, food_percent 0.05 (granary +1 absorbed → effective +2 +5%)
watermill : food +2, food_percent 0.15 (mill numbers absorbed → effective +2 +15%)
hydroponic_farm : food +5, food_percent 0.25, (watermill absorbed → marginal +3 +10%)
food_per_river_tile +1,
famine_resistance +1
```
The validator and Rust runtime do **not** re-sum predecessor effects — that
would double-count cities running parallel ladders (`consumes_existing: false`).
This rule keeps a single canonical effect-list per tier and avoids
reconciliation logic in the simulator.
### Slot constraint (`p1-43a` Q4)
There is **no artificial per-city slot pool** for the upgrade ladders. The
constraint surface is the natural economy: tiles owned, city population
(specialist allocation), production capacity, and gold treasury. No
`slot_capacity` or `max_concurrent_chains` field on the schema.
### `stack_mode` values
| Value | Semantics | Used by |