From 821aa2e8a489102fc946e7310f749d7f3fe7be9b Mon Sep 17 00:00:00 2001 From: ScriptSorcerer Date: Wed, 15 Jul 2026 22:16:18 +0200 Subject: [PATCH] added docstrings to every file and function. (with claude, checked it) tower and game_play dont meet pylint check yet. --- .pylintrc | 3 + game/enemy.py | 26 +++++---- game/game.py | 46 ++++++++------- game/screens/base_screen.py | 35 +++++++++-- game/screens/game_play.py | 112 ++++++++++++++++++++++++++++++------ game/screens/main_menu.py | 22 +++++-- game/tilemap.py | 109 ++++++++++++++++++++++++++--------- game/tower.py | 81 +++++++++++++++++++++++--- game/util.py | 103 ++++++++++++++++++++++++++++----- 9 files changed, 428 insertions(+), 109 deletions(-) diff --git a/.pylintrc b/.pylintrc index b6db2d1..b5e7d32 100644 --- a/.pylintrc +++ b/.pylintrc @@ -655,3 +655,6 @@ init-import=no # List of qualified module names which can have objects that can redefine # builtins. redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + +[MASTER] +extension-pkg-allow-list=pygame \ No newline at end of file diff --git a/game/enemy.py b/game/enemy.py index 89fcd9e..fafdc65 100644 --- a/game/enemy.py +++ b/game/enemy.py @@ -6,6 +6,7 @@ import pygame as pg from game.util import tile_to_pixel class Enemy(pg.sprite.Sprite): + # pylint: disable=too-many-instance-attributes """A single enemy that follows waypoints across the tilemap. @@ -13,31 +14,33 @@ class Enemy(pg.sprite.Sprite): game (Game): Whole Game instances, contains everything that the game class has. speed (float): Movement speed in pixels per update. 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): super().__init__() self.game = game + # coordinate on where the enemy should move to (in map json) self.waypoints = self.game.tilemap.waypoints - self.speed = speed - self.angle = 0 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.rect = self.image.get_rect() - self.pos = pg.Vector2( tile_to_pixel(self.waypoints[0], self.game.tilemap.tile_size, True) ) - - self.target_index = 1 self.rect.center = self.pos - self.healthbar = HealthBar( - 18, - 5, - self.health - ) def update(self): """Advance movement and update rotation each frame.""" @@ -133,6 +136,7 @@ class EnemyGroup(pg.sprite.Group): enemy.update() class HealthBar(): + # pylint: disable=too-few-public-methods """A simple two-rect health bar drawn in world space. Attributes: diff --git a/game/game.py b/game/game.py index 8e0be4c..d4a48ab 100644 --- a/game/game.py +++ b/game/game.py @@ -1,21 +1,28 @@ -""" -game class - -""" -import pygame +"""Game module - core game loop, screen management, and shared asset loading.""" 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.screens.main_menu import MainMenu from game.screens.game_play import GamePlay +#constants FPS=60 WINDOW_SIZE=(960,540) -TILE=16 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): pygame.init() @@ -45,40 +52,39 @@ class Game: self.current_screen = self.screens["menu"] 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 = self.screens[name] - self.current_screen.show() 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 - while running: - dt = self.clock.tick(FPS) / 1000.0 for event in pygame.event.get(): - if event.type == pygame.QUIT: running = False self.ui_manager.process_events(event) - self.current_screen.process_event(event) - + self.current_screen.update(dt) - self.ui_manager.update(dt) - self.window.fill((20, 20, 20)) - self.current_screen.draw(self.window) - self.ui_manager.draw_ui(self.window) pygame.display.update() - - pygame.quit() \ No newline at end of file + pygame.quit() diff --git a/game/screens/base_screen.py b/game/screens/base_screen.py index ea7d2a9..628db39 100644 --- a/game/screens/base_screen.py +++ b/game/screens/base_screen.py @@ -1,23 +1,48 @@ +"""Base screen module - shared interface for all game screens.""" + 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): self.game = game - self.elements = [] def process_event(self, event): - pass + """Handle a single pygame event. + + Args: + event (pygame.event.Event): The event to process. + """ 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): - pass + """Draw the screen onto the given surface. + + Args: + surface (pygame.Surface): Target surface to draw onto. + """ def show(self): + """Make all UI elements on this screen visible.""" for element in self.elements: element.show() def hide(self): + """Hide all UI elements on this screen.""" for element in self.elements: - element.hide() \ No newline at end of file + element.hide() diff --git a/game/screens/game_play.py b/game/screens/game_play.py index 3c0db7b..079c7e9 100644 --- a/game/screens/game_play.py +++ b/game/screens/game_play.py @@ -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.enemy import Enemy, EnemyGroup from game.camera import Camera from game.tower import Tower, TowerGroup -from game.util import * -import pygame as pg -import pygame_gui as pgi +from game.util import pixel_to_tile 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): super().__init__(game) # self.tile_multiplicator = 2.0 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.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.center = (self.game_surface.get_width() /2, self.game_surface.get_height() /2) @@ -65,7 +80,19 @@ class GamePlay(BaseScreen): self.cancel_button ] + self.mpos = (0, 0) + self.tile_pos = None + self.tile_pos_decimal = (0, 0) + 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 self.mpos = pg.mouse.get_pos() 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') if event.ui_element == self.cancel_button: self.bought = False - + self.tile_pos = (int(self.tile_pos_decimal[0]), int(self.tile_pos_decimal[1])) if event.type == pg.MOUSEBUTTONDOWN: if self.tile_pos is not None: @@ -101,7 +128,7 @@ class GamePlay(BaseScreen): print(f'{self.selected}') if self.bought: 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) self.tower_group.add(tower) self.bought = False @@ -109,7 +136,7 @@ class GamePlay(BaseScreen): for tower in self.tower_group: if tower.tile_pos == self.tile_pos: tower.kill() - + if event.type == pg.KEYDOWN: if event.key == pg.K_a: self.movment[0] = True @@ -119,7 +146,7 @@ class GamePlay(BaseScreen): self.movment[2] = True if event.key == pg.K_s: self.movment[3] = True - + if event.key == pg.K_q: self.camera.zoom_offset(self.center, -0.25) if event.key == pg.K_e: @@ -133,14 +160,26 @@ class GamePlay(BaseScreen): self.movment[2] = False if event.key == pg.K_s: self.movment[3] = False - + 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.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): + """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.map_surface.fill((40, 40, 40)) @@ -151,22 +190,59 @@ class GamePlay(BaseScreen): if self.bought: 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: - 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.scale(self.map_surface, self.game_surface.get_size())) + 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)) + ) 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): + """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: if (tile_pos[0], tile_pos[1]) == (tower.tile_pos[0], tower.tile_pos[1]): return tower + return None def clear_selection(self): + """Deselect all towers.""" for tower in self.tower_group: - tower.selected = False \ No newline at end of file + 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. + """ diff --git a/game/screens/main_menu.py b/game/screens/main_menu.py index ca36441..0a7f40c 100644 --- a/game/screens/main_menu.py +++ b/game/screens/main_menu.py @@ -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 -import pygame as pg -import pygame_gui as pgi - 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): super().__init__(game) @@ -66,6 +75,11 @@ class MainMenu(BaseScreen): ] 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: @@ -75,4 +89,4 @@ class MainMenu(BaseScreen): if event.ui_element == self.quit_button: pg.quit() - exit() \ No newline at end of file + sys.exit() diff --git a/game/tilemap.py b/game/tilemap.py index e1bd591..c377e1d 100644 --- a/game/tilemap.py +++ b/game/tilemap.py @@ -1,11 +1,21 @@ -import pygame, json +"""Tilemap module - tile storage, serialisation, placement rules, and rendering. -""" Todo: * Add prop system for Trees, etc. (like off_grid), handle blocked with game objects over props """ +import json +import pygame 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): self.game = game self.tile_size = tile @@ -16,20 +26,42 @@ class Tilemap: self.tower_placeable = [{"type": "desert", "variant": 0}] def save(self, path): - f = open(path, 'w') - json.dump({'tilemap': self.tilemap, 'tile_size': self.tile_size, 'waypoints': self.waypoints}, f) - f.close() + """Serialise the tilemap, tile size, and waypoints to a JSON file. + + 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): - f = open(path, 'r') - map_data = json.load(f) - f.close() + """Load tilemap data from a JSON file, replacing current state. - self.tilemap = map_data['tilemap'] - self.tile_size = map_data['tile_size'] - self.waypoints = map_data['waypoints'] + Args: + path (str): File path to read from. + """ + 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): + """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]}' if str_tile_pos not in self.tilemap: return False @@ -41,14 +73,39 @@ class Tilemap: return False 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): - for y in range(offset[1] // self.tile_size, (offset[1] + surf.get_height()) // self.tile_size + 1): + """Draw only the tiles visible within surf, using a pixel offset (editor). + + 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) if loc in self.tilemap: 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): + """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 zoom = camera.zoom scaled_tile_size = int(tile_size * zoom) @@ -67,16 +124,12 @@ class Tilemap: tile = self.tilemap[loc] image = self.game.assets[tile["type"]][tile["variant"]] - scaled_image = pygame.transform.scale( - image, - (scaled_tile_size, scaled_tile_size) - ) - - world_pos = ( - tile["pos"][0] * tile_size, - tile["pos"][1] * tile_size - ) - - screen_pos = camera.world_to_screen(world_pos) - - surf.blit(scaled_image, screen_pos) + surf.blit(pygame.transform.scale( + image, + (scaled_tile_size, scaled_tile_size) + ), camera.world_to_screen( + ( + tile["pos"][0] * tile_size, + tile["pos"][1] * tile_size + ) + )) diff --git a/game/tower.py b/game/tower.py index f746748..4a25092 100644 --- a/game/tower.py +++ b/game/tower.py @@ -1,8 +1,25 @@ -import pygame as pg -from game.util import * +"""Tower module - tower placement, targeting, shooting, and rendering.""" import math +import pygame as pg +from game.util import tile_to_pixel 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): super().__init__() self.game = game @@ -27,6 +44,11 @@ class Tower(pg.sprite.Sprite): self.set_action('shoot') 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.pick_target(enemy_group) self.rotate() @@ -39,11 +61,24 @@ class Tower(pg.sprite.Sprite): self.shoot() 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: self.action = action 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( 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) if self.selected: + #draw circle around tower if selected scaled_range = int(self.range * camera.zoom) 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) surf.blit(range_surf, range_rect) surf.blit(scaled_image, self.rect) - + def rotate(self): + """Face the tower toward its current target, or hold the last angle if no target.""" if self.target is None: # print("no target") 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 self.angle = math.degrees(math.atan2(-direction[1], direction[0])) -90 # print(f"angle {self.angle}") - + 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_dist = float('inf') @@ -96,17 +145,31 @@ class Tower(pg.sprite.Sprite): closest_dist = distance self.target = closest # explicitly None if nothing in range - + def shoot(self): + """Deal damage to the current target, play the shoot animation, and reset cooldown.""" self.target.take_damage(self.damage) self.animation.play_animation() self.fire_cooldown = self.fire_rate class TowerGroup(pg.sprite.Group): + """Sprite group that delegates rendering and updates to each tower.""" + 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(): tower.render(surf, camera) - + 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(): - tower.update(enemies) \ No newline at end of file + tower.update(enemies) diff --git a/game/util.py b/game/util.py index 9d63f3a..8458279 100644 --- a/game/util.py +++ b/game/util.py @@ -1,49 +1,112 @@ -import pygame as pg +"""Utility module - image loading, coordinate conversion, and sprite animation.""" import os +import pygame as pg -img_path='assets/images' +IMG_PATH='assets/images' WINDOW_SIZE=(800,600) 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) return img 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 = [] - 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)) return images 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 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 = [] - 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)) return images 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: return ( waypoint[0] * tile_size + tile_size // 2, waypoint[1] * tile_size + tile_size // 2 ) - else: - return ( - waypoint[0] * tile_size, - waypoint[1] * tile_size - ) + + return ( + waypoint[0] * tile_size, + waypoint[1] * 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 ( waypoint[0] / tile_size, waypoint[1] / tile_size ) 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): self.images = images self.loop = loop @@ -53,9 +116,15 @@ class Animation: self.playing = False # not playing until told to 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) - + def update(self): + """Advance the animation by one tick if it is currently playing.""" if not self.playing: return @@ -68,9 +137,15 @@ class Animation: self.playing = False def play_animation(self): + """Start the animation from the first frame.""" self.playing = True self.done = False self.frame = 0 - + def img(self): - return self.images[self.frame // self.img_duration] \ No newline at end of file + """Return the image for the current tick. + + Returns: + pg.Surface: The active animation frame. + """ + return self.images[self.frame // self.img_duration]