- All of a Dexterity modifier counts toward AC in light armor, at most +2 in medium armor, none in heavy armor, going by the equipped item's kind - The gear card says when the armor is holding something back - Library and seed armor notes and docs brought in line Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.9 KiB
D&D 5e Rules Reference — Implementation Spec
This is the canonical math the backend must implement. Build it as a single shared
calculation module (e.g. backend/src/game/calculations.ts) that is the only
place these formulas live — every API route and the character builder call into it
rather than re-deriving numbers. This mirrors the logic already built and verified
in the "DnD Character Sheet Calculator.xlsx" workbook delivered earlier in this
project; treat that workbook as the reference implementation if anything below is
ambiguous.
All rules are 5e (2014 core rules).
Ability Modifier
modifier = floor((score - 10) / 2)
Use Math.floor, not integer truncation — they differ for negative results
(e.g. score 7 → (7-10)/2 = -1.5 → floor = -2, not -1).
Proficiency Bonus
proficiencyBonus = 2 + floor((level - 1) / 4)
Level 1–4 → +2, 5–8 → +3, 9–12 → +4, 13–16 → +5, 17–20 → +6.
Final Ability Score
finalScore = rolledScore + speciesBonus(ability, species)
speciesBonus comes from the Species Ability Score Increase table below. See
seed-data/species.json.
Saving Throws
saveBonus = abilityModifier + (proficient ? proficiencyBonus : 0)
Proficiency per ability comes from the Class Quick Facts table (seed-data/classes.json).
Skills
skillBonus = governingAbilityModifier + (proficient ? proficiencyBonus : 0)
Governing ability per skill is fixed — see seed-data/skills.json. Proficiency is
a per-character, per-skill boolean the player/DM sets during character building
(CharacterSkillProficiency in the Prisma schema).
Passive Perception:
passivePerception = 10 + perceptionSkillBonus
Armor Class
armorClass = 10 + dexModifier + armorBonus
dexModifier here is as much of it as the equipped armor lets through
(armorDexterity): all of it with none or light armor, at most +2 in medium
armor, none in heavy armor. Armor is an item marked with its kind
(Item.armorType); its own AC is a gear effect.
Without armor, a class feature can replace the 10 + dexModifier part:
Unarmored Defense (barbarian: 10 + DEX + CON; monk, also without a shield:
10 + DEX + WIS) and Draconic Resilience (13 + DEX). The best one is used,
and only when it beats 10 + dexModifier. These, and the other class features
that change a sheet number (Fast Movement, Unarmored Movement, Aura of
Protection, Diamond Soul, Slippery Mind, Jack of All Trades), live in
backend/src/game/classFeatures.js; CHARACTERS.md section 8 lists them.
armorBonus is a flat number the player enters representing whatever armor/shield
they have equipped (this matches the workbook's "Armor Bonus (from armor worn)"
input cell). Do not try to model armor types as a lookup table unless you want to
extend scope later — a plain integer field on Character is enough for v1.
Initiative
initiative = dexModifier
Speed
Comes from the species table (30 ft standard; 25 ft for Gnome, Dwarf, Halfling).
Store as speed on the species seed data; characters may override it
(speedOverride on Character) for future feature support (e.g. barbarian speed
boosts) without a schema change.
Hit Points
Hit Die size comes from class (seed-data/classes.json).
hpMax(level) =
if level === 1:
hitDieMax + conModifier
else:
hitDieMax + conModifier
+ (level - 1) * (floor(hitDieSize / 2) + 1 + conModifier)
Important: level 1 always uses the maximum possible roll of the Hit Die, not
the average. Every level after 1 uses the fixed "average" value
floor(hitDieSize / 2) + 1 (e.g. d10 → 6, d6 → 4, d12 → 7) plus the CON modifier
again. This was a bug caught and fixed while building the Excel calculator —
do not regress it.
hitDice available to spend on a short rest = character level (one die of the
class's Hit Die size per level).
Spellcasting (only for classes with a Spellcasting Ability ≠ "None")
spellSaveDC = 8 + proficiencyBonus + spellcastingAbilityModifier
spellAttackBonus = proficiencyBonus + spellcastingAbilityModifier
Spellcasting ability per class is in seed-data/classes.json (Intelligence,
Wisdom, or Charisma). Classes with "None" have no spellcasting stats — the UI
should hide those fields entirely rather than show them as zero/blank.
Species Ability Score Increases & Speed
See seed-data/species.json for the full table (43 species). Note: Half-Elf gets
+2 Charisma fixed, plus +1 to two other abilities of the player's choice.
The 2024 species (Aasimar, Goliath) and those of Monsters of the Multiverse
have no fixed bonuses: the player chooses +2 and +1, or +1 to three
abilities. These choices are a species' flexible patterns; the builder must
ask for them (don't default them in the UI), and they are stored on the
character as speciesBonusChoices.
Class Quick Facts
See seed-data/classes.json for the full table (13 classes, the Artificer included): Hit Die, Primary
Ability, the two Saving Throw proficiencies, and Spellcasting Ability.
Skill → Governing Ability
See seed-data/skills.json (18 skills).
"Quick Stats" View (combat-context panel)
When the frontend shows the contextual quick-stats panel during an attack action, surface exactly these derived values (all computed via the module above, never stored redundantly):
- Attack bonus for the equipped/selected weapon:
proficiencyBonus (if proficient) + relevantAbilityModifier(STR normally, DEX for finesse/ranged weapons — seeseed-data/weapons.json'sattackAbilityfield, which is"STR","DEX", or"FINESSE"meaning "use whichever of STR/DEX is higher") - Damage: weapon's damage dice + relevant ability modifier
- Current HP / Max HP / Temp HP
- AC
- Any special abilities/features flagged on the character (free-text list is fine for v1 — a
specialAbilitiestext/JSON field onCharacteror a simple related table)
This is a read view assembled from existing data — it does not need its own schema table.