Chapter 14 · ~27 min read

Status Effects

Everything a potion has done so far has been over at once: you drink it, it heals you or makes you stronger, and that's that. Some of the best moments in a roguelike come from effects that last. Poison doesn't hurt once; it hurts every turn for a while, and you watch your health fall while you decide what to do about it.

This chapter adds four such statuses: poison, which takes a point of health every turn; regeneration, which gives one back; confusion, which sends your steps astray; and fear, which makes the monsters run. Each is a count of turns, and each turn counts it down. It's kept on Entity, so the player and every monster could have any of them. In this game, though, poison, regeneration and confusion come only to you, from potions and bites, and fear only to the monsters, from a scroll. The next image shows the end of the chapter, with fear at work.

The end of Chapter 14
Figure 14.1 — The end of Chapter 14

A cave spider bit you, and you read an unknown scroll, which turned out to be a scroll of fear. The spider, unhurt, is running for the room's far doorway, and the look panel says why: it's afraid. Its bite is still poisoning you, for two more turns.

Project folder: Chapter 14 in the book's repository holds this chapter's finished project. The chapter carries on in your own project from Chapter 13, or from the book's Chapter 13 folder.

In this chapter, we will:

  • Give the player and the monsters statuses: counts of turns that tick down
  • Add potions of poison and regeneration, and tick their statuses at the right moment in every turn
  • Make a death by poison final, whatever regeneration has to say
  • Add confusion, which sends half your steps in a direction of their own
  • Add a cave spider, whose bite poisons
  • Add a scroll of fear, which makes the monsters in sight run
  • Show the player's statuses in the side panel, and a monster's in the look panel

There are four checkpoints along the way. Let's start with the plan.

Planning the Chapter

This chapter adds no files. It changes six, along with the tile atlas:

File Status What it's for
src/Entity.h Changed The statuses, which the player and the monsters share
src/Item.h Changed Potions of poison, regeneration and confusion, and the scroll of fear
src/Monster.h Changed Venom, and the cave spider
src/Common.h Changed The statuses' colors in the side panel
src/Game.h, src/Game.cpp Changed The potions' effects, ticking the statuses in the turn's order, staggering, venom, fear, and the panels
assets/tiles.png, assets/tiles.txt Changed A picture of the cave spider
The other files Unchanged

The next image shows how the files fit together at the end of the chapter. Nothing is new. The statuses live in Entity, which the player and the monsters are both built on. So one set of fields, and one function to tick them, serves everyone.

The files at the end of Chapter 14
Figure 14.2 — The files at the end of Chapter 14

A status is a number: how many more turns it lasts. Zero means it isn't on. A potion of poison sets the player's count to six. Each turn takes the count down by one, and a point of health with it, until the count reaches zero and the poison is gone.

Counts are the simplest way there is to keep statuses. A bigger game might give each creature a list of status objects, each with a kind and a count of its own, so that a new status would need no new field. Four statuses don't need that machinery. A field each is easy to read, easy to show in the panel, and easy to save, which Chapter 17 will do.

When the ticking happens matters, since health is involved. The game already has an order for each turn: the player acts, the monsters act, the player heals a little now and then, and last of all comes the one check for the player's death. Statuses tick after the monsters, and before that check, as the next image shows.

One turn, in order
Figure 14.3 — One turn, in order

Ticking before the death check means a death by poison is found where every other death is. And because regeneration ticks after poison, the order inside the tick matters too, as you'll see.

Each checkpoint has a seed of its own this time, chosen for what lies near the start: a potion of poison, a potion of confusion, a cave spider, and a scroll of fear. We'll build the chapter in four steps: poison and regeneration, confusion, the cave spider, and fear.

Poison and Regeneration

The first step adds the statuses themselves, two potions that cause them, and the code that ticks them. It ends with both counting down in the side panel.

A Count of Turns

Open Entity.h. Replace the comment above struct Entity with this:

// What the player and the monsters have in common: a place on the map,
// health, a defense that takes something off every blow they suffer, and
// statuses

Then add the statuses below int defense = 0;:

// Statuses: how many more turns each one lasts. 0 means it isn't on
int poisoned = 0;       // loses 1 health a turn
int regenerating = 0;   // gains 1 health a turn

Each status is a plain int, with zero written in place, so every player and monster starts with none. The comments say what a turn of each does. Nothing reads them yet.

A count does two jobs at once. Anything above zero means the status is on. And the number says how long it has left, which the side panel will show. A bool and a separate timer would say the same thing in two fields, which could disagree.

Two Potions

Open Item.h, and add two effects at the end of ItemEffect, below Identify:

Poison,       // poisons the player
Regenerate,   // heals the player a little each turn, for a while

The comment above the table promised this chapter's potions, which are about to arrive. Replace the comment above ITEM_DEFS with this:

// Every kind of item there is, so far. Chapter 21 adds bows, arrows,
// darts, and the scrolls of lightning and fireball

Then add the two potions below the potion of strength:

{ "poison", ItemClass::Potion, ItemEffect::Poison, "potion" },
{ "regeneration", ItemClass::Potion, ItemEffect::Regenerate, "potion" },

A potion of poison is the first kind that's bad for you, and it hides behind a color like any other. That's what makes drinking an unknown potion a gamble, rather than a free gift. Each run, one of the five colors now belongs to a potion you'd rather not drink. Confusion, later in the chapter, makes it two of six.

How long each status lasts is a number to tune. Open Game.cpp, and add these to the unnamed namespace, below HEAL_AMOUNT:

// How long the statuses from potions last, in turns (*tune*)
constexpr int POISON_TURNS = 6;
constexpr int REGENERATION_TURNS = 12;

A potion of greater healing should cure poison too, as a potion that "gives back all of it" ought to. In applyEffect, add this line below player_.hp = player_.maxHp;:

player_.poisoned = 0;

Then add the two new effects at the end of the switch, below the scroll of identify's case:

case ItemEffect::Poison:
    player_.poisoned = SDL_max(player_.poisoned, POISON_TURNS);
    log_.add("You feel very sick!");
    break;
case ItemEffect::Regenerate:
    player_.regenerating = SDL_max(player_.regenerating,
                                   REGENERATION_TURNS);
    log_.add("You feel your wounds begin to close.");
    break;

Each potion sets its count with SDL_max, the larger of the two numbers it's given. A second dose while the first still lasts doesn't add to it: it starts the count again from the full six, or twelve, unless more than that is left. That keeps bad luck from piling up.

Ticking

One function ticks every status of any entity, player or monster. Add it below applyEffect:

// One turn of an entity's statuses, each counting down: poison hurts,
// then regeneration heals. Poison that kills ends it there, so no
// regeneration can bring the dead back
void Game::tickStatuses(Entity& entity)
{
    if (entity.poisoned > 0)
    {
        --entity.poisoned;
        --entity.hp;
    }
    if (!entity.isAlive())
    {
        return;
    }
    if (entity.regenerating > 0)
    {
        --entity.regenerating;
        entity.hp = SDL_min(entity.maxHp, entity.hp + 1);
    }
}

In the preceding code, poison comes first: it takes a turn off its count, and a point of health. Then, if the entity is still alive, regeneration takes a turn off its own count. It gives a point back, but never past the most it can have.

The return in the middle is the most important line in the chapter. Without it, a player with one point of health, poisoned and regenerating, would drop to zero. Then regeneration would lift them back to one. They'd be dead for a moment and then not. With it, poison that kills ends the tick, so a death by poison is final, and no status can undo it.

That's why the order inside the tick matters. With regeneration first, the same player would heal to two. Then the poison would leave them at one, alive, which is a different game. Putting the harm first, and stopping at a death, makes the rule simple: what kills you, kills you. The next image shows the three ways the turn could end for that player.

Harm first, and a death ends the tick
Figure 14.4 — Harm first, and a death ends the tick

Only the first is the game's, and the return is what makes it so.

Add the declaration to Game.h, below applyEffect's:

void tickStatuses(Entity& entity);

The parameter is a reference, as the & says, so the function changes the entity it's given. A copy would tick and be thrown away, and the real entity's statuses would never run out.

The Turn's Order

The statuses tick in finishTurn, which ends every turn the player takes. In Game.cpp, replace the comment above finishTurn with this:

// The rest of a turn, once the player has acted, always in this order:
// the monsters act; statuses tick, for the player and then the monsters;
// the player heals a little now and then; and last of all comes the one
// check for the player's death

Then add the player's statuses below ++turn_;:

// A player the monsters have killed has no statuses left to tick
if (player_.isAlive())
{
    bool wasPoisoned = player_.poisoned > 0;
    bool wasRegenerating = player_.regenerating > 0;
    tickStatuses(player_);
    if (!player_.isAlive())
    {
        player_.killedBy = "poison";
    }
    else if (wasPoisoned && player_.poisoned == 0)
    {
        log_.add("You feel the poison leave you.");
    }
    if (player_.isAlive() && wasRegenerating &&
        player_.regenerating == 0)
    {
        log_.add("Your wounds stop closing.");
    }
}

In the preceding code, a player the monsters have just killed is left alone, since a dead player's poison has nothing left to do. Otherwise, the statuses tick. Then the code compares what was on before with what's on after, to tell the player when something ends. If the poison has killed them, it's named as what did it, for the death screen: "Killed by poison". The check for their death comes later, at the end of finishTurn, as it always has.

The messages could have gone in tickStatuses, but that function works for any entity. Most monsters tick out of sight, where a message would make no sense. So tickStatuses does the ticking, and finishTurn, which knows it's dealing with the player, does the telling. The was flags are taken before the tick. Only by comparing before and after can the code tell that a status has just ended, rather than being off all along.

The monsters tick too. Add their loop below the player's:

for (Monster& monster : monsters_)
{
    tickStatuses(monster);
    if (!monster.isAlive())
    {
        announce(monster, "dies.");
    }
}
removeDeadMonsters();

A monster that dies of a status "dies.", which the player is told only if they can see it. Then removeDeadMonsters takes it away, as a blow's victim is. Nothing in the game poisons a monster, but the rule is written once, for every entity. The last step of this chapter gives monsters a status of their own, fear, which ticks in this loop. And an experiment at the end of the chapter poisons every monster, to watch the loop at work.

Design note: Rogue's fuses. Rogue timed its lasting effects with fuses: a function to call, and a count of turns, kept in a short list that the game counted down every turn. A potion of confusion set a flag on the player and lit a fuse of about twenty turns. When it burned out, its function cleared the flag: "you feel less confused now". Drinking a second lengthened the fuse, so doses added up, where ours keep the larger count. Rogue's potion of poison wasn't timed at all: it simply took one to three points of strength, with the words "you feel very sick now".

Ours keeps each status on the creature itself, rather than in a list of timers. So there's nothing to tidy up when a creature dies: its statuses go with it.

In the Panel

The side panel should show what's on, and for how long. Each status gets a color. Open Common.h, and add these to Palette, below SCROLL:

// Statuses, in the panel: poison is sickly green, and regeneration a
// healthy one
constexpr SDL_Color POISONED = { 150, 200, 60, 255 };
constexpr SDL_Color REGENERATING = { 110, 220, 160, 255 };

In Game.cpp, in drawSidePanel, add these lines below the equipment's:

// Statuses, with the turns each has left
if (player_.poisoned > 0)
{
    glyphs_.drawText("Poisoned (" + std::to_string(player_.poisoned) +
                     ")", x, y, Palette::POISONED);
    y += row;
}
if (player_.regenerating > 0)
{
    glyphs_.drawText("Regenerating (" +
                     std::to_string(player_.regenerating) + ")", x, y,
                     Palette::REGENERATING);
    y += row;
}

A status the player doesn't have takes no room, so the panel is as it was until something happens. Each line shows the turns left, which is the count after this turn's tick.

Checkpoint: Change the seed in Command Arguments to 77, and press F5. A potion lies two steps up from you: walk onto it with k twice, pick it up, and drink it with i and a. "It was a potion of poison." and "You feel very sick!" The side panel says "Poisoned (5)", in green, and your health is 29, since the turn's tick has already run. The next image shows it, beside the other new potion.

Two potions that last
Figure 14.5 — Two potions that last

Wait with the period key. Each turn takes a point of health and a turn off the count, until "You feel the poison leave you.", with 24 health left.

Then try seed 283. The potion two steps down and to the right, with n twice, is a potion of regeneration. At full health, it has nothing to heal, so the count runs down with nothing to show for it, until "Your wounds stop closing." A potion of regeneration is worth keeping until you're hurt.

Confusion

The second step adds confusion. A confused player still chooses where to go, but half their steps go somewhere else.

Staggering

Open Entity.h, and add the new status below regenerating:

int confused = 0;       // half of its moves go astray

In Item.h, add its effect below Regenerate:

Confuse,      // sends half the player's steps astray, for a while

Then add the potion below the potion of regeneration:

{ "confusion", ItemClass::Potion, ItemEffect::Confuse, "potion" },

In Common.h, add its color below REGENERATING:

constexpr SDL_Color CONFUSED = { 220, 150, 230, 255 };

In Game.cpp, add its length below REGENERATION_TURNS:

constexpr int CONFUSION_TURNS = 8;

Then the potion's effect. In applyEffect, add this case below regeneration's:

case ItemEffect::Confuse:
    player_.confused = SDL_max(player_.confused, CONFUSION_TURNS);
    log_.add("You feel dizzy!");
    break;

The stagger itself goes where every step is taken. In playerStep, add this below log_.markRead();, with a blank line on each side:

// Confused, half of the player's steps go in a direction of their own
bool moving = step.x != 0 || step.y != 0;
bool staggered = false;
if (moving && player_.confused > 0 && rng_.roll(0, 1) == 0)
{
    step = DIRECTIONS[rng_.roll(0, 7)];
    log_.add("You stagger.");
    staggered = true;
}

A confused step rolls for a coin's toss. Half the time, the step is replaced by one of the eight directions, chosen at random from DIRECTIONS, Chapter 5's list of them. Everything after that treats the new step as the player's own. The coin is rng_.roll(0, 1), and rng_.roll(0, 7) picks one of the eight entries of DIRECTIONS, numbered from zero to seven. One stagger in eight happens to go the way you meant anyway, so in all, nine steps in sixteen go where you sent them.

A stagger into a monster attacks it. Waiting is a step of nowhere, and moving keeps it from staggering, since standing still while confused should still be standing still.

A stagger into a wall is a bump, and a bump you choose costs nothing. One you didn't choose should cost the turn, or confusion would cost almost nothing, so staggered remembers that the step went astray. The wall's branch, at the end of playerStep, uses it. Add these lines below log_.add("You bump into the wall.");:

// A stagger into the wall still costs the turn, since the player
// didn't choose it
if (staggered)
{
    finishTurn();
}

Then replace the comment above playerStep with this:

// Stepping onto a monster attacks it. An attack or a step takes a turn, and
// then the rest of the turn plays out. Bumping into a wall takes no turn,
// unless the player staggered into it

A staggered bump ends the turn, so the monsters act, and the statuses tick, as after any step.

Confusion counts down like the others, and tickStatuses's comment already says so: each status counts down. Add this at the top of tickStatuses, above if (entity.poisoned > 0):

if (entity.confused > 0)
{
    --entity.confused;
}

The player should hear when it ends. In finishTurn, add this line below bool wasRegenerating = player_.regenerating > 0;:

bool wasConfused = player_.confused > 0;

Then add this below tickStatuses(player_);:

if (wasConfused && player_.confused == 0)
{
    log_.add("Your head clears.");
}

Last, the side panel. In drawSidePanel, add these lines below the regeneration's:

if (player_.confused > 0)
{
    glyphs_.drawText("Confused (" + std::to_string(player_.confused) +
                     ")", x, y, Palette::CONFUSED);
    y += row;
}

The panel's statuses are now three, each in its own color. Confusion doesn't hurt, so it has nothing to do with the death check, and nothing to say on the death screen.

Checkpoint: Change the seed to 15, and press F5. A potion lies two steps up and to the left of you: walk onto it with y and h, pick it up, and drink it with i and a. "It was a potion of confusion." and "You feel dizzy!" The panel says "Confused (7)". Now step down and to the right with n, and try walking east, along the passage out of the room, with l. About half your steps stagger, as the next image shows.

A confused step
Figure 14.6 — A confused step

A stagger into a wall is a bump that costs the turn, so the turn counter moves on. Wait with the period key instead, and you never stagger. On turn 11, "Your head clears."

A Venomous Bite

The third step brings a monster whose bite poisons: the cave spider. Poison from a potion was the player's own doing; poison from a bite is a fight that keeps hurting after it's over.

The Cave Spider

A monster's kind says how long its bite's poison lasts. Open Monster.h, and add the field below traits:

int venom = 0;      // a hit poisons for this many turns

Every other kind leaves it out, and takes the zero written in place: no venom. The table's comment lists the fields in order, and needs the new one. In Monster.h, change damage from and to, defense, traits to damage from and to, defense, traits, venom. Then add the spider at the end of the table, below the shrieker:

{ "cave spider", 'S', { 150, 130, 175, 255 }, "cave-spider",
  5, 5, 1, 3, 0, 0, 4 },

The spider sees five cells, has five health, bites for one to three, and has no defense. That makes it a weak fighter. The 0 after its defense is for its traits, since it has none. And the 4 is its venom: four turns of poison with every bite. Its character is a capital S, and it has a picture of its own in the tile view.

Venom belongs to the kind, in MonsterDef, since every cave spider bites the same way. Poison belongs to the entity, in Entity, since each victim has a count of its own. It's the same split as damage, which the kind decides, and health, which each monster has for itself.

The tile view needs its picture. From the book's repository, copy tiles.png and tiles.txt from the Chapter 14 folder's assets folder into your project's assets folder, in place of the old ones.

The bite is a monster's blow, in monsterAttacks. In Game.cpp, add this below the if that names what killed the player:

else if (monster.def->venom > 0)
{
    // A venomous bite poisons, for at least as long as its venom
    // lasts. Only the first bite needs saying so
    if (player_.poisoned == 0)
    {
        log_.add("You are poisoned!");
    }
    player_.poisoned = SDL_max(player_.poisoned, monster.def->venom);
}

A bite that didn't kill poisons the player for at least the venom's turns, by the same SDL_max as the potion. Only the first bite of a poisoning is announced: "You are poisoned!" Announcing every bite would bury the fight's messages. A bite that kills needs no poison, so the venom is on the else of the death's if.

Checkpoint: Change the seed to 46, and press F5. A cave spider sleeps in the room below yours. Walk down toward it with j: on your third step, "The cave spider wakes up!", and on your fifth, it bites. "The cave spider hits you for 1." and "You are poisoned!" The panel says "Poisoned (3)": four turns of venom, less this turn's tick. The next image shows the bite.

A cave spider's bite
Figure 14.7 — A cave spider's bite

A goblin has noticed you too, from the far end of the room. Hit the spider with j, and it flees, badly wounded, as Chapter 10's monsters do. Meanwhile, its poison runs its course: on turn 8, "You feel the poison leave you." Then deal with the goblin.

Fear

The last step adds a scroll that turns the tables: every monster in sight flees, whatever its health, for ten turns. Fleeing is Chapter 10's work, and Chapter 11 made it one of a monster's states. So most of what fear needs is already there.

The Scroll of Fear

Fear is a status too. In Entity.h, add it below confused:

int afraid = 0;         // a monster flees, whatever its health

In Item.h, add its effect below Confuse:

Frighten,     // makes the monsters in sight flee, for a while

Then add the scroll below the scroll of identify:

{ "fear", ItemClass::Scroll, ItemEffect::Frighten, "scroll" },

In Game.cpp, add its length below CONFUSION_TURNS:

constexpr int FEAR_TURNS = 10;

In applyEffect, add the scroll's case below confusion's:

case ItemEffect::Frighten:
    frightenMonsters();
    break;

It has a function of its own, since it works on every monster. Add frightenMonsters below applyEffect:

// The scroll of fear: every monster in sight is afraid for FEAR_TURNS,
// waking if it was asleep, and flees, whatever its health. A monster that
// can't move can't flee, so it takes no notice
void Game::frightenMonsters()
{
    bool anyone = false;
    for (Monster& monster : monsters_)
    {
        bool inSight = map_.isVisible(monster.pos.x, monster.pos.y);
        if (inSight && !monster.def->has(Trait::STATIONARY))
        {
            monster.afraid = FEAR_TURNS;
            monster.state = MonsterState::Fleeing;
            monster.lastSeen.reset();
            announce(monster, "flees in terror!");
            anyone = true;
        }
    }
    if (!anyone)
    {
        log_.add("A dreadful cry echoes, and fades.");
    }
}

In the preceding code, every monster the player can see, and that can move, gets ten turns of fear. It starts fleeing at once, asleep or not. Its lastSeen is cleared, as a hunter's is when it starts to flee, so it doesn't head back to where it saw the player.

A monster that can't move, such as the shrieker, can't flee, so it takes no notice. With nothing in sight, the scroll is wasted, and the log says so. Each monster's line comes from Chapter 11's announce, which speaks only of monsters the player can see, and every frightened monster is one.

Add the declaration to Game.h, below tickStatuses's:

void frightenMonsters();

Fear counts down like the rest. In Game.cpp, in tickStatuses, add this below the confusion's countdown:

if (entity.afraid > 0)
{
    --entity.afraid;
}

Fear doesn't hurt, so like confusion, it has nothing to do with the death check. And only monsters are ever afraid, since nothing frightens the player.

Running Scared

A fleeing monster stops fleeing once it has healed past two thirds of its health, and a hunter flees once it's badly wounded. Fear must win over both. Replace the comment above actHunting with this:

// A hunting monster goes after the player, unless it's badly wounded or
// afraid, when it flees instead. Seeing them, it attacks if it's next to
// them, and otherwise comes closer by the shortest way, remembering where
// they are. Having lost them, it goes to where it last saw them, and if
// they're not there, it searches nearby for a while, then gives up

Then, in actHunting, change if (isBadlyWounded(monster)) to if (isBadlyWounded(monster) || monster.afraid > 0). A hunter that's afraid flees, however healthy it is. Now replace the comment above actFleeing with this:

// A fleeing monster runs from the player, down the flee map, while it can
// see them, and lies low while it can't. Cornered, with nowhere lower to
// go, it turns and fights. Healed past two thirds of its health, and no
// longer afraid, it hunts again

Then find this line in actFleeing:

if (monster.hp * 3 > monster.maxHp * 2)

Replace it with these lines:

bool healed = monster.hp * 3 > monster.maxHp * 2;
if (healed && monster.afraid == 0)

A frightened monster that was never hurt is healed from the start. Without the second test, it would turn to hunting at once. Worse, actHunting sends an afraid monster straight back to actFleeing, and the two functions would call each other forever.

With the test, the monster flees until its fear has run out. Running from the player is Chapter 10's flee map at work, so a frightened monster runs the best way there is. And once it's out of sight, it lies low, as any fleeing monster does, and waits out its fear where it is.

One case is left. A monster you've hurt down to half its health is still fit to fight, but it isn't healed past two thirds. Frightened, it would flee, and when its fear ran out, it would flee on for good, since nothing in the game heals a monster. So the end of a monster's fear has to end its flight too. In finishTurn, find this line, where the monsters' statuses tick:

tickStatuses(monster);

Replace it with these lines:

bool wasAfraid = monster.afraid > 0;
tickStatuses(monster);
// Fear that runs out ends the flight it began, unless the monster
// is badly wounded, and would run anyway
if (wasAfraid && monster.afraid == 0 && !isBadlyWounded(monster))
{
    monster.state = MonsterState::Hunting;
}

As with the player's statuses, the flag is taken before the tick, so the code can tell that the fear has just ended. A monster that's badly wounded flees on, as it would have without the scroll. Any other goes back to hunting.

What the Look Panel Sees

A frightened monster should look frightened. The look panel names a monster and says how hurt it looks; now it can say more, as long as it's plain to see. The panel needs a line more for that, since a long description wraps. In Game.cpp, change LOOK_PANEL_LINES = 7 to LOOK_PANEL_LINES = 8.

Then, in drawLookPanel, find these lines:

std::string name = withArticle(monster->def->name);
y = drawWrapped(capitalized(name) + ", " + woundsOf(*monster) + ".",
                y, Palette::TEXT);

Replace them with these:

// Its name, how hurt it looks, and anything else that's plain to
// see
std::string text = capitalized(withArticle(monster->def->name)) +
                   ", " + woundsOf(*monster);
if (monster->state == MonsterState::Asleep)
{
    text += ", asleep";
}
if (monster->afraid > 0)
{
    text += ", afraid";
}
if (monster->poisoned > 0)
{
    text += ", poisoned";
}
y = drawWrapped(text + ".", y, Palette::TEXT);

The description is built a piece at a time: "A goblin, unhurt", then ", asleep" if it is, ", afraid", and ", poisoned". A sleeping monster is plain to see, and so is a frightened one, running. The panel never shows how many turns a monster's fear has left, since the player has no way to know that. Nothing in the game poisons a monster, so only the first experiment at the end of the chapter shows ", poisoned".

Design note: Scaring monsters. Rogue scared its monsters the other way around. Reading its scroll of scare monster did nothing but make you "hear maniacal laughter in the distance". The scroll's power worked on the floor: no monster would step onto it, or strike at a player standing on it. Pick it up a second time, though, and it turned to dust. Ours works the moment it's read, on everything in sight, and only for ten turns, so it's a way out of a fight, not a fortress.

Checkpoint: Change the seed to 121, press F5, and press F4 to see the monsters' minds. Walk down into the room below: press n, then j six times, then b, and j twice. A jackal comes for you; kill it with n. The fight's noise wakes a cave spider in the room, and it comes for you too. Press n, j, l and l, onto the scroll by the room's right-hand wall, and pick it up, while the spider bites.

Now read the scroll, with i and a. "It was a scroll of fear." and "The cave spider flees in terror!" Its outline turns yellow, and it runs for the doorway at the bottom of the room, as the next image shows.

The spider's flight
Figure 14.8 — The spider's flight

Point at it with the mouse, or with x, and the look panel says "A cave spider, unhurt, afraid.", as Figure 14.1 showed. Ten turns after you read the scroll, its fear runs out. It's unhurt, so it hunts again. But it has lost you, and soon "The cave spider gives up the hunt."

Playing the Game

Statuses make the potions a real gamble. An unknown potion might heal you, or might poison you, so drinking one with little health left is a bigger risk than it was. With plenty of health, poison's six points are a fair price for learning a color.

Cave spiders hurt more than their bites. A fight with one costs a point every turn for a while after it's over. So it's worth finishing quickly, and not worth starting with only a few points of health left. Confusion costs you near walls too, since a stagger into one still takes your turn, and whatever you're fighting strikes anyway. In the open, every stagger is a step you didn't choose, perhaps toward a monster.

The scroll of fear is a way out. Read it when a fight is going badly, and everything in sight runs for ten turns. That's time to heal, to drink, or to get away.

Understanding the Code

Statuses are counts of turns on Entity: poisoned, regenerating, confused and afraid. A potion or a scroll sets a count, with SDL_max, so doses don't add up. And a cave spider's bite sets the player's poison from its kind's venom. Each turn, finishTurn ticks every entity's statuses in tickStatuses, after the monsters have acted and before the death check. Then it tells the player when one ends.

Inside the tick, harm comes before healing, and a death ends it. So regeneration can't undo a death by poison. Confusion changes a step in playerStep. Fear works through Chapter 11's states: a frightened monster flees, and stays fleeing until its fear runs out. The side panel shows the player's statuses, and the look panel what can be seen of a monster's.

Experimenting

Try each of these, and put each back afterward:

  • Poisoned monsters. In placeMonsters, add monster.poisoned = 6; below monster.defense = monster.def->defense;. Press F5, with seed 1, then F1 and F4, and wait six turns with the period key. One by one, the monsters with six health or less die and vanish from the list. Only the kobold and the goblin are left, wounded. A monster that dies where you can see it "dies." in the log.
  • Death undone. In the constructor, below player_.maxHp = PLAYER_HP;, add three lines: player_.hp = 1;, player_.poisoned = 6; and player_.regenerating = 12;. Press F5, and wait a turn: "Killed by poison, on turn 1." Then delete the if with the return from the middle of tickStatuses, and try again. The player lives, at one health, until the poison runs out.
  • A longer fear. Set FEAR_TURNS to 100. The monsters run for a hundred turns, and a scroll of fear becomes a way to clear a room.

The second experiment is the chapter's main idea in a few lines: the order of two statuses, and one return, decide whether a death sticks.

Common Errors and Fixes

If the game didn't build, or doesn't behave as described, the problem is probably one of these.

LNK2019: unresolved external symbol "private: void __cdecl Game::tickStatuses(struct Entity &)", referenced in Game::finishTurn, followed by LNK1120. The function is declared in Game.h, but its definition is missing. Or the definition is missing its Game::, so it defines a function of its own that nothing calls.

C3490: 'poisoned' cannot be modified because it is being accessed through a const object, and the same for hp and regenerating, in tickStatuses. Its parameter is const Entity& entity, which promises not to change the entity. But ticking it is all about changing it. Take out the const, in Game.h and in Game.cpp.

Poison never ends, and never hurts: the panel says "Poisoned (6)" turn after turn. The parameter of tickStatuses is Entity entity, without the &. So each turn ticks a copy, and throws it away. Add the &, in both places.

Waiting while confused sometimes moves you, with "You stagger." The stagger's test in playerStep is missing moving &&, so the period key's step of nowhere can stagger too.

The cave spider never moves, and never bites. Its row in MONSTER_DEFS has left out its traits, as in 5, 5, 1, 3, 0, 4. The values fill the fields in order, so its venom, 4, has landed in its traits. There, 4 is the bit for Trait::STATIONARY. Put the 0 back before the 4.

A cave spider is a pink square in the tile view. The old tiles.txt has no picture called cave-spider. Copy tiles.png and tiles.txt from the Chapter 14 folder.

C2838: 'Frighten': illegal qualified name in member declaration, and C2065: 'Frighten': undeclared identifier, in Item.h, followed by many more. The value Frighten is missing from enum class ItemEffect.

C3861: 'frightenMonsters': identifier not found, followed by C2039: 'frightenMonsters': is not a member of 'Game', and more about monsters_ and log_. The declaration of frightenMonsters is missing from Game.h.

The game stops as soon as you read a scroll of fear with a healthy monster in sight, and Visual Studio reports an unhandled exception, 0xC00000FD, a stack overflow. The test in actFleeing is missing && monster.afraid == 0. A frightened monster that's healthy then goes from actFleeing to actHunting, which sends it back to actFleeing. The two call each other until the stack runs out.

FAQ

Q) Why doesn't a second potion of poison add to the first?

A) Adding would let bad luck pile up: three potions of poison in a row would be eighteen turns of it. Taking the larger count means the worst one dose can do is the worst any dose can do, which is easier to judge. Rogue's confusion added up, by lengthening its fuse.

Q) Why do statuses tick after the monsters, rather than when the player acts?

A) Any fixed order would work, as long as it's the same every turn. This one ticks after everything that can give a status, so a bite's poison starts working in the turn it's given, as a potion's does. And it puts every change to health before the one check for a death, at the very end.

Q) Why does a monster's poison never show?

A) Nothing in the game poisons a monster. The code treats every entity alike, so a monster that was poisoned would tick and die like the player, and the look panel would say so. The first experiment in this chapter shows it.

AI Exercise (Optional)

A new status is a good test of how the code is built. If you'd like to add one with an AI's help, open your AI chatbot of choice, and try a prompt like this:

"I'm writing a roguelike in C++ with SDL 3. A struct, Entity, has statuses as counts of turns left, such as int poisoned and int confused, where 0 means the status isn't on. The function void Game::tickStatuses(Entity& entity) counts each down once a turn, and poison takes one health a turn. Each potion's effect sets a count with SDL_max(player_.poisoned, POISON_TURNS), in a switch on an enum class ItemEffect. I'd like a new status, slowed, from a new potion of slowness: while the player is slowed, every step they take gives the monsters two turns instead of one. Show me every change, with each curly brace on its own line, and explain it."

Notice what the preceding prompt does. It shows the pattern the answer must follow: a count on Entity, a case in the potion's switch, and a countdown in tickStatuses. So the new status fits beside the others rather than in a system of its own.

When the answer comes back, check where the extra turn goes. It should be the monsters who act twice, through the same function that makes them act once. And the statuses should still tick once a turn. Then test it: start the player slowed, in the constructor, as the second experiment did with poison, and count a monster's blows.

Summary

The player and the monsters now have statuses, counts of turns that tick after the monsters act: poison, regeneration, confusion and fear. Harm ticks before healing, and a death ends the tick, so a death by poison is final. And a cave spider's bite keeps hurting after the fight. In Chapter 15, the dungeon goes deeper: stairs lead down to new floors, each with tougher monsters than the last.