Skip to content

Commit fb2fa57

Browse files
committed
Part 11: Delving into the Dungeon
1 parent e133967 commit fb2fa57

File tree

14 files changed

+282
-28
lines changed

14 files changed

+282
-28
lines changed

game/actions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,15 @@ def perform(self) -> None:
172172
# Type check to ensure entity is an Actor with inventory
173173
assert isinstance(self.entity, game.entity.Actor), "Entity must be an Actor for inventory access"
174174
self.entity.inventory.drop(self.item)
175+
176+
177+
class TakeStairsAction(Action):
178+
def perform(self) -> None:
179+
"""
180+
Take the stairs, if any exist at the entity's location.
181+
"""
182+
if (self.entity.x, self.entity.y) == self.engine.game_map.downstairs_location:
183+
self.engine.game_world.generate_floor()
184+
self.engine.message_log.add_message("You descend the staircase.", game.color.descend)
185+
else:
186+
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/ai.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ def perform(self) -> None:
7070
if self.path:
7171
dest_x, dest_y = self.path.pop(0)
7272
return game.actions.MovementAction(
73-
self.entity, dest_x - self.entity.x, dest_y - self.entity.y,
73+
self.entity,
74+
dest_x - self.entity.x,
75+
dest_y - self.entity.y,
7476
).perform()
7577

7678

@@ -80,9 +82,7 @@ class ConfusedEnemy(BaseAI):
8082
If an actor occupies a tile it is randomly moving into, it will attack.
8183
"""
8284

83-
def __init__(
84-
self, entity: game.entity.Actor, previous_ai: Optional[BaseAI], turns_remaining: int
85-
):
85+
def __init__(self, entity: game.entity.Actor, previous_ai: Optional[BaseAI], turns_remaining: int):
8686
super().__init__(entity)
8787

8888
self.previous_ai = previous_ai
@@ -91,9 +91,7 @@ def __init__(
9191
def perform(self) -> None:
9292
# Revert the AI back to the original state if the effect has run its course.
9393
if self.turns_remaining <= 0:
94-
self.engine.message_log.add_message(
95-
f"The {self.entity.name} is no longer confused."
96-
)
94+
self.engine.message_log.add_message(f"The {self.entity.name} is no longer confused.")
9795
self.entity.ai = self.previous_ai
9896
else:
9997
# Pick a random direction
@@ -114,4 +112,8 @@ def perform(self) -> None:
114112

115113
# The actor will either try to move or attack in the chosen random direction.
116114
# It's possible the actor will just bump into the wall, wasting a turn.
117-
return game.actions.BumpAction(self.entity, direction_x, direction_y,).perform()
115+
return game.actions.BumpAction(
116+
self.entity,
117+
direction_x,
118+
direction_y,
119+
).perform()

game/components/consumable.py

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

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

55
import game.actions
66
import game.color
@@ -16,7 +16,7 @@
1616
class Consumable(game.components.base_component.BaseComponent):
1717
parent: game.entity.Item
1818

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

@@ -88,9 +88,7 @@ def __init__(self, number_of_turns: int):
8888
self.number_of_turns = number_of_turns
8989

9090
def get_action(self, consumer: game.entity.Actor) -> Optional[game.input_handlers.ActionOrHandler]:
91-
self.engine.message_log.add_message(
92-
"Select a target location.", game.color.needs_target
93-
)
91+
self.engine.message_log.add_message("Select a target location.", game.color.needs_target)
9492
return game.input_handlers.SingleRangedAttackHandler(
9593
self.engine,
9694
callback=lambda xy: game.actions.ItemAction(consumer, self.parent, xy),
@@ -112,7 +110,9 @@ def activate(self, action: game.actions.ItemAction) -> None:
112110
game.color.status_effect_applied,
113111
)
114112
target.ai = game.components.ai.ConfusedEnemy(
115-
entity=target, previous_ai=target.ai, turns_remaining=self.number_of_turns,
113+
entity=target,
114+
previous_ai=target.ai,
115+
turns_remaining=self.number_of_turns,
116116
)
117117
self.consume()
118118

@@ -123,9 +123,7 @@ def __init__(self, damage: int, radius: int):
123123
self.radius = radius
124124

125125
def get_action(self, consumer: game.entity.Actor) -> Optional[game.input_handlers.ActionOrHandler]:
126-
self.engine.message_log.add_message(
127-
"Select a target location.", game.color.needs_target
128-
)
126+
self.engine.message_log.add_message("Select a target location.", game.color.needs_target)
129127
return game.input_handlers.AreaRangedAttackHandler(
130128
self.engine,
131129
radius=self.radius,

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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

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

2122
def __init__(self, player: game.entity.Actor):
2223
self.player = player
@@ -52,6 +53,18 @@ def render(self, console: tcod.console.Console) -> None:
5253
total_width=20,
5354
)
5455

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+
5568
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:

game/entity.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
import game.components.consumable
1010
import game.components.fighter
1111
import game.components.inventory
12-
13-
import game.game_map
12+
import game.components.level
13+
import game.game_map
1414

1515

1616
class Entity:
@@ -85,6 +85,7 @@ def __init__(
8585
ai_cls: Type[game.components.ai.BaseAI],
8686
fighter: game.components.fighter.Fighter,
8787
inventory: game.components.inventory.Inventory,
88+
level: game.components.level.Level,
8889
):
8990
super().__init__(
9091
parent=None,
@@ -104,6 +105,9 @@ def __init__(
104105
self.inventory = inventory
105106
self.inventory.parent = self
106107

108+
self.level = level
109+
self.level.parent = self
110+
107111
self.render_order = game.render_order.RenderOrder.ACTOR
108112

109113
@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(

game/game_map.py

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

3-
from typing import Iterator, Optional, Set, TYPE_CHECKING
3+
from typing import Iterable, Iterator, Optional, Set, TYPE_CHECKING
44

55
import numpy as np
66
import tcod
@@ -13,15 +13,19 @@
1313

1414

1515
class GameMap:
16-
def __init__(self, engine: game.engine.Engine, width: int, height: int):
16+
def __init__(
17+
self, engine: game.engine.Engine, width: int, height: int, entities: Iterable[game.entity.Entity] = ()
18+
):
1719
self.engine = engine
1820
self.width, self.height = width, height
19-
self.entities: Set[game.entity.Entity] = set()
21+
self.entities: Set[game.entity.Entity] = set(entities)
2022
self.tiles = np.full((width, height), fill_value=game.tiles.wall, order="F")
2123

2224
self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see
2325
self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before
2426

27+
self.downstairs_location = (0, 0)
28+
2529
@property
2630
def gamemap(self) -> GameMap:
2731
"""Part 8 refactoring prep: self reference for parent system"""
@@ -82,3 +86,62 @@ def render(self, console: tcod.console.Console) -> None:
8286
# Only print entities that are in the FOV
8387
if self.visible[entity.x, entity.y]:
8488
console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)
89+
90+
# Show stairs
91+
if self.visible[self.downstairs_location]:
92+
console.print(
93+
x=self.downstairs_location[0],
94+
y=self.downstairs_location[1],
95+
string=">",
96+
fg=(255, 255, 255),
97+
)
98+
99+
100+
class GameWorld:
101+
"""
102+
Holds the settings for the GameMap, and generates new maps when moving down the stairs.
103+
"""
104+
105+
def __init__(
106+
self,
107+
*,
108+
engine: game.engine.Engine,
109+
map_width: int,
110+
map_height: int,
111+
max_rooms: int,
112+
room_min_size: int,
113+
room_max_size: int,
114+
max_monsters_per_room: int,
115+
max_items_per_room: int,
116+
current_floor: int = 0,
117+
):
118+
self.engine = engine
119+
120+
self.map_width = map_width
121+
self.map_height = map_height
122+
123+
self.max_rooms = max_rooms
124+
125+
self.room_min_size = room_min_size
126+
self.room_max_size = room_max_size
127+
128+
self.max_monsters_per_room = max_monsters_per_room
129+
self.max_items_per_room = max_items_per_room
130+
131+
self.current_floor = current_floor
132+
133+
def generate_floor(self) -> None:
134+
from game.procgen import generate_dungeon
135+
136+
self.current_floor += 1
137+
138+
self.engine.game_map = generate_dungeon(
139+
max_rooms=self.max_rooms,
140+
room_min_size=self.room_min_size,
141+
room_max_size=self.room_max_size,
142+
map_width=self.map_width,
143+
map_height=self.map_height,
144+
max_monsters_per_room=self.max_monsters_per_room,
145+
max_items_per_room=self.max_items_per_room,
146+
engine=self.engine,
147+
)

0 commit comments

Comments
 (0)