magicciv/tools/migrate-fauna-biomes.py
Natalie a56cea7734 feat(@projects): add terrain and fauna systems
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-01 02:06:53 -04:00

212 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Migrate Game-1 fauna species biomes[] → substrate_climate[] arrays.
Only processes species listed in public/games/age-of-dwarves/data/manifests/fauna.json.
Usage:
python3 tools/migrate-fauna-biomes.py # dry-run (print diffs)
python3 tools/migrate-fauna-biomes.py --apply # write back to files
The legacy biomes[] field is kept (not deleted) for backwards compatibility.
New substrate_climate[] is additive.
"""
import argparse
import json
import os
import sys
# Translation table — same vocabulary as flora.
BIOME_TO_SUBSTRATE_CLIMATE = {
"forest": [{"substrate": "soil", "t_band_min": 2, "t_band_max": 4, "p_band_min": 2, "p_band_max": 4}],
"temperate_forest": [{"substrate": "soil", "t_band_min": 2, "t_band_max": 3, "p_band_min": 2, "p_band_max": 4}],
"boreal_forest": [{"substrate": "soil", "t_band_min": 0, "t_band_max": 1, "p_band_min": 2, "p_band_max": 4}],
"jungle": [{"substrate": "soil", "t_band_min": 4, "t_band_max": 4, "p_band_min": 4, "p_band_max": 4}],
"tropical_rainforest": [{"substrate": "soil", "t_band_min": 4, "t_band_max": 4, "p_band_min": 4, "p_band_max": 4}],
"tropical_forest": [{"substrate": "soil", "t_band_min": 3, "t_band_max": 4, "p_band_min": 3, "p_band_max": 4}],
"tropical_dry_forest": [{"substrate": "soil", "t_band_min": 3, "t_band_max": 4, "p_band_min": 1, "p_band_max": 2}],
"montane_forest": [{"substrate": "bedrock", "t_band_min": 1, "t_band_max": 2, "p_band_min": 2, "p_band_max": 4}],
"cloud_forest": [{"substrate": "soil", "t_band_min": 2, "t_band_max": 3, "p_band_min": 3, "p_band_max": 4}],
"old_growth": [{"substrate": "soil", "t_band_min": 2, "t_band_max": 3, "p_band_min": 2, "p_band_max": 4}],
"temperate_rainforest": [{"substrate": "soil", "t_band_min": 2, "t_band_max": 3, "p_band_min": 3, "p_band_max": 4}],
"swamp": [{"substrate": "peat", "t_band_min": 2, "t_band_max": 3, "p_band_min": 4, "p_band_max": 4}],
"bog": [{"substrate": "peat", "t_band_min": 1, "t_band_max": 3, "p_band_min": 4, "p_band_max": 4}],
"fen": [{"substrate": "peat", "t_band_min": 2, "t_band_max": 3, "p_band_min": 3, "p_band_max": 4}],
"wetland": [{"substrate": "peat", "t_band_min": 1, "t_band_max": 4, "p_band_min": 3, "p_band_max": 4}],
"coastal_wetland": [{"substrate": "peat", "t_band_min": 2, "t_band_max": 4, "p_band_min": 3, "p_band_max": 4}],
"mangrove_adjacent": [{"substrate": "peat", "t_band_min": 3, "t_band_max": 4, "p_band_min": 3, "p_band_max": 4}],
"mangrove": [{"substrate": "peat", "t_band_min": 3, "t_band_max": 4, "p_band_min": 3, "p_band_max": 4}],
"grassland": [{"substrate": "soil", "t_band_min": 2, "t_band_max": 3, "p_band_min": 1, "p_band_max": 2}],
"temperate_grassland": [{"substrate": "soil", "t_band_min": 2, "t_band_max": 3, "p_band_min": 1, "p_band_max": 2}],
"savanna": [{"substrate": "soil", "t_band_min": 3, "t_band_max": 4, "p_band_min": 1, "p_band_max": 2}],
"plains": [{"substrate": "soil", "t_band_min": 2, "t_band_max": 3, "p_band_min": 1, "p_band_max": 2}],
"tundra": [{"substrate": "permafrost", "t_band_min": 0, "t_band_max": 1, "p_band_min": 0, "p_band_max": 2}],
"arctic_tundra": [{"substrate": "permafrost", "t_band_min": 0, "t_band_max": 0, "p_band_min": 0, "p_band_max": 2}],
"alpine_tundra": [{"substrate": "bedrock", "t_band_min": 0, "t_band_max": 1, "p_band_min": 1, "p_band_max": 3}],
"mountains": [{"substrate": "bedrock", "t_band_min": 1, "t_band_max": 3, "p_band_min": 1, "p_band_max": 3}],
"alpine_meadow": [{"substrate": "soil", "t_band_min": 1, "t_band_max": 2, "p_band_min": 2, "p_band_max": 3}],
"highland": [{"substrate": "bedrock", "t_band_min": 1, "t_band_max": 3, "p_band_min": 1, "p_band_max": 3}],
"hills": [{"substrate": "soil", "t_band_min": 1, "t_band_max": 3, "p_band_min": 1, "p_band_max": 3}],
"basalt_highland": [{"substrate": "bedrock", "t_band_min": 1, "t_band_max": 3, "p_band_min": 0, "p_band_max": 2}],
"heathland": [{"substrate": "soil", "t_band_min": 1, "t_band_max": 2, "p_band_min": 1, "p_band_max": 3}],
"shrubland": [{"substrate": "soil", "t_band_min": 2, "t_band_max": 3, "p_band_min": 1, "p_band_max": 2}],
"chaparral": [{"substrate": "soil", "t_band_min": 3, "t_band_max": 4, "p_band_min": 1, "p_band_max": 2}],
"desert": [{"substrate": "sand", "t_band_min": 3, "t_band_max": 4, "p_band_min": 0, "p_band_max": 0}],
"dune_field": [{"substrate": "sand", "t_band_min": 3, "t_band_max": 4, "p_band_min": 0, "p_band_max": 1}],
"dust_plain": [{"substrate": "sand", "t_band_min": 3, "t_band_max": 4, "p_band_min": 0, "p_band_max": 1}],
"rocky_waste": [{"substrate": "bedrock", "t_band_min": 1, "t_band_max": 4, "p_band_min": 0, "p_band_max": 1}],
"canyon": [{"substrate": "bedrock", "t_band_min": 2, "t_band_max": 4, "p_band_min": 0, "p_band_max": 1}],
"polar_desert": [{"substrate": "ice", "t_band_min": 0, "t_band_max": 0, "p_band_min": 0, "p_band_max": 1}],
"ice": [{"substrate": "ice", "t_band_min": 0, "t_band_max": 0, "p_band_min": 0, "p_band_max": 4}],
"snow": [{"substrate": "ice", "t_band_min": 0, "t_band_max": 1, "p_band_min": 1, "p_band_max": 4}],
"sea_ice": [{"substrate": "ice", "t_band_min": 0, "t_band_max": 0, "p_band_min": 0, "p_band_max": 4}],
"river": [{"substrate": "water", "t_band_min": 0, "t_band_max": 4, "p_band_min": 2, "p_band_max": 4}],
"lake": [{"substrate": "water", "t_band_min": 0, "t_band_max": 4, "p_band_min": 2, "p_band_max": 4}],
"pond": [{"substrate": "water", "t_band_min": 0, "t_band_max": 4, "p_band_min": 2, "p_band_max": 4}],
"estuary": [{"substrate": "water", "t_band_min": 1, "t_band_max": 4, "p_band_min": 2, "p_band_max": 4}],
"intertidal": [{"substrate": "seawater", "t_band_min": 1, "t_band_max": 4, "p_band_min": 2, "p_band_max": 4}],
"coast": [{"substrate": "seawater", "t_band_min": 0, "t_band_max": 4, "p_band_min": 2, "p_band_max": 4}],
"coastal_dune": [{"substrate": "sand", "t_band_min": 1, "t_band_max": 4, "p_band_min": 1, "p_band_max": 3}],
"coastal_sea": [{"substrate": "seawater", "t_band_min": 0, "t_band_max": 4, "p_band_min": 2, "p_band_max": 4}],
"coastal_shallows": [{"substrate": "seawater", "t_band_min": 0, "t_band_max": 4, "p_band_min": 2, "p_band_max": 4}],
"coastal_cliffs": [{"substrate": "bedrock", "t_band_min": 0, "t_band_max": 4, "p_band_min": 1, "p_band_max": 4}],
"ocean": [{"substrate": "seawater", "t_band_min": 0, "t_band_max": 4, "p_band_min": 0, "p_band_max": 4}],
"deep_ocean": [{"substrate": "seawater", "t_band_min": 0, "t_band_max": 4, "p_band_min": 0, "p_band_max": 4}],
"ocean_surface": [{"substrate": "seawater", "t_band_min": 0, "t_band_max": 4, "p_band_min": 0, "p_band_max": 4}],
"shallow_ocean": [{"substrate": "seawater", "t_band_min": 0, "t_band_max": 4, "p_band_min": 0, "p_band_max": 4}],
"shallow_sea": [{"substrate": "seawater", "t_band_min": 0, "t_band_max": 4, "p_band_min": 0, "p_band_max": 4}],
"cold_shallow_sea": [{"substrate": "seawater", "t_band_min": 0, "t_band_max": 1, "p_band_min": 0, "p_band_max": 4}],
"reef": [{"substrate": "seawater", "t_band_min": 3, "t_band_max": 4, "p_band_min": 2, "p_band_max": 4}],
"coral_reef": [{"substrate": "seawater", "t_band_min": 3, "t_band_max": 4, "p_band_min": 2, "p_band_max": 4}],
"kelp_forest": [{"substrate": "seawater", "t_band_min": 1, "t_band_max": 3, "p_band_min": 2, "p_band_max": 4}],
"abyssal_plain": [{"substrate": "seawater", "t_band_min": 0, "t_band_max": 2, "p_band_min": 0, "p_band_max": 4}],
"hadal_zone": [{"substrate": "seawater", "t_band_min": 0, "t_band_max": 1, "p_band_min": 0, "p_band_max": 4}],
"inland_sea": [{"substrate": "seawater", "t_band_min": 1, "t_band_max": 4, "p_band_min": 0, "p_band_max": 4}],
"volcanic": [{"substrate": "lava", "t_band_min": 0, "t_band_max": 4, "p_band_min": 0, "p_band_max": 4}],
"lava_field": [{"substrate": "lava", "t_band_min": 0, "t_band_max": 4, "p_band_min": 0, "p_band_max": 4}],
"volcanic_plains": [{"substrate": "bedrock", "t_band_min": 2, "t_band_max": 4, "p_band_min": 0, "p_band_max": 2}],
"subterranean": [{"substrate": "bedrock", "t_band_min": 1, "t_band_max": 3, "p_band_min": 0, "p_band_max": 2}],
"cave": [{"substrate": "bedrock", "t_band_min": 1, "t_band_max": 3, "p_band_min": 0, "p_band_max": 2}],
"underdark": [{"substrate": "bedrock", "t_band_min": 1, "t_band_max": 3, "p_band_min": 0, "p_band_max": 2}],
"ancient_lakebed": [{"substrate": "soil", "t_band_min": 2, "t_band_max": 4, "p_band_min": 0, "p_band_max": 2}],
"lowland_basin": [{"substrate": "soil", "t_band_min": 2, "t_band_max": 4, "p_band_min": 1, "p_band_max": 3}],
"cliffs": [{"substrate": "bedrock", "t_band_min": 0, "t_band_max": 4, "p_band_min": 0, "p_band_max": 4}],
}
# Freshwater/marine overrides keyed by domain field value.
# If a species has domain = "freshwater", water biomes → water substrate.
# If domain = "marine", all water biomes → seawater.
DOMAIN_SUBSTRATE_HINTS = {
"freshwater": {"seawater": "water"},
"marine": {"water": "seawater"},
}
def build_substrate_climate(biomes: list[str], domain: str | None) -> tuple[list[dict], list[str]]:
"""Return (entries, missing_biomes) where missing_biomes had no mapping."""
seen_keys: set[tuple] = set()
result: list[dict] = []
missing: list[str] = []
overrides = DOMAIN_SUBSTRATE_HINTS.get(domain or "", {})
for biome in biomes:
if biome not in BIOME_TO_SUBSTRATE_CLIMATE:
missing.append(biome)
continue
for raw in BIOME_TO_SUBSTRATE_CLIMATE[biome]:
entry = dict(raw)
if entry["substrate"] in overrides:
entry = dict(entry)
entry["substrate"] = overrides[entry["substrate"]]
key = (entry["substrate"], entry["t_band_min"], entry["t_band_max"],
entry["p_band_min"], entry["p_band_max"])
if key not in seen_keys:
seen_keys.add(key)
result.append(entry)
return result, missing
def process_file(path: str, apply: bool) -> bool:
"""Process one species file. Returns True if file was (or would be) changed."""
with open(path) as f:
data = json.load(f)
biomes = data.get("biomes", [])
if not biomes:
return False
domain = data.get("domain")
new_entries, missing = build_substrate_climate(biomes, domain)
if missing:
rel = os.path.relpath(path)
for m in missing:
print(f" WARN {rel}: unmapped biome '{m}'", file=sys.stderr)
if not new_entries:
return False
existing = data.get("substrate_climate", [])
if existing == new_entries:
return False
if apply:
data["substrate_climate"] = new_entries
with open(path, "w") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n")
else:
rel = os.path.relpath(path)
print(f" DIFF {rel}")
print(f" biomes: {biomes}")
print(f" substrate_climate (new): {json.dumps(new_entries, separators=(',', ':'))}")
return True
def main() -> None:
parser = argparse.ArgumentParser(description="Migrate Game-1 fauna biomes[] → substrate_climate[]")
parser.add_argument("--apply", action="store_true", help="Write changes (default is dry-run)")
args = parser.parse_args()
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
manifest_path = os.path.join(
repo_root, "public/games/age-of-dwarves/data/manifests/fauna.json"
)
with open(manifest_path) as f:
manifest = json.load(f)
# Deduplicate species list; warn on duplicates.
species_raw = manifest["species"]
species_ids: set[str] = set()
for s in species_raw:
if s in species_ids:
print(f" WARN fauna.json: duplicate species id '{s}' — processing once", file=sys.stderr)
species_ids.add(s)
fauna_dir = os.path.join(repo_root, "public/resources/ecology/fauna/species")
changed = 0
skipped_missing = []
processed = 0
for species_id in sorted(species_ids):
path = os.path.join(fauna_dir, f"{species_id}.json")
if not os.path.exists(path):
skipped_missing.append(species_id)
continue
processed += 1
if process_file(path, apply=args.apply):
changed += 1
if skipped_missing:
print(f" WARN No JSON file for species: {skipped_missing}", file=sys.stderr)
mode = "Applied" if args.apply else "Would change"
print(f"{mode} {changed}/{processed} Game-1 fauna species files ({len(skipped_missing)} missing JSON skipped).")
if __name__ == "__main__":
main()