212 lines
14 KiB
Python
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()
|