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