diff --git a/source/actions.py b/source/actions.py index 1bafeba..cce0c16 100644 --- a/source/actions.py +++ b/source/actions.py @@ -1,21 +1,25 @@ +from typing import Any + class BaseAction: + entity: Any + def __init__(self, entity): self.entity = entity - def apply(self) -> None: + def apply(self) -> bool: raise NotImplementedError() class QuitAction(BaseAction): def __init__(self): #override the base __init__ pass - def apply(self) -> None: + def apply(self) -> bool: raise SystemExit() class WaitAction(BaseAction): - def apply(self) -> None: - pass + def apply(self) -> bool: + return True class DirectionAction(BaseAction): @@ -24,43 +28,52 @@ class DirectionAction(BaseAction): self.xdir = xdir self.ydir = ydir - def apply(self) -> None: + def apply(self) -> bool: raise NotImplementedError() class MovementAction(DirectionAction): - def apply(self) -> None: + def apply(self) -> bool: dest_x = self.entity.x + self.xdir dest_y = self.entity.y + self.ydir #bounds and collision checks if not self.entity.floor_map.in_bounds(dest_x, dest_y): - return + return False if not self.entity.floor_map.tiles["walkable"][dest_x, dest_y]: - return + return False if self.entity.floor_map.get_entity_at(dest_x, dest_y, unwalkable_only=True) is not None: - return + return False self.entity.set_pos(dest_x, dest_y) + return True class MeleeAction(DirectionAction): - def apply(self) -> None: + def apply(self) -> bool: dest_x = self.entity.x + self.xdir dest_y = self.entity.y + self.ydir - target = self.entity.floor_map.get_entity_at(dest_x, dest_y) + target = self.entity.floor_map.get_actor_at(dest_x, dest_y) if not target: - return + return False + + damage = self.entity.fighter.attack - target.fighter.defense - print(f"You kicked the {target.name}, which was funny") + if damage > 0: + print(f"{self.entity.name} attacked {target.name} for {damage} damage") + target.fighter.current_hp -= damage + else: + print(f"{self.entity.name} attacked {target.name} but was ineffective") + + return True class BumpAction(DirectionAction): #bad name, deal with it - def apply(self) -> None: + def apply(self) -> bool: dest_x = self.entity.x + self.xdir dest_y = self.entity.y + self.ydir - if self.entity.floor_map.get_entity_at(dest_x, dest_y): + if self.entity.floor_map.get_entity_at(dest_x, dest_y, unwalkable_only=True): return MeleeAction(self.entity, self.xdir, self.ydir).apply() else: return MovementAction(self.entity, self.xdir, self.ydir).apply() diff --git a/source/components/fighter.py b/source/components/fighter.py index 22968e8..a18b6f4 100644 --- a/source/components/fighter.py +++ b/source/components/fighter.py @@ -12,6 +12,10 @@ class Fighter(BaseComponent): self._current_hp = hp self.attack = attack self.defense = defense + + @property + def maximum_hp(self) -> int: + return self._maximum_hp @property def current_hp(self) -> int: @@ -20,4 +24,21 @@ class Fighter(BaseComponent): @current_hp.setter def current_hp(self, value: int) -> None: self._current_hp = max(0, min(value, self._maximum_hp)) + if self.current_hp <= 0: + self.die_and_despawn() + + def die_and_despawn(self) -> None: + if self.entity is self.entity.floor_map.engine.player and self.entity.ai: + from event_handler import GameOverEventHandler + self.entity.floor_map.engine.event_handler = GameOverEventHandler(self.entity.floor_map.engine) + print("You died") + + else: + print(f"The {self.entity.name} died") + + self.entity.char = "%" + self.entity.color = (191, 0, 0) + self.entity.walkable = True + self.entity.ai = None + self.entity.name = f"Dead {self.entity.name}" diff --git a/source/engine.py b/source/engine.py index 7b10cf9..3a9ee07 100644 --- a/source/engine.py +++ b/source/engine.py @@ -7,8 +7,8 @@ from floor_map import FloorMap #TODO: replace with "DungeonMap" class Engine: def __init__(self, floor_map: FloorMap): - from event_handler import EventHandler - self.event_handler = EventHandler(self) + from event_handler import InGameEventHandler + self.event_handler = InGameEventHandler(self) self.floor_map = floor_map self.floor_map.engine = self #references everywhere! @@ -18,6 +18,13 @@ class Engine: #kick off the render self.update_fov() + def run_loop(self, context: Context, console: Console) -> None: + while True: + if self.event_handler.handle_events(): + self.handle_entities() + + self.handle_rendering(context, console) + def handle_entities(self) -> None: self.update_fov() #knowing the FOV lets entities mess with it @@ -30,7 +37,11 @@ class Engine: #map and all entities within self.floor_map.render(console) - #TODO: UI + #UI + console.print( + x=1, y=47, + string=f"HP: {self.player.fighter.current_hp}/{self.player.fighter.current_hp}", + ) #send to the screen context.present(console) diff --git a/source/entity.py b/source/entity.py index 45a306f..ff3da22 100644 --- a/source/entity.py +++ b/source/entity.py @@ -25,7 +25,7 @@ class Entity: self.walkable = walkable self.floor_map = floor_map - def spawn(self: T, x: int, y: int, floor_map): + def spawn(self, x: int, y: int, floor_map): clone = copy.deepcopy(self) clone.x = x clone.y = y diff --git a/source/entity_types.py b/source/entity_types.py index f942eba..96a7af4 100644 --- a/source/entity_types.py +++ b/source/entity_types.py @@ -8,7 +8,7 @@ player = Actor( name = "Player", walkable = False, ai_class = BaseAI, - fighter = Fighter(hp = 10, attack = 2, defense = 2), + fighter = Fighter(hp = 10, attack = 2, defense = 0), ) #gobbos diff --git a/source/event_handler.py b/source/event_handler.py index 85f1c58..2d61959 100644 --- a/source/event_handler.py +++ b/source/event_handler.py @@ -2,46 +2,115 @@ from typing import Optional import tcod -from actions import BaseAction, QuitAction, BumpAction +from actions import BaseAction, QuitAction, BumpAction, WaitAction from engine import Engine +#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, +} + #event handler is one part of the engine class EventHandler(tcod.event.EventDispatch[BaseAction]): def __init__(self, engine: Engine): super().__init__() self.engine = engine - def handle_events(self) -> None: + #callbacks + def ev_quit(self, event: tcod.event.Quit) -> Optional[BaseAction]: + return QuitAction() + + def handle_events(self) -> bool: + raise NotImplementedError() + + +class InGameEventHandler(EventHandler): + def handle_events(self) -> bool: + result = False + for event in tcod.event.wait(): action = self.dispatch(event) if action is None: continue - action.apply() #entity references the engine + result |= action.apply() #entity references the engine - #callbacks - def ev_quit(self, event: tcod.event.Quit) -> Optional[BaseAction]: - return QuitAction() + return result def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]: key = event.sym #SDL stuff, neat. player = self.engine.player - #parse player input - match key: - case tcod.event.KeySym.ESCAPE: - return QuitAction() + #player input + if key == tcod.event.KeySym.ESCAPE: + return QuitAction() - case tcod.event.KeySym.UP: - return BumpAction(player, xdir = 0, ydir = -1) - case tcod.event.KeySym.DOWN: - return BumpAction(player, xdir = 0, ydir = 1) - case tcod.event.KeySym.LEFT: - return BumpAction(player, xdir = -1, ydir = 0) - case tcod.event.KeySym.RIGHT: - return BumpAction(player, xdir = 1, ydir = 0) + 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) - case _: - return None \ No newline at end of file + +class GameOverEventHandler(EventHandler): + def handle_events(self) -> bool: + result = False + + for event in tcod.event.wait(): + action = self.dispatch(event) + + if action is None: + continue + + result |= action.apply() + + return result + + 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() + + return None \ No newline at end of file diff --git a/source/floor_map.py b/source/floor_map.py index 052a958..c336641 100644 --- a/source/floor_map.py +++ b/source/floor_map.py @@ -48,7 +48,7 @@ class FloorMap: def get_actor_at(self, x: int, y: int) -> Optional[Actor]: for actor in self.actors: - if actor.x == x and actor.y == y: + if actor.x == x and actor.y == y and actor.is_alive(): return actor return None @@ -60,6 +60,18 @@ class FloorMap: default = tile_types.SHROUD ) - for entity in self.entities: + #render the dead stuff below the alive stuff + alive = (entity for entity in self.entities if entity.is_alive()) + dead = (entity for entity in self.entities if not entity.is_alive()) + + for entity in dead: if self.visible[entity.x, entity.y]: - console.print(entity.x, entity.y, entity.char, fg=entity.color) \ No newline at end of file + console.print(entity.x, entity.y, entity.char, fg=entity.color) + + for entity in alive: + if self.visible[entity.x, entity.y]: + console.print(entity.x, entity.y, entity.char, fg=entity.color) + + #print the player above everything else for clarity + if self.player: + console.print(self.player.x, self.player.y, self.player.char, fg=self.player.color) #TODO: I didn't realize the render order would be fixed in the tutorial \ No newline at end of file diff --git a/source/main.py b/source/main.py index b4f7f74..396fed5 100755 --- a/source/main.py +++ b/source/main.py @@ -18,7 +18,7 @@ def main() -> None: console = tcod.console.Console( width = w, - height = h, + height = h + 5, order = "F" ) @@ -27,11 +27,8 @@ def main() -> None: floor_map = generate_floor_map(80, 45, 10, 10) ) - # game loop - while True: - engine.event_handler.handle_events() - engine.handle_entities() - engine.handle_rendering(context, console) + #game loop that never returns + engine.run_loop(context, console) # this seems odd to me if __name__ == "__main__":