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