Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user