nodes/sprite_sheets.pyΒΆ
Part of Dodge the Creeps.
1"""Build horizontal sprite-sheet ndarrays from individual PNG files.
2
3Godot's `SpriteFrames` resource accepts a list of textures per animation, but
4SimVX's `AnimatedSprite2D` expects a single texture sliced into a grid. We
5load the upstream PNGs with PIL and stack them horizontally so each animation
6becomes a 2-frame strip indexable as `frames=[0, 1]`.
7"""
8
9from __future__ import annotations
10
11from functools import cache
12from pathlib import Path
13
14import numpy as np
15from PIL import Image
16
17ASSETS_DIR = Path(__file__).resolve().parent.parent / "assets"
18
19
20def _load_rgba(path: Path) -> np.ndarray:
21 img = Image.open(path).convert("RGBA")
22 return np.asarray(img, dtype=np.uint8)
23
24
25def _hstack_pad(frames: list[np.ndarray]) -> np.ndarray:
26 """Stack frames horizontally, padding to the max height with transparent
27 pixels so the resulting strip is rectangular and frame width is uniform.
28 """
29 h = max(f.shape[0] for f in frames)
30 w = max(f.shape[1] for f in frames)
31 sheet = np.zeros((h, w * len(frames), 4), dtype=np.uint8)
32 for i, f in enumerate(frames):
33 fh, fw = f.shape[:2]
34 # Centre each frame inside its uniform cell so the sprite doesn't jitter
35 # between frames of slightly different sizes.
36 y0 = (h - fh) // 2
37 x0 = i * w + (w - fw) // 2
38 sheet[y0 : y0 + fh, x0 : x0 + fw] = f
39 return sheet
40
41
42@cache
43def player_sheet(animation: str) -> tuple[np.ndarray, int, int]:
44 """(sheet_rgba, frame_w, frame_h) for the player animation 'walk' or 'up'."""
45 if animation == "walk":
46 names = ["playerGrey_walk1.png", "playerGrey_walk2.png"]
47 elif animation == "up":
48 names = ["playerGrey_up1.png", "playerGrey_up2.png"]
49 else:
50 raise ValueError(f"Unknown player animation: {animation}")
51 frames = [_load_rgba(ASSETS_DIR / n) for n in names]
52 sheet = _hstack_pad(frames)
53 h, total_w = sheet.shape[:2]
54 return sheet, total_w // len(frames), h
55
56
57@cache
58def mob_sheet(kind: str) -> tuple[np.ndarray, int, int]:
59 """(sheet_rgba, frame_w, frame_h) for a mob animation 'fly' / 'walk' / 'swim'."""
60 base = {"fly": "enemyFlyingAlt", "walk": "enemyWalking", "swim": "enemySwimming"}[kind]
61 frames = [_load_rgba(ASSETS_DIR / f"{base}_{i}.png") for i in (1, 2)]
62 sheet = _hstack_pad(frames)
63 h, total_w = sheet.shape[:2]
64 return sheet, total_w // len(frames), h