Skip to content

Commit 275812f

Browse files
committed
Part 11: Delving into the Dungeon
1 parent 5d8c7ed commit 275812f

File tree

13 files changed

+351
-92
lines changed

13 files changed

+351
-92
lines changed

game/actions.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from typing import TYPE_CHECKING, Optional, Tuple
44

5-
from game.color import enemy_atk, player_atk
5+
from game.color import descend, enemy_atk, player_atk
66
from game.entity import Actor
77
from game.exceptions import Impossible
88

@@ -173,3 +173,15 @@ def perform(self) -> None:
173173
# Type check to ensure entity is an Actor with inventory
174174
assert isinstance(self.entity, Actor), "Entity must be an Actor for inventory access"
175175
self.entity.inventory.drop(self.item)
176+
177+
178+
class TakeStairsAction(Action):
179+
def perform(self) -> None:
180+
"""
181+
Take the stairs, if any exist at the entity's location.
182+
"""
183+
if (self.entity.x, self.entity.y) == self.engine.game_map.downstairs_location:
184+
self.engine.game_world.generate_floor()
185+
self.engine.message_log.add_message("You descend the staircase.", descend)
186+
else:
187+
raise game.exceptions.Impossible("There are no stairs here.")

game/color.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
needs_target = (0x3F, 0xFF, 0xFF)
1616
status_effect_applied = (0x3F, 0xFF, 0x3F)
1717

18+
descend = (0x9F, 0x3F, 0xFF)
19+
1820
bar_text = white
1921
bar_filled = (0x0, 0x60, 0x0)
2022
bar_empty = (0x40, 0x10, 0x10)

game/components/consumable.py

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Optional
3+
from typing import TYPE_CHECKING, Optional, Union
44

5-
from game.actions import Action, ItemAction
6-
from game.color import health_recovered
7-
from game.components.ai import ConfusedEnemy
8-
from game.components.base_component import BaseComponent
9-
from game.components.inventory import Inventory
10-
from game.entity import Actor, Item
11-
from game.exceptions import Impossible
12-
from game.input_handlers import ActionOrHandler, AreaRangedAttackHandler, SingleRangedAttackHandler
5+
import game.actions
6+
import game.color
7+
import game.components.ai
8+
import game.components.base_component
9+
import game.exceptions
10+
import game.input_handlers
1311

1412
if TYPE_CHECKING:
15-
import game.actions
13+
import game.entity
1614

1715

18-
class Consumable(BaseComponent):
19-
parent: Item
16+
class Consumable(game.components.base_component.BaseComponent):
17+
parent: game.entity.Item
2018

21-
def get_action(self, consumer: Actor) -> Optional[Action]:
19+
def get_action(
20+
self, consumer: game.entity.Actor
21+
) -> Optional[Union[game.actions.Action, game.input_handlers.BaseEventHandler]]:
2222
"""Try to return the action for this item."""
23-
return ItemAction(consumer, self.parent)
23+
return game.actions.ItemAction(consumer, self.parent)
2424

25-
def activate(self, action: ItemAction) -> None:
25+
def activate(self, action: game.actions.ItemAction) -> None:
2626
"""Invoke this items ability.
2727
2828
`action` is the context for this activation.
@@ -33,28 +33,28 @@ def consume(self) -> None:
3333
"""Remove the consumed item from its containing inventory."""
3434
entity = self.parent
3535
inventory = entity.parent
36-
if isinstance(inventory, Inventory):
36+
if isinstance(inventory, game.components.inventory.Inventory):
3737
inventory.items.remove(entity)
3838

3939

4040
class HealingConsumable(Consumable):
4141
def __init__(self, amount: int):
4242
self.amount = amount
4343

44-
def activate(self, action: ItemAction) -> None:
44+
def activate(self, action: game.actions.ItemAction) -> None:
4545
# Type check to ensure consumer is an Actor
46-
assert isinstance(action.entity, Actor), "Consumer must be an Actor"
46+
assert isinstance(action.entity, game.entity.Actor), "Consumer must be an Actor"
4747
consumer = action.entity
4848
amount_recovered = consumer.fighter.heal(self.amount)
4949

5050
if amount_recovered > 0:
5151
self.engine.message_log.add_message(
5252
f"You consume the {self.parent.name}, and recover {amount_recovered} HP!",
53-
health_recovered,
53+
game.color.health_recovered,
5454
)
5555
self.consume()
5656
else:
57-
raise Impossible("Your health is already full.")
57+
raise game.exceptions.Impossible("Your health is already full.")
5858

5959

6060
class LightningDamageConsumable(Consumable):
@@ -89,9 +89,9 @@ class ConfusionConsumable(Consumable):
8989
def __init__(self, number_of_turns: int):
9090
self.number_of_turns = number_of_turns
9191

92-
def get_action(self, consumer: Actor) -> Optional[ActionOrHandler]:
92+
def get_action(self, consumer: game.entity.Actor) -> Optional[game.input_handlers.ActionOrHandler]:
9393
self.engine.message_log.add_message("Select a target location.", game.color.needs_target)
94-
return SingleRangedAttackHandler(
94+
return game.input_handlers.SingleRangedAttackHandler(
9595
self.engine,
9696
callback=lambda xy: game.actions.ItemAction(consumer, self.parent, xy),
9797
)
@@ -111,7 +111,7 @@ def activate(self, action: game.actions.ItemAction) -> None:
111111
f"The eyes of the {target.name} look vacant, as it starts to stumble around!",
112112
game.color.status_effect_applied,
113113
)
114-
target.ai = ConfusedEnemy(
114+
target.ai = game.components.ai.ConfusedEnemy(
115115
entity=target,
116116
previous_ai=target.ai,
117117
turns_remaining=self.number_of_turns,
@@ -124,9 +124,9 @@ def __init__(self, damage: int, radius: int):
124124
self.damage = damage
125125
self.radius = radius
126126

127-
def get_action(self, consumer: Actor) -> Optional[ActionOrHandler]:
127+
def get_action(self, consumer: game.entity.Actor) -> Optional[game.input_handlers.ActionOrHandler]:
128128
self.engine.message_log.add_message("Select a target location.", game.color.needs_target)
129-
return AreaRangedAttackHandler(
129+
return game.input_handlers.AreaRangedAttackHandler(
130130
self.engine,
131131
radius=self.radius,
132132
callback=lambda xy: game.actions.ItemAction(consumer, self.parent, xy),

game/components/fighter.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ def die(self) -> None:
4949

5050
self.engine.message_log.add_message(death_message, death_message_color)
5151

52+
self.engine.player.level.add_xp(self.parent.level.xp_given)
53+
5254
def heal(self, amount: int) -> int:
5355
if self.hp == self.max_hp:
5456
return 0

game/components/level.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from __future__ import annotations
2+
3+
from game.components.base_component import BaseComponent
4+
import game.entity
5+
6+
7+
class Level(BaseComponent):
8+
parent: game.entity.Actor
9+
10+
def __init__(
11+
self,
12+
current_level: int = 1,
13+
current_xp: int = 0,
14+
level_up_base: int = 0,
15+
level_up_factor: int = 150,
16+
xp_given: int = 0,
17+
):
18+
self.current_level = current_level
19+
self.current_xp = current_xp
20+
self.level_up_base = level_up_base
21+
self.level_up_factor = level_up_factor
22+
self.xp_given = xp_given
23+
24+
@property
25+
def experience_to_next_level(self) -> int:
26+
return self.level_up_base + self.current_level * self.level_up_factor
27+
28+
@property
29+
def requires_level_up(self) -> bool:
30+
return self.current_xp > self.experience_to_next_level
31+
32+
def add_xp(self, xp: int) -> None:
33+
if xp == 0 or self.level_up_base == 0:
34+
return
35+
36+
self.current_xp += xp
37+
38+
self.engine.message_log.add_message(f"You gain {xp} experience points.")
39+
40+
if self.requires_level_up:
41+
self.engine.message_log.add_message(f"You advance to level {self.current_level + 1}!")
42+
43+
def increase_level(self) -> None:
44+
self.current_xp -= self.experience_to_next_level
45+
46+
self.current_level += 1
47+
48+
def increase_max_hp(self, amount: int = 20) -> None:
49+
self.parent.fighter.max_hp += amount
50+
self.parent.fighter.hp += amount
51+
52+
self.engine.message_log.add_message("Your health improves!")
53+
54+
self.increase_level()
55+
56+
def increase_power(self, amount: int = 1) -> None:
57+
self.parent.fighter.power += amount
58+
59+
self.engine.message_log.add_message("You feel stronger!")
60+
61+
self.increase_level()
62+
63+
def increase_defense(self, amount: int = 1) -> None:
64+
self.parent.fighter.defense += amount
65+
66+
self.engine.message_log.add_message("Your movements are getting swifter!")
67+
68+
self.increase_level()

game/engine.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,23 @@
66

77
import tcod
88

9-
from game.color import welcome_text
10-
from game.entity import Actor
11-
from game.message_log import MessageLog
12-
from game.render_functions import render_bar, render_names_at_mouse_location
9+
import game.color
10+
import game.entity
11+
import game.message_log
12+
import game.render_functions
1313

1414
if TYPE_CHECKING:
1515
import game.game_map
1616

1717

1818
class Engine:
1919
game_map: game.game_map.GameMap
20+
game_world: game.game_map.GameWorld
2021

21-
def __init__(self, player: Actor):
22+
def __init__(self, player: game.entity.Actor):
2223
self.player = player
2324
self.mouse_location = (0, 0)
24-
self.message_log = MessageLog()
25+
self.message_log = game.message_log.MessageLog()
2526

2627
def update_fov(self) -> None:
2728
"""Recompute the visible area based on the players point of view."""
@@ -45,14 +46,26 @@ def render(self, console: tcod.console.Console) -> None:
4546

4647
self.message_log.render(console=console, x=21, y=45, width=40, height=5)
4748

48-
render_bar(
49+
game.render_functions.render_bar(
4950
console=console,
5051
current_value=self.player.fighter.hp,
5152
maximum_value=self.player.fighter.max_hp,
5253
total_width=20,
5354
)
5455

55-
render_names_at_mouse_location(console=console, x=21, y=44, engine=self)
56+
game.render_functions.render_dungeon_level(
57+
console=console,
58+
dungeon_level=self.game_world.current_floor,
59+
location=(0, 47),
60+
)
61+
62+
console.print(
63+
x=0,
64+
y=46,
65+
string=f"LVL: {self.player.level.current_level}",
66+
)
67+
68+
game.render_functions.render_names_at_mouse_location(console=console, x=21, y=44, engine=self)
5669

5770
def save_as(self, filename: str) -> None:
5871
"""Save this Engine instance as a compressed file."""

game/entity.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@
22

33
from typing import TYPE_CHECKING, Optional, Tuple, Type, Union
44

5+
from game.game_map import GameMap
56
from game.render_order import RenderOrder
67

78
if TYPE_CHECKING:
89
import game.components.ai
910
import game.components.consumable
1011
import game.components.fighter
1112
import game.components.inventory
12-
13-
from game.game_map import GameMap
13+
import game.components.level
14+
import game.game_map
1415

1516

1617
class Entity:
@@ -85,6 +86,7 @@ def __init__(
8586
ai_cls: Type[game.components.ai.BaseAI],
8687
fighter: game.components.fighter.Fighter,
8788
inventory: game.components.inventory.Inventory,
89+
level: game.components.level.Level,
8890
):
8991
super().__init__(
9092
parent=None,
@@ -104,6 +106,9 @@ def __init__(
104106
self.inventory = inventory
105107
self.inventory.parent = self
106108

109+
self.level = level
110+
self.level.parent = self
111+
107112
self.render_order = RenderOrder.ACTOR
108113

109114
@property

game/entity_factories.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
)
88
from game.components.fighter import Fighter
99
from game.components.inventory import Inventory
10+
from game.components.level import Level
1011
from game.entity import Actor, Item
1112

1213
player = Actor(
@@ -16,6 +17,7 @@
1617
ai_cls=HostileEnemy,
1718
fighter=Fighter(hp=30, defense=2, power=5),
1819
inventory=Inventory(capacity=26),
20+
level=Level(level_up_base=200),
1921
)
2022

2123
orc = Actor(
@@ -25,6 +27,7 @@
2527
ai_cls=HostileEnemy,
2628
fighter=Fighter(hp=10, defense=0, power=3),
2729
inventory=Inventory(capacity=0),
30+
level=Level(xp_given=35),
2831
)
2932

3033
troll = Actor(
@@ -34,6 +37,7 @@
3437
ai_cls=HostileEnemy,
3538
fighter=Fighter(hp=16, defense=1, power=4),
3639
inventory=Inventory(capacity=0),
40+
level=Level(xp_given=100),
3741
)
3842

3943
health_potion = Item(

0 commit comments

Comments
 (0)