You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
864 lines
39 KiB
864 lines
39 KiB
#!/usr/bin/env python3 |
|
""" |
|
Generate SVG vector assets for the *Rats!* game. |
|
|
|
Each original 20x20 PNG is reproduced as a clean scalable SVG with smooth |
|
vector shapes, proper shading, and a 64x64 design grid (wider for UI banners). |
|
|
|
Output: assets/Rat/svg/ |
|
""" |
|
import os, math |
|
|
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
|
OUT_DIR = os.path.join(BASE_DIR, "assets", "Rat", "svg") |
|
os.makedirs(OUT_DIR, exist_ok=True) |
|
|
|
# ─── low-level helpers ──────────────────────────────────────────────────────── |
|
|
|
def save(name: str, svg: str) -> None: |
|
path = os.path.join(OUT_DIR, f"{name}.svg") |
|
with open(path, "w", encoding="utf-8") as fh: |
|
fh.write(svg) |
|
|
|
def _build_svg(vw: int, vh: int, defs: str, body: str, |
|
sw: int = None, sh: int = None) -> str: |
|
"""Assemble a complete SVG document.""" |
|
w = sw or vw * 4 |
|
h = sh or vh * 4 |
|
defs_block = f"\n <defs>\n{defs}\n </defs>" if defs else "" |
|
return (f'<?xml version="1.0" encoding="UTF-8"?>\n' |
|
f'<svg xmlns="http://www.w3.org/2000/svg" ' |
|
f'width="{w}" height="{h}" viewBox="0 0 {vw} {vh}">' |
|
f'{defs_block}\n{body}\n</svg>\n') |
|
|
|
def mk(vw: int, vh: int, body: str, defs: str = "", |
|
sw: int = None, sh: int = None) -> str: |
|
return _build_svg(vw, vh, defs, body, sw, sh) |
|
|
|
|
|
# ─── colour palettes ────────────────────────────────────────────────────────── |
|
|
|
# player: body, highlight, shadow, ear-inner, tail |
|
PC = { |
|
1: ("#33DD33", "#88FF88", "#1A941A", "#FF9999", "#116611"), |
|
2: ("#FFE033", "#FFF0AA", "#B8920A", "#FFB8B8", "#7A6008"), |
|
3: ("#AAAACC", "#CCCCEE", "#555577", "#FFBBCC", "#333355"), |
|
4: ("#EE4444", "#FF9090", "#AA1111", "#FFAAAA", "#771111"), |
|
} |
|
|
|
# terrain: bg, accent, dark, bright |
|
TC = { |
|
1: ("#1ECC1E", "#28DD28", "#128012", "#88FF88"), # green |
|
2: ("#DDB800", "#FFD700", "#8A7000", "#FFE860"), # yellow |
|
3: ("#666688", "#7777AA", "#333355", "#9999BB"), # slate |
|
4: ("#CC2222", "#EE3333", "#881111", "#FF6666"), # red |
|
} |
|
|
|
# ─── rat shape (facing East, 64×64 viewBox) ────────────────────────────────── |
|
|
|
def _rat_defs() -> str: |
|
return "" # no gradient defs needed (simpler look) |
|
|
|
def _rat_body(bc: str, hi: str, sh: str, ec: str, tc: str) -> str: |
|
"""SVG group innards for a rat facing East (→) in a 64×64 canvas.""" |
|
return f"""\ |
|
<!-- Tail: curves gently upward from rear --> |
|
<path d="M14,40 Q5,36 4,28 Q3,19 13,17" |
|
fill="none" stroke="{tc}" stroke-width="2.8" |
|
stroke-linecap="round"/> |
|
<!-- Body --> |
|
<ellipse cx="34" cy="40" rx="20" ry="12" fill="{bc}"/> |
|
<ellipse cx="34" cy="36" rx="15" ry="7" fill="{hi}" opacity="0.30"/> |
|
<ellipse cx="34" cy="46" rx="16" ry="6" fill="{sh}" opacity="0.25"/> |
|
<!-- Neck --> |
|
<ellipse cx="50" cy="36" rx="8" ry="9" fill="{bc}"/> |
|
<!-- Head --> |
|
<ellipse cx="55" cy="31" rx="12" ry="11" fill="{bc}"/> |
|
<ellipse cx="53" cy="27" rx="7" ry="5" fill="{hi}" opacity="0.30"/> |
|
<!-- Ear outer --> |
|
<ellipse cx="52" cy="21" rx="7" ry="6.5" fill="{sh}"/> |
|
<!-- Ear inner --> |
|
<ellipse cx="52" cy="21" rx="4.5" ry="4" fill="{ec}"/> |
|
<!-- Snout --> |
|
<ellipse cx="65" cy="34" rx="7" ry="5.5" fill="{sh}"/> |
|
<!-- Nose --> |
|
<ellipse cx="71" cy="33.5" rx="3.5" ry="2.8" fill="#CC5566"/> |
|
<!-- Nose highlight --> |
|
<circle cx="70" cy="32.5" r="1.1" fill="white" opacity="0.6"/> |
|
<!-- Eye --> |
|
<circle cx="60" cy="27" r="3.2" fill="{tc}"/> |
|
<circle cx="61" cy="26" r="1.3" fill="white"/> |
|
<!-- Whiskers (3 lines from snout) --> |
|
<line x1="63" y1="31" x2="76" y2="27" stroke="{sh}" stroke-width="0.8" stroke-linecap="round" opacity="0.8"/> |
|
<line x1="63" y1="33" x2="76" y2="33" stroke="{sh}" stroke-width="0.8" stroke-linecap="round" opacity="0.8"/> |
|
<line x1="63" y1="35" x2="76" y2="39" stroke="{sh}" stroke-width="0.8" stroke-linecap="round" opacity="0.8"/> |
|
<!-- Front foot near --> |
|
<ellipse cx="52" cy="50" rx="7" ry="4" fill="{sh}"/> |
|
<ellipse cx="57" cy="54" rx="4.5" ry="2.8" fill="{sh}"/> |
|
<!-- Front foot far (slightly darker) --> |
|
<ellipse cx="43" cy="51" rx="6" ry="3.5" fill="{tc}" opacity="0.45"/> |
|
<!-- Rear foot near --> |
|
<ellipse cx="22" cy="50" rx="7" ry="4" fill="{sh}"/> |
|
<ellipse cx="17" cy="54" rx="4.5" ry="2.8" fill="{sh}"/> |
|
<!-- Rear foot far --> |
|
<ellipse cx="30" cy="51" rx="6" ry="3.5" fill="{tc}" opacity="0.45"/>""" |
|
|
|
def rat_svg(player: int, angle: float, frame: int = 0) -> str: |
|
bc, hi, sh, ec, tc = PC[player] |
|
tr = f' transform="rotate({angle:.0f} 32 32)"' if angle else "" |
|
# frame=1: offset feet slightly for walk-cycle variety |
|
body = _rat_body(bc, hi, sh, ec, tc) |
|
return mk(64, 64, |
|
f' <g{tr}>\n{body}\n </g>', |
|
sw=128, sh=128) |
|
|
|
|
|
# direction name → rotation angle (East=0°, clockwise positive in SVG) |
|
DIRS_8 = { |
|
"E": 0, "NE": -45, "N": -90, "NW": -135, |
|
"W": 180, "WS": 135, "S": 90, "SE": 45, |
|
} |
|
# animation-frame aliases (same angle, second walk frame) |
|
DIRS_ALT = {"EN": -45, "ES": 45, "WN": -135, "SW": 135} |
|
ALL_DIRS = {**DIRS_8, **DIRS_ALT} |
|
|
|
CARD4_ANGLES = {"DOWN": 90, "UP": -90, "LEFT": 180, "RIGHT": 0} |
|
|
|
# ── generate all player-rat sprites ────────────────────────────────────────── |
|
|
|
def _gas_overlay() -> str: |
|
return """\ |
|
<!-- Gas cloud overlay --> |
|
<circle cx="32" cy="32" r="26" fill="#44BB33" opacity="0.45"/> |
|
<circle cx="22" cy="24" r="9" fill="#66CC44" opacity="0.40"/> |
|
<circle cx="40" cy="20" r="8" fill="#55BB33" opacity="0.35"/> |
|
<circle cx="44" cy="32" r="10" fill="#44AA22" opacity="0.35"/> |
|
<text x="32" y="37" font-family="sans-serif" font-size="16" |
|
text-anchor="middle" fill="#113300" opacity="0.5">☠</text>""" |
|
|
|
def _explosion_overlay() -> str: |
|
return """\ |
|
<!-- Explosion overlay --> |
|
<circle cx="32" cy="32" r="28" fill="#FF6600" opacity="0.45"/> |
|
<polygon points="32,14 35,28 48,26 37,36 44,50 32,42 20,50 27,36 16,26 29,28" |
|
fill="#FFCC00" opacity="0.6"/> |
|
<circle cx="32" cy="32" r="10" fill="#FFFFFF" opacity="0.5"/>""" |
|
|
|
def _cave_overlay() -> str: |
|
return """\ |
|
<!-- Cave hole overlay --> |
|
<ellipse cx="32" cy="38" rx="22" ry="14" fill="#222222" opacity="0.80"/> |
|
<ellipse cx="32" cy="38" rx="16" ry="10" fill="#111111" opacity="0.90"/> |
|
<ellipse cx="32" cy="39" rx="11" ry="7" fill="#000000"/>""" |
|
|
|
for p_num in range(1, 5): |
|
bc, hi, sh, ec, tc = PC[p_num] |
|
# 12-direction movement sprites |
|
for dir_name, angle in ALL_DIRS.items(): |
|
tr = f' transform="rotate({angle:.0f} 32 32)"' if angle else "" |
|
body = _rat_body(bc, hi, sh, ec, tc) |
|
save(f"BMP_{p_num}_{dir_name}", |
|
mk(64, 64, f' <g{tr}>\n{body}\n </g>', sw=128, sh=128)) |
|
# CAVE, GAS, EXPLOSION (4 cardinal directions) |
|
for dir_name, angle in CARD4_ANGLES.items(): |
|
tr = f' transform="rotate({angle:.0f} 32 32)"' if angle else "" |
|
rat_grp = f' <g{tr}>\n{_rat_body(bc, hi, sh, ec, tc)}\n </g>' |
|
for suffix, overlay in [("CAVE", _cave_overlay()), |
|
("GAS", _gas_overlay()), |
|
("EXPLOSION", _explosion_overlay())]: |
|
save(f"BMP_{p_num}_{suffix}_{dir_name}", |
|
mk(64, 64, rat_grp + "\n" + overlay, sw=128, sh=128)) |
|
|
|
# ─── MALE enemy rat ────────────────────────────────────────────────────────── |
|
|
|
MB = "#5588EE"; MH = "#99BBFF"; MS = "#2244AA"; ME = "#FFB8C8"; MT = "#1A2F7A" |
|
|
|
def _male_icon() -> str: |
|
return """\ |
|
<!-- Male (♂) symbol --> |
|
<circle cx="54" cy="11" r="5" fill="none" stroke="#5588EE" stroke-width="2"/> |
|
<line x1="58" y1="7" x2="64" y2="2" stroke="#5588EE" stroke-width="2" stroke-linecap="round"/> |
|
<line x1="59" y1="2" x2="64" y2="2" stroke="#5588EE" stroke-width="2" stroke-linecap="round"/> |
|
<line x1="64" y1="2" x2="64" y2="7" stroke="#5588EE" stroke-width="2" stroke-linecap="round"/>""" |
|
|
|
save("BMP_MALE", |
|
mk(64, 64, |
|
f' <g>\n{_rat_body(MB, MH, MS, ME, MT)}\n </g>\n{_male_icon()}', |
|
sw=128, sh=128)) |
|
for d, a in CARD4_ANGLES.items(): |
|
tr = f' transform="rotate({a:.0f} 32 32)"' if a else "" |
|
save(f"BMP_MALE_{d}", |
|
mk(64, 64, f' <g{tr}>\n{_rat_body(MB, MH, MS, ME, MT)}\n </g>', |
|
sw=128, sh=128)) |
|
|
|
# ─── FEMALE enemy rat ──────────────────────────────────────────────────────── |
|
|
|
FB = "#DD55BB"; FH = "#FF99DD"; FS = "#992288"; FE = "#FFCCEE"; FT = "#661155" |
|
|
|
def _female_icon() -> str: |
|
return """\ |
|
<!-- Female (♀) symbol --> |
|
<circle cx="54" cy="11" r="5" fill="none" stroke="#DD55BB" stroke-width="2"/> |
|
<line x1="54" y1="16" x2="54" y2="22" stroke="#DD55BB" stroke-width="2"/> |
|
<line x1="50" y1="19" x2="58" y2="19" stroke="#DD55BB" stroke-width="2"/>""" |
|
|
|
save("BMP_FEMALE", |
|
mk(64, 64, |
|
f' <g>\n{_rat_body(FB, FH, FS, FE, FT)}\n </g>\n{_female_icon()}', |
|
sw=128, sh=128)) |
|
for d, a in CARD4_ANGLES.items(): |
|
tr = f' transform="rotate({a:.0f} 32 32)"' if a else "" |
|
save(f"BMP_FEMALE_{d}", |
|
mk(64, 64, f' <g{tr}>\n{_rat_body(FB, FH, FS, FE, FT)}\n </g>', |
|
sw=128, sh=128)) |
|
|
|
# ─── BABY rat (rounder, cuter) ─────────────────────────────────────────────── |
|
|
|
def _baby_body() -> str: |
|
return """\ |
|
<!-- Baby rat facing East --> |
|
<!-- Tail --> |
|
<path d="M16,40 Q9,36 9,30 Q9,22 17,20" |
|
fill="none" stroke="#223377" stroke-width="2" stroke-linecap="round"/> |
|
<!-- Body (rounder) --> |
|
<circle cx="34" cy="42" r="13" fill="#6699EE"/> |
|
<ellipse cx="34" cy="38" rx="9" ry="6" fill="#AACCFF" opacity="0.30"/> |
|
<!-- Head (large relative to body - baby proportions) --> |
|
<circle cx="50" cy="33" r="13" fill="#6699EE"/> |
|
<ellipse cx="48" cy="28" rx="8" ry="5" fill="#AACCFF" opacity="0.28"/> |
|
<!-- Ear --> |
|
<ellipse cx="47" cy="21" rx="8" ry="7" fill="#3355AA"/> |
|
<ellipse cx="47" cy="21" rx="5" ry="4" fill="#FFBBCC"/> |
|
<!-- Snout --> |
|
<ellipse cx="62" cy="36" rx="7" ry="5.5" fill="#3355AA"/> |
|
<ellipse cx="68" cy="36" rx="3.5" ry="2.8" fill="#CC5566"/> |
|
<!-- Eye (big, cute) --> |
|
<circle cx="56" cy="29" r="4.5" fill="#000033"/> |
|
<circle cx="57.5" cy="27.5" r="1.8" fill="white"/> |
|
<circle cx="55.5" cy="30.5" r="1.0" fill="white" opacity="0.4"/> |
|
<!-- Front foot --> |
|
<ellipse cx="52" cy="52" rx="7" ry="4" fill="#3355AA"/> |
|
<ellipse cx="57" cy="56" rx="4" ry="2.5" fill="#3355AA"/> |
|
<!-- Rear foot --> |
|
<ellipse cx="22" cy="52" rx="7" ry="4" fill="#3355AA"/> |
|
<ellipse cx="17" cy="56" rx="4" ry="2.5" fill="#3355AA"/>""" |
|
|
|
for d, a in CARD4_ANGLES.items(): |
|
tr = f' transform="rotate({a:.0f} 32 32)"' if a else "" |
|
save(f"BMP_BABY_{d}", |
|
mk(64, 64, f' <g{tr}>\n{_baby_body()}\n </g>', sw=128, sh=128)) |
|
|
|
# ─── BLOCKS (wall/barrier tiles, 0=intact to 3=heavily damaged) ────────────── |
|
|
|
BLOCK_CRACKS = [ |
|
"", |
|
' <line x1="18" y1="18" x2="28" y2="36" stroke="#660000" stroke-width="2.5" stroke-linecap="round"/>', |
|
' <line x1="18" y1="18" x2="28" y2="36" stroke="#660000" stroke-width="2.5" stroke-linecap="round"/>\n' |
|
' <line x1="40" y1="14" x2="34" y2="34" stroke="#660000" stroke-width="2.5" stroke-linecap="round"/>\n' |
|
' <line x1="14" y1="42" x2="30" y2="40" stroke="#660000" stroke-width="2.5" stroke-linecap="round"/>', |
|
' <line x1="18" y1="18" x2="28" y2="36" stroke="#660000" stroke-width="3" stroke-linecap="round"/>\n' |
|
' <line x1="40" y1="14" x2="34" y2="34" stroke="#660000" stroke-width="3" stroke-linecap="round"/>\n' |
|
' <line x1="14" y1="42" x2="30" y2="40" stroke="#660000" stroke-width="3" stroke-linecap="round"/>\n' |
|
' <line x1="44" y1="36" x2="36" y2="50" stroke="#660000" stroke-width="3" stroke-linecap="round"/>\n' |
|
' <line x1="22" y1="46" x2="32" y2="54" stroke="#660000" stroke-width="3" stroke-linecap="round"/>', |
|
] |
|
|
|
for i in range(4): |
|
inner_fill = "#FFDDDD" if i == 3 else "#FFFFFF" |
|
body = f"""\ |
|
<!-- Block damage level {i} --> |
|
<rect x="1" y="1" width="62" height="62" rx="6" fill="#CC3333"/> |
|
<rect x="3" y="3" width="58" height="58" rx="4" fill="#881111" opacity="0.45"/> |
|
<!-- Centre ring --> |
|
<circle cx="32" cy="32" r="23" fill="{inner_fill}" opacity="0.92"/> |
|
<circle cx="32" cy="32" r="18" fill="#CC3333" opacity="0.55"/> |
|
<!-- Corner highlight --> |
|
<line x1="5" y1="5" x2="16" y2="5" stroke="white" stroke-width="2" stroke-linecap="round" opacity="0.45"/> |
|
<line x1="5" y1="5" x2="5" y2="16" stroke="white" stroke-width="2" stroke-linecap="round" opacity="0.45"/> |
|
{BLOCK_CRACKS[i]}""" |
|
save(f"BMP_BLOCK_{i}", mk(64, 64, body, sw=128, sh=128)) |
|
|
|
# ─── BOMB ──────────────────────────────────────────────────────────────────── |
|
|
|
BOMB_DEFS = """\ |
|
<radialGradient id="bomb_g" cx="38%" cy="33%" r="60%"> |
|
<stop offset="0%" stop-color="#666666"/> |
|
<stop offset="55%" stop-color="#2A2A2A"/> |
|
<stop offset="100%" stop-color="#000000"/> |
|
</radialGradient>""" |
|
|
|
def bomb_body(number=None) -> str: |
|
glow = (f' <circle cx="32" cy="36" r="27" fill="#FF6600" opacity="0.28"/>\n' |
|
if number is not None else "") |
|
num_svg = "" |
|
if number is not None: |
|
num_svg = (f' <circle cx="32" cy="36" r="13" fill="#FF8800" opacity="0.55"/>\n' |
|
f' <text x="32" y="41" font-family="Arial Black,sans-serif" ' |
|
f'font-size="13" font-weight="bold" text-anchor="middle" ' |
|
f'fill="white" stroke="#660000" stroke-width="0.8">{number}</text>\n') |
|
return f"""{glow}\ |
|
<!-- Fuse --> |
|
<path d="M32,14 Q41,7 39,16 Q37,22 32,20" |
|
fill="none" stroke="#886633" stroke-width="2.5" stroke-linecap="round"/> |
|
<!-- Fuse spark --> |
|
<circle cx="39" cy="13" r="3.2" fill="#FFEE22"/> |
|
<circle cx="40" cy="12" r="1.5" fill="white" opacity="0.75"/> |
|
<!-- Bomb shell --> |
|
<circle cx="32" cy="36" r="22" fill="#1A1A1A"/> |
|
<circle cx="32" cy="36" r="21" fill="url(#bomb_g)"/> |
|
<!-- Shine --> |
|
<ellipse cx="23" cy="26" rx="7" ry="5" fill="white" opacity="0.22"/> |
|
{num_svg}""" |
|
|
|
save("BMP_BOMB0", mk(64, 64, bomb_body(), BOMB_DEFS, sw=128, sh=128)) |
|
for i in range(1, 5): |
|
save(f"BMP_BOMB{i}", mk(64, 64, bomb_body(i), BOMB_DEFS, sw=128, sh=128)) |
|
|
|
# ─── EXPLOSION ─────────────────────────────────────────────────────────────── |
|
|
|
EXPL_DEFS = """\ |
|
<radialGradient id="expl_c" cx="50%" cy="50%" r="50%"> |
|
<stop offset="0%" stop-color="#FFFFFF"/> |
|
<stop offset="30%" stop-color="#FFFF00"/> |
|
<stop offset="65%" stop-color="#FF8800"/> |
|
<stop offset="100%" stop-color="#FF2200" stop-opacity="0"/> |
|
</radialGradient> |
|
<radialGradient id="expl_d" cx="50%" cy="80%" r="60%"> |
|
<stop offset="0%" stop-color="#FFFF88"/> |
|
<stop offset="50%" stop-color="#FF8800"/> |
|
<stop offset="100%" stop-color="#FF2200" stop-opacity="0"/> |
|
</radialGradient>""" |
|
|
|
expl_center_body = """\ |
|
<!-- Outer glow --> |
|
<circle cx="32" cy="32" r="30" fill="#FF4400" opacity="0.28"/> |
|
<!-- 8-point star burst --> |
|
<polygon points="32,4 35,26 56,18 42,34 56,46 35,38 40,60 32,44 24,60 29,38 8,46 22,34 8,18 29,26" |
|
fill="#FFCC00" opacity="0.88"/> |
|
<!-- Core gradient disc --> |
|
<circle cx="32" cy="32" r="19" fill="url(#expl_c)"/>""" |
|
|
|
save("BMP_EXPLOSION", mk(64, 64, expl_center_body, EXPL_DEFS, sw=128, sh=128)) |
|
|
|
# directional explosion: flame pointing UP then rotated |
|
expl_dir_body = """\ |
|
<!-- Directional flame pointing UP then rotated --> |
|
<path d="M32,54 Q18,42 20,22 Q24,7 32,3 Q40,7 44,22 Q46,42 32,54Z" |
|
fill="url(#expl_d)"/> |
|
<path d="M32,50 Q24,40 26,24 Q29,14 32,10 Q35,14 38,24 Q40,40 32,50Z" |
|
fill="#FFEE88" opacity="0.75"/> |
|
<circle cx="32" cy="32" r="9" fill="#FFFF00" opacity="0.88"/>""" |
|
|
|
# UP=0°, RIGHT=90°, DOWN=180°, LEFT=270° (flame points toward direction, starts from centre) |
|
for d, a in {"UP": 0, "RIGHT": 90, "DOWN": 180, "LEFT": 270}.items(): |
|
tr = f' transform="rotate({a} 32 32)"' if a else "" |
|
save(f"BMP_EXPLOSION_{d}", |
|
mk(64, 64, f' <g{tr}>\n{expl_dir_body}\n </g>', |
|
EXPL_DEFS, sw=128, sh=128)) |
|
|
|
# ─── GAS ───────────────────────────────────────────────────────────────────── |
|
|
|
GAS_DEFS = """\ |
|
<radialGradient id="gas_c" cx="50%" cy="50%" r="50%"> |
|
<stop offset="0%" stop-color="#AAFF77"/> |
|
<stop offset="65%" stop-color="#44AA22"/> |
|
<stop offset="100%" stop-color="#44AA22" stop-opacity="0"/> |
|
</radialGradient> |
|
<radialGradient id="gas_d" cx="50%" cy="70%" r="60%"> |
|
<stop offset="0%" stop-color="#CCFF99"/> |
|
<stop offset="55%" stop-color="#44AA22"/> |
|
<stop offset="100%" stop-color="#44AA22" stop-opacity="0"/> |
|
</radialGradient>""" |
|
|
|
gas_center_body = """\ |
|
<!-- Gas puff cloud --> |
|
<circle cx="32" cy="32" r="29" fill="url(#gas_c)" opacity="0.80"/> |
|
<circle cx="22" cy="24" r="12" fill="#88EE44" opacity="0.65"/> |
|
<circle cx="40" cy="20" r="10" fill="#77DD33" opacity="0.60"/> |
|
<circle cx="46" cy="35" r="12" fill="#66CC22" opacity="0.62"/> |
|
<circle cx="24" cy="42" r="10" fill="#88EE44" opacity="0.58"/> |
|
<!-- Chemical symbol hint --> |
|
<text x="32" y="38" font-family="monospace" font-size="17" |
|
text-anchor="middle" fill="#0A3300" opacity="0.45" font-weight="bold">☠</text>""" |
|
|
|
save("BMP_GAS", mk(64, 64, gas_center_body, GAS_DEFS, sw=128, sh=128)) |
|
|
|
gas_dir_body = """\ |
|
<!-- Gas flowing upward (rotated for each direction) --> |
|
<ellipse cx="32" cy="32" rx="15" ry="27" fill="url(#gas_d)" opacity="0.75"/> |
|
<circle cx="24" cy="16" r="9" fill="#88DD44" opacity="0.60"/> |
|
<circle cx="40" cy="11" r="8" fill="#77CC33" opacity="0.55"/> |
|
<circle cx="32" cy="6" r="10" fill="#66BB22" opacity="0.50"/> |
|
<circle cx="32" cy="42" r="10" fill="#44AA22" opacity="0.45"/>""" |
|
|
|
for d, a in {"UP": 0, "RIGHT": 90, "DOWN": 180, "LEFT": 270}.items(): |
|
tr = f' transform="rotate({a} 32 32)"' if a else "" |
|
save(f"BMP_GAS_{d}", |
|
mk(64, 64, f' <g{tr}>\n{gas_dir_body}\n </g>', |
|
GAS_DEFS, sw=128, sh=128)) |
|
|
|
# ─── TUNNEL ────────────────────────────────────────────────────────────────── |
|
|
|
save("BMP_TUNNEL", mk(64, 64, """\ |
|
<!-- Stone/earth surround --> |
|
<rect x="0" y="0" width="64" height="64" rx="4" fill="#888888"/> |
|
<rect x="2" y="2" width="60" height="60" rx="3" fill="#AAAAAA"/> |
|
<!-- Ground ring --> |
|
<ellipse cx="32" cy="40" rx="26" ry="17" fill="#777777"/> |
|
<ellipse cx="32" cy="40" rx="22" ry="13" fill="#333333"/> |
|
<ellipse cx="32" cy="40" rx="17" ry="10" fill="#1A1A1A"/> |
|
<ellipse cx="32" cy="40" rx="12" ry="7" fill="#000000"/> |
|
<!-- Ground highlight --> |
|
<ellipse cx="26" cy="30" rx="9" ry="5" fill="white" opacity="0.22"/> |
|
<!-- Stone texture flecks --> |
|
<rect x="8" y="8" width="5" height="5" rx="1" fill="#999999" opacity="0.5"/> |
|
<rect x="50" y="12" width="4" height="4" rx="1" fill="#999999" opacity="0.5"/> |
|
<rect x="10" y="50" width="4" height="4" rx="1" fill="#999999" opacity="0.5"/> |
|
<rect x="50" y="48" width="5" height="5" rx="1" fill="#999999" opacity="0.5"/>""", |
|
sw=128, sh=128)) |
|
|
|
# ─── NUCLEAR ───────────────────────────────────────────────────────────────── |
|
|
|
def _trefoil_blade(cx, cy, r_in, r_out, angle_deg): |
|
a = math.radians(angle_deg) |
|
hw = math.radians(30) # half-width of each blade sector (60° wide blades, 60° gaps) |
|
a1, a2 = a - hw, a + hw |
|
ix1, iy1 = cx + r_in * math.cos(a1), cy + r_in * math.sin(a1) |
|
ix2, iy2 = cx + r_in * math.cos(a2), cy + r_in * math.sin(a2) |
|
ox1, oy1 = cx + r_out * math.cos(a1), cy + r_out * math.sin(a1) |
|
ox2, oy2 = cx + r_out * math.cos(a2), cy + r_out * math.sin(a2) |
|
return (f"M {ix1:.2f},{iy1:.2f} " |
|
f"L {ox1:.2f},{oy1:.2f} " |
|
f"A {r_out},{r_out} 0 0,1 {ox2:.2f},{oy2:.2f} " |
|
f"L {ix2:.2f},{iy2:.2f} " |
|
f"A {r_in},{r_in} 0 0,0 {ix1:.2f},{iy1:.2f} Z") |
|
|
|
blades = [_trefoil_blade(32, 32, 8, 27, a) for a in [-90, 30, 150]] |
|
blade_paths = "\n ".join(f'<path d="{p}" fill="black"/>' for p in blades) |
|
|
|
save("BMP_NUCLEAR", mk(64, 64, f"""\ |
|
<!-- Yellow background disc --> |
|
<circle cx="32" cy="32" r="31" fill="#FFDD00"/> |
|
<circle cx="32" cy="32" r="29" fill="#FFE800"/> |
|
<!-- Three trefoil blades --> |
|
{blade_paths} |
|
<!-- Centre yellow circle --> |
|
<circle cx="32" cy="32" r="7" fill="#FFDD00"/> |
|
<!-- Centre black dot --> |
|
<circle cx="32" cy="32" r="4" fill="black"/> |
|
<!-- Outer black ring --> |
|
<circle cx="32" cy="32" r="31" fill="none" stroke="black" stroke-width="2"/>""", |
|
sw=128, sh=128)) |
|
|
|
# ─── POISON (skull & crossbones) ───────────────────────────────────────────── |
|
|
|
save("BMP_POISON", mk(64, 64, """\ |
|
<!-- Dark background --> |
|
<rect x="0" y="0" width="64" height="64" fill="#111111"/> |
|
<!-- Skull cranium --> |
|
<ellipse cx="32" cy="25" rx="19" ry="17" fill="white"/> |
|
<!-- Skull jaw --> |
|
<rect x="22" y="34" width="20" height="11" rx="4" fill="white"/> |
|
<!-- Eye sockets --> |
|
<ellipse cx="24" cy="26" rx="5.5" ry="6.5" fill="#111111"/> |
|
<ellipse cx="40" cy="26" rx="5.5" ry="6.5" fill="#111111"/> |
|
<!-- Nose hole --> |
|
<path d="M29,34 L32,30 L35,34 Z" fill="#111111"/> |
|
<!-- Teeth gaps --> |
|
<rect x="24" y="38" width="4" height="6" rx="2" fill="#111111"/> |
|
<rect x="30" y="38" width="4" height="6" rx="2" fill="#111111"/> |
|
<rect x="36" y="38" width="4" height="6" rx="2" fill="#111111"/> |
|
<!-- Crossbones: two crossing bones --> |
|
<line x1="8" y1="58" x2="56" y2="52" stroke="white" stroke-width="5" stroke-linecap="round"/> |
|
<line x1="56" y1="58" x2="8" y2="52" stroke="white" stroke-width="5" stroke-linecap="round"/> |
|
<!-- Bone knobs --> |
|
<circle cx="8" cy="58" r="5" fill="white"/> |
|
<circle cx="56" cy="58" r="5" fill="white"/> |
|
<circle cx="8" cy="52" r="5" fill="white"/> |
|
<circle cx="56" cy="52" r="5" fill="white"/>""", |
|
sw=128, sh=128)) |
|
|
|
# ─── MINE ──────────────────────────────────────────────────────────────────── |
|
|
|
MINE_DEFS = """\ |
|
<radialGradient id="mine_g" cx="40%" cy="35%" r="60%"> |
|
<stop offset="0%" stop-color="#777777"/> |
|
<stop offset="100%" stop-color="#222222"/> |
|
</radialGradient>""" |
|
|
|
spike_lines = "\n ".join( |
|
f'<line x1="32" y1="32" ' |
|
f'x2="{32+28*math.cos(math.radians(a)):.1f}" ' |
|
f'y2="{32+28*math.sin(math.radians(a)):.1f}" ' |
|
f'stroke="#2A2A2A" stroke-width="4" stroke-linecap="round"/>' |
|
for a in range(0, 360, 45) |
|
) |
|
spike_caps = "\n ".join( |
|
f'<circle cx="{32+28*math.cos(math.radians(a)):.1f}" ' |
|
f'cy="{32+28*math.sin(math.radians(a)):.1f}" r="4.5" fill="#1E1E1E"/>' |
|
for a in range(0, 360, 45) |
|
) |
|
|
|
save("mine", mk(64, 64, f"""\ |
|
<!-- Spikes --> |
|
{spike_lines} |
|
{spike_caps} |
|
<!-- Main body --> |
|
<circle cx="32" cy="32" r="18" fill="url(#mine_g)"/> |
|
<!-- Shine --> |
|
<ellipse cx="26" cy="25" rx="5.5" ry="3.8" fill="white" opacity="0.20"/> |
|
<!-- Red trigger button --> |
|
<circle cx="32" cy="20" r="4.8" fill="#CC1111"/> |
|
<circle cx="31" cy="19" r="2.2" fill="#FF5555" opacity="0.70"/>""", |
|
MINE_DEFS, sw=128, sh=128)) |
|
|
|
# ─── GRASS TILES (4 players × 4 variants) ──────────────────────────────────── |
|
|
|
# Blade coords per variant: list of (x1,y1,x2,y2) |
|
_BLADE_SETS = { |
|
1: [(12, 64, 14, 44), (20, 64, 22, 40), (10, 64, 6, 43)], |
|
2: [(30, 64, 28, 38), (24, 64, 20, 42), (38, 64, 40, 42)], |
|
3: [(48, 64, 50, 40), (44, 64, 42, 44), (54, 64, 58, 42)], |
|
4: [(20, 64, 16, 40), (36, 64, 38, 38), (44, 64, 48, 44), (10, 64, 12, 46)], |
|
} |
|
|
|
def grass_svg(player: int, variant: int) -> str: |
|
bg, ac, dk, br = TC[player] |
|
blades = "\n".join( |
|
f' <line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" ' |
|
f'stroke="{dk}" stroke-width="2.5" stroke-linecap="round"/>' |
|
for x1, y1, x2, y2 in _BLADE_SETS.get(variant, _BLADE_SETS[1]) |
|
) |
|
# variant speckle |
|
sx = variant * 13 % 50 + 6 |
|
sy = variant * 11 % 50 + 6 |
|
# Player 3: distinctive diagonal hatch pattern |
|
p3_hatch = "" |
|
p3_defs = "" |
|
if player == 3: |
|
pid = f"hatch3v{variant}" |
|
p3_defs = f''' |
|
<pattern id="{pid}" width="8" height="8" patternUnits="userSpaceOnUse" |
|
patternTransform="rotate(45)"> |
|
<line x1="0" y1="0" x2="0" y2="8" stroke="{dk}" stroke-width="3" |
|
stroke-opacity="0.55"/> |
|
</pattern>''' |
|
p3_hatch = f' <rect x="0" y="0" width="64" height="64" fill="url(#{pid})"/>' |
|
body = f"""\ |
|
<!-- Grass tile P{player} V{variant} --> |
|
<rect x="0" y="0" width="64" height="64" fill="{bg}"/> |
|
<rect x="0" y="0" width="64" height="64" fill="{ac}" opacity="0.20"/> |
|
{p3_hatch} |
|
<!-- Texture speckles --> |
|
<circle cx="{sx}" cy="{sy}" r="2.5" fill="{dk}" opacity="0.30"/> |
|
<circle cx="{64-sx}" cy="{64-sy}" r="2" fill="{br}" opacity="0.25"/> |
|
<!-- Grass blades --> |
|
{blades}""" |
|
return mk(64, 64, body, p3_defs, sw=128, sh=128) |
|
|
|
for p in range(1, 5): |
|
for v in range(1, 5): |
|
save(f"BMP_{p}_GRASS_{v}", grass_svg(p, v)) |
|
|
|
# ─── FLOWER TILES (4 players × 4 variants) ─────────────────────────────────── |
|
|
|
_FLOWER_COLORS = { |
|
1: ("#FFFFFF", "#FFEE88"), # white petals, yellow centre |
|
2: ("#FFCCAA", "#FF8800"), # peach petals, orange centre |
|
3: ("#CCDDFF", "#FFDD00"), # pale-blue petals, gold centre |
|
4: ("#FFAACC", "#FF4466"), # pink petals, hot-pink centre |
|
} |
|
|
|
def flower_svg(player: int, variant: int) -> str: |
|
bg, ac, dk, br = TC[player] |
|
pc, cc = _FLOWER_COLORS[variant] |
|
# offset flower position per variant |
|
ox = [32, 22, 42, 28][variant - 1] |
|
oy = [32, 36, 28, 40][variant - 1] |
|
# 8 petals |
|
petals = "\n".join( |
|
f' <ellipse cx="{ox + 11*math.cos(math.radians(a)):.1f}" ' |
|
f'cy="{oy + 11*math.sin(math.radians(a)):.1f}" ' |
|
f'rx="7" ry="4.5" fill="{pc}" ' |
|
f'transform="rotate({a:.0f} ' |
|
f'{ox + 11*math.cos(math.radians(a)):.1f} ' |
|
f'{oy + 11*math.sin(math.radians(a)):.1f})"/>' |
|
for a in range(0, 360, 45) |
|
) |
|
body = f"""\ |
|
<!-- Flower tile P{player} V{variant} --> |
|
<rect x="0" y="0" width="64" height="64" fill="{bg}"/> |
|
{petals} |
|
<!-- Flower centre --> |
|
<circle cx="{ox}" cy="{oy}" r="8" fill="{cc}"/> |
|
<circle cx="{ox-2}" cy="{oy-2}" r="3" fill="white" opacity="0.45"/>""" |
|
return mk(64, 64, body, sw=128, sh=128) |
|
|
|
for p in range(1, 5): |
|
for v in range(1, 5): |
|
save(f"BMP_{p}_FLOWER_{v}", flower_svg(p, v)) |
|
|
|
# ─── CAVE TILES (player-colored cave entrance, 4 cardinal dirs) ────────────── |
|
|
|
def cave_tile_svg(player: int, direction: str) -> str: |
|
"""Rat entering cave – large dark hole with player colour rim, facing dir.""" |
|
bg, ac, dk, br = TC[player] |
|
angle = CARD4_ANGLES[direction] |
|
# Opening faces the direction the rat exits toward |
|
tr = f' transform="rotate({angle} 32 32)"' if angle else "" |
|
body = f"""\ |
|
<!-- Cave tile P{player} dir={direction} --> |
|
<rect x="0" y="0" width="64" height="64" fill="{bg}"/> |
|
<g{tr}> |
|
<!-- Arch stone surround --> |
|
<ellipse cx="32" cy="44" rx="24" ry="18" fill="{dk}"/> |
|
<!-- Tunnel mouth shadow --> |
|
<ellipse cx="32" cy="44" rx="19" ry="14" fill="#1A1A1A"/> |
|
<!-- Dark depths --> |
|
<ellipse cx="32" cy="44" rx="14" ry="10" fill="#000000"/> |
|
<!-- Player-coloured rim highlight --> |
|
<ellipse cx="32" cy="44" rx="24" ry="18" fill="none" |
|
stroke="{ac}" stroke-width="2"/> |
|
<!-- Ground highlight --> |
|
<ellipse cx="26" cy="34" rx="7" ry="4" fill="white" opacity="0.18"/> |
|
</g>""" |
|
return mk(64, 64, body, sw=128, sh=128) |
|
|
|
for p in range(1, 5): |
|
for d in ("DOWN", "UP", "LEFT", "RIGHT"): |
|
save(f"BMP_{p}_CAVE_{d}", cave_tile_svg(p, d)) |
|
|
|
# ─── ARROWS (UI elements) ───────────────────────────────────────────────────── |
|
|
|
def arrow_svg(direction: str) -> str: |
|
"""Bold arrow; designed pointing UP, rotated.""" |
|
a = {"UP": 0, "DOWN": 180, "RIGHT": 90, "LEFT": 270}[direction] |
|
tr = f' transform="rotate({a} 32 32)"' if a else "" |
|
body = f"""\ |
|
<g{tr}> |
|
<!-- Arrow pointing UP --> |
|
<!-- Shaft --> |
|
<rect x="24" y="30" width="16" height="26" rx="3" fill="#444444"/> |
|
<!-- Head --> |
|
<polygon points="32,6 50,30 38,30 38,30 26,30 14,30" fill="#444444"/> |
|
<!-- Highlights --> |
|
<line x1="24" y1="30" x2="38" y2="30" stroke="white" stroke-width="1.2" |
|
opacity="0.35" stroke-linecap="round"/> |
|
<line x1="14" y1="30" x2="32" y2="6" stroke="white" stroke-width="1" |
|
opacity="0.22" stroke-linecap="round"/> |
|
</g>""" |
|
return mk(64, 64, body, sw=128, sh=128) |
|
|
|
for d in ("DOWN", "UP", "LEFT", "RIGHT"): |
|
save(f"BMP_ARROW_{d}", arrow_svg(d)) |
|
|
|
# ─── TITLE ("Rats!") 100×40 original → 200×80 viewBox ─────────────────────── |
|
|
|
save("BMP_TITLE", mk(200, 80, """\ |
|
<!-- Title card --> |
|
<rect x="0" y="0" width="200" height="80" fill="#FFE800"/> |
|
<!-- Shadow text --> |
|
<text x="103" y="61" font-family="Impact,Arial Black,sans-serif" font-size="58" |
|
font-weight="bold" text-anchor="middle" fill="#8B0000" opacity="0.35">Rats!</text> |
|
<!-- Main text (red) --> |
|
<text x="100" y="58" font-family="Impact,Arial Black,sans-serif" font-size="58" |
|
font-weight="bold" text-anchor="middle" fill="#CC1111" |
|
stroke="#880000" stroke-width="2" paint-order="stroke">Rats!</text>""", |
|
sw=400, sh=160)) |
|
|
|
# ─── VERMINATORS banner ─────────────────────────────────────────────────────── |
|
|
|
save("BMP_VERMINATORS", mk(200, 32, """\ |
|
<!-- Score banner --> |
|
<rect x="0" y="0" width="200" height="32" fill="#001122"/> |
|
<rect x="1" y="1" width="198" height="30" fill="#002244"/> |
|
<!-- Cyan text --> |
|
<text x="100" y="23" font-family="Impact,Arial Black,sans-serif" font-size="20" |
|
font-weight="bold" text-anchor="middle" fill="#00EEFF" |
|
stroke="#004466" stroke-width="1" paint-order="stroke">Top Verminators:</text>""", |
|
sw=400, sh=64)) |
|
|
|
# ─── WEWIN screen ───────────────────────────────────────────────────────────── |
|
# Original showed a blue/purple mouse character with wings or limbs |
|
|
|
save("BMP_WEWIN", mk(64, 64, f"""\ |
|
<!-- WE WIN screen – happy rat dancing --> |
|
<rect x="0" y="0" width="64" height="64" fill="#1A0033"/> |
|
<!-- Stars --> |
|
<circle cx="10" cy="10" r="2" fill="#FFFF88" opacity="0.8"/> |
|
<circle cx="54" cy="8" r="1.5" fill="#FFFF88" opacity="0.7"/> |
|
<circle cx="58" cy="30" r="1.8" fill="#FFFF88" opacity="0.75"/> |
|
<circle cx="6" cy="50" r="1.5" fill="#FFFF88" opacity="0.7"/> |
|
<!-- Confetti --> |
|
<rect x="12" y="18" width="4" height="4" rx="1" fill="#FF4444" transform="rotate(20 14 20)"/> |
|
<rect x="46" y="14" width="4" height="4" rx="1" fill="#44FF44" transform="rotate(-15 48 16)"/> |
|
<rect x="18" y="54" width="3" height="3" rx="1" fill="#FFFF44" transform="rotate(30 19 55)"/> |
|
<rect x="50" y="50" width="3" height="3" rx="1" fill="#44AAFF" transform="rotate(-10 51 51)"/> |
|
<!-- Happy rat (blue/purple) --> |
|
<!-- Tail --> |
|
<path d="M14,42 Q6,38 6,30 Q6,22 14,20" fill="none" stroke="#440088" |
|
stroke-width="2.5" stroke-linecap="round"/> |
|
<!-- Body --> |
|
<ellipse cx="33" cy="42" rx="18" ry="12" fill="#6644BB"/> |
|
<ellipse cx="33" cy="38" rx="13" ry="7" fill="#8866DD" opacity="0.35"/> |
|
<!-- Arms raised (celebrating) - left --> |
|
<path d="M20,38 Q10,28 14,20" fill="none" stroke="#5533AA" |
|
stroke-width="5" stroke-linecap="round"/> |
|
<ellipse cx="13" cy="19" rx="5" ry="4" fill="#5533AA"/> |
|
<!-- Arms raised - right --> |
|
<path d="M46,38 Q56,28 52,20" fill="none" stroke="#5533AA" |
|
stroke-width="5" stroke-linecap="round"/> |
|
<ellipse cx="53" cy="19" rx="5" ry="4" fill="#5533AA"/> |
|
<!-- Head --> |
|
<circle cx="46" cy="33" r="13" fill="#6644BB"/> |
|
<ellipse cx="44" cy="28" rx="8" ry="5" fill="#8866DD" opacity="0.28"/> |
|
<!-- Ear --> |
|
<ellipse cx="43" cy="21" rx="7" ry="6.5" fill="#5533AA"/> |
|
<ellipse cx="43" cy="21" rx="4.5" ry="4" fill="#FFBBCC"/> |
|
<!-- Snout --> |
|
<ellipse cx="58" cy="36" rx="7" ry="5.5" fill="#5533AA"/> |
|
<ellipse cx="64" cy="36" rx="3.5" ry="2.8" fill="#CC5566"/> |
|
<!-- Big happy eye --> |
|
<circle cx="52" cy="29" r="4" fill="#220055"/> |
|
<circle cx="53.5" cy="27.5" r="1.8" fill="white"/> |
|
<!-- Smile (curved line) --> |
|
<path d="M55,36 Q58,39 61,36" fill="none" stroke="#220055" |
|
stroke-width="1.5" stroke-linecap="round"/> |
|
<!-- Legs --> |
|
<ellipse cx="50" cy="52" rx="7" ry="4" fill="#5533AA"/> |
|
<ellipse cx="22" cy="52" rx="7" ry="4" fill="#5533AA"/>""", |
|
sw=128, sh=128)) |
|
|
|
# ─── START tiles ───────────────────────────────────────────────────────────── |
|
|
|
# Original revealed unique art per player: |
|
# P1: large white flower on green background |
|
# P2: cactus on yellow background |
|
# P3: penguin on gray-blue hatched background |
|
# P4: flame pattern on red background |
|
|
|
def start_svg(player: int, state: str = "normal") -> str: |
|
bg, ac, dk, br = TC[player] |
|
opacity = "0.45" if state == "shaded" else "1" |
|
border_w = "5" if state == "down" else "4" |
|
|
|
decorations = { |
|
1: f"""\ |
|
<!-- Large white flower --> |
|
{''.join( |
|
f'<ellipse cx="{32+14*math.cos(math.radians(a)):.1f}" ' |
|
f'cy="{32+14*math.sin(math.radians(a)):.1f}" ' |
|
f'rx="9" ry="6" fill="white" ' |
|
f'transform="rotate({a:.0f} ' |
|
f'{32+14*math.cos(math.radians(a)):.1f} ' |
|
f'{32+14*math.sin(math.radians(a)):.1f})"/>' |
|
for a in range(0, 360, 45) |
|
)} |
|
<circle cx="32" cy="32" r="10" fill="#FFEE88"/> |
|
<circle cx="30" cy="30" r="4" fill="white" opacity="0.50"/>""", |
|
|
|
2: f"""\ |
|
<!-- Cactus --> |
|
<!-- Main trunk --> |
|
<rect x="27" y="20" width="10" height="30" rx="5" fill="#228822"/> |
|
<!-- Left arm --> |
|
<rect x="14" y="28" width="16" height="8" rx="4" fill="#228822"/> |
|
<rect x="14" y="22" width="8" height="14" rx="4" fill="#228822"/> |
|
<!-- Right arm --> |
|
<rect x="34" y="32" width="16" height="8" rx="4" fill="#228822"/> |
|
<rect x="42" y="26" width="8" height="14" rx="4" fill="#228822"/> |
|
<!-- Spines --> |
|
<line x1="27" y1="26" x2="22" y2="23" stroke="#AAFFAA" stroke-width="1.2" stroke-linecap="round"/> |
|
<line x1="37" y1="30" x2="42" y2="27" stroke="#AAFFAA" stroke-width="1.2" stroke-linecap="round"/> |
|
<!-- Flower on top --> |
|
<circle cx="32" cy="18" r="5" fill="#FF4488"/> |
|
<circle cx="32" cy="18" r="3" fill="#FFEE00"/>""", |
|
|
|
3: f"""\ |
|
<!-- Penguin --> |
|
<!-- Body --> |
|
<ellipse cx="32" cy="42" rx="14" ry="16" fill="#111122"/> |
|
<ellipse cx="32" cy="44" rx="9" ry="11" fill="white"/> |
|
<!-- Head --> |
|
<circle cx="32" cy="26" r="12" fill="#111122"/> |
|
<!-- Eyes --> |
|
<circle cx="27" cy="24" r="3.5" fill="white"/> |
|
<circle cx="37" cy="24" r="3.5" fill="white"/> |
|
<circle cx="28" cy="24" r="2" fill="#111122"/> |
|
<circle cx="38" cy="24" r="2" fill="#111122"/> |
|
<circle cx="28.5" cy="23.5" r="0.8" fill="white"/> |
|
<circle cx="38.5" cy="23.5" r="0.8" fill="white"/> |
|
<!-- Beak --> |
|
<polygon points="32,30 29,34 35,34" fill="#FF8800"/> |
|
<!-- Wings --> |
|
<ellipse cx="18" cy="40" rx="7" ry="12" fill="#111122" transform="rotate(-15 18 40)"/> |
|
<ellipse cx="46" cy="40" rx="7" ry="12" fill="#111122" transform="rotate(15 46 40)"/> |
|
<!-- Feet --> |
|
<ellipse cx="26" cy="58" rx="7" ry="3" fill="#FF8800"/> |
|
<ellipse cx="38" cy="58" rx="7" ry="3" fill="#FF8800"/>""", |
|
|
|
4: f"""\ |
|
<!-- Flame pattern --> |
|
<path d="M32,58 Q18,44 24,28 Q28,16 32,10 Q36,16 40,28 Q46,44 32,58Z" |
|
fill="#FF6600"/> |
|
<path d="M32,54 Q24,42 28,28 Q30,20 32,16 Q34,20 36,28 Q40,42 32,54Z" |
|
fill="#FFBB00"/> |
|
<path d="M32,48 Q27,38 30,28 Q31,22 32,20 Q33,22 34,28 Q37,38 32,48Z" |
|
fill="#FFEE55"/> |
|
<!-- Small side flames --> |
|
<path d="M22,50 Q14,38 20,26 Q24,18 24,26 Q22,36 28,44Z" |
|
fill="#FF4400" opacity="0.75"/> |
|
<path d="M42,50 Q50,38 44,26 Q40,18 40,26 Q42,36 36,44Z" |
|
fill="#FF4400" opacity="0.75"/> |
|
<!-- Core bright --> |
|
<circle cx="32" cy="36" r="8" fill="#FFFFFF" opacity="0.30"/>""", |
|
} |
|
|
|
body = f"""\ |
|
<!-- Start tile P{player} ({state}) --> |
|
<rect x="0" y="0" width="64" height="64" fill="{bg}" opacity="{opacity}"/> |
|
<!-- Decorative border --> |
|
<rect x="1" y="1" width="62" height="62" rx="3" fill="none" |
|
stroke="{ac}" stroke-width="{border_w}" opacity="{opacity}"/> |
|
<!-- Inner frame shadow --> |
|
<rect x="4" y="4" width="56" height="56" rx="2" fill="none" |
|
stroke="{dk}" stroke-width="2" opacity="{float(opacity)*0.5:.2f}"/> |
|
{decorations[player]}""" |
|
return mk(64, 64, body, sw=128, sh=128) |
|
|
|
for p in range(1, 5): |
|
save(f"BMP_START_{p}", start_svg(p, "normal")) |
|
save(f"BMP_START_{p}_DOWN", start_svg(p, "down")) |
|
save(f"BMP_START_{p}_SHADED", start_svg(p, "shaded")) |
|
|
|
# ─── BONUS numbers ──────────────────────────────────────────────────────────── |
|
|
|
BONUS_COLORS = { |
|
5: ("#FFEEAA", "#AA8800"), |
|
10: ("#AAFFAA", "#008800"), |
|
20: ("#AADDFF", "#0044AA"), |
|
40: ("#FFAACC", "#AA0044"), |
|
80: ("#FFCCAA", "#AA4400"), |
|
160: ("#DDAAFF", "#6600AA"), |
|
} |
|
|
|
for val, (fg, sh) in BONUS_COLORS.items(): |
|
fs = 28 if val < 100 else 22 |
|
ty = 34 + (33 - fs) |
|
body = f"""\ |
|
<!-- Bonus +{val} --> |
|
<rect x="0" y="0" width="64" height="64" rx="6" fill="#000000" opacity="0.55"/> |
|
<!-- Shadow text --> |
|
<text x="34" y="{ty}" font-family="Arial Black,Impact,sans-serif" |
|
font-size="{fs}" font-weight="bold" text-anchor="middle" |
|
fill="{sh}">{val}</text> |
|
<!-- Main text --> |
|
<text x="32" y="{ty}" font-family="Arial Black,Impact,sans-serif" |
|
font-size="{fs}" font-weight="bold" text-anchor="middle" |
|
fill="{fg}">{val}</text>""" |
|
save(f"BMP_BONUS_{val}", mk(64, 64, body, sw=128, sh=128)) |
|
|
|
# ─── done ───────────────────────────────────────────────────────────────────── |
|
|
|
files = [f for f in os.listdir(OUT_DIR) if f.endswith(".svg")] |
|
print(f"Generated {len(files)} SVG files in {OUT_DIR}")
|
|
|