added docstrings to every file and function. (with claude, checked it)
tower and game_play dont meet pylint check yet.
This commit is contained in:
@@ -655,3 +655,6 @@ init-import=no
|
|||||||
# List of qualified module names which can have objects that can redefine
|
# List of qualified module names which can have objects that can redefine
|
||||||
# builtins.
|
# builtins.
|
||||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||||
|
|
||||||
|
[MASTER]
|
||||||
|
extension-pkg-allow-list=pygame
|
||||||
+15
-11
@@ -6,6 +6,7 @@ import pygame as pg
|
|||||||
from game.util import tile_to_pixel
|
from game.util import tile_to_pixel
|
||||||
|
|
||||||
class Enemy(pg.sprite.Sprite):
|
class Enemy(pg.sprite.Sprite):
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
"""A single enemy that follows waypoints
|
"""A single enemy that follows waypoints
|
||||||
across the tilemap.
|
across the tilemap.
|
||||||
|
|
||||||
@@ -13,31 +14,33 @@ class Enemy(pg.sprite.Sprite):
|
|||||||
game (Game): Whole Game instances, contains everything that the game class has.
|
game (Game): Whole Game instances, contains everything that the game class has.
|
||||||
speed (float): Movement speed in pixels per update.
|
speed (float): Movement speed in pixels per update.
|
||||||
health (float): Current health points.
|
health (float): Current health points.
|
||||||
angle (float): Current facing angle in degrees.
|
angle (float): Current facing angle in degrees. (initalized on 0 degress)
|
||||||
"""
|
"""
|
||||||
def __init__(self, game, speed, health):
|
def __init__(self, game, speed, health):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.game = game
|
self.game = game
|
||||||
|
# coordinate on where the enemy should move to (in map json)
|
||||||
self.waypoints = self.game.tilemap.waypoints
|
self.waypoints = self.game.tilemap.waypoints
|
||||||
|
|
||||||
self.speed = speed
|
self.speed = speed
|
||||||
self.angle = 0
|
|
||||||
self.health = health
|
self.health = health
|
||||||
|
|
||||||
|
#starting waypoint index
|
||||||
|
self.target_index = 1
|
||||||
|
self.target = None #iititalise target var
|
||||||
|
self.angle = 0
|
||||||
|
self.healthbar = HealthBar(
|
||||||
|
18,
|
||||||
|
4,
|
||||||
|
self.health
|
||||||
|
)
|
||||||
|
|
||||||
|
#initalise objects need to be initalized last (depend on ealier dependencies)
|
||||||
self.image = pg.transform.rotate(self.game.assets['enemy'].copy(), self.angle)
|
self.image = pg.transform.rotate(self.game.assets['enemy'].copy(), self.angle)
|
||||||
self.rect = self.image.get_rect()
|
self.rect = self.image.get_rect()
|
||||||
|
|
||||||
self.pos = pg.Vector2(
|
self.pos = pg.Vector2(
|
||||||
tile_to_pixel(self.waypoints[0], self.game.tilemap.tile_size, True)
|
tile_to_pixel(self.waypoints[0], self.game.tilemap.tile_size, True)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.target_index = 1
|
|
||||||
self.rect.center = self.pos
|
self.rect.center = self.pos
|
||||||
self.healthbar = HealthBar(
|
|
||||||
18,
|
|
||||||
5,
|
|
||||||
self.health
|
|
||||||
)
|
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Advance movement and update rotation each frame."""
|
"""Advance movement and update rotation each frame."""
|
||||||
@@ -133,6 +136,7 @@ class EnemyGroup(pg.sprite.Group):
|
|||||||
enemy.update()
|
enemy.update()
|
||||||
|
|
||||||
class HealthBar():
|
class HealthBar():
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
"""A simple two-rect health bar drawn in world space.
|
"""A simple two-rect health bar drawn in world space.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
|
|||||||
+26
-20
@@ -1,21 +1,28 @@
|
|||||||
"""
|
"""Game module - core game loop, screen management, and shared asset loading."""
|
||||||
game class
|
|
||||||
|
|
||||||
"""
|
|
||||||
import pygame
|
|
||||||
import pygame_gui
|
import pygame_gui
|
||||||
|
import pygame
|
||||||
|
|
||||||
from game.util import *
|
from game.util import load_image, load_images, load_images_alpha, Animation
|
||||||
from game.tilemap import Tilemap
|
from game.tilemap import Tilemap
|
||||||
from game.screens.main_menu import MainMenu
|
from game.screens.main_menu import MainMenu
|
||||||
from game.screens.game_play import GamePlay
|
from game.screens.game_play import GamePlay
|
||||||
|
|
||||||
|
#constants
|
||||||
FPS=60
|
FPS=60
|
||||||
WINDOW_SIZE=(960,540)
|
WINDOW_SIZE=(960,540)
|
||||||
TILE=16
|
|
||||||
|
|
||||||
class Game:
|
class Game:
|
||||||
|
"""Central game class managing the loop, screens, and shared resources.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
window (pygame.Surface): The main display surface.
|
||||||
|
clock (pygame.time.Clock): Controls the frame rate.
|
||||||
|
ui_manager (pygame_gui.UIManager): Manages all pygame_gui UI elements.
|
||||||
|
tilemap (Tilemap): The active tilemap instance.
|
||||||
|
assets (dict): Shared images and animations, keyed by name.
|
||||||
|
screens (dict): All available screens, keyed by name.
|
||||||
|
current_screen: The screen currently being updated and drawn.
|
||||||
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
pygame.init()
|
pygame.init()
|
||||||
@@ -45,40 +52,39 @@ class Game:
|
|||||||
self.current_screen = self.screens["menu"]
|
self.current_screen = self.screens["menu"]
|
||||||
|
|
||||||
def change_screen(self, name):
|
def change_screen(self, name):
|
||||||
|
"""Transition to a different screen by name.
|
||||||
|
Hides the current screen and shows the requested one.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Key of the screen to switch to (e.g. ``"menu"``, ``"game"``).
|
||||||
|
"""
|
||||||
|
|
||||||
self.current_screen.hide()
|
self.current_screen.hide()
|
||||||
|
|
||||||
self.current_screen = self.screens[name]
|
self.current_screen = self.screens[name]
|
||||||
|
|
||||||
self.current_screen.show()
|
self.current_screen.show()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
"""Start the main game loop and block until the window is closed.
|
||||||
|
|
||||||
|
Each iteration: processes events, updates state, and draws the frame.
|
||||||
|
"""
|
||||||
|
|
||||||
running = True
|
running = True
|
||||||
|
|
||||||
while running:
|
while running:
|
||||||
|
|
||||||
dt = self.clock.tick(FPS) / 1000.0
|
dt = self.clock.tick(FPS) / 1000.0
|
||||||
|
|
||||||
for event in pygame.event.get():
|
for event in pygame.event.get():
|
||||||
|
|
||||||
if event.type == pygame.QUIT:
|
if event.type == pygame.QUIT:
|
||||||
running = False
|
running = False
|
||||||
|
|
||||||
self.ui_manager.process_events(event)
|
self.ui_manager.process_events(event)
|
||||||
|
|
||||||
self.current_screen.process_event(event)
|
self.current_screen.process_event(event)
|
||||||
|
|
||||||
self.current_screen.update(dt)
|
self.current_screen.update(dt)
|
||||||
|
|
||||||
self.ui_manager.update(dt)
|
self.ui_manager.update(dt)
|
||||||
|
|
||||||
self.window.fill((20, 20, 20))
|
self.window.fill((20, 20, 20))
|
||||||
|
|
||||||
self.current_screen.draw(self.window)
|
self.current_screen.draw(self.window)
|
||||||
|
|
||||||
self.ui_manager.draw_ui(self.window)
|
self.ui_manager.draw_ui(self.window)
|
||||||
|
|
||||||
pygame.display.update()
|
pygame.display.update()
|
||||||
|
pygame.quit()
|
||||||
pygame.quit()
|
|
||||||
|
|||||||
@@ -1,23 +1,48 @@
|
|||||||
|
"""Base screen module - shared interface for all game screens."""
|
||||||
|
|
||||||
class BaseScreen:
|
class BaseScreen:
|
||||||
|
"""Abstract base class defining the interface for all game screens.
|
||||||
|
|
||||||
|
Subclasses override :meth:`process_event`, :meth:`update`, and :meth:`draw`
|
||||||
|
to implement screen-specific behaviour. UI elements added to ``self.elements``
|
||||||
|
are shown and hidden automatically when the screen is switched.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
game (Game): The central game instance.
|
||||||
|
elements (list): pygame_gui elements managed by this screen.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, game):
|
def __init__(self, game):
|
||||||
self.game = game
|
self.game = game
|
||||||
|
|
||||||
self.elements = []
|
self.elements = []
|
||||||
|
|
||||||
def process_event(self, event):
|
def process_event(self, event):
|
||||||
pass
|
"""Handle a single pygame event.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event (pygame.event.Event): The event to process.
|
||||||
|
"""
|
||||||
|
|
||||||
def update(self, dt):
|
def update(self, dt):
|
||||||
pass
|
"""Update screen state for the current frame.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dt (float): Time elapsed since the last frame in seconds.
|
||||||
|
"""
|
||||||
|
|
||||||
def draw(self, surface):
|
def draw(self, surface):
|
||||||
pass
|
"""Draw the screen onto the given surface.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
surface (pygame.Surface): Target surface to draw onto.
|
||||||
|
"""
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
|
"""Make all UI elements on this screen visible."""
|
||||||
for element in self.elements:
|
for element in self.elements:
|
||||||
element.show()
|
element.show()
|
||||||
|
|
||||||
def hide(self):
|
def hide(self):
|
||||||
|
"""Hide all UI elements on this screen."""
|
||||||
for element in self.elements:
|
for element in self.elements:
|
||||||
element.hide()
|
element.hide()
|
||||||
|
|||||||
+94
-18
@@ -1,19 +1,34 @@
|
|||||||
|
"""Gameplay screen - map rendering, camera control, tower placement, and enemy waves."""
|
||||||
|
|
||||||
|
import pygame_gui as pgi
|
||||||
|
import pygame as pg
|
||||||
from game.screens.base_screen import BaseScreen
|
from game.screens.base_screen import BaseScreen
|
||||||
from game.enemy import Enemy, EnemyGroup
|
from game.enemy import Enemy, EnemyGroup
|
||||||
from game.camera import Camera
|
from game.camera import Camera
|
||||||
from game.tower import Tower, TowerGroup
|
from game.tower import Tower, TowerGroup
|
||||||
from game.util import *
|
from game.util import pixel_to_tile
|
||||||
import pygame as pg
|
|
||||||
import pygame_gui as pgi
|
|
||||||
|
|
||||||
class GamePlay(BaseScreen):
|
class GamePlay(BaseScreen):
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
"""The main gameplay screen handling the game loop, input, and rendering.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
game_surface (pg.Surface): The subsurface used for the game viewport.
|
||||||
|
camera (Camera): Controls zoom and pan over the tilemap.
|
||||||
|
movment (list[bool]): Movement flags for [left, right, up, down] (WASD).
|
||||||
|
selected (Tower | None): The currently selected tower, or ``None``.
|
||||||
|
bought (bool): Whether the player is currently placing a tower.
|
||||||
|
tile_pos (tuple[int, int] | None): Tile under the mouse cursor, or ``None``.
|
||||||
|
enemy_group (EnemyGroup): All active enemies.
|
||||||
|
tower_group (TowerGroup): All placed towers.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, game):
|
def __init__(self, game):
|
||||||
super().__init__(game)
|
super().__init__(game)
|
||||||
|
|
||||||
# self.tile_multiplicator = 2.0
|
# self.tile_multiplicator = 2.0
|
||||||
self.width, self.height = pg.display.get_surface().get_size()
|
self.width, self.height = pg.display.get_surface().get_size()
|
||||||
self.game_surface = pg.surface.Surface((int(self.width/4*3),int(self.height/6*5)))
|
self.game_surface = pg.surface.Surface((int(self.width/4*3),int(self.height/6*5)))
|
||||||
# self.map_surface = pg.surface.Surface((self.game_surface.get_width()/self.tile_multiplicator,self.game_surface.get_height()/self.tile_multiplicator))
|
|
||||||
|
|
||||||
self.camera = Camera()
|
self.camera = Camera()
|
||||||
self.center = (self.game_surface.get_width() /2, self.game_surface.get_height() /2)
|
self.center = (self.game_surface.get_width() /2, self.game_surface.get_height() /2)
|
||||||
@@ -65,7 +80,19 @@ class GamePlay(BaseScreen):
|
|||||||
self.cancel_button
|
self.cancel_button
|
||||||
]
|
]
|
||||||
|
|
||||||
|
self.mpos = (0, 0)
|
||||||
|
self.tile_pos = None
|
||||||
|
self.tile_pos_decimal = (0, 0)
|
||||||
|
|
||||||
def process_event(self, event):
|
def process_event(self, event):
|
||||||
|
"""Handle mouse clicks, keyboard input, and UI button presses.
|
||||||
|
|
||||||
|
Translates mouse position to tile coordinates, manages tower
|
||||||
|
placement and selection, camera movement keys, and zoom controls.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event (pg.event.Event): The pygame event to process.
|
||||||
|
"""
|
||||||
#calculate tile position from mouse position
|
#calculate tile position from mouse position
|
||||||
self.mpos = pg.mouse.get_pos()
|
self.mpos = pg.mouse.get_pos()
|
||||||
game_rect = self.game_surface.get_rect(topleft=(0, self.height / 6))
|
game_rect = self.game_surface.get_rect(topleft=(0, self.height / 6))
|
||||||
@@ -88,7 +115,7 @@ class GamePlay(BaseScreen):
|
|||||||
# self.tower = Tower(self.game, self.mpos, self.tile_pos, 'cannon')
|
# self.tower = Tower(self.game, self.mpos, self.tile_pos, 'cannon')
|
||||||
if event.ui_element == self.cancel_button:
|
if event.ui_element == self.cancel_button:
|
||||||
self.bought = False
|
self.bought = False
|
||||||
|
|
||||||
self.tile_pos = (int(self.tile_pos_decimal[0]), int(self.tile_pos_decimal[1]))
|
self.tile_pos = (int(self.tile_pos_decimal[0]), int(self.tile_pos_decimal[1]))
|
||||||
if event.type == pg.MOUSEBUTTONDOWN:
|
if event.type == pg.MOUSEBUTTONDOWN:
|
||||||
if self.tile_pos is not None:
|
if self.tile_pos is not None:
|
||||||
@@ -101,7 +128,7 @@ class GamePlay(BaseScreen):
|
|||||||
print(f'{self.selected}')
|
print(f'{self.selected}')
|
||||||
if self.bought:
|
if self.bought:
|
||||||
if self.game.tilemap.placeable(self.tile_pos):
|
if self.game.tilemap.placeable(self.tile_pos):
|
||||||
if not any(tower.tile_pos == self.tile_pos for tower in self.tower_group):
|
if not any(tw.tile_pos == self.tile_pos for tw in self.tower_group):
|
||||||
tower = Tower(self.game, self.mpos, self.tile_pos, 'cannon', 40)
|
tower = Tower(self.game, self.mpos, self.tile_pos, 'cannon', 40)
|
||||||
self.tower_group.add(tower)
|
self.tower_group.add(tower)
|
||||||
self.bought = False
|
self.bought = False
|
||||||
@@ -109,7 +136,7 @@ class GamePlay(BaseScreen):
|
|||||||
for tower in self.tower_group:
|
for tower in self.tower_group:
|
||||||
if tower.tile_pos == self.tile_pos:
|
if tower.tile_pos == self.tile_pos:
|
||||||
tower.kill()
|
tower.kill()
|
||||||
|
|
||||||
if event.type == pg.KEYDOWN:
|
if event.type == pg.KEYDOWN:
|
||||||
if event.key == pg.K_a:
|
if event.key == pg.K_a:
|
||||||
self.movment[0] = True
|
self.movment[0] = True
|
||||||
@@ -119,7 +146,7 @@ class GamePlay(BaseScreen):
|
|||||||
self.movment[2] = True
|
self.movment[2] = True
|
||||||
if event.key == pg.K_s:
|
if event.key == pg.K_s:
|
||||||
self.movment[3] = True
|
self.movment[3] = True
|
||||||
|
|
||||||
if event.key == pg.K_q:
|
if event.key == pg.K_q:
|
||||||
self.camera.zoom_offset(self.center, -0.25)
|
self.camera.zoom_offset(self.center, -0.25)
|
||||||
if event.key == pg.K_e:
|
if event.key == pg.K_e:
|
||||||
@@ -133,14 +160,26 @@ class GamePlay(BaseScreen):
|
|||||||
self.movment[2] = False
|
self.movment[2] = False
|
||||||
if event.key == pg.K_s:
|
if event.key == pg.K_s:
|
||||||
self.movment[3] = False
|
self.movment[3] = False
|
||||||
|
|
||||||
def update(self, dt):
|
def update(self, dt):
|
||||||
|
"""Update enemies, towers, and camera position.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dt (float): Time elapsed since the last frame in seconds.
|
||||||
|
"""
|
||||||
self.enemy_group.update()
|
self.enemy_group.update()
|
||||||
self.tower_group.update(self.enemy_group)
|
self.tower_group.update(self.enemy_group)
|
||||||
|
|
||||||
self.camera.move((self.movment[1] - self.movment[0]) * 2, (self.movment[3] - self.movment[2]) * 2)
|
self.camera.move(
|
||||||
|
(self.movment[1] - self.movment[0]) * 2, (self.movment[3] - self.movment[2]) * 2
|
||||||
|
)
|
||||||
|
|
||||||
def draw(self, surface):
|
def draw(self, surface):
|
||||||
|
"""Render the tilemap, enemies, towers, and tower placement preview.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
surface (pg.Surface): The main window surface to draw onto.
|
||||||
|
"""
|
||||||
self.game_surface.fill((40, 40, 40))
|
self.game_surface.fill((40, 40, 40))
|
||||||
# self.map_surface.fill((40, 40, 40))
|
# self.map_surface.fill((40, 40, 40))
|
||||||
|
|
||||||
@@ -151,22 +190,59 @@ class GamePlay(BaseScreen):
|
|||||||
|
|
||||||
if self.bought:
|
if self.bought:
|
||||||
if self.game.tilemap.placeable(self.tile_pos):
|
if self.game.tilemap.placeable(self.tile_pos):
|
||||||
self.game_surface.blit(pg.transform.scale2x(self.game.assets["cannon"]), (self.mpos[0] - (self.game.tilemap.tile_size //2), self.mpos[1] - (self.game.tilemap.tile_size //2 + self.height/6)))
|
self.game_surface.blit(
|
||||||
|
pg.transform.scale2x(self.game.assets["cannon"]),
|
||||||
|
(
|
||||||
|
self.mpos[0] - (self.game.tilemap.tile_size //2),
|
||||||
|
self.mpos[1] - (self.game.tilemap.tile_size //2 + self.height/6)
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.game_surface.blit(pg.transform.scale2x(self.game.assets["red_cannon"]), (self.mpos[0] - (self.game.tilemap.tile_size //2), self.mpos[1] - (self.game.tilemap.tile_size //2 + self.height/6)))
|
self.game_surface.blit(
|
||||||
|
pg.transform.scale2x(self.game.assets["red_cannon"]),
|
||||||
# self.game_surface.blit(pg.transform.scale(self.map_surface, self.game_surface.get_size()))
|
(self.mpos[0] - (self.game.tilemap.tile_size //2),
|
||||||
|
self.mpos[1] - (self.game.tilemap.tile_size //2 + self.height/6))
|
||||||
|
)
|
||||||
|
|
||||||
surface.blit(self.game_surface, (0, self.height/6))
|
surface.blit(self.game_surface, (0, self.height/6))
|
||||||
|
|
||||||
# if self.bought:
|
|
||||||
# self.game_surface.blit(pg.transform.scale2x(self.game.assets["cannon"]), (self.mpos[0] - (self.game.tilemap.tile_size //2), self.mpos[1] - (self.game.tilemap.tile_size //2)))
|
|
||||||
|
|
||||||
def select_turret(self, tile_pos):
|
def select_turret(self, tile_pos):
|
||||||
|
"""Return the tower at tile_pos, or ``None`` if the tile is empty.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tile_pos (tuple[int, int]): Grid position to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tower | None: The tower occupying that tile, or ``None``.
|
||||||
|
"""
|
||||||
for tower in self.tower_group:
|
for tower in self.tower_group:
|
||||||
if (tile_pos[0], tile_pos[1]) == (tower.tile_pos[0], tower.tile_pos[1]):
|
if (tile_pos[0], tile_pos[1]) == (tower.tile_pos[0], tower.tile_pos[1]):
|
||||||
return tower
|
return tower
|
||||||
|
return None
|
||||||
|
|
||||||
def clear_selection(self):
|
def clear_selection(self):
|
||||||
|
"""Deselect all towers."""
|
||||||
for tower in self.tower_group:
|
for tower in self.tower_group:
|
||||||
tower.selected = False
|
tower.selected = False
|
||||||
|
|
||||||
|
#private helpers jsut within class (planned not sure yet)
|
||||||
|
def _remove_tower_at(self, tile_pos):
|
||||||
|
"""Remove the tower at tile_pos on right click, if one exists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tile_pos (tuple[int, int]): Grid position to remove from.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _handle_keydown(self, key):
|
||||||
|
"""Set movement flags and trigger zoom on key press.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (int): pygame key constant.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _handle_keyup(self, key):
|
||||||
|
"""Clear movement flags on key release.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (int): pygame key constant.
|
||||||
|
"""
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
|
"""Main menu screen - title label and Play/Quit navigation."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import pygame_gui as pgi
|
||||||
|
import pygame as pg
|
||||||
from game.screens.base_screen import BaseScreen
|
from game.screens.base_screen import BaseScreen
|
||||||
|
|
||||||
import pygame as pg
|
|
||||||
import pygame_gui as pgi
|
|
||||||
|
|
||||||
class MainMenu(BaseScreen):
|
class MainMenu(BaseScreen):
|
||||||
|
"""The main menu screen with a title, Play button, and Quit button.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
title (pgi.elements.UILabel): The menu title label.
|
||||||
|
play_button (pgi.elements.UIButton): Transitions to the gameplay screen.
|
||||||
|
quit_button (pgi.elements.UIButton): Exits the application.
|
||||||
|
"""
|
||||||
def __init__(self, game):
|
def __init__(self, game):
|
||||||
|
|
||||||
super().__init__(game)
|
super().__init__(game)
|
||||||
@@ -66,6 +75,11 @@ class MainMenu(BaseScreen):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def process_event(self, event):
|
def process_event(self, event):
|
||||||
|
"""Handle Play and Quit button presses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event (pg.event.Event): The pygame event to process.
|
||||||
|
"""
|
||||||
|
|
||||||
if event.type == pgi.UI_BUTTON_PRESSED:
|
if event.type == pgi.UI_BUTTON_PRESSED:
|
||||||
|
|
||||||
@@ -75,4 +89,4 @@ class MainMenu(BaseScreen):
|
|||||||
|
|
||||||
if event.ui_element == self.quit_button:
|
if event.ui_element == self.quit_button:
|
||||||
pg.quit()
|
pg.quit()
|
||||||
exit()
|
sys.exit()
|
||||||
|
|||||||
+81
-28
@@ -1,11 +1,21 @@
|
|||||||
import pygame, json
|
"""Tilemap module - tile storage, serialisation, placement rules, and rendering.
|
||||||
|
|
||||||
"""
|
|
||||||
Todo:
|
Todo:
|
||||||
* Add prop system for Trees, etc. (like off_grid), handle blocked with game objects over props
|
* Add prop system for Trees, etc. (like off_grid), handle blocked with game objects over props
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
|
import pygame
|
||||||
|
|
||||||
class Tilemap:
|
class Tilemap:
|
||||||
|
"""Stores and manages the tile grid, waypoints, and rendering.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
tile_size (int): Width and height of a single tile in pixels.
|
||||||
|
tilemap (dict): Tile data keyed by ``"x;y"`` string positions.
|
||||||
|
planned: offgrid_tiles (list): Tiles placed outside the main grid (e.g. decorations).
|
||||||
|
waypoints (list): Ordered world positions enemies follow.
|
||||||
|
tower_placeable (list[dict]): Tile descriptors that allow tower placement.
|
||||||
|
"""
|
||||||
def __init__(self, game, tile=16):
|
def __init__(self, game, tile=16):
|
||||||
self.game = game
|
self.game = game
|
||||||
self.tile_size = tile
|
self.tile_size = tile
|
||||||
@@ -16,20 +26,42 @@ class Tilemap:
|
|||||||
self.tower_placeable = [{"type": "desert", "variant": 0}]
|
self.tower_placeable = [{"type": "desert", "variant": 0}]
|
||||||
|
|
||||||
def save(self, path):
|
def save(self, path):
|
||||||
f = open(path, 'w')
|
"""Serialise the tilemap, tile size, and waypoints to a JSON file.
|
||||||
json.dump({'tilemap': self.tilemap, 'tile_size': self.tile_size, 'waypoints': self.waypoints}, f)
|
|
||||||
f.close()
|
Args:
|
||||||
|
path (str): File path to write to.
|
||||||
|
"""
|
||||||
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump({
|
||||||
|
'tilemap': self.tilemap,
|
||||||
|
'tile_size': self.tile_size,
|
||||||
|
'waypoints': self.waypoints
|
||||||
|
}, f)
|
||||||
|
f.close()
|
||||||
|
|
||||||
def load(self, path):
|
def load(self, path):
|
||||||
f = open(path, 'r')
|
"""Load tilemap data from a JSON file, replacing current state.
|
||||||
map_data = json.load(f)
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
self.tilemap = map_data['tilemap']
|
Args:
|
||||||
self.tile_size = map_data['tile_size']
|
path (str): File path to read from.
|
||||||
self.waypoints = map_data['waypoints']
|
"""
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
map_data = json.load(f)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
self.tilemap = map_data['tilemap']
|
||||||
|
self.tile_size = map_data['tile_size']
|
||||||
|
self.waypoints = map_data['waypoints']
|
||||||
|
|
||||||
def placeable(self, tile_pos):
|
def placeable(self, tile_pos):
|
||||||
|
"""Check whether a tower can be placed on the tile at tile_pos.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tile_pos (tuple[int, int]): Grid position ``(x, y)`` to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: ``True`` if the tile exists and is in ``tower_placeable``.
|
||||||
|
"""
|
||||||
str_tile_pos = f'{tile_pos[0]};{tile_pos[1]}'
|
str_tile_pos = f'{tile_pos[0]};{tile_pos[1]}'
|
||||||
if str_tile_pos not in self.tilemap:
|
if str_tile_pos not in self.tilemap:
|
||||||
return False
|
return False
|
||||||
@@ -41,14 +73,39 @@ class Tilemap:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def render(self, surf, offset=(0, 0)):
|
def render(self, surf, offset=(0, 0)):
|
||||||
for x in range(offset[0] // self.tile_size, (offset[0] + surf.get_width()) // self.tile_size + 1):
|
"""Draw only the tiles visible within surf, using a pixel offset (editor).
|
||||||
for y in range(offset[1] // self.tile_size, (offset[1] + surf.get_height()) // self.tile_size + 1):
|
|
||||||
|
Args:
|
||||||
|
surf (pygame.Surface): Target surface to draw onto.
|
||||||
|
offset (tuple[int, int]): Pixel scroll offset ``(x, y)``.
|
||||||
|
"""
|
||||||
|
start_x = offset[0] // self.tile_size
|
||||||
|
end_x = (offset[0] + surf.get_width()) // self.tile_size + 1
|
||||||
|
|
||||||
|
start_y = offset[1] // self.tile_size
|
||||||
|
end_y = (offset[1] + surf.get_height()) // self.tile_size + 1
|
||||||
|
for x in range(start_x, end_x):
|
||||||
|
for y in range(start_y, end_y):
|
||||||
loc = str(x) + ';' + str(y)
|
loc = str(x) + ';' + str(y)
|
||||||
if loc in self.tilemap:
|
if loc in self.tilemap:
|
||||||
tile = self.tilemap[loc]
|
tile = self.tilemap[loc]
|
||||||
surf.blit(self.game.assets[tile['type']][tile['variant']], (tile['pos'][0] * self.tile_size - offset[0], tile['pos'][1] * self.tile_size - offset[1]))
|
surf.blit(
|
||||||
|
self.game.assets[tile['type']][tile['variant']],
|
||||||
|
(
|
||||||
|
tile['pos'][0] * self.tile_size - offset[0],
|
||||||
|
tile['pos'][1] * self.tile_size - offset[1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def render_game(self, surf, camera):
|
def render_game(self, surf, camera):
|
||||||
|
"""Draw only the tiles visible within surf, scaled to the camera zoom and pixel offset.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
surf (pygame.Surface): Target surface to draw onto.
|
||||||
|
camera (Camera): Active camera for zoom level and coordinate conversion.
|
||||||
|
|
||||||
|
Todo: document this code below, not full understanding
|
||||||
|
"""
|
||||||
tile_size = self.tile_size
|
tile_size = self.tile_size
|
||||||
zoom = camera.zoom
|
zoom = camera.zoom
|
||||||
scaled_tile_size = int(tile_size * zoom)
|
scaled_tile_size = int(tile_size * zoom)
|
||||||
@@ -67,16 +124,12 @@ class Tilemap:
|
|||||||
tile = self.tilemap[loc]
|
tile = self.tilemap[loc]
|
||||||
image = self.game.assets[tile["type"]][tile["variant"]]
|
image = self.game.assets[tile["type"]][tile["variant"]]
|
||||||
|
|
||||||
scaled_image = pygame.transform.scale(
|
surf.blit(pygame.transform.scale(
|
||||||
image,
|
image,
|
||||||
(scaled_tile_size, scaled_tile_size)
|
(scaled_tile_size, scaled_tile_size)
|
||||||
)
|
), camera.world_to_screen(
|
||||||
|
(
|
||||||
world_pos = (
|
tile["pos"][0] * tile_size,
|
||||||
tile["pos"][0] * tile_size,
|
tile["pos"][1] * tile_size
|
||||||
tile["pos"][1] * tile_size
|
)
|
||||||
)
|
))
|
||||||
|
|
||||||
screen_pos = camera.world_to_screen(world_pos)
|
|
||||||
|
|
||||||
surf.blit(scaled_image, screen_pos)
|
|
||||||
|
|||||||
+72
-9
@@ -1,8 +1,25 @@
|
|||||||
import pygame as pg
|
"""Tower module - tower placement, targeting, shooting, and rendering."""
|
||||||
from game.util import *
|
|
||||||
import math
|
import math
|
||||||
|
import pygame as pg
|
||||||
|
from game.util import tile_to_pixel
|
||||||
|
|
||||||
class Tower(pg.sprite.Sprite):
|
class Tower(pg.sprite.Sprite):
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
"""A player-placed tower that targets and damages nearby enemies.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
type (str): Asset key identifying the tower variant.
|
||||||
|
range (float): Attack radius in world-space pixels.
|
||||||
|
level (int): Current upgrade level.
|
||||||
|
selected (bool): Whether the tower is selected (draws range circle).
|
||||||
|
angle (float): Current facing angle in degrees.
|
||||||
|
target (Enemy | None): The enemy currently being targeted, or ``None``.
|
||||||
|
fire_rate (int): Frames between shots.
|
||||||
|
fire_cooldown (int): Frames remaining until the next shot.
|
||||||
|
damage (float): Damage dealt per shot.
|
||||||
|
pos (tuple): World-space pixel centre position.
|
||||||
|
tile_pos (tuple): Grid position the tower occupies.
|
||||||
|
"""
|
||||||
def __init__(self, game, pos, tile_pos, type, range, level=1):
|
def __init__(self, game, pos, tile_pos, type, range, level=1):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.game = game
|
self.game = game
|
||||||
@@ -27,6 +44,11 @@ class Tower(pg.sprite.Sprite):
|
|||||||
self.set_action('shoot')
|
self.set_action('shoot')
|
||||||
|
|
||||||
def update(self, enemy_group):
|
def update(self, enemy_group):
|
||||||
|
"""Advance animation, pick a target, rotate toward it, and shoot if ready.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enemy_group (EnemyGroup -> SpriteGroup): All active enemies to consider as targets.
|
||||||
|
"""
|
||||||
self.animation.update()
|
self.animation.update()
|
||||||
self.pick_target(enemy_group)
|
self.pick_target(enemy_group)
|
||||||
self.rotate()
|
self.rotate()
|
||||||
@@ -39,11 +61,24 @@ class Tower(pg.sprite.Sprite):
|
|||||||
self.shoot()
|
self.shoot()
|
||||||
|
|
||||||
def set_action(self, action):
|
def set_action(self, action):
|
||||||
|
"""Switch the active animation if it differs from the current one.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
action (str): Animation key (e.g. ``"shoot"``).
|
||||||
|
"""
|
||||||
if action != self.action:
|
if action != self.action:
|
||||||
self.action = action
|
self.action = action
|
||||||
self.animation = self.game.assets[self.type + '/' + self.action].copy()
|
self.animation = self.game.assets[self.type + '/' + self.action].copy()
|
||||||
|
|
||||||
def render(self, surf, camera):
|
def render(self, surf, camera):
|
||||||
|
"""Draw the tower animation frame, rotated and scaled to the camera zoom.
|
||||||
|
|
||||||
|
Also draws a semi-transparent range circle when the tower is selected.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
surf (pg.Surface): Target surface to draw onto.
|
||||||
|
camera (Camera): Active camera for zoom and coordinate conversion.
|
||||||
|
"""
|
||||||
screen_pos = camera.world_to_screen(pg.Vector2(
|
screen_pos = camera.world_to_screen(pg.Vector2(
|
||||||
tile_to_pixel(self.tile_pos, self.game.tilemap.tile_size, True)
|
tile_to_pixel(self.tile_pos, self.game.tilemap.tile_size, True)
|
||||||
))
|
))
|
||||||
@@ -61,15 +96,22 @@ class Tower(pg.sprite.Sprite):
|
|||||||
self.rect = scaled_image.get_rect(center=screen_pos)
|
self.rect = scaled_image.get_rect(center=screen_pos)
|
||||||
|
|
||||||
if self.selected:
|
if self.selected:
|
||||||
|
#draw circle around tower if selected
|
||||||
scaled_range = int(self.range * camera.zoom)
|
scaled_range = int(self.range * camera.zoom)
|
||||||
range_surf = pg.Surface((scaled_range * 2, scaled_range * 2), pg.SRCALPHA)
|
range_surf = pg.Surface((scaled_range * 2, scaled_range * 2), pg.SRCALPHA)
|
||||||
pg.draw.circle(range_surf, (200, 200, 200, 100), (scaled_range, scaled_range), scaled_range)
|
pg.draw.circle(
|
||||||
|
range_surf,
|
||||||
|
(200, 200, 200, 100),
|
||||||
|
(scaled_range, scaled_range),
|
||||||
|
scaled_range
|
||||||
|
)
|
||||||
range_rect = range_surf.get_rect(center=screen_pos)
|
range_rect = range_surf.get_rect(center=screen_pos)
|
||||||
surf.blit(range_surf, range_rect)
|
surf.blit(range_surf, range_rect)
|
||||||
|
|
||||||
surf.blit(scaled_image, self.rect)
|
surf.blit(scaled_image, self.rect)
|
||||||
|
|
||||||
def rotate(self):
|
def rotate(self):
|
||||||
|
"""Face the tower toward its current target, or hold the last angle if no target."""
|
||||||
if self.target is None:
|
if self.target is None:
|
||||||
# print("no target")
|
# print("no target")
|
||||||
return # keep last angle, or reset to a default — your call
|
return # keep last angle, or reset to a default — your call
|
||||||
@@ -80,8 +122,15 @@ class Tower(pg.sprite.Sprite):
|
|||||||
direction = self.target.pos - tower_world_pos
|
direction = self.target.pos - tower_world_pos
|
||||||
self.angle = math.degrees(math.atan2(-direction[1], direction[0])) -90
|
self.angle = math.degrees(math.atan2(-direction[1], direction[0])) -90
|
||||||
# print(f"angle {self.angle}")
|
# print(f"angle {self.angle}")
|
||||||
|
|
||||||
def pick_target(self, enemy_group):
|
def pick_target(self, enemy_group):
|
||||||
|
"""Select the closest enemy within range as the current target.
|
||||||
|
|
||||||
|
Sets ``self.target`` to ``None`` if no enemy is in range.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enemy_group (EnemyGroup): All active enemies to search through.
|
||||||
|
"""
|
||||||
closest = None
|
closest = None
|
||||||
closest_dist = float('inf')
|
closest_dist = float('inf')
|
||||||
|
|
||||||
@@ -96,17 +145,31 @@ class Tower(pg.sprite.Sprite):
|
|||||||
closest_dist = distance
|
closest_dist = distance
|
||||||
|
|
||||||
self.target = closest # explicitly None if nothing in range
|
self.target = closest # explicitly None if nothing in range
|
||||||
|
|
||||||
def shoot(self):
|
def shoot(self):
|
||||||
|
"""Deal damage to the current target, play the shoot animation, and reset cooldown."""
|
||||||
self.target.take_damage(self.damage)
|
self.target.take_damage(self.damage)
|
||||||
self.animation.play_animation()
|
self.animation.play_animation()
|
||||||
self.fire_cooldown = self.fire_rate
|
self.fire_cooldown = self.fire_rate
|
||||||
|
|
||||||
class TowerGroup(pg.sprite.Group):
|
class TowerGroup(pg.sprite.Group):
|
||||||
|
"""Sprite group that delegates rendering and updates to each tower."""
|
||||||
|
|
||||||
def render(self, surf, camera):
|
def render(self, surf, camera):
|
||||||
|
"""Render all towers in the group.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
surf (pg.Surface): Target surface to draw onto.
|
||||||
|
camera (Camera): Active camera for zoom and coordinate conversion.
|
||||||
|
"""
|
||||||
for tower in self.sprites():
|
for tower in self.sprites():
|
||||||
tower.render(surf, camera)
|
tower.render(surf, camera)
|
||||||
|
|
||||||
def update(self, enemies):
|
def update(self, enemies):
|
||||||
|
"""Update all towers in the group.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enemies (EnemyGroup): All active enemies, passed to each tower's update.
|
||||||
|
"""
|
||||||
for tower in self.sprites():
|
for tower in self.sprites():
|
||||||
tower.update(enemies)
|
tower.update(enemies)
|
||||||
|
|||||||
+89
-14
@@ -1,49 +1,112 @@
|
|||||||
import pygame as pg
|
"""Utility module - image loading, coordinate conversion, and sprite animation."""
|
||||||
import os
|
import os
|
||||||
|
import pygame as pg
|
||||||
|
|
||||||
img_path='assets/images'
|
IMG_PATH='assets/images'
|
||||||
WINDOW_SIZE=(800,600)
|
WINDOW_SIZE=(800,600)
|
||||||
|
|
||||||
def load_image(path, colorkey=(0,0,0)):
|
def load_image(path, colorkey=(0,0,0)):
|
||||||
img = pg.image.load(img_path + path).convert()
|
"""Load an image and set a colorkey for transparency.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): Path to the image relative to ``IMG_PATH``.
|
||||||
|
colorkey (tuple[int,int,int]): RGB colour treated as transparent. Defaults to black.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pg.Surface: The loaded and converted image.
|
||||||
|
"""
|
||||||
|
img = pg.image.load(IMG_PATH + path).convert()
|
||||||
img.set_colorkey(colorkey)
|
img.set_colorkey(colorkey)
|
||||||
return img
|
return img
|
||||||
|
|
||||||
def load_images(dir_path):
|
def load_images(dir_path):
|
||||||
|
"""Load all images from a directory using colorkey transparency.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dir_path (str): Directory path relative to ``IMG_PATH``.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[pg.Surface]: All images found in the directory, in filesystem order.
|
||||||
|
"""
|
||||||
images = []
|
images = []
|
||||||
for img_name in os.listdir(img_path + dir_path):
|
for img_name in os.listdir(IMG_PATH + dir_path):
|
||||||
images.append(load_image(dir_path + '/' + img_name))
|
images.append(load_image(dir_path + '/' + img_name))
|
||||||
return images
|
return images
|
||||||
|
|
||||||
def load_image_alpha(path):
|
def load_image_alpha(path):
|
||||||
img = pg.image.load(img_path + path).convert_alpha()
|
"""Load an image preserving its alpha channel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): Path to the image relative to ``IMG_PATH``.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pg.Surface: The loaded image with per-pixel alpha.
|
||||||
|
"""
|
||||||
|
img = pg.image.load(IMG_PATH + path).convert_alpha()
|
||||||
return img
|
return img
|
||||||
|
|
||||||
def load_images_alpha(dir_path):
|
def load_images_alpha(dir_path):
|
||||||
|
"""Load all images from a directory, preserving alpha channels.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dir_path (str): Directory path relative to ``IMG_PATH``.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[pg.Surface]: All images found in the directory, in filesystem order.
|
||||||
|
"""
|
||||||
images = []
|
images = []
|
||||||
for img_name in os.listdir(img_path + dir_path):
|
for img_name in os.listdir(IMG_PATH + dir_path):
|
||||||
images.append(load_image_alpha(dir_path + '/' + img_name))
|
images.append(load_image_alpha(dir_path + '/' + img_name))
|
||||||
return images
|
return images
|
||||||
|
|
||||||
def tile_to_pixel(waypoint, tile_size, center=False):
|
def tile_to_pixel(waypoint, tile_size, center=False):
|
||||||
|
"""Convert a tile grid position to world-space pixel coordinates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tile_pos (tuple[int, int]): Grid position ``(x, y)``.
|
||||||
|
tile_size (int): Width and height of a single tile in pixels.
|
||||||
|
center (bool): If ``True``, return the centre of the tile. Defaults to ``False``.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[int, int]: Pixel position ``(x, y)``.
|
||||||
|
"""
|
||||||
if center:
|
if center:
|
||||||
return (
|
return (
|
||||||
waypoint[0] * tile_size + tile_size // 2,
|
waypoint[0] * tile_size + tile_size // 2,
|
||||||
waypoint[1] * tile_size + tile_size // 2
|
waypoint[1] * tile_size + tile_size // 2
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
return (
|
return (
|
||||||
waypoint[0] * tile_size,
|
waypoint[0] * tile_size,
|
||||||
waypoint[1] * tile_size
|
waypoint[1] * tile_size
|
||||||
)
|
)
|
||||||
|
|
||||||
def pixel_to_tile(waypoint, tile_size):
|
def pixel_to_tile(waypoint, tile_size):
|
||||||
|
"""Convert a world-space pixel position to tile grid coordinates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
pixel_pos (tuple[float, float]): Pixel position ``(x, y)``.
|
||||||
|
tile_size (int): Width and height of a single tile in pixels.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[float, float]: Fractional tile position ``(x, y)``.
|
||||||
|
"""
|
||||||
return (
|
return (
|
||||||
waypoint[0] / tile_size,
|
waypoint[0] / tile_size,
|
||||||
waypoint[1] / tile_size
|
waypoint[1] / tile_size
|
||||||
)
|
)
|
||||||
|
|
||||||
class Animation:
|
class Animation:
|
||||||
|
"""Frame-based sprite animation with optional looping and manual play control.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
images (list[pg.Surface]): Ordered list of animation frames.
|
||||||
|
loop (bool): Whether the animation loops continuously.
|
||||||
|
img_duration (int): Number of update ticks each frame is shown for.
|
||||||
|
done (bool): ``True`` once a non-looping animation has finished.
|
||||||
|
frame (int): Current tick counter used to derive the active frame index.
|
||||||
|
playing (bool): Whether the animation is currently advancing.
|
||||||
|
"""
|
||||||
def __init__(self, images, img_dur=5, loop=True):
|
def __init__(self, images, img_dur=5, loop=True):
|
||||||
self.images = images
|
self.images = images
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
@@ -53,9 +116,15 @@ class Animation:
|
|||||||
self.playing = False # not playing until told to
|
self.playing = False # not playing until told to
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
|
"""Return a fresh copy of this animation, reset to its initial state.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Animation: A new Animation sharing the same images and settings.
|
||||||
|
"""
|
||||||
return Animation(self.images, self.img_duration, self.loop)
|
return Animation(self.images, self.img_duration, self.loop)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
|
"""Advance the animation by one tick if it is currently playing."""
|
||||||
if not self.playing:
|
if not self.playing:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -68,9 +137,15 @@ class Animation:
|
|||||||
self.playing = False
|
self.playing = False
|
||||||
|
|
||||||
def play_animation(self):
|
def play_animation(self):
|
||||||
|
"""Start the animation from the first frame."""
|
||||||
self.playing = True
|
self.playing = True
|
||||||
self.done = False
|
self.done = False
|
||||||
self.frame = 0
|
self.frame = 0
|
||||||
|
|
||||||
def img(self):
|
def img(self):
|
||||||
return self.images[self.frame // self.img_duration]
|
"""Return the image for the current tick.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pg.Surface: The active animation frame.
|
||||||
|
"""
|
||||||
|
return self.images[self.frame // self.img_duration]
|
||||||
|
|||||||
Reference in New Issue
Block a user