diff --git a/actions.py b/actions.py index 5bf200a..5e063f1 100644 --- a/actions.py +++ b/actions.py @@ -1,6 +1,8 @@ from __future__ import annotations -from typing import Optional, Tuple, TYPE_CHECKING, overload +from typing import overload, Optional, Tuple, TYPE_CHECKING + +import color if TYPE_CHECKING: from engine import Engine @@ -71,11 +73,22 @@ class MeleeAction(ActionWithDirection): damage = self.entity.fighter.power - target.fighter.defense attack_desc = f"{self.entity.name.capitalize()} attacks {target.name}" + if self.entity is self.engine.player: + attack_color = color.player_atk + else: + attack_color = color.enemy_atk + if damage > 0: - print(f"{attack_desc} for {damage} hit points.") + self.engine.message_log.add_message( + f"{attack_desc} for {damage} hit points.", + attack_color, + ) target.fighter.hp -= damage else: - print(f"{attack_desc} but does no damage.") + self.engine.message_log.add_message( + f"{attack_desc} but does no damage.", + attack_color, + ) class MovementAction(ActionWithDirection): diff --git a/components/fighter.py b/components/fighter.py index 755d399..acb4fed 100644 --- a/components/fighter.py +++ b/components/fighter.py @@ -2,6 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING +import color from components.base_component import BaseComponent from input_handlers import GameOverEventHandler from render_order import RenderOrder @@ -32,9 +33,11 @@ class Fighter(BaseComponent): def die(self) -> None: if self.engine.player is self.entity: death_message = "You died!" + death_message_color = color.player_die self.engine.event_handler = GameOverEventHandler(self.engine) else: death_message = f"{self.entity.name} is dead!" + death_message_color = color.enemy_die self.entity.char = "%" self.entity.color = (191, 0, 0) @@ -43,4 +46,4 @@ class Fighter(BaseComponent): self.entity.name = f"remains of {self.entity.name}" self.entity.render_order = RenderOrder.CORPSE - print(death_message) + self.engine.message_log.add_message(death_message, death_message_color) diff --git a/engine.py b/engine.py index 8f8a2c9..f950500 100644 --- a/engine.py +++ b/engine.py @@ -7,6 +7,7 @@ from tcod.console import Console from tcod.map import compute_fov from input_handlers import MainGameEventHandler +from message_log import MessageLog from render_functions import render_bar if TYPE_CHECKING: @@ -19,6 +20,7 @@ class Engine: def __init__(self, player: Actor): self.event_handler: EventHandler = MainGameEventHandler(self) + self.message_log = MessageLog() self.player = player def handle_enemy_turns(self) -> None: @@ -40,6 +42,8 @@ class Engine: def render(self, console: Console, context: Context) -> None: self.game_map.render(console) + self.message_log.render(console=console, x=21, y=45, width=40, height=5) + render_bar( console, current_value=self.player.fighter.hp, diff --git a/main.py b/main.py index 74719ca..ffaded0 100755 --- a/main.py +++ b/main.py @@ -3,6 +3,7 @@ import copy import tcod +import color from engine import Engine import entity_factories from procgen import generate_dungeon @@ -13,7 +14,7 @@ def main() -> None: screen_height = 50 map_width = 80 - map_height = 45 + map_height = 43 room_max_size = 10 room_min_size = 6 @@ -43,6 +44,11 @@ def main() -> None: ) engine.update_fov() + engine.message_log.add_message( + "Hello and welcome, adventurer, to yet another dungeon!", + color.welcome_text + ) + with tcod.context.new_terminal( screen_width, screen_height, diff --git a/message_log.py b/message_log.py new file mode 100644 index 0000000..d9aff5d --- /dev/null +++ b/message_log.py @@ -0,0 +1,79 @@ +from typing import List, Reversible, Tuple +import textwrap + +import tcod + +import color + + +class Message: + def __init__(self, text: str, fg: Tuple[int, int, int]): + self.plain_text = text + self.fg = fg + self.count = 1 + + @property + def full_text(self) -> str: + """The full text of this message, including the count if necessary.""" + if self.count > 1: + return f"{self.plain_text} (x{self.count}" + + return self.plain_text + + +class MessageLog: + def __init__(self) -> None: + self.messages: List[Message] = [] + + def add_message( + self, + text: str, + fg: Tuple[int, int, int] = color.white, + *, + stack: bool = True + ) -> None: + """Add a message to this log. + `text` is the message text, `fg` is the text color. + If `stack is True then the message can stack with a previous message + of the same text. + """ + if stack and self.messages and text == self.messages[-1].plain_text: + self.messages[-1].count += 1 + else: + self.messages.append(Message(text, fg)) + + def render( + self, + console: tcod.Console, + x: int, + y: int, + width: int, + height: int, + ) -> None: + """Render this log over the given area. + `x`, `y`, `width`, `height` is the rectangular region to render onto + the `console`. + """ + self.render_messages(console, x, y, width, height, self.messages) + + @staticmethod + def render_messages( + console: tcod.Console, + x: int, + y: int, + width: int, + height: int, + messages: Reversible[Message], + ) -> None: + """Render the messages provided. + The `messages` are rendered starting at the last message and working + backwards. + """ + y_offset = height - 1 + + for message in reversed(messages): + for line in reversed(textwrap.wrap(message.full_text, width)): + console.print(x=x, y=y + y_offset, string=line, fg=message.fg) + y_offset -= 1 + if y_offset < 0: + return # No more space to print messages