magicciv/tools/gen-fallback-sprites.py

266 lines
11 KiB
Python
Raw Permalink Normal View History

#!/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()