@@ -20,19 +20,27 @@ class EntityAnim:
2020 entity holding one needs no special handling, and frames advance off the
2121 clock on their own: drawing it is all the caller has to do. The animation
2222 loops for as long as it keeps being drawn, at the pace the file asks for.
23+
24+ Pass ``loop=False`` for a one-shot animation, such as a jump: it plays
25+ through once, then holds its last frame and reports ``finished``, so the
26+ caller can swap in another animation or leave the pose standing. Calling
27+ ``restart`` plays it again from the top, which is how the same jump is
28+ re-triggered each time the entity leaves the ground.
2329 """
2430
2531 frames : list [pygame .Surface ]
2632
27- def __init__ (self , anim_path : str ) -> None :
33+ def __init__ (self , anim_path : str , loop : bool = True ) -> None :
2834 """
2935 Initialize the EntityAnim by loading the animation at ``anim_path``.
3036
3137 Args:
3238 anim_path (str): The path to the animation file.
39+ loop (bool): Whether the animation repeats. Defaults to True.
3340 """
3441
3542 self .frames = []
43+ self .loop : bool = loop
3644
3745 self ._starts : list [float ] = []
3846 self ._duration : float = 0.0
@@ -43,14 +51,16 @@ def __init__(self, anim_path: str) -> None:
4351
4452 self .set_image (anim_path )
4553
46- def set_image (self , anim_path : str ) -> None :
54+ def set_image (self , anim_path : str , loop : Optional [ bool ] = None ) -> None :
4755 """
4856 Load ``anim_path`` and store it as an animation.
4957
5058 The animation starts over from its first frame.
5159
5260 Args:
5361 anim_path (str): The path to the animation file.
62+ loop (Optional[bool]): Whether the animation repeats. Keeps the
63+ current setting when None. Defaults to None.
5464 """
5565
5666 assert pygame .get_init (), ( # nosec B101
@@ -63,6 +73,9 @@ def set_image(self, anim_path: str) -> None:
6373
6474 self .frames = [frame .convert_alpha () for frame , _ in loaded ]
6575
76+ if loop is not None :
77+ self .loop = loop
78+
6679 self ._starts = []
6780 self ._duration = 0.0
6881
@@ -75,6 +88,35 @@ def set_image(self, anim_path: str) -> None:
7588 self ._scaled = None
7689 self ._scaled_key = None
7790
91+ def restart (self ) -> None :
92+ """
93+ Play the animation again from its first frame.
94+
95+ This is what re-triggers a one-shot animation: call it on every jump
96+ rather than reloading the file each time.
97+ """
98+
99+ self ._started_at = pygame .time .get_ticks ()
100+
101+ @property
102+ def finished (self ) -> bool :
103+ """
104+ Whether a one-shot animation has already played through its last frame.
105+
106+ A looping animation never finishes, so this is always False for one.
107+
108+ Returns:
109+ bool: True once a non-looping animation has run its course.
110+ """
111+
112+ if self .loop :
113+ return False
114+
115+ if self ._duration <= 0.0 :
116+ return True
117+
118+ return (pygame .time .get_ticks () - self ._started_at ) >= self ._duration
119+
78120 def _current_index (self ) -> int :
79121 """
80122 Work out which frame is due, from how long the animation has been running.
@@ -86,7 +128,13 @@ def _current_index(self) -> int:
86128 if self ._duration <= 0.0 :
87129 return 0
88130
89- elapsed : float = (pygame .time .get_ticks () - self ._started_at ) % self ._duration
131+ elapsed : float = float (pygame .time .get_ticks () - self ._started_at )
132+
133+ if not self .loop :
134+ if elapsed >= self ._duration :
135+ return len (self .frames ) - 1
136+ else :
137+ elapsed %= self ._duration
90138
91139 return bisect .bisect_right (self ._starts , elapsed ) - 1
92140
0 commit comments