1
0

Refactor event handling

This commit is contained in:
Timothy Warren 2022-01-18 14:04:05 -05:00
parent a1e6125c34
commit e82d2e5b49
7 changed files with 113 additions and 70 deletions

View File

@ -20,3 +20,6 @@ health_recovered = (0x0, 0xFF, 0x0)
bar_text = white bar_text = white
bar_filled = (0x0, 0x60, 0x0) bar_filled = (0x0, 0x60, 0x0)
bar_empty = (0x40, 0x10, 0x10) bar_empty = (0x40, 0x10, 0x10)
menu_title = (255, 255, 63)
menu_text = white

View File

@ -8,7 +8,11 @@ import components.ai
import components.inventory import components.inventory
from components.base_component import BaseComponent from components.base_component import BaseComponent
from exceptions import Impossible from exceptions import Impossible
from input_handlers import AreaRangedAttackHandler, SingleRangedAttackHandler from input_handlers import (
ActionOrHandler,
AreaRangedAttackHandler,
SingleRangedAttackHandler
)
if TYPE_CHECKING: if TYPE_CHECKING:
from entity import Actor, Item from entity import Actor, Item
@ -17,7 +21,7 @@ if TYPE_CHECKING:
class Consumable(BaseComponent): class Consumable(BaseComponent):
parent: Item 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.""" """Try to return the action for this item."""
return actions.ItemAction(consumer, self.parent) return actions.ItemAction(consumer, self.parent)
@ -40,18 +44,17 @@ class ConfusionConsumable(Consumable):
def __init__(self, number_of_turns: int): def __init__(self, number_of_turns: int):
self.number_of_turns = number_of_turns 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( self.engine.message_log.add_message(
"Select a target location.", "Select a target location.",
color.needs_target color.needs_target
) )
self.engine.event_handler = SingleRangedAttackHandler(
return SingleRangedAttackHandler(
self.engine, self.engine,
callback=lambda xy: actions.ItemAction(consumer, self.parent, xy), callback=lambda xy: actions.ItemAction(consumer, self.parent, xy),
) )
return None
def activate(self, action: actions.ItemAction) -> None: def activate(self, action: actions.ItemAction) -> None:
consumer = action.entity consumer = action.entity
target = action.target_actor target = action.target_actor
@ -99,19 +102,18 @@ class FireballDamageConsumable(Consumable):
self.damage = damage self.damage = damage
self.radius = radius 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( self.engine.message_log.add_message(
"Select a target location.", "Select a target location.",
color.needs_target color.needs_target
) )
self.engine.event_handler = AreaRangedAttackHandler(
return AreaRangedAttackHandler(
self.engine, self.engine,
self.radius, self.radius,
lambda xy: actions.ItemAction(consumer, self.parent, xy), lambda xy: actions.ItemAction(consumer, self.parent, xy),
) )
return None
def activate(self, action: actions.ItemAction) -> None: def activate(self, action: actions.ItemAction) -> None:
target_xy = action.target_xy target_xy = action.target_xy

View File

@ -4,7 +4,6 @@ from typing import TYPE_CHECKING
import color import color
from components.base_component import BaseComponent from components.base_component import BaseComponent
from input_handlers import GameOverEventHandler
from render_order import RenderOrder from render_order import RenderOrder
if TYPE_CHECKING: if TYPE_CHECKING:
@ -34,7 +33,6 @@ class Fighter(BaseComponent):
if self.engine.player is self.parent: if self.engine.player is self.parent:
death_message = "You died!" death_message = "You died!"
death_message_color = color.player_die death_message_color = color.player_die
self.engine.event_handler = GameOverEventHandler(self.engine)
else: else:
death_message = f"{self.parent.name} is dead!" death_message = f"{self.parent.name} is dead!"
death_message_color = color.enemy_die death_message_color = color.enemy_die

View File

@ -2,12 +2,10 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from tcod.context import Context
from tcod.console import Console from tcod.console import Console
from tcod.map import compute_fov from tcod.map import compute_fov
import exceptions import exceptions
from input_handlers import MainGameEventHandler
from message_log import MessageLog from message_log import MessageLog
from render_functions import render_bar, render_names_at_mouse_location from render_functions import render_bar, render_names_at_mouse_location
@ -20,7 +18,6 @@ class Engine:
game_map: GameMap game_map: GameMap
def __init__(self, player: Actor): def __init__(self, player: Actor):
self.event_handler: EventHandler = MainGameEventHandler(self)
self.message_log = MessageLog() self.message_log = MessageLog()
self.mouse_location = (0, 0) self.mouse_location = (0, 0)
self.player = player self.player = player

View File

@ -3,3 +3,7 @@ class Impossible(Exception):
The reason is given as the exception message. The reason is given as the exception message.
""" """
class QuitWithoutSaving(SystemExit):
"""Can be raised to exit the game without automatically saving."""

View File

@ -1,6 +1,6 @@
from __future__ import annotations 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 import tcod.event
@ -61,13 +61,50 @@ CONFIRM_KEYS = {
tcod.event.K_KP_ENTER, 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): def __init__(self, engine: Engine):
self.engine = engine self.engine = engine
def handle_events(self, event: tcod.event.Event) -> None: def handle_events(self, event: tcod.event.Event) -> BaseEventHandler:
self.handle_action(self.dispatch(event)) """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: def handle_action(self, action: Optional[Action]) -> bool:
"""Handle actions returned from event methods. """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): if self.engine.game_map.in_bounds(event.tile.x, event.tile.y):
self.engine.mouse_location = 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: def on_render(self, console: tcod.Console) -> None:
self.engine.render(console) self.engine.render(console)
@ -103,16 +137,7 @@ class EventHandler(tcod.event.EventDispatch[Action]):
class AskUserEventHandler(EventHandler): class AskUserEventHandler(EventHandler):
"""Handles user input for actions which require special input.""" """Handles user input for actions which require special input."""
def handle_action(self, action: Optional[Action]) -> bool: def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
"""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]:
"""By default any key exits this input handler.""" """By default any key exits this input handler."""
# Ignore modifier keys. # Ignore modifier keys.
if event.sym in { if event.sym in {
@ -127,18 +152,16 @@ class AskUserEventHandler(EventHandler):
return self.on_exit() 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.""" """By default any mouse click exits this input handler."""
return self.on_exit() 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. """Called when the user is trying to exit or cancel an action.
By default this returns to the main event handler. By default this returns to the main event handler.
""" """
self.engine.event_handler = MainGameEventHandler(self.engine) return MainGameEventHandler(self.engine)
return None
class InventoryEventHandler(AskUserEventHandler): class InventoryEventHandler(AskUserEventHandler):
@ -189,7 +212,7 @@ class InventoryEventHandler(AskUserEventHandler):
else: else:
console.print(x + 1, y + 1, "(Empty)") 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 player = self.engine.player
key = event.sym key = event.sym
index = key - tcod.event.K_a index = key - tcod.event.K_a
@ -206,7 +229,7 @@ class InventoryEventHandler(AskUserEventHandler):
return super().ev_keydown(event) return super().ev_keydown(event)
@overload @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.""" """Called when the user selects a valid item."""
@ -215,7 +238,7 @@ class InventoryActivateHandler(InventoryEventHandler):
TITLE = "Select an item to use" 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 the action for the selected item."""
return item.consumable.get_action(self.engine.player) return item.consumable.get_action(self.engine.player)
@ -225,7 +248,7 @@ class InventoryDropHandler(InventoryEventHandler):
TITLE = "Select an item to drop" 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.""" """Drop this item."""
return actions.DropItem(self.engine.player, 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["bg"][x, y] = color.white
console.tiles_rgb["fg"][x, y] = color.black 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.""" """Check for key movement or confirmation keys."""
key = event.sym key = event.sym
if key in MOVE_KEYS: if key in MOVE_KEYS:
@ -273,7 +296,7 @@ class SelectIndexHandler(AskUserEventHandler):
return super().ev_keydown(event) 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.""" """Left click confirms a selection."""
if self.engine.game_map.in_bounds(*event.tile): if self.engine.game_map.in_bounds(*event.tile):
if event.button == 1: if event.button == 1:
@ -282,16 +305,16 @@ class SelectIndexHandler(AskUserEventHandler):
return super().ev_mousebuttondown(event) return super().ev_mousebuttondown(event)
@overload @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.""" """Called when an index is selected."""
class LookHandler(SelectIndexHandler): class LookHandler(SelectIndexHandler):
"""Lets the player look around using the keyboard.""" """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.""" """Return to main handler."""
self.engine.event_handler = MainGameEventHandler(self.engine) return MainGameEventHandler(self.engine)
class SingleRangedAttackHandler(SelectIndexHandler): class SingleRangedAttackHandler(SelectIndexHandler):
@ -300,13 +323,13 @@ class SingleRangedAttackHandler(SelectIndexHandler):
def __init__( def __init__(
self, self,
engine: Engine, engine: Engine,
callback: Callable[[Tuple[int, int]], Optional[Action]] callback: Callable[[Tuple[int, int]], Optional[ActionOrHandler]]
): ):
super().__init__(engine) super().__init__(engine)
self.callback = callback 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)) return self.callback((x, y))
@ -320,7 +343,7 @@ class AreaRangedAttackHandler(SelectIndexHandler):
self, self,
engine: Engine, engine: Engine,
radius: int, radius: int,
callback: Callable[[Tuple[int, int]], Optional[Action]] callback: Callable[[Tuple[int, int]], Optional[ActionOrHandler]]
): ):
super().__init__(engine) super().__init__(engine)
@ -343,12 +366,12 @@ class AreaRangedAttackHandler(SelectIndexHandler):
clear=False, 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)) return self.callback((x, y))
class MainGameEventHandler(EventHandler): 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 action: Optional[Action] = None
key = event.sym key = event.sym
@ -365,17 +388,17 @@ class MainGameEventHandler(EventHandler):
raise SystemExit() raise SystemExit()
elif key == tcod.event.K_v: elif key == tcod.event.K_v:
self.engine.event_handler = HistoryViewer(self.engine) return HistoryViewer(self.engine)
elif key == tcod.event.K_g: elif key == tcod.event.K_g:
action = PickupAction(player) action = PickupAction(player)
elif key == tcod.event.K_i: elif key == tcod.event.K_i:
self.engine.event_handler = InventoryActivateHandler(self.engine) return InventoryActivateHandler(self.engine)
elif key == tcod.event.K_d: elif key == tcod.event.K_d:
self.engine.event_handler = InventoryDropHandler(self.engine) return InventoryDropHandler(self.engine)
elif key == tcod.event.K_SLASH: elif key == tcod.event.K_SLASH:
self.engine.event_handler = LookHandler(self.engine) return LookHandler(self.engine)
# No valid key was pressed # No valid key was pressed
return action return action
@ -430,7 +453,7 @@ class HistoryViewer(EventHandler):
) )
log_console.blit(console, 3, 3) 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. # Fancy conditional movement to make it feel right.
if event.sym in CURSOR_Y_KEYS: if event.sym in CURSOR_Y_KEYS:
adjust = CURSOR_Y_KEYS[event.sym] adjust = CURSOR_Y_KEYS[event.sym]
@ -448,4 +471,6 @@ class HistoryViewer(EventHandler):
elif event.sym == tcod.event.K_END: elif event.sym == tcod.event.K_END:
self.cursor = self.log_length - 1 # Move directly to the last message. self.cursor = self.log_length - 1 # Move directly to the last message.
else: # Any other key moves back to the main game state. 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
View File

@ -7,6 +7,8 @@ import tcod
import color import color
from engine import Engine from engine import Engine
import entity_factories import entity_factories
import exceptions
import input_handlers
from procgen import generate_dungeon from procgen import generate_dungeon
@ -52,6 +54,8 @@ def main() -> None:
color.welcome_text color.welcome_text
) )
handler: input_handlers.BaseEventHandler = input_handlers.MainGameEventHandler(engine)
with tcod.context.new_terminal( with tcod.context.new_terminal(
screen_width, screen_width,
screen_height, screen_height,
@ -60,19 +64,29 @@ def main() -> None:
vsync=True, vsync=True,
) as context: ) as context:
root_console = tcod.Console(screen_width, screen_height, order="F") root_console = tcod.Console(screen_width, screen_height, order="F")
while True: try:
root_console.clear() while True:
engine.event_handler.on_render(console=root_console) root_console.clear()
context.present(root_console) engine.event_handler.on_render(console=root_console)
context.present(root_console)
try: try:
for event in tcod.event.wait(): for event in tcod.event.wait():
context.convert_event(event) context.convert_event(event)
engine.event_handler.handle_events(event) engine.event_handler.handle_events(event)
except Exception: # Handle exceptions in game. except Exception: # Handle exceptions in game.
traceback.print_exc() # Print error to stderr. traceback.print_exc() # Print error to stderr.
# Then print the error to the message log. # Then print the error to the message log.
engine.message_log.add_message(traceback.format_exc(), color.error) 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__": if __name__ == "__main__":