magicciv/tools/gen-fallback-sprites.py
2026-04-07 17:52:04 -07:00

265 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Generate simple SVG fallback sprites for all terrain biomes and unit types.
Outputs:
public/games/age-of-dwarves/assets/sprites/terrain/<biome_id>.svg (384×332 flat-top hex)
public/games/age-of-dwarves/assets/sprites/units/<unit_id>.svg (64×64 circle)
Run from repo root:
python3 tools/gen-fallback-sprites.py
"""
import os
from pathlib import Path
REPO_ROOT = Path(__file__).parent.parent
TERRAIN_DIR = REPO_ROOT / "public/games/age-of-dwarves/assets/sprites/terrain"
UNITS_DIR = REPO_ROOT / "public/games/age-of-dwarves/assets/sprites/units"
HEX_POINTS = "96,0 288,0 384,166 288,332 96,332 0,166"
# biome_id -> (base_hex_color, shade_hex_color)
# base is the highlight (lighter), shade is the deep shadow corner
BIOME_COLORS: dict[str, tuple[str, str]] = {
# --- land ---
"grassland": ("#73AD4D", "#3E6E24"),
"temperate_grassland": ("#8CB84D", "#5A7E28"),
"plains": ("#B8A560", "#7A6C34"),
"chaparral": ("#9E8E5A", "#6A5E34"),
"savanna": ("#C7B86B", "#8A7A38"),
"steppe": ("#A69E73", "#706840"),
"land": ("#7F9960", "#4E6634"),
# --- forests ---
"forest": ("#3C8C3C", "#1C501C"),
"temperate_forest": ("#2E7A38", "#144E1E"),
"taiga": ("#4D735A", "#284E38"),
"boreal_forest": ("#265A47", "#103C2C"),
"jungle": ("#267A27", "#104E12"),
"tropical_rainforest": ("#0F6126", "#063814"),
"tropical_dry_forest": ("#6B8038", "#445420"),
"temperate_rainforest":("#2A6040", "#12402A"),
"enchanted_forest": ("#7040A8", "#3A1A78"),
"montane_forest": ("#285C3D", "#123C26"),
"cloud_forest": ("#337057", "#184838"),
"mangrove": ("#1E5869", "#0C323E"),
# --- dry / arid ---
"desert": ("#E0D08B", "#A89040"),
"badlands": ("#B86B47", "#7A3C1E"),
"dust_plain": ("#B8784A", "#804420"),
"dune_field": ("#CC8C59", "#8A5428"),
"canyon": ("#9E4D2A", "#663010"),
"ancient_lakebed": ("#8C7252", "#5A4628"),
# --- elevated ---
"hills": ("#947D60", "#5E4E34"),
"highland": ("#9E8F70", "#6A5C40"),
"mountain": ("#8C807A", "#4E4844"),
"mountains": ("#72697A", "#3E3648"),
"highland_plain": ("#8A8060", "#5A5236"),
"alpine_meadow": ("#80997A", "#4A6844"),
"alpine_tundra": ("#9EA68F", "#606850"),
"peak": ("#C8C6D0", "#8888A0"),
"basalt_highland": ("#4D3F38", "#241E1A"),
# --- volcanic ---
"volcanic": ("#935238", "#4E2414"),
"volcano": ("#7A3329", "#401410"),
"volcanic_plains": ("#3F2E24", "#1E160E"),
"lava_field": ("#CC2A0A", "#7A1006"),
"caldera": ("#721A14", "#3A0808"),
# --- wetland ---
"marsh": ("#667856", "#384A2C"),
"swamp": ("#33471E", "#1A2C0A"),
"bog": ("#615232", "#3C301A"),
"wetland": ("#3E5234", "#1E2E18"),
"oasis": ("#599E7A", "#2A6848"),
# --- cold / ice ---
"tundra": ("#C0C8CC", "#7A8C94"),
"snow": ("#EBF0F5", "#A8C0D0"),
"ice": ("#D9E8F2", "#90B8D0"),
"permanent_ice": ("#C8DDE8", "#88B2CC"),
"glacial": ("#E6EDF5", "#A0C2DC"),
"sea_ice": ("#C0D9F2", "#80B0D8"),
"polar_desert": ("#B8BBAD", "#7A8474"),
"subterranean": ("#523D2E", "#2A1E14"),
"cave": ("#2E2820", "#12100A"),
# --- water ---
"ocean": ("#264D8C", "#102C5E"),
"deep_ocean": ("#142E66", "#081838"),
"coast": ("#3F7AA6", "#1E4E78"),
"shallow_ocean": ("#2E61B8", "#183A7E"),
"lake": ("#387099", "#1A4268"),
"inland_sea": ("#2E5E8E", "#163C62"),
"river": ("#3F7ACC", "#1E4898"),
"pond": ("#61A0D1", "#2E68A8"),
"estuary": ("#3F7285", "#204E5E"),
"coral_reef": ("#269E94", "#0E6660"),
"deep_water": ("#0A1A40", "#040C22"),
"shallow_water": ("#387ACC", "#1A4898"),
"lake_bed": ("#3E6274", "#1E3C4E"),
# --- substrates (rarely tile-level but complete coverage) ---
"lowland": ("#8CB854", "#5A7E2A"),
"midland": ("#7A9E52", "#4A6C28"),
# --- special ---
"mana_node": ("#8A5AAD", "#4A2076"),
"_default": ("#7A7A7A", "#4A4A4A"),
}
WATER_BIOMES = {
"ocean", "coast", "lake", "deep_ocean", "inland_sea", "shallow_ocean",
"deep_water", "shallow_water", "river", "pond", "estuary", "coral_reef",
"mangrove", "sea_ice", "lake_bed",
}
ICE_BIOMES = {
"ice", "snow", "permanent_ice", "glacial", "sea_ice", "polar_desert",
"alpine_tundra", "tundra",
}
LAVA_BIOMES = {"volcanic", "volcano", "volcanic_plains", "lava_field", "caldera"}
MAGIC_BIOMES = {"mana_node", "enchanted_forest"}
FOREST_BIOMES = {
"forest", "temperate_forest", "taiga", "boreal_forest", "jungle",
"tropical_rainforest", "tropical_dry_forest", "temperate_rainforest",
"montane_forest", "cloud_forest", "mangrove",
}
ELEVATED_BIOMES = {
"mountain", "mountains", "hills", "highland", "peak", "alpine_meadow",
"alpine_tundra", "basalt_highland",
}
def terrain_detail(biome_id: str, base: str) -> str:
"""Return SVG elements for biome-specific visual detail."""
if biome_id in WATER_BIOMES:
return (
f'\n <g clip-path="url(#hex)" opacity="0.12">'
f'\n <line x1="0" y1="110" x2="384" y2="120" stroke="white" stroke-width="4"/>'
f'\n <line x1="0" y1="166" x2="384" y2="172" stroke="white" stroke-width="3"/>'
f'\n <line x1="0" y1="222" x2="384" y2="228" stroke="white" stroke-width="4"/>'
f'\n </g>'
)
if biome_id in ICE_BIOMES:
return (
f'\n <g clip-path="url(#hex)" opacity="0.18">'
f'\n <line x1="140" y1="100" x2="250" y2="230" stroke="white" stroke-width="2.5"/>'
f'\n <line x1="250" y1="100" x2="140" y2="230" stroke="white" stroke-width="2.5"/>'
f'\n <line x1="192" y1="80" x2="192" y2="250" stroke="white" stroke-width="2"/>'
f'\n <line x1="110" y1="166" x2="274" y2="166" stroke="white" stroke-width="2"/>'
f'\n </g>'
)
if biome_id in LAVA_BIOMES:
return (
f'\n <g clip-path="url(#hex)" opacity="0.28">'
f'\n <line x1="152" y1="80" x2="192" y2="166" stroke="#FF6B00" stroke-width="4"/>'
f'\n <line x1="192" y1="166" x2="258" y2="205" stroke="#FF6B00" stroke-width="4"/>'
f'\n <line x1="192" y1="166" x2="138" y2="242" stroke="#FF4400" stroke-width="3"/>'
f'\n <line x1="192" y1="166" x2="120" y2="140" stroke="#FF8800" stroke-width="2.5"/>'
f'\n </g>'
)
if biome_id in MAGIC_BIOMES:
return (
f'\n <g clip-path="url(#hex)" opacity="0.28">'
f'\n <circle cx="152" cy="116" r="6" fill="white"/>'
f'\n <circle cx="238" cy="136" r="5" fill="white"/>'
f'\n <circle cx="180" cy="214" r="7" fill="white"/>'
f'\n <circle cx="258" cy="192" r="4" fill="white"/>'
f'\n <circle cx="122" cy="182" r="5" fill="white"/>'
f'\n <circle cx="200" cy="166" r="8" fill="white"/>'
f'\n </g>'
)
if biome_id in FOREST_BIOMES:
# Tree crown silhouettes
return (
f'\n <g clip-path="url(#hex)" opacity="0.18">'
f'\n <circle cx="148" cy="140" r="36" fill="black"/>'
f'\n <circle cx="236" cy="148" r="32" fill="black"/>'
f'\n <circle cx="188" cy="200" r="40" fill="black"/>'
f'\n </g>'
)
if biome_id in ELEVATED_BIOMES:
# Mountain ridge silhouette
return (
f'\n <g clip-path="url(#hex)" opacity="0.18">'
f'\n <polygon points="96,332 180,140 240,220 296,120 380,332" fill="black"/>'
f'\n </g>'
)
return ""
def make_terrain_svg(biome_id: str, base: str, shade: str) -> str:
detail = terrain_detail(biome_id, base)
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="384" height="332" viewBox="0 0 384 332">
<defs>
<clipPath id="hex">
<polygon points="{HEX_POINTS}"/>
</clipPath>
<radialGradient id="grad" cx="220" cy="100" r="290" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="{base}"/>
<stop offset="100%" stop-color="{shade}"/>
</radialGradient>
</defs>
<polygon points="{HEX_POINTS}" fill="url(#grad)"/>{detail}
<polygon points="{HEX_POINTS}" fill="none" stroke="rgba(0,0,0,0.22)" stroke-width="3"/>
</svg>"""
# unit_id -> (fill_color, ring_color, label)
UNIT_DEFS: dict[str, tuple[str, str, str]] = {
"founder": ("#D4A017", "#7A5A06", "F"),
"wanderer": ("#8C6E3A", "#4E3A18", "W"),
"scout": ("#5A8A3A", "#2E5218", "S"),
"warrior": ("#8C2A2A", "#4E1010", "W"),
"archer": ("#4A7A2E", "#244E10", "A"),
"worker": ("#7A6550", "#3E3224", "W"),
"mage": ("#5A2A8C", "#2E0E5E", "M"),
"settler": ("#C87828", "#7A440C", "S"),
"horseman": ("#7A5A28", "#4A3010", "H"),
"knight": ("#3A5A8C", "#1A3060", "K"),
"catapult": ("#5A3E28", "#2E1E10", "C"),
"galley": ("#2A5A8C", "#0E2E5E", "G"),
}
def make_unit_svg(unit_id: str, fill: str, ring: str, label: str) -> str:
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<radialGradient id="grad" cx="40" cy="24" r="30" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="{fill}"/>
<stop offset="100%" stop-color="{ring}"/>
</radialGradient>
<filter id="shadow" x="-25%" y="-25%" width="150%" height="150%">
<feDropShadow dx="1" dy="2" stdDeviation="2.5" flood-color="black" flood-opacity="0.45"/>
</filter>
</defs>
<circle cx="32" cy="32" r="29" fill="{ring}" filter="url(#shadow)"/>
<circle cx="32" cy="32" r="27" fill="url(#grad)"/>
<circle cx="32" cy="32" r="27" fill="none" stroke="{ring}" stroke-width="2.5"/>
<circle cx="32" cy="32" r="27" fill="none" stroke="rgba(255,255,255,0.18)" stroke-width="1.5"/>
<text x="32" y="41" text-anchor="middle" font-size="28" fill="white"
font-family="sans-serif" font-weight="bold" opacity="0.92">{label}</text>
</svg>"""
def generate_all() -> None:
TERRAIN_DIR.mkdir(parents=True, exist_ok=True)
UNITS_DIR.mkdir(parents=True, exist_ok=True)
# Terrain
terrain_count = 0
for biome_id, (base, shade) in BIOME_COLORS.items():
if biome_id == "_default":
continue
path = TERRAIN_DIR / f"{biome_id}.svg"
path.write_text(make_terrain_svg(biome_id, base, shade), encoding="utf-8")
terrain_count += 1
# Units
unit_count = 0
for unit_id, (fill, ring, label) in UNIT_DEFS.items():
path = UNITS_DIR / f"{unit_id}.svg"
path.write_text(make_unit_svg(unit_id, fill, ring, label), encoding="utf-8")
unit_count += 1
print(f"Generated {terrain_count} terrain SVGs → {TERRAIN_DIR}")
print(f"Generated {unit_count} unit SVGs → {UNITS_DIR}")
if __name__ == "__main__":
generate_all()