Files
mice/tools/generate_weapon_sprites.py
T

363 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Generate detailed 64×64 pixel art weapon sprites for mice game.
Logical grid: 16×16, each logical pixel = 4×4 real pixels → 64×64 output.
Sprites generated:
BMP_BOMB0 … BMP_BOMB4 — bomb fuse animation (0=long fuse, 4=spark)
BMP_GAS — toxic gas cloud (symmetric)
BMP_GAS_LEFT/RIGHT/UP/DOWN — gas half-sprites (directional clip)
BMP_EXPLOSION — central starburst
BMP_EXPLOSION_LEFT/RIGHT/UP/DOWN
BMP_NUCLEAR — mushroom cloud
BMP_POISON — poison vial with skull
Usage (from project root):
python tools/generate_weapon_sprites.py
"""
from pathlib import Path
from PIL import Image, ImageDraw
import math
REPO = Path(__file__).resolve().parent.parent
OUT = REPO / "assets" / "Rat"
SZ = 64 # canvas size (real pixels)
LP = 4 # logical pixel size in real pixels
LG = SZ // LP # 16 — logical grid dimension
# ─── Palette ───────────────────────────────────────────────────────────────────
T = (0, 0, 0, 0)
# Bomb body
BK = (10, 10, 20, 255) # outline
BD = (35, 35, 60, 255) # dark body
BM = (58, 58, 90, 255) # mid body
BH = (88, 92, 130, 255) # highlight
BG = (150, 155, 205, 255) # gloss spot
# Fuse rope
FD = (70, 40, 10, 255) # dark strand
FL = (120, 80, 28, 255) # light strand
# Sparks
SK = (255, 225, 30, 255) # yellow
SO = (255, 130, 0, 255) # orange
EW = (255, 255, 210, 255) # spark white core
# Gas / toxic cloud
GK = (15, 80, 8, 255) # dark-green outline
GD = (30, 140, 18, 255) # dark green body
GM = (55, 195, 40, 255) # mid green
GL = (120, 235, 75, 255) # light green
GH = (205, 255, 155, 255) # gloss highlight
# Explosion
EK = (140, 10, 0, 255) # dark red core
EM = (230, 70, 0, 255) # orange rays
EL = (255, 200, 0, 255) # yellow outer
# Nuclear cloud
NC = (120, 120, 120, 255) # cloud dark grey
NL = (185, 185, 185, 255) # cloud mid grey
NH = (245, 245, 245, 255) # cloud light / highlight
NK = (155, 30, 0, 255) # stem dark red
NM = (235, 110, 15, 255) # stem orange
NY = (255, 215, 25, 255) # inner glow yellow
# Poison vial
PD = (70, 0, 115, 255) # dark purple stopper
PM = (125, 20, 170, 255) # mid purple cork
PW = (235, 235, 235, 255) # white glass / label
PBK = (5, 5, 10, 255) # skull black
PG = (25, 165, 20, 255) # green liquid
PGH = (85, 230, 55, 255) # green highlight
# ─── Drawing helpers ────────────────────────────────────────────────────────────
def _img():
return Image.new("RGBA", (SZ, SZ), (0, 0, 0, 0))
def _put(img, lx, ly, color):
"""Paint one logical pixel (LP×LP block)."""
if 0 <= lx < LG and 0 <= ly < LG:
drw = ImageDraw.Draw(img)
x0, y0 = lx * LP, ly * LP
drw.rectangle([x0, y0, x0 + LP - 1, y0 + LP - 1], fill=color)
def _circle(img, cx, cy, layers):
"""
Paint concentric circles. cx/cy in logical float coords.
layers = [(outer_radius, color), ...] tested in order; first hit wins.
"""
drw = ImageDraw.Draw(img)
for ly in range(LG):
for lx in range(LG):
d = math.sqrt((lx - cx) ** 2 + (ly - cy) ** 2)
for r, color in layers:
if d <= r:
x0, y0 = lx * LP, ly * LP
drw.rectangle([x0, y0, x0 + LP - 1, y0 + LP - 1], fill=color)
break
def _grid(img, rows, palette):
"""
Paint from a character-grid string list.
rows: list of 16 strings, each with 16 non-space chars.
'.' = skip (transparent). palette maps char → RGBA.
"""
for row_idx, row_str in enumerate(rows):
chars = [c for c in row_str if c != ' ']
for col_idx, ch in enumerate(chars):
if ch == '.' or col_idx >= LG or row_idx >= LG:
continue
color = palette.get(ch)
if color:
_put(img, col_idx, row_idx, color)
# ─── BOMB ──────────────────────────────────────────────────────────────────────
# Fuse rope path (logical coords), body-attachment → spark tip
_FUSE = [(9, 6), (9, 5), (10, 4), (11, 3), (11, 2), (12, 1)]
def make_bomb(frame: int) -> Image.Image:
"""frame 0 = long fuse / tiny spark; frame 4 = no fuse / huge spark."""
img = _img()
# Body: nested circles centered at logical (7.5, 10.0)
cx, cy = 7.5, 10.0
_circle(img, cx, cy, [
(5.4, BK), # outline ring
(4.9, BD), # dark body edge
(4.0, BM), # mid body fill
])
# Highlight blob (upper-left of body)
_circle(img, 5.5, 7.8, [(2.3, BH), (1.2, BG)])
# Fuse rope — show only the remaining segments
segs = max(1, len(_FUSE) - frame)
for i in range(segs):
fx, fy = _FUSE[i]
_put(img, fx, fy, FL if i % 2 == 0 else FD)
# Spark at the fuse tip — grows with frame
ti = min(segs - 1, len(_FUSE) - 1)
tx, ty = _FUSE[ti]
if frame == 0:
_put(img, tx, ty, SK)
elif frame == 1:
_put(img, tx, ty, EW)
_put(img, tx, ty - 1, SK)
elif frame == 2:
_put(img, tx, ty, EW)
_put(img, tx + 1, ty, SK)
_put(img, tx, ty - 1, SK)
elif frame == 3:
for dx, dy, c in [(-1, 0, SK), (1, 0, SK), (0, -1, SK), (0, 1, SO), (0, 0, EW)]:
_put(img, tx + dx, ty + dy, c)
else: # frame 4 — about to detonate
for dx, dy, c in [
(-2, 0, SO), (-1, 0, SK), (-1, -1, SK), (-1, 1, SO),
(0, -2, SK), (0, -1, EW), (0, 0, EW), (0, 1, SK),
(1, 0, SK), (1, -1, SK), (2, 0, SO), (0, -3, SO),
]:
_put(img, tx + dx, ty + dy, c)
return img
# ─── GAS ───────────────────────────────────────────────────────────────────────
_GAS_ROWS = [
'. . . . . . . . . . . . . . . .',
'. . . . . K K K K . . . . . . .',
'. . . K K D D D D K K . . . . .',
'. . K D D M M M M D D K . . . .',
'. K D M M M L L M M D D K . . .',
'. K D M L G L L G L M D K . . .',
'K D M M L L L L L L M D D K . .',
'K D M L L L L L L L L M D K . .',
'K D M M L L L L L L M M D K . .',
'K D D M M L L L L M M D D K . .',
'. K D D M M M M M M D D K . . .',
'. . K D D D M M D D D K . . . .',
'. . . K K D D D D K K . . . . .',
'. . . . . K K K K . . . . . . .',
'. . . . . . . . . . . . . . . .',
'. . . . . . . . . . . . . . . .',
]
_GAS_PAL = {'K': GK, 'D': GD, 'M': GM, 'L': GL, 'G': GH}
def make_gas(direction=None) -> Image.Image:
img = _img()
_grid(img, _GAS_ROWS, _GAS_PAL)
# Directional clip: erase the half the gas does NOT flow toward
drw = ImageDraw.Draw(img)
clips = {
'LEFT': (32, 0, 63, 63),
'RIGHT': (0, 0, 31, 63),
'UP': (0, 32, 63, 63),
'DOWN': (0, 0, 63, 31),
}
if direction in clips:
drw.rectangle(list(clips[direction]), fill=(0, 0, 0, 0))
return img
# ─── EXPLOSION ─────────────────────────────────────────────────────────────────
def make_explosion(direction=None) -> Image.Image:
img = _img()
cx, cy = 7.5, 7.5
# 8-way diagonal rays (thin)
for deg in range(0, 360, 45):
rad = math.radians(deg)
for step in range(1, 110):
d = step * 0.07
if d > 7.8:
break
lx = round(cx + d * math.cos(rad))
ly = round(cy + d * math.sin(rad))
if 0 <= lx < LG and 0 <= ly < LG:
c = EM if d > 6 else (EL if d > 3.5 else EW)
_put(img, lx, ly, c)
# 4-way cardinal rays (thicker — 3 pixels wide)
for deg in [0, 90, 180, 270]:
rad = math.radians(deg)
perp = math.radians(deg + 90)
for step in range(1, 120):
d = step * 0.07
if d > 7.8:
break
for spread in (-0.35, 0.0, 0.35):
lx = round(cx + d * math.cos(rad) + spread * math.cos(perp))
ly = round(cy + d * math.sin(rad) + spread * math.sin(perp))
if 0 <= lx < LG and 0 <= ly < LG:
c = EM if d > 5.5 else (EL if d > 3.0 else EW)
_put(img, lx, ly, c)
# Hot core
_circle(img, cx, cy, [(2.5, EK), (1.8, EM), (1.0, EL), (0.5, EW)])
# Directional clip
drw = ImageDraw.Draw(img)
clips = {
'LEFT': (32, 0, 63, 63),
'RIGHT': (0, 0, 31, 63),
'UP': (0, 32, 63, 63),
'DOWN': (0, 0, 63, 31),
}
if direction in clips:
drw.rectangle(list(clips[direction]), fill=(0, 0, 0, 0))
return img
# ─── NUCLEAR (mushroom cloud) ───────────────────────────────────────────────────
_NUCLEAR_ROWS = [
'. . . . . . . . . . . . . . . .',
'. . . . N L L L L L L N . . . .',
'. . . N L H H H H H H L N . . .',
'. . N L H H N N H H N H L N . .',
'. . N L H N N N H N N H L N . .',
'. . N L N N N H H N N N L N . .',
'. . N L N N N H H N N N L N . .',
'. . . N L H H H H H H L N . . .',
'. . . . N L L L L L L N . . . .',
'. . . . . . M Y Y M . . . . . .',
'. . . . . . M Y Y M . . . . . .',
'. . . . . Z M Y Y M Z . . . . .',
'. . . . Z Z M Y Y M Z Z . . . .',
'. . . Z Z Z M Y Y M Z Z Z . . .',
'. . . Z Z Z Z Z Z Z Z Z Z . . .',
'. . . . . . . . . . . . . . . .',
]
_NUCLEAR_PAL = {'N': NC, 'L': NL, 'H': NH, 'M': NM, 'Y': NY, 'Z': NK}
def make_nuclear() -> Image.Image:
img = _img()
_grid(img, _NUCLEAR_ROWS, _NUCLEAR_PAL)
return img
# ─── POISON VIAL ───────────────────────────────────────────────────────────────
_POISON_ROWS = [
'. . . . . . . . . . . . . . . .',
'. . . . . . . D D D . . . . . .',
'. . . . . . D M M M D . . . . .',
'. . . . . . W W W W W W . . . .',
'. . . . . W B B B B B B W . . .',
'. . . . . W B W . . B B W . . .',
'. . . . . W B . W W . B W . . .',
'. . . . . W B G G G B B W . . .',
'. . . . . W B G H G B B W . . .',
'. . . . . W B G G G B B W . . .',
'. . . . . W B B B B B B W . . .',
'. . . . . W B B B B B B W . . .',
'. . . . . . W W W W W W . . . .',
'. . . . . . . . . . . . . . . .',
'. . . . . . . . . . . . . . . .',
'. . . . . . . . . . . . . . . .',
]
_POISON_PAL = {
'D': PD, # dark purple stopper
'M': PM, # mid purple cork
'W': PW, # white glass
'B': PBK, # skull / label black
'G': PG, # green liquid
'H': PGH, # green highlight
}
def make_poison() -> Image.Image:
img = _img()
_grid(img, _POISON_ROWS, _POISON_PAL)
return img
# ─── Main ───────────────────────────────────────────────────────────────────────
def main():
OUT.mkdir(parents=True, exist_ok=True)
# Bombs (5 animation frames)
for frame in range(5):
p = OUT / f"BMP_BOMB{frame}.png"
make_bomb(frame).save(p)
print(f" wrote {p.name}")
# Gas
for direction in [None, 'LEFT', 'RIGHT', 'UP', 'DOWN']:
suffix = f"_{direction}" if direction else ""
p = OUT / f"BMP_GAS{suffix}.png"
make_gas(direction).save(p)
print(f" wrote {p.name}")
# Explosion
for direction in [None, 'LEFT', 'RIGHT', 'UP', 'DOWN']:
suffix = f"_{direction}" if direction else ""
p = OUT / f"BMP_EXPLOSION{suffix}.png"
make_explosion(direction).save(p)
print(f" wrote {p.name}")
# Nuclear & Poison
make_nuclear().save(OUT / "BMP_NUCLEAR.png")
print(" wrote BMP_NUCLEAR.png")
make_poison().save(OUT / "BMP_POISON.png")
print(" wrote BMP_POISON.png")
total = 5 + 5 + 5 + 2
print(f"\nDone — {total} sprites saved to {OUT}")
if __name__ == "__main__":
main()