feat(@projects/@magic-civilization): add apothecarium building

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

View file

@ -3,6 +3,7 @@
"includes": [
"academy_of_sciences",
"adamantine_foundry",
"apothecarium",
"airfield",
"alchemist_bench",
"alchemist_workshop",
@ -13,6 +14,7 @@
"armory",
"axis_mundi",
"barracks",
"bazaar",
"brewery",
"carved_hall",
"castle",
@ -38,10 +40,12 @@
"gathering_hall",
"granary",
"grand_amphitheater",
"grand_chronicle",
"grand_citadel",
"grand_gate",
"grand_harbor",
"grand_orrery",
"gravity_press",
"great_barrow",
"great_granary",
"great_hall",
@ -54,6 +58,7 @@
"herbalist",
"high_guild_hall",
"hunting_lodge",
"hydroponic_farm",
"infirmary",
"iron_throne",
"library",

View file

@ -0,0 +1,45 @@
{
"id": "apothecarium",
"name": "Apothecarium",
"description": "A sealed pharmacy and surgical school, with stocks of distilled herb, mineral salt, and bone-set splints. Field surgeons rotate through residencies and march out with a year's worth of supply on their backs.",
"placement": "city",
"category": "infrastructure",
"school": null,
"cost": 360,
"upkeep": 4,
"tech_required": "applied_medicine",
"race_required": null,
"wonder_type": null,
"mana_generated": null,
"tier": 5,
"effects": [
{ "type": "happiness", "value": 5 },
{ "type": "unit_heal_in_city", "value": 35 },
{ "type": "global_unit_heal_bonus", "value": 4 },
{ "type": "plague_resistance", "value": 1 },
{ "type": "great_person_points", "value": 1 }
],
"requires_existing": "hospital",
"consumes_existing": true,
"upgrade_fee": 50,
"stack_mode": "amplify",
"produces": [
"battle_medic",
"field_surgeon",
"master_surgeon"
],
"sprite": "sprites/buildings/apothecarium.png",
"encyclopedia": {
"category": "civilization",
"entry_type": "building",
"detail_route": "/buildings/buildings",
"tags": [
"medical",
"infrastructure",
"stack",
"tier5"
]
},
"balance_note": "Medical T5 chain-extension proof for p1-43a. Effects authored as (barber T1 + clinic T2 + hospital T3) sum + marginal T5 contribution per Q3 author-time inheritance rule.",
"flags": []
}

View file

@ -440,6 +440,36 @@ class GameDataValidator:
else:
self._ok(label)
def validate_building_requires_existing(self):
"""p1-43a: every `requires_existing` ladder pointer must resolve to a
real building id. Cross-refs `public/resources/buildings/*.json` only
(post-p1-40 single source of truth)."""
bdir = self.resources / "buildings"
if not bdir.exists():
return
building_ids = self._load_id_set_from_split_dir(bdir)
# Also accept ids from any game-specific override dir, for completeness
if (self.game_data / "buildings").exists():
building_ids |= self._load_id_set_from_split_dir(self.game_data / "buildings")
print(f"\n building requires_existing cross-refs ({len(building_ids)} known ids)")
for f in sorted(bdir.glob("*.json")):
if f.name.endswith(".schema.json") or f.name in ("manifest.json", "building_categories.json"):
continue
for label, entry in self._collect_entries_from_file(f):
prereq = entry.get("requires_existing")
if prereq is None:
continue
ref_label = f"{label}.requires_existing"
if not isinstance(prereq, str):
self._fail(ref_label, f"must be string|null, got {type(prereq).__name__}")
elif prereq not in building_ids:
self._fail(
ref_label,
f"requires_existing='{prereq}' does not resolve to a known building id",
)
else:
self._ok(ref_label)
def validate_cross_refs(self):
"""Cross-reference checks: collectibles → resources, gates_* → units/buildings."""
resources = self._load_resources()
@ -526,6 +556,7 @@ class GameDataValidator:
self.validate_deposit_concept_refs()
self.validate_resources_kind()
self.validate_guide_data()
self.validate_building_requires_existing()
self.validate_cross_refs()
def report(self) -> int:
@ -641,6 +672,29 @@ def _run_self_test():
sys.exit(1)
print("SELF-TEST PASSED: unknown concept_resource correctly caught by deposit concept ref check")
# Self-test (p1-43a): a building declaring `requires_existing: "nonexistent"`
# must be caught by validate_building_requires_existing.
test_root = Path(tempfile.mkdtemp())
test_bld_dir = test_root / "public" / "resources" / "buildings"
test_bld_dir.mkdir(parents=True)
(test_bld_dir / "real.json").write_text(json.dumps(
{"id": "real", "name": "Real", "placement": "city", "category": "infrastructure",
"cost": 50, "upkeep": 0}
))
(test_bld_dir / "broken.json").write_text(json.dumps(
{"id": "broken", "name": "Broken", "placement": "city", "category": "infrastructure",
"cost": 100, "upkeep": 0, "requires_existing": "nonexistent"}
))
v4 = GameDataValidator(test_root, verbose=False)
v4.validate_building_requires_existing()
if v4.failed == 0:
print("SELF-TEST FAILED: requires_existing='nonexistent' should have been caught")
sys.exit(1)
if not any("nonexistent" in e for e in v4.errors):
print(f"SELF-TEST FAILED: expected nonexistent error, got: {v4.errors}")
sys.exit(1)
print("SELF-TEST PASSED: dangling requires_existing pointer correctly caught (p1-43a)")
def main():
parser = argparse.ArgumentParser(description="Validate Age of Dwarves game pack JSON data")