Added ScrollOfLightning as an example. Not finished yet, confusion & fireball are still in part 9.
547 lines
15 KiB
Python
547 lines
15 KiB
Python
from __future__ import annotations
|
|
from typing import List, Optional, Tuple, TYPE_CHECKING
|
|
|
|
import tcod
|
|
|
|
import colors
|
|
from actions import (
|
|
BaseAction,
|
|
QuitAction,
|
|
BumpAction,
|
|
WaitAction,
|
|
PickupAction,
|
|
DropAction,
|
|
DropPartialStackAction,
|
|
UsageAction,
|
|
)
|
|
|
|
from useable import (
|
|
BaseUseable,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from engine import Engine
|
|
from entity import Entity
|
|
from floor_map import FloorMap
|
|
|
|
#input options
|
|
MOVE_KEYS = {
|
|
#arrow keys
|
|
tcod.event.KeySym.UP: (0, -1),
|
|
tcod.event.KeySym.DOWN: (0, 1),
|
|
tcod.event.KeySym.LEFT: (-1, 0),
|
|
tcod.event.KeySym.RIGHT: (1, 0),
|
|
|
|
tcod.event.KeySym.HOME: (-1, -1),
|
|
tcod.event.KeySym.END: (-1, 1),
|
|
tcod.event.KeySym.PAGEUP: (1, -1),
|
|
tcod.event.KeySym.PAGEDOWN: (1, 1),
|
|
|
|
#numpad keys
|
|
tcod.event.KeySym.KP_1: (-1, 1),
|
|
tcod.event.KeySym.KP_2: (0, 1),
|
|
tcod.event.KeySym.KP_3: (1, 1),
|
|
tcod.event.KeySym.KP_4: (-1, 0),
|
|
|
|
tcod.event.KeySym.KP_6: (1, 0),
|
|
tcod.event.KeySym.KP_7: (-1, -1),
|
|
tcod.event.KeySym.KP_8: (0, -1),
|
|
tcod.event.KeySym.KP_9: (1, -1),
|
|
|
|
#vi key mapping
|
|
tcod.event.KeySym.h: (-1, 0),
|
|
tcod.event.KeySym.j: (0, 1),
|
|
tcod.event.KeySym.k: (0, -1),
|
|
tcod.event.KeySym.l: (1, 0),
|
|
|
|
tcod.event.KeySym.y: (-1, -1),
|
|
tcod.event.KeySym.u: (1, -1),
|
|
tcod.event.KeySym.b: (-1, 1),
|
|
tcod.event.KeySym.n: (1, 1),
|
|
}
|
|
|
|
WAIT_KEYS = {
|
|
tcod.event.KeySym.PERIOD,
|
|
tcod.event.KeySym.KP_5,
|
|
tcod.event.KeySym.CLEAR,
|
|
}
|
|
|
|
PICKUP_KEYS = {
|
|
tcod.event.KeySym.COMMA,
|
|
tcod.event.KeySym.SPACE,
|
|
}
|
|
|
|
CURSOR_SCROLL_KEYS = {
|
|
tcod.event.KeySym.UP: -1,
|
|
tcod.event.KeySym.DOWN: 1,
|
|
tcod.event.KeySym.PAGEUP: -10,
|
|
tcod.event.KeySym.PAGEDOWN: 10,
|
|
|
|
tcod.event.KeySym.KP_2: 1,
|
|
tcod.event.KeySym.KP_8: -1,
|
|
}
|
|
|
|
CURSOR_CONFIRM_KEYS = {
|
|
tcod.event.KeySym.RETURN,
|
|
tcod.event.KeySym.SPACE,
|
|
}
|
|
|
|
TILE_SCROLL_KEYS = MOVE_KEYS #copied
|
|
|
|
# TILE_SELECTOR_KEYS
|
|
|
|
TILE_CONFIRM_KEYS = CURSOR_CONFIRM_KEYS #copied
|
|
|
|
#the event handlers are a big part of the engine
|
|
class EventHandler(tcod.event.EventDispatch[BaseAction]):
|
|
engine: Engine
|
|
parent_handler: EventHandler
|
|
|
|
def __init__(self,engine: Engine, parent_handler: EventHandler):
|
|
super().__init__()
|
|
self.engine = engine
|
|
self.parent_handler = parent_handler
|
|
|
|
def render(self, console: tcod.console.Console) -> None:
|
|
if self.parent_handler:
|
|
self.parent_handler.render(console)
|
|
|
|
#callbacks
|
|
def ev_quit(self, event: tcod.event.Quit) -> Optional[BaseAction]:
|
|
return QuitAction()
|
|
|
|
def handle_events(self, context: tcod.context.Context) -> bool:
|
|
"""If any Action signals True, then the game state should be progressed after this"""
|
|
result = False
|
|
|
|
for event in tcod.event.wait():
|
|
context.convert_event(event) #adds mouse position info
|
|
action = self.dispatch(event)
|
|
|
|
if action is None:
|
|
continue
|
|
|
|
result |= action.perform()
|
|
|
|
return result
|
|
|
|
|
|
class GameoverViewer(EventHandler):
|
|
"""Game over, man, GAME OVER!"""
|
|
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
|
|
key = event.sym #SDL stuff, neat.
|
|
|
|
#player input
|
|
if key == tcod.event.KeySym.ESCAPE:
|
|
return QuitAction()
|
|
|
|
if key == tcod.event.KeySym.BACKQUOTE: #lowercase tilde
|
|
self.engine.event_handler = LogHistoryViewer(self.engine, self)
|
|
|
|
return None
|
|
|
|
|
|
class GameplayViewer(EventHandler):
|
|
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
|
|
key = event.sym #SDL stuff, neat.
|
|
|
|
player = self.engine.player
|
|
|
|
#special keys
|
|
if key == tcod.event.KeySym.ESCAPE:
|
|
return QuitAction()
|
|
|
|
#gameplay keys
|
|
if key in MOVE_KEYS:
|
|
xdir, ydir = MOVE_KEYS[key]
|
|
return BumpAction(player, xdir = xdir, ydir = ydir)
|
|
|
|
if key in WAIT_KEYS:
|
|
return WaitAction(player)
|
|
|
|
if key in PICKUP_KEYS:
|
|
return PickupAction(player)
|
|
|
|
#menu keys
|
|
if key == tcod.event.KeySym.BACKQUOTE: #lowercase tilde
|
|
self.engine.event_handler = LogHistoryViewer(self.engine, self)
|
|
|
|
if key == tcod.event.KeySym.TAB:
|
|
self.engine.event_handler = InventoryViewer(self.engine, self, player)
|
|
|
|
#debugging - for various controls and testing needs
|
|
if (event.mod & tcod.event.Modifier.CTRL) and key == tcod.event.KeySym.d:
|
|
self.engine.event_handler = OptionSelector(
|
|
self.engine,
|
|
self,
|
|
title = "Debug Selector",
|
|
options = ["Tile Selector"],
|
|
callback = lambda x: self.dbg_callback(x),
|
|
margin_x = 20,
|
|
margin_y = 12,
|
|
)
|
|
|
|
def ev_mousemotion(self, event: tcod.event.MouseMotion) -> None:
|
|
if self.engine.floor_map.in_bounds(event.tile.x, event.tile.y):
|
|
self.engine.mouse_location = (event.tile.x, event.tile.y)
|
|
|
|
def dbg_callback(self, selected: int) -> Optional[BaseAction]:
|
|
if selected == 0:
|
|
player: Entity = self.engine.player
|
|
|
|
self.engine.event_handler = TileSelector(
|
|
self.engine,
|
|
self,
|
|
floor_map = self.engine.floor_map,
|
|
initial_pointer = (player.x, player.y),
|
|
callback=lambda: None
|
|
)
|
|
|
|
return None
|
|
|
|
|
|
class LogHistoryViewer(EventHandler):
|
|
def __init__(self, engine: Engine, parent_handler: EventHandler):
|
|
super().__init__(engine, parent_handler)
|
|
self.length = len(engine.message_log.messages)
|
|
self.cursor = self.length - 1 #start at the bottom
|
|
|
|
def render(self, console: tcod.console.Console) -> None:
|
|
super().render(console)
|
|
|
|
log_console = tcod.console.Console(console.width - 6, console.height - 6)
|
|
|
|
#rendering a nice window
|
|
log_console.draw_frame(
|
|
0,0, log_console.width, log_console.height,
|
|
# "╔═╗║ ║╚═╝"
|
|
decoration="\\x/x x/x\\",
|
|
fg=colors.terminal_dark, bg=colors.black
|
|
)
|
|
log_console.print_box(
|
|
0, 0, log_console.width, log_console.height,
|
|
string = "Message History",
|
|
alignment=tcod.constants.CENTER,
|
|
fg=colors.terminal_light, bg=colors.black
|
|
)
|
|
self.engine.message_log.render_messages(
|
|
log_console,
|
|
2, 2,
|
|
log_console.width - 4, log_console.height - 4,
|
|
self.engine.message_log.messages[:self.cursor + 1]
|
|
)
|
|
|
|
log_console.blit(console, 3, 3)
|
|
|
|
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
|
|
if event.sym in CURSOR_SCROLL_KEYS:
|
|
adjust = CURSOR_SCROLL_KEYS[event.sym]
|
|
if adjust < 0 and self.cursor == 0:
|
|
pass #do nothing
|
|
elif adjust > 0 and self.cursor == self.length - 1:
|
|
pass #do nothing
|
|
else:
|
|
self.cursor = max(0, min(self.length - 1, self.cursor + adjust))
|
|
|
|
elif event.sym == tcod.event.KeySym.HOME:
|
|
self.cursor = 0
|
|
elif event.sym == tcod.event.KeySym.END:
|
|
self.cursor = self.length - 1
|
|
else:
|
|
#return to the game - where's the any key?
|
|
self.engine.event_handler = self.parent_handler
|
|
|
|
|
|
class InventoryViewer(EventHandler):
|
|
def __init__(self, engine: Engine, parent_handler: EventHandler, entity: Entity): #this entity's inventory
|
|
super().__init__(engine, parent_handler)
|
|
self.entity = entity
|
|
self.length = len(self.entity.inventory.contents)
|
|
self.cursor = 0
|
|
|
|
def render(self, console: tcod.console.Console) -> None:
|
|
super().render(console)
|
|
|
|
inner_console = tcod.console.Console(console.width - 6, console.height - 6)
|
|
|
|
#rendering a nice window
|
|
inner_console.draw_frame(
|
|
0,0, inner_console.width, inner_console.height,
|
|
# "╔═╗║ ║╚═╝"
|
|
decoration="\\x/x x/x\\",
|
|
fg=colors.terminal_dark, bg=colors.black
|
|
)
|
|
inner_console.print_box(
|
|
0, 0, inner_console.width, inner_console.height,
|
|
string = "Inventory",
|
|
alignment=tcod.constants.CENTER,
|
|
fg=colors.terminal_light, bg=colors.black
|
|
)
|
|
|
|
#render the cursor & inventory contents
|
|
offset: int = 0
|
|
for item in self.entity.inventory.contents:
|
|
msg = item.name
|
|
if item.useable.current_stack > 1:
|
|
msg = f"{msg} x{item.useable.current_stack}"
|
|
inner_console.print(
|
|
4, 2 + offset,
|
|
string = msg,
|
|
fg=colors.terminal_light, bg=colors.black,
|
|
)
|
|
offset += 2
|
|
|
|
if self.length > 0:
|
|
inner_console.print(2, 2 + self.cursor * 2, string = ">", fg=colors.terminal_light, bg=colors.black)
|
|
else:
|
|
#if inventory is empty, show a default message
|
|
inner_console.print_box(
|
|
0,inner_console.height // 2, inner_console.width, inner_console.height,
|
|
string = "EMPTY",
|
|
fg=colors.terminal_dark, bg=colors.black,
|
|
alignment=tcod.constants.CENTER
|
|
)
|
|
|
|
inner_console.blit(console, 3, 3)
|
|
|
|
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
|
|
if event.sym in CURSOR_SCROLL_KEYS:
|
|
adjust = CURSOR_SCROLL_KEYS[event.sym]
|
|
if adjust < 0 and self.cursor == 0:
|
|
pass #do nothing
|
|
elif adjust > 0 and self.cursor == self.length - 1:
|
|
pass #do nothing
|
|
else:
|
|
self.cursor = max(0, min(self.length - 1, self.cursor + adjust))
|
|
|
|
#extra size check, so empty inventories still work
|
|
elif self.length > 0 and event.sym in CURSOR_CONFIRM_KEYS:
|
|
#drop an item form this entity's inventory
|
|
item: Entity = self.entity.inventory.access(self.cursor)
|
|
|
|
#defaults
|
|
options = ["Use", "Drop", "Back"]
|
|
callback = lambda x: self.default_selector_callback(x)
|
|
|
|
#different options for different situations
|
|
if item.useable.current_stack > 1:
|
|
options = ["Use", "Drop 1", "Drop All", "Back"]
|
|
callback = lambda x: self.stack_selector_callback(x)
|
|
|
|
self.engine.event_handler = OptionSelector(
|
|
self.engine,
|
|
self,
|
|
title=item.name,
|
|
options=options,
|
|
callback=callback,
|
|
margin_x=20,
|
|
margin_y=12,
|
|
)
|
|
|
|
#TODO: hotkeys via a config
|
|
|
|
elif event.sym == tcod.event.KeySym.HOME:
|
|
self.cursor = 0
|
|
elif event.sym == tcod.event.KeySym.END:
|
|
self.cursor = self.length - 1
|
|
else:
|
|
#return to the game - where's the any key?
|
|
self.engine.event_handler = self.parent_handler
|
|
|
|
#utils
|
|
def default_selector_callback(self, selected: int) -> Optional[BaseAction]:
|
|
if selected == 0: #Use
|
|
return self.use()
|
|
elif selected == 1: #Drop
|
|
return self.drop()
|
|
elif selected == 2: #Back
|
|
return None #the selector does the change
|
|
|
|
def stack_selector_callback(self, selected: int) -> Optional[BaseAction]:
|
|
if selected == 0: #Use
|
|
return self.use()
|
|
elif selected == 1: #Drop 1
|
|
return self.drop_partial_stack(1)
|
|
elif selected == 2: #Drop all
|
|
return self.drop()
|
|
elif selected == 3: #Back
|
|
return None #the selector does the change
|
|
|
|
def use(self) -> Optional[BaseAction]:
|
|
"""Use the item at the cursor's position."""
|
|
if self.length > 0:
|
|
item: Entity = self.entity.inventory.access(self.cursor)
|
|
usable: BaseUseable = item.useable
|
|
|
|
#for ranged items, delegate to the tile selector
|
|
if usable.maximum_range > 0:
|
|
self.engine.event_handler = TileSelector(
|
|
self.engine,
|
|
parent_handler=self.engine.event_handler,
|
|
floor_map = self.engine.floor_map,
|
|
initial_pointer = (self.entity.x, self.entity.y),
|
|
callback=lambda x, y: self.use_at_range(x, y)
|
|
)
|
|
else:
|
|
#non-ranged items target the entity
|
|
return UsageAction(self.entity, self.cursor, self.entity, lambda x: self.adjust_length(x))
|
|
|
|
def use_at_range(self, target_x: int, target_y: int) -> Optional[BaseAction]:
|
|
#TODO: For now, just target living entities
|
|
targets: List[Entity] = list(filter(lambda entity: entity.is_alive() and entity.x == target_x and entity.y == target_y, self.engine.floor_map.entities))
|
|
|
|
#TODO: skip targeting for AOE
|
|
#TODO: close the inventory if you've used a consumable?
|
|
if len(targets) > 0:
|
|
return UsageAction(self.entity, self.cursor, targets.pop(), lambda x: self.adjust_length(x))
|
|
else:
|
|
self.engine.message_log.add_message("No target found.", colors.yellow)
|
|
|
|
def drop_partial_stack(self, amount: int) -> Optional[BaseAction]:
|
|
"""Drop part of an item stack at the cursor's position, and adjust the cursor if needed."""
|
|
if self.length > 0:
|
|
return DropPartialStackAction(self.entity, self.cursor, amount, lambda x: self.adjust_length(x))
|
|
|
|
def drop(self) -> Optional[BaseAction]:
|
|
"""Drop the item at the cursor's position, and adjust the cursor if needed."""
|
|
if self.length > 0:
|
|
return DropAction(self.entity, self.cursor, lambda x: self.adjust_length(x))
|
|
|
|
def adjust_length(self, amount: int):
|
|
self.length += amount
|
|
if self.cursor >= self.length:
|
|
self.cursor = self.length - 1
|
|
|
|
|
|
#generic tools
|
|
class OptionSelector(EventHandler):
|
|
def __init__(
|
|
self,
|
|
engine: Engine,
|
|
parent_handler: EventHandler,
|
|
*,
|
|
options: List[str],
|
|
callback: function,
|
|
title: str = "Select Option",
|
|
margin_x: int = 10,
|
|
margin_y: int = 8
|
|
):
|
|
super().__init__(engine, parent_handler)
|
|
self.options = options
|
|
self.callback = callback
|
|
self.length = len(options)
|
|
self.cursor = 0
|
|
|
|
#graphical prettiness
|
|
self.title = title
|
|
self.margin_x = margin_x
|
|
self.margin_y = margin_y
|
|
|
|
def render(self, console: tcod.console.Console) -> None:
|
|
super().render(console)
|
|
|
|
select_console = tcod.console.Console(console.width - self.margin_x*2, console.height - self.margin_y*2)
|
|
|
|
#rendering a nice window
|
|
select_console.draw_frame(
|
|
0,0, select_console.width, select_console.height,
|
|
# "╔═╗║ ║╚═╝"
|
|
decoration="\\x/x x/x\\",
|
|
fg=colors.terminal_dark, bg=colors.black
|
|
)
|
|
select_console.print_box(
|
|
0, 0, select_console.width, select_console.height,
|
|
string = self.title,
|
|
alignment=tcod.constants.CENTER,
|
|
fg=colors.terminal_light, bg=colors.black
|
|
)
|
|
|
|
#render the cursor & options
|
|
offset: int = 0
|
|
for option in self.options:
|
|
select_console.print(
|
|
4, 2 + offset,
|
|
string = option,
|
|
fg=colors.terminal_light, bg=colors.black,
|
|
)
|
|
offset += 2
|
|
|
|
select_console.print(2, 2 + self.cursor * 2, string = ">", fg=colors.terminal_light, bg=colors.black)
|
|
|
|
select_console.blit(console, self.margin_x, self.margin_y)
|
|
|
|
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
|
|
if event.sym in CURSOR_SCROLL_KEYS:
|
|
adjust = CURSOR_SCROLL_KEYS[event.sym]
|
|
if adjust < 0 and self.cursor == 0:
|
|
pass #do nothing
|
|
elif adjust > 0 and self.cursor == self.length - 1:
|
|
pass #do nothing
|
|
else:
|
|
self.cursor = max(0, min(self.length - 1, self.cursor + adjust))
|
|
|
|
elif event.sym in CURSOR_CONFIRM_KEYS:
|
|
self.engine.event_handler = self.parent_handler
|
|
return self.callback(self.cursor)
|
|
|
|
elif event.sym == tcod.event.KeySym.HOME:
|
|
self.cursor = 0
|
|
elif event.sym == tcod.event.KeySym.END:
|
|
self.cursor = self.length - 1
|
|
else:
|
|
#return to the game
|
|
self.engine.event_handler = self.parent_handler
|
|
|
|
|
|
class TileSelector(EventHandler):
|
|
floor_map: FloorMap
|
|
cursor_x: int
|
|
cursor_y: int
|
|
callback: function
|
|
|
|
def __init__(
|
|
self,
|
|
engine,
|
|
parent_handler,
|
|
*,
|
|
floor_map: FloorMap,
|
|
initial_pointer: Tuple[int, int],
|
|
callback: function,
|
|
):
|
|
super().__init__(engine, parent_handler)
|
|
self.floor_map = floor_map
|
|
self.cursor_x, self.cursor_y = initial_pointer
|
|
self.callback = callback
|
|
|
|
def render(self, console: tcod.console.Console) -> None:
|
|
#DON'T render the parent, instead, find and render the gameplay handler
|
|
parent: EventHandler = self.parent_handler
|
|
while parent and parent is not GameplayViewer:
|
|
parent = parent.parent_handler
|
|
if parent:
|
|
parent.render(console)
|
|
|
|
#highlight via inverting colors
|
|
console.rgb["fg"][self.cursor_x, self.cursor_y] ^= 0xff
|
|
console.rgb["bg"][self.cursor_x, self.cursor_y] ^= 0xff
|
|
|
|
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
|
|
key = event.sym #SDL stuff, neat.
|
|
|
|
#special keys
|
|
if key == tcod.event.KeySym.ESCAPE:
|
|
#return to the game
|
|
self.engine.event_handler = self.parent_handler
|
|
|
|
#selection keys
|
|
elif key in TILE_SCROLL_KEYS:
|
|
xdir, ydir = TILE_SCROLL_KEYS[key]
|
|
|
|
#because actions "only" change the game state, rather than selecting something, just move the cursor from here
|
|
self.cursor_x += xdir
|
|
self.cursor_y += ydir
|
|
|
|
elif key in TILE_CONFIRM_KEYS:
|
|
self.engine.event_handler = self.parent_handler
|
|
return self.callback(self.cursor_x, self.cursor_y)
|