Part 14 lets the player decide who they are before the descent. Three heroes — a Fighter, a Rogue, and a Mage — each begin with different health, melee strength, magic, and starting gear, which makes every run play differently from the first turn. And it all snaps in because every stat a class wants to set already exists: base power, max HP, the magic bonus we planted in Part 13, and the equipment slots from Part 9.
A New State: the Title Screen
The game now starts on a menu, not in the dungeon, so we add a fourth GameState and an enum of classes:
enum class GameState { ClassSelect, Playing, Inventory, Dead };
enum class PlayerClass { Fighter, Rogue, Mage };
The Game constructor no longer starts a run — it just sets m_state = ClassSelect and waits. render checks that state first (before it ever touches a level, since none exists yet) and draws the menu; pressing 1, 2 or 3 calls newGame with the chosen class. This is the same state-machine pattern that has absorbed the death screen and the inventory — adding a title screen is now routine.
Classes Are Just Different Starting Values
A "class" sounds like it needs a hierarchy of subclasses. It doesn't. Because the player's stats are already derived (Part 9) and the magic bonus is already a number (Part 13), a class is nothing more than a set of opening values and a starting kit, chosen in a switch:
switch (cls)
{
case PlayerClass::Fighter:
m_baseMaxHp = 36; m_basePower = 6;
m_weapon = makeWeapon("sword", 4); m_hasWeapon = true;
m_armour = makeArmour("chain mail", 3); m_hasArmour = true;
m_inventory.push_back(makePotion(PotionType::Healing));
break;
case PlayerClass::Rogue:
m_baseMaxHp = 28; m_basePower = 5;
m_weapon = makeWeapon("dagger", 2); m_hasWeapon = true;
m_armour = makeArmour("leather", 1); m_hasArmour = true;
m_inventory.push_back(makePotion(PotionType::Healing));
m_inventory.push_back(makeScroll(ScrollType::Lightning));
break;
case PlayerClass::Mage:
m_baseMaxHp = 22; m_basePower = 3; m_magicBonus = 6;
m_inventory.push_back(makeScroll(ScrollType::Lightning));
m_inventory.push_back(makeScroll(ScrollType::Fireball));
m_inventory.push_back(makePotion(PotionType::Healing));
break;
}
m_player.maxHp = m_baseMaxHp;
m_player.hp = m_baseMaxHp;
recomputeStats(); // power = m_basePower + bonuses + weapon; defense = armour
The Fighter is a wall of HP and steel; the Rogue is leaner with a ranged option; the Mage is glass, but his m_magicBonus = 6 flows straight into the scroll-damage formula from Part 13, so his fireballs land for more than double a warrior's. Three genuinely different play styles, expressed entirely in numbers and a few starting items — no inheritance, no special-case combat code. (The little makeWeapon/makeScroll/makePotion helpers just stamp out a fully-formed Item so the starting kit is built the same way loot is.)
The Menu, and Death
drawClassSelect is a few centred lines describing the heroes; the input loop maps 1/2/3 to newGame. And death now loops back to the menu rather than restarting the same hero — so a fresh run means a fresh choice:
if (m_state == GameState::Dead)
{
if (sc == SDL_SCANCODE_SPACE) { m_state = GameState::ClassSelect; dirty = true; }
if (sc == SDL_SCANCODE_ESCAPE) running = false;
break;
}
Try It
Build and run — you're greeted by the hero menu now, not a dungeon. Take the Fighter and wade in; the early floors barely scratch you. Then die, pick the Mage, and play completely differently: hang back, open with a fireball into the pack, and pray nothing reaches you while you're at 22 HP. Same dungeon, same systems, three distinct experiences — the hallmark of a class system done with data instead of code.
Notes on the Code
Changed: Game.h / Game.cpp only (the ClassSelect state, PlayerClass, the class switch in newGame, the item-maker helpers, and drawClassSelect). Full Game.cpp in the repo. Everything else is unchanged from Part 13.
Common Errors
Crash on launch. Something accessed cur() before a class was chosen, when m_levels is still empty. render must check the ClassSelect state and return before touching a level.
Every class feels the same. recomputeStats isn't using m_basePower, or the class switch runs after stats are computed. Set the bases first, then recomputeStats.
The Mage's spells are no stronger. m_magicBonus isn't being added in readScroll (it was wired up in Part 13) — or it's reset after the class sets it.
Starting gear isn't equipped. The class set m_weapon but not m_hasWeapon = true, or recomputeStats wasn't called after assigning the kit.
What's Next
Part 15 — Save & Load lets a run survive closing the program. We serialise the whole game — the player, inventory, equipment, identification, every floor's map and inhabitants — to a plain-text file, and read it back. It's the most mechanical part of the series and a good lesson in turning a live object graph into bytes and back.