]> Untitled Git - axy/ft/pacman.git/commitdiff
Gui button system
author= <=>
Mon, 12 Oct 2026 12:20:49 +0000 (14:20 +0200)
committer= <=>
Mon, 12 Oct 2026 12:20:49 +0000 (14:20 +0200)
src/pacman/ecs/graphics.py
src/pacman/ecs/gui.py
src/pacman/game/__init__.py

index 887a486495ea941e96edda5b3de555ce32dd5d0f..00f684d833b7bcc5b536a5324b078c17456a22c0 100644 (file)
@@ -1,8 +1,10 @@
 from collections.abc import Iterator
 from dataclasses import dataclass
+from time import time
 from typing import cast, override
 
 import pygame
+from pygame import K_RETURN, K_a, K_d, K_s, K_w
 
 from pacman.ecs.assets import Asset
 from pacman.ecs.coord import CoordAbs2D, PropagateTransform
@@ -30,6 +32,9 @@ class Window(Resource):
     clock: pygame.time.Clock
 
 
+dir_lookup = {K_w: 0, K_d: 1, K_s: 2, K_a: 3}
+
+
 @dataclass
 class WindowEvents(Resource):
     events: list[pygame.event.Event]
@@ -49,12 +54,22 @@ class WindowEvents(Resource):
         )
 
 
-def init_window(world: World) -> None:
-    pygame.init()
-    world.spawn_resource(
-        Window(pygame.display.set_mode((1600, 1200)), pygame.time.Clock()),
-    )
-    world.spawn_resource(WindowEvents([]))
+@dataclass
+class Inputs(Resource):
+    inputs: dict[int, float]
+
+    def get_dir(self) -> int | None:
+        res = sorted(
+            ((k, v) for k, v in self.inputs.items() if k in dir_lookup),
+            key=lambda e: e[1],
+            reverse=True,
+        )
+        if len(res) == 0:
+            return None
+        return dir_lookup[res[0][0]]
+
+    def activate(self) -> bool:
+        return K_RETURN in self.inputs
 
 
 @dataclass
@@ -71,6 +86,15 @@ class Sprite2D(Component, Asset):
         return Sprite2D(self.surface)
 
 
+def init_graphics(world: World) -> None:
+    pygame.init()
+    world.spawn_resource(
+        Window(pygame.display.set_mode((1600, 1200)), pygame.time.Clock()),
+    )
+    world.spawn_resource(WindowEvents([]))
+    world.spawn_resource(Inputs({}))
+
+
 def poll_events(events: WindowEvents) -> None:
     events.events = pygame.event.get()
 
@@ -134,6 +158,14 @@ def update_animations(
         sprite.surface = anim.curr_frame.surface
 
 
+def update_inputs(inputs: Inputs, events: WindowEvents) -> None:
+    for key, state in events.keys_itr():
+        if state:
+            inputs.inputs[key] = time()
+        else:
+            del inputs.inputs[key]
+
+
 GraphicsSystems = SystemSet("graphics-systems")
 InputsSystems = SystemSet("inputs-systems")
 
@@ -141,7 +173,7 @@ 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(
+    world.with_systems(StartupSchedule, init_graphics).with_systems(
         UpdateSchedule,
         Systems(
             insert_animation_sprites,
@@ -150,7 +182,7 @@ def graphics_plugins(world: World) -> None:
         )
         .chain()
         .in_set(PostUpdate, GraphicsSystems),
-        Systems(poll_events, close_on_close)
+        Systems(poll_events, close_on_close, update_inputs)
         .chain()
         .in_set(PreUpdate, InputsSystems),
     ).with_plugins(gui_plugins)
index ff6e93c83904b306cf1e5982b10a80969007fc1c..5bd5aeb7ae08f192e557dc1fb1c9be0ca25b6617 100644 (file)
@@ -1,11 +1,21 @@
+from collections.abc import Callable
 from dataclasses import dataclass, field
+from enum import Enum, auto
 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.coord import CoordAbs2D, PropagateTransform
+from pacman.ecs.graphics import (
+    AnimationSet,
+    GraphicsSystems,
+    Inputs,
+    Sprite2D,
+    WindowEvents,
+    dir_lookup,
+)
 from pacman.ecs.schedule import StartupSchedule, UpdateSchedule
 from pacman.ecs.update import PostUpdate
 from pacman.ecs.world import (
@@ -13,6 +23,7 @@ from pacman.ecs.world import (
     Component,
     Entity,
     Query,
+    Resource,
     System,
     Systems,
     World,
@@ -44,6 +55,78 @@ class ComputedText(Component):
     cb: System[str]
 
 
+class ButtonState(Enum):
+    unselected = auto()
+    selected = auto()
+    pressed = auto()
+
+
+@dataclass
+class Button(Component):
+    cb: System[None]
+    state: ButtonState = ButtonState.unselected
+
+
+@dataclass
+class SelectedButton(Resource):
+    button: Entity
+
+
+def update_selected_button(
+    buttons: Query[tuple[Entity, CoordAbs2D], Button],
+    selected: SelectedButton,
+    events: WindowEvents,
+) -> None:
+    for key, active in events.keys_itr():
+        if not active:
+            continue
+        if (direction := dir_lookup.get(key)) is None:
+            continue
+        extract: Callable[[CoordAbs2D], float] = (
+            (lambda e: e._vec.x)
+            if direction % 2 == 1
+            else (lambda e: e._vec.y)
+        )
+        curr_select = buttons[selected.button][1]
+        candidates = sorted(
+            (
+                (ent, coord)
+                for ent, coord in buttons
+                if ent is selected.button
+                or extract(curr_select) != extract(coord)
+            ),
+            key=lambda e: extract(e[1]),
+            reverse=direction % 3 != 0,
+        )
+        i = candidates.index((selected.button, curr_select))
+        selected.button = candidates[i - 1][0]
+
+
+def update_button_state(
+    buttons: Query[tuple[Entity, Button]],
+    selected: SelectedButton,
+    inputs: Inputs,
+    world: World,
+) -> None:
+    cbs = []
+    for entity, button in buttons:
+        if entity is not selected.button:
+            button.state = ButtonState.unselected
+            continue
+        if button.state == ButtonState.selected and inputs.activate():
+            button.state = ButtonState.pressed
+            cbs.append(button.cb)
+        elif not inputs.activate():
+            button.state = ButtonState.selected
+    for cb in cbs:
+        world.run_raw_system(cb)
+
+
+def update_button_texture(buttons: Query[tuple[Button, AnimationSet]]) -> None:
+    for button, anim in buttons:
+        anim.idx = button.state.value - 1
+
+
 def update_computed_text(
     elems: Query[tuple[TextDisplay, ComputedText]],
     world: World,
@@ -83,8 +166,15 @@ def setup_gui_resources() -> None:
 def gui_plugins(world: World) -> None:
     world.with_systems(StartupSchedule, setup_gui_resources).with_systems(
         UpdateSchedule,
-        Systems(update_computed_text, update_text_sprites)
+        Systems(
+            update_selected_button,
+            update_button_state,
+            update_button_texture,
+            update_computed_text,
+            update_text_sprites,
+        )
         .chain()
         .in_set(PostUpdate)
-        .before(GraphicsSystems),
+        .before(GraphicsSystems)
+        .after(PropagateTransform),
     )
index 8ab73dc61bb37dd98bcecde9fda502b404529b55..662c46c6da49b0cef14d9fb31509bb7f931ca9ec 100644 (file)
@@ -2,12 +2,11 @@ from collections.abc import Callable
 from dataclasses import dataclass
 from enum import Enum, auto
 from random import random, shuffle
-from time import time
 from typing import override
 
 from mazegenerator import MazeGenerator  # type: ignore[import-untyped]
 from pydantic import BaseModel
-from pygame import K_a, K_d, K_s, K_w, Vector2
+from pygame import Vector2
 from pygame.math import clamp
 
 from pacman.ecs import minimal_plugins
@@ -21,16 +20,21 @@ from pacman.ecs.coord import (
 from pacman.ecs.graphics import (
     AnimationSet,
     GraphicsSystems,
-    InputsSystems,
+    Inputs,
     ShouldRender,
     Sprite2D,
-    WindowEvents,
     graphics_plugins,
 )
-from pacman.ecs.gui import ComputedText, Font, TextDisplay
+from pacman.ecs.gui import (
+    Button,
+    ComputedText,
+    Font,
+    SelectedButton,
+    TextDisplay,
+)
 from pacman.ecs.hierarchy import ChildOf, Hierarchy
 from pacman.ecs.schedule import StartupSchedule, UpdateSchedule
-from pacman.ecs.update import PostUpdate, PreUpdate, Update
+from pacman.ecs.update import PostUpdate, Update
 from pacman.ecs.world import (
     CommandQueue,
     Component,
@@ -115,22 +119,6 @@ class GameState(Resource):
     lives: int = 3
 
 
-@dataclass
-class Inputs(Resource):
-    inputs: dict[int, float]
-
-    def get_dir(self) -> int | None:
-        lookup = {K_w: 0, K_d: 1, K_s: 2, K_a: 3}
-        res = sorted(
-            ((k, v) for k, v in self.inputs.items() if k in lookup),
-            key=lambda e: e[1],
-            reverse=True,
-        )
-        if len(res) == 0:
-            return None
-        return lookup[res[0][0]]
-
-
 class PacGum(Component):
     pass
 
@@ -240,9 +228,6 @@ def create_maze(
     return inner
 
 
-
-
-
 def spawn_superpacgums(
     world: World, mazes: Query[Maze], assets: Assets[Sprite2D]
 ) -> None:
@@ -724,14 +709,6 @@ def setup_new_maze(world: World) -> None:
         world.run_raw_system(sys)
 
 
-def update_inputs(inputs: Inputs, events: WindowEvents) -> None:
-    for key, state in events.keys_itr():
-        if state:
-            inputs.inputs[key] = time()
-        else:
-            del inputs.inputs[key]
-
-
 def level_is_finished(
     pacgums: Query[SuperPacGum | PacGum], _: Not[GameIsOver]
 ) -> bool:
@@ -788,7 +765,6 @@ def initial_pause_done(timer: InitialPauseTimer, config: Config) -> bool:
 
 
 def setup_resources(world: World) -> None:
-    world.spawn_resource(Inputs({}))
     world.spawn_resource(GameState())
     world.spawn_resource(Config())
 
@@ -802,13 +778,25 @@ def print_lives(state: GameState) -> str:
 
 
 def print_timer(state: GameState, config: Config) -> str:
-    time =  config.level_timer - state.timer
+    time = config.level_timer - state.timer
     return f"{time // 60}"
 
 
+def print_a() -> None:
+    print("a")
+
+
+def print_b() -> None:
+    print("b")
+
+
+def print_c() -> None:
+    print("c")
+
+
 def setup_text(world: World, fonts: Assets[Font]) -> None:
-    world.spawn_many(
-        tuple[CoordBundle, ShouldRender, ComputedText, TextDisplay],
+    spawned = world.spawn_many(
+        tuple[CoordBundle, ShouldRender, ComputedText, TextDisplay, Button],
         [
             (
                 CoordBundle(depth=-20),
@@ -818,6 +806,7 @@ def setup_text(world: World, fonts: Assets[Font]) -> None:
                     "",
                     fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:30"),
                 ),
+                Button(print_a),
             ),
             (
                 CoordBundle(y=100, depth=-20),
@@ -827,6 +816,7 @@ def setup_text(world: World, fonts: Assets[Font]) -> None:
                     "",
                     fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:30"),
                 ),
+                Button(print_b),
             ),
             (
                 CoordBundle(y=200, depth=-20),
@@ -836,9 +826,11 @@ def setup_text(world: World, fonts: Assets[Font]) -> None:
                     "",
                     fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:30"),
                 ),
+                Button(print_c),
             ),
         ],
     )
+    world.spawn_resource(SelectedButton(spawned[0]))
 
 
 def run_game() -> None:
@@ -860,7 +852,6 @@ def run_game() -> None:
         .chain()
         .run_if(initial_pause_done)
         .in_set(Update),
-        Systems(update_inputs).in_set(PreUpdate).after(InputsSystems),
         Systems(update_pacman_anim, update_ghost_anim)
         .before(GraphicsSystems)
         .in_set(PostUpdate),