Skip to content

Creature stats

Joachim de Groot edited this page Sep 24, 2026 · 1 revision

The eight figures on a creature's line, what each one is actually read for, and what it is worth.

Checked against the tree on 2026-09-24. Every effect below was found by following the stat to the code that reads it; where a stat is read nowhere, this says so rather than inventing a purpose for it.


The eight

XStats::Id in magic/stats.h:

enum Id {UNKNOWN = -1, STR, DEX, TOU, LEN, WIL, MAN, PER, CHR, COUNT};
Keyword Name Enum Read for
St Strength XStats::STR damage (through tactics), carrying, unwieldy weapons
Dx Dexterity XStats::DEX to-hit and dodge (through tactics), climbing out of pits
To Toughness XStats::TOU maximum hit points, PV, hit points gained per level
Le Learning XStats::LEN reading speed, skill picks per level (hero only)
Wi Willpower XStats::WIL spell power, and so spell range
Ma Mana XStats::MAN maximum power points, power points per level
Pe Perception XStats::PER how far the creature can see
Ch Charisma XStats::CHR nothing - see below

The keyword is what content writes; helpers/keyword_dice.cpp maps the two-letter form to the enum. Both "St 1d3+10 ..." and "St:1d3+10 ..." parse - ParseKeywordDice() takes either.


What each one does

Strength - St

  • Damage, but only by way of the creature's tactics (XCreature::GetTacticsDMGBonus). The divisor is the tactic: berserker STR/2, aggressive STR/5, normal STR/7, defensive STR/10, coward STR/20. Note that GetDMG() is added_DMG + GetTacticsDMGBonus()
    • strength is not added to a weapon's damage directly, it arrives entirely through this bonus.
  • Heavy weapons: GetUnwieldyHITPenalty() compares 30 * strength against the weapon's weight, halved again when the other hand is also full. Over that weight the to-hit penalty grows with the logarithm of how far over; under it there is no bonus.
  • Carrying: GetSpeed() slows the creature at strength * 120, * 200 and * 280 of carried weight (to 110%, 130% and 200% of its turn length).

Dexterity - Dx

  • To-hit, through tactics (GetTacticsHITBonus): berserker DEX/2, aggressive DEX/3, normal DEX/4, defensive DEX/7, coward DEX/10.
  • Dodging, through tactics (GetTacticsDVBonus): coward 3*(DEX+tactics)/2, defensive DEX+tactics, normal 2*DEX/3 + tactics. Both bonuses also add the TACTICS skill level.
  • Climbing out of a pit: 30 + climbing*5 + DEX*2 per cent (map/map_objects.cpp).

Toughness - To

The most valuable stat in the game, because it is read three times:

  • Maximum hit points: GetMaxHP() = MAX_HP + MAX_HP * TOU / 20. At Toughness 20 a creature has twice the hit points its dice rolled.
  • Protection: GetPV() = pv + added_PV + TOU / 10.
  • Growth: each level adds vRand(TOU/5 + 1) + 1 to MAX_HP.
  • For the hero only, it also sets the starting pool: MAX_HP = TOU/2 + 3 (creature/xhero.cpp).

Learning - Le

Two effects, both modest, and one of them is the hero's alone:

  • Reading: a book's remaining pages fall by skill level + LEN per turn (item/xbook.cpp).
  • Skill picks on levelling: XHero::IncLevel() grants LEN/5 + 1 skill improvements, with a floor of 3. A monster's IncLevel() does not do this - it only gains hit points and power points - so Learning does nothing at all for a monster except let it read faster, which no monster does.

It does not affect experience gained. Nothing reads it for that.

Willpower - Wi

  • Spell power: XMagic::GetSpellPower() = WIL + spell effectivity + the caster's level in that school, and spell power decides a spell's range through XEffect::GetRange().

That is its only reader. It is not consulted for resistance to fear, confusion or charm - those are resistances, not stats.

Mana - Ma

  • Maximum power points: GetMaxPP() = MAX_PP + MAX_PP * MAN / 10. Note the divisor is 10, not Toughness's 20, so Mana is twice as potent per point for power as Toughness is for health.
  • Growth: each level adds vRand(MAN/2 + 1) + 1 to MAX_PP.
  • For the hero only: MAX_PP = MAN/2 + 1 at the start.

Perception - Pe

  • Sight radius, and nothing else - XCreature::GetVisibleRadius():

    Perception Radius
    under 5 3
    5-9 4
    10-19 5
    20-49 6
    50-89 7
    90 and over more

    This matters more than it looks: the radius is what AnalyzeView() is given, so it sets how far a creature notices enemies, friends, items and stairs.

Charisma - Ch

Read nowhere but the status line. The only GetStats(XStats::CHR) in the tree is the one printing it in XCreature::PutStatus(). It does not affect prices (the shop never asks), reactions, morale or anything else.

Every creature in the world still rolls one, and some roll it high - Ahk-Ulan has Ch 7d5, the shopkeeper Ch 5d5. It is currently flavour. Worth either wiring up or admitting to.


How the numbers are stored

Stats are held multiplied by 100 and divided back on the way out:

int  Get(Id s)     const { return stats[s] / 100; }   // stats.h
void SetStat(Id s, int val) { stats[s] = val * 100; } // stats.h

So St 2d4 rolls 2-8, is stored as 200-800, and reads back as 2-8. The hundredths exist so that an effect can move a stat by a fraction of a point; nothing in content can, since Stats() only takes whole dice.

Two routes in:

  • XStats::Set(const char*) parses a keyword string and adds: stats[keyword_index] += dice.Throw() * 100. It rolls the dice there and then, at world-load time - which is why adding or removing a Stats() line shifts every seed downstream.
  • XStatsGenerator::Generate() rolls the generator's stored dice into a fresh XStats at creature creation, through SetStat().

XStatsGenerator::Init() merges: it only overwrites the stats named in the string and leaves the rest as they were. That matters for a creature inheriting from another - name a stat and you replace it, leave it out and you keep the parent's.


Worked examples from the world

-- world/creatures/rats.lua - a rat
:Stats("St 1d2 Dx 2d4 To 1d2 Le 1d1 Wi 1d1 Ma 1d1 Pe 2d4 Ch 1d1")

Toughness 1-2, so it keeps roughly the hit points its dice rolled; Perception 2-8 gives it a sight radius of 3 or 4.

-- world/creatures/humans.lua - a dwarven guardian
:Stats("St 1d3+20 Dx 1d3+7 To 1d3+22 Le 2d3 Wi 1d3 Ma 1d2 Pe 1d6 Ch 1d3")

Toughness 23-25 more than doubles its hit points (×(1 + 23/20)), and adds +2 PV. Strength 21-23 on normal tactics is STR/7 = +3 damage.

-- world/uniques/ahkulan.lua - Ahk-Ulan
:Stats("St 1d8+15 Dx 1d8+40 To 1d8+10 Le 1d5+45 Wi 1d5+45 Ma 1d5+45 Pe 5d6 Ch 7d5")

Willpower 46-50 is what makes his spells reach and bite; Mana 46-50 with the /10 divisor gives him roughly six times his rolled power points. Learning 46-50 buys him nothing - he is not the hero and he reads no books.


Where it lives

What Where
The enum, Get, SetStat magic/stats.h, class XStats
Parsing, generation magic/stats.cpp
Keyword to enum helpers/keyword_dice.cpp
The Lua table XStats.STR … XStats::RegisterLua
Derived combat values XCreature::GetHIT/GetDV/GetPV/GetDMG and the GetTactics*Bonus family, creature/creature.cpp
Hit points and power points GetMaxHP(), GetMaxPP(), IncLevel(), creature/creature.cpp
From Lua SetStats, AddStats, SetMaxStats, ClampStats, GetStats in lua/api_actor.cpp

Clone this wiki locally