Add new PNG images for clean and preview outputs
- Added `image_clean.png` to the output directory for the clean image representation. - Added `image_clean_preview.png` for the preview of the clean image. - Introduced `image_svg_clean.png` for the SVG clean image representation.
@@ -0,0 +1,864 @@
|
||||
#!/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}")
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert an image file into a JSON matrix of RGBA pixels."
|
||||
)
|
||||
parser.add_argument("input_image", type=Path, help="Source image path")
|
||||
parser.add_argument("output_json", type=Path, help="Destination JSON path")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def image_to_matrix(image_path: Path) -> dict:
|
||||
with Image.open(image_path) as image:
|
||||
rgba_image = image.convert("RGBA")
|
||||
width, height = rgba_image.size
|
||||
pixel_map = rgba_image.load()
|
||||
rows = [
|
||||
[list(pixel_map[x, y]) for x in range(width)]
|
||||
for y in range(height)
|
||||
]
|
||||
|
||||
return {
|
||||
"source": image_path.name,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"mode": "RGBA",
|
||||
"pixels": rows,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
data = image_to_matrix(args.input_image)
|
||||
args.output_json.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output_json.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
||||
print(
|
||||
f"Wrote {args.output_json} with {data['width']}x{data['height']} RGBA pixels"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert a JSON RGBA matrix back into a PNG image."
|
||||
)
|
||||
parser.add_argument("input_json", type=Path, help="Source JSON path")
|
||||
parser.add_argument("output_png", type=Path, help="Destination PNG path")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_matrix(json_path: Path) -> tuple[int, int, list[tuple[int, int, int, int]]]:
|
||||
data = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
width = int(data["width"])
|
||||
height = int(data["height"])
|
||||
rows = data["pixels"]
|
||||
|
||||
if len(rows) != height:
|
||||
raise ValueError(f"Expected {height} rows, found {len(rows)}")
|
||||
|
||||
flat_pixels: list[tuple[int, int, int, int]] = []
|
||||
for row_index, row in enumerate(rows):
|
||||
if len(row) != width:
|
||||
raise ValueError(
|
||||
f"Expected row {row_index} to contain {width} pixels, found {len(row)}"
|
||||
)
|
||||
for pixel in row:
|
||||
if len(pixel) != 4:
|
||||
raise ValueError("Each pixel must contain 4 RGBA channels")
|
||||
flat_pixels.append(tuple(int(channel) for channel in pixel))
|
||||
|
||||
return width, height, flat_pixels
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
width, height, pixels = load_matrix(args.input_json)
|
||||
image = Image.new("RGBA", (width, height))
|
||||
image.putdata(pixels)
|
||||
args.output_png.parent.mkdir(parents=True, exist_ok=True)
|
||||
image.save(args.output_png, format="PNG")
|
||||
print(f"Wrote {args.output_png} with {width}x{height} RGBA pixels")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
After Width: | Height: | Size: 421 B |
|
After Width: | Height: | Size: 804 B |
|
After Width: | Height: | Size: 815 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 885 B |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 781 B |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.2 KiB |