1591 lines
87 KiB
HTML
1591 lines
87 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 5 - Placing Enemies and kicking them (harmlessly) · 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="What good is a dungeon with no monsters to bash? This chapter will focus on placing the enemies throughout the dungeon, and setting them up to be attacked (the actual attacking part we’ll save for next time).
|
||
When we’re building our dungeon, we’ll need to place the enemies in the rooms. In order to do that, we will need to make a change to the way entities are stored in our game.">
|
||
<meta name="keywords" content="">
|
||
|
||
<meta name="twitter:card" content="summary">
|
||
<meta name="twitter:title" content="Part 5 - Placing Enemies and kicking them (harmlessly)">
|
||
<meta name="twitter:description" content="What good is a dungeon with no monsters to bash? This chapter will focus on placing the enemies throughout the dungeon, and setting them up to be attacked (the actual attacking part we’ll save for next time).
|
||
When we’re building our dungeon, we’ll need to place the enemies in the rooms. In order to do that, we will need to make a change to the way entities are stored in our game.">
|
||
|
||
<meta property="og:title" content="Part 5 - Placing Enemies and kicking them (harmlessly)">
|
||
<meta property="og:description" content="What good is a dungeon with no monsters to bash? This chapter will focus on placing the enemies throughout the dungeon, and setting them up to be attacked (the actual attacking part we’ll save for next time).
|
||
When we’re building our dungeon, we’ll need to place the enemies in the rooms. In order to do that, we will need to make a change to the way entities are stored in our game.">
|
||
<meta property="og:type" content="article">
|
||
<meta property="og:url" content="https://rogueliketutorials.com/tutorials/tcod/v2/part-5/"><meta property="article:section" content="tutorials">
|
||
<meta property="article:published_time" content="2020-06-29T00:00:00+00:00">
|
||
<meta property="article:modified_time" content="2020-06-29T00:00:00+00:00">
|
||
|
||
|
||
|
||
|
||
<link rel="canonical" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-5/">
|
||
|
||
|
||
<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%205%20-%20Placing%20Enemies%20and%20kicking%20them%20(harmlessly)%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.c4d7e93a158eda5a65b3df343745d2092a0a1e2170feeec909.css" integrity="sha256-xNfpOhWO2lpls980N0XSCSoKHiFw/u7JCbiolEOQPGo=" crossorigin="anonymous" media="screen">
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<link rel="stylesheet" href="Part%205%20-%20Placing%20Enemies%20and%20kicking%20them%20(harmlessly)%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%205%20-%20Placing%20Enemies%20and%20kicking%20them%20(harmlessly)%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-5/">
|
||
Part 5 - Placing Enemies and kicking them (harmlessly)
|
||
</a>
|
||
</h1>
|
||
</header>
|
||
|
||
<p>What good is a dungeon with no monsters to bash? This chapter
|
||
will focus on placing the enemies throughout the dungeon, and setting
|
||
them up to be attacked (the actual attacking part we’ll save for next
|
||
time).</p>
|
||
<p>When we’re building our dungeon, we’ll need to place the enemies in
|
||
the rooms. In order to do that, we will need to make a change to the way
|
||
<code>entities</code> are stored in our game. Currently, they’re saved in the <code>Engine</code>
|
||
class. However, for the sake of placing enemies in the dungeon, and
|
||
when we get to the part where we move between dungeon floors, it will be
|
||
better to store them in the <code>GameMap</code> class. That way, the
|
||
map has access to the entities directly, and we can preserve which
|
||
entities are on which floors fairly easily.</p>
|
||
<p>Start by modifying <code>GameMap</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 style="color:#a6e22e">+from __future__ import annotations
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from typing import Iterable, TYPE_CHECKING
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>import numpy as np # type: ignore
|
||
</span></span><span style="display:flex;"><span>from tcod.console import Console
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>import tile_types
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+if TYPE_CHECKING:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from entity import Entity
|
||
</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 GameMap:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def __init__(self, width: int, height: int):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.width, self.height = width, height
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entities = set(entities)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre><span class="new-text">from __future__ import annotations
|
||
|
||
from typing import Iterable, TYPE_CHECKING</span>
|
||
|
||
import numpy as np # type: ignore
|
||
from tcod.console import Console
|
||
|
||
import tile_types
|
||
|
||
<span class="new-text">if TYPE_CHECKING:
|
||
from entity import Entity</span>
|
||
|
||
|
||
class GameMap:
|
||
<span class="crossed-out-text">def __init__(self, width: int, height: int):</span>
|
||
<span class="new-text">def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):</span>
|
||
self.width, self.height = width, height
|
||
<span class="new-text">self.entities = set(entities)</span>
|
||
self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Then, let’s modify <code>Engine</code> to remove the <code>entities</code> from it:</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 style="color:#f92672">-from typing import Set, Iterable, Any
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Iterable, Any
|
||
</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><span style="color:#f92672">- def __init__(self, entities: Set[Entity], event_handler: EventHandler, game_map: GameMap, player: Entity):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- self.entities = entities
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> self.event_handler = event_handler
|
||
</span></span><span style="display:flex;"><span> self.game_map = game_map
|
||
</span></span><span style="display:flex;"><span> self.player = player
|
||
</span></span><span style="display:flex;"><span> self.update_fov()
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre><span class="crossed-out-text">from typing import Set, Iterable, Any</span>
|
||
<span class="new-text">from typing import Iterable, Any</span>
|
||
|
||
|
||
class Engine:
|
||
<span class="crossed-out-text">def __init__(self, entities: Set[Entity], event_handler: EventHandler, game_map: GameMap, player: Entity):</span>
|
||
<span class="new-text">def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):</span>
|
||
<span class="crossed-out-text">self.entities = entities</span>
|
||
self.event_handler = event_handler
|
||
self.game_map = game_map
|
||
self.player = player
|
||
self.update_fov()</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Because we’ve modified the definition of <code>Engine.__init__</code>, we need to modify <code>main.py</code> where we create our <code>game_map</code> variable. We might as well remove that <code>npc</code> as well, since we won’t be needing it anymore.</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> player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), "@", (255, 255, 0))
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- entities = {npc, player}
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span> game_map = generate_dungeon(
|
||
</span></span><span style="display:flex;"><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> player=player,
|
||
</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">- engine = Engine(entities=entities, event_handler=event_handler, game_map=game_map, player=player)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ engine = Engine(event_handler=event_handler, game_map=game_map, player=player)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> with tcod.context.new_terminal(
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre> ...
|
||
player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))
|
||
<span class="crossed-out-text">npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), "@", (255, 255, 0))</span>
|
||
<span class="crossed-out-text">entities = {npc, player}</span>
|
||
|
||
game_map = generate_dungeon(
|
||
max_rooms=max_rooms,
|
||
room_min_size=room_min_size,
|
||
room_max_size=room_max_size,
|
||
map_width=map_width,
|
||
map_height=map_height,
|
||
player=player,
|
||
)
|
||
|
||
<span class="crossed-out-text">engine = Engine(entities=entities, event_handler=event_handler, game_map=game_map, player=player)</span>
|
||
<span class="new-text">engine = Engine(event_handler=event_handler, game_map=game_map, player=player)</span>
|
||
|
||
with tcod.context.new_terminal(
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>We can remove the part in <code>Engine.render</code> that loops through the entities and renders the ones that are visible. That part will also be handled by the <code>GameMap</code> from now on.</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 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, context: Context) -> 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><span style="color:#f92672">- for entity in self.entities:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- # Only print entities that are in the FOV
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- if self.game_map.visible[entity.x, entity.y]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- console.print(entity.x, entity.y, entity.char, fg=entity.color)
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class Engine:
|
||
...
|
||
|
||
def render(self, console: Console, context: Context) -> None:
|
||
self.game_map.render(console)
|
||
|
||
<span class="crossed-out-text">for entity in self.entities:</span>
|
||
<span class="crossed-out-text"># Only print entities that are in the FOV</span>
|
||
<span class="crossed-out-text">if self.game_map.visible[entity.x, entity.y]:</span>
|
||
<span class="crossed-out-text">console.print(entity.x, entity.y, entity.char, fg=entity.color)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>We can move this block into <code>GameMap.render</code>, though take note that the line that checks for visibility has a slight change: it goes from:</p>
|
||
<p><code>if self.game_map.visible[entity.x, entity.y]:</code></p>
|
||
<p>To:</p>
|
||
<p><code>if self.visible[entity.x, entity.y]:</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> ...
|
||
</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> """
|
||
</span></span><span style="display:flex;"><span> Renders the map.
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> If a tile is in the "visible" array, then draw it with the "light" colors.
|
||
</span></span><span style="display:flex;"><span> If it isn't, but it's in the "explored" array, then draw it with the "dark" colors.
|
||
</span></span><span style="display:flex;"><span> Otherwise, the default is "SHROUD".
|
||
</span></span><span style="display:flex;"><span> """
|
||
</span></span><span style="display:flex;"><span> console.tiles_rgb[0:self.width, 0:self.height] = np.select(
|
||
</span></span><span style="display:flex;"><span> condlist=[self.visible, self.explored],
|
||
</span></span><span style="display:flex;"><span> choicelist=[self.tiles["light"], self.tiles["dark"]],
|
||
</span></span><span style="display:flex;"><span> default=tile_types.SHROUD
|
||
</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">+ for entity in self.entities:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Only print entities that are in the FOV
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.visible[entity.x, entity.y]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class GameMap:
|
||
...
|
||
|
||
def render(self, console: Console) -> None:
|
||
"""
|
||
Renders the map.
|
||
|
||
If a tile is in the "visible" array, then draw it with the "light" colors.
|
||
If it isn't, but it's in the "explored" array, then draw it with the "dark" colors.
|
||
Otherwise, the default is "SHROUD".
|
||
"""
|
||
console.tiles_rgb[0:self.width, 0:self.height] = np.select(
|
||
condlist=[self.visible, self.explored],
|
||
choicelist=[self.tiles["light"], self.tiles["dark"]],
|
||
default=tile_types.SHROUD
|
||
)
|
||
|
||
<span class="new-text">for entity in self.entities:
|
||
# Only print entities that are in the FOV
|
||
if self.visible[entity.x, entity.y]:
|
||
console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Finally, we need to alter the part in <code>generate_dungeon</code> that creates the instance of <code>GameMap</code>, so that the <code>player</code> is passed into the <code>entities</code> argument.</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>def generate_dungeon(
|
||
</span></span><span style="display:flex;"><span> max_rooms: int,
|
||
</span></span><span style="display:flex;"><span> room_min_size: int,
|
||
</span></span><span style="display:flex;"><span> room_max_size: int,
|
||
</span></span><span style="display:flex;"><span> map_width: int,
|
||
</span></span><span style="display:flex;"><span> map_height: int,
|
||
</span></span><span style="display:flex;"><span> player: Entity,
|
||
</span></span><span style="display:flex;"><span>) -> GameMap:
|
||
</span></span><span style="display:flex;"><span> """Generate a new dungeon map."""
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- dungeon = GameMap(map_width, map_height)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ dungeon = GameMap(map_width, map_height, entities=[player])
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> rooms: List[RectangularRoom] = []
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>def generate_dungeon(
|
||
max_rooms: int,
|
||
room_min_size: int,
|
||
room_max_size: int,
|
||
map_width: int,
|
||
map_height: int,
|
||
player: Entity,
|
||
) -> GameMap:
|
||
"""Generate a new dungeon map."""
|
||
<span class="crossed-out-text">dungeon = GameMap(map_width, map_height)</span>
|
||
<span class="new-text">dungeon = GameMap(map_width, map_height, entities=[player])</span>
|
||
|
||
rooms: List[RectangularRoom] = []
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>If you run the project now, things should look the same as before, minus the NPC that we had earlier for testing.</p>
|
||
<p>Now, moving on to actually placing monsters in our dungeon. Our logic
|
||
will be simple enough: For each room that’s created in our dungeon,
|
||
we’ll place a random number of enemies, between 0 and a maximum (2 for
|
||
now). We’ll make it so that there’s an 80% chance of spawning an Orc (a
|
||
weaker enemy) and a 20% chance of it being a Troll (a stronger enemy).</p>
|
||
<p>In order to specify the maximum number of monsters that can be spawned into a room, let’s create a new variable, <code>max_monsters_per_room</code>, and place it in <code>main.py</code>. We’ll also modify our call to <code>generate_dungeon</code> to pass this new variable in.</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> max_rooms = 30
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_monsters_per_room = 2
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> tileset = tcod.tileset.load_tilesheet(
|
||
</span></span><span style="display:flex;"><span> "dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD
|
||
</span></span><span style="display:flex;"><span> )
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> event_handler = EventHandler()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> game_map = generate_dungeon(
|
||
</span></span><span style="display:flex;"><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><span style="color:#a6e22e">+ max_monsters_per_room=max_monsters_per_room,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> player=player
|
||
</span></span><span style="display:flex;"><span> )
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> engine = Engine(event_handler=event_handler, game_map=game_map, player=player)
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre> ...
|
||
max_rooms = 30
|
||
|
||
<span class="new-text">max_monsters_per_room = 2</span>
|
||
|
||
tileset = tcod.tileset.load_tilesheet(
|
||
"dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD
|
||
)
|
||
|
||
event_handler = EventHandler()
|
||
|
||
player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))
|
||
|
||
game_map = generate_dungeon(
|
||
max_rooms=max_rooms,
|
||
room_min_size=room_min_size,
|
||
room_max_size=room_max_size,
|
||
map_width=map_width,
|
||
map_height=map_height,
|
||
<span class="new-text">max_monsters_per_room=max_monsters_per_room,</span>
|
||
player=player
|
||
)
|
||
|
||
engine = Engine(event_handler=event_handler, game_map=game_map, player=player)
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Pretty straightforward. Now we’ll need to modify the definition of <code>generate_dungeon</code> to take this new variable, 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>def generate_dungeon(
|
||
</span></span><span style="display:flex;"><span> max_rooms: int,
|
||
</span></span><span style="display:flex;"><span> room_min_size: int,
|
||
</span></span><span style="display:flex;"><span> room_max_size: int,
|
||
</span></span><span style="display:flex;"><span> map_width: int,
|
||
</span></span><span style="display:flex;"><span> map_height: int,
|
||
</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"></span> player: Entity,
|
||
</span></span><span style="display:flex;"><span>) -> GameMap:
|
||
</span></span><span style="display:flex;"><span> """Generate a new dungeon map."""
|
||
</span></span><span style="display:flex;"><span> dungeon = GameMap(map_width, map_height, entities=[player])
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>def generate_dungeon(
|
||
max_rooms: int,
|
||
room_min_size: int,
|
||
room_max_size: int,
|
||
map_width: int,
|
||
map_height: int,
|
||
<span class="new-text">max_monsters_per_room: int,</span>
|
||
player: Entity,
|
||
) -> GameMap:
|
||
"""Generate a new dungeon map."""
|
||
dungeon = GameMap(map_width, map_height, entities=[player])</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Easy enough, but now how do we actually place the enemies?</p>
|
||
<p>After we’ve created our room, we’ll want to call a function to put the entities in their places. Let’s call the function <code>place_entities</code>, and it will take three arguments: The <code>RectangularRoom</code> that we’ve created, the <code>dungeon</code> so that it can add the entities to it (remember that <code>dungeon</code> is an instance of <code>GameMap</code>, which now holds entities), and the <code>max_monsters_per_room</code>, so that we know how many monsters to make.</p>
|
||
<p>While we haven’t written the function yet, let’s place our call to it in <code>generate_dungeon</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> 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">+ place_entities(new_room, dungeon, max_monsters_per_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> ...
|
||
dungeon.tiles[x, y] = tile_types.floor
|
||
|
||
<span class="new-text">place_entities(new_room, dungeon, max_monsters_per_room)</span>
|
||
|
||
# Finally, append the new room to the list.
|
||
rooms.append(new_room)
|
||
|
||
return dungeon</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Now, let’s write the <code>place_entities</code> function so that this actually works.</p>
|
||
<p>Our first version of <code>place_entities</code> won’t actually place
|
||
the entities. Why not? Because we’ll need to do a few other things to
|
||
make spawning the entities here work. However, we can at least fill in
|
||
most of the function, and skip over the part that actually creates the
|
||
entities for the moment.</p>
|
||
<p>Create the function 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>class RectangularRoom:
|
||
</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 place_entities(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room: RectangularRoom, dungeon: GameMap, maximum_monsters: 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">+ number_of_monsters = random.randint(0, maximum_monsters)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for i in range(number_of_monsters):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x = random.randint(room.x1 + 1, room.x2 - 1)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y = random.randint(room.y1 + 1, room.y2 - 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">+ if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if random.random() < 0.8:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ pass # TODO: Place an Orc here
|
||
</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">+ pass # TODO: Place a Troll 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>def tunnel_between(
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class RectangularRoom:
|
||
...
|
||
|
||
|
||
<span class="new-text">def place_entities(
|
||
room: RectangularRoom, dungeon: GameMap, maximum_monsters: int,
|
||
) -> None:
|
||
number_of_monsters = random.randint(0, maximum_monsters)
|
||
|
||
for i in range(number_of_monsters):
|
||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||
|
||
if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
|
||
if random.random() < 0.8:
|
||
pass # TODO: Place an Orc here
|
||
else:
|
||
pass # TODO: Place a Troll here</span>
|
||
|
||
|
||
def tunnel_between(
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>The first line in the function takes a random number between 0 and
|
||
the provided maximum (2, in this case). From there, it iterates from 0
|
||
to the number.</p>
|
||
<p>We select a random <code>x</code> and <code>y</code> to place the
|
||
entity, and do a quick check to make sure there’s no other entities in
|
||
that location before dropping the enemy there. This is to ensure we
|
||
don’t get stacks of enemies.</p>
|
||
<p>As described earlier, there should be an 80% chance of there being an Orc, and 20% chance for a Troll. For now, we’re using <code>pass</code> to skip over actually putting them down, because that requires a bit more work first.</p>
|
||
<p>There’s a few ways we could go about creating the new entities.
|
||
Assuming that every Orc and Troll we spawn will always have the same
|
||
attributes as their brethren, we can create initial instances of <code>orc</code> and <code>troll</code>, then copy those every time we want to create a new one.</p>
|
||
<p>Why not just create the entities right here in the function? We could
|
||
(the 1st version of this tutorial does, in fact), but that’s a bit of a
|
||
pain to go back and edit. Imagine if you had 100 enemies in your game
|
||
at some point in the future. Would you rather search for those entity
|
||
definitions in one file that <em>only</em> exists to define entities, or
|
||
try finding it in the file that generates our dungeon? Not to mention,
|
||
what happens if you want to create a new dungeon generator? Are you
|
||
going to copy over the entity definitions and have them defined in two
|
||
places?</p>
|
||
<p>Let’s modify <code>Entity</code> to prepare for this new copying method. Modify <code>entity.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><span style="color:#a6e22e">+from __future__ import annotations
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import copy
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">-from typing import Tuple
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Tuple, TypeVar, TYPE_CHECKING
|
||
</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 TYPE_CHECKING:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from game_map import GameMap
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+T = TypeVar("T", bound="Entity")
|
||
</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 Entity:
|
||
</span></span><span style="display:flex;"><span> """
|
||
</span></span><span style="display:flex;"><span> A generic object to represent players, enemies, items, etc.
|
||
</span></span><span style="display:flex;"><span> """
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def __init__(self, x: int, y: int, char: str, color: Tuple[int, int, int]):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></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">+ x: int = 0,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y: int = 0,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ char: str = "?",
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ color: Tuple[int, int, int] = (255, 255, 255),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ name: str = "<Unnamed>",
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ blocks_movement: bool = False,
|
||
</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> self.x = x
|
||
</span></span><span style="display:flex;"><span> self.y = y
|
||
</span></span><span style="display:flex;"><span> self.char = char
|
||
</span></span><span style="display:flex;"><span> self.color = color
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.name = name
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.blocks_movement = blocks_movement
|
||
</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 spawn(self: T, gamemap: GameMap, x: int, y: int) -> T:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Spawn a copy of this instance at the given location."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ clone = copy.deepcopy(self)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ clone.x = x
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ clone.y = y
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ gamemap.entities.add(clone)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return clone
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> def move(self, dx: int, dy: int) -> None:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre><span class="new-text">from __future__ import annotations
|
||
|
||
import copy</span>
|
||
<span class="crossed-out-text">from typing import Tuple</span>
|
||
<span class="new-text">from typing import Tuple, TypeVar, TYPE_CHECKING
|
||
|
||
if TYPE_CHECKING:
|
||
from game_map import GameMap
|
||
|
||
T = TypeVar("T", bound="Entity")</span>
|
||
|
||
|
||
class Entity:
|
||
"""
|
||
A generic object to represent players, enemies, items, etc.
|
||
"""
|
||
<span class="crossed-out-text">def __init__(self, x: int, y: int, char: str, color: Tuple[int, int, int]):</span>
|
||
<span class="new-text">def __init__(
|
||
self,
|
||
x: int = 0,
|
||
y: int = 0,
|
||
char: str = "?",
|
||
color: Tuple[int, int, int] = (255, 255, 255),
|
||
name: str = "<Unnamed>",
|
||
blocks_movement: bool = False,
|
||
):</span>
|
||
self.x = x
|
||
self.y = y
|
||
self.char = char
|
||
self.color = color
|
||
<span class="new-text">self.name = name
|
||
self.blocks_movement = blocks_movement</span>
|
||
|
||
<span class="new-text">def spawn(self: T, gamemap: GameMap, x: int, y: int) -> T:
|
||
"""Spawn a copy of this instance at the given location."""
|
||
clone = copy.deepcopy(self)
|
||
clone.x = x
|
||
clone.y = y
|
||
gamemap.entities.add(clone)
|
||
return clone</span>
|
||
|
||
def move(self, dx: int, dy: int) -> None:
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>We’ve added two new attributes to <code>Entity</code>: <code>name</code> and <code>blocks_movement</code>. <code>name</code> is straightforward: it’s what the Entity is called. <code>blocks_movement</code> describes whether or not this <code>Entity</code> can be moved over or not. Enemies will have <code>blocks_movement</code> set to <code>True</code>, while in the future, things like consumable items and equipment will be set to <code>False</code>.</p>
|
||
<p>Notice that we’ve also provided defaults for each of the attributes in the <code>__init__</code> function as well, whereas we were not before. This is because we’ll soon not need to pass <code>x</code> and <code>y</code> during the initialization. More on that in a second.</p>
|
||
<p>The more complex section is the <code>spawn</code> method. It takes the <code>GameMap</code> instance, along with <code>x</code> and <code>y</code> for locations. It then creates a <code>clone</code> of the instance of <code>Entity</code>, and assigns the <code>x</code> and <code>y</code> variables to it (this is why we don’t need <code>x</code> and <code>y</code> in the initializer anymore, they’re set here). It then adds the entity to the <code>gamemap</code>’s entities, and returns the <code>clone</code>.</p>
|
||
<p>This new <code>spawn</code> method will probably make a lot more sense by putting it to use. To do that, let’s create a new file, called <code>entity_factories.py</code>, and fill it with the following contents:</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> entity <span style="color:#f92672">import</span> Entity
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>player <span style="color:#f92672">=</span> Entity(char<span style="color:#f92672">=</span><span style="color:#e6db74">"@"</span>, color<span style="color:#f92672">=</span>(<span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">255</span>), name<span style="color:#f92672">=</span><span style="color:#e6db74">"Player"</span>, blocks_movement<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>orc <span style="color:#f92672">=</span> Entity(char<span style="color:#f92672">=</span><span style="color:#e6db74">"o"</span>, color<span style="color:#f92672">=</span>(<span style="color:#ae81ff">63</span>, <span style="color:#ae81ff">127</span>, <span style="color:#ae81ff">63</span>), name<span style="color:#f92672">=</span><span style="color:#e6db74">"Orc"</span>, blocks_movement<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
|
||
</span></span><span style="display:flex;"><span>troll <span style="color:#f92672">=</span> Entity(char<span style="color:#f92672">=</span><span style="color:#e6db74">"T"</span>, color<span style="color:#f92672">=</span>(<span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">127</span>, <span style="color:#ae81ff">0</span>), name<span style="color:#f92672">=</span><span style="color:#e6db74">"Troll"</span>, blocks_movement<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
|
||
</span></span></code></pre></div><p>This is where we’re defining our entities. <code>player</code> should look familiar, and <code>orc</code> and <code>troll</code> are not all that different, besides their characters and colors.</p>
|
||
<p>These are the instances we’ll be cloning to create our new entities. Using these, we can at last fill in our <code>place_entities</code> function back 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>import tcod
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import entity_factories
|
||
</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>
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span> if random.random() < 0.8:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- pass # TODO: Place an Orc here
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ entity_factories.orc.spawn(dungeon, x, y)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> else:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- pass # TODO: Place a Troll here
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ entity_factories.troll.spawn(dungeon, x, y)
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>...
|
||
import tcod
|
||
|
||
<span class="new-text">import entity_factories</span>
|
||
from game_map import GameMap
|
||
...
|
||
|
||
...
|
||
if random.random() < 0.8:
|
||
<span class="crossed-out-text">pass # TODO: Place an Orc here</span>
|
||
<span class="new-text">entity_factories.orc.spawn(dungeon, x, y)</span>
|
||
else:
|
||
<span class="crossed-out-text">pass # TODO: Place a Troll here</span>
|
||
<span class="new-text">entity_factories.troll.spawn(dungeon, x, y)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Let’s also modify the way we create the <code>player</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>#!/usr/bin/env python3
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import copy
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>import tcod
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>from engine import Engine
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from entity import Entity
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+import entity_factories
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from input_handlers import EventHandler
|
||
</span></span><span style="display:flex;"><span>from procgen import generate_dungeon
|
||
</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> event_handler = EventHandler()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ player = copy.deepcopy(entity_factories.player)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> game_map = generate_dungeon(
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>#!/usr/bin/env python3
|
||
<span class="new-text">import copy</span>
|
||
|
||
import tcod
|
||
|
||
from engine import Engine
|
||
<span class="crossed-out-text">from entity import Entity</span>
|
||
<span class="new-text">import entity_factories</span>
|
||
from input_handlers import EventHandler
|
||
from procgen import generate_dungeon
|
||
...
|
||
|
||
...
|
||
event_handler = EventHandler()
|
||
|
||
<span class="crossed-out-text">player = Entity(int(screen_width / 2), int(screen_height / 2), "@", (255, 255, 255))</span>
|
||
<span class="new-text">player = copy.deepcopy(entity_factories.player)</span>
|
||
|
||
game_map = generate_dungeon(
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><em>Note: We can’t use <code>player.spawn</code> here, because <code>spawn</code> requires the <code>GameMap</code>, which isn’t created until after we create the player.</em></p>
|
||
<p>With that, your dungeon should now be populated with enemies.</p>
|
||
<p><img src="Part%205%20-%20Placing%20Enemies%20and%20kicking%20them%20(harmlessly)%20%C2%B7%20Roguelike%20Tutorials_files/part-5-monsters.png" alt="Font File"></p>
|
||
<p>They’re… not exactly intimidating, are they? In fact, they don’t
|
||
really do much of anything right now. But that’s okay, we’ll work on
|
||
that.</p>
|
||
<p>The first step towards making our monsters scarier is making them
|
||
stand their ground… literally! The player can currently walk over (or
|
||
under) the enemies by simply moving into the same space. Let’s fix that,
|
||
and ensure that when the player tries to move towards an enemy, we
|
||
attack instead.</p>
|
||
<p>To begin, we need to determine if the space the player is trying to
|
||
move into has an Entity in it. Not just any Entity, however: we’ll check
|
||
if the Entity has “blocks_movement” set to <code>True</code>. If it does, our player can’t move there, and tries to attack instead.</p>
|
||
<p>Add the following to the map:</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 Iterable, TYPE_CHECKING
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Iterable, Optional, TYPE_CHECKING
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>import numpy as np # type: ignore
|
||
</span></span><span style="display:flex;"><span>from tcod.console import Console
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>import tile_types
|
||
</span></span><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 Entity
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>class GameMap:
|
||
</span></span><span style="display:flex;"><span> def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
|
||
</span></span><span style="display:flex;"><span> self.width, self.height = width, height
|
||
</span></span><span style="display:flex;"><span> self.entities = set(entities)
|
||
</span></span><span style="display:flex;"><span> self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see
|
||
</span></span><span style="display:flex;"><span> self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def get_blocking_entity_at_location(self, location_x: int, location_y: int) -> Optional[Entity]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for entity in self.entities:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if entity.blocks_movement and entity.x == location_x and entity.y == location_y:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return entity
|
||
</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> def in_bounds(self, x: int, y: int) -> bool:
|
||
</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 Iterable, TYPE_CHECKING</span>
|
||
<span class="new-text">from typing import Iterable, Optional, TYPE_CHECKING</span>
|
||
|
||
import numpy as np # type: ignore
|
||
from tcod.console import Console
|
||
|
||
import tile_types
|
||
|
||
if TYPE_CHECKING:
|
||
from entity import Entity
|
||
|
||
|
||
class GameMap:
|
||
def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
|
||
self.width, self.height = width, height
|
||
self.entities = set(entities)
|
||
self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")
|
||
|
||
self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see
|
||
self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before
|
||
|
||
<span class="new-text">def get_blocking_entity_at_location(self, location_x: int, location_y: int) -> Optional[Entity]:
|
||
for entity in self.entities:
|
||
if entity.blocks_movement and entity.x == location_x and entity.y == location_y:
|
||
return entity
|
||
|
||
return None</span>
|
||
|
||
def in_bounds(self, x: int, y: int) -> bool:
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>This new function iterates through all the <code>entities</code>, and if one is found that both blocks movement and occupies the given <code>location_x</code> and <code>location_y</code> coordinates, it returns that Entity. Otherwise, we return <code>None</code> instead.</p>
|
||
<p>Where can we check if a tile is occupied or not? And what do we do if it is?</p>
|
||
<p>One way to handle all this is to modify our “actions” a bit. Our current <code>MovementAction</code>
|
||
doesn’t take into account what occupies the tile we’re moving into.
|
||
That’s fine, it doesn’t necessarily need to, but there probably should
|
||
be an action that does. What if we created an <code>Action</code> subclass that could tell what was in the tile, and call either <code>MovementAction</code> if it was empty, or some other “attack” action if it wasn’t?</p>
|
||
<p>Let’s do a few things. We’ll start by defining a new class, called <code>ActionWithDirection</code>, which will actually become the new superclass for <code>MovementAction</code>. This new class will take the initializer from <code>MovementAction</code>, but won’t implement its own <code>perform</code> method. It looks 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>class EscapeAction(Action):
|
||
</span></span><span style="display:flex;"><span> def perform(self, engine: Engine, entity: Entity) -> None:
|
||
</span></span><span style="display:flex;"><span> raise SystemExit()
|
||
</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 ActionWithDirection(Action):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def __init__(self, dx: int, dy: int):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ super().__init__()
|
||
</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.dx = dx
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.dy = dy
|
||
</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 perform(self, engine: Engine, entity: Entity) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise NotImplementedError()
|
||
</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><span style="color:#f92672">-class MovementAction(Action):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+class MovementAction(ActionWithDirection):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- def __init__(self, dx: int, dy: int):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- super().__init__()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.dx = dx
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.dy = dy
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span> def perform(self, engine: Engine, entity: Entity) -> None:
|
||
</span></span><span style="display:flex;"><span> dest_x = entity.x + self.dx
|
||
</span></span><span style="display:flex;"><span> dest_y = entity.y + self.dy
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> if not engine.game_map.in_bounds(dest_x, dest_y):
|
||
</span></span><span style="display:flex;"><span> return # Destination is out of bounds.
|
||
</span></span><span style="display:flex;"><span> if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
|
||
</span></span><span style="display:flex;"><span> return # Destination is blocked by a tile.
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return # Destination is blocked by an entity.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> entity.move(self.dx, self.dy)
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>...
|
||
class EscapeAction(Action):
|
||
def perform(self, engine: Engine, entity: Entity) -> None:
|
||
raise SystemExit()
|
||
|
||
|
||
<span class="new-text">class ActionWithDirection(Action):
|
||
def __init__(self, dx: int, dy: int):
|
||
super().__init__()
|
||
|
||
self.dx = dx
|
||
self.dy = dy
|
||
|
||
def perform(self, engine: Engine, entity: Entity) -> None:
|
||
raise NotImplementedError()</span>
|
||
|
||
|
||
<span class="crossed-out-text">class MovementAction(Action):</span>
|
||
<span class="new-text">class MovementAction(ActionWithDirection):</span>
|
||
<span class="crossed-out-text">def __init__(self, dx: int, dy: int):</span>
|
||
<span class="crossed-out-text">super().__init__()</span>
|
||
|
||
<span class="crossed-out-text">self.dx = dx</span>
|
||
<span class="crossed-out-text">self.dy = dy</span>
|
||
|
||
def perform(self, engine: Engine, entity: Entity) -> None:
|
||
dest_x = entity.x + self.dx
|
||
dest_y = entity.y + self.dy
|
||
|
||
if not engine.game_map.in_bounds(dest_x, dest_y):
|
||
return # Destination is out of bounds.
|
||
if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
|
||
return # Destination is blocked by a tile.
|
||
<span class="new-text">if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
|
||
return # Destination is blocked by an entity.</span>
|
||
|
||
entity.move(self.dx, self.dy)</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Notice that we’ve added an extra check in <code>MovementAction</code>
|
||
to ensure we’re not moving into a space with a blocking entity.
|
||
Theoretically, this bit of code won’t ever trigger, but it’s nice to
|
||
have it there as a safeguard.</p>
|
||
<p>But wait, <code>MovementAction</code> still doesn’t do anything differently. So what’s the point? Well, now we can use the new <code>ActionWithDirection</code> class to define two more subclasses, which will do what we want.</p>
|
||
<p>The first one will be the action we use to actually attack. It looks 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>class ActionWithDirection(Action):
|
||
</span></span><span style="display:flex;"><span> def __init__(self, dx: int, dy: int):
|
||
</span></span><span style="display:flex;"><span> super().__init__()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self.dx = dx
|
||
</span></span><span style="display:flex;"><span> self.dy = dy
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> def perform(self, engine: Engine, entity: Entity) -> None:
|
||
</span></span><span style="display:flex;"><span> raise NotImplementedError()
|
||
</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 MeleeAction(ActionWithDirection):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def perform(self, engine: Engine, entity: Entity) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dest_x = entity.x + self.dx
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dest_y = entity.y + self.dy
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ target = engine.game_map.get_blocking_entity_at_location(dest_x, dest_y)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if not target:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return # No entity to attack.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ print(f"You kick the {target.name}, much to its annoyance!")
|
||
</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 MovementAction(ActionWithDirection):
|
||
</span></span><span style="display:flex;"><span> def perform(self, engine: Engine, entity: Entity) -> None:
|
||
</span></span><span style="display:flex;"><span> dest_x = entity.x + self.dx
|
||
</span></span><span style="display:flex;"><span> dest_y = entity.y + self.dy
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> if not engine.game_map.in_bounds(dest_x, dest_y):
|
||
</span></span><span style="display:flex;"><span> return # Destination is out of bounds.
|
||
</span></span><span style="display:flex;"><span> if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
|
||
</span></span><span style="display:flex;"><span> return # Destination is blocked by a tile.
|
||
</span></span><span style="display:flex;"><span> if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y)
|
||
</span></span><span style="display:flex;"><span> return # Destination is blocked by an entity.
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> entity.move(self.dx, self.dy)
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class ActionWithDirection(Action):
|
||
def __init__(self, dx: int, dy: int):
|
||
super().__init__()
|
||
|
||
self.dx = dx
|
||
self.dy = dy
|
||
|
||
def perform(self, engine: Engine, entity: Entity) -> None:
|
||
raise NotImplementedError()
|
||
|
||
|
||
<span class="new-text">class MeleeAction(ActionWithDirection):
|
||
def perform(self, engine: Engine, entity: Entity) -> None:
|
||
dest_x = entity.x + self.dx
|
||
dest_y = entity.y + self.dy
|
||
target = engine.game_map.get_blocking_entity_at_location(dest_x, dest_y)
|
||
if not target:
|
||
return # No entity to attack.
|
||
|
||
print(f"You kick the {target.name}, much to its annoyance!")</span>
|
||
|
||
|
||
class MovementAction(ActionWithDirection):
|
||
def perform(self, engine: Engine, entity: Entity) -> None:
|
||
dest_x = entity.x + self.dx
|
||
dest_y = entity.y + self.dy
|
||
|
||
if not engine.game_map.in_bounds(dest_x, dest_y):
|
||
return # Destination is out of bounds.
|
||
if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
|
||
return # Destination is blocked by a tile.
|
||
if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
|
||
return # Destination is blocked by an entity.
|
||
|
||
entity.move(self.dx, self.dy)</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Just like <code>MovementAction</code>, <code>MeleeAction</code> inherits from <code>ActionWithDirection</code>. The <code>perform</code>
|
||
method it implements is what we’ll use to attack… eventually. Right
|
||
now, we’re just printing out a little message. The actual attacking will
|
||
have to wait until the next part (this one is getting long as it is).</p>
|
||
<p>Still, we’re not actually <em>using</em> <code>MeleeAction</code>
|
||
anywhere, yet. Let’s add one more class, which is what will make the
|
||
determination on whether our player is moving or attacking:</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 MovementAction(ActionWithDirection):
|
||
</span></span><span style="display:flex;"><span> def perform(self, engine: Engine, entity: Entity) -> None:
|
||
</span></span><span style="display:flex;"><span> dest_x = entity.x + self.dx
|
||
</span></span><span style="display:flex;"><span> dest_y = entity.y + self.dy
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> if not engine.game_map.in_bounds(dest_x, dest_y):
|
||
</span></span><span style="display:flex;"><span> return # Destination is out of bounds.
|
||
</span></span><span style="display:flex;"><span> if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
|
||
</span></span><span style="display:flex;"><span> return # Destination is blocked by a tile.
|
||
</span></span><span style="display:flex;"><span> if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
|
||
</span></span><span style="display:flex;"><span> return # Destination is blocked by an entity.
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> entity.move(self.dx, self.dy)
|
||
</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 BumpAction(ActionWithDirection):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def perform(self, engine: Engine, entity: Entity) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dest_x = entity.x + self.dx
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dest_y = entity.y + self.dy
|
||
</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 engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return MeleeAction(self.dx, self.dy).perform(engine, entity)
|
||
</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">+ return MovementAction(self.dx, self.dy).perform(engine, entity)
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class MovementAction(ActionWithDirection):
|
||
def perform(self, engine: Engine, entity: Entity) -> None:
|
||
dest_x = entity.x + self.dx
|
||
dest_y = entity.y + self.dy
|
||
|
||
if not engine.game_map.in_bounds(dest_x, dest_y):
|
||
return # Destination is out of bounds.
|
||
if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
|
||
return # Destination is blocked by a tile.
|
||
if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
|
||
return # Destination is blocked by an entity.
|
||
|
||
entity.move(self.dx, self.dy)
|
||
|
||
|
||
<span class="new-text">class BumpAction(ActionWithDirection):
|
||
def perform(self, engine: Engine, entity: Entity) -> None:
|
||
dest_x = entity.x + self.dx
|
||
dest_y = entity.y + self.dy
|
||
|
||
if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
|
||
return MeleeAction(self.dx, self.dy).perform(engine, entity)
|
||
|
||
else:
|
||
return MovementAction(self.dx, self.dy).perform(engine, entity)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>This class also inherits from <code>ActionWithDirection</code>, but its <code>perform</code> method doesn’t actually perform anything, except deciding which class, between <code>MeleeAction</code> and <code>MovementAction</code> to return. Those classes are what are actually doing the work. <code>BumpAction</code>
|
||
just determines which one is appropriate to call, based on whether
|
||
there is a blocking entity at the given destination or not. Notice we’re
|
||
using the function we defined earlier in our map to decide if there’s a
|
||
valid target or not.</p>
|
||
<p>Now that our new actions are in place, we need to modify our <code>input_handlers.py</code> file to use <code>BumpAction</code> instead of <code>MovementAction</code>. It’s a pretty simple change:</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 typing import Optional
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>import tcod.event
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from actions import Action, EscapeAction, MovementAction
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from actions import Action, BumpAction, EscapeAction
|
||
</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 EventHandler(tcod.event.EventDispatch[Action]):
|
||
</span></span><span style="display:flex;"><span> def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
|
||
</span></span><span style="display:flex;"><span> raise SystemExit()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||
</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></span><span style="display:flex;"><span> if key == tcod.event.K_UP:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action = MovementAction(dx=0, dy=-1)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(dx=0, dy=-1)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> elif key == tcod.event.K_DOWN:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action = MovementAction(dx=0, dy=1)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(dx=0, dy=1)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> elif key == tcod.event.K_LEFT:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action = MovementAction(dx=-1, dy=0)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(dx=-1, dy=0)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> elif key == tcod.event.K_RIGHT:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action = MovementAction(dx=1, dy=0)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(dx=1, dy=0)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> elif key == tcod.event.K_ESCAPE:
|
||
</span></span><span style="display:flex;"><span> action = EscapeAction()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> # No valid key was pressed
|
||
</span></span><span style="display:flex;"><span> return action
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>from typing import Optional
|
||
|
||
import tcod.event
|
||
|
||
<span class="crossed-out-text">from actions import Action, EscapeAction, MovementAction</span>
|
||
<span class="new-text">from actions import Action, BumpAction, EscapeAction</span>
|
||
|
||
|
||
class EventHandler(tcod.event.EventDispatch[Action]):
|
||
def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
|
||
raise SystemExit()
|
||
|
||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||
action: Optional[Action] = None
|
||
|
||
key = event.sym
|
||
|
||
if key == tcod.event.K_UP:
|
||
<span class="crossed-out-text">action = MovementAction(dx=0, dy=-1)</span>
|
||
<span class="new-text">action = BumpAction(dx=0, dy=-1)</span>
|
||
elif key == tcod.event.K_DOWN:
|
||
<span class="crossed-out-text">action = MovementAction(dx=0, dy=1)</span>
|
||
<span class="new-text">action = BumpAction(dx=0, dy=1)</span>
|
||
elif key == tcod.event.K_LEFT:
|
||
<span class="crossed-out-text">action = MovementAction(dx=-1, dy=0)</span>
|
||
<span class="new-text">action = BumpAction(dx=-1, dy=0)</span>
|
||
elif key == tcod.event.K_RIGHT:
|
||
<span class="crossed-out-text">action = MovementAction(dx=1, dy=0)</span>
|
||
<span class="new-text">action = BumpAction(dx=1, dy=0)</span>
|
||
|
||
elif key == tcod.event.K_ESCAPE:
|
||
action = EscapeAction()
|
||
|
||
# No valid key was pressed
|
||
return action</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Run the project now. At this point, you shouldn’t be able to move
|
||
over the enemies, and you should get a message in the terminal,
|
||
indicating that you’re attacking the enemy (albeit not for any damage).</p>
|
||
<p>Before we wrap this part up, let’s set ourselves up to allow for
|
||
enemy turns as well. They won’t actually be doing anything at the
|
||
moment, we’ll just get a message in the terminal that indicates
|
||
something is happening.</p>
|
||
<p>Add these small modifications to <code>engine.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 Engine:
|
||
</span></span><span style="display:flex;"><span> def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):
|
||
</span></span><span style="display:flex;"><span> self.event_handler = event_handler
|
||
</span></span><span style="display:flex;"><span> self.game_map = game_map
|
||
</span></span><span style="display:flex;"><span> self.player = player
|
||
</span></span><span style="display:flex;"><span> self.update_fov()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def handle_enemy_turns(self) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for entity in self.game_map.entities - {self.player}:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ print(f'The {entity.name} wonders when it will get to take a real turn.')
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> def handle_events(self, events: Iterable[Any]) -> None:
|
||
</span></span><span style="display:flex;"><span> for event in events:
|
||
</span></span><span style="display:flex;"><span> action = self.event_handler.dispatch(event)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> if action is None:
|
||
</span></span><span style="display:flex;"><span> continue
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> action.perform(self, self.player)
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.handle_enemy_turns()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.update_fov() # Update the FOV before the players next action.
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class Engine:
|
||
def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):
|
||
self.event_handler = event_handler
|
||
self.game_map = game_map
|
||
self.player = player
|
||
self.update_fov()
|
||
|
||
<span class="new-text">def handle_enemy_turns(self) -> None:
|
||
for entity in self.game_map.entities - {self.player}:
|
||
print(f'The {entity.name} wonders when it will get to take a real turn.')</span>
|
||
|
||
def handle_events(self, events: Iterable[Any]) -> None:
|
||
for event in events:
|
||
action = self.event_handler.dispatch(event)
|
||
|
||
if action is None:
|
||
continue
|
||
|
||
action.perform(self, self.player)
|
||
<span class="new-text">self.handle_enemy_turns()</span>
|
||
self.update_fov() # Update the FOV before the players next action.</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>The <code>handle_enemy_turns</code> function loops through each
|
||
entity (minus the player) and prints out a message for them. In the next
|
||
part, we’ll replace this with some code that will allow those entities
|
||
to take real turns.</p>
|
||
<p>We call <code>handle_enemy_turns</code> right after <code>action.perform</code>,
|
||
so that the enemies move right after the player. Other roguelike games
|
||
have more complex timing mechanisms for when entities take their turns,
|
||
but our tutorial will stick with probably the simplest method of all:
|
||
the player moves, then all the enemies move.</p>
|
||
<p>That’s all for this chapter. Next time, we’ll look at moving the
|
||
enemies around on their turns, and doing some real damage to both the
|
||
enemies and the player.</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-5">click
|
||
here</a>.</p>
|
||
<p><a href="https://rogueliketutorials.com/tutorials/tcod/v2/part-6">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%205%20-%20Placing%20Enemies%20and%20kicking%20them%20(harmlessly)%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.236049395dc3682fb2719640872958e12f1f24067bb09c327b2.js" integrity="sha256-I2BJOV3DaC+ycZZAhylY4S8fJAZ7sJwyeyM+YpDH7aw="></script>
|
||
|
||
|
||
|
||
|
||
|
||
<script src="Part%205%20-%20Placing%20Enemies%20and%20kicking%20them%20(harmlessly)%20%C2%B7%20Roguelike%20Tutorials_files/codetabs.min.cc52451e7f25e50f64c1c893826f606d58410d742c214dce.js" integrity="sha256-zFJFHn8l5Q9kwciTgm9gbVhBDXQsIU3OI/tEfJlh8rA="></script>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
</body></html> |