Skip to content

Commit beba38e

Browse files
committed
Part 11: Delving into the Dungeon
1 parent dfafddc commit beba38e

File tree

13 files changed

+269
-13
lines changed

13 files changed

+269
-13
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/consumable.py

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

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

55
import game.actions
66
import game.color
@@ -16,7 +16,9 @@
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(
20+
self, consumer: game.entity.Actor
21+
) -> Optional[Union[game.actions.Action, game.input_handlers.BaseEventHandler]]:
2022
"""Try to return the action for this item."""
2123
return game.actions.ItemAction(consumer, self.parent)
2224

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 TYPE_CHECKING, Iterator, Optional, Set
3+
from typing import TYPE_CHECKING, Iterable, Iterator, Optional, Set
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+
)

game/input_handlers.py

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ def handle_events(self, event: tcod.event.Event) -> BaseEventHandler:
9191
if not self.engine.player.is_alive:
9292
# The player was killed sometime during or after the action.
9393
return GameOverEventHandler(self.engine)
94+
elif self.engine.player.level.requires_level_up:
95+
return LevelUpEventHandler(self.engine)
9496
return MainGameEventHandler(self.engine) # Return to the main handler.
9597
return self
9698

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

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

0 commit comments

Comments
 (0)