Refactor event handling
This commit is contained in:
parent
a1e6125c34
commit
e82d2e5b49
3
color.py
3
color.py
@ -20,3 +20,6 @@ health_recovered = (0x0, 0xFF, 0x0)
|
||||
bar_text = white
|
||||
bar_filled = (0x0, 0x60, 0x0)
|
||||
bar_empty = (0x40, 0x10, 0x10)
|
||||
|
||||
menu_title = (255, 255, 63)
|
||||
menu_text = white
|
||||
|
@ -8,7 +8,11 @@ import components.ai
|
||||
import components.inventory
|
||||
from components.base_component import BaseComponent
|
||||
from exceptions import Impossible
|
||||
from input_handlers import AreaRangedAttackHandler, SingleRangedAttackHandler
|
||||
from input_handlers import (
|
||||
ActionOrHandler,
|
||||
AreaRangedAttackHandler,
|
||||
SingleRangedAttackHandler
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from entity import Actor, Item
|
||||
@ -17,7 +21,7 @@ if TYPE_CHECKING:
|
||||
class Consumable(BaseComponent):
|
||||
parent: Item
|
||||
|
||||
def get_action(self, consumer: Actor) -> Optional[actions.Action]:
|
||||
def get_action(self, consumer: Actor) -> Optional[ActionOrHandler]:
|
||||
"""Try to return the action for this item."""
|
||||
return actions.ItemAction(consumer, self.parent)
|
||||
|
||||
@ -40,18 +44,17 @@ class ConfusionConsumable(Consumable):
|
||||
def __init__(self, number_of_turns: int):
|
||||
self.number_of_turns = number_of_turns
|
||||
|
||||
def get_action(self, consumer: Actor) -> Optional[actions.Action]:
|
||||
def get_action(self, consumer: Actor) -> SingleRangedAttackHandler:
|
||||
self.engine.message_log.add_message(
|
||||
"Select a target location.",
|
||||
color.needs_target
|
||||
)
|
||||
self.engine.event_handler = SingleRangedAttackHandler(
|
||||
|
||||
return SingleRangedAttackHandler(
|
||||
self.engine,
|
||||
callback=lambda xy: actions.ItemAction(consumer, self.parent, xy),
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def activate(self, action: actions.ItemAction) -> None:
|
||||
consumer = action.entity
|
||||
target = action.target_actor
|
||||
@ -99,19 +102,18 @@ class FireballDamageConsumable(Consumable):
|
||||
self.damage = damage
|
||||
self.radius = radius
|
||||
|
||||
def get_action(self, consumer: Actor) -> Optional[actions.Action]:
|
||||
def get_action(self, consumer: Actor) -> AreaRangedAttackHandler:
|
||||
self.engine.message_log.add_message(
|
||||
"Select a target location.",
|
||||
color.needs_target
|
||||
)
|
||||
self.engine.event_handler = AreaRangedAttackHandler(
|
||||
|
||||
return AreaRangedAttackHandler(
|
||||
self.engine,
|
||||
self.radius,
|
||||
lambda xy: actions.ItemAction(consumer, self.parent, xy),
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def activate(self, action: actions.ItemAction) -> None:
|
||||
target_xy = action.target_xy
|
||||
|
||||
|
@ -4,7 +4,6 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import color
|
||||
from components.base_component import BaseComponent
|
||||
from input_handlers import GameOverEventHandler
|
||||
from render_order import RenderOrder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -34,7 +33,6 @@ class Fighter(BaseComponent):
|
||||
if self.engine.player is self.parent:
|
||||
death_message = "You died!"
|
||||
death_message_color = color.player_die
|
||||
self.engine.event_handler = GameOverEventHandler(self.engine)
|
||||
else:
|
||||
death_message = f"{self.parent.name} is dead!"
|
||||
death_message_color = color.enemy_die
|
||||
|
@ -2,12 +2,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from tcod.context import Context
|
||||
from tcod.console import Console
|
||||
from tcod.map import compute_fov
|
||||
|
||||
import exceptions
|
||||
from input_handlers import MainGameEventHandler
|
||||
from message_log import MessageLog
|
||||
from render_functions import render_bar, render_names_at_mouse_location
|
||||
|
||||
@ -20,7 +18,6 @@ class Engine:
|
||||
game_map: GameMap
|
||||
|
||||
def __init__(self, player: Actor):
|
||||
self.event_handler: EventHandler = MainGameEventHandler(self)
|
||||
self.message_log = MessageLog()
|
||||
self.mouse_location = (0, 0)
|
||||
self.player = player
|
||||
|
@ -3,3 +3,7 @@ class Impossible(Exception):
|
||||
|
||||
The reason is given as the exception message.
|
||||
"""
|
||||
|
||||
|
||||
class QuitWithoutSaving(SystemExit):
|
||||
"""Can be raised to exit the game without automatically saving."""
|
@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import overload, Callable, Optional, Tuple, TYPE_CHECKING
|
||||
from typing import overload, Callable, Optional, Tuple, TYPE_CHECKING, Union
|
||||
|
||||
import tcod.event
|
||||
|
||||
@ -61,13 +61,50 @@ CONFIRM_KEYS = {
|
||||
tcod.event.K_KP_ENTER,
|
||||
}
|
||||
|
||||
ActionOrHandler = Union[Action, "BaseEventHandler"]
|
||||
"""
|
||||
An event handler return value which can trigger an action or switch active handlers.
|
||||
|
||||
class EventHandler(tcod.event.EventDispatch[Action]):
|
||||
If a handler is returned than it will become the active handler for future events.
|
||||
if an action is returned it will be attempted and if it's valid then
|
||||
MainGameEventHandler will become the active handler.
|
||||
"""
|
||||
|
||||
|
||||
class BaseEventHandler(tcod.event.EventDispatch[ActionOrHandler]):
|
||||
def handle_events(self, event: tcod.event.Event) -> BaseEventHandler:
|
||||
"""Handle an event and return the next active event handler."""
|
||||
state = self.dispatch(event)
|
||||
if isinstance(state, BaseEventHandler):
|
||||
return state
|
||||
assert not isinstance(state, Action), f"{self!r} can not handle actions."
|
||||
|
||||
return self
|
||||
|
||||
@overload
|
||||
def on_render(self, console: tcod.Console) -> None:
|
||||
"""Must be implemented"""
|
||||
|
||||
def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
|
||||
raise SystemExit()
|
||||
|
||||
|
||||
class EventHandler(BaseEventHandler):
|
||||
def __init__(self, engine: Engine):
|
||||
self.engine = engine
|
||||
|
||||
def handle_events(self, event: tcod.event.Event) -> None:
|
||||
self.handle_action(self.dispatch(event))
|
||||
def handle_events(self, event: tcod.event.Event) -> BaseEventHandler:
|
||||
"""Handle events for input handlers with an engine."""
|
||||
action_or_state = self.dispatch(event)
|
||||
if isinstance(action_or_state, BaseEventHandler):
|
||||
return action_or_state
|
||||
if self.handle_action(action_or_state):
|
||||
# A valid action was performed.
|
||||
if not self.engine.player.is_alive:
|
||||
# The player was killed sometime during or after the action.
|
||||
return GameOverEventHandler(self.engine)
|
||||
return MainGameEventHandler(self.engine) # Return to the main handler.
|
||||
return self
|
||||
|
||||
def handle_action(self, action: Optional[Action]) -> bool:
|
||||
"""Handle actions returned from event methods.
|
||||
@ -93,9 +130,6 @@ class EventHandler(tcod.event.EventDispatch[Action]):
|
||||
if self.engine.game_map.in_bounds(event.tile.x, event.tile.y):
|
||||
self.engine.mouse_location = event.tile.x, event.tile.y
|
||||
|
||||
def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
|
||||
raise SystemExit()
|
||||
|
||||
def on_render(self, console: tcod.Console) -> None:
|
||||
self.engine.render(console)
|
||||
|
||||
@ -103,16 +137,7 @@ class EventHandler(tcod.event.EventDispatch[Action]):
|
||||
class AskUserEventHandler(EventHandler):
|
||||
"""Handles user input for actions which require special input."""
|
||||
|
||||
def handle_action(self, action: Optional[Action]) -> bool:
|
||||
"""Return to the main event handler when a valid action was performed."""
|
||||
if super().handle_action(action):
|
||||
self.engine.event_handler = MainGameEventHandler(self.engine)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
|
||||
"""By default any key exits this input handler."""
|
||||
# Ignore modifier keys.
|
||||
if event.sym in {
|
||||
@ -127,18 +152,16 @@ class AskUserEventHandler(EventHandler):
|
||||
|
||||
return self.on_exit()
|
||||
|
||||
def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> Optional[Action]:
|
||||
def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> Optional[ActionOrHandler]:
|
||||
"""By default any mouse click exits this input handler."""
|
||||
return self.on_exit()
|
||||
|
||||
def on_exit(self) -> Optional[Action]:
|
||||
def on_exit(self) -> Optional[ActionOrHandler]:
|
||||
"""Called when the user is trying to exit or cancel an action.
|
||||
|
||||
By default this returns to the main event handler.
|
||||
"""
|
||||
self.engine.event_handler = MainGameEventHandler(self.engine)
|
||||
|
||||
return None
|
||||
return MainGameEventHandler(self.engine)
|
||||
|
||||
|
||||
class InventoryEventHandler(AskUserEventHandler):
|
||||
@ -189,7 +212,7 @@ class InventoryEventHandler(AskUserEventHandler):
|
||||
else:
|
||||
console.print(x + 1, y + 1, "(Empty)")
|
||||
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
|
||||
player = self.engine.player
|
||||
key = event.sym
|
||||
index = key - tcod.event.K_a
|
||||
@ -206,7 +229,7 @@ class InventoryEventHandler(AskUserEventHandler):
|
||||
return super().ev_keydown(event)
|
||||
|
||||
@overload
|
||||
def on_item_selected(self, item: Item) -> Optional[Action]:
|
||||
def on_item_selected(self, item: Item) -> Optional[ActionOrHandler]:
|
||||
"""Called when the user selects a valid item."""
|
||||
|
||||
|
||||
@ -215,7 +238,7 @@ class InventoryActivateHandler(InventoryEventHandler):
|
||||
|
||||
TITLE = "Select an item to use"
|
||||
|
||||
def on_item_selected(self, item: Item) -> Optional[Action]:
|
||||
def on_item_selected(self, item: Item) -> Optional[ActionOrHandler]:
|
||||
"""Return the action for the selected item."""
|
||||
return item.consumable.get_action(self.engine.player)
|
||||
|
||||
@ -225,7 +248,7 @@ class InventoryDropHandler(InventoryEventHandler):
|
||||
|
||||
TITLE = "Select an item to drop"
|
||||
|
||||
def on_item_selected(self, item: Item) -> Optional[Action]:
|
||||
def on_item_selected(self, item: Item) -> Optional[ActionOrHandler]:
|
||||
"""Drop this item."""
|
||||
return actions.DropItem(self.engine.player, item)
|
||||
|
||||
@ -246,7 +269,7 @@ class SelectIndexHandler(AskUserEventHandler):
|
||||
console.tiles_rgb["bg"][x, y] = color.white
|
||||
console.tiles_rgb["fg"][x, y] = color.black
|
||||
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
|
||||
"""Check for key movement or confirmation keys."""
|
||||
key = event.sym
|
||||
if key in MOVE_KEYS:
|
||||
@ -273,7 +296,7 @@ class SelectIndexHandler(AskUserEventHandler):
|
||||
|
||||
return super().ev_keydown(event)
|
||||
|
||||
def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> Optional[Action]:
|
||||
def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> Optional[ActionOrHandler]:
|
||||
"""Left click confirms a selection."""
|
||||
if self.engine.game_map.in_bounds(*event.tile):
|
||||
if event.button == 1:
|
||||
@ -282,16 +305,16 @@ class SelectIndexHandler(AskUserEventHandler):
|
||||
return super().ev_mousebuttondown(event)
|
||||
|
||||
@overload
|
||||
def on_index_selected(self, x: int, y: int) -> Optional[Action]:
|
||||
def on_index_selected(self, x: int, y: int) -> Optional[ActionOrHandler]:
|
||||
"""Called when an index is selected."""
|
||||
|
||||
|
||||
class LookHandler(SelectIndexHandler):
|
||||
"""Lets the player look around using the keyboard."""
|
||||
|
||||
def on_index_selected(self, x: int, y: int) -> None:
|
||||
def on_index_selected(self, x: int, y: int) -> MainGameEventHandler:
|
||||
"""Return to main handler."""
|
||||
self.engine.event_handler = MainGameEventHandler(self.engine)
|
||||
return MainGameEventHandler(self.engine)
|
||||
|
||||
|
||||
class SingleRangedAttackHandler(SelectIndexHandler):
|
||||
@ -300,13 +323,13 @@ class SingleRangedAttackHandler(SelectIndexHandler):
|
||||
def __init__(
|
||||
self,
|
||||
engine: Engine,
|
||||
callback: Callable[[Tuple[int, int]], Optional[Action]]
|
||||
callback: Callable[[Tuple[int, int]], Optional[ActionOrHandler]]
|
||||
):
|
||||
super().__init__(engine)
|
||||
|
||||
self.callback = callback
|
||||
|
||||
def on_index_selected(self, x: int, y: int) -> Optional[Action]:
|
||||
def on_index_selected(self, x: int, y: int) -> Optional[ActionOrHandler]:
|
||||
return self.callback((x, y))
|
||||
|
||||
|
||||
@ -320,7 +343,7 @@ class AreaRangedAttackHandler(SelectIndexHandler):
|
||||
self,
|
||||
engine: Engine,
|
||||
radius: int,
|
||||
callback: Callable[[Tuple[int, int]], Optional[Action]]
|
||||
callback: Callable[[Tuple[int, int]], Optional[ActionOrHandler]]
|
||||
):
|
||||
super().__init__(engine)
|
||||
|
||||
@ -343,12 +366,12 @@ class AreaRangedAttackHandler(SelectIndexHandler):
|
||||
clear=False,
|
||||
)
|
||||
|
||||
def on_index_selected(self, x: int, y: int) -> Optional[Action]:
|
||||
def on_index_selected(self, x: int, y: int) -> Optional[ActionOrHandler]:
|
||||
return self.callback((x, y))
|
||||
|
||||
|
||||
class MainGameEventHandler(EventHandler):
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
|
||||
action: Optional[Action] = None
|
||||
|
||||
key = event.sym
|
||||
@ -365,17 +388,17 @@ class MainGameEventHandler(EventHandler):
|
||||
raise SystemExit()
|
||||
|
||||
elif key == tcod.event.K_v:
|
||||
self.engine.event_handler = HistoryViewer(self.engine)
|
||||
return HistoryViewer(self.engine)
|
||||
|
||||
elif key == tcod.event.K_g:
|
||||
action = PickupAction(player)
|
||||
|
||||
elif key == tcod.event.K_i:
|
||||
self.engine.event_handler = InventoryActivateHandler(self.engine)
|
||||
return InventoryActivateHandler(self.engine)
|
||||
elif key == tcod.event.K_d:
|
||||
self.engine.event_handler = InventoryDropHandler(self.engine)
|
||||
return InventoryDropHandler(self.engine)
|
||||
elif key == tcod.event.K_SLASH:
|
||||
self.engine.event_handler = LookHandler(self.engine)
|
||||
return LookHandler(self.engine)
|
||||
|
||||
# No valid key was pressed
|
||||
return action
|
||||
@ -430,7 +453,7 @@ class HistoryViewer(EventHandler):
|
||||
)
|
||||
log_console.blit(console, 3, 3)
|
||||
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> None:
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[MainGameEventHandler]:
|
||||
# Fancy conditional movement to make it feel right.
|
||||
if event.sym in CURSOR_Y_KEYS:
|
||||
adjust = CURSOR_Y_KEYS[event.sym]
|
||||
@ -448,4 +471,6 @@ class HistoryViewer(EventHandler):
|
||||
elif event.sym == tcod.event.K_END:
|
||||
self.cursor = self.log_length - 1 # Move directly to the last message.
|
||||
else: # Any other key moves back to the main game state.
|
||||
self.engine.event_handler = MainGameEventHandler(self.engine)
|
||||
return MainGameEventHandler(self.engine)
|
||||
|
||||
return None
|
||||
|
38
main.py
38
main.py
@ -7,6 +7,8 @@ import tcod
|
||||
import color
|
||||
from engine import Engine
|
||||
import entity_factories
|
||||
import exceptions
|
||||
import input_handlers
|
||||
from procgen import generate_dungeon
|
||||
|
||||
|
||||
@ -52,6 +54,8 @@ def main() -> None:
|
||||
color.welcome_text
|
||||
)
|
||||
|
||||
handler: input_handlers.BaseEventHandler = input_handlers.MainGameEventHandler(engine)
|
||||
|
||||
with tcod.context.new_terminal(
|
||||
screen_width,
|
||||
screen_height,
|
||||
@ -60,19 +64,29 @@ def main() -> None:
|
||||
vsync=True,
|
||||
) as context:
|
||||
root_console = tcod.Console(screen_width, screen_height, order="F")
|
||||
while True:
|
||||
root_console.clear()
|
||||
engine.event_handler.on_render(console=root_console)
|
||||
context.present(root_console)
|
||||
try:
|
||||
while True:
|
||||
root_console.clear()
|
||||
engine.event_handler.on_render(console=root_console)
|
||||
context.present(root_console)
|
||||
|
||||
try:
|
||||
for event in tcod.event.wait():
|
||||
context.convert_event(event)
|
||||
engine.event_handler.handle_events(event)
|
||||
except Exception: # Handle exceptions in game.
|
||||
traceback.print_exc() # Print error to stderr.
|
||||
# Then print the error to the message log.
|
||||
engine.message_log.add_message(traceback.format_exc(), color.error)
|
||||
try:
|
||||
for event in tcod.event.wait():
|
||||
context.convert_event(event)
|
||||
engine.event_handler.handle_events(event)
|
||||
except Exception: # Handle exceptions in game.
|
||||
traceback.print_exc() # Print error to stderr.
|
||||
# Then print the error to the message log.
|
||||
engine.message_log.add_message(traceback.format_exc(), color.error)
|
||||
|
||||
except exceptions.QuitWithoutSaving:
|
||||
raise
|
||||
except SystemExit: # Save and quit.
|
||||
# TODO: Add the save function here
|
||||
raise
|
||||
except BaseException: # Save on any other unexpected exception.
|
||||
# TODO: Add the save function here
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
Loading…
Reference in New Issue
Block a user