feat(@projects/@magic-civilization): ✨ add apothecarium building
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
e025a563cd
commit
7ba49aa59e
3 changed files with 104 additions and 0 deletions
|
|
@ -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",
|
||||
|
|
|
|||
45
public/resources/buildings/apothecarium.json
Normal file
45
public/resources/buildings/apothecarium.json
Normal 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": []
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue