nodes/folder_sprite.pyΒΆ

Part of PirateMaker.

 1"""Multi-PNG animated sprite: cycles Sprite2D.texture across a folder of frames.
 2
 3Same pattern used in clear_code_zelda. PirateMaker assets are stored as folders
 4of individual PNGs, and SimVX's AnimatedSprite2D.from_frames composes them into
 5a strip, but we want per-instance frame control (water, coin, palm, enemies)
 6without re-uploading 232 land textures or 8-frame water on every cell.
 7
 8This implementation lazily swaps Sprite2D.texture each frame; the engine's
 9_invalidate_texture_slot hook + scene adapter handle the GPU upload.
10"""
11
12from __future__ import annotations
13
14from simvx.core import Property, Sprite2D
15
16
17class FolderSprite(Sprite2D):
18    """Sprite2D that cycles a list of file paths."""
19
20    fps = Property(8.0, range=(0.1, 60.0), hint="Animation frames per second")
21
22    def __init__(
23        self, frames: list[str] | None = None, fps: float = 8.0, loop: bool = True, anim_offset: float = 0.0, **kwargs
24    ):
25        first = frames[0] if frames else None
26        super().__init__(texture=first, filter="nearest", **kwargs)
27        self._frames = list(frames) if frames else []
28        self.fps = fps
29        self._loop = loop
30        self._frame_index = anim_offset
31        self._finished = False
32
33    @property
34    def finished(self) -> bool:
35        return self._finished
36
37    @property
38    def frame_count(self) -> int:
39        return len(self._frames)
40
41    def play(self, frames: list[str], fps: float | None = None, loop: bool = True) -> None:
42        """Switch to ``frames`` and restart from frame 0.
43
44        Re-requesting the frames already playing is a no-op, so callers can drive
45        this straight from a per-frame state check without stuttering.
46        """
47        if frames == self._frames:
48            return
49        self._frames = list(frames)
50        if fps is not None:
51            self.fps = fps
52        self._loop = loop
53        self._frame_index = 0.0
54        self._finished = False
55        if self._frames:
56            self.texture = self._frames[0]
57
58    def on_update(self, dt: float) -> None:
59        if not self._frames:
60            return
61        self._frame_index += dt * self.fps
62        if self._frame_index >= len(self._frames):
63            if self._loop:
64                self._frame_index %= len(self._frames)
65            else:
66                self._frame_index = len(self._frames) - 1
67                self._finished = True
68        idx = int(self._frame_index)
69        new_tex = self._frames[idx]
70        if self.texture != new_tex:
71            self.texture = new_tex