Skip to content

Commit 06cca26

Browse files
committed
Enhance EntityAnim class with loop control and restart functionality
1 parent 1091b0f commit 06cca26

2 files changed

Lines changed: 55 additions & 10 deletions

File tree

engine/core/__init__.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import pygame
1414
import importlib.util
1515
import sys
16-
import tkinter.messagebox
1716
import uuid
1817
import os
1918
import colorama
@@ -98,10 +97,8 @@ def __init__(
9897
self.image = EntityImage(image)
9998
else:
10099
self.image = EntityAnim(image)
101-
except pygame.error as e:
100+
except (pygame.error, FileNotFoundError) as e:
102101
logger(f"Failed to load image or animation '{image}': {str(e)}", status=LoggerStatus.WARNING)
103-
except FileNotFoundError as e:
104-
tkinter.messagebox.showerror("File not found", str(e))
105102

106103
if scriptfile is not None:
107104
esfid = f"esf-{self.id}"
@@ -120,12 +117,12 @@ def __init__(
120117
try:
121118
spec.loader.exec_module(self.scriptfile_module)
122119
except FileNotFoundError:
123-
tkinter.messagebox.showerror(
124-
"Error",
120+
logger(
125121
f'Script file "{scriptfile}" not found. Please ensure the file exists and try again.',
122+
status=LoggerStatus.CRITICAL,
126123
)
127124
except ImportError as e:
128-
tkinter.messagebox.showerror("Error", f"Error when loading script: {e}")
125+
logger(f"Error when loading script: {e}", status=LoggerStatus.CRITICAL)
129126

130127
if self.scriptfile_module is not None:
131128
if self.scriptfile is not None:

engine/core/animation.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)