#!/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 \n{defs}\n " if defs else "" return (f'\n' f'' f'{defs_block}\n{body}\n\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"""\ """ 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' \n{body}\n ', 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 """\ """ def _explosion_overlay() -> str: return """\ """ def _cave_overlay() -> str: return """\ """ 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' \n{body}\n ', 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' \n{_rat_body(bc, hi, sh, ec, tc)}\n ' 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 """\ """ save("BMP_MALE", mk(64, 64, f' \n{_rat_body(MB, MH, MS, ME, MT)}\n \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' \n{_rat_body(MB, MH, MS, ME, MT)}\n ', sw=128, sh=128)) # ─── FEMALE enemy rat ──────────────────────────────────────────────────────── FB = "#DD55BB"; FH = "#FF99DD"; FS = "#992288"; FE = "#FFCCEE"; FT = "#661155" def _female_icon() -> str: return """\ """ save("BMP_FEMALE", mk(64, 64, f' \n{_rat_body(FB, FH, FS, FE, FT)}\n \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' \n{_rat_body(FB, FH, FS, FE, FT)}\n ', sw=128, sh=128)) # ─── BABY rat (rounder, cuter) ─────────────────────────────────────────────── def _baby_body() -> str: return """\ """ 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' \n{_baby_body()}\n ', sw=128, sh=128)) # ─── BLOCKS (wall/barrier tiles, 0=intact to 3=heavily damaged) ────────────── BLOCK_CRACKS = [ "", ' ', ' \n' ' \n' ' ', ' \n' ' \n' ' \n' ' \n' ' ', ] for i in range(4): inner_fill = "#FFDDDD" if i == 3 else "#FFFFFF" body = f"""\ {BLOCK_CRACKS[i]}""" save(f"BMP_BLOCK_{i}", mk(64, 64, body, sw=128, sh=128)) # ─── BOMB ──────────────────────────────────────────────────────────────────── BOMB_DEFS = """\ """ def bomb_body(number=None) -> str: glow = (f' \n' if number is not None else "") num_svg = "" if number is not None: num_svg = (f' \n' f' {number}\n') return f"""{glow}\ {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 = """\ """ expl_center_body = """\ """ 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 = """\ """ # 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' \n{expl_dir_body}\n ', EXPL_DEFS, sw=128, sh=128)) # ─── GAS ───────────────────────────────────────────────────────────────────── GAS_DEFS = """\ """ gas_center_body = """\ """ save("BMP_GAS", mk(64, 64, gas_center_body, GAS_DEFS, sw=128, sh=128)) gas_dir_body = """\ """ 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' \n{gas_dir_body}\n ', GAS_DEFS, sw=128, sh=128)) # ─── TUNNEL ────────────────────────────────────────────────────────────────── save("BMP_TUNNEL", mk(64, 64, """\ """, 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'' for p in blades) save("BMP_NUCLEAR", mk(64, 64, f"""\ {blade_paths} """, sw=128, sh=128)) # ─── POISON (skull & crossbones) ───────────────────────────────────────────── save("BMP_POISON", mk(64, 64, """\ """, sw=128, sh=128)) # ─── MINE ──────────────────────────────────────────────────────────────────── MINE_DEFS = """\ """ spike_lines = "\n ".join( f'' for a in range(0, 360, 45) ) spike_caps = "\n ".join( f'' for a in range(0, 360, 45) ) save("mine", mk(64, 64, f"""\ {spike_lines} {spike_caps} """, 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' ' 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''' ''' p3_hatch = f' ' body = f"""\ {p3_hatch} {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' ' for a in range(0, 360, 45) ) body = f"""\ {petals} """ 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"""\ """ 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"""\ """ 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, """\ Rats! Rats!""", sw=400, sh=160)) # ─── VERMINATORS banner ─────────────────────────────────────────────────────── save("BMP_VERMINATORS", mk(200, 32, """\ Top Verminators:""", sw=400, sh=64)) # ─── WEWIN screen ───────────────────────────────────────────────────────────── # Original showed a blue/purple mouse character with wings or limbs save("BMP_WEWIN", mk(64, 64, f"""\ """, 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"""\ {''.join( f'' for a in range(0, 360, 45) )} """, 2: f"""\ """, 3: f"""\ """, 4: f"""\ """, } body = f"""\ {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"""\ {val} {val}""" 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}")