From: = <=> Date: Mon, 12 Oct 2026 10:07:03 +0000 (+0200) Subject: Simple text gui stuff X-Git-Url: https://git.uwuaxy.net/Project%20Status.png?a=commitdiff_plain;h=44cf2ced1c235946c2dd738fabbc9d5151930eb7;p=axy%2Fft%2Fpacman.git Simple text gui stuff --- diff --git a/asset/fonts/JetBrainsMonoNL-Regular.ttf b/asset/fonts/JetBrainsMonoNL-Regular.ttf new file mode 100644 index 0000000..70d2ec9 Binary files /dev/null and b/asset/fonts/JetBrainsMonoNL-Regular.ttf differ diff --git a/src/pacman/ecs/coord.py b/src/pacman/ecs/coord.py index cc3547b..dc8e6a3 100644 --- a/src/pacman/ecs/coord.py +++ b/src/pacman/ecs/coord.py @@ -1,9 +1,12 @@ +from collections.abc import Iterator +from typing import override + from pygame import Vector3 from pacman.ecs.hierarchy import Hierarchy from pacman.ecs.schedule import SystemSet, UpdateSchedule from pacman.ecs.update import PostUpdate -from pacman.ecs.world import Component, Query, Systems, World +from pacman.ecs.world import Bundle, Component, Query, Systems, World class CoordAbs2D(Component): @@ -17,6 +20,22 @@ class Coord2D(Vector3, Component): pass +class CoordBundle(Bundle): + def __init__(self, x: float = 0, y: float = 0, depth: float = -1) -> None: + self.coord = Coord2D(x, y, depth) + self.coord_abs = CoordAbs2D() + + @override + def components_iter(self) -> Iterator[Component]: + return iter((self.coord, self.coord_abs)) + + @classmethod + @override + def components_type_iter(cls, ty: type) -> Iterator[type[Component]]: + del ty + return iter((Coord2D, CoordAbs2D)) + + def update_coord(entities: Query[tuple[CoordAbs2D, Coord2D]]) -> None: for abs_coord, coord in entities: abs_coord._vec = coord.xyz diff --git a/src/pacman/ecs/graphics.py b/src/pacman/ecs/graphics.py index de2416b..887a486 100644 --- a/src/pacman/ecs/graphics.py +++ b/src/pacman/ecs/graphics.py @@ -139,6 +139,8 @@ InputsSystems = SystemSet("inputs-systems") def graphics_plugins(world: World) -> None: + from pacman.ecs.gui import gui_plugins + world.with_systems(StartupSchedule, init_window).with_systems( UpdateSchedule, Systems( @@ -151,4 +153,4 @@ def graphics_plugins(world: World) -> None: Systems(poll_events, close_on_close) .chain() .in_set(PreUpdate, InputsSystems), - ) + ).with_plugins(gui_plugins) diff --git a/src/pacman/ecs/gui.py b/src/pacman/ecs/gui.py new file mode 100644 index 0000000..ff6e93c --- /dev/null +++ b/src/pacman/ecs/gui.py @@ -0,0 +1,90 @@ +from dataclasses import dataclass, field +from typing import override + +import pygame +from pygame import Color + +from pacman.ecs.assets import Asset +from pacman.ecs.graphics import GraphicsSystems, Sprite2D +from pacman.ecs.schedule import StartupSchedule, UpdateSchedule +from pacman.ecs.update import PostUpdate +from pacman.ecs.world import ( + CommandQueue, + Component, + Entity, + Query, + System, + Systems, + World, +) + + +@dataclass +class Font(Asset, Component): + font: pygame.font.Font + + @override + @classmethod + def load(cls, ty: type, path: str) -> "Font": + del ty + path, size = path.split(":") + return Font(pygame.font.Font(path, int(size))) + + +@dataclass +class TextDisplay(Component): + text: str + font: Font + color: Color = field(default_factory=lambda: Color(255, 255, 255)) + _previously_rendered: tuple[str, Font, Color] | None = None + + +@dataclass +class ComputedText(Component): + cb: System[str] + + +def update_computed_text( + elems: Query[tuple[TextDisplay, ComputedText]], + world: World, +) -> None: + for display, compute in elems: + display.text = world.run_raw_system(compute.cb) + + +def update_text_sprites( + elems: Query[tuple[TextDisplay, Sprite2D | Entity]], queue: CommandQueue +) -> None: + inserts = [] + for display, e in elems: + if ( + display.text, + display.font, + display.color, + ) == display._previously_rendered: + continue + surface = display.font.font.render(display.text, True, display.color) + if isinstance(e, Sprite2D): + e.surface = surface + else: + inserts.append((e, Sprite2D(surface))) + display._previously_rendered = ( + display.text, + display.font, + display.color, + ) + queue.queue(lambda world: world.insert_many(Sprite2D, inserts)) + + +def setup_gui_resources() -> None: + pygame.font.init() + + +def gui_plugins(world: World) -> None: + world.with_systems(StartupSchedule, setup_gui_resources).with_systems( + UpdateSchedule, + Systems(update_computed_text, update_text_sprites) + .chain() + .in_set(PostUpdate) + .before(GraphicsSystems), + ) diff --git a/src/pacman/ecs/world.py b/src/pacman/ecs/world.py index 9d0135f..2dc6757 100644 --- a/src/pacman/ecs/world.py +++ b/src/pacman/ecs/world.py @@ -772,7 +772,7 @@ class World(FromWorld): def remove(query: Query[Entity, Ty], world: World) -> None: world.remove_many(ty, list(query)) - self.add_raw_system(remove)() + self.run_raw_system(remove) return self.spawn(ty, comp) def spawn_many[T: Components]( @@ -902,6 +902,21 @@ class World(FromWorld): return inner + def run_system[T, *P]( + self, + system: System[Callable[[*P], T]], + *args: *P, + default: Callable[[*P], T] = _system_failed_va, + ) -> T: + return self.add_system(system, default)(*args) + + def run_raw_system[T]( + self, + system: System[T], + default: Callable[[], T] = _system_failed, + ) -> T: + return self.add_raw_system(system, default)() + @classmethod @override def extract_from_world( diff --git a/src/pacman/game/__init__.py b/src/pacman/game/__init__.py index aaf4e32..59805a2 100644 --- a/src/pacman/game/__init__.py +++ b/src/pacman/game/__init__.py @@ -12,7 +12,12 @@ from pygame.math import clamp from pacman.ecs import minimal_plugins from pacman.ecs.assets import Assets -from pacman.ecs.coord import Coord2D, CoordAbs2D, PropagateTransform +from pacman.ecs.coord import ( + Coord2D, + CoordAbs2D, + CoordBundle, + PropagateTransform, +) from pacman.ecs.graphics import ( AnimationSet, GraphicsSystems, @@ -22,6 +27,7 @@ from pacman.ecs.graphics import ( WindowEvents, graphics_plugins, ) +from pacman.ecs.gui import ComputedText, Font, TextDisplay from pacman.ecs.hierarchy import ChildOf, Hierarchy from pacman.ecs.schedule import StartupSchedule, UpdateSchedule from pacman.ecs.update import PostUpdate, PreUpdate, Update @@ -209,24 +215,23 @@ def create_maze( def inner(maze: MazeGenerator) -> Entity: root_maze = Maze(len(maze.maze[0]), len(maze.maze)) root = world.spawn( - tuple[Maze, Coord2D, CoordAbs2D], - (root_maze, Coord2D(), CoordAbs2D()), + tuple[Maze, CoordBundle], + (root_maze, CoordBundle()), ) - cell_dims = Coord2D(64, 64, 0) + cell_dims = Vector2(64, 64) cells = { (x, y): ( ChildOf(root), - Coord2D(Coord2D(x, y, 0).elementwise() * cell_dims), + CoordBundle(*(Vector2(x, y).elementwise() * cell_dims).xy), Cell(cell, (x, y)), ShouldRender(), - CoordAbs2D(), assets.load(f"./asset/walls/{cell}.png"), ) for y, line in enumerate(maze.maze) for x, cell in enumerate(line) } spawned_cells = world.spawn_many( - tuple[ChildOf, Coord2D, Cell, ShouldRender, CoordAbs2D, Sprite2D], + tuple[ChildOf, CoordBundle, Cell, ShouldRender, Sprite2D], list(cells.values()), ) root_maze._cells = dict(zip(cells.keys(), spawned_cells, strict=True)) @@ -250,17 +255,14 @@ def spawn_superpacgums( pacgums.append( ( ChildOf(cell), - Coord2D(0, 0, -1), - CoordAbs2D(), + CoordBundle(), assets.load("./asset/misc/superpacgum.png"), ShouldRender(), SuperPacGum(), ) ) world.spawn_many( - tuple[ - ChildOf, Coord2D, CoordAbs2D, Sprite2D, ShouldRender, SuperPacGum - ], + tuple[ChildOf, CoordBundle, Sprite2D, ShouldRender, SuperPacGum], pacgums, ) @@ -283,21 +285,20 @@ def spawn_pacgums( pacgums.append( ( ChildOf(ent), - Coord2D(0, 0, -1), - CoordAbs2D(), + CoordBundle(), assets.load("./asset/misc/pacgum.png"), ShouldRender(), PacGum(), ) ) world.spawn_many( - tuple[ChildOf, Coord2D, CoordAbs2D, Sprite2D, ShouldRender, PacGum], + tuple[ChildOf, CoordBundle, Sprite2D, ShouldRender, PacGum], pacgums, ) def setup_maze(world: World) -> None: - world.add_system(create_maze)(MazeGenerator((14, 14))) + world.run_system(create_maze, MazeGenerator((14, 14))) def move_in_motion( @@ -384,8 +385,7 @@ def spawn_pacman( pos = home_cell.grid_pos world.spawn( tuple[ - Coord2D, - CoordAbs2D, + CoordBundle, AnimationSet, Pacman, ShouldRender, @@ -395,8 +395,7 @@ def spawn_pacman( ChildOf, ], ( - Coord2D(pos[0] * 64, pos[1] * 64, -10), - CoordAbs2D(), + CoordBundle(pos[0] * 64, pos[1] * 64, -10), AnimationSet( [ [ @@ -430,8 +429,7 @@ def spawn_ghosts( maze, maze_ent = maze_t world.spawn_many( tuple[ - Coord2D, - CoordAbs2D, + CoordBundle, AnimationSet, ShouldRender, InMotion, @@ -444,8 +442,7 @@ def spawn_ghosts( ], [ ( - Coord2D(pos[0] * 64, pos[1] * 64, -10), - CoordAbs2D(), + CoordBundle(pos[0] * 64, pos[1] * 64, -10), AnimationSet( [ [ @@ -591,9 +588,9 @@ def collide_ghost( continue match status.state: case GhostState.wander | GhostState.chase: - world.add_raw_system(respawn_pacman)() - world.add_raw_system(respawn_ghost)() - world.add_raw_system(start_pause_timer)() + world.run_raw_system(respawn_pacman) + world.run_raw_system(respawn_ghost) + world.run_raw_system(start_pause_timer) state.lives -= 1 break @@ -721,7 +718,7 @@ def setup_new_maze(world: World) -> None: spawn_ghosts, start_pause_timer, ): - world.add_raw_system(sys)() + world.run_raw_system(sys) def update_inputs(inputs: Inputs, events: WindowEvents) -> None: @@ -793,10 +790,44 @@ def setup_resources(world: World) -> None: world.spawn_resource(Config()) +def print_score(state: GameState) -> str: + return f"Score: {state.score}" + + +def print_lives(state: GameState) -> str: + return f"Lives: {state.lives}" + + +def setup_text(world: World, fonts: Assets[Font]) -> None: + world.spawn_many( + tuple[CoordBundle, ShouldRender, ComputedText, TextDisplay], + [ + ( + CoordBundle(depth=-20), + ShouldRender(), + ComputedText(print_score), + TextDisplay( + "", + fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:30"), + ), + ), + ( + CoordBundle(y=100, depth=-20), + ShouldRender(), + ComputedText(print_lives), + TextDisplay( + "", + fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:30"), + ), + ), + ], + ) + + def run_game() -> None: World().with_plugins(graphics_plugins, minimal_plugins).with_systems( StartupSchedule, - Systems(setup_resources, setup_new_maze).chain(), + Systems(setup_resources, setup_new_maze, setup_text).chain(), ).with_systems( UpdateSchedule, Systems(