1810 lines
107 KiB
HTML
1810 lines
107 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en" style="color-scheme: dark;"><head>
|
||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||
<title>
|
||
Part 11 - Delving into the Dungeon · Roguelike Tutorials
|
||
</title>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta name="color-scheme" content="light dark">
|
||
|
||
|
||
|
||
|
||
<meta name="description" content="Our game isn’t much of a “dungeon crawler” if there’s only one floor to our dungeon. In this chapter, we’ll allow the player to go down a level, and we’ll put a very basic leveling up system in place, to make the dive all the more rewarding.
|
||
Before diving into the code for this section, let’s add the color we’ll need this chapter, for when the player descends down a level in the dungeon.">
|
||
<meta name="keywords" content="">
|
||
|
||
<meta name="twitter:card" content="summary">
|
||
<meta name="twitter:title" content="Part 11 - Delving into the Dungeon">
|
||
<meta name="twitter:description" content="Our game isn’t much of a “dungeon crawler” if there’s only one floor to our dungeon. In this chapter, we’ll allow the player to go down a level, and we’ll put a very basic leveling up system in place, to make the dive all the more rewarding.
|
||
Before diving into the code for this section, let’s add the color we’ll need this chapter, for when the player descends down a level in the dungeon.">
|
||
|
||
<meta property="og:title" content="Part 11 - Delving into the Dungeon">
|
||
<meta property="og:description" content="Our game isn’t much of a “dungeon crawler” if there’s only one floor to our dungeon. In this chapter, we’ll allow the player to go down a level, and we’ll put a very basic leveling up system in place, to make the dive all the more rewarding.
|
||
Before diving into the code for this section, let’s add the color we’ll need this chapter, for when the player descends down a level in the dungeon.">
|
||
<meta property="og:type" content="article">
|
||
<meta property="og:url" content="https://rogueliketutorials.com/tutorials/tcod/v2/part-11/"><meta property="article:section" content="tutorials">
|
||
<meta property="article:published_time" content="2020-07-21T00:00:00+00:00">
|
||
<meta property="article:modified_time" content="2020-07-21T00:00:00+00:00">
|
||
|
||
|
||
|
||
|
||
<link rel="canonical" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-11/">
|
||
|
||
|
||
<link rel="preload" href="https://rogueliketutorials.com/fonts/forkawesome-webfont.woff2?v=1.2.0" as="font" type="font/woff2" crossorigin="">
|
||
|
||
|
||
|
||
|
||
<link rel="stylesheet" href="Part%2011%20-%20Delving%20into%20the%20Dungeon%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.c4d7e93a158eda5a65b3df343745d2092a0a1e2170feeec909.css" integrity="sha256-xNfpOhWO2lpls980N0XSCSoKHiFw/u7JCbiolEOQPGo=" crossorigin="anonymous" media="screen">
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<link rel="stylesheet" href="Part%2011%20-%20Delving%20into%20the%20Dungeon%20%C2%B7%20Roguelike%20Tutorials_files/coder-dark.min.78b5fe3864945faf5207fb8fe3ab2320d49c3365def0e.css" integrity="sha256-eLX+OGSUX69SB/uP46sjINScM2Xe8OiKwd8N2txUoDw=" crossorigin="anonymous" media="screen">
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<link rel="stylesheet" href="Part%2011%20-%20Delving%20into%20the%20Dungeon%20%C2%B7%20Roguelike%20Tutorials_files/style.min.9d3eb202952dddb888856ff12c83bc88de866c596286bfb4c1.css" integrity="sha256-nT6yApUt3biIhW/xLIO8iN6GbFlihr+0wfjmvq2a42Y=" crossorigin="anonymous" media="screen">
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<link rel="icon" type="image/png" href="https://rogueliketutorials.com/images/favicon-32x32.png" sizes="32x32">
|
||
<link rel="icon" type="image/png" href="https://rogueliketutorials.com/images/favicon-16x16.png" sizes="16x16">
|
||
|
||
<link rel="apple-touch-icon" href="https://rogueliketutorials.com/images/apple-touch-icon.png">
|
||
<link rel="apple-touch-icon" sizes="180x180" href="https://rogueliketutorials.com/images/apple-touch-icon.png">
|
||
|
||
<link rel="manifest" href="https://rogueliketutorials.com/site.webmanifest">
|
||
<link rel="mask-icon" href="https://rogueliketutorials.com/images/safari-pinned-tab.svg" color="#5bbad5">
|
||
|
||
|
||
|
||
|
||
<meta name="generator" content="Hugo 0.110.0">
|
||
|
||
|
||
|
||
|
||
|
||
<style>:is([id*='google_ads_iframe'],[id*='taboola-'],.taboolaHeight,.taboola-placeholder,#top-ad,#credential_picker_container,#credentials-picker-container,#credential_picker_iframe,[id*='google-one-tap-iframe'],#google-one-tap-popup-container,.google-one-tap__module,.google-one-tap-modal-div,#amp_floatingAdDiv,#ez-content-blocker-container) {display:none!important;min-height:0!important;height:0!important;}</style></head>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<body class="colorscheme-dark vsc-initialized">
|
||
|
||
<div class="float-container">
|
||
<a id="dark-mode-toggle" class="colorscheme-toggle">
|
||
<i class="fa fa-adjust fa-fw" aria-hidden="true"></i>
|
||
</a>
|
||
</div>
|
||
|
||
|
||
<main class="wrapper">
|
||
<nav class="navigation">
|
||
<section class="container">
|
||
<a class="navigation-title" href="https://rogueliketutorials.com/">
|
||
Roguelike Tutorials
|
||
</a>
|
||
|
||
<input type="checkbox" id="menu-toggle">
|
||
<label class="menu-button float-right" for="menu-toggle">
|
||
<i class="fa fa-bars fa-fw" aria-hidden="true"></i>
|
||
</label>
|
||
<ul class="navigation-list">
|
||
|
||
|
||
<li class="navigation-item">
|
||
<a class="navigation-link" href="https://rogueliketutorials.com/">Home</a>
|
||
</li>
|
||
|
||
<li class="navigation-item">
|
||
<a class="navigation-link" href="https://rogueliketutorials.com/tutorials/tcod/v2/">TCOD Tutorial (2020)</a>
|
||
</li>
|
||
|
||
<li class="navigation-item">
|
||
<a class="navigation-link" href="https://rogueliketutorials.com/tutorials/tcod/2019/">TCOD Tutorial (2019)</a>
|
||
</li>
|
||
|
||
<li class="navigation-item">
|
||
<a class="navigation-link" href="https://rogueliketutorials.com/about/">About</a>
|
||
</li>
|
||
|
||
|
||
|
||
</ul>
|
||
|
||
</section>
|
||
</nav>
|
||
|
||
|
||
<div class="content">
|
||
|
||
<section class="container page">
|
||
<article>
|
||
<header>
|
||
<h1 class="title">
|
||
<a class="title-link" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-11/">
|
||
Part 11 - Delving into the Dungeon
|
||
</a>
|
||
</h1>
|
||
</header>
|
||
|
||
<p>Our game isn’t much of a “dungeon crawler” if there’s only one
|
||
floor to our dungeon. In this chapter, we’ll allow the player to go down
|
||
a level, and we’ll put a very basic leveling up system in place, to
|
||
make the dive all the more rewarding.</p>
|
||
<p>Before diving into the code for this section, let’s add the color
|
||
we’ll need this chapter, for when the player descends down a level in
|
||
the dungeon. Open up <code>color.py</code> and add this line:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>...
|
||
</span></span><span style="display:flex;"><span>enemy_atk = (0xFF, 0xC0, 0xC0)
|
||
</span></span><span style="display:flex;"><span>needs_target = (0x3F, 0xFF, 0xFF)
|
||
</span></span><span style="display:flex;"><span>status_effect_applied = (0x3F, 0xFF, 0x3F)
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+descend = (0x9F, 0x3F, 0xFF)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>player_die = (0xFF, 0x30, 0x30)
|
||
</span></span><span style="display:flex;"><span>enemy_die = (0xFF, 0xA0, 0x30)
|
||
</span></span><span style="display:flex;"><span>...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>...
|
||
enemy_atk = (0xFF, 0xC0, 0xC0)
|
||
needs_target = (0x3F, 0xFF, 0xFF)
|
||
status_effect_applied = (0x3F, 0xFF, 0x3F)
|
||
<span class="new-text">descend = (0x9F, 0x3F, 0xFF)</span>
|
||
|
||
player_die = (0xFF, 0x30, 0x30)
|
||
enemy_die = (0xFF, 0xA0, 0x30)
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>We will use this color later on, when adding a message to the message log that the player went down one floor.</p>
|
||
<p>We’ll also need a new tile type to represent the downward stairs in the dungeon. Typically, roguelikes represent this with the <code>></code> character, and we’ll do the same. Add the following to <code>tile_types.py</code>:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>...
|
||
</span></span><span style="display:flex;"><span>wall = new_tile(
|
||
</span></span><span style="display:flex;"><span> walkable=False,
|
||
</span></span><span style="display:flex;"><span> transparent=False,
|
||
</span></span><span style="display:flex;"><span> dark=(ord(" "), (255, 255, 255), (0, 0, 100)),
|
||
</span></span><span style="display:flex;"><span> light=(ord(" "), (255, 255, 255), (130, 110, 50)),
|
||
</span></span><span style="display:flex;"><span>)
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+down_stairs = new_tile(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ walkable=True,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ transparent=True,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dark=(ord(">"), (0, 0, 100), (50, 50, 150)),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ light=(ord(">"), (255, 255, 255), (200, 180, 50)),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+)
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>...
|
||
wall = new_tile(
|
||
walkable=False,
|
||
transparent=False,
|
||
dark=(ord(" "), (255, 255, 255), (0, 0, 100)),
|
||
light=(ord(" "), (255, 255, 255), (130, 110, 50)),
|
||
)
|
||
<span class="new-text">down_stairs = new_tile(
|
||
walkable=True,
|
||
transparent=True,
|
||
dark=(ord(">"), (0, 0, 100), (50, 50, 150)),
|
||
light=(ord(">"), (255, 255, 255), (200, 180, 50)),
|
||
)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>To keep track of where our downwards stairs are located on the map, we can add a new variable in out <code>__init__</code> function in the <code>GameMap</code> class. The variable needs some sort of default, so to start, we can set that up to be <code>(0, 0)</code> by default. Add the following line to <code>game_map.py</code>:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class GameMap:
|
||
</span></span><span style="display:flex;"><span> def __init__(
|
||
</span></span><span style="display:flex;"><span> self, engine: Engine, width: int, height: int, entities: Iterable[Entity] = ()
|
||
</span></span><span style="display:flex;"><span> ):
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span> self.explored = np.full(
|
||
</span></span><span style="display:flex;"><span> (width, height), fill_value=False, order="F"
|
||
</span></span><span style="display:flex;"><span> ) # Tiles the player has seen before
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.downstairs_location = (0, 0)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> @property
|
||
</span></span><span style="display:flex;"><span> def gamemap(self) -> GameMap:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class GameMap:
|
||
def __init__(
|
||
self, engine: Engine, width: int, height: int, entities: Iterable[Entity] = ()
|
||
):
|
||
...
|
||
self.explored = np.full(
|
||
(width, height), fill_value=False, order="F"
|
||
) # Tiles the player has seen before
|
||
|
||
<span class="new-text">self.downstairs_location = (0, 0)</span>
|
||
|
||
@property
|
||
def gamemap(self) -> GameMap:
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Of course, <code>(0, 0)</code> won’t be the actual location of the
|
||
stairs. In order to actually place the downwards stairs, we’ll need to
|
||
edit our procedural dungeon generator to place the stairs at the proper
|
||
place. We’ll keep things simple and just place the stairs in the last
|
||
room that our algorithm generates, by keeping track of the center
|
||
coordinates of the last room we created. Modify <code>generate_dungeon</code> function in <code>procgen.py</code>:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span> rooms: List[RectangularRoom] = []
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ center_of_last_room = (0, 0)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> for r in range(max_rooms):
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span> for x, y in tunnel_between(rooms[-1].center, new_room.center):
|
||
</span></span><span style="display:flex;"><span> dungeon.tiles[x, y] = tile_types.floor
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ center_of_last_room = new_room.center
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> place_entities(new_room, dungeon, max_monsters_per_room, max_items_per_room)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dungeon.tiles[center_of_last_room] = tile_types.down_stairs
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dungeon.downstairs_location = center_of_last_room
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> # Finally, append the new room to the list.
|
||
</span></span><span style="display:flex;"><span> rooms.append(new_room)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> return dungeon
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre> ...
|
||
rooms: List[RectangularRoom] = []
|
||
|
||
<span class="new-text">center_of_last_room = (0, 0)</span>
|
||
|
||
for r in range(max_rooms):
|
||
...
|
||
...
|
||
for x, y in tunnel_between(rooms[-1].center, new_room.center):
|
||
dungeon.tiles[x, y] = tile_types.floor
|
||
|
||
<span class="new-text">center_of_last_room = new_room.center</span>
|
||
|
||
place_entities(new_room, dungeon, max_monsters_per_room, max_items_per_room)
|
||
|
||
<span class="new-text">dungeon.tiles[center_of_last_room] = tile_types.down_stairs
|
||
dungeon.downstairs_location = center_of_last_room</span>
|
||
|
||
# Finally, append the new room to the list.
|
||
rooms.append(new_room)
|
||
|
||
return dungeon</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Whichever room is generated last, we take its center and set the <code>downstairs_location</code> equal to those coordinates. We also replace whatever tile type with the <code>down_stairs</code>, so the player can clearly see the location.</p>
|
||
<p>To hold the information about the maps, including the size, the room
|
||
variables (size and maximum number), along with the floor that the
|
||
player is currently on, we can add a class to hold these variables, as
|
||
well as generate new maps when the time comes. Open up <code>game_map.py</code> and add the following class:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class GameMap:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+class GameWorld:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ Holds the settings for the GameMap, and generates new maps when moving down the stairs.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def __init__(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ *,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine: Engine,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ map_width: int,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ map_height: int,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_rooms: int,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room_min_size: int,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room_max_size: int,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_monsters_per_room: int,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_items_per_room: int,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ current_floor: int = 0
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine = engine
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.map_width = map_width
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.map_height = map_height
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.max_rooms = max_rooms
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.room_min_size = room_min_size
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.room_max_size = room_max_size
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.max_monsters_per_room = max_monsters_per_room
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.max_items_per_room = max_items_per_room
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.current_floor = current_floor
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def generate_floor(self) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from procgen import generate_dungeon
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.current_floor += 1
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.game_map = generate_dungeon(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_rooms=self.max_rooms,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room_min_size=self.room_min_size,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room_max_size=self.room_max_size,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ map_width=self.map_width,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ map_height=self.map_height,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_monsters_per_room=self.max_monsters_per_room,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_items_per_room=self.max_items_per_room,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine=self.engine,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class GameMap:
|
||
...
|
||
|
||
|
||
<span class="new-text">class GameWorld:
|
||
"""
|
||
Holds the settings for the GameMap, and generates new maps when moving down the stairs.
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
*,
|
||
engine: Engine,
|
||
map_width: int,
|
||
map_height: int,
|
||
max_rooms: int,
|
||
room_min_size: int,
|
||
room_max_size: int,
|
||
max_monsters_per_room: int,
|
||
max_items_per_room: int,
|
||
current_floor: int = 0
|
||
):
|
||
self.engine = engine
|
||
|
||
self.map_width = map_width
|
||
self.map_height = map_height
|
||
|
||
self.max_rooms = max_rooms
|
||
|
||
self.room_min_size = room_min_size
|
||
self.room_max_size = room_max_size
|
||
|
||
self.max_monsters_per_room = max_monsters_per_room
|
||
self.max_items_per_room = max_items_per_room
|
||
|
||
self.current_floor = current_floor
|
||
|
||
def generate_floor(self) -> None:
|
||
from procgen import generate_dungeon
|
||
|
||
self.current_floor += 1
|
||
|
||
self.engine.game_map = generate_dungeon(
|
||
max_rooms=self.max_rooms,
|
||
room_min_size=self.room_min_size,
|
||
room_max_size=self.room_max_size,
|
||
map_width=self.map_width,
|
||
map_height=self.map_height,
|
||
max_monsters_per_room=self.max_monsters_per_room,
|
||
max_items_per_room=self.max_items_per_room,
|
||
engine=self.engine,
|
||
)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>The <code>generate_floor</code> method will create the new maps each time we go down a floor, using the variables that <code>GameWorld</code>
|
||
stores. In this tutorial, we won’t program in the ability to go back up
|
||
a floor after going down one, but you could perhaps modify <code>GameWorld</code> to hold the previous maps.</p>
|
||
<p>In order to utilize the new <code>GameWorld</code> class, we’ll need to add it to the <code>Engine</code>, like this:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>...
|
||
</span></span><span style="display:flex;"><span>if TYPE_CHECKING:
|
||
</span></span><span style="display:flex;"><span> from entity import Actor
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- from game_map import GameMap
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ from game_map import GameMap, GameWorld
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>class Engine:
|
||
</span></span><span style="display:flex;"><span> game_map: GameMap
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ game_world: GameWorld
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> def __init__(self, player: Actor):
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>...
|
||
if TYPE_CHECKING:
|
||
from entity import Actor
|
||
<span class="crossed-out-text">from game_map import GameMap</span>
|
||
<span class="new-text">from game_map import GameMap, GameWorld</span>
|
||
|
||
|
||
class Engine:
|
||
game_map: GameMap
|
||
<span class="new-text">game_world: GameWorld</span>
|
||
|
||
def __init__(self, player: Actor):
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Pretty simple. To utilize the new <code>game_world</code> class attribute, edit <code>setup_game.py</code> like this:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>import tcod
|
||
</span></span><span style="display:flex;"><span>import color
|
||
</span></span><span style="display:flex;"><span>from engine import Engine
|
||
</span></span><span style="display:flex;"><span>import entity_factories
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from game_map import GameWorld
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>import input_handlers
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from procgen import generate_dungeon
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span> engine = Engine(player=player)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- engine.game_map = generate_dungeon(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ engine.game_world = GameWorld(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine=engine,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> max_rooms=max_rooms,
|
||
</span></span><span style="display:flex;"><span> room_min_size=room_min_size,
|
||
</span></span><span style="display:flex;"><span> room_max_size=room_max_size,
|
||
</span></span><span style="display:flex;"><span> map_width=map_width,
|
||
</span></span><span style="display:flex;"><span> map_height=map_height,
|
||
</span></span><span style="display:flex;"><span> max_monsters_per_room=max_monsters_per_room,
|
||
</span></span><span style="display:flex;"><span> max_items_per_room=max_items_per_room,
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- engine=engine,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> )
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine.game_world.generate_floor()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> engine.update_fov()
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>import tcod
|
||
import color
|
||
from engine import Engine
|
||
import entity_factories
|
||
<span class="new-text">from game_map import GameWorld</span>
|
||
import input_handlers
|
||
<span class="crossed-out-text">from procgen import generate_dungeon</span>
|
||
...
|
||
|
||
...
|
||
engine = Engine(player=player)
|
||
|
||
<span class="crossed-out-text">engine.game_map = generate_dungeon(</span>
|
||
<span class="new-text">engine.game_world = GameWorld(
|
||
engine=engine,</span>
|
||
max_rooms=max_rooms,
|
||
room_min_size=room_min_size,
|
||
room_max_size=room_max_size,
|
||
map_width=map_width,
|
||
map_height=map_height,
|
||
max_monsters_per_room=max_monsters_per_room,
|
||
max_items_per_room=max_items_per_room,
|
||
<span class="crossed-out-text">engine=engine,</span>
|
||
)
|
||
|
||
<span class="new-text">engine.game_world.generate_floor()</span>
|
||
engine.update_fov()
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Now, instead of calling <code>generate_dungeon</code> directly, we create a new <code>GameWorld</code> and allow it to call its <code>generate_floor</code>
|
||
method. While this doesn’t change anything for the first floor that’s
|
||
created, it will allow us to more easily create new floors on the fly.</p>
|
||
<p>In order to actually take the stairs, we’ll need to add an action and
|
||
a way for the player to trigger it. Adding the action is pretty simple.
|
||
Add the following to <code>actions.py</code>:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class WaitAction(Action):
|
||
</span></span><span style="display:flex;"><span> pass
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+class TakeStairsAction(Action):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def perform(self) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ Take the stairs, if any exist at the entity's location.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if (self.entity.x, self.entity.y) == self.engine.game_map.downstairs_location:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.game_world.generate_floor()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.message_log.add_message(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ "You descend the staircase.", color.descend
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ else:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise exceptions.Impossible("There are no stairs here.")
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>class ActionWithDirection(Action):
|
||
</span></span><span style="display:flex;"><span> def __init__(self, entity: Actor, dx: int, dy: int):
|
||
</span></span><span style="display:flex;"><span> super().__init__(entity)
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class WaitAction(Action):
|
||
pass
|
||
|
||
|
||
<span class="new-text">class TakeStairsAction(Action):
|
||
def perform(self) -> None:
|
||
"""
|
||
Take the stairs, if any exist at the entity's location.
|
||
"""
|
||
if (self.entity.x, self.entity.y) == self.engine.game_map.downstairs_location:
|
||
self.engine.game_world.generate_floor()
|
||
self.engine.message_log.add_message(
|
||
"You descend the staircase.", color.descend
|
||
)
|
||
else:
|
||
raise exceptions.Impossible("There are no stairs here.")</span>
|
||
|
||
|
||
class ActionWithDirection(Action):
|
||
def __init__(self, entity: Actor, dx: int, dy: int):
|
||
super().__init__(entity)</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>To call this action, the player should be able to press the <code>></code> key. This can be accomplished by adding this to <code>input_handlers.py</code>:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class MainGameEventHandler(EventHandler):
|
||
</span></span><span style="display:flex;"><span> def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
|
||
</span></span><span style="display:flex;"><span> action: Optional[Action] = None
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> key = event.sym
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ modifier = event.mod
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> player = self.engine.player
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if key == tcod.event.K_PERIOD and modifier & (
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.KMOD_LSHIFT | tcod.event.KMOD_RSHIFT
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return actions.TakeStairsAction(player)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> if key in MOVE_KEYS:
|
||
</span></span><span style="display:flex;"><span> dx, dy = MOVE_KEYS[key]
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class MainGameEventHandler(EventHandler):
|
||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
|
||
action: Optional[Action] = None
|
||
|
||
key = event.sym
|
||
<span class="new-text">modifier = event.mod</span>
|
||
|
||
player = self.engine.player
|
||
|
||
<span class="new-text">if key == tcod.event.K_PERIOD and modifier & (
|
||
tcod.event.KMOD_LSHIFT | tcod.event.KMOD_RSHIFT
|
||
):
|
||
return actions.TakeStairsAction(player)</span>
|
||
|
||
if key in MOVE_KEYS:
|
||
dx, dy = MOVE_KEYS[key]</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><code>modifier</code> tells us if the player is holding a key like
|
||
control, alt, or shift. In this case, we’re checking if the user is
|
||
holding shift while pressing the period key, which gives us the “>”
|
||
key.</p>
|
||
<p>With that, the player can now descend the staircase to the next floor of the dungeon!</p>
|
||
<p><img src="Part%2011%20-%20Delving%20into%20the%20Dungeon%20%C2%B7%20Roguelike%20Tutorials_files/part-11-stairs.png" alt="Part 11 - Stairs">
|
||
<img src="Part%2011%20-%20Delving%20into%20the%20Dungeon%20%C2%B7%20Roguelike%20Tutorials_files/part-11-stairs-taken.png" alt="Part 11 - Stairs Taken"></p>
|
||
<p>One little touch we can add before moving on to the next section is
|
||
adding a way to see which floor the player is on. It’s simple enough:
|
||
We’ll use the <code>current_floor</code> in <code>GameWorld</code> to know which floor we’re on, and we’ll modify our <code>render_functions.py</code> file to add a method to print this information out to the UI.</p>
|
||
<p>Add this function to <code>render_functions.py</code>:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from __future__ import annotations
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from typing import TYPE_CHECKING
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Tuple, TYPE_CHECKING
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>import color
|
||
</span></span><span style="display:flex;"><span>...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>...
|
||
</span></span><span style="display:flex;"><span>def render_bar(
|
||
</span></span><span style="display:flex;"><span> console: Console, current_value: int, maximum_value: int, total_width: int
|
||
</span></span><span style="display:flex;"><span>) -> None:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+def render_dungeon_level(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console: Console, dungeon_level: int, location: Tuple[int, int]
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ Render the level the player is currently on, at the given location.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x, y = location
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(x=x, y=y, string=f"Dungeon level: {dungeon_level}")
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>def render_names_at_mouse_location(
|
||
</span></span><span style="display:flex;"><span> console: Console, x: int, y: int, engine: Engine
|
||
</span></span><span style="display:flex;"><span>) -> None:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>from __future__ import annotations
|
||
|
||
<span class="crossed-out-text">from typing import TYPE_CHECKING</span>
|
||
<span class="new-text">from typing import Tuple, TYPE_CHECKING</span>
|
||
|
||
import color
|
||
...
|
||
|
||
...
|
||
def render_bar(
|
||
console: Console, current_value: int, maximum_value: int, total_width: int
|
||
) -> None:
|
||
...
|
||
|
||
|
||
<span class="new-text">def render_dungeon_level(
|
||
console: Console, dungeon_level: int, location: Tuple[int, int]
|
||
) -> None:
|
||
"""
|
||
Render the level the player is currently on, at the given location.
|
||
"""
|
||
x, y = location
|
||
|
||
console.print(x=x, y=y, string=f"Dungeon level: {dungeon_level}")</span>
|
||
|
||
|
||
def render_names_at_mouse_location(
|
||
console: Console, x: int, y: int, engine: Engine
|
||
) -> None:
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>The <code>render_dungeon_level</code> function is fairly straightforward: Given a set of <code>(x, y)</code> coordinates as a Tuple, it prints to the console which dungeon level was passed to the function.</p>
|
||
<p>To call this function, we can edit the <code>Engine</code>’s <code>render</code> function, like so:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>...
|
||
</span></span><span style="display:flex;"><span>import exceptions
|
||
</span></span><span style="display:flex;"><span>from message_log import MessageLog
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from render_functions import (
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- render_bar,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- render_names_at_mouse_location,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">-)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+import render_functions
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>if TYPE_CHECKING:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>class Engine:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> def render(self, console: Console) -> None:
|
||
</span></span><span style="display:flex;"><span> self.game_map.render(console)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self.message_log.render(console=console, x=21, y=45, width=40, height=5)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- render_bar(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ render_functions.render_bar(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> console=console,
|
||
</span></span><span style="display:flex;"><span> current_value=self.player.fighter.hp,
|
||
</span></span><span style="display:flex;"><span> maximum_value=self.player.fighter.max_hp,
|
||
</span></span><span style="display:flex;"><span> total_width=20,
|
||
</span></span><span style="display:flex;"><span> )
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- render_names_at_mouse_location(console=console, x=21, y=44, engine=self)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ render_functions.render_dungeon_level(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console=console,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dungeon_level=self.game_world.current_floor,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ location=(0, 47),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ render_functions.render_names_at_mouse_location(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console=console, x=21, y=44, engine=self
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> def save_as(self, filename: str) -> None:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>...
|
||
import exceptions
|
||
from message_log import MessageLog
|
||
<span class="crossed-out-text">from render_functions import (</span>
|
||
<span class="crossed-out-text">render_bar,</span>
|
||
<span class="crossed-out-text">render_names_at_mouse_location,</span>
|
||
<span class="crossed-out-text">)</span>
|
||
<span class="new-text">import render_functions</span>
|
||
|
||
if TYPE_CHECKING:
|
||
...
|
||
|
||
|
||
class Engine:
|
||
...
|
||
|
||
def render(self, console: Console) -> None:
|
||
self.game_map.render(console)
|
||
|
||
self.message_log.render(console=console, x=21, y=45, width=40, height=5)
|
||
|
||
<span class="crossed-out-text">render_bar(</span>
|
||
<span class="new-text">render_functions.render_bar(</span>
|
||
console=console,
|
||
current_value=self.player.fighter.hp,
|
||
maximum_value=self.player.fighter.max_hp,
|
||
total_width=20,
|
||
)
|
||
|
||
<span class="crossed-out-text">render_names_at_mouse_location(console=console, x=21, y=44, engine=self)</span>
|
||
<span class="new-text">render_functions.render_dungeon_level(
|
||
console=console,
|
||
dungeon_level=self.game_world.current_floor,
|
||
location=(0, 47),
|
||
)
|
||
|
||
render_functions.render_names_at_mouse_location(
|
||
console=console, x=21, y=44, engine=self
|
||
)</span>
|
||
|
||
def save_as(self, filename: str) -> None:
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Note that we’re now importing <code>render_functions</code> instead
|
||
of importing the functions it contains. After awhile, it makes sense to
|
||
just import the entire module rather than a few functions here and
|
||
there. Otherwise, the file can get a bit difficult to read.</p>
|
||
<p>The call to <code>render_dungeon_level</code> shouldn’t be anything too surprising. We use <code>self.game_world.current_floor</code> as our <code>dungeon_level</code>, and the location of the printed string is below the health bar (feel free to move this somewhere else, if you like).</p>
|
||
<p>Try going down a few levels and make sure everything works as
|
||
expected. If so, congratulations! Your dungeon now has multiple levels!</p>
|
||
<p><img src="Part%2011%20-%20Delving%20into%20the%20Dungeon%20%C2%B7%20Roguelike%20Tutorials_files/part-11-dungeon-level.png" alt="Part 11 - Dungeon Level"></p>
|
||
<p>Speaking of “levels”, many roguelikes (not all!) feature some sort of
|
||
level-up system, where your character gains experience and gets
|
||
stronger by fighting monsters. The rest of this chapter will be spent
|
||
implementing one such system.</p>
|
||
<p>In order to allow the rogue to level up, we need to modify the actors in two ways:</p>
|
||
<ol>
|
||
<li>The player needs to gain experience points, keeping track of the XP gained thus far, and know when it’s time to level up.</li>
|
||
<li>The enemies need to give experience points when they are defeated.</li>
|
||
</ol>
|
||
<p>There are several calculations we could use to compute how much XP a
|
||
player needs to level up (or, theoretically, you could just hard code
|
||
the values). Ours will be fairly simple: We’ll start with a base number,
|
||
and add the product of our player’s current level and some other
|
||
number, which will make it so each level up requires more XP than the
|
||
last. For this tutorial, the “base” will be 200, and the “factor” will
|
||
be 150 (so going to level 2 will take 350 XP, level 3 will take 500, and
|
||
so on).</p>
|
||
<p>We can accomplish both of these goals by adding one component: <code>Level</code>. The <code>Level</code> component will hold all of the information that we need to accomplish these goals. Create a file called <code>level.py</code> in the <code>components</code> directory, and put the following contents in it:</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#f92672">from</span> __future__ <span style="color:#f92672">import</span> annotations
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> TYPE_CHECKING
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> components.base_component <span style="color:#f92672">import</span> BaseComponent
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> TYPE_CHECKING:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">from</span> entity <span style="color:#f92672">import</span> Actor
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Level</span>(BaseComponent):
|
||
</span></span><span style="display:flex;"><span> parent: Actor
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> __init__(
|
||
</span></span><span style="display:flex;"><span> self,
|
||
</span></span><span style="display:flex;"><span> current_level: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>,
|
||
</span></span><span style="display:flex;"><span> current_xp: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>,
|
||
</span></span><span style="display:flex;"><span> level_up_base: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>,
|
||
</span></span><span style="display:flex;"><span> level_up_factor: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">150</span>,
|
||
</span></span><span style="display:flex;"><span> xp_given: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>,
|
||
</span></span><span style="display:flex;"><span> ):
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>current_level <span style="color:#f92672">=</span> current_level
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>current_xp <span style="color:#f92672">=</span> current_xp
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>level_up_base <span style="color:#f92672">=</span> level_up_base
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>level_up_factor <span style="color:#f92672">=</span> level_up_factor
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>xp_given <span style="color:#f92672">=</span> xp_given
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@property</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">experience_to_next_level</span>(self) <span style="color:#f92672">-></span> int:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>level_up_base <span style="color:#f92672">+</span> self<span style="color:#f92672">.</span>current_level <span style="color:#f92672">*</span> self<span style="color:#f92672">.</span>level_up_factor
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@property</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">requires_level_up</span>(self) <span style="color:#f92672">-></span> bool:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>current_xp <span style="color:#f92672">></span> self<span style="color:#f92672">.</span>experience_to_next_level
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">add_xp</span>(self, xp: int) <span style="color:#f92672">-></span> <span style="color:#66d9ef">None</span>:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> xp <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">or</span> self<span style="color:#f92672">.</span>level_up_base <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>current_xp <span style="color:#f92672">+=</span> xp
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>engine<span style="color:#f92672">.</span>message_log<span style="color:#f92672">.</span>add_message(<span style="color:#e6db74">f</span><span style="color:#e6db74">"You gain </span><span style="color:#e6db74">{</span>xp<span style="color:#e6db74">}</span><span style="color:#e6db74"> experience points."</span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> self<span style="color:#f92672">.</span>requires_level_up:
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>engine<span style="color:#f92672">.</span>message_log<span style="color:#f92672">.</span>add_message(
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">f</span><span style="color:#e6db74">"You advance to level </span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>current_level <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">}</span><span style="color:#e6db74">!"</span>
|
||
</span></span><span style="display:flex;"><span> )
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">increase_level</span>(self) <span style="color:#f92672">-></span> <span style="color:#66d9ef">None</span>:
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>current_xp <span style="color:#f92672">-=</span> self<span style="color:#f92672">.</span>experience_to_next_level
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>current_level <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">increase_max_hp</span>(self, amount: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">20</span>) <span style="color:#f92672">-></span> <span style="color:#66d9ef">None</span>:
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>parent<span style="color:#f92672">.</span>fighter<span style="color:#f92672">.</span>max_hp <span style="color:#f92672">+=</span> amount
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>parent<span style="color:#f92672">.</span>fighter<span style="color:#f92672">.</span>hp <span style="color:#f92672">+=</span> amount
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>engine<span style="color:#f92672">.</span>message_log<span style="color:#f92672">.</span>add_message(<span style="color:#e6db74">"Your health improves!"</span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>increase_level()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">increase_power</span>(self, amount: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>) <span style="color:#f92672">-></span> <span style="color:#66d9ef">None</span>:
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>parent<span style="color:#f92672">.</span>fighter<span style="color:#f92672">.</span>power <span style="color:#f92672">+=</span> amount
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>engine<span style="color:#f92672">.</span>message_log<span style="color:#f92672">.</span>add_message(<span style="color:#e6db74">"You feel stronger!"</span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>increase_level()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">increase_defense</span>(self, amount: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>) <span style="color:#f92672">-></span> <span style="color:#66d9ef">None</span>:
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>parent<span style="color:#f92672">.</span>fighter<span style="color:#f92672">.</span>defense <span style="color:#f92672">+=</span> amount
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>engine<span style="color:#f92672">.</span>message_log<span style="color:#f92672">.</span>add_message(<span style="color:#e6db74">"Your movements are getting swifter!"</span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>increase_level()
|
||
</span></span></code></pre></div><p>Let’s go over what was just added.</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Level</span>(BaseComponent):
|
||
</span></span><span style="display:flex;"><span> parent: Actor
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> __init__(
|
||
</span></span><span style="display:flex;"><span> self,
|
||
</span></span><span style="display:flex;"><span> current_level: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>,
|
||
</span></span><span style="display:flex;"><span> current_xp: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>,
|
||
</span></span><span style="display:flex;"><span> level_up_base: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>,
|
||
</span></span><span style="display:flex;"><span> level_up_factor: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">150</span>,
|
||
</span></span><span style="display:flex;"><span> xp_given: int <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>,
|
||
</span></span><span style="display:flex;"><span> ):
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>current_level <span style="color:#f92672">=</span> current_level
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>current_xp <span style="color:#f92672">=</span> current_xp
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>level_up_base <span style="color:#f92672">=</span> level_up_base
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>level_up_factor <span style="color:#f92672">=</span> level_up_factor
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>xp_given <span style="color:#f92672">=</span> xp_given
|
||
</span></span></code></pre></div><p>The values in our <code>__init__</code> function break down like this:</p>
|
||
<ul>
|
||
<li>current_level: The current level of the Entity, defaults to 1.</li>
|
||
<li>current_xp: The Entity’s current experience points.</li>
|
||
<li>level_up_base: The base number we decide for leveling up. We’ll set this to 200 when creating the Player.</li>
|
||
<li>level_up_factor: The number to multiply against the Entity’s current level.</li>
|
||
<li>xp_given: When the Entity dies, this is how much XP the Player will gain.</li>
|
||
</ul>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#a6e22e">@property</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">experience_to_next_level</span>(self) <span style="color:#f92672">-></span> int:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>level_up_base <span style="color:#f92672">+</span> self<span style="color:#f92672">.</span>current_level <span style="color:#f92672">*</span> self<span style="color:#f92672">.</span>level_up_factor
|
||
</span></span></code></pre></div><p>This represents how much experience
|
||
the player needs until hitting the next level. The formula is explained
|
||
above. Again, feel free to tweak this formula in any way you see fit.</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#a6e22e">@property</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">requires_level_up</span>(self) <span style="color:#f92672">-></span> bool:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>current_xp <span style="color:#f92672">></span> self<span style="color:#f92672">.</span>experience_to_next_level
|
||
</span></span></code></pre></div><p>We’ll use this property to determine if the player needs to level up or not. If the <code>current_xp</code> is higher than the <code>experience_to_next_level</code> property, then the player levels up. If not, nothing happens.</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">add_xp</span>(self, xp: int) <span style="color:#f92672">-></span> <span style="color:#66d9ef">None</span>:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> xp <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">or</span> self<span style="color:#f92672">.</span>level_up_base <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>current_xp <span style="color:#f92672">+=</span> xp
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>engine<span style="color:#f92672">.</span>message_log<span style="color:#f92672">.</span>add_message(<span style="color:#e6db74">f</span><span style="color:#e6db74">"You gain </span><span style="color:#e6db74">{</span>xp<span style="color:#e6db74">}</span><span style="color:#e6db74"> experience points."</span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> self<span style="color:#f92672">.</span>requires_level_up:
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>engine<span style="color:#f92672">.</span>message_log<span style="color:#f92672">.</span>add_message(
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">f</span><span style="color:#e6db74">"You advance to level </span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>current_level <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span><span style="color:#e6db74">}</span><span style="color:#e6db74">!"</span>
|
||
</span></span><span style="display:flex;"><span> )
|
||
</span></span></code></pre></div><p>This method adds experience points
|
||
to the Entity’s XP pool, as the name implies. If the value is 0, we just
|
||
return, as there’s nothing to do. Notice that we also return if the <code>level_up_base</code> is set to 0. Why? In this tutorial, the enemies don’t gain XP, so we’ll set their <code>level_up_base</code> to 0 so that there’s no way they could ever gain experience. Perhaps in your game, monsters <em>will</em> gain XP, and you’ll want to adjust this, but that’s left up to you.</p>
|
||
<p>The rest of the method adds the xp, adds a message to the message log, and, if the Entity levels up, posts another message.</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">increase_level</span>(self) <span style="color:#f92672">-></span> <span style="color:#66d9ef">None</span>:
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>current_xp <span style="color:#f92672">-=</span> self<span style="color:#f92672">.</span>experience_to_next_level
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>current_level <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
|
||
</span></span></code></pre></div><p>This method adds +1 to the <code>current_level</code>, while decreasing the <code>current_xp</code> by the <code>experience_to_next_level</code>. We do this because if we didn’t it would always just take the <code>level_up_factor</code> amount to level up, which isn’t what we want. If you wanted to keep track of the player’s <em>cumulative</em> XP throughout the playthrough, you could skip decrementing the <code>current_xp</code> and instead adjust the <code>experience_to_next_level</code> formula accordingly.</p>
|
||
<p>Lastly, the functions <code>increase_max_hp</code>, <code>increase_power</code>, and <code>increase_defense</code> all do basically the same thing: they raise one of the Entity’s attributes, add a message to the message log, then call <code>increase_level</code>.</p>
|
||
<p>To use this component, we need to add it to our <code>Actor</code> class. Make the following changes to the file <code>entity.py</code>:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>if TYPE_CHECKING:
|
||
</span></span><span style="display:flex;"><span> from components.ai import BaseAI
|
||
</span></span><span style="display:flex;"><span> from components.consumable import Consumable
|
||
</span></span><span style="display:flex;"><span> from components.fighter import Fighter
|
||
</span></span><span style="display:flex;"><span> from components.inventory import Inventory
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from components.level import Level
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> from game_map import GameMap
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>T = TypeVar("T", bound="Entity")
|
||
</span></span><span style="display:flex;"><span>...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>class Actor(Entity):
|
||
</span></span><span style="display:flex;"><span> def __init__(
|
||
</span></span><span style="display:flex;"><span> self,
|
||
</span></span><span style="display:flex;"><span> *,
|
||
</span></span><span style="display:flex;"><span> x: int = 0,
|
||
</span></span><span style="display:flex;"><span> y: int = 0,
|
||
</span></span><span style="display:flex;"><span> char: str = "?",
|
||
</span></span><span style="display:flex;"><span> color: Tuple[int, int, int] = (255, 255, 255),
|
||
</span></span><span style="display:flex;"><span> name: str = "<Unnamed>",
|
||
</span></span><span style="display:flex;"><span> ai_cls: Type[BaseAI],
|
||
</span></span><span style="display:flex;"><span> fighter: Fighter,
|
||
</span></span><span style="display:flex;"><span> inventory: Inventory,
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ level: Level,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> ):
|
||
</span></span><span style="display:flex;"><span> super().__init__(
|
||
</span></span><span style="display:flex;"><span> x=x,
|
||
</span></span><span style="display:flex;"><span> y=y,
|
||
</span></span><span style="display:flex;"><span> char=char,
|
||
</span></span><span style="display:flex;"><span> color=color,
|
||
</span></span><span style="display:flex;"><span> name=name,
|
||
</span></span><span style="display:flex;"><span> blocks_movement=True,
|
||
</span></span><span style="display:flex;"><span> render_order=RenderOrder.ACTOR,
|
||
</span></span><span style="display:flex;"><span> )
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self.ai: Optional[BaseAI] = ai_cls(self)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self.fighter = fighter
|
||
</span></span><span style="display:flex;"><span> self.fighter.parent = self
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self.inventory = inventory
|
||
</span></span><span style="display:flex;"><span> self.inventory.parent = self
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.level = level
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.level.parent = self
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> @property
|
||
</span></span><span style="display:flex;"><span> def is_alive(self) -> bool:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>if TYPE_CHECKING:
|
||
from components.ai import BaseAI
|
||
from components.consumable import Consumable
|
||
from components.fighter import Fighter
|
||
from components.inventory import Inventory
|
||
<span class="new-text">from components.level import Level</span>
|
||
from game_map import GameMap
|
||
|
||
T = TypeVar("T", bound="Entity")
|
||
...
|
||
|
||
class Actor(Entity):
|
||
def __init__(
|
||
self,
|
||
*,
|
||
x: int = 0,
|
||
y: int = 0,
|
||
char: str = "?",
|
||
color: Tuple[int, int, int] = (255, 255, 255),
|
||
name: str = "<Unnamed>",
|
||
ai_cls: Type[BaseAI],
|
||
fighter: Fighter,
|
||
inventory: Inventory,
|
||
<span class="new-text">level: Level,</span>
|
||
):
|
||
super().__init__(
|
||
x=x,
|
||
y=y,
|
||
char=char,
|
||
color=color,
|
||
name=name,
|
||
blocks_movement=True,
|
||
render_order=RenderOrder.ACTOR,
|
||
)
|
||
|
||
self.ai: Optional[BaseAI] = ai_cls(self)
|
||
|
||
self.fighter = fighter
|
||
self.fighter.parent = self
|
||
|
||
self.inventory = inventory
|
||
self.inventory.parent = self
|
||
|
||
<span class="new-text">self.level = level
|
||
self.level.parent = self</span>
|
||
|
||
@property
|
||
def is_alive(self) -> bool:
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Let’s also modify our entities in <code>entity_factories.py</code> now:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from components.ai import HostileEnemy
|
||
</span></span><span style="display:flex;"><span>from components import consumable
|
||
</span></span><span style="display:flex;"><span>from components.fighter import Fighter
|
||
</span></span><span style="display:flex;"><span>from components.inventory import Inventory
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from components.level import Level
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from entity import Actor, Item
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>player = Actor(
|
||
</span></span><span style="display:flex;"><span> char="@",
|
||
</span></span><span style="display:flex;"><span> color=(255, 255, 255),
|
||
</span></span><span style="display:flex;"><span> name="Player",
|
||
</span></span><span style="display:flex;"><span> ai_cls=HostileEnemy,
|
||
</span></span><span style="display:flex;"><span> fighter=Fighter(hp=30, defense=2, power=5),
|
||
</span></span><span style="display:flex;"><span> inventory=Inventory(capacity=26),
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ level=Level(level_up_base=200),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>orc = Actor(
|
||
</span></span><span style="display:flex;"><span> char="o",
|
||
</span></span><span style="display:flex;"><span> color=(63, 127, 63),
|
||
</span></span><span style="display:flex;"><span> name="Orc",
|
||
</span></span><span style="display:flex;"><span> ai_cls=HostileEnemy,
|
||
</span></span><span style="display:flex;"><span> fighter=Fighter(hp=10, defense=0, power=3),
|
||
</span></span><span style="display:flex;"><span> inventory=Inventory(capacity=0),
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ level=Level(xp_given=35),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>)
|
||
</span></span><span style="display:flex;"><span>troll = Actor(
|
||
</span></span><span style="display:flex;"><span> char="T",
|
||
</span></span><span style="display:flex;"><span> color=(0, 127, 0),
|
||
</span></span><span style="display:flex;"><span> name="Troll",
|
||
</span></span><span style="display:flex;"><span> ai_cls=HostileEnemy,
|
||
</span></span><span style="display:flex;"><span> fighter=Fighter(hp=16, defense=1, power=4),
|
||
</span></span><span style="display:flex;"><span> inventory=Inventory(capacity=0),
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ level=Level(xp_given=100),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>)
|
||
</span></span><span style="display:flex;"><span>...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>from components.ai import HostileEnemy
|
||
from components import consumable
|
||
from components.fighter import Fighter
|
||
from components.inventory import Inventory
|
||
<span class="new-text">from components.level import Level</span>
|
||
from entity import Actor, Item
|
||
|
||
|
||
player = Actor(
|
||
char="@",
|
||
color=(255, 255, 255),
|
||
name="Player",
|
||
ai_cls=HostileEnemy,
|
||
fighter=Fighter(hp=30, defense=2, power=5),
|
||
inventory=Inventory(capacity=26),
|
||
<span class="new-text">level=Level(level_up_base=200),</span>
|
||
)
|
||
|
||
orc = Actor(
|
||
char="o",
|
||
color=(63, 127, 63),
|
||
name="Orc",
|
||
ai_cls=HostileEnemy,
|
||
fighter=Fighter(hp=10, defense=0, power=3),
|
||
inventory=Inventory(capacity=0),
|
||
<span class="new-text">level=Level(xp_given=35),</span>
|
||
)
|
||
troll = Actor(
|
||
char="T",
|
||
color=(0, 127, 0),
|
||
name="Troll",
|
||
ai_cls=HostileEnemy,
|
||
fighter=Fighter(hp=16, defense=1, power=4),
|
||
inventory=Inventory(capacity=0),
|
||
<span class="new-text">level=Level(xp_given=100),</span>
|
||
)
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>As mentioned, the <code>level_up_base</code> for the player is set to
|
||
200. Orcs give 35 XP, and Trolls give 100, since they’re stronger.
|
||
These values are completely arbitrary, so feel free to adjust them in
|
||
any way you see fit.</p>
|
||
<p>When an enemy dies, we need to give the player XP. This is as simple as adding one line to the <code>Fighter</code> component, so open up <code>fighter.py</code> and add this:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class Fighter(BaseComponent):
|
||
</span></span><span style="display:flex;"><span> def die(self) -> None:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self.engine.message_log.add_message(death_message, death_message_color)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.player.level.add_xp(self.parent.level.xp_given)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> def heal(self, amount: int) -> int:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class Fighter(BaseComponent):
|
||
def die(self) -> None:
|
||
...
|
||
|
||
self.engine.message_log.add_message(death_message, death_message_color)
|
||
|
||
<span class="new-text">self.engine.player.level.add_xp(self.parent.level.xp_given)</span>
|
||
|
||
def heal(self, amount: int) -> int:
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Now the player will gain XP for defeating enemies!</p>
|
||
<p>While the player does gain XP now, notice that we haven’t actually <em>called</em>
|
||
the functions that increase the player’s stats and levels the player
|
||
up. We’ll need a new interface to do this. The way it will work is that
|
||
as soon as the player gets enough experience to level up, we’ll display a
|
||
message to the player, giving the player three choices on what stat to
|
||
increase. When chosen, the appropriate function will be called, and the
|
||
message will close.</p>
|
||
<p>Let’s create a new event handler, called <code>LevelUpEventHandler</code>, that will do just that. Create the following class in <code>input_handlers.py</code>:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class AskUserEventHandler(EventHandler):
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+class LevelUpEventHandler(AskUserEventHandler):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ TITLE = "Level Up"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def on_render(self, console: tcod.Console) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ super().on_render(console)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.engine.player.x <= 30:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x = 40
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ else:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x = 0
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.draw_frame(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=x,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y=0,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ width=35,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ height=8,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ title=self.TITLE,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ clear=True,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ fg=(255, 255, 255),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ bg=(0, 0, 0),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(x=x + 1, y=1, string="Congratulations! You level up!")
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(x=x + 1, y=2, string="Select an attribute to increase.")
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=x + 1,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y=4,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ string=f"a) Constitution (+20 HP, from {self.engine.player.fighter.max_hp})",
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=x + 1,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y=5,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ string=f"b) Strength (+1 attack, from {self.engine.player.fighter.power})",
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=x + 1,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y=6,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ string=f"c) Agility (+1 defense, from {self.engine.player.fighter.defense})",
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[ActionOrHandler]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player = self.engine.player
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ key = event.sym
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ index = key - tcod.event.K_a
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if 0 <= index <= 2:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if index == 0:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player.level.increase_max_hp()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ elif index == 1:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player.level.increase_power()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ else:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player.level.increase_defense()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ else:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.message_log.add_message("Invalid entry.", color.invalid)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return None
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return super().ev_keydown(event)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def ev_mousebuttondown(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self, event: tcod.event.MouseButtonDown
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ) -> Optional[ActionOrHandler]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ Don't allow the player to click to exit the menu, like normal.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return None
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>class InventoryEventHandler(AskUserEventHandler):
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class AskUserEventHandler(EventHandler):
|
||
...
|
||
|
||
<span class="new-text">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=x + 1, y=1, string="Congratulations! You level up!")
|
||
console.print(x=x + 1, y=2, string="Select an attribute 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</span>
|
||
|
||
|
||
class InventoryEventHandler(AskUserEventHandler):
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>The idea here is very similar to <code>InventoryEventHandler</code> (it inherits from the same <code>AskUserEventHandler</code>
|
||
class), but instead of having a variable number of options, it’s set to
|
||
three, one for each of the primary attributes. Furthermore, there’s no
|
||
way to exit this menu without selecting something. The user <strong>must</strong> level up before continuing. (Notice, we had to override <code>ev_mousebutton</code> to prevent clicks from closing the menu.)</p>
|
||
<p>Using <code>LevelUpEventHandler</code> is actually quite simple: We
|
||
can check when the player requires a level up at the same time when we
|
||
check if the player is still alive. Edit the <code>handle_events</code> method of <code>EventHandler</code> like this:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span> if not self.engine.player.is_alive:
|
||
</span></span><span style="display:flex;"><span> # The player was killed sometime during or after the action.
|
||
</span></span><span style="display:flex;"><span> return GameOverEventHandler(self.engine)
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ elif self.engine.player.level.requires_level_up:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return LevelUpEventHandler(self.engine)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> return MainGameEventHandler(self.engine) # Return to the main handler.
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre> if not self.engine.player.is_alive:
|
||
# The player was killed sometime during or after the action.
|
||
return GameOverEventHandler(self.engine)
|
||
<span class="new-text">elif self.engine.player.level.requires_level_up:
|
||
return LevelUpEventHandler(self.engine)</span>
|
||
return MainGameEventHandler(self.engine) # Return to the main handler.</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Now, when the player gains the necessary number of experience points, the player will have the chance to level up!</p>
|
||
<p><img src="Part%2011%20-%20Delving%20into%20the%20Dungeon%20%C2%B7%20Roguelike%20Tutorials_files/part-11-level-up.png" alt="Part 11 - Level Up"></p>
|
||
<p>Before finishing this chapter, there’s one last quick thing we can do
|
||
to improve the user experience: Add a “character information” screen,
|
||
which displays the player’s stats and current experience. It’s actually
|
||
quite simple. Add the following class to <code>input_handlers.py</code>:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class AskUserEventHandler(EventHandler):
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+class CharacterScreenEventHandler(AskUserEventHandler):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ TITLE = "Character Information"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def on_render(self, console: tcod.Console) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ super().on_render(console)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.engine.player.x <= 30:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x = 40
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ else:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x = 0
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y = 0
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ width = len(self.TITLE) + 4
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.draw_frame(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=x,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y=y,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ width=width,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ height=7,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ title=self.TITLE,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ clear=True,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ fg=(255, 255, 255),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ bg=(0, 0, 0),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=x + 1, y=y + 1, string=f"Level: {self.engine.player.level.current_level}"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=x + 1, y=y + 2, string=f"XP: {self.engine.player.level.current_xp}"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=x + 1,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y=y + 3,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ string=f"XP for next Level: {self.engine.player.level.experience_to_next_level}",
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=x + 1, y=y + 4, string=f"Attack: {self.engine.player.fighter.power}"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=x + 1, y=y + 5, string=f"Defense: {self.engine.player.fighter.defense}"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>class LevelUpEventHandler(AskUserEventHandler):
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class AskUserEventHandler(EventHandler):
|
||
...
|
||
|
||
<span class="new-text">class CharacterScreenEventHandler(AskUserEventHandler):
|
||
TITLE = "Character Information"
|
||
|
||
def on_render(self, console: tcod.Console) -> None:
|
||
super().on_render(console)
|
||
|
||
if self.engine.player.x <= 30:
|
||
x = 40
|
||
else:
|
||
x = 0
|
||
|
||
y = 0
|
||
|
||
width = len(self.TITLE) + 4
|
||
|
||
console.draw_frame(
|
||
x=x,
|
||
y=y,
|
||
width=width,
|
||
height=7,
|
||
title=self.TITLE,
|
||
clear=True,
|
||
fg=(255, 255, 255),
|
||
bg=(0, 0, 0),
|
||
)
|
||
|
||
console.print(
|
||
x=x + 1, y=y + 1, string=f"Level: {self.engine.player.level.current_level}"
|
||
)
|
||
console.print(
|
||
x=x + 1, y=y + 2, string=f"XP: {self.engine.player.level.current_xp}"
|
||
)
|
||
console.print(
|
||
x=x + 1,
|
||
y=y + 3,
|
||
string=f"XP for next Level: {self.engine.player.level.experience_to_next_level}",
|
||
)
|
||
|
||
console.print(
|
||
x=x + 1, y=y + 4, string=f"Attack: {self.engine.player.fighter.power}"
|
||
)
|
||
console.print(
|
||
x=x + 1, y=y + 5, string=f"Defense: {self.engine.player.fighter.defense}"
|
||
)</span>
|
||
|
||
class LevelUpEventHandler(AskUserEventHandler):
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Similar to <code>LevelUpEventHandler</code>, <code>CharacterScreenEventHandler</code> shows information in a window, but there’s no real “choices” to be made here. Any input will simply close the screen.</p>
|
||
<p>To open the screen, we’ll have the player press the <code>c</code> key. Add the following to <code>MainGameEventHandler</code>:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span> elif key == tcod.event.K_d:
|
||
</span></span><span style="display:flex;"><span> return InventoryDropHandler(self.engine)
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ elif key == tcod.event.K_c:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return CharacterScreenEventHandler(self.engine)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> elif key == tcod.event.K_SLASH:
|
||
</span></span><span style="display:flex;"><span> return LookHandler(self.engine)
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre> elif key == tcod.event.K_d:
|
||
return InventoryDropHandler(self.engine)
|
||
<span class="new-text">elif key == tcod.event.K_c:
|
||
return CharacterScreenEventHandler(self.engine)</span>
|
||
elif key == tcod.event.K_SLASH:
|
||
return LookHandler(self.engine)</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><img src="Part%2011%20-%20Delving%20into%20the%20Dungeon%20%C2%B7%20Roguelike%20Tutorials_files/part-11-character-screen.png" alt="Part 11 - Character Screen"></p>
|
||
<p>That’s it for this chapter. We’ve added the ability to go down
|
||
floors, and to level up. While the player can now “progress”, the
|
||
environment itself doesn’t. The items that spawn on each floor are
|
||
always the same, and the enemies don’t get tougher as we go down floors.
|
||
The next part will address that.</p>
|
||
<p>If you want to see the code so far in its entirety, <a href="https://github.com/TStand90/tcod_tutorial_v2/tree/2020/part-11">click here</a>.</p>
|
||
<p><a href="https://rogueliketutorials.com/tutorials/tcod/v2/part-12">Click here to move on to the next part of this tutorial.</a></p>
|
||
|
||
</article>
|
||
</section>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
<footer class="footer">
|
||
<section class="container">
|
||
©
|
||
|
||
2023
|
||
|
||
·
|
||
|
||
Powered by <a href="https://gohugo.io/">Hugo</a> & <a href="https://github.com/luizdepra/hugo-coder/">Coder</a>.
|
||
|
||
</section>
|
||
</footer>
|
||
|
||
</main>
|
||
|
||
|
||
|
||
|
||
|
||
<script src="Part%2011%20-%20Delving%20into%20the%20Dungeon%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.236049395dc3682fb2719640872958e12f1f24067bb09c327b2.js" integrity="sha256-I2BJOV3DaC+ycZZAhylY4S8fJAZ7sJwyeyM+YpDH7aw="></script>
|
||
|
||
|
||
|
||
|
||
|
||
<script src="Part%2011%20-%20Delving%20into%20the%20Dungeon%20%C2%B7%20Roguelike%20Tutorials_files/codetabs.min.cc52451e7f25e50f64c1c893826f606d58410d742c214dce.js" integrity="sha256-zFJFHn8l5Q9kwciTgm9gbVhBDXQsIU3OI/tEfJlh8rA="></script>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
</body></html> |