3235 lines
181 KiB
HTML
3235 lines
181 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 8 - Items and Inventory · 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="Quick refactors Link to heading Once again, apologies to everyone reading this right now. After publishing the last two parts, there were once again a few refactors on code written in those parts, like at the beginning of part 6. Luckily, the changes are much less extensive this time.
|
||
ai.py
|
||
Diff Original ... import numpy as np # type: ignore import tcod from actions import Action, MeleeAction, MovementAction, WaitAction -from components.">
|
||
<meta name="keywords" content="">
|
||
|
||
<meta name="twitter:card" content="summary">
|
||
<meta name="twitter:title" content="Part 8 - Items and Inventory">
|
||
<meta name="twitter:description" content="Quick refactors Link to heading Once again, apologies to everyone reading this right now. After publishing the last two parts, there were once again a few refactors on code written in those parts, like at the beginning of part 6. Luckily, the changes are much less extensive this time.
|
||
ai.py
|
||
Diff Original ... import numpy as np # type: ignore import tcod from actions import Action, MeleeAction, MovementAction, WaitAction -from components.">
|
||
|
||
<meta property="og:title" content="Part 8 - Items and Inventory">
|
||
<meta property="og:description" content="Quick refactors Link to heading Once again, apologies to everyone reading this right now. After publishing the last two parts, there were once again a few refactors on code written in those parts, like at the beginning of part 6. Luckily, the changes are much less extensive this time.
|
||
ai.py
|
||
Diff Original ... import numpy as np # type: ignore import tcod from actions import Action, MeleeAction, MovementAction, WaitAction -from components.">
|
||
<meta property="og:type" content="article">
|
||
<meta property="og:url" content="https://rogueliketutorials.com/tutorials/tcod/v2/part-8/"><meta property="article:section" content="tutorials">
|
||
<meta property="article:published_time" content="2020-07-14T00:00:00+00:00">
|
||
<meta property="article:modified_time" content="2020-07-14T00:00:00+00:00">
|
||
|
||
|
||
|
||
|
||
<link rel="canonical" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-8/">
|
||
|
||
|
||
<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%208%20-%20Items%20and%20Inventory%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.c4d7e93a158eda5a65b3df343745d2092a0a1e2170feeec909.css" integrity="sha256-xNfpOhWO2lpls980N0XSCSoKHiFw/u7JCbiolEOQPGo=" crossorigin="anonymous" media="screen">
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<link rel="stylesheet" href="Part%208%20-%20Items%20and%20Inventory%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%208%20-%20Items%20and%20Inventory%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-8/">
|
||
Part 8 - Items and Inventory
|
||
</a>
|
||
</h1>
|
||
</header>
|
||
|
||
<h2 id="quick-refactors">
|
||
Quick refactors
|
||
<a class="heading-link" href="#quick-refactors">
|
||
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
|
||
<span class="sr-only">Link to heading</span>
|
||
</a>
|
||
</h2>
|
||
<p>Once again, apologies to everyone reading this right now. After
|
||
publishing the last two parts, there were once again a few refactors on
|
||
code written in those parts, like at the beginning of part 6. Luckily,
|
||
the changes are much less extensive this time.</p>
|
||
<p><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>...
|
||
</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>from actions import Action, MeleeAction, MovementAction, WaitAction
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from components.base_component import BaseComponent
|
||
</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 entity import Actor
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">-class BaseAI(Action, BaseComponent):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+class BaseAI(Action):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> entity: Actor
|
||
</span></span><span style="display:flex;"><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></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>...
|
||
import numpy as np # type: ignore
|
||
import tcod
|
||
|
||
from actions import Action, MeleeAction, MovementAction, WaitAction
|
||
<span class="crossed-out-text">from components.base_component import BaseComponent</span>
|
||
|
||
if TYPE_CHECKING:
|
||
from entity import Actor
|
||
|
||
|
||
<span class="crossed-out-text">class BaseAI(Action, BaseComponent):</span>
|
||
<span class="new-text">class BaseAI(Action):</span>
|
||
<span class="crossed-out-text">entity: Actor</span>
|
||
|
||
def perform(self) -> None:
|
||
raise NotImplementedError()
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><code>message_log.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 typing import Iterable, List, Reversible, Tuple
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">-from typing import List, Reversible, Tuple
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>import textwrap
|
||
</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>import color
|
||
</span></span><span style="display:flex;"><span>...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>class MessageLog:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> def render(
|
||
</span></span><span style="display:flex;"><span> self, console: tcod.Console, x: int, y: int, width: int, height: int,
|
||
</span></span><span style="display:flex;"><span> ) -> None:
|
||
</span></span><span style="display:flex;"><span> """Render this log over the given area.
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> `x`, `y`, `width`, `height` is the rectangular region to render onto
|
||
</span></span><span style="display:flex;"><span> the `console`.
|
||
</span></span><span style="display:flex;"><span> """
|
||
</span></span><span style="display:flex;"><span> self.render_messages(console, x, y, width, height, self.messages)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ @staticmethod
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def wrap(string: str, width: int) -> Iterable[str]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Return a wrapped text message."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for line in string.splitlines(): # Handle newlines in messages.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ yield from textwrap.wrap(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ line, width, expand_tabs=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:#f92672">- @staticmethod
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ @classmethod
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> def render_messages(
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ cls,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> console: tcod.Console,
|
||
</span></span><span style="display:flex;"><span> x: int,
|
||
</span></span><span style="display:flex;"><span> y: int,
|
||
</span></span><span style="display:flex;"><span> width: int,
|
||
</span></span><span style="display:flex;"><span> height: int,
|
||
</span></span><span style="display:flex;"><span> messages: Reversible[Message],
|
||
</span></span><span style="display:flex;"><span> ) -> None:
|
||
</span></span><span style="display:flex;"><span> """Render the messages provided.
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> The `messages` are rendered starting at the last message and working
|
||
</span></span><span style="display:flex;"><span> backwards.
|
||
</span></span><span style="display:flex;"><span> """
|
||
</span></span><span style="display:flex;"><span> y_offset = height - 1
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> for message in reversed(messages):
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- for line in reversed(textwrap.wrap(message.full_text, width)):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ for line in reversed(list(cls.wrap(message.full_text, width))):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> console.print(x=x, y=y + y_offset, string=line, fg=message.fg)
|
||
</span></span><span style="display:flex;"><span> y_offset -= 1
|
||
</span></span><span style="display:flex;"><span> if y_offset < 0:
|
||
</span></span><span style="display:flex;"><span> return # No more space to print messages.
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre><span class="new-text">from typing import Iterable, List, Reversible, Tuple</span>
|
||
<span class="crossed-out-text">from typing import List, Reversible, Tuple</span>
|
||
import textwrap
|
||
|
||
import tcod
|
||
|
||
import color
|
||
...
|
||
|
||
|
||
class MessageLog:
|
||
...
|
||
|
||
def render(
|
||
self, console: tcod.Console, x: int, y: int, width: int, height: int,
|
||
) -> None:
|
||
"""Render this log over the given area.
|
||
|
||
`x`, `y`, `width`, `height` is the rectangular region to render onto
|
||
the `console`.
|
||
"""
|
||
self.render_messages(console, x, y, width, height, self.messages)
|
||
|
||
<span class="new-text">@staticmethod
|
||
def wrap(string: str, width: int) -> Iterable[str]:
|
||
"""Return a wrapped text message."""
|
||
for line in string.splitlines(): # Handle newlines in messages.
|
||
yield from textwrap.wrap(
|
||
line, width, expand_tabs=True,
|
||
)</span>
|
||
|
||
<span class="crossed-out-text">@staticmethod</span>
|
||
<span class="new-text">@classmethod</span>
|
||
def render_messages(
|
||
<span class="new-text">cls,</span>
|
||
console: tcod.Console,
|
||
x: int,
|
||
y: int,
|
||
width: int,
|
||
height: int,
|
||
messages: Reversible[Message],
|
||
) -> None:
|
||
"""Render the messages provided.
|
||
|
||
The `messages` are rendered starting at the last message and working
|
||
backwards.
|
||
"""
|
||
y_offset = height - 1
|
||
|
||
for message in reversed(messages):
|
||
<span class="crossed-out-text">for line in reversed(textwrap.wrap(message.full_text, width)):</span>
|
||
<span class="new-text">for line in reversed(list(cls.wrap(message.full_text, width))):</span>
|
||
console.print(x=x, y=y + y_offset, string=line, fg=message.fg)
|
||
y_offset -= 1
|
||
if y_offset < 0:
|
||
return # No more space to print messages.</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>class GameMap:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span> ) # Tiles the player has seen before
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ @property
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def gamemap(self) -> GameMap:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> @property
|
||
</span></span><span style="display:flex;"><span> def actors(self) -> Iterator[Actor]:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class GameMap:
|
||
...
|
||
) # Tiles the player has seen before
|
||
|
||
<span class="new-text">@property
|
||
def gamemap(self) -> GameMap:
|
||
return self</span>
|
||
|
||
@property
|
||
def actors(self) -> Iterator[Actor]:
|
||
...</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>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:#f92672">- gamemap: GameMap
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ parent: 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:#f92672">- gamemap: Optional[GameMap] = None,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ parent: 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> render_order: RenderOrder = RenderOrder.CORPSE,
|
||
</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> self.render_order = render_order
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if gamemap:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- # If gamemap isn't provided now then it will be set later.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.gamemap = gamemap
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- gamemap.entities.add(self)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ if parent:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # If parent isn't provided now then it will be set later.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent = parent
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ parent.entities.add(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 gamemap(self) -> GameMap:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.parent.gamemap
|
||
</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:#f92672">- clone.gamemap = gamemap
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ clone.parent = 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> def place(self, x: int, y: int, gamemap: Optional[GameMap] = None) -> None:
|
||
</span></span><span style="display:flex;"><span> """Place this entity at a new location. Handles moving across GameMaps."""
|
||
</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> if gamemap:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- if hasattr(self, "gamemap"): # Possibly uninitialized.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.gamemap.entities.remove(self)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.gamemap = gamemap
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ if hasattr(self, "parent"): # Possibly uninitialized.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.parent is self.gamemap:
|
||
</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.parent = gamemap
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> gamemap.entities.add(self)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> def move(self, dx: int, dy: int) -> None:
|
||
</span></span><span style="display:flex;"><span> # Move the entity by a given amount
|
||
</span></span><span style="display:flex;"><span> self.x += dx
|
||
</span></span><span style="display:flex;"><span> self.y += dy
|
||
</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> render_order=RenderOrder.ACTOR,
|
||
</span></span><span style="display:flex;"><span> )
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self.ai: Optional[BaseAI] = ai_cls(self)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self.fighter = fighter
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.fighter.entity = self
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ self.fighter.parent = self
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> @property
|
||
</span></span><span style="display:flex;"><span> def is_alive(self) -> bool:
|
||
</span></span><span style="display:flex;"><span> """Returns True as long as this actor can perform actions."""
|
||
</span></span><span style="display:flex;"><span> return bool(self.ai)
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class Entity:
|
||
"""
|
||
A generic object to represent players, enemies, items, etc.
|
||
"""
|
||
|
||
<span class="crossed-out-text">gamemap: GameMap</span>
|
||
<span class="new-text">parent: GameMap</span>
|
||
|
||
def __init__(
|
||
self,
|
||
<span class="crossed-out-text">gamemap: Optional[GameMap] = None,</span>
|
||
<span class="new-text">parent: 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,
|
||
render_order: RenderOrder = RenderOrder.CORPSE,
|
||
):
|
||
self.x = x
|
||
self.y = y
|
||
self.char = char
|
||
self.color = color
|
||
self.name = name
|
||
self.blocks_movement = blocks_movement
|
||
self.render_order = render_order
|
||
<span class="crossed-out-text">if gamemap:</span>
|
||
<span class="crossed-out-text"># If gamemap isn't provided now then it will be set later.</span>
|
||
<span class="crossed-out-text">self.gamemap = gamemap</span>
|
||
<span class="crossed-out-text">gamemap.entities.add(self)</span>
|
||
<span class="new-text">if parent:
|
||
# If parent isn't provided now then it will be set later.
|
||
self.parent = parent
|
||
parent.entities.add(self)
|
||
|
||
@property
|
||
def gamemap(self) -> GameMap:
|
||
return self.parent.gamemap</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="crossed-out-text">clone.gamemap = gamemap</span>
|
||
<span class="new-text">clone.parent = gamemap</span>
|
||
gamemap.entities.add(clone)
|
||
return clone
|
||
|
||
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:
|
||
<span class="crossed-out-text">if hasattr(self, "gamemap"): # Possibly uninitialized.</span>
|
||
<span class="crossed-out-text">self.gamemap.entities.remove(self)</span>
|
||
<span class="crossed-out-text">self.gamemap = gamemap</span>
|
||
<span class="new-text">if hasattr(self, "parent"): # Possibly uninitialized.
|
||
if self.parent is self.gamemap:
|
||
self.gamemap.entities.remove(self)
|
||
self.parent = gamemap</span>
|
||
gamemap.entities.add(self)
|
||
|
||
def move(self, dx: int, dy: int) -> None:
|
||
# Move the entity by a given amount
|
||
self.x += dx
|
||
self.y += dy
|
||
|
||
|
||
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,
|
||
render_order=RenderOrder.ACTOR,
|
||
)
|
||
|
||
self.ai: Optional[BaseAI] = ai_cls(self)
|
||
|
||
self.fighter = fighter
|
||
<span class="crossed-out-text">self.fighter.entity = self</span>
|
||
<span class="new-text">self.fighter.parent = self</span>
|
||
|
||
@property
|
||
def is_alive(self) -> bool:
|
||
"""Returns True as long as this actor can perform actions."""
|
||
return bool(self.ai)</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><code>base_component.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 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> from entity import Entity
|
||
</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 BaseComponent:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- entity: Entity # Owning entity instance.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ parent: Entity # Owning entity instance.
|
||
</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 gamemap(self) -> GameMap:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.parent.gamemap
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> @property
|
||
</span></span><span style="display:flex;"><span> def engine(self) -> Engine:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- return self.entity.gamemap.engine
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ return self.gamemap.engine
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>from __future__ import annotations
|
||
|
||
from typing import TYPE_CHECKING
|
||
|
||
if TYPE_CHECKING:
|
||
from engine import Engine
|
||
from entity import Entity
|
||
<span class="new-text">from game_map import GameMap</span>
|
||
|
||
|
||
class BaseComponent:
|
||
<span class="crossed-out-text">entity: Entity # Owning entity instance.</span>
|
||
<span class="new-text">parent: Entity # Owning entity instance.
|
||
|
||
@property
|
||
def gamemap(self) -> GameMap:
|
||
return self.parent.gamemap</span>
|
||
|
||
@property
|
||
def engine(self) -> Engine:
|
||
<span class="crossed-out-text">return self.entity.gamemap.engine</span>
|
||
<span class="new-text">return self.gamemap.engine</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><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>class Fighter(BaseComponent):
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- entity: Actor
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ parent: 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:#f92672">- if self._hp == 0 and self.entity.ai:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ if self._hp == 0 and self.parent.ai:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.die()
|
||
</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><span style="color:#f92672">- if self.engine.player is self.entity:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ if self.engine.player is self.parent:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> death_message = "You died!"
|
||
</span></span><span style="display:flex;"><span> death_message_color = color.player_die
|
||
</span></span><span style="display:flex;"><span> self.engine.event_handler = GameOverEventHandler(self.engine)
|
||
</span></span><span style="display:flex;"><span> else:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- death_message = f"{self.entity.name} is dead!"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ death_message = f"{self.parent.name} is dead!"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> death_message_color = color.enemy_die
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent.char = "%"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent.color = (191, 0, 0)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent.blocks_movement = False
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent.ai = None
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent.name = f"remains of {self.parent.name}"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.parent.render_order = RenderOrder.CORPSE
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- self.entity.char = "%"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.entity.color = (191, 0, 0)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.entity.blocks_movement = False
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.entity.ai = None
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.entity.name = f"remains of {self.entity.name}"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.entity.render_order = RenderOrder.CORPSE
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span> self.engine.message_log.add_message(death_message, death_message_color)
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class Fighter(BaseComponent):
|
||
<span class="crossed-out-text">entity: Actor</span>
|
||
<span class="new-text">parent: 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="crossed-out-text">if self._hp == 0 and self.entity.ai:</span>
|
||
<span class="new-text">if self._hp == 0 and self.parent.ai:</span>
|
||
self.die()
|
||
|
||
def die(self) -> None:
|
||
<span class="crossed-out-text">if self.engine.player is self.entity:</span>
|
||
<span class="new-text">if self.engine.player is self.parent:</span>
|
||
death_message = "You died!"
|
||
death_message_color = color.player_die
|
||
self.engine.event_handler = GameOverEventHandler(self.engine)
|
||
else:
|
||
<span class="crossed-out-text">death_message = f"{self.entity.name} is dead!"</span>
|
||
<span class="new-text">death_message = f"{self.parent.name} is dead!"</span>
|
||
death_message_color = color.enemy_die
|
||
|
||
<span class="new-text">self.parent.char = "%"
|
||
self.parent.color = (191, 0, 0)
|
||
self.parent.blocks_movement = False
|
||
self.parent.ai = None
|
||
self.parent.name = f"remains of {self.parent.name}"
|
||
self.parent.render_order = RenderOrder.CORPSE</span>
|
||
<span class="crossed-out-text">self.entity.char = "%"</span>
|
||
<span class="crossed-out-text">self.entity.color = (191, 0, 0)</span>
|
||
<span class="crossed-out-text">self.entity.blocks_movement = False</span>
|
||
<span class="crossed-out-text">self.entity.ai = None</span>
|
||
<span class="crossed-out-text">self.entity.name = f"remains of {self.entity.name}"</span>
|
||
<span class="crossed-out-text">self.entity.render_order = RenderOrder.CORPSE</span>
|
||
|
||
self.engine.message_log.add_message(death_message, death_message_color)</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<h2 id="part-8">
|
||
Part 8
|
||
<a class="heading-link" href="#part-8">
|
||
<i class="fa fa-link" aria-hidden="true" title="Link to heading"></i>
|
||
<span class="sr-only">Link to heading</span>
|
||
</a>
|
||
</h2>
|
||
<p>So far, our game has movement, dungeon exploring, combat, and AI (okay, we’re stretching the meaning of “intelligence” in <em>artificial intelligence</em>
|
||
to its limits, but bear with me here). Now it’s time for another staple
|
||
of the roguelike genre: items! Why would our rogue venture into the
|
||
dungeons of doom if not for some sweet loot, after all?</p>
|
||
<p>In this part of the tutorial, we’ll achieve a few things: a working
|
||
inventory, and a functioning healing potion. The next part will add more
|
||
items that can be picked up, but for now, just the healing potion will
|
||
suffice.</p>
|
||
<p>For this part, we’ll need four more colors. Let’s get adding those out of the way now. Open up <code>color.py</code> and add these colors:</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>white = (0xFF, 0xFF, 0xFF)
|
||
</span></span><span style="display:flex;"><span>black = (0x0, 0x0, 0x0)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>player_atk = (0xE0, 0xE0, 0xE0)
|
||
</span></span><span style="display:flex;"><span>enemy_atk = (0xFF, 0xC0, 0xC0)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>player_die = (0xFF, 0x30, 0x30)
|
||
</span></span><span style="display:flex;"><span>enemy_die = (0xFF, 0xA0, 0x30)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+invalid = (0xFF, 0xFF, 0x00)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+impossible = (0x80, 0x80, 0x80)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+error = (0xFF, 0x40, 0x40)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>welcome_text = (0x20, 0xA0, 0xFF)
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+health_recovered = (0x0, 0xFF, 0x0)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>bar_text = white
|
||
</span></span><span style="display:flex;"><span>bar_filled = (0x0, 0x60, 0x0)
|
||
</span></span><span style="display:flex;"><span>bar_empty = (0x40, 0x10, 0x10)
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>white = (0xFF, 0xFF, 0xFF)
|
||
black = (0x0, 0x0, 0x0)
|
||
|
||
player_atk = (0xE0, 0xE0, 0xE0)
|
||
enemy_atk = (0xFF, 0xC0, 0xC0)
|
||
|
||
player_die = (0xFF, 0x30, 0x30)
|
||
enemy_die = (0xFF, 0xA0, 0x30)
|
||
|
||
<span class="new-text">invalid = (0xFF, 0xFF, 0x00)
|
||
impossible = (0x80, 0x80, 0x80)
|
||
error = (0xFF, 0x40, 0x40)</span>
|
||
|
||
welcome_text = (0x20, 0xA0, 0xFF)
|
||
<span class="new-text">health_recovered = (0x0, 0xFF, 0x0)</span>
|
||
|
||
bar_text = white
|
||
bar_filled = (0x0, 0x60, 0x0)
|
||
bar_empty = (0x40, 0x10, 0x10)</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>These will become useful shortly.</p>
|
||
<p>There’s another thing we can knock out right now that we’ll use later: The ability for a <code>Fighter</code>
|
||
component to recover health, and the ability to take damage directly
|
||
(without the defense modifier). We won’t use the damage function this
|
||
chapter, but since the two functions are effectively opposites, we can
|
||
get writing it over with now.</p>
|
||
<p>Open up <code>fighter.py</code> and add these two functions:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class Fighter:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def heal(self, amount: int) -> int:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.hp == self.max_hp:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return 0
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ new_hp_value = self.hp + amount
|
||
</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 new_hp_value > self.max_hp:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ new_hp_value = self.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">+ amount_recovered = new_hp_value - self.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">+ self.hp = new_hp_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">+ return amount_recovered
|
||
</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 take_damage(self, amount: int) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.hp -= amount
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class Fighter:
|
||
...
|
||
<span class="new-text">def heal(self, amount: int) -> int:
|
||
if self.hp == self.max_hp:
|
||
return 0
|
||
|
||
new_hp_value = self.hp + amount
|
||
|
||
if new_hp_value > self.max_hp:
|
||
new_hp_value = self.max_hp
|
||
|
||
amount_recovered = new_hp_value - self.hp
|
||
|
||
self.hp = new_hp_value
|
||
|
||
return amount_recovered
|
||
|
||
def take_damage(self, amount: int) -> None:
|
||
self.hp -= amount</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><code>heal</code> will restore a certain amount of HP, up to the
|
||
maximum, and return the amount that was healed. If the entity’s health
|
||
is at full, then just return 0. The function that handles this should
|
||
display an error if the returned amount is 0, since the entity can’t be
|
||
healed.</p>
|
||
<p>One thing we’re going to need is a way to <em>not</em> consume an
|
||
item or take a turn if something goes wrong during the process. For our
|
||
health potion, think about what should happen if the player declares
|
||
they want to use a health potion, but their health is already full. What
|
||
should happen?</p>
|
||
<p>We could just consume the potion anyway, and have it go to waste, but
|
||
if you’ve played a game that does that, you know how frustrating it can
|
||
be, especially if the player clicked the health potion on accident. A
|
||
better way would be to warn the user that they’re trying to do something
|
||
that makes no sense, and save the player from wasting both the potion
|
||
and their turn.</p>
|
||
<p>But how can we achieve that? We’ll discuss it a bit more later on,
|
||
but the idea is that if we do something impossible, we should raise an
|
||
exception. Which one? Well, we can define a custom exception, which can
|
||
give us details on what happened. Create a new file called <code>exceptions.py</code> and put the following class 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:#66d9ef">class</span> <span style="color:#a6e22e">Impossible</span>(<span style="color:#a6e22e">Exception</span>):
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Exception raised when an action is impossible to be performed.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> The reason is given as the exception message.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> """</span>
|
||
</span></span></code></pre></div><p>… And that’s it! When we write <code>raise Impossible("An exception message")</code> in our program, the <code>Impossible</code> exception will be raised, with the given message.</p>
|
||
<p>So what do we do with the raised exception? Well, we should catch it! But where?</p>
|
||
<p>Let’s modify the <code>main.py</code> file to catch the exceptions, 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>#!/usr/bin/env python3
|
||
</span></span><span style="display:flex;"><span>import copy
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import traceback
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>import tcod
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> context.present(root_console)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ try:
|
||
</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">+ context.convert_event(event)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine.event_handler.handle_events(event)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ except Exception: # Handle exceptions in game.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ traceback.print_exc() # Print error to stderr.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Then print the error to the message log.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine.message_log.add_message(traceback.format_exc(), color.error)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- engine.event_handler.handle_events(context)
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>#!/usr/bin/env python3
|
||
import copy
|
||
<span class="new-text">import traceback</span>
|
||
|
||
import tcod
|
||
|
||
...
|
||
|
||
context.present(root_console)
|
||
|
||
<span class="new-text">try:
|
||
for event in tcod.event.wait():
|
||
context.convert_event(event)
|
||
engine.event_handler.handle_events(event)
|
||
except Exception: # Handle exceptions in game.
|
||
traceback.print_exc() # Print error to stderr.
|
||
# Then print the error to the message log.
|
||
engine.message_log.add_message(traceback.format_exc(), color.error)</span>
|
||
<span class="crossed-out-text">engine.event_handler.handle_events(context)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>This is a generalized, catch all solution, which will print <em>all</em> exceptions to the message log, not just instances of <code>Impossible</code>. This can be helpful for debugging your game, or getting error reports from users.</p>
|
||
<p>However, this solution doesn’t mesh with our current implementation of the <code>EventHandler</code>. <code>EventHandler</code> currently loops through the events and converts them (to get the mouse information). We’ll need to edit a few things in <code>input_handlers.py</code> to get back on track.</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>import tcod
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from actions import (
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ Action,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ BumpAction,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ EscapeAction,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ WaitAction
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import color
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import exceptions
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">-from actions import Action, BumpAction, EscapeAction, WaitAction
|
||
</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 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, event: tcod.event.Event) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.handle_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">+ def handle_action(self, action: Optional[Action]) -> bool:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Handle actions returned from event methods.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ Returns True if the action will advance a turn.
|
||
</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">+ return False
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ try:
|
||
</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">+ except exceptions.Impossible as exc:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.message_log.add_message(exc.args[0], color.impossible)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return False # Skip enemy turn on exceptions.
|
||
</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"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.update_fov()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return True
|
||
</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 handle_events(self, context: tcod.context.Context) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- for event in tcod.event.wait():
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- context.convert_event(event)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- self.dispatch(event)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>class MainGameEventHandler(EventHandler):
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def handle_events(self, context: tcod.context.Context) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- for event in tcod.event.wait():
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- context.convert_event(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">- action = self.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()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.engine.handle_enemy_turns()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.engine.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> ...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>class GameOverEventHandler(EventHandler):
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def handle_events(self, context: tcod.context.Context) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- for event in tcod.event.wait():
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- action = self.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()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- action: Optional[Action] = None
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- key = event.sym
|
||
</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 key == tcod.event.K_ESCAPE:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- action = EscapeAction(self.engine.player)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- # No valid key was pressed
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- return action
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def ev_keydown(self, event: tcod.event.KeyDown) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if event.sym == tcod.event.K_ESCAPE:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise SystemExit()
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>import tcod
|
||
|
||
<span class="new-text">from actions import (
|
||
Action,
|
||
BumpAction,
|
||
EscapeAction,
|
||
WaitAction
|
||
)
|
||
import color
|
||
import exceptions</span>
|
||
<span class="crossed-out-text">from actions import Action, BumpAction, EscapeAction, WaitAction</span>
|
||
|
||
|
||
class EventHandler(tcod.event.EventDispatch[Action]):
|
||
def __init__(self, engine: Engine):
|
||
self.engine = engine
|
||
|
||
<span class="new-text">def handle_events(self, event: tcod.event.Event) -> None:
|
||
self.handle_action(self.dispatch(event))
|
||
|
||
def handle_action(self, action: Optional[Action]) -> bool:
|
||
"""Handle actions returned from event methods.
|
||
|
||
Returns True if the action will advance a turn.
|
||
"""
|
||
if action is None:
|
||
return False
|
||
|
||
try:
|
||
action.perform()
|
||
except exceptions.Impossible as exc:
|
||
self.engine.message_log.add_message(exc.args[0], color.impossible)
|
||
return False # Skip enemy turn on exceptions.
|
||
|
||
self.engine.handle_enemy_turns()
|
||
|
||
self.engine.update_fov()
|
||
return True</span>
|
||
|
||
<span class="crossed-out-text">def handle_events(self, context: tcod.context.Context) -> None:</span>
|
||
<span class="crossed-out-text">for event in tcod.event.wait():</span>
|
||
<span class="crossed-out-text">context.convert_event(event)</span>
|
||
<span class="crossed-out-text">self.dispatch(event)</span>
|
||
|
||
...
|
||
|
||
|
||
class MainGameEventHandler(EventHandler):
|
||
<span class="crossed-out-text">def handle_events(self, context: tcod.context.Context) -> None:</span>
|
||
<span class="crossed-out-text">for event in tcod.event.wait():</span>
|
||
<span class="crossed-out-text">context.convert_event(event)</span>
|
||
|
||
<span class="crossed-out-text">action = self.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()</span>
|
||
|
||
<span class="crossed-out-text">self.engine.handle_enemy_turns()</span>
|
||
|
||
<span class="crossed-out-text">self.engine.update_fov() # Update the FOV before the players next action.</span>
|
||
|
||
...
|
||
|
||
class GameOverEventHandler(EventHandler):
|
||
<span class="crossed-out-text">def handle_events(self, context: tcod.context.Context) -> None:</span>
|
||
<span class="crossed-out-text">for event in tcod.event.wait():</span>
|
||
<span class="crossed-out-text">action = self.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()</span>
|
||
|
||
<span class="crossed-out-text">def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:</span>
|
||
<span class="crossed-out-text">action: Optional[Action] = None</span>
|
||
|
||
<span class="crossed-out-text">key = event.sym</span>
|
||
|
||
<span class="crossed-out-text">if key == tcod.event.K_ESCAPE:</span>
|
||
<span class="crossed-out-text">action = EscapeAction(self.engine.player)</span>
|
||
|
||
<span class="crossed-out-text"># No valid key was pressed</span>
|
||
<span class="crossed-out-text">return action</span>
|
||
<span class="new-text">def ev_keydown(self, event: tcod.event.KeyDown) -> None:
|
||
if event.sym == tcod.event.K_ESCAPE:
|
||
raise SystemExit()</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Now that we’ve got our event handlers updated, let’s actually put the <code>Impossible</code> exception to good use. We can start by editing <code>actions.py</code> to make use of it when the player tries to move into an invalid area:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>...
|
||
</span></span><span style="display:flex;"><span>import color
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import exceptions
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>if TYPE_CHECKING:
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>class MeleeAction(ActionWithDirection):
|
||
</span></span><span style="display:flex;"><span> def perform(self) -> None:
|
||
</span></span><span style="display:flex;"><span> target = self.target_actor
|
||
</span></span><span style="display:flex;"><span> if not target:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- return # No entity to attack.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ raise exceptions.Impossible("Nothing to attack.")
|
||
</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>class MovementAction(ActionWithDirection):
|
||
</span></span><span style="display:flex;"><span> def perform(self) -> None:
|
||
</span></span><span style="display:flex;"><span> dest_x, dest_y = self.dest_xy
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> if not self.engine.game_map.in_bounds(dest_x, dest_y):
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Destination is out of bounds.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise exceptions.Impossible("That way is blocked.")
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- return # Destination is out of bounds.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> if not self.engine.game_map.tiles["walkable"][dest_x, dest_y]:
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Destination is blocked by a tile.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise exceptions.Impossible("That way is blocked.")
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- return # Destination is blocked by a tile.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> if self.engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Destination is blocked by an entity.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise exceptions.Impossible("That way is blocked.")
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- return # Destination is blocked by an entity.
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>...
|
||
import color
|
||
<span class="new-text">import exceptions</span>
|
||
|
||
if TYPE_CHECKING:
|
||
...
|
||
|
||
|
||
class MeleeAction(ActionWithDirection):
|
||
def perform(self) -> None:
|
||
target = self.target_actor
|
||
if not target:
|
||
<span class="crossed-out-text">return # No entity to attack.</span>
|
||
<span class="new-text">raise exceptions.Impossible("Nothing to attack.")</span>
|
||
|
||
...
|
||
|
||
class MovementAction(ActionWithDirection):
|
||
def perform(self) -> None:
|
||
dest_x, dest_y = self.dest_xy
|
||
|
||
if not self.engine.game_map.in_bounds(dest_x, dest_y):
|
||
<span class="new-text"># Destination is out of bounds.
|
||
raise exceptions.Impossible("That way is blocked.")</span>
|
||
<span class="crossed-out-text">return # Destination is out of bounds.</span>
|
||
if not self.engine.game_map.tiles["walkable"][dest_x, dest_y]:
|
||
<span class="new-text"># Destination is blocked by a tile.
|
||
raise exceptions.Impossible("That way is blocked.")</span>
|
||
<span class="crossed-out-text">return # Destination is blocked by a tile.</span>
|
||
if self.engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
|
||
<span class="new-text"># Destination is blocked by an entity.
|
||
raise exceptions.Impossible("That way is blocked.")</span>
|
||
<span class="crossed-out-text">return # Destination is blocked by an entity.</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Now, if you try moving into a wall, you’ll get a message in the log, and the player’s turn won’t be wasted.</p>
|
||
<p>So what about when the enemies try doing something impossible? You
|
||
might want to know when that happens for debugging purposes, but during
|
||
normal execution of our game, we can simply ignore it, and have the
|
||
enemy skip their turn. To do this, modify <code>engine.py</code> like this:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>...
|
||
</span></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:#a6e22e">+import exceptions
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from input_handlers import MainGameEventHandler
|
||
</span></span><span style="display:flex;"><span>from message_log import MessageLog
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> def handle_enemy_turns(self) -> None:
|
||
</span></span><span style="display:flex;"><span> for entity in set(self.game_map.actors) - {self.player}:
|
||
</span></span><span style="display:flex;"><span> if entity.ai:
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ try:
|
||
</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">+ except exceptions.Impossible:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ pass # Ignore impossible action exceptions from AI.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- entity.ai.perform()
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>...
|
||
from tcod.map import compute_fov
|
||
|
||
<span class="new-text">import exceptions</span>
|
||
from input_handlers import MainGameEventHandler
|
||
from message_log import MessageLog
|
||
|
||
...
|
||
|
||
def handle_enemy_turns(self) -> None:
|
||
for entity in set(self.game_map.actors) - {self.player}:
|
||
if entity.ai:
|
||
<span class="new-text">try:
|
||
entity.ai.perform()
|
||
except exceptions.Impossible:
|
||
pass # Ignore impossible action exceptions from AI.</span>
|
||
<span class="crossed-out-text">entity.ai.perform()</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>This is great and all, but wasn’t this chapter supposed to be about
|
||
implementing items? And, yes, that’s true, and we’re going to transition
|
||
to that now, but it’ll be helpful to have a way to stop the player from
|
||
wasting a turn in just a moment.</p>
|
||
<p>The way we’ll implement our health potions will be similar to how we
|
||
implemented enemies: We’ll create a component that holds the
|
||
functionality we want, and we’ll create a subclass of <code>Entity</code> that holds the relevant component. From the <code>Consumable</code>
|
||
component, we can create subclasses that implement the specific
|
||
functionality we want for each item. In this case, it’ll be a health
|
||
potion, but in the next chapter, we’ll be implementing other types of
|
||
consumables, so we’ll want to stay flexible.</p>
|
||
<p>In the <code>components</code> directory, create a file called <code>consumable.py</code> and fill it with the following contents:</p>
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#f92672">from</span> __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> Optional, TYPE_CHECKING
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> actions
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> color
|
||
</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 style="color:#f92672">from</span> exceptions <span style="color:#f92672">import</span> Impossible
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> TYPE_CHECKING:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">from</span> entity <span style="color:#f92672">import</span> Actor, Item
|
||
</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">Consumable</span>(BaseComponent):
|
||
</span></span><span style="display:flex;"><span> parent: Item
|
||
</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_action</span>(self, consumer: Actor) <span style="color:#f92672">-></span> Optional[actions<span style="color:#f92672">.</span>Action]:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Try to return the action for this item."""</span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> actions<span style="color:#f92672">.</span>ItemAction(consumer, self<span style="color:#f92672">.</span>parent)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">activate</span>(self, action: actions<span style="color:#f92672">.</span>ItemAction) <span style="color:#f92672">-></span> <span style="color:#66d9ef">None</span>:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Invoke this items ability.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> `action` is the context for this activation.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> """</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></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">HealingConsumable</span>(Consumable):
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> __init__(self, amount: int):
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>amount <span style="color:#f92672">=</span> amount
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">activate</span>(self, action: actions<span style="color:#f92672">.</span>ItemAction) <span style="color:#f92672">-></span> <span style="color:#66d9ef">None</span>:
|
||
</span></span><span style="display:flex;"><span> consumer <span style="color:#f92672">=</span> action<span style="color:#f92672">.</span>entity
|
||
</span></span><span style="display:flex;"><span> amount_recovered <span style="color:#f92672">=</span> consumer<span style="color:#f92672">.</span>fighter<span style="color:#f92672">.</span>heal(self<span style="color:#f92672">.</span>amount)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> amount_recovered <span style="color:#f92672">></span> <span style="color:#ae81ff">0</span>:
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>engine<span style="color:#f92672">.</span>message_log<span style="color:#f92672">.</span>add_message(
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">f</span><span style="color:#e6db74">"You consume the </span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>parent<span style="color:#f92672">.</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74">, and recover </span><span style="color:#e6db74">{</span>amount_recovered<span style="color:#e6db74">}</span><span style="color:#e6db74"> HP!"</span>,
|
||
</span></span><span style="display:flex;"><span> color<span style="color:#f92672">.</span>health_recovered,
|
||
</span></span><span style="display:flex;"><span> )
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">else</span>:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">raise</span> Impossible(<span style="color:#e6db74">f</span><span style="color:#e6db74">"Your health is already full."</span>)
|
||
</span></span></code></pre></div><p>The <code>Consumable</code> class knows its parent, and it defines two methods: <code>get_action</code> and <code>activate</code>.</p>
|
||
<p><code>get_action</code> gets <code>ItemAction</code>, which we haven’t defined just yet (we will soon). Subclasses can override this to provide more information to <code>ItemAction</code> if needed, such as the position of a potential target (this will be useful when we have ranged targeting).</p>
|
||
<p><code>activate</code> is just an abstract method, it’s up to the
|
||
subclasses to define their own implementation. The subclasses should
|
||
call this method when they’re trying to actually cause the effect that
|
||
they’ve defined for themselves (healing for healing potions, damage for
|
||
lightning scrolls, etc.).</p>
|
||
<p><code>HealingConsumable</code> is initialized with an <code>amount</code>, which is how much the user will be healed when using the item. The <code>activate</code> function calls <code>fighter.heal</code>,
|
||
and logs a message to the message log, if the entity recovered health.
|
||
If not (because the user had full health already), we return that <code>Impossible</code>
|
||
exception we defined earlier. This will give us a message in the log
|
||
that the player’s health is already full, and it won’t waste the health
|
||
potion.</p>
|
||
<p>So what does this component get attached to? In order to create our health potions, we can create another subclass of <code>Entity</code>, which will represent non-actor items. Open up <code>entity.py</code> and add the following class:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>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 components.ai import BaseAI
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from components.consumable import Consumable
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></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>...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span> @property
|
||
</span></span><span style="display:flex;"><span> def is_alive(self) -> bool:
|
||
</span></span><span style="display:flex;"><span> """Returns True as long as this actor can perform actions."""
|
||
</span></span><span style="display:flex;"><span> return bool(self.ai)
|
||
</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 Item(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">+ consumable: Consumable,
|
||
</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=False,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ render_order=RenderOrder.ITEM,
|
||
</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.consumable = consumable
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.consumable.parent = self
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>from render_order import RenderOrder
|
||
|
||
if TYPE_CHECKING:
|
||
from components.ai import BaseAI
|
||
<span class="new-text">from components.consumable import Consumable</span>
|
||
from components.fighter import Fighter
|
||
from game_map import GameMap
|
||
|
||
...
|
||
|
||
...
|
||
@property
|
||
def is_alive(self) -> bool:
|
||
"""Returns True as long as this actor can perform actions."""
|
||
return bool(self.ai)
|
||
|
||
|
||
<span class="new-text">class Item(Entity):
|
||
def __init__(
|
||
self,
|
||
*,
|
||
x: int = 0,
|
||
y: int = 0,
|
||
char: str = "?",
|
||
color: Tuple[int, int, int] = (255, 255, 255),
|
||
name: str = "<Unnamed>",
|
||
consumable: Consumable,
|
||
):
|
||
super().__init__(
|
||
x=x,
|
||
y=y,
|
||
char=char,
|
||
color=color,
|
||
name=name,
|
||
blocks_movement=False,
|
||
render_order=RenderOrder.ITEM,
|
||
)
|
||
|
||
self.consumable = consumable
|
||
self.consumable.parent = self</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><code>Item</code> isn’t too different from <code>Actor</code>, except instead of implementing <code>fighter</code> and <code>ai</code>, it does <code>consumable</code>. When we create an item, we’ll assign the <code>consumable</code>, which will determine what actually happens when the item gets used.</p>
|
||
<p>The next thing we need to implement that we used in the <code>Consumable</code> class is the <code>ItemAction</code> class. Open up <code>actions.py</code> and put 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>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 Actor, Entity
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ from entity import Actor, Entity, Item
|
||
</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>class 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 ItemAction(Action):
|
||
</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, entity: Actor, item: Item, target_xy: Optional[Tuple[int, int]] = 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">+ super().__init__(entity)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.item = item
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if not target_xy:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ target_xy = entity.x, entity.y
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.target_xy = target_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">+ @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.target_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">+ """Invoke the items ability, this action will be given to provide context."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.item.consumable.activate(self)
|
||
</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 EscapeAction(Action):
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>if TYPE_CHECKING:
|
||
from engine import Engine
|
||
<span class="crossed-out-text">from entity import Actor, Entity</span>
|
||
<span class="new-text">from entity import Actor, Entity, Item</span>
|
||
...
|
||
|
||
|
||
class Action:
|
||
...
|
||
|
||
|
||
<span class="new-text">class ItemAction(Action):
|
||
def __init__(
|
||
self, entity: Actor, item: Item, target_xy: Optional[Tuple[int, int]] = None
|
||
):
|
||
super().__init__(entity)
|
||
self.item = item
|
||
if not target_xy:
|
||
target_xy = entity.x, entity.y
|
||
self.target_xy = target_xy
|
||
|
||
@property
|
||
def target_actor(self) -> Optional[Actor]:
|
||
"""Return the actor at this actions destination."""
|
||
return self.engine.game_map.get_actor_at_location(*self.target_xy)
|
||
|
||
def perform(self) -> None:
|
||
"""Invoke the items ability, this action will be given to provide context."""
|
||
self.item.consumable.activate(self)</span>
|
||
|
||
|
||
class EscapeAction(Action):
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><code>ItemAction</code> takes several arguments in its <code>__init__</code> function: <code>entity</code>, which is the entity using the item, <code>item</code>, which is the item itself, and <code>target_xy</code>,
|
||
which is the x and y coordinates of the “target” of the item, if there
|
||
is one. We won’t actually use this in this chapter, but it’ll come in
|
||
handy soon.</p>
|
||
<p><code>target_actor</code> gets the actor at the target location.
|
||
Again, we won’t actually use it this chapter, since health potions don’t
|
||
“target” anything.</p>
|
||
<p><code>perform</code> activates the consumable, with its <code>activate</code> method we defined earlier.</p>
|
||
<p>To utilize our new <code>Item</code>, let’s add the health potion to <code>entity_factories.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 components.ai import HostileEnemy
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from components.consumable import HealingConsumable
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from components.fighter import Fighter
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from entity import Actor
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from entity import Actor, Item
|
||
</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>player = Actor(
|
||
</span></span><span style="display:flex;"><span> char="@",
|
||
</span></span><span style="display:flex;"><span> color=(255, 255, 255),
|
||
</span></span><span style="display:flex;"><span> name="Player",
|
||
</span></span><span style="display:flex;"><span> ai_cls=HostileEnemy,
|
||
</span></span><span style="display:flex;"><span> fighter=Fighter(hp=30, defense=2, power=5),
|
||
</span></span><span style="display:flex;"><span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>orc = Actor(
|
||
</span></span><span style="display:flex;"><span> char="o",
|
||
</span></span><span style="display:flex;"><span> color=(63, 127, 63),
|
||
</span></span><span style="display:flex;"><span> name="Orc",
|
||
</span></span><span style="display:flex;"><span> ai_cls=HostileEnemy,
|
||
</span></span><span style="display:flex;"><span> fighter=Fighter(hp=10, defense=0, power=3),
|
||
</span></span><span style="display:flex;"><span>)
|
||
</span></span><span style="display:flex;"><span>troll = Actor(
|
||
</span></span><span style="display:flex;"><span> char="T",
|
||
</span></span><span style="display:flex;"><span> color=(0, 127, 0),
|
||
</span></span><span style="display:flex;"><span> name="Troll",
|
||
</span></span><span style="display:flex;"><span> ai_cls=HostileEnemy,
|
||
</span></span><span style="display:flex;"><span> fighter=Fighter(hp=16, defense=1, power=4),
|
||
</span></span><span style="display:flex;"><span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+health_potion = Item(
|
||
</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=(127, 0, 255),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ name="Health Potion",
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ consumable=HealingConsumable(amount=4),
|
||
</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 components.ai import HostileEnemy
|
||
<span class="new-text">from components.consumable import HealingConsumable</span>
|
||
from components.fighter import Fighter
|
||
<span class="crossed-out-text">from entity import Actor</span>
|
||
<span class="new-text">from entity import Actor, Item</span>
|
||
|
||
|
||
player = Actor(
|
||
char="@",
|
||
color=(255, 255, 255),
|
||
name="Player",
|
||
ai_cls=HostileEnemy,
|
||
fighter=Fighter(hp=30, defense=2, power=5),
|
||
)
|
||
|
||
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 class="new-text">health_potion = Item(
|
||
char="!",
|
||
color=(127, 0, 255),
|
||
name="Health Potion",
|
||
consumable=HealingConsumable(amount=4),
|
||
)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>We’re defining a new entity type, called <code>health_potion</code> (no surprises there), and utilizing the <code>Item</code> and <code>HealingConsumable</code>
|
||
classes we just wrote. The health potion will recover 4 HP of the
|
||
user’s health. Feel free to adjust that value however you see fit.</p>
|
||
<p>Alright, we’re now ready to put some health potions in the dungeon. As you may have already guessed, we’ll need to adjust the <code>generate_dungeon</code> and <code>place_entities</code> functions in <code>procgen.py</code> to actually put the potions in. Edit <code>procgen.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>def place_entities(
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- room: RectangularRoom, dungeon: GameMap, maximum_monsters: int,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ room: RectangularRoom, dungeon: GameMap, maximum_monsters: int, maximum_items: int
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>) -> None:
|
||
</span></span><span style="display:flex;"><span> number_of_monsters = random.randint(0, maximum_monsters)
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ number_of_items = random.randint(0, maximum_items)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> for i in range(number_of_monsters):
|
||
</span></span><span style="display:flex;"><span> x = random.randint(room.x1 + 1, room.x2 - 1)
|
||
</span></span><span style="display:flex;"><span> y = random.randint(room.y1 + 1, room.y2 - 1)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
|
||
</span></span><span style="display:flex;"><span> if random.random() < 0.8:
|
||
</span></span><span style="display:flex;"><span> entity_factories.orc.spawn(dungeon, x, y)
|
||
</span></span><span style="display:flex;"><span> else:
|
||
</span></span><span style="display:flex;"><span> entity_factories.troll.spawn(dungeon, x, y)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for i in range(number_of_items):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x = random.randint(room.x1 + 1, room.x2 - 1)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y = random.randint(room.y1 + 1, room.y2 - 1)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ entity_factories.health_potion.spawn(dungeon, x, y)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>def tunnel_between(
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><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> 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">+ max_items_per_room: int,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> engine: Engine,
|
||
</span></span><span style="display:flex;"><span>) -> GameMap:
|
||
</span></span><span style="display:flex;"><span> """Generate a new dungeon map."""
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- place_entities(new_room, dungeon, max_monsters_per_room)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ place_entities(new_room, dungeon, max_monsters_per_room, max_items_per_room)
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>def place_entities(
|
||
<span class="crossed-out-text">room: RectangularRoom, dungeon: GameMap, maximum_monsters: int,</span>
|
||
<span class="new-text">room: RectangularRoom, dungeon: GameMap, maximum_monsters: int, maximum_items: int</span>
|
||
) -> None:
|
||
number_of_monsters = random.randint(0, maximum_monsters)
|
||
<span class="new-text">number_of_items = random.randint(0, maximum_items)</span>
|
||
|
||
for i in range(number_of_monsters):
|
||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||
|
||
if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
|
||
if random.random() < 0.8:
|
||
entity_factories.orc.spawn(dungeon, x, y)
|
||
else:
|
||
entity_factories.troll.spawn(dungeon, x, y)
|
||
|
||
<span class="new-text">for i in range(number_of_items):
|
||
x = random.randint(room.x1 + 1, room.x2 - 1)
|
||
y = random.randint(room.y1 + 1, room.y2 - 1)
|
||
|
||
if not any(entity.x == x and entity.y == y for entity in dungeon.entities):
|
||
entity_factories.health_potion.spawn(dungeon, x, y)</span>
|
||
|
||
|
||
def tunnel_between(
|
||
...
|
||
|
||
|
||
def generate_dungeon(
|
||
map_width: int,
|
||
map_height: int,
|
||
max_monsters_per_room: int,
|
||
<span class="new-text">max_items_per_room: int,</span>
|
||
engine: Engine,
|
||
) -> GameMap:
|
||
"""Generate a new dungeon map."""
|
||
...
|
||
|
||
...
|
||
<span class="crossed-out-text">place_entities(new_room, dungeon, max_monsters_per_room)</span>
|
||
<span class="new-text">place_entities(new_room, dungeon, max_monsters_per_room, max_items_per_room)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>We’re doing essentially the same thing we did to create our enemies:
|
||
Giving a maximum possible number for the number of items in each room,
|
||
selecting a random number between that and 0, and spawning the items in a
|
||
random spot in the room, assuming nothing else already exists there.</p>
|
||
<p>Lastly, to make the health potions appear, we need to update our call in <code>main.py</code> to <code>generate_dungeon</code>, since we’ve added the <code>max_items_per_room</code> argument. Open up <code>main.py</code> and add the following lines:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span> max_monsters_per_room = 2
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_items_per_room = 2
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> tileset = tcod.tileset.load_tilesheet(
|
||
</span></span><span style="display:flex;"><span> "dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD
|
||
</span></span><span style="display:flex;"><span> )
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> player = copy.deepcopy(entity_factories.player)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> engine = Engine(player=player)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> engine.game_map = generate_dungeon(
|
||
</span></span><span style="display:flex;"><span> max_rooms=max_rooms,
|
||
</span></span><span style="display:flex;"><span> room_min_size=room_min_size,
|
||
</span></span><span style="display:flex;"><span> room_max_size=room_max_size,
|
||
</span></span><span style="display:flex;"><span> map_width=map_width,
|
||
</span></span><span style="display:flex;"><span> map_height=map_height,
|
||
</span></span><span style="display:flex;"><span> max_monsters_per_room=max_monsters_per_room,
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_items_per_room=max_items_per_room,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> engine=engine,
|
||
</span></span><span style="display:flex;"><span> )
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre> ...
|
||
max_monsters_per_room = 2
|
||
<span class="new-text">max_items_per_room = 2</span>
|
||
|
||
tileset = tcod.tileset.load_tilesheet(
|
||
"dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD
|
||
)
|
||
|
||
player = copy.deepcopy(entity_factories.player)
|
||
|
||
engine = Engine(player=player)
|
||
|
||
engine.game_map = generate_dungeon(
|
||
max_rooms=max_rooms,
|
||
room_min_size=room_min_size,
|
||
room_max_size=room_max_size,
|
||
map_width=map_width,
|
||
map_height=map_height,
|
||
max_monsters_per_room=max_monsters_per_room,
|
||
<span class="new-text">max_items_per_room=max_items_per_room,</span>
|
||
engine=engine,
|
||
)
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Run the project now, and you should see a few health potions laying around. Success! Well, not really…</p>
|
||
<p><img src="Part%208%20-%20Items%20and%20Inventory%20%C2%B7%20Roguelike%20Tutorials_files/part-8-health-potions.png" alt="Part 8 - Health Potions"></p>
|
||
<p>Those potions don’t do our rogue any good right now, because we can’t
|
||
pick them up! We need to add the items to an inventory before we can
|
||
start chugging them.</p>
|
||
<p>To implement the inventory, we can create a new component, called <code>Inventory</code>. Create a new file in the <code>components</code> directory, called <code>inventory.py</code>, and add this class:</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, TYPE_CHECKING
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> components.base_component <span style="color:#f92672">import</span> BaseComponent
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> TYPE_CHECKING:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#f92672">from</span> entity <span style="color:#f92672">import</span> Actor, Item
|
||
</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">Inventory</span>(BaseComponent):
|
||
</span></span><span style="display:flex;"><span> parent: Actor
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> __init__(self, capacity: int):
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>capacity <span style="color:#f92672">=</span> capacity
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>items: List[Item] <span style="color:#f92672">=</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">drop</span>(self, item: Item) <span style="color:#f92672">-></span> <span style="color:#66d9ef">None</span>:
|
||
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> Removes an item from the inventory and restores it to the game map, at the player's current location.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> """</span>
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>items<span style="color:#f92672">.</span>remove(item)
|
||
</span></span><span style="display:flex;"><span> item<span style="color:#f92672">.</span>place(self<span style="color:#f92672">.</span>parent<span style="color:#f92672">.</span>x, self<span style="color:#f92672">.</span>parent<span style="color:#f92672">.</span>y, self<span style="color:#f92672">.</span>gamemap)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>engine<span style="color:#f92672">.</span>message_log<span style="color:#f92672">.</span>add_message(<span style="color:#e6db74">f</span><span style="color:#e6db74">"You dropped the </span><span style="color:#e6db74">{</span>item<span style="color:#f92672">.</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74">."</span>)
|
||
</span></span></code></pre></div><p>The <code>Inventory</code> class belongs to an <code>Actor</code>, and its initialized with a <code>capacity</code>, which is the maximum number of items that can be held, and the <code>items</code> list, which will actually hold the items. The <code>drop</code>
|
||
method, as the name implies, will be called when the player decides to
|
||
drop something out of the inventory, back onto the ground.</p>
|
||
<p>Let’s add this new component to our <code>Actor</code> class. Open up <code>entity.py</code> and modify <code>Actor</code> like this:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>...
|
||
</span></span><span style="display:flex;"><span>if TYPE_CHECKING:
|
||
</span></span><span style="display:flex;"><span> from components.ai import BaseAI
|
||
</span></span><span style="display:flex;"><span> from components.consumable import Consumable
|
||
</span></span><span style="display:flex;"><span> from components.fighter import Fighter
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from components.inventory import Inventory
|
||
</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 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 style="color:#a6e22e">+ inventory: Inventory,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> ):
|
||
</span></span><span style="display:flex;"><span> super().__init__(
|
||
</span></span><span style="display:flex;"><span> x=x,
|
||
</span></span><span style="display:flex;"><span> y=y,
|
||
</span></span><span style="display:flex;"><span> char=char,
|
||
</span></span><span style="display:flex;"><span> color=color,
|
||
</span></span><span style="display:flex;"><span> name=name,
|
||
</span></span><span style="display:flex;"><span> blocks_movement=True,
|
||
</span></span><span style="display:flex;"><span> render_order=RenderOrder.ACTOR,
|
||
</span></span><span style="display:flex;"><span> )
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self.ai: Optional[BaseAI] = ai_cls(self)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> self.fighter = fighter
|
||
</span></span><span style="display:flex;"><span> self.fighter.parent = self
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.inventory = inventory
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.inventory.parent = self
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>...
|
||
if TYPE_CHECKING:
|
||
from components.ai import BaseAI
|
||
from components.consumable import Consumable
|
||
from components.fighter import Fighter
|
||
<span class="new-text">from components.inventory import Inventory</span>
|
||
from game_map import GameMap
|
||
|
||
...
|
||
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,
|
||
<span class="new-text">inventory: Inventory,</span>
|
||
):
|
||
super().__init__(
|
||
x=x,
|
||
y=y,
|
||
char=char,
|
||
color=color,
|
||
name=name,
|
||
blocks_movement=True,
|
||
render_order=RenderOrder.ACTOR,
|
||
)
|
||
|
||
self.ai: Optional[BaseAI] = ai_cls(self)
|
||
|
||
self.fighter = fighter
|
||
self.fighter.parent = self
|
||
|
||
<span class="new-text">self.inventory = inventory
|
||
self.inventory.parent = self</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Now, each actor will have their own inventory. Our tutorial won’t
|
||
implement monster inventories (they won’t pick up, hold, or use items),
|
||
but hopefully this setup gives you a good starting place to implement it
|
||
yourself, if you so choose.</p>
|
||
<p>We’ll need to update <code>entity_factories.py</code> to take the new component into account:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from components.ai import HostileEnemy
|
||
</span></span><span style="display:flex;"><span>from components.consumable import HealingConsumable
|
||
</span></span><span style="display:flex;"><span>from components.fighter import Fighter
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from components.inventory import Inventory
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from entity import Actor, Item
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>player = Actor(
|
||
</span></span><span style="display:flex;"><span> char="@",
|
||
</span></span><span style="display:flex;"><span> color=(255, 255, 255),
|
||
</span></span><span style="display:flex;"><span> name="Player",
|
||
</span></span><span style="display:flex;"><span> ai_cls=HostileEnemy,
|
||
</span></span><span style="display:flex;"><span> fighter=Fighter(hp=30, defense=2, power=5),
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ inventory=Inventory(capacity=26),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>orc = Actor(
|
||
</span></span><span style="display:flex;"><span> char="o",
|
||
</span></span><span style="display:flex;"><span> color=(63, 127, 63),
|
||
</span></span><span style="display:flex;"><span> name="Orc",
|
||
</span></span><span style="display:flex;"><span> ai_cls=HostileEnemy,
|
||
</span></span><span style="display:flex;"><span> fighter=Fighter(hp=10, defense=0, power=3),
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ inventory=Inventory(capacity=0),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>)
|
||
</span></span><span style="display:flex;"><span>troll = Actor(
|
||
</span></span><span style="display:flex;"><span> char="T",
|
||
</span></span><span style="display:flex;"><span> color=(0, 127, 0),
|
||
</span></span><span style="display:flex;"><span> name="Troll",
|
||
</span></span><span style="display:flex;"><span> ai_cls=HostileEnemy,
|
||
</span></span><span style="display:flex;"><span> fighter=Fighter(hp=16, defense=1, power=4),
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ inventory=Inventory(capacity=0),
|
||
</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>health_potion = Item(
|
||
</span></span><span style="display:flex;"><span> char="!",
|
||
</span></span><span style="display:flex;"><span> color=(127, 0, 255),
|
||
</span></span><span style="display:flex;"><span> name="Health Potion",
|
||
</span></span><span style="display:flex;"><span> consumable=HealingConsumable(amount=4),
|
||
</span></span><span style="display:flex;"><span>)
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>from components.ai import HostileEnemy
|
||
from components.consumable import HealingConsumable
|
||
from components.fighter import Fighter
|
||
<span class="new-text">from components.inventory import Inventory</span>
|
||
from entity import Actor, Item
|
||
|
||
player = Actor(
|
||
char="@",
|
||
color=(255, 255, 255),
|
||
name="Player",
|
||
ai_cls=HostileEnemy,
|
||
fighter=Fighter(hp=30, defense=2, power=5),
|
||
<span class="new-text">inventory=Inventory(capacity=26),</span>
|
||
)
|
||
|
||
orc = Actor(
|
||
char="o",
|
||
color=(63, 127, 63),
|
||
name="Orc",
|
||
ai_cls=HostileEnemy,
|
||
fighter=Fighter(hp=10, defense=0, power=3),
|
||
<span class="new-text">inventory=Inventory(capacity=0),</span>
|
||
)
|
||
troll = Actor(
|
||
char="T",
|
||
color=(0, 127, 0),
|
||
name="Troll",
|
||
ai_cls=HostileEnemy,
|
||
fighter=Fighter(hp=16, defense=1, power=4),
|
||
<span class="new-text">inventory=Inventory(capacity=0),</span>
|
||
)
|
||
|
||
health_potion = Item(
|
||
char="!",
|
||
color=(127, 0, 255),
|
||
name="Health Potion",
|
||
consumable=HealingConsumable(amount=4),
|
||
)</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>We’re setting the player’s inventory to 26, because when we implement
|
||
the menu system, each letter in the (English) alphabet will correspond
|
||
to one item slot. You can expand the inventory if you want, though
|
||
you’ll need to come up with an alternative menu system to accommodate
|
||
having more choices.</p>
|
||
<p>In order to actually pick up an item of the floor, we’ll require the
|
||
rogue to move onto the same tile and press a key. First, we’ll want an
|
||
easy way to grab all the items that currently exist in the map. Open up <code>game_map.py</code> 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>...
|
||
</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:#f92672">-from entity import Actor
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from entity import Actor, Item
|
||
</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>
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span> @property
|
||
</span></span><span style="display:flex;"><span> def actors(self) -> Iterator[Actor]:
|
||
</span></span><span style="display:flex;"><span> """Iterate over this maps living actors."""
|
||
</span></span><span style="display:flex;"><span> yield from (
|
||
</span></span><span style="display:flex;"><span> entity
|
||
</span></span><span style="display:flex;"><span> for entity in self.entities
|
||
</span></span><span style="display:flex;"><span> if isinstance(entity, Actor) and entity.is_alive
|
||
</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 items(self) -> Iterator[Item]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ yield from (entity for entity in self.entities if isinstance(entity, Item))
|
||
</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></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>...
|
||
import numpy as np # type: ignore
|
||
from tcod.console import Console
|
||
|
||
<span class="crossed-out-text">from entity import Actor</span>
|
||
<span class="new-text">from entity import Actor, Item</span>
|
||
import tile_types
|
||
...
|
||
|
||
...
|
||
@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 class="new-text">@property
|
||
def items(self) -> Iterator[Item]:
|
||
yield from (entity for entity in self.entities if isinstance(entity, Item))</span>
|
||
|
||
def get_blocking_entity_at_location(
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>We can use this new property in an action to find the item(s) on the same tile as the player. Let’s define a <code>PickupAction</code>, which will handle picking up the item and adding it to the inventory.</p>
|
||
<p>Open up <code>actions.py</code> and define <code>PickupAction</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>class 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 PickupAction(Action):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Pickup an item and add it to the inventory, if there is room for it."""
|
||
</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, 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"></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">+ actor_location_x = self.entity.x
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ actor_location_y = self.entity.y
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ inventory = self.entity.inventory
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for item in self.engine.game_map.items:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if actor_location_x == item.x and actor_location_y == item.y:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if len(inventory.items) >= inventory.capacity:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise exceptions.Impossible("Your inventory is full.")
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.game_map.entities.remove(item)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ item.parent = self.entity.inventory
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ inventory.items.append(item)
|
||
</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.message_log.add_message(f"You picked up the {item.name}!")
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise exceptions.Impossible("There is nothing here to pick up.")
|
||
</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 ItemAction(Action):
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class Action:
|
||
...
|
||
|
||
|
||
<span class="new-text">class PickupAction(Action):
|
||
"""Pickup an item and add it to the inventory, if there is room for it."""
|
||
|
||
def __init__(self, entity: Actor):
|
||
super().__init__(entity)
|
||
|
||
def perform(self) -> None:
|
||
actor_location_x = self.entity.x
|
||
actor_location_y = self.entity.y
|
||
inventory = self.entity.inventory
|
||
|
||
for item in self.engine.game_map.items:
|
||
if actor_location_x == item.x and actor_location_y == item.y:
|
||
if len(inventory.items) >= inventory.capacity:
|
||
raise exceptions.Impossible("Your inventory is full.")
|
||
|
||
self.engine.game_map.entities.remove(item)
|
||
item.parent = self.entity.inventory
|
||
inventory.items.append(item)
|
||
|
||
self.engine.message_log.add_message(f"You picked up the {item.name}!")
|
||
return
|
||
|
||
raise exceptions.Impossible("There is nothing here to pick up.")</span>
|
||
|
||
|
||
class ItemAction(Action):
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>The action gets the entity’s location, and tries to find an item that exists in the same location, iterating through <code>self.engine.game_map.items</code> (which we just defined). If an item is found, we try to add it to the inventory, checking the capacity first, and returning <code>Impossible</code>
|
||
if its full. When adding an item to the inventory, we remove it from
|
||
the game map and store it in the inventory, and print out a message. We
|
||
then return, since only one item can be picked up per turn (it’ll be
|
||
possible later for multiple items to be on the same spot).</p>
|
||
<p>If no item is found in the location, we just return <code>Impossible</code>, informing the player that there’s nothing there.</p>
|
||
<p>Let’s add our new action to the event handler. Open up <code>input_handlers.py</code> and edit the key checking section of <code>MainGameEventHandler</code> to add the key for picking up items:</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 actions import (
|
||
</span></span><span style="display:flex;"><span> Action,
|
||
</span></span><span style="display:flex;"><span> BumpAction,
|
||
</span></span><span style="display:flex;"><span> EscapeAction,
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ PickupAction,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> WaitAction,
|
||
</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> elif key == tcod.event.K_v:
|
||
</span></span><span style="display:flex;"><span> self.engine.event_handler = HistoryViewer(self.engine)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ elif key == tcod.event.K_g:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ action = PickupAction(player)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> # No valid key was pressed
|
||
</span></span><span style="display:flex;"><span> return action
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>from actions import (
|
||
Action,
|
||
BumpAction,
|
||
EscapeAction,
|
||
<span class="new-text">PickupAction,</span>
|
||
WaitAction,
|
||
)
|
||
...
|
||
|
||
...
|
||
elif key == tcod.event.K_v:
|
||
self.engine.event_handler = HistoryViewer(self.engine)
|
||
|
||
<span class="new-text">elif key == tcod.event.K_g:
|
||
action = PickupAction(player)</span>
|
||
|
||
# No valid key was pressed
|
||
return action</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Simple enough, if the player presses the “g” key (“g” for “get”), we call the <code>PickupAction</code>. Run the project now, and pick up those potions!</p>
|
||
<p>Now that the player can pick up items, we’ll need to create our
|
||
inventory menu, where the player can see what items are in the
|
||
inventory, and select which one to use. This will require a few steps.</p>
|
||
<p>First, we need a way to get input from the user. When the user opens
|
||
the inventory menu, we need to get the input from the user, and if it
|
||
was valid, we return to the main game’s event handler, so the enemies
|
||
can take their turns.</p>
|
||
<p>To start, let’s create a new event handler, which will return to the <code>MainGameEventHandler</code> when it handles an action successfully. Open <code>input_handlers.py</code> and add the following class:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class EventHandler(tcod.event.EventDispatch[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 AskUserEventHandler(EventHandler):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Handles user input for actions which require special input."""
|
||
</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_action(self, action: Optional[Action]) -> bool:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Return to the main event handler when a valid action was performed."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if super().handle_action(action):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.event_handler = MainGameEventHandler(self.engine)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return True
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return False
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """By default any key exits this input handler."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if event.sym in { # Ignore modifier keys.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_LSHIFT,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_RSHIFT,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_LCTRL,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_RCTRL,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_LALT,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_RALT,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ }:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return None
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.on_exit()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> Optional[Action]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """By default any mouse click exits this input handler."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.on_exit()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def on_exit(self) -> Optional[Action]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Called when the user is trying to exit or cancel an action.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ By default this returns to the main event handler.
|
||
</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.event_handler = MainGameEventHandler(self.engine)
|
||
</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>class EventHandler(tcod.event.EventDispatch[Action]):
|
||
...
|
||
|
||
|
||
<span class="new-text">class AskUserEventHandler(EventHandler):
|
||
"""Handles user input for actions which require special input."""
|
||
|
||
def handle_action(self, action: Optional[Action]) -> bool:
|
||
"""Return to the main event handler when a valid action was performed."""
|
||
if super().handle_action(action):
|
||
self.engine.event_handler = MainGameEventHandler(self.engine)
|
||
return True
|
||
return False
|
||
|
||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||
"""By default any key exits this input handler."""
|
||
if event.sym in { # Ignore modifier keys.
|
||
tcod.event.K_LSHIFT,
|
||
tcod.event.K_RSHIFT,
|
||
tcod.event.K_LCTRL,
|
||
tcod.event.K_RCTRL,
|
||
tcod.event.K_LALT,
|
||
tcod.event.K_RALT,
|
||
}:
|
||
return None
|
||
return self.on_exit()
|
||
|
||
def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> Optional[Action]:
|
||
"""By default any mouse click exits this input handler."""
|
||
return self.on_exit()
|
||
|
||
def on_exit(self) -> Optional[Action]:
|
||
"""Called when the user is trying to exit or cancel an action.
|
||
|
||
By default this returns to the main event handler.
|
||
"""
|
||
self.engine.event_handler = MainGameEventHandler(self.engine)
|
||
return None</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><code>AskUserEventHandler</code>, by default, just exits itself when
|
||
any key is pressed, besides one of the “modifier” keys (shift, control,
|
||
and alt). It also exits when clicking the mouse.</p>
|
||
<p>What’s the point of this class? By itself, nothing really. But we can
|
||
create subclasses of it that actually do something useful, which is
|
||
what we’ll do now. Let’s keep editing <code>input_handlers.py</code> and add this 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> from engine import Engine
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from entity import Item
|
||
</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>class AskUserEventHandler(EventHandler):
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+class InventoryEventHandler(AskUserEventHandler):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """This handler lets the user select an item.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ What happens then depends on the subclass.
|
||
</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">+ TITLE = "<missing title>"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def on_render(self, console: tcod.Console) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Render an inventory menu, which displays the items in the inventory, and the letter to select them.
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ Will move to a different position based on where the player is located, so the player can always see where
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ they are.
|
||
</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().on_render(console)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ number_of_items_in_inventory = len(self.engine.player.inventory.items)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ height = number_of_items_in_inventory + 2
|
||
</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 height <= 3:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ height = 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">+ if self.engine.player.x <= 30:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x = 40
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ else:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x = 0
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y = 0
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ width = len(self.TITLE) + 4
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.draw_frame(
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=x,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y=y,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ width=width,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ height=height,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ title=self.TITLE,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ clear=True,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ fg=(255, 255, 255),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ bg=(0, 0, 0),
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if number_of_items_in_inventory > 0:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for i, item in enumerate(self.engine.player.inventory.items):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ item_key = chr(ord("a") + i)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(x + 1, y + i + 1, f"({item_key}) {item.name}")
|
||
</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">+ console.print(x + 1, y + 1, "(Empty)")
|
||
</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">+ player = self.engine.player
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ key = event.sym
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ index = key - tcod.event.K_a
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if 0 <= index <= 26:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ try:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ selected_item = player.inventory.items[index]
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ except IndexError:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.message_log.add_message("Invalid entry.", color.invalid)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return None
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return self.on_item_selected(selected_item)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return super().ev_keydown(event)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def on_item_selected(self, item: Item) -> Optional[Action]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Called when the user selects a valid item."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ raise NotImplementedError()
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>if TYPE_CHECKING:
|
||
from engine import Engine
|
||
<span class="new-text">from entity import Item</span>
|
||
...
|
||
|
||
|
||
class AskUserEventHandler(EventHandler):
|
||
...
|
||
|
||
|
||
<span class="new-text">class InventoryEventHandler(AskUserEventHandler):
|
||
"""This handler lets the user select an item.
|
||
|
||
What happens then depends on the subclass.
|
||
"""
|
||
|
||
TITLE = "<missing title>"
|
||
|
||
def on_render(self, console: tcod.Console) -> None:
|
||
"""Render an inventory menu, which displays the items in the inventory, and the letter to select them.
|
||
Will move to a different position based on where the player is located, so the player can always see where
|
||
they are.
|
||
"""
|
||
super().on_render(console)
|
||
number_of_items_in_inventory = len(self.engine.player.inventory.items)
|
||
|
||
height = number_of_items_in_inventory + 2
|
||
|
||
if height <= 3:
|
||
height = 3
|
||
|
||
if self.engine.player.x <= 30:
|
||
x = 40
|
||
else:
|
||
x = 0
|
||
|
||
y = 0
|
||
|
||
width = len(self.TITLE) + 4
|
||
|
||
console.draw_frame(
|
||
x=x,
|
||
y=y,
|
||
width=width,
|
||
height=height,
|
||
title=self.TITLE,
|
||
clear=True,
|
||
fg=(255, 255, 255),
|
||
bg=(0, 0, 0),
|
||
)
|
||
|
||
if number_of_items_in_inventory > 0:
|
||
for i, item in enumerate(self.engine.player.inventory.items):
|
||
item_key = chr(ord("a") + i)
|
||
console.print(x + 1, y + i + 1, f"({item_key}) {item.name}")
|
||
else:
|
||
console.print(x + 1, y + 1, "(Empty)")
|
||
|
||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||
player = self.engine.player
|
||
key = event.sym
|
||
index = key - tcod.event.K_a
|
||
|
||
if 0 <= index <= 26:
|
||
try:
|
||
selected_item = player.inventory.items[index]
|
||
except IndexError:
|
||
self.engine.message_log.add_message("Invalid entry.", color.invalid)
|
||
return None
|
||
return self.on_item_selected(selected_item)
|
||
return super().ev_keydown(event)
|
||
|
||
def on_item_selected(self, item: Item) -> Optional[Action]:
|
||
"""Called when the user selects a valid item."""
|
||
raise NotImplementedError()</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><code>InventoryEventHandler</code> subclasses <code>AskUserEventHandler</code>, and renders the items within the player’s <code>Inventory</code>.
|
||
Depending on where the player is standing, the menu will render off to
|
||
the side, so the menu won’t cover the player. If there’s nothing in the
|
||
inventory, it just prints “Empty”. Notice that it doesn’t give itself a
|
||
title, as that will be defined in a different subclass (more on that in a
|
||
bit).</p>
|
||
<p>The <code>ev_keydown</code> function takes the user’s input, from
|
||
letters a - z, and associates that with an index in the inventory. If
|
||
the player pressed “b”, for example, the second item in the inventory
|
||
will be selected and returned. If the player presses a key like “c”
|
||
(item 3) but only has one item, then the message “Invalid entry” will
|
||
display. If any other key is pressed, the menu will close.</p>
|
||
<p>This class, still, does not actually do anything for us right now,
|
||
but I promise we’re close. Before we implement the menus to use and drop
|
||
items, we’ll need to define the <code>Action</code> that drops items. Add the following class to <code>actions.py</code>:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class EscapeAction(Action):
|
||
</span></span><span style="display:flex;"><span> def perform(self) -> None:
|
||
</span></span><span style="display:flex;"><span> raise SystemExit()
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+class DropItem(ItemAction):
|
||
</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">+ self.entity.inventory.drop(self.item)
|
||
</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 WaitAction(Action):
|
||
</span></span><span style="display:flex;"><span> def perform(self) -> None:
|
||
</span></span><span style="display:flex;"><span> pass
|
||
</span></span><span style="display:flex;"><span>...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class EscapeAction(Action):
|
||
def perform(self) -> None:
|
||
raise SystemExit()
|
||
|
||
|
||
<span class="new-text">class DropItem(ItemAction):
|
||
def perform(self) -> None:
|
||
self.entity.inventory.drop(self.item)</span>
|
||
|
||
|
||
class WaitAction(Action):
|
||
def perform(self) -> None:
|
||
pass
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><code>DropItem</code> will be used to drop something from the inventory. It just calls the <code>drop</code> method of the <code>Inventory</code> component.</p>
|
||
<p>Now, let’s put this new action into… well… action! Open up <code>input_handlers.py</code> once again, and let’s add the handlers that will handle both selecting an item and dropping one.</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>...
|
||
</span></span><span style="display:flex;"><span>import tcod
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import actions
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from actions import (
|
||
</span></span><span style="display:flex;"><span> Action,
|
||
</span></span><span style="display:flex;"><span> BumpAction,
|
||
</span></span><span style="display:flex;"><span> EscapeAction,
|
||
</span></span><span style="display:flex;"><span> PickupAction,
|
||
</span></span><span style="display:flex;"><span> WaitAction,
|
||
</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 InventoryEventHandler(AskUserEventHandler):
|
||
</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 InventoryActivateHandler(InventoryEventHandler):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Handle using an inventory item."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ TITLE = "Select an item to use"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def on_item_selected(self, item: Item) -> Optional[Action]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Return the action for the selected item."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return item.consumable.get_action(self.engine.player)
|
||
</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 InventoryDropHandler(InventoryEventHandler):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Handle dropping an inventory item."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ TITLE = "Select an item to drop"
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def on_item_selected(self, item: Item) -> Optional[Action]:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Drop this item."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return actions.DropItem(self.engine.player, item)
|
||
</span></span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>...
|
||
import tcod
|
||
|
||
<span class="new-text">import actions</span>
|
||
from actions import (
|
||
Action,
|
||
BumpAction,
|
||
EscapeAction,
|
||
PickupAction,
|
||
WaitAction,
|
||
)
|
||
...
|
||
|
||
|
||
class InventoryEventHandler(AskUserEventHandler):
|
||
...
|
||
|
||
|
||
<span class="new-text">class InventoryActivateHandler(InventoryEventHandler):
|
||
"""Handle using an inventory item."""
|
||
|
||
TITLE = "Select an item to use"
|
||
|
||
def on_item_selected(self, item: Item) -> Optional[Action]:
|
||
"""Return the action for the selected item."""
|
||
return item.consumable.get_action(self.engine.player)
|
||
|
||
|
||
class InventoryDropHandler(InventoryEventHandler):
|
||
"""Handle dropping an inventory item."""
|
||
|
||
TITLE = "Select an item to drop"
|
||
|
||
def on_item_selected(self, item: Item) -> Optional[Action]:
|
||
"""Drop this item."""
|
||
return actions.DropItem(self.engine.player, item)</span></pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>At long last, we’ve got the <code>InventoryActivateHandler</code> and the <code>InventoryDropHandler</code>, which will handle using and dropping items, respectively. They both inherit from <code>InventoryEventHandler</code>,
|
||
allowing the player to select an item in both menus using what we wrote
|
||
in that class (selecting an item with a letter), but both handlers
|
||
display a different title and call different actions, depending on the
|
||
selection.</p>
|
||
<p>All that’s left now is to utilize these event handlers, based on the
|
||
key we press. Let’s set the game up to open the inventory menu when
|
||
pressing “i”, and the drop menu when pressing “d”. Open <code>input_handlers.py</code>, and add the following lines to <code>MainGameEventHandler</code>:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span> ...
|
||
</span></span><span style="display:flex;"><span> elif key == tcod.event.K_g:
|
||
</span></span><span style="display:flex;"><span> action = PickupAction(player)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ elif key == tcod.event.K_i:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.event_handler = InventoryActivateHandler(self.engine)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ elif key == tcod.event.K_d:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.event_handler = InventoryDropHandler(self.engine)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span> # No valid key was pressed
|
||
</span></span><span style="display:flex;"><span> return action
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre> ...
|
||
elif key == tcod.event.K_g:
|
||
action = PickupAction(player)
|
||
|
||
<span class="new-text">elif key == tcod.event.K_i:
|
||
self.engine.event_handler = InventoryActivateHandler(self.engine)
|
||
elif key == tcod.event.K_d:
|
||
self.engine.event_handler = InventoryDropHandler(self.engine)</span>
|
||
|
||
# No valid key was pressed
|
||
return action</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Now, when you run the project, you can, at long last, use and drop the health potions!</p>
|
||
<p><img src="Part%208%20-%20Items%20and%20Inventory%20%C2%B7%20Roguelike%20Tutorials_files/part-8-using-items.png" alt="Part 8 - Using items"></p>
|
||
<p>There’s a major bug with our implementation though: used items won’t
|
||
disappear after using them. This means the player could keep consuming
|
||
the same health potion over and over!</p>
|
||
<p>Let’s fix that, by opening up <code>consumable.py</code> 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 Optional, TYPE_CHECKING
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>import actions
|
||
</span></span><span style="display:flex;"><span>import color
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import components.inventory
|
||
</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>from exceptions import Impossible
|
||
</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, Item
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span>class Consumable(BaseComponent):
|
||
</span></span><span style="display:flex;"><span> parent: Item
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> def get_action(self, consumer: Actor) -> Optional[actions.Action]:
|
||
</span></span><span style="display:flex;"><span> """Try to return the action for this item."""
|
||
</span></span><span style="display:flex;"><span> return actions.ItemAction(consumer, self.parent)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> def activate(self, action: actions.ItemAction) -> None:
|
||
</span></span><span style="display:flex;"><span> """Invoke this items ability.
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> `action` is the context for this activation.
|
||
</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 style="color:#a6e22e">+ def consume(self) -> None:
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Remove the consumed item from its containing inventory."""
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ entity = self.parent
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ inventory = entity.parent
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if isinstance(inventory, components.inventory.Inventory):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ inventory.items.remove(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 HealingConsumable(Consumable):
|
||
</span></span><span style="display:flex;"><span> def __init__(self, amount: int):
|
||
</span></span><span style="display:flex;"><span> self.amount = amount
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> def activate(self, action: actions.ItemAction) -> None:
|
||
</span></span><span style="display:flex;"><span> consumer = action.entity
|
||
</span></span><span style="display:flex;"><span> amount_recovered = consumer.fighter.heal(self.amount)
|
||
</span></span><span style="display:flex;"><span>
|
||
</span></span><span style="display:flex;"><span> if amount_recovered > 0:
|
||
</span></span><span style="display:flex;"><span> self.engine.message_log.add_message(
|
||
</span></span><span style="display:flex;"><span> f"You consume the {self.parent.name}, and recover {amount_recovered} HP!",
|
||
</span></span><span style="display:flex;"><span> color.health_recovered,
|
||
</span></span><span style="display:flex;"><span> )
|
||
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.consume()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> else:
|
||
</span></span><span style="display:flex;"><span> raise Impossible(f"Your health is already full.")
|
||
</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 actions
|
||
import color
|
||
<span class="new-text">import components.inventory</span>
|
||
from components.base_component import BaseComponent
|
||
from exceptions import Impossible
|
||
|
||
if TYPE_CHECKING:
|
||
from entity import Actor, Item
|
||
|
||
|
||
class Consumable(BaseComponent):
|
||
parent: Item
|
||
|
||
def get_action(self, consumer: Actor) -> Optional[actions.Action]:
|
||
"""Try to return the action for this item."""
|
||
return actions.ItemAction(consumer, self.parent)
|
||
|
||
def activate(self, action: actions.ItemAction) -> None:
|
||
"""Invoke this items ability.
|
||
|
||
`action` is the context for this activation.
|
||
"""
|
||
raise NotImplementedError()
|
||
|
||
<span class="new-text">def consume(self) -> None:
|
||
"""Remove the consumed item from its containing inventory."""
|
||
entity = self.parent
|
||
inventory = entity.parent
|
||
if isinstance(inventory, components.inventory.Inventory):
|
||
inventory.items.remove(entity)</span>
|
||
|
||
|
||
class HealingConsumable(Consumable):
|
||
def __init__(self, amount: int):
|
||
self.amount = amount
|
||
|
||
def activate(self, action: actions.ItemAction) -> None:
|
||
consumer = action.entity
|
||
amount_recovered = consumer.fighter.heal(self.amount)
|
||
|
||
if amount_recovered > 0:
|
||
self.engine.message_log.add_message(
|
||
f"You consume the {self.parent.name}, and recover {amount_recovered} HP!",
|
||
color.health_recovered,
|
||
)
|
||
<span class="new-text">self.consume()</span>
|
||
else:
|
||
raise Impossible(f"Your health is already full.")</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p><code>consume</code> removes the item from the <code>Inventory</code> container it occupies. Since it no longer belongs to the inventory or the map, it disappears from the game. We use the <code>consume</code> method when the health potion is successfully used, and we don’t if it’s not.</p>
|
||
<p>With that, the health potions will disappear after use.</p>
|
||
<p>There’s two last bits of housekeeping we need to do before moving on to the next part. The <code>parent</code> class attribute in the <code>Entity</code> class has a bit of a problem: it’s designated as a <code>GameMap</code> type right now, but when an item moves from the map to the inventory, that isn’t really true any more. Let’s fix that now:</p>
|
||
<div>
|
||
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
|
||
Diff
|
||
</button>
|
||
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
|
||
Original
|
||
</button>
|
||
|
||
|
||
<div class="data-pane active" data-pane="diff">
|
||
|
||
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from __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 Optional, Tuple, Type, 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, Union
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
|
||
</span></span><span style="display:flex;"><span>from render_order import RenderOrder
|
||
</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:#f92672">- parent: GameMap
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ parent: Union[GameMap, Inventory]
|
||
</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> ...
|
||
</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 Optional, Tuple, Type, TypeVar, TYPE_CHECKING</span>
|
||
<span class="new-text">from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING, Union</span>
|
||
|
||
from render_order import RenderOrder
|
||
|
||
...
|
||
class Entity:
|
||
"""
|
||
A generic object to represent players, enemies, items, etc.
|
||
"""
|
||
|
||
<span class="crossed-out-text">parent: GameMap</span>
|
||
<span class="new-text">parent: Union[GameMap, Inventory]</span>
|
||
|
||
def __init__(
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Lastly, we can actually remove <code>EscapeAction</code>, as it can just be handled by the event handlers. Open <code>actions.py</code> and remove <code>EscapeAction</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 PickupAction(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:#f92672">-class EscapeAction(Action):
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- def perform(self) -> None:
|
||
</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>
|
||
</span></span><span style="display:flex;"><span>class ItemAction(Action):
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>class PickupAction(Action):
|
||
...
|
||
|
||
|
||
<span class="crossed-out-text">class EscapeAction(Action):</span>
|
||
<span class="crossed-out-text">def perform(self) -> None:</span>
|
||
<span class="crossed-out-text">raise SystemExit()</span>
|
||
|
||
|
||
class ItemAction(Action):
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>Then, remove <code>EscapeAction</code> from <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></span><span style="display:flex;"><span>from actions import (
|
||
</span></span><span style="display:flex;"><span> Action,
|
||
</span></span><span style="display:flex;"><span> BumpAction,
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- EscapeAction,
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> PickupAction,
|
||
</span></span><span style="display:flex;"><span> WaitAction
|
||
</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> elif key == tcod.event.K_ESCAPE:
|
||
</span></span><span style="display:flex;"><span><span style="color:#f92672">- action = EscapeAction(player)
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ raise SystemExit()
|
||
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> elif key == tcod.event.K_v:
|
||
</span></span><span style="display:flex;"><span> self.engine.event_handler = HistoryViewer(self.engine)
|
||
</span></span><span style="display:flex;"><span> ...
|
||
</span></span></code></pre></div>
|
||
|
||
</div>
|
||
<div class="data-pane" data-pane="original">
|
||
|
||
<pre>...
|
||
from actions import (
|
||
Action,
|
||
BumpAction,
|
||
<span class="crossed-out-text">EscapeAction,</span>
|
||
PickupAction,
|
||
WaitAction
|
||
)
|
||
...
|
||
|
||
...
|
||
elif key == tcod.event.K_ESCAPE:
|
||
<span class="crossed-out-text">action = EscapeAction(player)</span>
|
||
<span class="new-text">raise SystemExit()</span>
|
||
elif key == tcod.event.K_v:
|
||
self.engine.event_handler = HistoryViewer(self.engine)
|
||
...</pre>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<p>This was another long chapter, but this is an important step towards a
|
||
functioning game. Next chapter, we’ll add a few more item types to use.</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-8">click here</a>.</p>
|
||
<p><a href="https://rogueliketutorials.com/tutorials/tcod/v2/part-9">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%208%20-%20Items%20and%20Inventory%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.236049395dc3682fb2719640872958e12f1f24067bb09c327b2.js" integrity="sha256-I2BJOV3DaC+ycZZAhylY4S8fJAZ7sJwyeyM+YpDH7aw="></script>
|
||
|
||
|
||
|
||
|
||
|
||
<script src="Part%208%20-%20Items%20and%20Inventory%20%C2%B7%20Roguelike%20Tutorials_files/codetabs.min.cc52451e7f25e50f64c1c893826f606d58410d742c214dce.js" integrity="sha256-zFJFHn8l5Q9kwciTgm9gbVhBDXQsIU3OI/tEfJlh8rA="></script>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
</body></html> |