from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import pygame
from nqp.base_classes.image import Image
from nqp.core.constants import AnimationState, DEFAULT_IMAGE_SIZE
if TYPE_CHECKING:
from typing import Dict, List, Tuple
__all__ = ["Animation"]
[docs]class Animation:
"""
Class to hold visual information for a series of images
"""
[docs] def __init__(
self,
frames: Dict[str, List[Image]],
frame_duration: float = 0.6,
loop: bool = True,
starting_frame_set_name: str = None,
uses_simulation_time: bool = True,
):
self._frame_sets: Dict[str, List[Image]] = frames
self._frame_duration: float = max(frame_duration, 0.1) # must be greater than 1
self._is_looping: bool = loop
if starting_frame_set_name:
self._current_frame_set_name: str = starting_frame_set_name
else:
self._current_frame_set_name: str = list(self._frame_sets)[0]
self.uses_simulation_time: bool = uses_simulation_time # simulation or absolute, i.e. uses game speed
self._current_num_frames: int = len(self.current_frame_set)
self._animation_length: float = self._current_num_frames * self._frame_duration
# flags
self.delete_on_finish: bool = True
# progress
self._state: AnimationState = AnimationState.PLAYING
self._current_frame_num: int = 0
self._duration: float = 0
self._flash_timer: float = 0
self._flash_colour: None | Tuple[int, int, int] = None
[docs] def update(self, delta_time: float, game_speed: float):
# exit if not playing
if self._state != AnimationState.PLAYING:
return
# mod delta time if needed
if self.uses_simulation_time:
delta_time = delta_time * game_speed
else:
delta_time = delta_time
# update timers
self._duration += delta_time
if self._flash_timer > 0:
self._flash_timer -= delta_time
# have we reached the end?
if self._duration >= self._animation_length:
# reset duration if looping
if self._is_looping:
self._duration = 0
else:
self._state = AnimationState.FINISHED
# update frame
self._current_frame_num = (
int(self._duration / self._frame_duration * self._current_num_frames) % self._current_num_frames
)
[docs] def set_current_frame_set_name(self, frame_set_name: str):
"""
Set the frame set to be used.
If the name given isnt a valid key no action is taken.
"""
# catch if we have been passed current frame set
if frame_set_name == self._current_frame_set_name:
return
if frame_set_name in self._frame_sets.keys():
self._current_frame_set_name = frame_set_name
# update frame count
self._current_num_frames = len(self.current_frame_set)
else:
logging.warning(f"Tried to set frame set to {frame_set_name} but it doesnt exist.")
[docs] def play(self):
"""
Resume the animation
"""
self._state = AnimationState.PLAYING
[docs] def pause(self):
"""
Pause the animation
"""
self._state = AnimationState.PAUSED
[docs] def stop(self):
"""
Finish the animation
"""
self._state = AnimationState.FINISHED
[docs] def reset(self):
"""
Reset and pause the animation
"""
self._state = AnimationState.PAUSED
self._duration = 0
self._current_frame_num = 0
self.delete_on_finish = True
[docs] def get_frame(self, frame_num: int) -> Image:
"""
Return the Image of the nth frame
"""
try:
frame = self.current_frame_set[frame_num]
except IndexError:
logging.debug(f"Asked for frame {frame_num} but only have {len(self._frame_sets)}.")
frame = Image(image=pygame.Surface((DEFAULT_IMAGE_SIZE, DEFAULT_IMAGE_SIZE)))
return frame
@property
def image(self) -> Image:
"""
Return the current frame.
"""
return self.get_frame(self._current_frame_num)
@property
def surface(self) -> pygame.Surface:
"""
Return the current frame's surface.
"""
surf = self.get_frame(self._current_frame_num).surface
# apply flash
if self._flash_timer > 0:
surf = surf.copy()
colour_surf = pygame.Surface(surf.get_size()).convert_alpha()
colour_surf.fill(self._flash_colour)
surf.blit(colour_surf, (0, 0), special_flags=pygame.BLEND_ADD)
return surf
@property
def is_finished(self) -> bool:
""" "
Return True if this animation has finished playing.
"""
if self._state == AnimationState.FINISHED:
result = True
else:
result = False
return result
@property
def width(self) -> int:
return self.current_frame_set[0].width
@property
def height(self) -> int:
return self.current_frame_set[0].height
@property
def current_frame_set(self) -> List[Image]:
return self._frame_sets[self._current_frame_set_name]
[docs] def flash(self, colour: Tuple[int, int, int], duration: float = 0.05):
"""
Change the colour of the sprite for a given period.
"""
self._flash_timer = duration
self._flash_colour = colour