265 lines
11 KiB
Python
265 lines
11 KiB
Python
#!/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()
|