diff --git a/components/fighter.py b/components/fighter.py index 79713ae..6705d0a 100644 --- a/components/fighter.py +++ b/components/fighter.py @@ -46,6 +46,8 @@ class Fighter(BaseComponent): self.engine.message_log.add_message(death_message, death_message_color) + self.engine.player.level.add_xp(self.parent.level.xp_given) + def heal(self, amount: int) -> int: if self.hp == self.max_hp: return 0 diff --git a/components/level.py b/components/level.py new file mode 100644 index 0000000..b871a2d --- /dev/null +++ b/components/level.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from components.base_component import BaseComponent + +if TYPE_CHECKING: + from entity import Actor + + +class Level(BaseComponent): + parent: Actor + + def __init__( + self, + current_level: int = 1, + current_xp: int = 0, + level_up_base: int = 0, + level_up_factor: int = 150, + xp_given: int = 0 + ): + self.current_level = current_level + self.current_xp = current_xp + self.level_up_base = level_up_base + self.level_up_factor = level_up_factor + self.xp_given = xp_given + + @property + def experience_to_next_level(self) -> int: + return self.level_up_base + self.current_level * self.level_up_factor + + @property + def requires_level_up(self) -> bool: + return self.current_xp > self.experience_to_next_level + + def add_xp(self, xp: int) -> None: + if xp == 0 or self.level_up_base == 0: + return + + self.current_xp += xp + + self.engine.message_log.add_message(f"You gain {xp} experience points.") + + if self.requires_level_up: + self.engine.message_log.add_message( + f"You advance to level {self.current_level + 1}!" + ) + + def increase_level(self) -> None: + self.current_xp -= self.experience_to_next_level + + self.current_level += 1 + + def increase_max_hp(self, amount: int = 20) -> None: + self.parent.fighter.max_hp += amount + self.parent.fighter.hp += amount + + self.engine.message_log.add_message("Your health improves!") + + self.increase_level() + + def increase_power(self, amount: int = 1) -> None: + self.parent.fighter.power += amount + + self.engine.message_log.add_message("You feel stronger!") + + self.increase_level() + + def increase_defense(self, amount: int = 1) -> None: + self.parent.fighter.defense += amount + + self.engine.message_log.add_message("Your movements are getting swifter!") + + self.increase_level() diff --git a/entity.py b/entity.py index 7122150..90b3690 100644 --- a/entity.py +++ b/entity.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from components.consumable import Consumable from components.fighter import Fighter from components.inventory import Inventory + from components.level import Level from game_map import GameMap T = TypeVar("T", bound="Entity") @@ -95,6 +96,7 @@ class Actor(Entity): ai_cls: Type[BaseAI], fighter: Fighter, inventory: Inventory, + level: Level ): super().__init__( x=x, @@ -114,6 +116,9 @@ class Actor(Entity): self.inventory = inventory self.inventory.parent = self + self.level = level + self.level.parent = self + @property def is_alive(self) -> bool: """Returns True as long as this actor can perform actions.""" diff --git a/entity_factories.py b/entity_factories.py index 8d46696..b90d757 100644 --- a/entity_factories.py +++ b/entity_factories.py @@ -2,6 +2,7 @@ from components.ai import HostileEnemy from components import consumable from components.fighter import Fighter from components.inventory import Inventory +from components.level import Level from entity import Actor, Item player = Actor( @@ -11,6 +12,7 @@ player = Actor( ai_cls=HostileEnemy, fighter=Fighter(hp=30, defense=2, power=5), inventory=Inventory(capacity=26), + level=Level(level_up_base=200), ) orc = Actor( @@ -20,6 +22,7 @@ orc = Actor( ai_cls=HostileEnemy, fighter=Fighter(hp=10, defense=0, power=3), inventory=Inventory(capacity=0), + level=Level(xp_given=35), ) troll = Actor( char="T", @@ -28,6 +31,7 @@ troll = Actor( ai_cls=HostileEnemy, fighter=Fighter(hp=16, defense=1, power=4), inventory=Inventory(capacity=0), + level=Level(xp_given=100), ) confusion_scroll = Item( diff --git a/input_handlers.py b/input_handlers.py index 0067ce4..b23a660 100644 --- a/input_handlers.py +++ b/input_handlers.py @@ -132,6 +132,8 @@ class EventHandler(BaseEventHandler): if not self.engine.player.is_alive: # The player was killed sometime during or after the action. return GameOverEventHandler(self.engine) + elif self.engine.player.level.requires_level_up: + return LevelUpEventHandler(self.engine) return MainGameEventHandler(self.engine) # Return to the main handler. return self @@ -193,6 +195,73 @@ class AskUserEventHandler(EventHandler): return MainGameEventHandler(self.engine) +class LevelUpEventHandler(AskUserEventHandler): + TITLE = "Level Up" + + def on_render(self, console: tcod.Console) -> None: + super().on_render(console) + + if self.engine.player.x <= 30: + x = 40 + else: + x = 0 + + console.draw_frame( + x=x, + y=0, + width=35, + height=8, + title=self.TITLE, + clear=True, + fg=(255, 255, 255), + bg=(0, 0, 0) + ) + + console.print(x + 1, 1, "Congratulations! You level up!") + console.print(x + 1, 2, "Select an attribut to increase.") + + console.print( + x=x + 1, + y=4, + string=f"a) Constitution (+20 HP, from {self.engine.player.fighter.max_hp})" + ) + console.print( + x=x + 1, + y=5, + string=f"b) Strength (+1 attack, from {self.engine.player.fighter.power})" + ) + console.print( + x=x + 1, + y=6, + string=f"c) Agility (+1 defense, from {self.engine.player.fighter.defense})" + ) + + def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]: + player = self.engine.player + key = event.sym + index = key - tcod.event.K_a + + if 0 <= index <= 2: + if index == 0: + player.level.increase_max_hp() + elif index == 1: + player.level.increase_power() + else: + player.level.increase_defense() + else: + self.engine.message_log.add_message("Invalid entry.", color.invalid) + + return None + + return super().ev_keydown(event) + + def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> Optional[ActionOrHandler]: + """ + Don't allow the player to click to exit the menu, like normal + """ + return None + + class InventoryEventHandler(AskUserEventHandler): """This handler lets the user select an item.