nodes/anim_sprite.py

Part of Clear Code Zelda.

 1"""Multi-PNG animated sprite: swaps Sprite2D.texture between a list of files.
 2
 3The upstream game stores each animation as a folder of separate PNGs
 4(``graphics/player/down/down_0.png`` … ``down_3.png``). SimVX has a real
 5``AnimatedSprite2D`` for sprite-sheet atlases, but we don't have an atlas
 6here, since we have lots of tiny files. Reassigning ``Sprite2D.texture`` triggers
 7``_invalidate_texture_slot`` and the scene adapter reloads the GPU texture, so
 8this is the cleanest path.
 9"""
10
11from __future__ import annotations
12
13from simvx.core import Property, Sprite2D
14
15
16class FolderSprite(Sprite2D):
17    """A Sprite2D that cycles between a list of texture file paths.
18
19    Set :attr:`frames` to a list of paths and :attr:`fps` to control speed,
20    then call :meth:`play(frames, loop=...)`.  Set ``visible`` directly when
21    you want the sprite hidden (e.g. between attack swings).
22    """
23
24    fps = Property(10.0, range=(0.1, 60.0), hint="Animation frames per second")
25
26    def __init__(self, frames: list[str] | None = None, fps: float = 10.0, loop: bool = True, **kwargs):
27        # Use the first frame as the initial texture so the engine resolves
28        # native size on the very first scene-adapter pass.
29        first = frames[0] if frames else None
30        super().__init__(texture=first, **kwargs)
31        self._frames: list[str] = list(frames) if frames else []
32        self.fps = fps
33        self._loop = loop
34        self._frame_index = 0.0
35        self._finished = False
36
37    def play(self, frames: list[str], fps: float | None = None, loop: bool = True, reset: bool = True) -> None:
38        if frames is self._frames and not reset:
39            self._loop = loop
40            return
41        self._frames = list(frames)
42        if fps is not None:
43            self.fps = fps
44        self._loop = loop
45        self._frame_index = 0.0
46        self._finished = False
47        if self._frames:
48            self.texture = self._frames[0]
49
50    @property
51    def finished(self) -> bool:
52        return self._finished
53
54    @property
55    def frame_index(self) -> int:
56        return int(self._frame_index)
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