3061 lines
188 KiB
HTML
3061 lines
188 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 6 - Doing (and taking) some damage · 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="Check your TCOD installation Link to heading Before proceeding any further, you’ll want to upgrade to TCOD version 11.15, if you don’t already have it. This version of TCOD was released during the tutorial event, so if you’re following along on a weekly basis, you probably don’t have this version installed!
|
||
Refactoring previous code Link to heading After parts 1-5 for this tutorial were written, we decided to change a few things around, to hopefully make the codebase a bit cleaner and easier to extend in the future.">
|
||
<meta name="keywords" content="">
|
||
|
||
<meta name="twitter:card" content="summary">
|
||
<meta name="twitter:title" content="Part 6 - Doing (and taking) some damage">
|
||
<meta name="twitter:description" content="Check your TCOD installation Link to heading Before proceeding any further, you’ll want to upgrade to TCOD version 11.15, if you don’t already have it. This version of TCOD was released during the tutorial event, so if you’re following along on a weekly basis, you probably don’t have this version installed!
|
||
Refactoring previous code Link to heading After parts 1-5 for this tutorial were written, we decided to change a few things around, to hopefully make the codebase a bit cleaner and easier to extend in the future.">
|
||
|
||
<meta property="og:title" content="Part 6 - Doing (and taking) some damage">
|
||
<meta property="og:description" content="Check your TCOD installation Link to heading Before proceeding any further, you’ll want to upgrade to TCOD version 11.15, if you don’t already have it. This version of TCOD was released during the tutorial event, so if you’re following along on a weekly basis, you probably don’t have this version installed!
|
||
Refactoring previous code Link to heading After parts 1-5 for this tutorial were written, we decided to change a few things around, to hopefully make the codebase a bit cleaner and easier to extend in the future.">
|
||
<meta property="og:type" content="article">
|
||
<meta property="og:url" content="https://rogueliketutorials.com/tutorials/tcod/v2/part-6/"><meta property="article:section" content="tutorials">
|
||
<meta property="article:published_time" content="2020-07-07T00:00:00+00:00">
|
||
<meta property="article:modified_time" content="2020-07-07T00:00:00+00:00">
|
||
|
||
|
||
|
||
|
||
<link rel="canonical" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-6/">
|
||
|
||
|
||
<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%206%20-%20Doing%20(and%20taking)%20some%20damage%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.c4d7e93a158eda5a65b3df343745d2092a0a1e2170feeec909.css" integrity="sha256-xNfpOhWO2lpls980N0XSCSoKHiFw/u7JCbiolEOQPGo=" crossorigin="anonymous" media="screen">
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<link rel="stylesheet" href="Part%206%20-%20Doing%20(and%20taking)%20some%20damage%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%206%20-%20Doing%20(and%20taking)%20some%20damage%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-6/">
|
||
Part 6 - Doing (and taking) some damage
|
||
</a>
|
||
</h1>
|
||
</header>
|
||
|
||
<h2 id="check-your-tcod-installation">
|
||
Check your TCOD installation
|
||
<a class="heading-link" href="#check-your-tcod-installation">
|
||
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
|
||
<span class="sr-only">Link to heading</span>
|
||
</a>
|
||
</h2>
|
||
<p>Before proceeding any further, you’ll want to upgrade to TCOD version
|
||
11.15, if you don’t already have it. This version of TCOD was released <em>during</em> the tutorial event, so if you’re following along on a weekly basis, you probably <em>don’t</em> have this version installed!</p>
|
||
<h2 id="refactoring-previous-code">
|
||
Refactoring previous code
|
||
<a class="heading-link" href="#refactoring-previous-code">
|
||
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
|
||
<span class="sr-only">Link to heading</span>
|
||
</a>
|
||
</h2>
|
||
<p>After parts 1-5 for this tutorial were written, we decided to change a
|
||
few things around, to hopefully make the codebase a bit cleaner and
|
||
easier to extend in the future. Unfortunately, this means that code
|
||
written in previous parts now has to be modified.</p>
|
||
<p>I would go back and edit the tutorial text and Github branches to reflect these changes, except for two things:</p>
|
||
<ol>
|
||
<li>I don’t have time at the moment. Writing the sections that get published every week is taking all of my time as it is.</li>
|
||
<li>It wouldn’t be fair to those who are following this tutorial on a weekly basis.</li>
|
||
</ol>
|
||
<p>Someday, when the event is over, the previous parts will be
|
||
rewritten, and all will be well. But until then, there’s several changes
|
||
that need to be made before proceeding with Part 6.</p>
|
||
<p>I won’t explain all of the changes (again, time is a limiting factor), but here’s the basic ideas:</p>
|
||
<ul>
|
||
<li>Event handlers will have the <code>handle_events</code> method instead of <code>Engine</code>.</li>
|
||
<li>The game map will have a reference to <code>Engine</code>, and entities will have a reference to the map.</li>
|
||
<li>Actions will be initialized with the entity doing the action</li>
|
||
<li>Because of the above points, Actions will have a reference to the <code>Engine</code>, through <code>Entity</code>-><code>GameMap</code>-><code>Engine</code></li>
|
||
</ul>
|
||
<p>Make the changes to each file, and when you’re finished, verify the project works as it did before.</p>
|
||
<p><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><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:#f92672">-from typing import Optional
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Optional, TYPE_CHECKING
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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>from actions import Action, BumpAction, EscapeAction
|
||
</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 engine import Engine
|
||
</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><span style="color:#a6e22e">+ def __init__(self, engine: Engine):
|
||
</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">+ def handle_events(self) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for event in tcod.event.wait():
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ action = self.dispatch(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">+ if action is None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ continue
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ action.perform()
|
||
</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.handle_enemy_turns()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.update_fov() # Update the FOV before the players next action.
|
||
</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 ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</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><span style="color:#a6e22e">+ player = self.engine.player
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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 = BumpAction(dx=0, dy=-1)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(player, 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 = BumpAction(dx=0, dy=1)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(player, 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 = BumpAction(dx=-1, dy=0)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(player, 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 = BumpAction(dx=1, dy=0)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = BumpAction(player, 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><span style="color:#f92672">- action = EscapeAction()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ action = EscapeAction(player)
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre><span class="new-text">from __future__ import annotations</span>
|
||
|
||
<span class="crossed-out-text">from typing import Optional</span>
|
||
<span class="new-text">from typing import Optional, TYPE_CHECKING</span>
|
||
|
||
import tcod.event
|
||
|
||
from actions import Action, BumpAction, EscapeAction
|
||
|
||
<span class="new-text">if TYPE_CHECKING:
|
||
from engine import Engine</span>
|
||
|
||
|
||
class EventHandler(tcod.event.EventDispatch[Action]):
|
||
<span class="new-text">def __init__(self, engine: Engine):
|
||
self.engine = engine
|
||
|
||
def handle_events(self) -> None:
|
||
for event in tcod.event.wait():
|
||
action = self.dispatch(event)
|
||
|
||
if action is None:
|
||
continue
|
||
|
||
action.perform()
|
||
|
||
self.engine.handle_enemy_turns()
|
||
self.engine.update_fov() # Update the FOV before the players next action.</span>
|
||
|
||
|
||
def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
|
||
...
|
||
|
||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||
action: Optional[Action] = None
|
||
|
||
key = event.sym
|
||
|
||
<span class="new-text">player = self.engine.player</span>
|
||
|
||
if key == tcod.event.K_UP:
|
||
<span class="crossed-out-text">action = BumpAction(dx=0, dy=-1)</span>
|
||
<span class="new-text">action = BumpAction(player, dx=0, dy=-1)</span>
|
||
elif key == tcod.event.K_DOWN:
|
||
<span class="crossed-out-text">action = BumpAction(dx=0, dy=1)</span>
|
||
<span class="new-text">action = BumpAction(player, dx=0, dy=1)</span>
|
||
elif key == tcod.event.K_LEFT:
|
||
<span class="crossed-out-text">action = BumpAction(dx=-1, dy=0)</span>
|
||
<span class="new-text">action = BumpAction(player, dx=-1, dy=0)</span>
|
||
elif key == tcod.event.K_RIGHT:
|
||
<span class="crossed-out-text">action = BumpAction(dx=1, dy=0)</span>
|
||
<span class="new-text">action = BumpAction(player, dx=1, dy=0)</span>
|
||
|
||
elif key == tcod.event.K_ESCAPE:
|
||
<span class="crossed-out-text">action = EscapeAction()</span>
|
||
<span class="new-text">action = EscapeAction(player)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><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>from __future__ import annotations
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from typing import Optional, Tuple, TYPE_CHECKING
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">-from typing import TYPE_CHECKING
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span>if TYPE_CHECKING:
|
||
</span></span><span style="display:flex;"><span> from engine import Engine
|
||
</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 Action:
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def __init__(self, entity: Entity) -> None:
|
||
</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">+ self.entity = 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">+ @property
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def engine(self) -> Engine:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Return the engine this action belongs to."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.entity.gamemap.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">+ def perform(self) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- def perform(self, engine: Engine, entity: Entity) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> """Perform this action with the objects needed to determine its scope.
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ `self.engine` is the scope this action is being performed in.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- `engine` is the scope this action is being performed in.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ `self.entity` is the object performing the action.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- `entity` is the object performing the action.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span> This method must be overridden by Action subclasses.
|
||
</span></span><span style="display:flex;"><span> """
|
||
</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>class EscapeAction(Action):
|
||
</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 style="color:#f92672">- def perform(self, engine: Engine, entity: Entity) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></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></span><span style="display:flex;"><span>class ActionWithDirection(Action):
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def __init__(self, entity: Entity, dx: int, dy: int):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ super().__init__(entity)
|
||
</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> 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><span style="color:#a6e22e">+ @property
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def dest_xy(self) -> Tuple[int, int]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Returns this actions destination."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.entity.x + self.dx, self.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">+ @property
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def blocking_entity(self) -> Optional[Entity]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Return the blocking entity at this actions destination.."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.engine.game_map.get_blocking_entity_at_location(*self.dest_xy)
|
||
</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) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- def perform(self, engine: Engine, entity: Entity) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> raise NotImplementedError()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>class MeleeAction(ActionWithDirection):
|
||
</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">+ target = self.blocking_entity
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- def perform(self, engine: Engine, entity: Entity) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- dest_x = entity.x + self.dx
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- dest_y = entity.y + self.dy
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- target = engine.game_map.get_blocking_entity_at_location(dest_x, dest_y)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> if not target:
|
||
</span></span><span style="display:flex;"><span> return # No entity to attack.
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> print(f"You kick the {target.name}, much to its annoyance!")
|
||
</span></span><span style="display:flex;"><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><span style="color:#a6e22e">+ def perform(self) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dest_x, dest_y = self.dest_xy
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- def perform(self, engine: Engine, entity: Entity) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- dest_x = entity.x + self.dx
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- dest_y = entity.y + self.dy
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if not self.engine.game_map.in_bounds(dest_x, dest_y):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- if not engine.game_map.in_bounds(dest_x, dest_y):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> return # Destination is out of bounds.
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if not self.engine.game_map.tiles["walkable"][dest_x, dest_y]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- if not engine.game_map.tiles["walkable"][dest_x, dest_y]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> return # Destination is blocked by a tile.
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> return # Destination is blocked by an entity.
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entity.move(self.dx, self.dy)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- entity.move(self.dx, self.dy)
|
||
</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>class BumpAction(ActionWithDirection):
|
||
</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">+ if self.blocking_entity:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return MeleeAction(self.entity, self.dx, self.dy).perform()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- def perform(self, engine: Engine, entity: Entity) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- dest_x = entity.x + self.dx
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- dest_y = entity.y + self.dy
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- return MeleeAction(self.dx, self.dy).perform(engine, entity)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span> else:
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return MovementAction(self.entity, self.dx, self.dy).perform()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- return MovementAction(self.dx, self.dy).perform(engine, entity)
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>from __future__ import annotations
|
||
|
||
<span class="new-text">from typing import Optional, Tuple, TYPE_CHECKING</span>
|
||
<span class="crossed-out-text">from typing import TYPE_CHECKING</span>
|
||
|
||
if TYPE_CHECKING:
|
||
from engine import Engine
|
||
from entity import Entity
|
||
|
||
|
||
class Action:
|
||
<span class="new-text">def __init__(self, entity: Entity) -> None:
|
||
super().__init__()
|
||
self.entity = entity
|
||
|
||
@property
|
||
def engine(self) -> Engine:
|
||
"""Return the engine this action belongs to."""
|
||
return self.entity.gamemap.engine
|
||
|
||
def perform(self) -> None:</span>
|
||
<span class="crossed-out-text">def perform(self, engine: Engine, entity: Entity) -> None:</span>
|
||
"""Perform this action with the objects needed to determine its scope.
|
||
|
||
<span class="new-text">`self.engine` is the scope this action is being performed in.</span>
|
||
<span class="crossed-out-text">`engine` is the scope this action is being performed in.</span>
|
||
|
||
<span class="new-text">`self.entity` is the object performing the action.</span>
|
||
<span class="crossed-out-text">`entity` is the object performing the action.</span>
|
||
|
||
This method must be overridden by Action subclasses.
|
||
"""
|
||
raise NotImplementedError()
|
||
|
||
|
||
class EscapeAction(Action):
|
||
<span class="new-text">def perform(self) -> None:</span>
|
||
<span class="crossed-out-text">def perform(self, engine: Engine, entity: Entity) -> None:</span>
|
||
raise SystemExit()
|
||
|
||
|
||
|
||
class ActionWithDirection(Action):
|
||
<span class="new-text">def __init__(self, entity: Entity, dx: int, dy: int):
|
||
super().__init__(entity)</span>
|
||
<span class="crossed-out-text">def __init__(self, dx: int, dy: int):</span>
|
||
<span class="crossed-out-text">super().__init__()</span>
|
||
|
||
self.dx = dx
|
||
self.dy = dy
|
||
|
||
<span class="new-text">@property
|
||
def dest_xy(self) -> Tuple[int, int]:
|
||
"""Returns this actions destination."""
|
||
return self.entity.x + self.dx, self.entity.y + self.dy
|
||
|
||
@property
|
||
def blocking_entity(self) -> Optional[Entity]:
|
||
"""Return the blocking entity at this actions destination.."""
|
||
return self.engine.game_map.get_blocking_entity_at_location(*self.dest_xy)
|
||
|
||
def perform(self) -> None:</span>
|
||
<span class="crossed-out-text">def perform(self, engine: Engine, entity: Entity) -> None:</span>
|
||
raise NotImplementedError()
|
||
|
||
|
||
class MeleeAction(ActionWithDirection):
|
||
<span class="new-text">def perform(self) -> None:
|
||
target = self.blocking_entity</span>
|
||
<span class="crossed-out-text">def perform(self, engine: Engine, entity: Entity) -> None:</span>
|
||
<span class="crossed-out-text">dest_x = entity.x + self.dx</span>
|
||
<span class="crossed-out-text">dest_y = entity.y + self.dy</span>
|
||
<span class="crossed-out-text">target = engine.game_map.get_blocking_entity_at_location(dest_x, dest_y)</span>
|
||
if not target:
|
||
return # No entity to attack.
|
||
|
||
print(f"You kick the {target.name}, much to its annoyance!")
|
||
|
||
|
||
class MovementAction(ActionWithDirection):
|
||
<span class="new-text">def perform(self) -> None:
|
||
dest_x, dest_y = self.dest_xy</span>
|
||
<span class="crossed-out-text">def perform(self, engine: Engine, entity: Entity) -> None:</span>
|
||
<span class="crossed-out-text">dest_x = entity.x + self.dx</span>
|
||
<span class="crossed-out-text">dest_y = entity.y + self.dy</span>
|
||
|
||
<span class="new-text">if not self.engine.game_map.in_bounds(dest_x, dest_y):</span>
|
||
<span class="crossed-out-text">if not engine.game_map.in_bounds(dest_x, dest_y):</span>
|
||
return # Destination is out of bounds.
|
||
<span class="new-text">if not self.engine.game_map.tiles["walkable"][dest_x, dest_y]:</span>
|
||
<span class="crossed-out-text">if not engine.game_map.tiles["walkable"][dest_x, dest_y]:</span>
|
||
return # Destination is blocked by a tile.
|
||
<span class="new-text">if self.engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):</span>
|
||
<span class="crossed-out-text">if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):</span>
|
||
return # Destination is blocked by an entity.
|
||
|
||
<span class="new-text">self.entity.move(self.dx, self.dy)</span>
|
||
<span class="crossed-out-text">entity.move(self.dx, self.dy)</span>
|
||
|
||
|
||
class BumpAction(ActionWithDirection):
|
||
<span class="new-text">def perform(self) -> None:
|
||
if self.blocking_entity:
|
||
return MeleeAction(self.entity, self.dx, self.dy).perform()</span>
|
||
<span class="crossed-out-text">def perform(self, engine: Engine, entity: Entity) -> None:</span>
|
||
<span class="crossed-out-text">dest_x = entity.x + self.dx</span>
|
||
<span class="crossed-out-text">dest_y = entity.y + self.dy</span>
|
||
|
||
<span class="crossed-out-text">if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):</span>
|
||
<span class="crossed-out-text">return MeleeAction(self.dx, self.dy).perform(engine, entity)</span>
|
||
|
||
else:
|
||
<span class="new-text">return MovementAction(self.entity, self.dx, self.dy).perform()</span>
|
||
<span class="crossed-out-text">return MovementAction(self.dx, self.dy).perform(engine, entity)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><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>from __future__ import annotations
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>from typing import Iterable, Optional, TYPE_CHECKING
|
||
</span></span><span style="display:flex;"><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><span style="color:#a6e22e">+ from engine import Engine
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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><span style="color:#f92672">- def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
|
||
</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, engine: Engine, width: int, height: int, entities: Iterable[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">+ self.engine = engine
|
||
</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> 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><span style="color:#f92672">- self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ self.visible = np.full(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ (width, height), fill_value=False, order="F"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ) # Tiles the player can currently see
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ self.explored = np.full(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ (width, height), fill_value=False, order="F"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ) # Tiles the player has seen before
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- 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:#f92672"></span><span style="color:#a6e22e">+ def get_blocking_entity_at_location(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self, location_x: int, location_y: int,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ) -> Optional[Entity]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> for entity in self.entities:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- 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:#f92672"></span><span style="color:#a6e22e">+ if (
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ entity.blocks_movement
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ and entity.x == location_x
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ and entity.y == location_y
|
||
</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> return entity
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> return None
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> def in_bounds(self, x: int, y: int) -> bool:
|
||
</span></span><span style="display:flex;"><span> """Return True if x and y are inside of the bounds of this map."""
|
||
</span></span><span style="display:flex;"><span> return 0 <= x < self.width and 0 <= y < self.height
|
||
</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><span style="color:#f92672">- console.tiles_rgb[0:self.width, 0:self.height] = np.select(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ console.tiles_rgb[0 : self.width, 0 : self.height] = np.select(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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><span style="color:#f92672">- default=tile_types.SHROUD
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ default=tile_types.SHROUD,
|
||
</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>from __future__ import annotations
|
||
|
||
from typing import Iterable, Optional, TYPE_CHECKING
|
||
|
||
import numpy as np # type: ignore
|
||
from tcod.console import Console
|
||
|
||
import tile_types
|
||
|
||
if TYPE_CHECKING:
|
||
<span class="new-text">from engine import Engine</span>
|
||
from entity import Entity
|
||
|
||
|
||
class GameMap:
|
||
<span class="crossed-out-text">def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):</span>
|
||
<span class="new-text">def __init__(
|
||
self, engine: Engine, width: int, height: int, entities: Iterable[Entity] = ()
|
||
):
|
||
self.engine = engine</span>
|
||
self.width, self.height = width, height
|
||
self.entities = set(entities)
|
||
self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")
|
||
|
||
<span class="crossed-out-text">self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see</span>
|
||
<span class="new-text">self.visible = np.full(
|
||
(width, height), fill_value=False, order="F"
|
||
) # Tiles the player can currently see</span>
|
||
<span class="crossed-out-text">self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before</span>
|
||
<span class="new-text">self.explored = np.full(
|
||
(width, height), fill_value=False, order="F"
|
||
) # Tiles the player has seen before</span>
|
||
|
||
<span class="crossed-out-text">def get_blocking_entity_at_location(self, location_x: int, location_y: int) -> Optional[Entity]:</span>
|
||
<span class="new-text">def get_blocking_entity_at_location(
|
||
self, location_x: int, location_y: int,
|
||
) -> Optional[Entity]:</span>
|
||
for entity in self.entities:
|
||
<span class="crossed-out-text">if entity.blocks_movement and entity.x == location_x and entity.y == location_y:</span>
|
||
<span class="new-text">if (
|
||
entity.blocks_movement
|
||
and entity.x == location_x
|
||
and entity.y == location_y
|
||
):</span>
|
||
return entity
|
||
|
||
return None
|
||
|
||
def in_bounds(self, x: int, y: int) -> bool:
|
||
"""Return True if x and y are inside of the bounds of this map."""
|
||
return 0 <= x < self.width and 0 <= y < self.height
|
||
|
||
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".
|
||
"""
|
||
<span class="crossed-out-text">console.tiles_rgb[0:self.width, 0:self.height] = np.select(</span>
|
||
<span class="new-text">console.tiles_rgb[0 : self.width, 0 : self.height] = np.select(</span>
|
||
condlist=[self.visible, self.explored],
|
||
choicelist=[self.tiles["light"], self.tiles["dark"]],
|
||
<span class="crossed-out-text">default=tile_types.SHROUD</span>
|
||
<span class="new-text">default=tile_types.SHROUD,</span>
|
||
)</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><code>main.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>#!/usr/bin/env python3
|
||
</span></span><span style="display:flex;"><span>import copy
|
||
</span></span><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>from engine import Engine
|
||
</span></span><span style="display:flex;"><span>import entity_factories
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from input_handlers import EventHandler
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></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 style="color:#a6e22e">+ player = copy.deepcopy(entity_factories.player)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- event_handler = EventHandler()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine = Engine(player=player)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- player = copy.deepcopy(entity_factories.player)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine.game_map = generate_dungeon(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- game_map = generate_dungeon(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></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><span style="color:#a6e22e">+ engine=engine,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- player=player,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> )
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine.update_fov()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- engine = Engine(event_handler=event_handler, game_map=game_map, player=player)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span> with tcod.context.new_terminal(
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span> while True:
|
||
</span></span><span style="display:flex;"><span> engine.render(console=root_console, context=context)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine.event_handler.handle_events()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- events = tcod.event.wait()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- engine.handle_events(events)
|
||
</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>if __name__ == "__main__":
|
||
</span></span><span style="display:flex;"><span> main()
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>#!/usr/bin/env python3
|
||
import copy
|
||
|
||
import tcod
|
||
|
||
from engine import Engine
|
||
import entity_factories
|
||
<span class="crossed-out-text">from input_handlers import EventHandler</span>
|
||
from procgen import generate_dungeon
|
||
|
||
...
|
||
<span class="new-text">player = copy.deepcopy(entity_factories.player)</span>
|
||
<span class="crossed-out-text">event_handler = EventHandler()</span>
|
||
|
||
<span class="new-text">engine = Engine(player=player)</span>
|
||
<span class="crossed-out-text">player = copy.deepcopy(entity_factories.player)</span>
|
||
|
||
<span class="new-text">engine.game_map = generate_dungeon(</span>
|
||
<span class="crossed-out-text">game_map = generate_dungeon(</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,
|
||
<span class="new-text">engine=engine,</span>
|
||
<span class="crossed-out-text">player=player,</span>
|
||
)
|
||
<span class="new-text">engine.update_fov()</span>
|
||
|
||
<span class="crossed-out-text">engine = Engine(event_handler=event_handler, game_map=game_map, player=player)</span>
|
||
|
||
with tcod.context.new_terminal(
|
||
...
|
||
while True:
|
||
engine.render(console=root_console, context=context)
|
||
|
||
<span class="new-text">engine.event_handler.handle_events()</span>
|
||
<span class="crossed-out-text">events = tcod.event.wait()</span>
|
||
|
||
<span class="crossed-out-text">engine.handle_events(events)</span>
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><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>from __future__ import annotations
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>import copy
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from typing import Tuple, TypeVar, TYPE_CHECKING
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Optional, Tuple, TypeVar, TYPE_CHECKING
|
||
</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> 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 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></span><span style="display:flex;"><span><span style="color:#a6e22e">+ gamemap: GameMap
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</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 style="color:#a6e22e">+ gamemap: Optional[GameMap] = None,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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> blocks_movement: bool = False,
|
||
</span></span><span style="display:flex;"><span> ):
|
||
</span></span><span style="display:flex;"><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> self.name = name
|
||
</span></span><span style="display:flex;"><span> self.blocks_movement = blocks_movement
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if gamemap:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # If gamemap isn't provided now then it will be set later.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.gamemap = gamemap
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ gamemap.entities.add(self)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> def spawn(self: T, gamemap: GameMap, x: int, y: int) -> T:
|
||
</span></span><span style="display:flex;"><span> """Spawn a copy of this instance at the given location."""
|
||
</span></span><span style="display:flex;"><span> clone = copy.deepcopy(self)
|
||
</span></span><span style="display:flex;"><span> clone.x = x
|
||
</span></span><span style="display:flex;"><span> clone.y = y
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ clone.gamemap = gamemap
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> gamemap.entities.add(clone)
|
||
</span></span><span style="display:flex;"><span> return clone
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def place(self, x: int, y: int, gamemap: Optional[GameMap] = None) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Place this entity at a new location. Handles moving across GameMaps."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.x = x
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.y = y
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if gamemap:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if hasattr(self, "gamemap"): # Possibly uninitialized.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.gamemap.entities.remove(self)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.gamemap = gamemap
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ gamemap.entities.add(self)
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>from __future__ import annotations
|
||
|
||
import copy
|
||
<span class="crossed-out-text">from typing import Tuple, TypeVar, TYPE_CHECKING</span>
|
||
<span class="new-text">from typing import Optional, Tuple, TypeVar, TYPE_CHECKING</span>
|
||
|
||
if TYPE_CHECKING:
|
||
from game_map import GameMap
|
||
|
||
T = TypeVar("T", bound="Entity")
|
||
|
||
|
||
class Entity:
|
||
"""
|
||
A generic object to represent players, enemies, items, etc.
|
||
"""
|
||
|
||
<span class="new-text">gamemap: GameMap</span>
|
||
|
||
def __init__(
|
||
self,
|
||
<span class="new-text">gamemap: Optional[GameMap] = None,</span>
|
||
x: int = 0,
|
||
y: int = 0,
|
||
char: str = "?",
|
||
color: Tuple[int, int, int] = (255, 255, 255),
|
||
name: str = "<Unnamed>",
|
||
blocks_movement: bool = False,
|
||
):
|
||
self.x = x
|
||
self.y = y
|
||
self.char = char
|
||
self.color = color
|
||
self.name = name
|
||
self.blocks_movement = blocks_movement
|
||
<span class="new-text">if gamemap:
|
||
# If gamemap isn't provided now then it will be set later.
|
||
self.gamemap = gamemap
|
||
gamemap.entities.add(self)</span>
|
||
|
||
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
|
||
<span class="new-text">clone.gamemap = gamemap</span>
|
||
gamemap.entities.add(clone)
|
||
return clone
|
||
|
||
<span class="new-text">def place(self, x: int, y: int, gamemap: Optional[GameMap] = None) -> None:
|
||
"""Place this entity at a new location. Handles moving across GameMaps."""
|
||
self.x = x
|
||
self.y = y
|
||
if gamemap:
|
||
if hasattr(self, "gamemap"): # Possibly uninitialized.
|
||
self.gamemap.entities.remove(self)
|
||
self.gamemap = gamemap
|
||
gamemap.entities.add(self)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><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>import tile_types
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>if TYPE_CHECKING:
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from engine import Engine
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- from entity import Entity
|
||
</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>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> max_monsters_per_room: int,
|
||
</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><span style="color:#f92672">- player: Entity,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>) -> GameMap:
|
||
</span></span><span style="display:flex;"><span> """Generate a new dungeon map."""
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player = engine.player
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dungeon = GameMap(engine, map_width, map_height, entities=[player])
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- dungeon = GameMap(map_width, map_height, entities=[player])
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></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></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span> if len(rooms) == 0:
|
||
</span></span><span style="display:flex;"><span> # The first room, where the player starts.
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player.place(*new_room.center, dungeon)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- player.x, player.y = new_room.center
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> else: # All rooms after the first.
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>import tile_types
|
||
|
||
|
||
if TYPE_CHECKING:
|
||
<span class="new-text">from engine import Engine</span>
|
||
<span class="crossed-out-text">from entity import Entity</span>
|
||
|
||
...
|
||
def generate_dungeon(
|
||
max_rooms: int,
|
||
room_min_size: int,
|
||
room_max_size: int,
|
||
map_width: int,
|
||
map_height: int,
|
||
max_monsters_per_room: int,
|
||
<span class="new-text">engine: Engine,</span>
|
||
<span class="crossed-out-text">player: Entity,</span>
|
||
) -> GameMap:
|
||
"""Generate a new dungeon map."""
|
||
<span class="new-text">player = engine.player</span>
|
||
<span class="new-text">dungeon = GameMap(engine, map_width, map_height, entities=[player])</span>
|
||
<span class="crossed-out-text">dungeon = GameMap(map_width, map_height, entities=[player])</span>
|
||
|
||
rooms: List[RectangularRoom] = []
|
||
...
|
||
|
||
...
|
||
if len(rooms) == 0:
|
||
# The first room, where the player starts.
|
||
<span class="new-text">player.place(*new_room.center, dungeon)</span>
|
||
<span class="crossed-out-text">player.x, player.y = new_room.center</span>
|
||
else: # All rooms after the first.
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><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><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 TYPE_CHECKING
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">-from typing import Iterable, Any
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span>from tcod.context import Context
|
||
</span></span><span style="display:flex;"><span>from tcod.console import Console
|
||
</span></span><span style="display:flex;"><span>from tcod.map import compute_fov
|
||
</span></span><span style="display:flex;"><span>
|
||
</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">-from game_map import GameMap
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>from input_handlers import EventHandler
|
||
</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">+ 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></span><span style="display:flex;"><span>class Engine:
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ game_map: 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">+ def __init__(self, player: Entity):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.event_handler: EventHandler = EventHandler(self)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.player = player
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.event_handler = event_handler
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.game_map = game_map
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.player = player
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.update_fov()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span> def handle_enemy_turns(self) -> None:
|
||
</span></span><span style="display:flex;"><span> for entity in self.game_map.entities - {self.player}:
|
||
</span></span><span style="display:flex;"><span> print(f'The {entity.name} wonders when it will get to take a real turn.')
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def handle_events(self, events: Iterable[Any]) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- for event in events:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- action = self.event_handler.dispatch(event)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if action is None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- continue
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action.perform(self, self.player)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.handle_enemy_turns()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.update_fov() # Update the FOV before the players next action.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span> def update_fov(self) -> 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</span>
|
||
|
||
<span class="new-text">from typing import TYPE_CHECKING</span>
|
||
<span class="crossed-out-text">from typing import Iterable, Any</span>
|
||
|
||
from tcod.context import Context
|
||
from tcod.console import Console
|
||
from tcod.map import compute_fov
|
||
|
||
<span class="crossed-out-text">from entity import Entity</span>
|
||
<span class="crossed-out-text">from game_map import GameMap</span>
|
||
from input_handlers import EventHandler
|
||
|
||
<span class="new-text">if TYPE_CHECKING:
|
||
from entity import Entity
|
||
from game_map import GameMap</span>
|
||
|
||
|
||
class Engine:
|
||
<span class="new-text">game_map: GameMap</span>
|
||
|
||
<span class="new-text">def __init__(self, player: Entity):
|
||
self.event_handler: EventHandler = EventHandler(self)
|
||
self.player = player</span>
|
||
<span class="crossed-out-text">def __init__(self, event_handler: EventHandler, game_map: GameMap, player: Entity):</span>
|
||
<span class="crossed-out-text">self.event_handler = event_handler</span>
|
||
<span class="crossed-out-text">self.game_map = game_map</span>
|
||
<span class="crossed-out-text">self.player = player</span>
|
||
<span class="crossed-out-text">self.update_fov()</span>
|
||
|
||
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 class="crossed-out-text">def handle_events(self, events: Iterable[Any]) -> None:</span>
|
||
<span class="crossed-out-text">for event in events:</span>
|
||
<span class="crossed-out-text">action = self.event_handler.dispatch(event)</span>
|
||
|
||
<span class="crossed-out-text">if action is None:</span>
|
||
<span class="crossed-out-text">continue</span>
|
||
|
||
<span class="crossed-out-text">action.perform(self, self.player)</span>
|
||
<span class="crossed-out-text">self.handle_enemy_turns()</span>
|
||
<span class="crossed-out-text">self.update_fov() # Update the FOV before the players next action.</span>
|
||
|
||
def update_fov(self) -> None:
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<h2 id="onwards-to-part-6">
|
||
Onwards to Part 6
|
||
<a class="heading-link" href="#onwards-to-part-6">
|
||
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
|
||
<span class="sr-only">Link to heading</span>
|
||
</a>
|
||
</h2>
|
||
<p>The last part of this tutorial set us up for combat, so now it’s time to actually implement it.</p>
|
||
<p>In order to make “killable” Entities, rather than attaching hit points to each Entity we create, we’ll create a <strong>component</strong>, called <code>Fighter</code>,
|
||
which will hold information related to combat, like HP, max HP, attack,
|
||
and defense. If an Entity can fight, it will have this component
|
||
attached to it, and if not, it won’t. This way of doing things is called
|
||
<strong>composition</strong>, and it’s an alternative to your typical inheritance-based programming model. (This tutorial uses both composition <em>and</em> inheritance).</p>
|
||
<p>Create a new Python package (a folder with an empty __init__.py file), called <code>components</code>. In that new directory, add two new files, one called <code>base_component.py</code>, and another called <code>fighter.py</code>. The <code>Fighter</code> class in <code>fighter.py</code> will inherit from the class we put in <code>base_component.py</code>, so let’s start with that one:</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:#66d9ef">if</span> TYPE_CHECKING:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">from</span> engine <span style="color:#f92672">import</span> Engine
|
||
</span></span><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>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BaseComponent</span>:
|
||
</span></span><span style="display:flex;"><span> entity: Entity <span style="color:#75715e"># Owning entity instance.</span>
|
||
</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">engine</span>(self) <span style="color:#f92672">-></span> Engine:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>entity<span style="color:#f92672">.</span>gamemap<span style="color:#f92672">.</span>engine
|
||
</span></span></code></pre></div><p>With that, let’s now open up <code>fighter.py</code> and put the following into 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> 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></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Fighter</span>(BaseComponent):
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> __init__(self, hp: int, defense: int, power: int):
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>max_hp <span style="color:#f92672">=</span> hp
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>_hp <span style="color:#f92672">=</span> hp
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>defense <span style="color:#f92672">=</span> defense
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>power <span style="color:#f92672">=</span> power
|
||
</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">hp</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>_hp
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@hp.setter</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">hp</span>(self, value: int) <span style="color:#f92672">-></span> <span style="color:#66d9ef">None</span>:
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>_hp <span style="color:#f92672">=</span> max(<span style="color:#ae81ff">0</span>, min(value, self<span style="color:#f92672">.</span>max_hp))
|
||
</span></span></code></pre></div><p>We import and inherit from <code>BaseComponent</code>, which gives us access to the parent entity and the engine, which will be useful later on.</p>
|
||
<p>The <code>__init__</code> function takes a few arguments. <code>hp</code> represents the entity’s hit points. <code>defense</code> is how much taken damage will be reduced. <code>power</code> is the entity’s raw attack power.</p>
|
||
<p>What’s with the <code>hp</code> property? We define both a getter and setter, which will allow the class to access <code>hp</code> like a normal variable. The getter (the one with the <code>@property</code> thing above the method) doesn’t do anything special: it just returns the HP. The <code>setter</code> (<code>@hp.setter</code>) is where things get more interesting.</p>
|
||
<p>By defining HP this way, we can modify the value as it’s set within the method. This line:</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> self<span style="color:#f92672">.</span>_hp <span style="color:#f92672">=</span> max(<span style="color:#ae81ff">0</span>, min(value, self<span style="color:#f92672">.</span>max_hp))
|
||
</span></span></code></pre></div><p>Means that <code>_hp</code> (which we access through <code>hp</code>) will never be set to less than 0, but also won’t ever go higher than the <code>max_hp</code> attribute.</p>
|
||
<p>So that’s our <code>Fighter</code> component. It won’t do us much
|
||
good at the moment, because the entities in our game still don’t move or
|
||
do much of anything (besides the player, anyway). To give some life to
|
||
our entities, we can add another component, which, when attached to our
|
||
entities, will allow them to take turns and move around.</p>
|
||
<p>Create a file in the <code>components</code> directory called <code>ai.py</code>, and put the following contents into 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> List, Tuple
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> numpy <span style="color:#66d9ef">as</span> np <span style="color:#75715e"># type: ignore</span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> tcod
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> actions <span style="color:#f92672">import</span> Action
|
||
</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></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BaseAI</span>(Action, BaseComponent):
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">perform</span>(self) <span style="color:#f92672">-></span> <span style="color:#66d9ef">None</span>:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">NotImplementedError</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">get_path_to</span>(self, dest_x: int, dest_y: int) <span style="color:#f92672">-></span> List[Tuple[int, int]]:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Compute and return a path to the target position.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> If there is no valid path then returns an empty list.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> """</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Copy the walkable array.</span>
|
||
</span></span><span style="display:flex;"><span> cost <span style="color:#f92672">=</span> np<span style="color:#f92672">.</span>array(self<span style="color:#f92672">.</span>entity<span style="color:#f92672">.</span>gamemap<span style="color:#f92672">.</span>tiles[<span style="color:#e6db74">"walkable"</span>], dtype<span style="color:#f92672">=</span>np<span style="color:#f92672">.</span>int8)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> entity <span style="color:#f92672">in</span> self<span style="color:#f92672">.</span>entity<span style="color:#f92672">.</span>gamemap<span style="color:#f92672">.</span>entities:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Check that an enitiy blocks movement and the cost isn't zero (blocking.)</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> entity<span style="color:#f92672">.</span>blocks_movement <span style="color:#f92672">and</span> cost[entity<span style="color:#f92672">.</span>x, entity<span style="color:#f92672">.</span>y]:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Add to the cost of a blocked position.</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># A lower number means more enemies will crowd behind each other in</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># hallways. A higher number means enemies will take longer paths in</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># order to surround the player.</span>
|
||
</span></span><span style="display:flex;"><span> cost[entity<span style="color:#f92672">.</span>x, entity<span style="color:#f92672">.</span>y] <span style="color:#f92672">+=</span> <span style="color:#ae81ff">10</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Create a graph from the cost array and pass that graph to a new pathfinder.</span>
|
||
</span></span><span style="display:flex;"><span> graph <span style="color:#f92672">=</span> tcod<span style="color:#f92672">.</span>path<span style="color:#f92672">.</span>SimpleGraph(cost<span style="color:#f92672">=</span>cost, cardinal<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>, diagonal<span style="color:#f92672">=</span><span style="color:#ae81ff">3</span>)
|
||
</span></span><span style="display:flex;"><span> pathfinder <span style="color:#f92672">=</span> tcod<span style="color:#f92672">.</span>path<span style="color:#f92672">.</span>Pathfinder(graph)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> pathfinder<span style="color:#f92672">.</span>add_root((self<span style="color:#f92672">.</span>entity<span style="color:#f92672">.</span>x, self<span style="color:#f92672">.</span>entity<span style="color:#f92672">.</span>y)) <span style="color:#75715e"># Start position.</span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Compute the path to the destination and remove the starting point.</span>
|
||
</span></span><span style="display:flex;"><span> path: List[List[int]] <span style="color:#f92672">=</span> pathfinder<span style="color:#f92672">.</span>path_to((dest_x, dest_y))[<span style="color:#ae81ff">1</span>:]<span style="color:#f92672">.</span>tolist()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Convert from List[List[int]] to List[Tuple[int, int]].</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> [(index[<span style="color:#ae81ff">0</span>], index[<span style="color:#ae81ff">1</span>]) <span style="color:#66d9ef">for</span> index <span style="color:#f92672">in</span> path]
|
||
</span></span></code></pre></div><p><code>BaseAI</code> doesn’t implement a <code>perform</code> method, since the entities which will be using AI to act will have to have an AI class that inherits from this one.</p>
|
||
<p><code>get_path_to</code> uses the “walkable” tiles in our map, along with some TCOD pathfinding tools to get the path from the <code>BaseAI</code>’s
|
||
parent entity to whatever their target might be. In the case of this
|
||
tutorial, the target will always be the player, though you could
|
||
theoretically write a monster that cares more about food or treasure
|
||
than attacking the player.</p>
|
||
<p>The pathfinder first builds an array of <code>cost</code>, which is
|
||
how “costly” (time consuming) it will take to get to the target. If a
|
||
piece of terrain takes longer to traverse, its cost will be higher. In
|
||
the case of our simple game, all parts of the map have the same cost,
|
||
but what this cost array allows us to do is take other entities into
|
||
account.</p>
|
||
<p>How? Well, if an entity exists at a spot on the map, we increase the
|
||
cost of moving there to “10”. What this does is encourages the entity to
|
||
move around the entity that’s blocking them from their target. Higher
|
||
values will cause the entity to take a longer path around; shorter
|
||
values will cause groups to gather into crowds, since they don’t want to
|
||
move around.</p>
|
||
<p>More information about TCOD’s pathfinding can be <a href="https://python-tcod.readthedocs.io/en/latest/tcod/path.html">found here</a>.</p>
|
||
<p>To make use of our new <code>Fighter</code> and <code>AI</code> components, we could attach them directly onto the <code>Entity</code>
|
||
class. However, it might be useful to differentiate between entities
|
||
that can act, and those that can’t. Right now, our game only consists of
|
||
acting entities, but soon enough, we’ll be adding things like
|
||
consumable items and, eventually, equipment, which won’t be able to take
|
||
turns or take damage.</p>
|
||
<p>One way to handle this is to create a new subclass of <code>Entity</code>, called <code>Actor</code>, and give it all the same attributes as <code>Entity</code>, plus the <code>ai</code> and <code>fighter</code> components it will need. 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>from __future__ import annotations
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>import copy
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from typing import Tuple, TypeVar, TYPE_CHECKING
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING
|
||
</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>if TYPE_CHECKING:
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from components.ai import BaseAI
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from components.fighter import Fighter
|
||
</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 Entity:
|
||
</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 Actor(Entity):
|
||
</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">+ 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">+ ai_cls: Type[BaseAI],
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ fighter: Fighter
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ):
|
||
</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">+ 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">+ char=char,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ color=color,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ name=name,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ blocks_movement=True,
|
||
</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">+ self.ai: Optional[BaseAI] = ai_cls(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">+ self.fighter = fighter
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.fighter.entity = 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">+ @property
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def is_alive(self) -> bool:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Returns True as long as this actor can perform actions."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return bool(self.ai)
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>from __future__ import annotations
|
||
|
||
import copy
|
||
<span class="crossed-out-text">from typing import Tuple, TypeVar, TYPE_CHECKING</span>
|
||
<span class="new-text">from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING</span>
|
||
|
||
|
||
if TYPE_CHECKING:
|
||
<span class="new-text">from components.ai import BaseAI
|
||
from components.fighter import Fighter</span>
|
||
from game_map import GameMap
|
||
|
||
T = TypeVar("T", bound="Entity")
|
||
|
||
|
||
class Entity:
|
||
...
|
||
|
||
|
||
<span class="new-text">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
|
||
):
|
||
super().__init__(
|
||
x=x,
|
||
y=y,
|
||
char=char,
|
||
color=color,
|
||
name=name,
|
||
blocks_movement=True,
|
||
)
|
||
|
||
self.ai: Optional[BaseAI] = ai_cls(self)
|
||
|
||
self.fighter = fighter
|
||
self.fighter.entity = self
|
||
|
||
@property
|
||
def is_alive(self) -> bool:
|
||
"""Returns True as long as this actor can perform actions."""
|
||
return bool(self.ai)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>The first thing our <code>Actor</code> class does in its <code>__init__()</code> function is call its superclass’s <code>__init__()</code>, which in this case, is the <code>Entity</code> class. We’re passing <code>blocks_movement</code> as <code>True</code> every time, because we can assume that all the “actors” will block movement.</p>
|
||
<p>Besides calling the <code>Entity.__init__()</code>, we also set the two components for the <code>Actor</code> class: <code>ai</code> and <code>fighter</code>.
|
||
The idea is that each actor will need two things to function: the
|
||
ability to move around and make decisions, and the ability to take (and
|
||
receive) damage.</p>
|
||
<p>This new <code>Actor</code> class isn’t quite enough to get our enemies up and moving around, but we’re getting there. We actually need to revisit <code>ai.py</code>, and add a new class there to handle hostile enemies. Enter the following changes in <code>ai.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 List, Tuple
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import List, Tuple, 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>import tcod
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from actions import Action
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from actions import Action, MeleeAction, MovementAction, WaitAction
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from components.base_component import BaseComponent
|
||
</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 Actor
|
||
</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 BaseAI(Action, BaseComponent):
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ entity: Actor
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> def perform(self) -> 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">+class HostileEnemy(BaseAI):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def __init__(self, entity: Actor):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ super().__init__(entity)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.path: List[Tuple[int, int]] = []
|
||
</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) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ target = self.engine.player
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dx = target.x - self.entity.x
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dy = target.y - self.entity.y
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ distance = max(abs(dx), abs(dy)) # Chebyshev distance.
|
||
</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.game_map.visible[self.entity.x, self.entity.y]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if distance <= 1:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return MeleeAction(self.entity, dx, dy).perform()
|
||
</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.path = self.get_path_to(target.x, target.y)
|
||
</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.path:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dest_x, dest_y = self.path.pop(0)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return MovementAction(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entity, dest_x - self.entity.x, dest_y - self.entity.y,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ).perform()
|
||
</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 WaitAction(self.entity).perform()
|
||
</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 List, Tuple</span>
|
||
<span class="new-text">from typing import List, Tuple, TYPE_CHECKING</span>
|
||
|
||
import numpy as np # type: ignore
|
||
import tcod
|
||
|
||
<span class="crossed-out-text">from actions import Action</span>
|
||
<span class="new-text">from actions import Action, MeleeAction, MovementAction, WaitAction</span>
|
||
from components.base_component import BaseComponent
|
||
|
||
<span class="new-text">if TYPE_CHECKING:
|
||
from entity import Actor</span>
|
||
|
||
|
||
class BaseAI(Action, BaseComponent):
|
||
<span class="new-text">entity: Actor</span>
|
||
|
||
def perform(self) -> None:
|
||
...
|
||
|
||
|
||
<span class="new-text">class HostileEnemy(BaseAI):
|
||
def __init__(self, entity: Actor):
|
||
super().__init__(entity)
|
||
self.path: List[Tuple[int, int]] = []
|
||
|
||
def perform(self) -> None:
|
||
target = self.engine.player
|
||
dx = target.x - self.entity.x
|
||
dy = target.y - self.entity.y
|
||
distance = max(abs(dx), abs(dy)) # Chebyshev distance.
|
||
|
||
if self.engine.game_map.visible[self.entity.x, self.entity.y]:
|
||
if distance <= 1:
|
||
return MeleeAction(self.entity, dx, dy).perform()
|
||
|
||
self.path = self.get_path_to(target.x, target.y)
|
||
|
||
if self.path:
|
||
dest_x, dest_y = self.path.pop(0)
|
||
return MovementAction(
|
||
self.entity, dest_x - self.entity.x, dest_y - self.entity.y,
|
||
).perform()
|
||
|
||
return WaitAction(self.entity).perform()</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><code>HostileEnemy</code> is the AI class we’ll use for our enemies. It defines the <code>perform</code> method, which does the following:</p>
|
||
<ul>
|
||
<li>If the entity is not in the player’s vision, simply wait.</li>
|
||
<li>If the player is right next to the entity (<code>distance <= 1</code>), attack the player.</li>
|
||
<li>If the player can see the entity, but the entity is too far away to attack, then move towards the player.</li>
|
||
</ul>
|
||
<p>The last line actually calls an action that we haven’t defined yet: <code>WaitAction</code>. This action will be used when the player or an enemy decides to wait where they are rather than taking a turn.</p>
|
||
<p>Implement <code>WaitAction</code> by opening <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 EscapeAction(Action):
|
||
</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 WaitAction(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">+ pass
|
||
</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> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class EscapeAction(Action):
|
||
...
|
||
|
||
|
||
<span class="new-text">class WaitAction(Action):
|
||
def perform(self) -> None:
|
||
pass</span>
|
||
|
||
|
||
class ActionWithDirection(Action):
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>As you can see, <code>WaitAction</code> does… well, nothing. And that’s what we want it to do, as it represents an actor saying “I’ll do nothing this turn.”</p>
|
||
<p>With all that in place, we’ll need to refactor our <code>entity_factories.py</code> file to make use of the new <code>Actor</code> class, as well as its components. Modify <code>entity_factories.py</code> to look 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 components.ai import HostileEnemy
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from components.fighter import Fighter
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from entity import Actor
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">-from entity import Entity
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+player = Actor(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ char="@",
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ color=(255, 255, 255),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ name="Player",
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ai_cls=HostileEnemy,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ fighter=Fighter(hp=30, defense=2, power=5),
|
||
</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 style="color:#f92672">-player = Entity(char="@", color=(255, 255, 255), name="Player", blocks_movement=True)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+orc = Actor(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ char="o",
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ color=(63, 127, 63),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ name="Orc",
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ai_cls=HostileEnemy,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ fighter=Fighter(hp=10, defense=0, power=3),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+troll = Actor(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ char="T",
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ color=(0, 127, 0),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ name="Troll",
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ai_cls=HostileEnemy,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ fighter=Fighter(hp=16, defense=1, power=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"></span><span style="color:#f92672">-orc = Entity(char="o", color=(63, 127, 63), name="Orc", blocks_movement=True)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">-troll = Entity(char="T", color=(0, 127, 0), name="Troll", blocks_movement=True)
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre><span class="new-text">from components.ai import HostileEnemy
|
||
from components.fighter import Fighter
|
||
from entity import Actor</span>
|
||
<span class="crossed-out-text">from entity import Entity</span>
|
||
|
||
<span class="new-text">player = Actor(
|
||
char="@",
|
||
color=(255, 255, 255),
|
||
name="Player",
|
||
ai_cls=HostileEnemy,
|
||
fighter=Fighter(hp=30, defense=2, power=5),
|
||
)</span>
|
||
<span class="crossed-out-text">player = Entity(char="@", color=(255, 255, 255), name="Player", blocks_movement=True)</span>
|
||
|
||
<span class="new-text">orc = Actor(
|
||
char="o",
|
||
color=(63, 127, 63),
|
||
name="Orc",
|
||
ai_cls=HostileEnemy,
|
||
fighter=Fighter(hp=10, defense=0, power=3),
|
||
)
|
||
troll = Actor(
|
||
char="T",
|
||
color=(0, 127, 0),
|
||
name="Troll",
|
||
ai_cls=HostileEnemy,
|
||
fighter=Fighter(hp=16, defense=1, power=4),
|
||
)</span>
|
||
<span class="crossed-out-text">orc = Entity(char="o", color=(63, 127, 63), name="Orc", blocks_movement=True)</span>
|
||
<span class="crossed-out-text">troll = Entity(char="T", color=(0, 127, 0), name="Troll", blocks_movement=True)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>We’ve changed each entity to make use of the <code>Actor</code> class, and used the <code>HostileEnemy</code> AI class for the Orc and the Troll types.
|
||
The player doesn’t use the AI, so the AI given to it doesn’t matter other than that an AI must be specified for all <code>Actor</code>’s.
|
||
Also, we defined the <code>Fighter</code> component for each, giving a
|
||
few different values to make the Trolls stronger than the Orcs. Feel
|
||
free to modify these values to your liking.</p>
|
||
<p>How do enemies actually take their turns, though? It’s actually
|
||
pretty simple: rather than printing the message we were before, we just
|
||
check if the entity has an AI, and if it does, we call the <code>perform</code> method from that AI component. Modify <code>engine.py</code> to do 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 handle_enemy_turns(self) -> None:
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for entity in set(self.game_map.actors) - {self.player}:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if entity.ai:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ entity.ai.perform()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- for entity in self.game_map.entities - {self.player}:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- print(f'The {entity.name} wonders when it will get to take a real turn.')
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre> def handle_enemy_turns(self) -> None:
|
||
<span class="new-text">for entity in set(self.game_map.actors) - {self.player}:
|
||
if entity.ai:
|
||
entity.ai.perform()</span>
|
||
<span class="crossed-out-text">for entity in self.game_map.entities - {self.player}:</span>
|
||
<span class="crossed-out-text">print(f'The {entity.name} wonders when it will get to take a real turn.')</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>But wait, <code>game_map.actors</code> isn’t defined. What should it do, though? Same thing as <code>game_map.entities</code>, except it should return only the <code>Actor</code> entities.</p>
|
||
<p>Let’s add this method to <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>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, Optional, TYPE_CHECKING
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Iterable, Iterator, 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><span style="color:#a6e22e">+from entity import Actor
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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 engine import Engine
|
||
</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>class GameMap:
|
||
</span></span><span style="display:flex;"><span> def __init__(
|
||
</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">+ @property
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def actors(self) -> Iterator[Actor]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Iterate over this maps living actors."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ yield from (
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ 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 isinstance(entity, Actor) and entity.is_alive
|
||
</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 get_blocking_entity_at_location(
|
||
</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 get_actor_at_location(self, x: int, y: int) -> Optional[Actor]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for actor in self.actors:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if actor.x == x and actor.y == y:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return actor
|
||
</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></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, Optional, TYPE_CHECKING</span>
|
||
<span class="new-text">from typing import Iterable, Iterator, Optional, TYPE_CHECKING</span>
|
||
|
||
import numpy as np # type: ignore
|
||
from tcod.console import Console
|
||
|
||
<span class="new-text">from entity import Actor</span>
|
||
import tile_types
|
||
|
||
if TYPE_CHECKING:
|
||
from engine import Engine
|
||
from entity import Entity
|
||
|
||
class GameMap:
|
||
def __init__(
|
||
...
|
||
|
||
<span class="new-text">@property
|
||
def actors(self) -> Iterator[Actor]:
|
||
"""Iterate over this maps living actors."""
|
||
yield from (
|
||
entity
|
||
for entity in self.entities
|
||
if isinstance(entity, Actor) and entity.is_alive
|
||
)</span>
|
||
|
||
def get_blocking_entity_at_location(
|
||
...
|
||
|
||
<span class="new-text">def get_actor_at_location(self, x: int, y: int) -> Optional[Actor]:
|
||
for actor in self.actors:
|
||
if actor.x == x and actor.y == y:
|
||
return actor
|
||
|
||
return None</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Our <code>actors</code> property will return all the <code>Actor</code> entities in the map, but only those that are currently “alive”.</p>
|
||
<p>We’ve also went ahead and added a <code>get_actor_at_location</code>, which, as the name implies, acts similarly to <code>get_blocking_entity_at_location</code>, but returns only an <code>Actor</code>. This will come in handy later on.</p>
|
||
<p>Run the project now, and the enemies should chase you around! They can’t really attack just yet, but we’re getting there.</p>
|
||
<p><img src="Part%206%20-%20Doing%20(and%20taking)%20some%20damage%20%C2%B7%20Roguelike%20Tutorials_files/part-6-chase.png" alt="Part 6 - The Chase"></p>
|
||
<p>One thing you might have noticed is that we’re letting our enemies
|
||
move and attack in diagonal directions, but our player can only move in
|
||
the four cardinal directions (up, down, left, right). We can fix that by
|
||
adjusting <code>input_handlers.py</code>. While we’re at it, we might want to define a more flexible way of defining the movement keys rather than the <code>if...elif</code>
|
||
structure we’ve used so far. While that does work, it gets a bit clunky
|
||
after more than just a few options. We can fix this by modifying <code>input_handlers.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>from __future__ import annotations
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>from typing import Optional, TYPE_CHECKING
|
||
</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, BumpAction, EscapeAction
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from actions import Action, BumpAction, EscapeAction, WaitAction
|
||
</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> from engine import Engine
|
||
</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">+MOVE_KEYS = {
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Arrow keys.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_UP: (0, -1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_DOWN: (0, 1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_LEFT: (-1, 0),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_RIGHT: (1, 0),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_HOME: (-1, -1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_END: (-1, 1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_PAGEUP: (1, -1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_PAGEDOWN: (1, 1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Numpad keys.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_1: (-1, 1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_2: (0, 1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_3: (1, 1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_4: (-1, 0),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_6: (1, 0),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_7: (-1, -1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_8: (0, -1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_9: (1, -1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Vi keys.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_h: (-1, 0),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_j: (0, 1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_k: (0, -1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_l: (1, 0),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_y: (-1, -1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_u: (1, -1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_b: (-1, 1),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_n: (1, 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"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+WAIT_KEYS = {
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_PERIOD,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_KP_5,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_CLEAR,
|
||
</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></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if key == tcod.event.K_UP:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- action = BumpAction(dx=0, dy=-1)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- elif key == tcod.event.K_DOWN:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- action = BumpAction(dx=0, dy=1)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- elif key == tcod.event.K_LEFT:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- action = BumpAction(dx=-1, dy=0)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- elif key == tcod.event.K_RIGHT:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- action = BumpAction(dx=1, dy=0)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ if key in MOVE_KEYS:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dx, dy = MOVE_KEYS[key]
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ action = BumpAction(player, dx, dy)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ elif key in WAIT_KEYS:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ action = WaitAction(player)
|
||
</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 __future__ import annotations
|
||
|
||
from typing import Optional, TYPE_CHECKING
|
||
|
||
import tcod.event
|
||
|
||
<span class="crossed-out-text">from actions import Action, BumpAction, EscapeAction</span>
|
||
<span class="new-text">from actions import Action, BumpAction, EscapeAction, WaitAction</span>
|
||
|
||
if TYPE_CHECKING:
|
||
from engine import Engine
|
||
|
||
|
||
<span class="new-text">MOVE_KEYS = {
|
||
# Arrow keys.
|
||
tcod.event.K_UP: (0, -1),
|
||
tcod.event.K_DOWN: (0, 1),
|
||
tcod.event.K_LEFT: (-1, 0),
|
||
tcod.event.K_RIGHT: (1, 0),
|
||
tcod.event.K_HOME: (-1, -1),
|
||
tcod.event.K_END: (-1, 1),
|
||
tcod.event.K_PAGEUP: (1, -1),
|
||
tcod.event.K_PAGEDOWN: (1, 1),
|
||
# Numpad keys.
|
||
tcod.event.K_KP_1: (-1, 1),
|
||
tcod.event.K_KP_2: (0, 1),
|
||
tcod.event.K_KP_3: (1, 1),
|
||
tcod.event.K_KP_4: (-1, 0),
|
||
tcod.event.K_KP_6: (1, 0),
|
||
tcod.event.K_KP_7: (-1, -1),
|
||
tcod.event.K_KP_8: (0, -1),
|
||
tcod.event.K_KP_9: (1, -1),
|
||
# Vi keys.
|
||
tcod.event.K_h: (-1, 0),
|
||
tcod.event.K_j: (0, 1),
|
||
tcod.event.K_k: (0, -1),
|
||
tcod.event.K_l: (1, 0),
|
||
tcod.event.K_y: (-1, -1),
|
||
tcod.event.K_u: (1, -1),
|
||
tcod.event.K_b: (-1, 1),
|
||
tcod.event.K_n: (1, 1),
|
||
}
|
||
|
||
WAIT_KEYS = {
|
||
tcod.event.K_PERIOD,
|
||
tcod.event.K_KP_5,
|
||
tcod.event.K_CLEAR,
|
||
}</span>
|
||
|
||
|
||
...
|
||
|
||
<span class="crossed-out-text">if key == tcod.event.K_UP:</span>
|
||
<span class="crossed-out-text">action = BumpAction(player, dx=0, dy=-1)</span>
|
||
<span class="crossed-out-text">elif key == tcod.event.K_DOWN:</span>
|
||
<span class="crossed-out-text">action = BumpAction(player, dx=0, dy=1)</span>
|
||
<span class="crossed-out-text">elif key == tcod.event.K_LEFT:</span>
|
||
<span class="crossed-out-text">action = BumpAction(player, dx=-1, dy=0)</span>
|
||
<span class="crossed-out-text">elif key == tcod.event.K_RIGHT:</span>
|
||
<span class="crossed-out-text">action = BumpAction(player, dx=1, dy=0)</span>
|
||
<span class="new-text">if key in MOVE_KEYS:
|
||
dx, dy = MOVE_KEYS[key]
|
||
action = BumpAction(player, dx, dy)
|
||
elif key in WAIT_KEYS:
|
||
action = WaitAction(player)</span>
|
||
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>The <code>MOVE_KEYS</code> dictionary holds various different
|
||
possibilities for movement. Some roguelikes utilize the numpad for
|
||
movement, some use “Vi Keys.” Ours will actually use both for the time
|
||
being. Feel free to change the key scheme if you’re not a fan of it.</p>
|
||
<p>Where we used to do <code>if...elif</code> statements for each direction, we can now just check if the key was part of <code>MOVE_KEYS</code>, and if it was, we return the <code>dx</code> and <code>dy</code> values from the dictionary. This is a lot simpler and cleaner than our previous format.</p>
|
||
<p>So now that our enemies can chase us down, it’s time to make them do some real damage.</p>
|
||
<p>Open up <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>from __future__ import annotations
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>from typing import Optional, Tuple, TYPE_CHECKING
|
||
</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 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">+ from entity import Actor, 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 Action:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def __init__(self, entity: Entity) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def __init__(self, entity: Actor) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> super().__init__()
|
||
</span></span><span style="display:flex;"><span> self.entity = entity
|
||
</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></span><span style="display:flex;"><span>class ActionWithDirection(Action):
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def __init__(self, entity: Entity, dx: int, dy: int):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def __init__(self, entity: Actor, dx: int, dy: int):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> super().__init__(entity)
|
||
</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> @property
|
||
</span></span><span style="display:flex;"><span> def dest_xy(self) -> Tuple[int, int]:
|
||
</span></span><span style="display:flex;"><span> """Returns this actions destination."""
|
||
</span></span><span style="display:flex;"><span> return self.entity.x + self.dx, self.entity.y + self.dy
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> @property
|
||
</span></span><span style="display:flex;"><span> def blocking_entity(self) -> Optional[Entity]:
|
||
</span></span><span style="display:flex;"><span> """Return the blocking entity at this actions destination.."""
|
||
</span></span><span style="display:flex;"><span> return self.engine.game_map.get_blocking_entity_at_location(*self.dest_xy)
|
||
</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:#a6e22e">+ def target_actor(self) -> Optional[Actor]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Return the actor at this actions destination."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.engine.game_map.get_actor_at_location(*self.dest_xy)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> def perform(self) -> 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>class MeleeAction(ActionWithDirection):
|
||
</span></span><span style="display:flex;"><span> def perform(self) -> None:
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ target = self.target_actor
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- target = self.blocking_entity
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> if not target:
|
||
</span></span><span style="display:flex;"><span> return # No entity to attack.
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ damage = self.entity.fighter.power - target.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">+ attack_desc = f"{self.entity.name.capitalize()} attacks {target.name}"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if damage > 0:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ print(f"{attack_desc} for {damage} hit points.")
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ target.fighter.hp -= damage
|
||
</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">+ print(f"{attack_desc} but does no damage.")
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- print(f"You kick the {target.name}, much to its annoyance!")
|
||
</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>class MovementAction(ActionWithDirection):
|
||
</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 BumpAction(ActionWithDirection):
|
||
</span></span><span style="display:flex;"><span> def perform(self) -> None:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if self.blocking_entity:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ if self.target_actor:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> return MeleeAction(self.entity, self.dx, self.dy).perform()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> else:
|
||
</span></span><span style="display:flex;"><span> return MovementAction(self.entity, self.dx, self.dy).perform()
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>from __future__ import annotations
|
||
|
||
from typing import Optional, Tuple, TYPE_CHECKING
|
||
|
||
if TYPE_CHECKING:
|
||
from engine import Engine
|
||
<span class="crossed-out-text">from entity import Entity</span>
|
||
<span class="new-text">from entity import Actor, Entity</span>
|
||
|
||
|
||
class Action:
|
||
<span class="crossed-out-text">def __init__(self, entity: Entity) -> None:</span>
|
||
<span class="new-text">def __init__(self, entity: Actor) -> None:</span>
|
||
super().__init__()
|
||
self.entity = entity
|
||
|
||
...
|
||
|
||
|
||
class ActionWithDirection(Action):
|
||
<span class="crossed-out-text">def __init__(self, entity: Entity, dx: int, dy: int):</span>
|
||
<span class="new-text">def __init__(self, entity: Actor, dx: int, dy: int):</span>
|
||
super().__init__(entity)
|
||
|
||
self.dx = dx
|
||
self.dy = dy
|
||
|
||
@property
|
||
def dest_xy(self) -> Tuple[int, int]:
|
||
"""Returns this actions destination."""
|
||
return self.entity.x + self.dx, self.entity.y + self.dy
|
||
|
||
@property
|
||
def blocking_entity(self) -> Optional[Entity]:
|
||
"""Return the blocking entity at this actions destination.."""
|
||
return self.engine.game_map.get_blocking_entity_at_location(*self.dest_xy)
|
||
|
||
<span class="new-text">@property
|
||
def target_actor(self) -> Optional[Actor]:
|
||
"""Return the actor at this actions destination."""
|
||
return self.engine.game_map.get_actor_at_location(*self.dest_xy)</span>
|
||
|
||
def perform(self) -> None:
|
||
raise NotImplementedError()
|
||
|
||
|
||
class MeleeAction(ActionWithDirection):
|
||
def perform(self) -> None:
|
||
<span class="new-text">target = self.target_actor</span>
|
||
<span class="crossed-out-text">target = self.blocking_entity</span>
|
||
if not target:
|
||
return # No entity to attack.
|
||
|
||
<span class="new-text">damage = self.entity.fighter.power - target.fighter.defense
|
||
|
||
attack_desc = f"{self.entity.name.capitalize()} attacks {target.name}"
|
||
if damage > 0:
|
||
print(f"{attack_desc} for {damage} hit points.")
|
||
target.fighter.hp -= damage
|
||
else:
|
||
print(f"{attack_desc} but does no damage.")</span>
|
||
<span class="crossed-out-text">print(f"You kick the {target.name}, much to its annoyance!")</span>
|
||
|
||
|
||
class MovementAction(ActionWithDirection):
|
||
...
|
||
|
||
|
||
class BumpAction(ActionWithDirection):
|
||
def perform(self) -> None:
|
||
<span class="crossed-out-text">if self.blocking_entity:</span>
|
||
<span class="new-text">if self.target_actor:</span>
|
||
return MeleeAction(self.entity, self.dx, self.dy).perform()
|
||
|
||
else:
|
||
return MovementAction(self.entity, self.dx, self.dy).perform()</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>We’re replacing the type hint for <code>entity</code> in <code>Action</code> and <code>ActionWithDirection</code> with <code>Actor</code> instead of <code>Entity</code>, since only <code>Actor</code>s should be taking actions.</p>
|
||
<p>We’ve also added the <code>target_actor</code> property to <code>ActionWithDirection</code>, which will give us the <code>Actor</code> at the destination we’re moving to, if there is one. We utilize that property instead of <code>blocking_entity</code> in both <code>BumpAction</code> and <code>MeleeAction</code>.</p>
|
||
<p>Lastly, we modify <code>MeleeAction</code> to actually do an attack,
|
||
instead of just printing a message. We calculate the damage (attacker’s
|
||
power minus defender’s defense), and assign a description to the attack,
|
||
based on whether any damage was done or not. If the damage is greater
|
||
than 0, we subtract it from the defender’s HP.</p>
|
||
<p>If you run the project now, you’ll see the print statements
|
||
indicating that the player and the enemies are doing damage to each
|
||
other. But since neither side can actually die, combat doesn’t feel all
|
||
that high stakes just yet.</p>
|
||
<p>What do we do when an Entity reaches 0 HP or lower? Well, it should drop dead, obviously! But what should our <em>code</em> do to make this happen? To handle this, we can refer back to our <code>Fighter</code> component.</p>
|
||
<p>Remember when we created a setter for <code>hp</code>? It will come
|
||
in handy right now, as we can utilize it to automatically “kill” the
|
||
actor when their HP drops to zero. Add the following to <code>fighter.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 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 TYPE_CHECKING
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>from components.base_component import BaseComponent
|
||
</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 Actor
|
||
</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 Fighter(BaseComponent):
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ entity: Actor
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> def __init__(self, hp: int, defense: int, power: int):
|
||
</span></span><span style="display:flex;"><span> self.max_hp = hp
|
||
</span></span><span style="display:flex;"><span> self._hp = hp
|
||
</span></span><span style="display:flex;"><span> self.defense = defense
|
||
</span></span><span style="display:flex;"><span> self.power = power
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> @property
|
||
</span></span><span style="display:flex;"><span> def hp(self) -> int:
|
||
</span></span><span style="display:flex;"><span> return self._hp
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> @hp.setter
|
||
</span></span><span style="display:flex;"><span> def hp(self, value: int) -> None:
|
||
</span></span><span style="display:flex;"><span> self._hp = max(0, min(value, self.max_hp))
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self._hp == 0 and self.entity.ai:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.die()
|
||
</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 die(self) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.engine.player is self.entity:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ death_message = "You died!"
|
||
</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">+ death_message = f"{self.entity.name} is dead!"
|
||
</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.entity.char = "%"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entity.color = (191, 0, 0)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entity.blocks_movement = False
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entity.ai = None
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entity.name = f"remains of {self.entity.name}"
|
||
</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(death_message)
|
||
</span></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 TYPE_CHECKING</span>
|
||
|
||
from components.base_component import BaseComponent
|
||
|
||
<span class="new-text">if TYPE_CHECKING:
|
||
from entity import Actor</span>
|
||
|
||
|
||
class Fighter(BaseComponent):
|
||
<span class="new-text">entity: Actor</span>
|
||
|
||
def __init__(self, hp: int, defense: int, power: int):
|
||
self.max_hp = hp
|
||
self._hp = hp
|
||
self.defense = defense
|
||
self.power = power
|
||
|
||
@property
|
||
def hp(self) -> int:
|
||
return self._hp
|
||
|
||
@hp.setter
|
||
def hp(self, value: int) -> None:
|
||
self._hp = max(0, min(value, self.max_hp))
|
||
<span class="new-text">if self._hp == 0 and self.entity.ai:
|
||
self.die()
|
||
|
||
def die(self) -> None:
|
||
if self.engine.player is self.entity:
|
||
death_message = "You died!"
|
||
else:
|
||
death_message = f"{self.entity.name} is dead!"
|
||
|
||
self.entity.char = "%"
|
||
self.entity.color = (191, 0, 0)
|
||
self.entity.blocks_movement = False
|
||
self.entity.ai = None
|
||
self.entity.name = f"remains of {self.entity.name}"
|
||
|
||
print(death_message)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>When the actor dies, we use the <code>die</code> method to do several things:</p>
|
||
<ul>
|
||
<li>Print out a message, indicating the death of the entity</li>
|
||
<li>Set the entity’s character to “%” (most roguelikes use this for corpses)</li>
|
||
<li>Set its color to red (for a bloody, gory mess)</li>
|
||
<li>Set <code>blocks_movement</code> to <code>False</code>, so that the entities can walk over the corpse</li>
|
||
<li>Remove the AI from the entity, so it’ll be marked as dead and won’t take any more turns.</li>
|
||
<li>Change the name to “remains of {entity name}”</li>
|
||
</ul>
|
||
<p>Run the project now, and enjoy slaughtering some Orcs and Trolls!</p>
|
||
<p><img src="Part%206%20-%20Doing%20(and%20taking)%20some%20damage%20%C2%B7%20Roguelike%20Tutorials_files/part-6-killing-enemies.png" alt="Part 6 - Killing Enemies"></p>
|
||
<p>As satisfying as it would be to end here, our work is not quite done. If you play the game a bit, you’ll notice two problems.</p>
|
||
<p>The first is that, sometimes, corpses actually cover up entities.</p>
|
||
<p><img src="Part%206%20-%20Doing%20(and%20taking)%20some%20damage%20%C2%B7%20Roguelike%20Tutorials_files/part-6-player-under-corpse.png" alt="Part 6 - Player under a Corpse"></p>
|
||
<p><em>The player is currently under the corpse in the screenshot.</em></p>
|
||
<p>This not only makes no sense, since the entities should be walking <em>over</em> the corpses, but it can confuse the player rather easily.</p>
|
||
<p>The other issue is much more severe. Try playing the game and letting yourself die on purpose.</p>
|
||
<p><img src="Part%206%20-%20Doing%20(and%20taking)%20some%20damage%20%C2%B7%20Roguelike%20Tutorials_files/part-6-dead-player.png" alt="Part 6 - Dead Player"></p>
|
||
<p>The player does indeed turn into a corpse, but… you can still move
|
||
around, and even attack enemies! This is because the game doesn’t really
|
||
“end” at the moment when the player dies. The only thing that changes
|
||
is that the player’s AI component is set to <code>None</code>, but that isn’t actually what controls the player, the <code>EventHandler</code> class does that.</p>
|
||
<p>Let’s focus on the first issue first. Solving it is actually pretty easy. What we’ll do is assign a value to each <code>Entity</code>,
|
||
and this value will represent which order the entities should be
|
||
rendered in. Lower values will be rendered first, and higher values will
|
||
be rendered after. Therefore, if we assign a low value to a corpse, it
|
||
will get drawn before an entity. If two things are on the same tile,
|
||
whatever gets drawn last will be what the player sees.</p>
|
||
<p>To create the render values we’ll need, create a new file, called <code>render_order.py</code>, and put the following class 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> enum <span style="color:#f92672">import</span> auto, Enum
|
||
</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">RenderOrder</span>(Enum):
|
||
</span></span><span style="display:flex;"><span> CORPSE <span style="color:#f92672">=</span> auto()
|
||
</span></span><span style="display:flex;"><span> ITEM <span style="color:#f92672">=</span> auto()
|
||
</span></span><span style="display:flex;"><span> ACTOR <span style="color:#f92672">=</span> auto()
|
||
</span></span></code></pre></div><p><em>Note: You’ll need Python 3.6 or higher for the <code>auto</code> function to work.</em></p>
|
||
<p><code>RenderOrder</code> is an <code>Enum</code>. An “Enum” is a set of named values that won’t change, so it’s perfect for things like this. <code>auto</code> assigns incrementing integer values automatically, so we don’t need to retype them if we add more values later on.</p>
|
||
<p>To use this new Enum, let’s edit <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>from __future__ import annotations
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>import copy
|
||
</span></span><span style="display:flex;"><span>from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from render_order import RenderOrder
|
||
</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> from components.ai import BaseAI
|
||
</span></span><span style="display:flex;"><span> from components.fighter import Fighter
|
||
</span></span><span style="display:flex;"><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 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></span><span style="display:flex;"><span> gamemap: GameMap
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> def __init__(
|
||
</span></span><span style="display:flex;"><span> self,
|
||
</span></span><span style="display:flex;"><span> gamemap: Optional[GameMap] = None,
|
||
</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> blocks_movement: bool = False,
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ render_order: RenderOrder = RenderOrder.CORPSE,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> ):
|
||
</span></span><span style="display:flex;"><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> self.name = name
|
||
</span></span><span style="display:flex;"><span> self.blocks_movement = blocks_movement
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.render_order = render_order
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> if gamemap:
|
||
</span></span><span style="display:flex;"><span> # If gamemap isn't provided now then it will be set later.
|
||
</span></span><span style="display:flex;"><span> self.gamemap = gamemap
|
||
</span></span><span style="display:flex;"><span> gamemap.entities.add(self)
|
||
</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> ):
|
||
</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><span style="color:#a6e22e">+ render_order=RenderOrder.ACTOR,
|
||
</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>from __future__ import annotations
|
||
|
||
import copy
|
||
from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING
|
||
|
||
<span class="new-text">from render_order import RenderOrder</span>
|
||
|
||
if TYPE_CHECKING:
|
||
from components.ai import BaseAI
|
||
from components.fighter import Fighter
|
||
from game_map import GameMap
|
||
|
||
T = TypeVar("T", bound="Entity")
|
||
|
||
|
||
class Entity:
|
||
"""
|
||
A generic object to represent players, enemies, items, etc.
|
||
"""
|
||
|
||
gamemap: GameMap
|
||
|
||
def __init__(
|
||
self,
|
||
gamemap: Optional[GameMap] = None,
|
||
x: int = 0,
|
||
y: int = 0,
|
||
char: str = "?",
|
||
color: Tuple[int, int, int] = (255, 255, 255),
|
||
name: str = "<Unnamed>",
|
||
blocks_movement: bool = False,
|
||
<span class="new-text">render_order: RenderOrder = RenderOrder.CORPSE,</span>
|
||
):
|
||
self.x = x
|
||
self.y = y
|
||
self.char = char
|
||
self.color = color
|
||
self.name = name
|
||
self.blocks_movement = blocks_movement
|
||
<span class="new-text">self.render_order = render_order</span>
|
||
if gamemap:
|
||
# If gamemap isn't provided now then it will be set later.
|
||
self.gamemap = gamemap
|
||
gamemap.entities.add(self)
|
||
...
|
||
|
||
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
|
||
):
|
||
super().__init__(
|
||
x=x,
|
||
y=y,
|
||
char=char,
|
||
color=color,
|
||
name=name,
|
||
blocks_movement=True,
|
||
<span class="new-text">render_order=RenderOrder.ACTOR,</span>
|
||
)</unnamed></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>We’re now passing the render order to the <code>Entity</code> class, with a default of <code>CORPSE</code>. Notice that we don’t pass it to <code>Actor</code>, and instead, assume that the actor’s default will be the <code>ACTOR</code> value.</p>
|
||
<p>In order to actually take advantage of the rendering order, we’ll need to modify the part of <code>GameMap</code> that renders the entities to the screen. Modify the <code>render</code> method in <code>GameMap</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> 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">+ entities_sorted_for_rendering = sorted(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entities, key=lambda x: x.render_order.value
|
||
</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:#f92672">- for entity in self.entities:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ for entity in entities_sorted_for_rendering:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> if self.visible[entity.x, entity.y]:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ console.print(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=entity.x, y=entity.y, string=entity.char, fg=entity.color
|
||
</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> ...
|
||
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">entities_sorted_for_rendering = sorted(
|
||
self.entities, key=lambda x: x.render_order.value
|
||
)</span>
|
||
|
||
<span class="crossed-out-text">for entity in self.entities:</span>
|
||
<span class="new-text">for entity in entities_sorted_for_rendering:</span>
|
||
if self.visible[entity.x, entity.y]:
|
||
<span class="crossed-out-text">console.print(x=entity.x, y=entity.y, string=entity.char, fg=entity.color)</span>
|
||
<span class="new-text">console.print(
|
||
x=entity.x, y=entity.y, string=entity.char, fg=entity.color
|
||
)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
<p></p>
|
||
<p>The <code>sorted</code> function takes two arguments: The collection to sort, and the function used to sort it. By using <code>key</code> in <code>sorted</code>, we’re defining a custom way to sort the <code>self.entities</code>, which in this case, we’re using a <code>lambda</code>
|
||
function (basically, a function that’s limited to one line that we
|
||
don’t need to write a formal definition for). The lambda function itself
|
||
tells <code>sorted</code> to sort by the value of <code>render_order</code>. Since the <code>RenderOrder</code>
|
||
enum defines its order from 1 (Corpse, lowest) to 3 (Actor, highest),
|
||
corpses should be sent to the front of the sorted list. That way, when
|
||
rendering, they’ll get drawn first, so if there’s something else on top
|
||
of them, they’ll get overwritten, and we’ll just see the <code>Actor</code> instead of the corpse.</p>
|
||
<p>Last thing we need to do is rewrite the <code>render_order</code> of an entity when it dies. Go back to the <code>Fighter</code> class and add the following:</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>from typing import TYPE_CHECKING
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>from components.base_component import BaseComponent
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from render_order import RenderOrder
|
||
</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> from entity import Actor
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>class Fighter(BaseComponent):
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span> self.entity.ai = None
|
||
</span></span><span style="display:flex;"><span> self.entity.name = f"remains of {self.entity.name}"
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.entity.render_order = RenderOrder.CORPSE
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> print(death_message)
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>from __future__ import annotations
|
||
|
||
from typing import TYPE_CHECKING
|
||
|
||
from components.base_component import BaseComponent
|
||
<span class="new-text">from render_order import RenderOrder</span>
|
||
|
||
if TYPE_CHECKING:
|
||
from entity import Actor
|
||
|
||
|
||
class Fighter(BaseComponent):
|
||
...
|
||
...
|
||
self.entity.ai = None
|
||
self.entity.name = f"remains of {self.entity.name}"
|
||
<span class="new-text">self.entity.render_order = RenderOrder.CORPSE</span>
|
||
|
||
print(death_message)</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Run the project now, and the corpse ordering issue should be resolved.</p>
|
||
<p>Now, onto the more important issue: solving the player’s death.</p>
|
||
<p>One thing that would be helpful right now is being able to see the
|
||
player’s HP. Otherwise, the player will just kinda drop dead after a
|
||
while, and it’ll be difficult for the player to know how close they are
|
||
to death’s door.</p>
|
||
<p>Add the following line to the <code>render</code> function in the <code>Engine</code> 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>if TYPE_CHECKING:
|
||
</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">+ from entity import Actor
|
||
</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>class Engine:
|
||
</span></span><span style="display:flex;"><span> game_map: GameMap
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def __init__(self, player: Entity):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def __init__(self, player: Actor):
|
||
</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(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:#a6e22e">+ console.print(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=1,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y=47,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ string=f"HP: {self.player.fighter.hp}/{self.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"></span>
|
||
</span></span><span style="display:flex;"><span> context.present(console)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> console.clear()
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>if TYPE_CHECKING:
|
||
<span class="crossed-out-text">from entity import Entity</span>
|
||
<span class="new-text">from entity import Actor</span>
|
||
from game_map import GameMap
|
||
|
||
|
||
class Engine:
|
||
game_map: GameMap
|
||
|
||
<span class="crossed-out-text">def __init__(self, player: Entity):</span>
|
||
<span class="new-text">def __init__(self, player: Actor):</span>
|
||
...
|
||
|
||
def render(self, console: Console, context: Context) -> None:
|
||
self.game_map.render(console)
|
||
|
||
<span class="new-text">console.print(
|
||
x=1,
|
||
y=47,
|
||
string=f"HP: {self.player.fighter.hp}/{self.player.fighter.max_hp}",
|
||
)</span>
|
||
|
||
context.present(console)
|
||
|
||
console.clear()</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Pretty simple. We’re printing the player’s HP current health over
|
||
maximum health below the map. It’s not the most attractive looking
|
||
health display, that’s for sure, but it should suffice for now. A better
|
||
looking way to show the character’s health is coming shortly anyway, in
|
||
the next chapter.</p>
|
||
<p>Notice that we also updated the type hint for the <code>player</code> argument in the Engine’s <code>__init__</code> function.</p>
|
||
<p>The health indicator is great and all, but our player is still
|
||
animated after death. There’s a few ways to handle this, but the way
|
||
we’ll go with is swapping out the <code>EventHandler</code> class. Why?
|
||
Because what we want to do right now is disallow the player from moving
|
||
around after dying. An easy way to do that is to stop reacting to the
|
||
movement keypresses. By switching to a different <code>EventHandler</code>, we can do just that.</p>
|
||
<p>What we’ll want to do is actually modify our existing <code>EventHandler</code> to be a base class, and inherit from it in two new classes: <code>MainGameEventHandler</code>, and <code>GameOverEventHandler</code>. <code>MainGameEventHandler</code> will actually do what our current implementation of <code>EventHandler</code> does, and <code>GameOverEventHandler</code> will handle things when the main character meets his or her untimely demise.</p>
|
||
<p>Open up <code>input_handlers.py</code> and make the following adjustments:</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 EventHandler(tcod.event.EventDispatch[Action]):
|
||
</span></span><span style="display:flex;"><span> def __init__(self, engine: Engine):
|
||
</span></span><span style="display:flex;"><span> self.engine = engine
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def handle_events(self) -> 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 style="color:#a6e22e">+ def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise SystemExit()
|
||
</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:#a6e22e">+class MainGameEventHandler(EventHandler):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> def handle_events(self) -> None:
|
||
</span></span><span style="display:flex;"><span> for event in tcod.event.wait():
|
||
</span></span><span style="display:flex;"><span> action = self.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()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self.engine.handle_enemy_turns()
|
||
</span></span><span style="display:flex;"><span> self.engine.update_fov() # Update the FOV before the players next action.
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- raise SystemExit()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></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> player = self.engine.player
|
||
</span></span><span style="display:flex;"><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><span style="display:flex;"><span> action = BumpAction(player, dx, dy)
|
||
</span></span><span style="display:flex;"><span> elif key in WAIT_KEYS:
|
||
</span></span><span style="display:flex;"><span> action = WaitAction(player)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> elif key == tcod.event.K_ESCAPE:
|
||
</span></span><span style="display:flex;"><span> action = EscapeAction(player)
|
||
</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><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+class GameOverEventHandler(EventHandler):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def handle_events(self) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for event in tcod.event.wait():
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ action = self.dispatch(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">+ if action is None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ continue
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ action.perform()
|
||
</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[Action]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ action: Optional[Action] = 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">+ key = event.sym
|
||
</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 key == tcod.event.K_ESCAPE:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ action = EscapeAction(self.engine.player)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # No valid key was pressed
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return action
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class EventHandler(tcod.event.EventDispatch[Action]):
|
||
def __init__(self, engine: Engine):
|
||
self.engine = engine
|
||
|
||
<span class="new-text">def handle_events(self) -> None:
|
||
raise NotImplementedError()
|
||
|
||
def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
|
||
raise SystemExit()</span>
|
||
|
||
|
||
<span class="new-text">class MainGameEventHandler(EventHandler):</span>
|
||
def handle_events(self) -> None:
|
||
for event in tcod.event.wait():
|
||
action = self.dispatch(event)
|
||
|
||
if action is None:
|
||
continue
|
||
|
||
action.perform()
|
||
|
||
self.engine.handle_enemy_turns()
|
||
self.engine.update_fov() # Update the FOV before the players next action.
|
||
|
||
<span class="crossed-out-text">def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:</span>
|
||
<span class="crossed-out-text">raise SystemExit()</span>
|
||
|
||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||
action: Optional[Action] = None
|
||
|
||
key = event.sym
|
||
|
||
player = self.engine.player
|
||
|
||
if key in MOVE_KEYS:
|
||
dx, dy = MOVE_KEYS[key]
|
||
action = BumpAction(player, dx, dy)
|
||
elif key in WAIT_KEYS:
|
||
action = WaitAction(player)
|
||
|
||
elif key == tcod.event.K_ESCAPE:
|
||
action = EscapeAction(player)
|
||
|
||
# No valid key was pressed
|
||
return action
|
||
|
||
|
||
<span class="new-text">class GameOverEventHandler(EventHandler):
|
||
def handle_events(self) -> None:
|
||
for event in tcod.event.wait():
|
||
action = self.dispatch(event)
|
||
|
||
if action is None:
|
||
continue
|
||
|
||
action.perform()
|
||
|
||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||
action: Optional[Action] = None
|
||
|
||
key = event.sym
|
||
|
||
if key == tcod.event.K_ESCAPE:
|
||
action = EscapeAction(self.engine.player)
|
||
|
||
# No valid key was pressed
|
||
return action</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><code>EventHandler</code> is now the base class for our other two classes.</p>
|
||
<p><code>MainGameEventHandler</code> is almost identical to our original <code>EventHandler</code> class, except that it doesn’t need to implement <code>ev_quit</code>, as <code>EventHandler</code> takes care of that just fine.</p>
|
||
<p><code>GameOverEventHandler</code> is what’s really new here. It doesn’t look terribly different from <code>MainGameEventHandler</code>, except for a few key differences.</p>
|
||
<ul>
|
||
<li>After performing its actions, it doesn’t call the enemy turns nor update the FOV.</li>
|
||
<li>It also doesn’t respond to the movement keys, just <code>Esc</code>, so the player can still exit the game.</li>
|
||
</ul>
|
||
<p>Because we’re replacing our old implementation of <code>EventHandler</code> with <code>MainGameEventHandler</code>, we’ll need to adjust <code>engine.py</code> to use <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>from tcod.map import compute_fov
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from input_handlers import EventHandler
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from input_handlers import MainGameEventHandler
|
||
</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> from entity import Actor
|
||
</span></span><span style="display:flex;"><span> from game_map import GameMap
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from input_handlers import EventHandler
|
||
</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></span><span style="display:flex;"><span> def __init__(self, player: Actor):
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.event_handler: EventHandler = EventHandler(self)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ self.event_handler: EventHandler = MainGameEventHandler(self)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.player = player
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>from tcod.map import compute_fov
|
||
|
||
<span class="crossed-out-text">from input_handlers import EventHandler</span>
|
||
<span class="new-text">from input_handlers import MainGameEventHandler</span>
|
||
|
||
if TYPE_CHECKING:
|
||
from entity import Actor
|
||
from game_map import GameMap
|
||
<span class="new-text">from input_handlers import EventHandler</span>
|
||
|
||
|
||
class Engine:
|
||
game_map: GameMap
|
||
|
||
def __init__(self, player: Actor):
|
||
<span class="crossed-out-text">self.event_handler: EventHandler = EventHandler(self)</span>
|
||
<span class="new-text">self.event_handler: EventHandler = MainGameEventHandler(self)</span>
|
||
self.player = player</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Lastly, we can use the <code>GameOverEventHandler</code> in <code>fighter.py</code> to ensure the player cannot move after death:</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>from typing import TYPE_CHECKING
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>from components.base_component import BaseComponent
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from input_handlers import GameOverEventHandler
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from render_order import RenderOrder
|
||
</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 Actor
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>class Fighter(BaseComponent):
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> def die(self) -> None:
|
||
</span></span><span style="display:flex;"><span> if self.engine.player is self.entity:
|
||
</span></span><span style="display:flex;"><span> death_message = "You died!"
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.event_handler = GameOverEventHandler(self.engine)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> else:
|
||
</span></span><span style="display:flex;"><span> death_message = f"{self.entity.name} is dead!"
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>from __future__ import annotations
|
||
|
||
from typing import TYPE_CHECKING
|
||
|
||
from components.base_component import BaseComponent
|
||
<span class="new-text">from input_handlers import GameOverEventHandler</span>
|
||
from render_order import RenderOrder
|
||
|
||
if TYPE_CHECKING:
|
||
from entity import Actor
|
||
|
||
|
||
class Fighter(BaseComponent):
|
||
...
|
||
|
||
def die(self) -> None:
|
||
if self.engine.player is self.entity:
|
||
death_message = "You died!"
|
||
<span class="new-text">self.engine.event_handler = GameOverEventHandler(self.engine)</span>
|
||
else:
|
||
death_message = f"{self.entity.name} is dead!"</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>And with that last change, the main character should die, for real
|
||
this time! You’ll be unable to move or attack, but you can still exit
|
||
the game as normal.</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-6">click
|
||
here</a>.</p>
|
||
<p><a href="https://rogueliketutorials.com/tutorials/tcod/v2/part-7">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%206%20-%20Doing%20(and%20taking)%20some%20damage%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.236049395dc3682fb2719640872958e12f1f24067bb09c327b2.js" integrity="sha256-I2BJOV3DaC+ycZZAhylY4S8fJAZ7sJwyeyM+YpDH7aw="></script>
|
||
|
||
|
||
|
||
|
||
|
||
<script src="Part%206%20-%20Doing%20(and%20taking)%20some%20damage%20%C2%B7%20Roguelike%20Tutorials_files/codetabs.min.cc52451e7f25e50f64c1c893826f606d58410d742c214dce.js" integrity="sha256-zFJFHn8l5Q9kwciTgm9gbVhBDXQsIU3OI/tEfJlh8rA="></script>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
</body></html> |