McRogueFace/roguelike_tutorial/rogueliketutorials.com/Part 7 - Creating the Inter...

1521 lines
98 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en" style="color-scheme: dark;"><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>
Part 7 - Creating the Interface · Roguelike Tutorials
</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<meta name="description" content="Our game is looking more and more playable by the chapter, but before we move forward with the gameplay, we ought to take a moment to focus on how the project looks. Despite what some roguelike traditionalists may tell you, a good UI goes a long way.
One of the first things we can do is define a file that will hold our RGB colors. Weve just been hard-coding them up until now, but it would be nice if they were all in one place and then imported when needed, so that we could easily update them if need be.">
<meta name="keywords" content="">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Part 7 - Creating the Interface">
<meta name="twitter:description" content="Our game is looking more and more playable by the chapter, but before we move forward with the gameplay, we ought to take a moment to focus on how the project looks. Despite what some roguelike traditionalists may tell you, a good UI goes a long way.
One of the first things we can do is define a file that will hold our RGB colors. Weve just been hard-coding them up until now, but it would be nice if they were all in one place and then imported when needed, so that we could easily update them if need be.">
<meta property="og:title" content="Part 7 - Creating the Interface">
<meta property="og:description" content="Our game is looking more and more playable by the chapter, but before we move forward with the gameplay, we ought to take a moment to focus on how the project looks. Despite what some roguelike traditionalists may tell you, a good UI goes a long way.
One of the first things we can do is define a file that will hold our RGB colors. Weve just been hard-coding them up until now, but it would be nice if they were all in one place and then imported when needed, so that we could easily update them if need be.">
<meta property="og:type" content="article">
<meta property="og:url" content="https://rogueliketutorials.com/tutorials/tcod/v2/part-7/"><meta property="article:section" content="tutorials">
<meta property="article:published_time" content="2020-07-07T00:00:00+00:00">
<meta property="article:modified_time" content="2020-07-07T00:00:00+00:00">
<link rel="canonical" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-7/">
<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%207%20-%20Creating%20the%20Interface%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.c4d7e93a158eda5a65b3df343745d2092a0a1e2170feeec909.css" integrity="sha256-xNfpOhWO2lpls980N0XSCSoKHiFw/u7JCbiolEOQPGo=" crossorigin="anonymous" media="screen">
<link rel="stylesheet" href="Part%207%20-%20Creating%20the%20Interface%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%207%20-%20Creating%20the%20Interface%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-7/">
Part 7 - Creating the Interface
</a>
</h1>
</header>
<p>Our game is looking more and more playable by the chapter, but
before we move forward with the gameplay, we ought to take a moment to
focus on how the project <em>looks</em>. Despite what some roguelike traditionalists may tell you, a good UI goes a long way.</p>
<p>One of the first things we can do is define a file that will hold our
RGB colors. Weve just been hard-coding them up until now, but it would
be nice if they were all in one place and then imported when needed, so
that we could easily update them if need be.</p>
<p>Create a new file, called <code>color.py</code>, and fill it with the following:</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>white <span style="color:#f92672">=</span> (<span style="color:#ae81ff">0xFF</span>, <span style="color:#ae81ff">0xFF</span>, <span style="color:#ae81ff">0xFF</span>)
</span></span><span style="display:flex;"><span>black <span style="color:#f92672">=</span> (<span style="color:#ae81ff">0x0</span>, <span style="color:#ae81ff">0x0</span>, <span style="color:#ae81ff">0x0</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>player_atk <span style="color:#f92672">=</span> (<span style="color:#ae81ff">0xE0</span>, <span style="color:#ae81ff">0xE0</span>, <span style="color:#ae81ff">0xE0</span>)
</span></span><span style="display:flex;"><span>enemy_atk <span style="color:#f92672">=</span> (<span style="color:#ae81ff">0xFF</span>, <span style="color:#ae81ff">0xC0</span>, <span style="color:#ae81ff">0xC0</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>player_die <span style="color:#f92672">=</span> (<span style="color:#ae81ff">0xFF</span>, <span style="color:#ae81ff">0x30</span>, <span style="color:#ae81ff">0x30</span>)
</span></span><span style="display:flex;"><span>enemy_die <span style="color:#f92672">=</span> (<span style="color:#ae81ff">0xFF</span>, <span style="color:#ae81ff">0xA0</span>, <span style="color:#ae81ff">0x30</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>welcome_text <span style="color:#f92672">=</span> (<span style="color:#ae81ff">0x20</span>, <span style="color:#ae81ff">0xA0</span>, <span style="color:#ae81ff">0xFF</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>bar_text <span style="color:#f92672">=</span> white
</span></span><span style="display:flex;"><span>bar_filled <span style="color:#f92672">=</span> (<span style="color:#ae81ff">0x0</span>, <span style="color:#ae81ff">0x60</span>, <span style="color:#ae81ff">0x0</span>)
</span></span><span style="display:flex;"><span>bar_empty <span style="color:#f92672">=</span> (<span style="color:#ae81ff">0x40</span>, <span style="color:#ae81ff">0x10</span>, <span style="color:#ae81ff">0x10</span>)
</span></span></code></pre></div><p>Some of these colors, like <code>welcome_text</code> and <code>bar_filled</code> are things we havent added yet, but dont worry, well utilize them by the end of the chapter.</p>
<p>Last chapter, we implemented a basic HP tracker for the player, with
the promise that wed revisit in this chapter to make it look better.
And now, the time has come!</p>
<p>Well create a bar that will gradually decrease as the player loses
HP. This will help the player visualize how much HP is remaining. To do
this, well create a generic <code>render_bar</code> function, which can accept different values and change the bars length based on the <code>current_value</code> and <code>maximum_value</code> we give to it.</p>
<p>To house this new function (as well as some other functions that are coming soon), lets create a new file, called <code>render_functions.py</code>. Put the following into it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#f92672">from</span> __future__ <span style="color:#f92672">import</span> annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> TYPE_CHECKING
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> color
</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> tcod <span style="color:#f92672">import</span> Console
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">render_bar</span>(
</span></span><span style="display:flex;"><span> console: Console, current_value: int, maximum_value: int, total_width: int
</span></span><span style="display:flex;"><span>) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> bar_width <span style="color:#f92672">=</span> int(float(current_value) <span style="color:#f92672">/</span> maximum_value <span style="color:#f92672">*</span> total_width)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>draw_rect(x<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>, y<span style="color:#f92672">=</span><span style="color:#ae81ff">45</span>, width<span style="color:#f92672">=</span>total_width, height<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>, ch<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>, bg<span style="color:#f92672">=</span>color<span style="color:#f92672">.</span>bar_empty)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> bar_width <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>draw_rect(
</span></span><span style="display:flex;"><span> x<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span>, y<span style="color:#f92672">=</span><span style="color:#ae81ff">45</span>, width<span style="color:#f92672">=</span>bar_width, height<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>, ch<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>, bg<span style="color:#f92672">=</span>color<span style="color:#f92672">.</span>bar_filled
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>print(
</span></span><span style="display:flex;"><span> x<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>, y<span style="color:#f92672">=</span><span style="color:#ae81ff">45</span>, string<span style="color:#f92672">=</span><span style="color:#e6db74">f</span><span style="color:#e6db74">"HP: </span><span style="color:#e6db74">{</span>current_value<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">{</span>maximum_value<span style="color:#e6db74">}</span><span style="color:#e6db74">"</span>, fg<span style="color:#f92672">=</span>color<span style="color:#f92672">.</span>bar_text
</span></span><span style="display:flex;"><span> )
</span></span></code></pre></div><p>Were utilizing the <code>draw_rect</code>
functions provided by TCOD to draw rectangular bars. Were actually
drawing two bars, one on top of the other. The first one will be the
background color, which in the case of our health bar, will be a red
color. The second goes on top, and is green. The one on top will
gradually decrease as the player drops hit points, as its width is
determined by the <code>bar_width</code> variable, which is itself determined by the <code>current_value</code> over the <code>maximum_value</code>.</p>
<p>We also print the “HP” value over the bar, so the player knows the exact number.</p>
<p>In order to utilize this new function, make the following changes to <code>engine.py</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>from input_handlers import MainGameEventHandler
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from render_functions import render_bar
</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> def render(self, console: Console, context: Context) -&gt; None:
</span></span><span style="display:flex;"><span> self.game_map.render(console)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ render_bar(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console=console,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ current_value=self.player.fighter.hp,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ maximum_value=self.player.fighter.max_hp,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ total_width=20,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- console.print(
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- x=1,
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- y=47,
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- string=f"HP: {self.player.fighter.hp}/{self.player.fighter.max_hp}",
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- )
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span> context.present(console)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> console.clear()
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
from input_handlers import MainGameEventHandler
<span class="new-text">from render_functions import render_bar</span>
if TYPE_CHECKING:
...
...
def render(self, console: Console, context: Context) -&gt; None:
self.game_map.render(console)
<span class="new-text">render_bar(
console=console,
current_value=self.player.fighter.hp,
maximum_value=self.player.fighter.max_hp,
total_width=20,
)</span>
<span class="crossed-out-text">console.print(</span>
<span class="crossed-out-text">x=1,</span>
<span class="crossed-out-text">y=47,</span>
<span class="crossed-out-text">string=f"HP: {self.player.fighter.hp}/{self.player.fighter.max_hp}",</span>
<span class="crossed-out-text">)</span>
context.present(console)
console.clear()</pre>
</div>
</div>
<p>Run the project now, and you should have a functioning health bar!</p>
<p><img src="Part%207%20-%20Creating%20the%20Interface%20%C2%B7%20Roguelike%20Tutorials_files/part-7-health-bar.png" alt="Part 7 - Health bar"></p>
<p>What next? One obvious problem with our project at the moment is that
the messages get printed to the terminal rather than showing up in the
actual game. We can fix that by adding a message log, which can display
messages along with different colors for a bit of flash.</p>
<p>Create a new file, called <code>message_log.py</code>, and put the following contents inside:</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> typing <span style="color:#f92672">import</span> List, Reversible, Tuple
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> textwrap
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> tcod
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> color
</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">Message</span>:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> __init__(self, text: str, fg: Tuple[int, int, int]):
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>plain_text <span style="color:#f92672">=</span> text
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>fg <span style="color:#f92672">=</span> fg
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>count <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@property</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">full_text</span>(self) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""The full text of this message, including the count if necessary."""</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> self<span style="color:#f92672">.</span>count <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">1</span>:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">"</span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>plain_text<span style="color:#e6db74">}</span><span style="color:#e6db74"> (x</span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>count<span style="color:#e6db74">}</span><span style="color:#e6db74">)"</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>plain_text
</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">MessageLog</span>:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> __init__(self) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>messages: List[Message] <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">add_message</span>(
</span></span><span style="display:flex;"><span> self, text: str, fg: Tuple[int, int, int] <span style="color:#f92672">=</span> color<span style="color:#f92672">.</span>white, <span style="color:#f92672">*</span>, stack: bool <span style="color:#f92672">=</span> <span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span> ) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Add a message to this log.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> `text` is the message text, `fg` is the text color.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> If `stack` is True then the message can stack with a previous message
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> of the same text.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> """</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> stack <span style="color:#f92672">and</span> self<span style="color:#f92672">.</span>messages <span style="color:#f92672">and</span> text <span style="color:#f92672">==</span> self<span style="color:#f92672">.</span>messages[<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>]<span style="color:#f92672">.</span>plain_text:
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>messages[<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>]<span style="color:#f92672">.</span>count <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>messages<span style="color:#f92672">.</span>append(Message(text, fg))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">render</span>(
</span></span><span style="display:flex;"><span> self, console: tcod<span style="color:#f92672">.</span>Console, x: int, y: int, width: int, height: int,
</span></span><span style="display:flex;"><span> ) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Render this log over the given area.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> `x`, `y`, `width`, `height` is the rectangular region to render onto
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> the `console`.
</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>render_messages(console, x, y, width, height, self<span style="color:#f92672">.</span>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:#66d9ef">def</span> <span style="color:#a6e22e">render_messages</span>(
</span></span><span style="display:flex;"><span> console: tcod<span style="color:#f92672">.</span>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> ) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Render the messages provided.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> The `messages` are rendered starting at the last message and working
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> backwards.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> """</span>
</span></span><span style="display:flex;"><span> y_offset <span style="color:#f92672">=</span> height <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> message <span style="color:#f92672">in</span> reversed(messages):
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> line <span style="color:#f92672">in</span> reversed(textwrap<span style="color:#f92672">.</span>wrap(message<span style="color:#f92672">.</span>full_text, width)):
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>print(x<span style="color:#f92672">=</span>x, y<span style="color:#f92672">=</span>y <span style="color:#f92672">+</span> y_offset, string<span style="color:#f92672">=</span>line, fg<span style="color:#f92672">=</span>message<span style="color:#f92672">.</span>fg)
</span></span><span style="display:flex;"><span> y_offset <span style="color:#f92672">-=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> y_offset <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#75715e"># No more space to print messages.</span>
</span></span></code></pre></div><p>Lets go through the additions piece by piece.</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">Message</span>:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> __init__(self, text: str, fg: Tuple[int, int, int]):
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>plain_text <span style="color:#f92672">=</span> text
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>fg <span style="color:#f92672">=</span> fg
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>count <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@property</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">full_text</span>(self) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""The full text of this message, including the count if necessary."""</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> self<span style="color:#f92672">.</span>count <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">1</span>:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">"</span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>plain_text<span style="color:#e6db74">}</span><span style="color:#e6db74"> (x</span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>count<span style="color:#e6db74">}</span><span style="color:#e6db74">)"</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> self<span style="color:#f92672">.</span>plain_text
</span></span></code></pre></div><p>The <code>Message</code> will be used to save and display messages in our log. It includes three pieces of information:</p>
<ul>
<li><code>plain_text</code>: The actual message text.</li>
<li><code>fg</code>: The “foreground” color of the message.</li>
<li><code>count</code>: This is used to display something like “The Orc
attacks (x3).” Rather than crowding our message log with the same
message over and over, we can “stack” the messages by increasing a
messages count. This only happens when the same message appears several
times in a row.</li>
</ul>
<p>The <code>full_text</code> property returns the text with its count, if the count is greater than 1. Otherwise, it just returns the message as-is.</p>
<p>Now, the actual message log:</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">MessageLog</span>:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> __init__(self) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>messages: List[Message] <span style="color:#f92672">=</span> []
</span></span></code></pre></div><p>It keeps a list of the <code>Message</code>s received. Nothing too complex here.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">add_message</span>(
</span></span><span style="display:flex;"><span> self, text: str, fg: Tuple[int, int, int] <span style="color:#f92672">=</span> color<span style="color:#f92672">.</span>white, <span style="color:#f92672">*</span>, stack: bool <span style="color:#f92672">=</span> <span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span> ) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Add a message to this log.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> `text` is the message text, `fg` is the text color.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> If `stack` is True then the message can stack with a previous message
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> of the same text.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> """</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> stack <span style="color:#f92672">and</span> self<span style="color:#f92672">.</span>messages <span style="color:#f92672">and</span> text <span style="color:#f92672">==</span> self<span style="color:#f92672">.</span>messages[<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>]<span style="color:#f92672">.</span>plain_text:
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>messages[<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>]<span style="color:#f92672">.</span>count <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>messages<span style="color:#f92672">.</span>append(Message(text, fg))
</span></span></code></pre></div><p><code>add_message</code> is what adds the message to the log. <code>text</code> is required, but <code>fg</code> will just default to white if nothing is given. <code>stack</code> tells us whether to stack messages or not (which allows us to disable this behavior, if desired).</p>
<p>If we are allowing stacking, and the added message matches the
previous message, we just increment the previous messages count by 1.
If its not a match, we add it to the list.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">render</span>(
</span></span><span style="display:flex;"><span> self, console: tcod<span style="color:#f92672">.</span>Console, x: int, y: int, width: int, height: int,
</span></span><span style="display:flex;"><span> ) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Render this log over the given area.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> `x`, `y`, `width`, `height` is the rectangular region to render onto
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> the `console`.
</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>render_messages(console, x, y, width, height, self<span style="color:#f92672">.</span>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:#66d9ef">def</span> <span style="color:#a6e22e">render_messages</span>(
</span></span><span style="display:flex;"><span> console: tcod<span style="color:#f92672">.</span>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> ) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Render the messages provided.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> The `messages` are rendered starting at the last message and working
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> backwards.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> """</span>
</span></span><span style="display:flex;"><span> y_offset <span style="color:#f92672">=</span> height <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> message <span style="color:#f92672">in</span> reversed(messages):
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> line <span style="color:#f92672">in</span> reversed(textwrap<span style="color:#f92672">.</span>wrap(message<span style="color:#f92672">.</span>full_text, width)):
</span></span><span style="display:flex;"><span> console<span style="color:#f92672">.</span>print(x<span style="color:#f92672">=</span>x, y<span style="color:#f92672">=</span>y <span style="color:#f92672">+</span> y_offset, string<span style="color:#f92672">=</span>line, fg<span style="color:#f92672">=</span>message<span style="color:#f92672">.</span>fg)
</span></span><span style="display:flex;"><span> y_offset <span style="color:#f92672">-=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> y_offset <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> <span style="color:#75715e"># No more space to print messages.</span>
</span></span></code></pre></div><p>This <code>render</code> calls <code>render_messages</code>,
which is a static method that actually renders the messages to the
screen. It renders them in reverse order, to make it appear that the
messages are scrolling in an upwards direction. We use the <code>textwrap.wrap</code>
function to wrap the text to fit within the given area, and then print
each line to the console. We can only print so many messages to the
console, however, so if <code>y_offset</code> reaches -1, we stop.</p>
<p>To utilize the message log, well first need to add it to the <code>Engine</code> class. 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 input_handlers import MainGameEventHandler
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from message_log import MessageLog
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from render_functions import render_bar
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>if TYPE_CHECKING:
</span></span><span style="display:flex;"><span> from entity import Actor
</span></span><span style="display:flex;"><span> from game_map import GameMap
</span></span><span style="display:flex;"><span> from input_handlers import EventHandler
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class Engine:
</span></span><span style="display:flex;"><span> game_map: GameMap
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def __init__(self, player: Actor):
</span></span><span style="display:flex;"><span> self.event_handler: EventHandler = MainGameEventHandler(self)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.message_log = MessageLog()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.player = player
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def render(self, console: Console, context: Context) -&gt; None:
</span></span><span style="display:flex;"><span> self.game_map.render(console)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.message_log.render(console=console, x=21, y=45, width=40, height=5)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> render_bar(
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
from input_handlers import MainGameEventHandler
<span class="new-text">from message_log import MessageLog</span>
from render_functions import render_bar
if TYPE_CHECKING:
from entity import Actor
from game_map import GameMap
from input_handlers import EventHandler
class Engine:
game_map: GameMap
def __init__(self, player: Actor):
self.event_handler: EventHandler = MainGameEventHandler(self)
<span class="new-text">self.message_log = MessageLog()</span>
self.player = player
...
def render(self, console: Console, context: Context) -&gt; None:
self.game_map.render(console)
<span class="new-text">self.message_log.render(console=console, x=21, y=45, width=40, height=5)</span>
render_bar(
...</pre>
</div>
</div>
<p>Were adding an instance of <code>MessageLog</code> in the initializer, and rendering the log in the Engines <code>render</code> method. Nothing too complicated here.</p>
<p>We need to make a small change to <code>main.py</code> in order to actually make room for our message log. We can also add a friendly welcome message here.</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>#!/usr/bin/env python3
</span></span><span style="display:flex;"><span>import copy
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>import tcod
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import color
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>from engine import Engine
</span></span><span style="display:flex;"><span>import entity_factories
</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> map_width = 80
</span></span><span style="display:flex;"><span><span style="color:#f92672">- map_height = 45
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ map_height = 43
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> room_max_size = 10
</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> engine.update_fov()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine.message_log.add_message(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ "Hello and welcome, adventurer, to yet another dungeon!", color.welcome_text
</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> with tcod.context.new_terminal(
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>#!/usr/bin/env python3
import copy
import tcod
<span class="new-text">import color</span>
from engine import Engine
import entity_factories
...
...
map_width = 80
<span class="crossed-out-text">map_height = 45</span>
<span class="new-text">map_height = 43</span>
room_max_size = 10
...
...
engine.update_fov()
<span class="new-text">engine.message_log.add_message(
"Hello and welcome, adventurer, to yet another dungeon!", color.welcome_text
)</span>
with tcod.context.new_terminal(
...</pre>
</div>
</div>
<p>Feel free to experiment with different window and map sizes, if you like.</p>
<p>Run the project, and you should see the welcome message.</p>
<p><img src="Part%207%20-%20Creating%20the%20Interface%20%C2%B7%20Roguelike%20Tutorials_files/part-7-welcome-message.png" alt="Part 7 - Welcome Message"></p>
<p>Now that weve confirmed our message log accepts and displays messages, well need to replace all of our previous <code>print</code> statements to push messages to the log instead.</p>
<p>Lets start with our attack action, in <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>...
</span></span><span style="display:flex;"><span>from typing import Optional, Tuple, TYPE_CHECKING
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import color
</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> damage = self.entity.fighter.power - target.fighter.defense
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> attack_desc = f"{self.entity.name.capitalize()} attacks {target.name}"
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.entity is self.engine.player:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ attack_color = color.player_atk
</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">+ attack_color = color.enemy_atk
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> if damage &gt; 0:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- print(f"{attack_desc} for {damage} hit points.")
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ self.engine.message_log.add_message(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ f"{attack_desc} for {damage} hit points.", attack_color
</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> target.fighter.hp -= damage
</span></span><span style="display:flex;"><span> else:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- print(f"{attack_desc} but does no damage.")
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ self.engine.message_log.add_message(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ f"{attack_desc} but does no damage.", attack_color
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
from typing import Optional, Tuple, TYPE_CHECKING
<span class="new-text">import color</span>
if TYPE_CHECKING:
...
...
damage = self.entity.fighter.power - target.fighter.defense
attack_desc = f"{self.entity.name.capitalize()} attacks {target.name}"
<span class="new-text">if self.entity is self.engine.player:
attack_color = color.player_atk
else:
attack_color = color.enemy_atk</span>
if damage &gt; 0:
<span class="crossed-out-text">print(f"{attack_desc} for {damage} hit points.")</span>
<span class="new-text">self.engine.message_log.add_message(
f"{attack_desc} for {damage} hit points.", attack_color
)</span>
target.fighter.hp -= damage
else:
<span class="crossed-out-text">print(f"{attack_desc} but does no damage.")</span>
<span class="new-text">self.engine.message_log.add_message(
f"{attack_desc} but does no damage.", attack_color
)</span></pre>
</div>
</div>
<p>We determine the color based on who is doing the attacking. Other
than that, theres really nothing new here, were just pushing those
messages to the log rather than printing them.</p>
<p>Now we just need to update our death messages. Open up <code>fighter.py</code> and modify it like this:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from __future__ import annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>from typing import TYPE_CHECKING
</span></span><span style="display:flex;"><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"></span>from components.base_component import BaseComponent
</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 die(self) -&gt; None:
</span></span><span style="display:flex;"><span> if self.engine.player is self.entity:
</span></span><span style="display:flex;"><span> death_message = "You died!"
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ death_message_color = color.player_die
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.engine.event_handler = GameOverEventHandler(self.engine)
</span></span><span style="display:flex;"><span> else:
</span></span><span style="display:flex;"><span> death_message = f"{self.entity.name} is dead!"
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ death_message_color = color.enemy_die
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> self.entity.char = "%"
</span></span><span style="display:flex;"><span> self.entity.color = (191, 0, 0)
</span></span><span style="display:flex;"><span> self.entity.blocks_movement = False
</span></span><span style="display:flex;"><span> self.entity.ai = None
</span></span><span style="display:flex;"><span> self.entity.name = f"remains of {self.entity.name}"
</span></span><span style="display:flex;"><span> self.entity.render_order = RenderOrder.CORPSE
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- print(death_message)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ self.engine.message_log.add_message(death_message, death_message_color)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
from typing import TYPE_CHECKING
<span class="new-text">import color</span>
from components.base_component import BaseComponent
...
...
def die(self) -&gt; None:
if self.engine.player is self.entity:
death_message = "You died!"
<span class="new-text">death_message_color = color.player_die</span>
self.engine.event_handler = GameOverEventHandler(self.engine)
else:
death_message = f"{self.entity.name} is dead!"
<span class="new-text">death_message_color = color.enemy_die</span>
self.entity.char = "%"
self.entity.color = (191, 0, 0)
self.entity.blocks_movement = False
self.entity.ai = None
self.entity.name = f"remains of {self.entity.name}"
self.entity.render_order = RenderOrder.CORPSE
<span class="crossed-out-text">print(death_message)</span>
<span class="new-text">self.engine.message_log.add_message(death_message, death_message_color)</span></pre>
</div>
</div>
<p>Run the project now. You should see messages for both attacks and deaths!</p>
<p><img src="Part%207%20-%20Creating%20the%20Interface%20%C2%B7%20Roguelike%20Tutorials_files/part-7-death-messages.png" alt="Part 7 - Death Messages"></p>
<p>What next? One thing that would be nice is to see the names of the
different entities. This will become useful later on if you decide to
add more enemy types. Its easy enough to remember “Orc” and “Troll”,
but most roguelikes have a wide variety of enemies, so its helpful to
know what each letter on the screen means.</p>
<p>We can accomplish this by displaying the names of the entities that
are currently under the players mouse. Well need to make a few changes
to our project to capture the mouses current position, however.</p>
<p>Edit <code>main.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> root_console = tcod.Console(screen_width, screen_height, order="F")
</span></span><span style="display:flex;"><span> while True:
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ root_console.clear()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine.event_handler.on_render(console=root_console)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ context.present(root_console)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- engine.render(console=root_console, context=context)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ engine.event_handler.handle_events(context)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">- engine.event_handler.handle_events()
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre> root_console = tcod.Console(screen_width, screen_height, order="F")
while True:
<span class="new-text">root_console.clear()
engine.event_handler.on_render(console=root_console)
context.present(root_console)</span>
<span class="crossed-out-text">engine.render(console=root_console, context=context)</span>
<span class="new-text">engine.event_handler.handle_events(context)</span>
<span class="crossed-out-text">engine.event_handler.handle_events()</span></pre>
</div>
</div>
<p>Were adding the consoles <code>clear</code> back to main, as well as the contexts <code>present</code>. Also, were calling a method that we havent defined yet: <code>on_render</code>, but dont worry, well define it in a moment. Basically, this method tells the engine to render.</p>
<p>Were also passing the <code>context</code> to <code>handle_events</code> now, because we need to call an extra method on it to capture the mouse input.</p>
<p>Now lets modify <code>input_handlers.py</code> to contain the methods were calling in <code>main.py</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>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:#f92672">- def handle_events(self) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- raise NotImplementedError()
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def handle_events(self, context: tcod.context.Context) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for event in tcod.event.wait():
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ context.convert_event(event)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.dispatch(event)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> def ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:
</span></span><span style="display:flex;"><span> raise SystemExit()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def on_render(self, console: tcod.Console) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.render(console)
</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 MainGameEventHandler(EventHandler):
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def handle_events(self) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def handle_events(self, context: tcod.context.Context) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> for event in tcod.event.wait():
</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"></span>
</span></span><span style="display:flex;"><span> action = self.dispatch(event)
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</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) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def handle_events(self, context: tcod.context.Context) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class EventHandler(tcod.event.EventDispatch[Action]):
def __init__(self, engine: Engine):
self.engine = engine
<span class="crossed-out-text">def handle_events(self) -&gt; None:</span>
<span class="crossed-out-text">raise NotImplementedError()</span>
<span class="new-text">def handle_events(self, context: tcod.context.Context) -&gt; None:
for event in tcod.event.wait():
context.convert_event(event)
self.dispatch(event)</span>
def ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:
raise SystemExit()
<span class="new-text">def on_render(self, console: tcod.Console) -&gt; None:
self.engine.render(console)</span>
class MainGameEventHandler(EventHandler):
<span class="crossed-out-text">def handle_events(self) -&gt; None:</span>
<span class="new-text">def handle_events(self, context: tcod.context.Context) -&gt; None:</span>
for event in tcod.event.wait():
<span class="new-text">context.convert_event(event)</span>
action = self.dispatch(event)
...
class GameOverEventHandler(EventHandler):
<span class="crossed-out-text">def handle_events(self) -&gt; None:</span>
<span class="new-text">def handle_events(self, context: tcod.context.Context) -&gt; None:</span>
...</pre>
</div>
</div>
<p>Were modifying the <code>handle_events</code> method in <code>EventHandler</code> to actually have an implementation. It iterates through the events, and uses <code>context.convert_event</code> to give the event knowledge on the mouse position. It then dispatches that event, to be handled like normal.</p>
<p><code>on_render</code> just tells the <code>Engine</code> class to call its render method, using the given console.</p>
<p><code>MainGameEventHandler</code> and <code>GameOverEventHandler</code> have small changes to their <code>handle_events</code> methods to match the signature of <code>EventHandler</code>, and <code>MainGameEventHandler</code> also uses <code>context.convert_event</code>.</p>
<p>Were no longer passing the <code>context</code> to the <code>Engine</code> classs <code>render</code> method, so lets change the method 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>...
</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><span style="color:#f92672">-from tcod.context import Context
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>from tcod.console import Console
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class Engine:
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- def render(self, console: Console, context: Context) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ def render(self, console: Console) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.game_map.render(console)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> self.message_log.render(console=console, x=21, y=45, width=40, height=5)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> render_bar(
</span></span><span style="display:flex;"><span> console=console,
</span></span><span style="display:flex;"><span> current_value=self.player.fighter.hp,
</span></span><span style="display:flex;"><span> maximum_value=self.player.fighter.max_hp,
</span></span><span style="display:flex;"><span> total_width=20,
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- context.present(console)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- console.clear()
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
from typing import TYPE_CHECKING
<span class="crossed-out-text">from tcod.context import Context</span>
from tcod.console import Console
...
class Engine:
...
<span class="crossed-out-text">def render(self, console: Console, context: Context) -&gt; None:</span>
<span class="new-text">def render(self, console: Console) -&gt; None:</span>
self.game_map.render(console)
self.message_log.render(console=console, x=21, y=45, width=40, height=5)
render_bar(
console=console,
current_value=self.player.fighter.hp,
maximum_value=self.player.fighter.max_hp,
total_width=20,
)
<span class="crossed-out-text">context.present(console)</span>
<span class="crossed-out-text">console.clear()</span></pre>
</div>
</div>
<p>Weve also removed the <code>console.clear</code> call, as thats being handled by <code>main.py</code>.</p>
<p>So were passing the context around to different classes and
converting the events to capture the mouse location. But where does that
information actually get stored? Lets add a data point on to the <code>Engine</code> class to hold that information. Add the following to <code>engine.py</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class Engine:
</span></span><span style="display:flex;"><span> game_map: GameMap
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def __init__(self, player: Actor):
</span></span><span style="display:flex;"><span> self.event_handler: EventHandler = MainGameEventHandler(self)
</span></span><span style="display:flex;"><span> self.message_log = MessageLog()
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.mouse_location = (0, 0)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> self.player = player
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class Engine:
game_map: GameMap
def __init__(self, player: Actor):
self.event_handler: EventHandler = MainGameEventHandler(self)
self.message_log = MessageLog()
<span class="new-text">self.mouse_location = (0, 0)</span>
self.player = player</pre>
</div>
</div>
<p>Okay, so weve got a place to store the mouse location, but where do we actually get that information?</p>
<p>Theres an easy way: by overriding a method in <code>EventHandler</code>, which is called <code>ev_mousemotion</code>. By doing that, we can write the mouse location to the engine for access later. Heres how that looks:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class EventHandler(tcod.event.EventDispatch[Action]):
</span></span><span style="display:flex;"><span> def __init__(self, engine: Engine):
</span></span><span style="display:flex;"><span> self.engine = engine
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def handle_events(self, context: tcod.context.Context) -&gt; None:
</span></span><span style="display:flex;"><span> for event in tcod.event.wait():
</span></span><span style="display:flex;"><span> context.convert_event(event)
</span></span><span style="display:flex;"><span> self.dispatch(event)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def ev_mousemotion(self, event: tcod.event.MouseMotion) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.engine.game_map.in_bounds(event.tile.x, event.tile.y):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.mouse_location = event.tile.x, event.tile.y
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> def ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:
</span></span><span style="display:flex;"><span> raise SystemExit()
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class EventHandler(tcod.event.EventDispatch[Action]):
def __init__(self, engine: Engine):
self.engine = engine
def handle_events(self, context: tcod.context.Context) -&gt; None:
for event in tcod.event.wait():
context.convert_event(event)
self.dispatch(event)
<span class="new-text">def ev_mousemotion(self, event: tcod.event.MouseMotion) -&gt; None:
if self.engine.game_map.in_bounds(event.tile.x, event.tile.y):
self.engine.mouse_location = event.tile.x, event.tile.y</span>
def ev_quit(self, event: tcod.event.Quit) -&gt; Optional[Action]:
raise SystemExit()</pre>
</div>
</div>
<p>Great! Now were saving the mouses location, so its time to
actually make use of it. Our original goal was to display the entity
names that are in the mouses current position. The hard part is already
done, now all we need to do is check which entities are in the given
location, get their names, and print them out to the screen.</p>
<p>Since this has to do with rendering, lets put these new functions in <code>render_functions.py</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from __future__ import annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>from typing import TYPE_CHECKING
</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>if TYPE_CHECKING:
</span></span><span style="display:flex;"><span> from tcod import Console
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from engine import Engine
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ 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><span style="color:#a6e22e">+def get_names_at_location(x: int, y: int, game_map: GameMap) -&gt; str:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if not game_map.in_bounds(x, y) or not game_map.visible[x, y]:
</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">+ names = ", ".join(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ entity.name for entity in game_map.entities if entity.x == x and entity.y == y
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return names.capitalize()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>def render_bar(
</span></span><span style="display:flex;"><span> console: Console, current_value: int, maximum_value: int, total_width: int
</span></span><span style="display:flex;"><span>) -&gt; None:
</span></span><span style="display:flex;"><span> bar_width = int(float(current_value) / maximum_value * total_width)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> console.draw_rect(x=0, y=45, width=20, height=1, ch=1, bg=color.bar_empty)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> if bar_width &gt; 0:
</span></span><span style="display:flex;"><span> console.draw_rect(
</span></span><span style="display:flex;"><span> x=0, y=45, width=bar_width, height=1, ch=1, bg=color.bar_filled
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> console.print(
</span></span><span style="display:flex;"><span> x=1, y=45, string=f"HP: {current_value}/{maximum_value}", fg=color.bar_text
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+def render_names_at_mouse_location(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console: Console, x: int, y: int, engine: Engine
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ mouse_x, mouse_y = engine.mouse_location
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ names_at_mouse_location = get_names_at_location(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x=mouse_x, y=mouse_y, game_map=engine.game_map
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(x=x, y=y, string=names_at_mouse_location)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
from typing import TYPE_CHECKING
import color
if TYPE_CHECKING:
from tcod import Console
<span class="new-text">from engine import Engine
from game_map import GameMap
def get_names_at_location(x: int, y: int, game_map: GameMap) -&gt; str:
if not game_map.in_bounds(x, y) or not game_map.visible[x, y]:
return ""
names = ", ".join(
entity.name for entity in game_map.entities if entity.x == x and entity.y == y
)
return names.capitalize()</span>
def render_bar(
console: Console, current_value: int, maximum_value: int, total_width: int
) -&gt; None:
bar_width = int(float(current_value) / maximum_value * total_width)
console.draw_rect(x=0, y=45, width=20, height=1, ch=1, bg=color.bar_empty)
if bar_width &gt; 0:
console.draw_rect(
x=0, y=45, width=bar_width, height=1, ch=1, bg=color.bar_filled
)
console.print(
x=1, y=45, string=f"HP: {current_value}/{maximum_value}", fg=color.bar_text
)
<span class="new-text">def render_names_at_mouse_location(
console: Console, x: int, y: int, engine: Engine
) -&gt; None:
mouse_x, mouse_y = engine.mouse_location
names_at_mouse_location = get_names_at_location(
x=mouse_x, y=mouse_y, game_map=engine.game_map
)
console.print(x=x, y=y, string=names_at_mouse_location)</span></pre>
</div>
</div>
<p>Weve added two new functions, <code>render_names_at_mouse_location</code> and <code>get_names_at_location</code>. Lets discuss what each one does.</p>
<p><code>render_names_at_mouse_location</code> takes the console, x and y
coordinates (the location to draw the names), and the engine. From the
engine, it grabs the mouses current x and y positions, and passes them
to <code>get_names_at_location</code>, which we can assume for the
moment will return the list of entity names we want. Once we have these
entity names as a string, we can print that string to the given x and y
location on the screen, with <code>console.print</code>.</p>
<p><code>get_names_at_location</code> also takes “x” and “y” variables,
though these represent a spot on the map. We first check that the x and y
coordinates are within the map, and are currently visible to the
player. If they are, then we create a string of the entity names at that
spot, separated by a comma. We then return that string, adding <code>capitalize</code> to make sure the first letter in the string is capitalized.</p>
<p>Now all we need to do is modify <code>engine.py</code> to import these functions and utilize them in the <code>render</code> method. Make the following modifications:
</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 message_log import MessageLog
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from render_functions import render_bar
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from render_functions import render_bar, render_names_at_mouse_location
</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> def render(self, console: Console) -&gt; None:
</span></span><span style="display:flex;"><span> self.game_map.render(console)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> self.message_log.render(console=console, x=21, y=45, width=40, height=5)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> render_bar(
</span></span><span style="display:flex;"><span> console=console,
</span></span><span style="display:flex;"><span> current_value=self.player.fighter.hp,
</span></span><span style="display:flex;"><span> maximum_value=self.player.fighter.max_hp,
</span></span><span style="display:flex;"><span> total_width=20,
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ render_names_at_mouse_location(console=console, x=21, y=44, engine=self)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
from message_log import MessageLog
<span class="crossed-out-text">from render_functions import render_bar</span>
<span class="new-text">from render_functions import render_bar, render_names_at_mouse_location</span>
if TYPE_CHECKING:
...
...
def render(self, console: Console) -&gt; None:
self.game_map.render(console)
self.message_log.render(console=console, x=21, y=45, width=40, height=5)
render_bar(
console=console,
current_value=self.player.fighter.hp,
maximum_value=self.player.fighter.max_hp,
total_width=20,
)
<span class="new-text">render_names_at_mouse_location(console=console, x=21, y=44, engine=self)</span></pre>
</div>
</div>
<p></p>
<p>Now if you hover your mouse over an entity, youll see its name. If
you stack a few corpses up, youll notice that it prints a list of the
names.</p>
<p>Were almost finished with this chapter. Before we wrap up, lets
revisit our message log for a moment. One issue with it is that we cant
see messages that are too far back. However, HexDecimal was kind enough
to provide a method for viewing the whole log, with the ability to
scroll.</p>
<p>Add the following to <code>input_handlers.py</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class GameOverEventHandler(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">+CURSOR_Y_KEYS = {
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_UP: -1,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_DOWN: 1,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_PAGEUP: -10,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ tcod.event.K_PAGEDOWN: 10,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+}
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+class HistoryViewer(EventHandler):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Print the history on a larger window which can be navigated."""
</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, engine: Engine):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ super().__init__(engine)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.log_length = len(engine.message_log.messages)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.cursor = self.log_length - 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">+ def on_render(self, console: tcod.Console) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ super().on_render(console) # Draw the main state as the background.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ log_console = tcod.Console(console.width - 6, console.height - 6)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Draw a frame with a custom banner title.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ log_console.draw_frame(0, 0, log_console.width, log_console.height)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ log_console.print_box(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ 0, 0, log_console.width, 1, "┤Message history├", alignment=tcod.CENTER
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Render the message log using the cursor parameter.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.message_log.render_messages(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ log_console,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ 1,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ 1,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ log_console.width - 2,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ log_console.height - 2,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.message_log.messages[: self.cursor + 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">+ log_console.blit(console, 3, 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">+ def ev_keydown(self, event: tcod.event.KeyDown) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Fancy conditional movement to make it feel right.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if event.sym in CURSOR_Y_KEYS:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ adjust = CURSOR_Y_KEYS[event.sym]
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if adjust &lt; 0 and self.cursor == 0:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Only move from the top to the bottom when you're on the edge.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.cursor = self.log_length - 1
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ elif adjust &gt; 0 and self.cursor == self.log_length - 1:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Same with bottom to top movement.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.cursor = 0
</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">+ # Otherwise move while staying clamped to the bounds of the history log.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.cursor = max(0, min(self.cursor + adjust, self.log_length - 1))
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ elif event.sym == tcod.event.K_HOME:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.cursor = 0 # Move directly to the top message.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ elif event.sym == tcod.event.K_END:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.cursor = self.log_length - 1 # Move directly to the last message.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ else: # Any other key moves back to the main game state.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.event_handler = MainGameEventHandler(self.engine)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class GameOverEventHandler(EventHandler):
...
<span class="new-text">CURSOR_Y_KEYS = {
tcod.event.K_UP: -1,
tcod.event.K_DOWN: 1,
tcod.event.K_PAGEUP: -10,
tcod.event.K_PAGEDOWN: 10,
}
class HistoryViewer(EventHandler):
"""Print the history on a larger window which can be navigated."""
def __init__(self, engine: Engine):
super().__init__(engine)
self.log_length = len(engine.message_log.messages)
self.cursor = self.log_length - 1
def on_render(self, console: tcod.Console) -&gt; None:
super().on_render(console) # Draw the main state as the background.
log_console = tcod.Console(console.width - 6, console.height - 6)
# Draw a frame with a custom banner title.
log_console.draw_frame(0, 0, log_console.width, log_console.height)
log_console.print_box(
0, 0, log_console.width, 1, "┤Message history├", alignment=tcod.CENTER
)
# Render the message log using the cursor parameter.
self.engine.message_log.render_messages(
log_console,
1,
1,
log_console.width - 2,
log_console.height - 2,
self.engine.message_log.messages[: self.cursor + 1],
)
log_console.blit(console, 3, 3)
def ev_keydown(self, event: tcod.event.KeyDown) -&gt; None:
# Fancy conditional movement to make it feel right.
if event.sym in CURSOR_Y_KEYS:
adjust = CURSOR_Y_KEYS[event.sym]
if adjust &lt; 0 and self.cursor == 0:
# Only move from the top to the bottom when you're on the edge.
self.cursor = self.log_length - 1
elif adjust &gt; 0 and self.cursor == self.log_length - 1:
# Same with bottom to top movement.
self.cursor = 0
else:
# Otherwise move while staying clamped to the bounds of the history log.
self.cursor = max(0, min(self.cursor + adjust, self.log_length - 1))
elif event.sym == tcod.event.K_HOME:
self.cursor = 0 # Move directly to the top message.
elif event.sym == tcod.event.K_END:
self.cursor = self.log_length - 1 # Move directly to the last message.
else: # Any other key moves back to the main game state.
self.engine.event_handler = MainGameEventHandler(self.engine)</span></pre>
</div>
</div>
<p>To show this new view, all we need to do is this, in <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_ESCAPE:
</span></span><span style="display:flex;"><span> action = EscapeAction(player)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ elif key == tcod.event.K_v:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.engine.event_handler = HistoryViewer(self.engine)
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre> ...
elif key == tcod.event.K_ESCAPE:
action = EscapeAction(player)
<span class="new-text">elif key == tcod.event.K_v:
self.engine.event_handler = HistoryViewer(self.engine)</span></pre>
</div>
</div>
<p>Now all the player has to do is press the “v” key to see a log of all
past messages. By using the up and down keys, you can scroll through
the log.</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-7">click here</a>.</p>
<p><a href="https://rogueliketutorials.com/tutorials/tcod/v2/part-8">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> &amp; <a href="https://github.com/luizdepra/hugo-coder/">Coder</a>.
</section>
</footer>
</main>
<script src="Part%207%20-%20Creating%20the%20Interface%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.236049395dc3682fb2719640872958e12f1f24067bb09c327b2.js" integrity="sha256-I2BJOV3DaC+ycZZAhylY4S8fJAZ7sJwyeyM+YpDH7aw="></script>
<script src="Part%207%20-%20Creating%20the%20Interface%20%C2%B7%20Roguelike%20Tutorials_files/codetabs.min.cc52451e7f25e50f64c1c893826f606d58410d742c214dce.js" integrity="sha256-zFJFHn8l5Q9kwciTgm9gbVhBDXQsIU3OI/tEfJlh8rA="></script>
</body></html>