Every system so far has been instantaneous: you hit, damage lands; you drink, you heal. Part 10 adds the dimension of time. A status effect is a condition that lasts for a number of turns and does something each one — poison that drains you, regeneration that mends you, and a venomous snake whose bite leaves you ailing long after it's dead. It's a small, general system, and almost every interesting mechanic a roguelike adds later ("burning", "slowed", "blessed", "confused") is just another entry in it.
Statuses Live on the Entity
Because both the player and monsters can be afflicted, the timers belong on the shared Entity. We use a fixed array indexed by a Status enum — one slot per effect, holding turns remaining:
enum class Status { Poisoned, Regen, Count };
struct Entity
{
// ...existing fields...
int statusTurns[(int)Status::Count] = {};
bool hasStatus(Status s) const { return statusTurns[(int)s] > 0; }
void addStatus(Status s, int turns)
{
if (turns > statusTurns[(int)s]) statusTurns[(int)s] = turns; // refresh to longer
}
};
The Count sentinel at the end of the enum is a tidy trick: it's automatically the number of real statuses, so the array sizes itself and adding "Burning" later means adding one enum value and nothing else. addStatus refreshes to the longer duration rather than stacking, so being bitten twice tops your poison back up instead of doubling it.
Ticking — One Function, Every Entity
Each turn, every effect on an entity advances by one. That's tickStatuses, and it runs on the player and on every monster alike:
void Game::tickStatuses(Entity& e)
{
if (e.statusTurns[(int)Status::Poisoned] > 0)
{
e.hp -= 1;
e.statusTurns[(int)Status::Poisoned]--;
SDL_Log("%s is wracked by poison. (hp %d)", e.name, e.hp > 0 ? e.hp : 0);
if (e.hp <= 0) { e.hp = 0; e.alive = false; SDL_Log("%s succumbs to poison.", e.name); }
}
if (e.statusTurns[(int)Status::Regen] > 0)
{
if (e.hp < e.maxHp) e.hp += 1;
e.statusTurns[(int)Status::Regen]--;
}
}
Notice poison can kill — which means death no longer happens only in the attack routine. That makes the turn order the thing to get right.
The Turn Order, Made Explicit
Two parts of the code used to end the player's turn (a move, and using an item), and now both also need to tick poison and re-check death. So we name that sequence once, in endPlayerTurn, and call it from both:
void Game::endPlayerTurn()
{
monstersAct(); // monsters take their turns (and tick their own statuses)
tickStatuses(m_player); // then poison/regen tick on the player
updateTitle();
if (m_player.hp <= 0) m_state = GameState::Dead;
}
Monsters tick their own statuses at the top of their turn inside monstersAct (skipping any that the tick just killed). Pulling the end-of-turn logic into one method is the kind of small tidy that the Part 6 Game class makes natural — and it guarantees a move and a potion both resolve the turn identically.
Inflicting Statuses
Two sources apply effects this part. First, potions: the poison potion no longer hits for instant damage — it inflicts Poisoned — and a new regeneration potion grants Regen:
case PotionType::Poison:
m_player.addStatus(Status::Poisoned, POISON_TURNS); // 6 turns
SDL_Log("Your veins burn — poison!");
break;
case PotionType::Regen:
m_player.addStatus(Status::Regen, REGEN_TURNS); // 12 turns
SDL_Log("Your wounds begin to knit closed.");
break;
Second, monsters. A new venomous snake S carries venomous = true, and when one lands a bite we afflict the player right after the hit:
attack(m, m_player);
if (m.venomous && m_player.isAlive())
{
m_player.addStatus(Status::Poisoned, VENOM_TURNS);
SDL_Log("%s's bite is venomous!", m.name);
}
The active effects show in the title bar — POISON(4), REGEN(9) — counting down each turn, so you can see exactly how long you've got to find a cure or a safe corner.
Try It
Build and run, then go get poisoned — drink an unidentified potion that turns out to be poison, or let a green S snake bite you. Watch POISON(n) appear in the title and your HP tick down each step; a healing potion patches the damage but doesn't cure the poison, while waiting it out costs you blood you might not have. Then find a regeneration potion and feel the opposite: HP creeping back up turn by turn. Poison stacking off a snake while you're already low is exactly the tension this system exists to create.
POISON(5) ticking in the title bar — damage that outlives the monster.Notes on the Code
Changed files: Entity.h (the status array), Item.h (the new Regen potion type), Monster.h (the venomous flag), and Game.h / Game.cpp. Full Game.cpp is in the repo. Map, GlyphCache, FOV, Player and DijkstraMap are unchanged.
Common Errors
Poison ticks forever. The duration isn't being decremented — every tick must do statusTurns[...]--, and only act while it's above zero.
Dying to poison doesn't end the game. Death is only checked in attack. Since poison kills in tickStatuses, the player.hp <= 0 check must run after the tick — that's why it lives in endPlayerTurn.
A dead, poisoned monster keeps acting. After tickStatuses(m) at the top of its turn, re-check isAlive() and continue if the poison just finished it.
Statuses carry over into a new game. newGame must reset them. The simplest fix is to reconstruct the player (m_player = Player{};), which zeroes the status array along with everything else.
What's Next
Part 11 — Multiple Floors & Depth Scaling takes the game vertical. We add stairs, descend into deeper and more dangerous levels, and — the refactor we planned back at the Interlude — extract a Level class so the map, monsters and items belong to a floor rather than to the game globally. The Game will hold a stack of levels, and going down will mean building a tougher one.