Skip to content

Commit e5d5385

Browse files
committed
Part 11: Delving into the Dungeon
1 parent a22d942 commit e5d5385

File tree

12 files changed

+273
-12
lines changed

12 files changed

+273
-12
lines changed

game/actions.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,18 @@ def perform(self) -> None:
164164

165165
class DropItem(ItemAction):
166166
def perform(self) -> None:
167-
self.entity.inventory.drop(self.item)
167+
self.entity.inventory.drop(self.item)
168+
169+
170+
class TakeStairsAction(Action):
171+
def perform(self) -> None:
172+
"""
173+
Take the stairs, if any exist at the entity's location.
174+
"""
175+
if (self.entity.x, self.entity.y) == self.engine.game_map.downstairs_location:
176+
self.engine.game_world.generate_floor()
177+
self.engine.message_log.add_message(
178+
"You descend the staircase.", game.color.descend
179+
)
180+
else:
181+
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/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: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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(
42+
f"You advance to level {self.current_level + 1}!"
43+
)
44+
45+
def increase_level(self) -> None:
46+
self.current_xp -= self.experience_to_next_level
47+
48+
self.current_level += 1
49+
50+
def increase_max_hp(self, amount: int = 20) -> None:
51+
self.parent.fighter.max_hp += amount
52+
self.parent.fighter.hp += amount
53+
54+
self.engine.message_log.add_message("Your health improves!")
55+
56+
self.increase_level()
57+
58+
def increase_power(self, amount: int = 1) -> None:
59+
self.parent.fighter.power += amount
60+
61+
self.engine.message_log.add_message("You feel stronger!")
62+
63+
self.increase_level()
64+
65+
def increase_defense(self, amount: int = 1) -> None:
66+
self.parent.fighter.defense += amount
67+
68+
self.engine.message_log.add_message("Your movements are getting swifter!")
69+
70+
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.Entity):
2223
self.player = player
@@ -51,6 +52,18 @@ def render(self, console: tcod.console.Console) -> None:
5152
total_width=20,
5253
)
5354

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

5669
def save_as(self, filename: str) -> None:

game/entity.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import game.components.consumable
1010
import game.components.fighter
1111
import game.components.inventory
12+
import game.components.level
1213
import game.game_map
1314

1415

@@ -79,6 +80,7 @@ def __init__(
7980
ai_cls: Type[game.components.ai.BaseAI],
8081
fighter: game.components.fighter.Fighter,
8182
inventory: game.components.inventory.Inventory,
83+
level: game.components.level.Level,
8284
):
8385
super().__init__(
8486
parent=None,
@@ -98,6 +100,9 @@ def __init__(
98100
self.inventory = inventory
99101
self.inventory.parent = self
100102

103+
self.level = level
104+
self.level.parent = self
105+
101106
self.render_order = game.render_order.RenderOrder.ACTOR
102107

103108
@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: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

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

55
import numpy as np
66
import tcod
@@ -13,14 +13,18 @@
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
26+
27+
self.downstairs_location = (0, 0)
2428

2529
@property
2630
def gamemap(self) -> GameMap:
@@ -91,4 +95,63 @@ def render(self, console: tcod.console.Console) -> None:
9195
for entity in entities_sorted_for_rendering:
9296
# Only print entities that are in the FOV
9397
if self.visible[entity.x, entity.y]:
94-
console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)
98+
console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)
99+
100+
# Show stairs
101+
if self.visible[self.downstairs_location]:
102+
console.print(
103+
x=self.downstairs_location[0],
104+
y=self.downstairs_location[1],
105+
string=">",
106+
fg=(255, 255, 255),
107+
)
108+
109+
110+
class GameWorld:
111+
"""
112+
Holds the settings for the GameMap, and generates new maps when moving down the stairs.
113+
"""
114+
115+
def __init__(
116+
self,
117+
*,
118+
engine: game.engine.Engine,
119+
map_width: int,
120+
map_height: int,
121+
max_rooms: int,
122+
room_min_size: int,
123+
room_max_size: int,
124+
max_monsters_per_room: int,
125+
max_items_per_room: int,
126+
current_floor: int = 0,
127+
):
128+
self.engine = engine
129+
130+
self.map_width = map_width
131+
self.map_height = map_height
132+
133+
self.max_rooms = max_rooms
134+
135+
self.room_min_size = room_min_size
136+
self.room_max_size = room_max_size
137+
138+
self.max_monsters_per_room = max_monsters_per_room
139+
self.max_items_per_room = max_items_per_room
140+
141+
self.current_floor = current_floor
142+
143+
def generate_floor(self) -> None:
144+
from game.procgen import generate_dungeon
145+
146+
self.current_floor += 1
147+
148+
self.engine.game_map = generate_dungeon(
149+
max_rooms=self.max_rooms,
150+
room_min_size=self.room_min_size,
151+
room_max_size=self.room_max_size,
152+
map_width=self.map_width,
153+
map_height=self.map_height,
154+
max_monsters_per_room=self.max_monsters_per_room,
155+
max_items_per_room=self.max_items_per_room,
156+
engine=self.engine,
157+
)

game/input_handlers.py

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ def handle_events(self, event: tcod.event.Event) -> BaseEventHandler:
8989
if not self.engine.player.is_alive:
9090
# The player was killed sometime during or after the action.
9191
return GameOverEventHandler(self.engine)
92+
elif self.engine.player.level.requires_level_up:
93+
return LevelUpEventHandler(self.engine)
9294
return MainGameEventHandler(self.engine) # Return to the main handler.
9395
return self
9496

@@ -142,11 +144,13 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
142144
return InventoryDropHandler(self.engine)
143145
elif key == tcod.event.KeySym.SLASH:
144146
return LookHandler(self.engine)
145-
elif key == tcod.event.KeySym.PERIOD and modifiers & (
146-
tcod.event.KMOD_LSHIFT | tcod.event.KMOD_RSHIFT
147-
):
148-
# Wait if user presses '>' (shift + period)
149-
action = game.actions.WaitAction(player)
147+
elif key == tcod.event.KeySym.PERIOD:
148+
if modifiers & (tcod.event.KMOD_LSHIFT | tcod.event.KMOD_RSHIFT):
149+
# Take stairs down if user presses '>'
150+
return game.actions.TakeStairsAction(player)
151+
else:
152+
# Wait if user presses '.'
153+
action = game.actions.WaitAction(player)
150154

151155
# No valid key was pressed
152156
return action
@@ -454,3 +458,72 @@ def on_render(self, console: tcod.console.Console) -> None:
454458
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseEventHandler]:
455459
"""Any key returns to the parent handler."""
456460
return self.parent
461+
462+
463+
class LevelUpEventHandler(AskUserEventHandler):
464+
TITLE = "Level Up"
465+
466+
def on_render(self, console: tcod.console.Console) -> None:
467+
super().on_render(console)
468+
469+
if self.engine.player.x <= 30:
470+
x = 40
471+
else:
472+
x = 0
473+
474+
console.draw_frame(
475+
x=x,
476+
y=0,
477+
width=35,
478+
height=8,
479+
title=self.TITLE,
480+
clear=True,
481+
fg=(255, 255, 255),
482+
bg=(0, 0, 0),
483+
)
484+
485+
console.print(x=x + 1, y=1, string="Congratulations! You level up!")
486+
console.print(x=x + 1, y=2, string="Select an attribute to increase.")
487+
488+
console.print(
489+
x=x + 1,
490+
y=4,
491+
string=f"a) Constitution (+20 HP, from {self.engine.player.fighter.max_hp})",
492+
)
493+
console.print(
494+
x=x + 1,
495+
y=5,
496+
string=f"b) Strength (+1 attack, from {self.engine.player.fighter.power})",
497+
)
498+
console.print(
499+
x=x + 1,
500+
y=6,
501+
string=f"c) Agility (+1 defense, from {self.engine.player.fighter.defense})",
502+
)
503+
504+
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
505+
player = self.engine.player
506+
key = event.sym
507+
index = key - tcod.event.KeySym.A
508+
509+
if 0 <= index <= 2:
510+
if index == 0:
511+
player.level.increase_max_hp()
512+
elif index == 1:
513+
player.level.increase_power()
514+
else:
515+
player.level.increase_defense()
516+
else:
517+
self.engine.message_log.add_message("Invalid entry.", game.color.invalid)
518+
519+
return None
520+
521+
return super().ev_keydown(event)
522+
523+
def ev_mousebuttondown(
524+
self, event: tcod.event.MouseButtonDown
525+
) -> Optional[ActionOrHandler]:
526+
"""
527+
Don't allow the player to click to exit the menu, like normal.
528+
"""
529+
return None

0 commit comments

Comments
 (0)