diff --git a/engine/build_tools.py b/engine/build_tools.py index 7571554..d9da939 100644 --- a/engine/build_tools.py +++ b/engine/build_tools.py @@ -5,8 +5,6 @@ Build and development utilities for the engine. """ -import os - import shutil from tkinter import messagebox @@ -19,17 +17,14 @@ def build(directory: Path, ENGINE_DATA_PATH: str) -> None: - """ - Build the game + """Build the game. Args: directory (Path): Path to build the game to ENGINE_DATA_PATH (str): Path of the data directory """ - launch_game_script = None - - if not os.path.exists(directory): + if not directory.exists(): logger( f'Build directory "{str(directory.resolve())}" does not exist.', status=Status.WARNING ) @@ -39,19 +34,12 @@ def build(directory: Path, ENGINE_DATA_PATH: str) -> None: ) return - with open(resource_path("data/scripts/launch_game.py")) as f: - launch_game_script = f.read() - f.close() - - with open(os.path.join(directory, "run.py"), "w") as f: - f.write(launch_game_script) - f.close() + launch_game_script = Path(resource_path("data/scripts/launch_game.py")).read_text( + encoding="utf-8" + ) + (directory / "run.py").write_text(launch_game_script, encoding="utf-8") ignore = shutil.ignore_patterns("*.pyc", "__pycache__") - shutil.copytree( - engine_path, os.path.join(directory, "engine"), dirs_exist_ok=True, ignore=ignore - ) - shutil.copytree( - ENGINE_DATA_PATH, os.path.join(directory, "data"), dirs_exist_ok=True, ignore=ignore - ) + shutil.copytree(engine_path, directory / "engine", dirs_exist_ok=True, ignore=ignore) + shutil.copytree(Path(ENGINE_DATA_PATH), directory / "data", dirs_exist_ok=True, ignore=ignore) diff --git a/engine/core/__init__.py b/engine/core/__init__.py index 8938d79..4dc4970 100644 --- a/engine/core/__init__.py +++ b/engine/core/__init__.py @@ -303,9 +303,6 @@ def add(self, obj: Entity) -> None: if not obj.did_init: obj.init() - if self.no_entities: - self.no_entities = False - def update(self, dt: float) -> None: """ Update all entities in the scene. @@ -314,9 +311,8 @@ def update(self, dt: float) -> None: dt (float): Time elapsed since last update """ - if not self.no_entities: - for obj in self.objects: - obj.update(dt) + for obj in self.objects: + obj.update(dt) def event(self, event: pygame.event.Event) -> None: """ @@ -326,9 +322,8 @@ def event(self, event: pygame.event.Event) -> None: event (pygame.event.Event): Event passed to each entity. """ - if not self.no_entities: - for obj in self.objects: - obj.event(event) + for obj in self.objects: + obj.event(event) def draw(self, surface: pygame.Surface) -> None: """ @@ -338,9 +333,8 @@ def draw(self, surface: pygame.Surface) -> None: surface (pygame.Surface): Surface to draw entities onto. """ - if not self.no_entities: - for obj in self.objects: - obj.draw(surface) + for obj in self.objects: + obj.draw(surface) def remove(self, obj: Entity) -> None: """ @@ -351,7 +345,6 @@ def remove(self, obj: Entity) -> None: """ self.objects.remove(obj) - self.no_entities = len(self.objects) == 0 class Game: @@ -522,15 +515,17 @@ def step(self, dt: float) -> None: dt (float): The time elapsed since the last step. """ + active_scene = self.scenes[self.current_scene] + for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False else: - self.scenes[self.current_scene].event(event) + active_scene.event(event) - self.scenes[self.current_scene].update(dt) + active_scene.update(dt) self.screen.fill(self._bg_color) - self.scenes[self.current_scene].draw(self.screen) + active_scene.draw(self.screen) pygame.display.flip() def run(self, fps: int = 60) -> None: diff --git a/engine/core/image.py b/engine/core/image.py index 05f86f4..a7e33eb 100644 --- a/engine/core/image.py +++ b/engine/core/image.py @@ -16,43 +16,30 @@ class EntityImage: """ surface: Optional[pygame.Surface] + _scaled_surface: Optional[pygame.Surface] + _scaled_size: tuple[int, int] | None def __init__(self, image_path: str) -> None: - """ - Initialize the EntityImage by loading the image at ``image_path``. - - Args: - image_path (str): The path to the image file. - """ - self.surface = None - + self._scaled_surface = None + self._scaled_size = None self.set_image(image_path) def set_image(self, image_path: str) -> None: - """ - Load ``image_path`` and store it as an alpha-enabled pygame surface. - - Args: - image_path (str): The path to the image file. - """ - assert pygame.get_init(), ( # nosec B101 "EntityImage: pygame must be initialized before loading images" ) self.surface = pygame.image.load(image_path).convert_alpha() + self._scaled_surface = None + self._scaled_size = None def draw(self, surface: pygame.Surface, rect: pygame.Rect) -> None: - """ - Draw the image scaled to ``rect`` onto ``surface``. - - Args: - surface (pygame.Surface): surface to draw onto - rect (pygame.Rect): rect to scale image to - """ - assert self.surface is not None, "EntityImage.surface was not initialized" # nosec B101 - scaled_image = pygame.transform.scale(self.surface, (rect.width, rect.height)) - surface.blit(scaled_image, (rect.x, rect.y)) + size = (rect.width, rect.height) + if self._scaled_surface is None or self._scaled_size != size: + self._scaled_surface = pygame.transform.scale(self.surface, size) + self._scaled_size = size + + surface.blit(self._scaled_surface, (rect.x, rect.y)) diff --git a/engine/saveload.py b/engine/saveload.py index c0c5df5..9902352 100644 --- a/engine/saveload.py +++ b/engine/saveload.py @@ -1,110 +1,99 @@ # Copyright (C) Natuworkguy # See the LICENSE file for GPLv3 -""" -Handles saving and loading of engine projects and data. -""" +"""Handles saving and loading of engine projects and data.""" -from tkinter import messagebox as messagebox -from tkinter import filedialog as filedialog from json import dump, load -from typing import Optional, Any +import os from pathlib import Path - -from .logger import logger, Status as LoggerStatus - import sys -import os +from tkinter import filedialog, messagebox +from typing import Any, Dict, Optional, Tuple + +from .logger import Status as LoggerStatus, logger def resource_path(relative: str) -> str: - """ - Convert a relative resource path into an absolute path. + """Convert a relative resource path into an absolute path. Args: - relative (str): Relative path to a resource. + relative: The relative path to the resource file. Returns: - str: Absolute path to the resource. + The absolute file path as a string. """ - if hasattr(sys, "_MEIPASS"): - return os.path.join(sys._MEIPASS, relative) # pyright: ignore[reportAttributeAccessIssue] - return os.path.join(os.path.abspath("."), relative) + meipass_path: str = sys._MEIPASS # type: ignore[unused-ignore] + return os.path.join(meipass_path, relative) + return str(Path.cwd() / relative) def save_project(engine: Any, dir: Optional[str] = None) -> Optional[str]: - """ - Save the project as an absp file + """Save the engine project to a .absp file. Args: - engine (Any): engine instance to extract the project information from - dir (Optional[str]): Directory to save the project in + engine: The engine instance containing project data and properties + (e.g., project_name, game_dimensions, cursor_visible, + fullscreen, and entities). + dir: An optional explicit directory path to save into. Returns: - Optional[str]: Path to the saved file or None + The path to the saved project file as a string if saved, or None + if the user cancelled the file dialog. """ - - if dir is None: - dir = filedialog.askdirectory() - - if dir and os.path.exists(dir): - gamefile = str( - Path(dir) / "game.absp", - ) - - with open(gamefile, "w", encoding="utf-8") as f: - dump( - { - "name": engine.project_name, - "game": { - "dimensions": engine.game_dimensions, - "cursor_visible": engine.cursor_visible, - "fullscreen": engine.fullscreen, - }, - "entities": engine.entities, + directory: str = dir if dir else filedialog.askdirectory() + + if not directory: + return None + + project_path = Path(directory) / "game.absp" + project_path.parent.mkdir(parents=True, exist_ok=True) + + with project_path.open("w", encoding="utf-8") as f: + dump( + { + "name": engine.project_name, + "game": { + "dimensions": engine.game_dimensions, + "cursor_visible": engine.cursor_visible, + "fullscreen": engine.fullscreen, }, - f, - ) - - messagebox.showinfo("Success", "Project saved successfully.") - - return gamefile + "entities": engine.entities, + }, + f, + indent=2, + ) - return None + messagebox.showinfo("Success", "Project saved successfully.") + return str(project_path) -def load_project() -> Optional[list]: - """ - Ask the user to open an absp file, then return the contents +def load_project() -> Optional[Tuple[Dict[str, Any], str]]: + """Ask the user for a project directory and return the loaded game data. Returns: - Optional[list]: file content + A tuple of `(data_dict, file_path_str)`, or None if cancelled/failed. """ + directory: str = filedialog.askdirectory() - dir = filedialog.askdirectory() - - if dir and os.path.exists(dir): - gamefile = str(Path(dir) / "game.absp") - - if not os.path.exists(gamefile): - logger( - "game.absp file not found in selected directory. Creating.", - status=LoggerStatus.WARNING, - ) + if not directory: + return None - with open(gamefile, "w", encoding="utf-8") as f: - f.write("{}") + project_path = Path(directory) / "game.absp" - return [{}, gamefile] - - if os.path.isdir(gamefile): - messagebox.showerror("Error", "game.absp project file is a directory.") - return None + if not project_path.exists(): + logger( + "game.absp file not found in selected directory. Creating.", + status=LoggerStatus.WARNING, + ) + project_path.write_text("{}", encoding="utf-8") + return {}, str(project_path) - with open(gamefile, "r") as f: - data: dict = load(f) + if project_path.is_dir(): + messagebox.showerror("Error", "game.absp project file is a directory.") + return None - return [data, gamefile] + with project_path.open("r", encoding="utf-8") as f: + data: Dict[str, Any] = load(f) - return None + return data, str(project_path)