Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 8 additions & 20 deletions engine/build_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
Build and development utilities for the engine.
"""

import os

import shutil

from tkinter import messagebox
Expand All @@ -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
)
Expand All @@ -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)
27 changes: 11 additions & 16 deletions engine/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
"""
Expand All @@ -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:
"""
Expand All @@ -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:
"""
Expand All @@ -351,7 +345,6 @@ def remove(self, obj: Entity) -> None:
"""

self.objects.remove(obj)
self.no_entities = len(self.objects) == 0


class Game:
Expand Down Expand Up @@ -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:
Expand Down
37 changes: 12 additions & 25 deletions engine/core/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
135 changes: 62 additions & 73 deletions engine/saveload.py
Original file line number Diff line number Diff line change
@@ -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)