@ -0,0 +1,107 @@ |
|||||||
|
--- |
||||||
|
applyTo: "tools/vernon/**,assets/Rat/**" |
||||||
|
--- |
||||||
|
|
||||||
|
# Pixel Art Sprite Workflow — mice project |
||||||
|
|
||||||
|
## Strumenti disponibili |
||||||
|
|
||||||
|
| Script | Uso | |
||||||
|
|--------|-----| |
||||||
|
| `tools/vernon/image_to_json.py <INPUT.png> <OUTPUT.json>` | Converte PNG → matrice JSON RGBA 64×64 | |
||||||
|
| `tools/vernon/json_to_png.py <INPUT.json> <OUTPUT.png>` | Converte matrice JSON RGBA → PNG | |
||||||
|
|
||||||
|
Entrambi usano Pillow e richiedono il `venv` attivo: |
||||||
|
```bash |
||||||
|
source .venv/bin/activate |
||||||
|
``` |
||||||
|
|
||||||
|
## Formato JSON |
||||||
|
|
||||||
|
```json |
||||||
|
{ |
||||||
|
"source": "BMP_BOMB0.png", |
||||||
|
"width": 64, |
||||||
|
"height": 64, |
||||||
|
"mode": "RGBA", |
||||||
|
"pixels": [ |
||||||
|
[ [R, G, B, A], ... ], // riga 0, 64 pixel |
||||||
|
... // 64 righe totali |
||||||
|
] |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Ogni pixel è `[R, G, B, A]` con valori 0–255. |
||||||
|
|
||||||
|
## Convenzioni cromatiche del gioco |
||||||
|
|
||||||
|
- **Colore trasparente (chromakey):** `[128, 128, 128, 192]` — usato come sfondo, il motore lo rende hidden |
||||||
|
- **Alpha standard:** `192` per tutti i pixel visibili (coerente con gli asset originali) |
||||||
|
|
||||||
|
## Workflow iterativo di redesign (passi 0–4) |
||||||
|
|
||||||
|
``` |
||||||
|
0. BACKUP → prima di sovrascrivere, copia l'originale: |
||||||
|
cp assets/Rat/<NAME>.png assets/Rat/backup/<NAME>_original.png |
||||||
|
1. image_to_json.py → esamina JSON e PNG originale |
||||||
|
2. capire struttura: sfondo, palette, forma principale |
||||||
|
3. modificare JSON (o generarlo via script Python) con: |
||||||
|
- più livelli di shading (8+ valori invece di 3) |
||||||
|
- dettagli geometrici aggiuntivi (texture, bordi, ombre interne) |
||||||
|
- palette più ricca mantenendo stile pixel art (bordi netti, no anti-alias) |
||||||
|
4. json_to_png.py → valuta risultato visivo; se non soddisfacente, torna a 3 |
||||||
|
``` |
||||||
|
|
||||||
|
## Pattern Python per generare JSON programmaticamente |
||||||
|
|
||||||
|
```python |
||||||
|
import json, math |
||||||
|
from pathlib import Path |
||||||
|
|
||||||
|
W, H = 64, 64 |
||||||
|
A = 192 # alpha standard |
||||||
|
|
||||||
|
def px(r, g, b): return [r, g, b, A] |
||||||
|
|
||||||
|
TRANSPARENT = px(128, 128, 128) |
||||||
|
grid = [[TRANSPARENT[:] for _ in range(W)] for _ in range(H)] |
||||||
|
|
||||||
|
def put(x, y, col): |
||||||
|
if 0 <= x < W and 0 <= y < H: |
||||||
|
grid[y][x] = col[:] |
||||||
|
|
||||||
|
# ... disegna su grid ... |
||||||
|
|
||||||
|
data = {"source": "BMP_X.png", "width": W, "height": H, "mode": "RGBA", "pixels": grid} |
||||||
|
Path("tools/vernon/output/BMP_X_v2.json").write_text(json.dumps(data, indent=2)) |
||||||
|
``` |
||||||
|
|
||||||
|
## Tecniche pixel art a 64×64 |
||||||
|
|
||||||
|
- **Shading sferico:** calcola normale + dot product con luce per N livelli di grigio discreti |
||||||
|
- **Rope/miccia:** traccia bezier quadratica, alterna 2–3 toni in sequenza (effetto intrecciato) |
||||||
|
- **Scintilla:** pixel centrali chiari (bianco/giallo), bordi che degradano in arancio → rosso |
||||||
|
- **Outline:** bordo di 1px nero (`[0,0,0,192]`) attorno a tutte le forme principali |
||||||
|
- **Nessun anti-aliasing:** ogni pixel è un colore solido discreto della palette scelta |
||||||
|
|
||||||
|
## Asset da redesignare (tutti 64×64) |
||||||
|
|
||||||
|
| File | Gruppo | |
||||||
|
|------|--------| |
||||||
|
| `BMP_BOMB0.png` … `BMP_BOMB4.png` | Animazione bomba (0=quieta, 4=accesa) | |
||||||
|
| `BMP_1_GRASS_1.png` … `BMP_1_GRASS_4.png` | Tile erba tema 1 (verde) — **redesignate con FBM 7-toni** | |
||||||
|
| `BMP_2_GRASS_1.png` … `BMP_2_GRASS_4.png` | Tile erba tema 2 (secca/autunnale) | |
||||||
|
| `BMP_3_GRASS_1.png` … `BMP_3_GRASS_4.png` | Tile erba tema 3 (dungeon/pietra) | |
||||||
|
| `BMP_4_GRASS_1.png` … `BMP_4_GRASS_4.png` | Tile erba tema 4 (fuoco/lava) | |
||||||
|
| `BMP_GAS.png`, `BMP_GAS_{DIR}.png` | Gas generico + 4 direzioni | |
||||||
|
| `BMP_EXPLOSION.png`, `BMP_EXPLOSION_{DIR}.png` | Esplosione generica + 4 direzioni | |
||||||
|
| `BMP_NUCLEAR.png` | Fungo nucleare | |
||||||
|
| `BMP_POISON.png` | Veleno | |
||||||
|
|
||||||
|
## Note sull'animazione BOMB (frame 0–4) |
||||||
|
|
||||||
|
- `BOMB0`: bomba ferma, scintilla piccola a riposo |
||||||
|
- `BOMB1`–`BOMB3`: miccia che brucia (la scintilla avanza verso il corpo, la corda si accorcia) |
||||||
|
- `BOMB4`: quasi esplode (glow rosso/arancio sul corpo, scintilla grande) |
||||||
|
|
||||||
|
Per i frame animati: mantieni identici corpo + miccia, varia solo posizione/dimensione scintilla e eventuale glow progressivo. |
||||||
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 279 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 296 B After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 243 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 174 B After Width: | Height: | Size: 683 B |
|
Before Width: | Height: | Size: 189 B After Width: | Height: | Size: 665 B |
|
Before Width: | Height: | Size: 184 B After Width: | Height: | Size: 685 B |
|
Before Width: | Height: | Size: 400 B After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 401 B After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 390 B After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 324 B After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 332 B After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 197 B After Width: | Height: | Size: 711 B |
|
Before Width: | Height: | Size: 197 B After Width: | Height: | Size: 713 B |
|
Before Width: | Height: | Size: 193 B After Width: | Height: | Size: 618 B |
|
Before Width: | Height: | Size: 171 B After Width: | Height: | Size: 695 B |
|
Before Width: | Height: | Size: 194 B After Width: | Height: | Size: 696 B |
|
Before Width: | Height: | Size: 191 B After Width: | Height: | Size: 661 B |
|
Before Width: | Height: | Size: 187 B After Width: | Height: | Size: 728 B |
|
Before Width: | Height: | Size: 187 B After Width: | Height: | Size: 717 B |
|
Before Width: | Height: | Size: 187 B After Width: | Height: | Size: 722 B |
|
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 390 B |
|
Before Width: | Height: | Size: 434 B After Width: | Height: | Size: 405 B |
|
Before Width: | Height: | Size: 435 B After Width: | Height: | Size: 403 B |
|
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 396 B |
|
Before Width: | Height: | Size: 255 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 250 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 266 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 266 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 280 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 785 B After Width: | Height: | Size: 772 B |
|
Before Width: | Height: | Size: 797 B After Width: | Height: | Size: 788 B |
|
Before Width: | Height: | Size: 788 B After Width: | Height: | Size: 771 B |
|
Before Width: | Height: | Size: 782 B After Width: | Height: | Size: 775 B |
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 314 B After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 324 B After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 331 B After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 305 B After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 843 B After Width: | Height: | Size: 780 B |
|
Before Width: | Height: | Size: 865 B After Width: | Height: | Size: 800 B |
|
Before Width: | Height: | Size: 841 B After Width: | Height: | Size: 780 B |
|
Before Width: | Height: | Size: 830 B After Width: | Height: | Size: 773 B |
|
After Width: | Height: | Size: 257 B |
|
After Width: | Height: | Size: 279 B |
|
After Width: | Height: | Size: 296 B |
|
After Width: | Height: | Size: 243 B |
|
After Width: | Height: | Size: 189 B |
|
After Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 174 B |
|
After Width: | Height: | Size: 400 B |
|
After Width: | Height: | Size: 402 B |
|
After Width: | Height: | Size: 401 B |
|
After Width: | Height: | Size: 390 B |
|
After Width: | Height: | Size: 473 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 325 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 325 B |
|
After Width: | Height: | Size: 197 B |
|
After Width: | Height: | Size: 193 B |
|
After Width: | Height: | Size: 197 B |
|
After Width: | Height: | Size: 194 B |
|
After Width: | Height: | Size: 191 B |
|
After Width: | Height: | Size: 171 B |
|
After Width: | Height: | Size: 187 B |
|
After Width: | Height: | Size: 187 B |
|
After Width: | Height: | Size: 187 B |
|
After Width: | Height: | Size: 272 B |
|
After Width: | Height: | Size: 304 B |
|
After Width: | Height: | Size: 297 B |
|
After Width: | Height: | Size: 279 B |
|
After Width: | Height: | Size: 267 B |
|
After Width: | Height: | Size: 297 B |
|
After Width: | Height: | Size: 304 B |
|
After Width: | Height: | Size: 299 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 335 B |
|
After Width: | Height: | Size: 356 B |
|
After Width: | Height: | Size: 354 B |
|
After Width: | Height: | Size: 314 B |
|
After Width: | Height: | Size: 324 B |
|
After Width: | Height: | Size: 331 B |
|
After Width: | Height: | Size: 305 B |
|
After Width: | Height: | Size: 354 B |
@ -1,4 +1,5 @@ |
|||||||
pysdl2 |
pysdl2 |
||||||
Pillow |
Pillow |
||||||
pyaml |
pyaml |
||||||
numpy |
numpy |
||||||
|
requests |
||||||
@ -0,0 +1,62 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
""" |
||||||
|
Generate directional rat sprites (LEFT, RIGHT, UP, DOWN) from source LEFT PNGs. |
||||||
|
|
||||||
|
Source files (tools/vernon/output/): |
||||||
|
BMP_MALE_LEFT.png |
||||||
|
BMP_FEMALE_LEFT.png |
||||||
|
BMP_BABY_LEFT.png |
||||||
|
|
||||||
|
Output goes to assets/Rat/ as: |
||||||
|
BMP_<SEX>_LEFT.png — copy of source |
||||||
|
BMP_<SEX>_RIGHT.png — horizontal flip |
||||||
|
BMP_<SEX>_UP.png — rotate 270° (nose up) |
||||||
|
BMP_<SEX>_DOWN.png — rotate 90° (nose down) |
||||||
|
|
||||||
|
Usage (from project root): |
||||||
|
python tools/generate_rat_sprites.py |
||||||
|
""" |
||||||
|
|
||||||
|
import sys |
||||||
|
import os |
||||||
|
from pathlib import Path |
||||||
|
from PIL import Image |
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parent.parent |
||||||
|
SOURCE_DIR = REPO_ROOT / "tools" / "vernon" / "output" |
||||||
|
OUTPUT_DIR = REPO_ROOT / "assets" / "Rat" |
||||||
|
|
||||||
|
SEXES = ["MALE", "FEMALE", "BABY"] |
||||||
|
|
||||||
|
|
||||||
|
def generate(sex: str) -> None: |
||||||
|
src_path = SOURCE_DIR / f"BMP_{sex}_LEFT.png" |
||||||
|
if not src_path.exists(): |
||||||
|
print(f" SKIP {sex}: source not found at {src_path}", file=sys.stderr) |
||||||
|
return |
||||||
|
|
||||||
|
src = Image.open(src_path).convert("RGBA") |
||||||
|
|
||||||
|
variants = { |
||||||
|
"LEFT": src, |
||||||
|
"RIGHT": src.transpose(Image.FLIP_LEFT_RIGHT), |
||||||
|
"UP": src.rotate(270, expand=True), |
||||||
|
"DOWN": src.rotate(90, expand=True), |
||||||
|
} |
||||||
|
|
||||||
|
for direction, img in variants.items(): |
||||||
|
out_path = OUTPUT_DIR / f"BMP_{sex}_{direction}.png" |
||||||
|
img.save(out_path) |
||||||
|
print(f" wrote {out_path.relative_to(REPO_ROOT)} {img.size}") |
||||||
|
|
||||||
|
|
||||||
|
def main() -> None: |
||||||
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True) |
||||||
|
for sex in SEXES: |
||||||
|
print(f"[{sex}]") |
||||||
|
generate(sex) |
||||||
|
print("Done.") |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
main() |
||||||
@ -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() |
||||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.1 KiB |