Skip to content

Commit d613403

Browse files
committed
Expand and refactor menu items
Needs more cleanup
1 parent fe9df1e commit d613403

2 files changed

Lines changed: 165 additions & 24 deletions

File tree

game/rendering.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import tcod.console
99
import tcod.ecs
1010

11+
import g
1112
from game.message_tools import get_log
13+
from game.state import State
1214

1315

1416
@attrs.define()
@@ -48,3 +50,13 @@ def render(self) -> tcod.console.Console:
4850
break
4951
y += console.print_box(x=0, y=y, width=self.width, height=0, string=msg.text, fg=(255, 255, 255))
5052
return console
53+
54+
55+
def draw_previous_state(console: tcod.console.Console, state: State) -> None:
56+
"""Draw the state before this state."""
57+
current_index = g.states.index(state)
58+
if current_index > 0:
59+
g.states[current_index - 1].on_draw(console)
60+
if g.states[-1] is state: # Darken the screen before drawing the last state.
61+
console.rgb["fg"] //= 4
62+
console.rgb["bg"] //= 4

game/states.py

Lines changed: 153 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from collections.abc import Callable
6-
from typing import Final, Self
6+
from typing import Final, Protocol, Self
77

88
import attrs
99
import tcod.console
@@ -15,7 +15,7 @@
1515
import game.world_tools
1616
from game.components import Gold, Graphic, Position
1717
from game.message_tools import report
18-
from game.rendering import LogRenderer
18+
from game.rendering import LogRenderer, draw_previous_state
1919
from game.state import Pop, Push, Reset, State, StateResult
2020
from game.tags import IsItem, IsPlayer
2121

@@ -88,13 +88,81 @@ def on_draw(self, console: tcod.console.Console) -> None:
8888
LogRenderer.init(g.world, console.width, 5).render().blit(dest=console, dest_x=0, dest_y=console.height - 5)
8989

9090

91+
class MenuItem(Protocol):
92+
"""Menu item protocol."""
93+
94+
__slots__ = ()
95+
96+
@property
97+
def label(self) -> str:
98+
"""Label for the menu item."""
99+
100+
def on_event(self, event: tcod.event.Event) -> StateResult:
101+
"""Handle events passed to the menu item."""
102+
103+
91104
@attrs.define()
92-
class MenuItem:
105+
class SelectItem(MenuItem):
93106
"""Clickable menu item."""
94107

95108
label: str
96109
callback: Callable[[], StateResult]
97110

111+
def on_event(self, event: tcod.event.Event) -> StateResult:
112+
"""Handle events selecting this item."""
113+
match event:
114+
case tcod.event.KeyDown(sym=sym) if sym in {KeySym.RETURN, KeySym.RETURN2, KeySym.KP_ENTER}:
115+
return self.callback()
116+
case tcod.event.MouseButtonUp(button=tcod.event.MouseButton.LEFT):
117+
return self.callback()
118+
case _:
119+
return None
120+
121+
122+
@attrs.define()
123+
class IntItem(MenuItem):
124+
"""Numbered item."""
125+
126+
format: str = "{}"
127+
value: int = 0
128+
min_value: int | None = 0
129+
max_value: int | None = None
130+
on_changed_callback: Callable[[int], None] | None = None
131+
132+
@property
133+
def label(self) -> str:
134+
"""Return a label including the current value."""
135+
return self.format.format(self.value)
136+
137+
def set_value(self, value: int | str) -> None:
138+
"""Set and clamp the value."""
139+
if isinstance(value, str):
140+
try:
141+
value = int(value)
142+
except ValueError:
143+
return
144+
if self.min_value is not None:
145+
value = max(value, self.min_value)
146+
if self.max_value is not None:
147+
value = min(value, self.max_value)
148+
self.value = value
149+
if self.on_changed_callback is not None:
150+
self.on_changed_callback(value)
151+
152+
def on_event(self, event: tcod.event.Event) -> StateResult:
153+
"""Handle events for updating the current value."""
154+
match event:
155+
case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS:
156+
dx, dy = DIRECTION_KEYS[sym]
157+
self.set_value(self.value + dx)
158+
return None
159+
case tcod.event.KeyDown(sym=sym) if sym in {KeySym.RETURN, KeySym.RETURN2, KeySym.KP_ENTER}:
160+
return Push(TextFieldWindow(buffer=str(self.value), on_done_callback=self.set_value))
161+
case tcod.event.MouseButtonUp(button=tcod.event.MouseButton.LEFT):
162+
return Push(TextFieldWindow(buffer=str(self.value), on_done_callback=self.set_value))
163+
case _:
164+
return None
165+
98166

99167
@attrs.define(eq=False)
100168
class ListMenu(State):
@@ -105,15 +173,15 @@ class ListMenu(State):
105173
x: int = 0
106174
y: int = 0
107175

108-
def on_event(self, event: tcod.event.Event) -> StateResult: # noqa: PLR0911
176+
def on_event(self, event: tcod.event.Event) -> StateResult:
109177
"""Handle events for menus."""
110178
match event:
111179
case tcod.event.Quit():
112180
raise SystemExit()
113181
case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS:
114182
dx, dy = DIRECTION_KEYS[sym]
115183
if dx != 0 or dy == 0:
116-
return None
184+
return self.activate_selected(event)
117185
if self.selected is not None:
118186
self.selected += dy
119187
self.selected %= len(self.items)
@@ -124,21 +192,17 @@ def on_event(self, event: tcod.event.Event) -> StateResult: # noqa: PLR0911
124192
y -= self.y
125193
self.selected = y if 0 <= y < len(self.items) else None
126194
return None
127-
case tcod.event.KeyDown(sym=KeySym.RETURN):
128-
return self.activate_selected()
129-
case tcod.event.MouseButtonUp(button=tcod.event.MouseButton.LEFT):
130-
return self.activate_selected()
131195
case tcod.event.KeyDown(sym=KeySym.ESCAPE):
132196
return self.on_cancel()
133197
case tcod.event.MouseButtonUp(button=tcod.event.MouseButton.RIGHT):
134198
return self.on_cancel()
135199
case _:
136-
return None
200+
return self.activate_selected(event)
137201

138-
def activate_selected(self) -> StateResult:
202+
def activate_selected(self, event: tcod.event.Event) -> StateResult:
139203
"""Call the selected menu items callback."""
140204
if self.selected is not None:
141-
return self.items[self.selected].callback()
205+
return self.items[self.selected].on_event(event)
142206
return None
143207

144208
def on_cancel(self) -> StateResult:
@@ -164,6 +228,54 @@ def on_draw(self, console: tcod.console.Console) -> None:
164228
)
165229

166230

231+
@attrs.define(eq=False)
232+
class TextFieldWindow(State):
233+
"""Modal user-editable text field window."""
234+
235+
buffer: str
236+
on_done_callback: Callable[[str], None]
237+
238+
x: int = 5
239+
y: int = 5
240+
width: int = 24
241+
242+
def on_done(self) -> StateResult:
243+
"""Called when the editing is finished."""
244+
self.on_done_callback(self.buffer)
245+
return Pop()
246+
247+
def on_event(self, event: tcod.event.Event) -> StateResult:
248+
"""Handle events for text editing."""
249+
match event:
250+
case tcod.event.Quit():
251+
raise SystemExit()
252+
case tcod.event.KeyDown(sym=sym) if sym in {KeySym.RETURN, KeySym.RETURN2, KeySym.KP_ENTER}:
253+
return self.on_done()
254+
case tcod.event.KeyDown(sym=KeySym.ESCAPE):
255+
return self.on_done()
256+
case tcod.event.KeyDown(sym=KeySym.BACKSPACE):
257+
self.buffer = self.buffer[:-1]
258+
return None
259+
case tcod.event.TextInput(text=text):
260+
self.buffer += text
261+
return None
262+
case _:
263+
return None
264+
265+
def on_draw(self, console: tcod.console.Console) -> None:
266+
"""Draw the text buffer."""
267+
draw_previous_state(console, self)
268+
269+
console.draw_frame(self.x, self.y, self.width + 2, 3)
270+
console.print(
271+
self.x + 1,
272+
self.y + 1,
273+
self.buffer + "_",
274+
fg=(255, 255, 255),
275+
bg=(0, 0, 0),
276+
)
277+
278+
167279
class MainMenu(ListMenu):
168280
"""Main/escape menu."""
169281

@@ -172,11 +284,20 @@ class MainMenu(ListMenu):
172284
def __init__(self) -> None:
173285
"""Initialize the main menu."""
174286
items = [
175-
MenuItem("New game", self.new_game),
176-
MenuItem("Quit", self.quit),
287+
SelectItem("New game", self.new_game),
288+
SelectItem("Quit", self.quit),
289+
IntItem(
290+
"Console width: {}",
291+
g.config.console.columns,
292+
min_value=10,
293+
on_changed_callback=self.set_console_columns,
294+
),
295+
IntItem(
296+
"Console height: {}", g.config.console.rows, min_value=10, on_changed_callback=self.set_console_rows
297+
),
177298
]
178299
if hasattr(g, "world"):
179-
items.insert(0, MenuItem("Continue", self.continue_))
300+
items.insert(0, SelectItem("Continue", self.continue_))
180301

181302
super().__init__(
182303
items=tuple(items),
@@ -185,16 +306,29 @@ def __init__(self) -> None:
185306
y=5,
186307
)
187308

188-
def continue_(self) -> StateResult:
309+
@staticmethod
310+
def set_console_columns(v: int) -> None:
311+
"""Adjust the console size."""
312+
g.config.console.columns = v
313+
314+
@staticmethod
315+
def set_console_rows(v: int) -> None:
316+
"""Adjust the console size."""
317+
g.config.console.rows = v
318+
319+
@staticmethod
320+
def continue_() -> StateResult:
189321
"""Return to the game."""
190322
return Reset(InGame())
191323

192-
def new_game(self) -> StateResult:
324+
@staticmethod
325+
def new_game() -> StateResult:
193326
"""Begin a new game."""
194327
g.world = game.world_tools.new_world()
195328
return Reset(InGame())
196329

197-
def quit(self) -> StateResult:
330+
@staticmethod
331+
def quit() -> StateResult:
198332
"""Close the program."""
199333
raise SystemExit()
200334

@@ -259,12 +393,7 @@ def on_event(self, event: tcod.event.Event) -> StateResult: # noqa: PLR0911
259393

260394
def on_draw(self, console: tcod.console.Console) -> None:
261395
"""Render the menu."""
262-
current_index = g.states.index(self)
263-
if current_index > 0:
264-
g.states[current_index - 1].on_draw(console)
265-
if g.states[-1] is self:
266-
console.rgb["fg"] //= 4
267-
console.rgb["bg"] //= 4
396+
draw_previous_state(console, self)
268397

269398
console.draw_frame(
270399
self.x, self.y, self.width, self.height, fg=(255, 255, 255), bg=(0, 0, 0), decoration="┌─┐│ │└─┘"

0 commit comments

Comments
 (0)