Files
the-sunken-reach/backend/src/game/data/levelFeatures.js
T
joshuaandClaude Opus 5.5 57fba68a48 V1.21.0: a DM's toolkit: the battle at a glance, subclasses, character state, rolls, XP
Seven steps in one commit, 1.15.0 to 1.21.0. They change the same files
and were not saved apart, so they go in together.

- The battle at a glance (1.15). A strip across the top of an encounter
  lists everyone in turn order and stays in view; a click goes to that
  card. On a wide screen the stat block of whoever is on turn stays
  beside the cards, and Pin keeps anyone else's there. Saving throws,
  every skill and passive Perception, Investigation and Insight are on
  encounter cards and in the character panel, which "Sheet" on a card
  and a button on the party overview open. For the DM, N is Next Turn
  and S opens the sheet of whoever is on turn. The party overview shows
  passive scores and spell save DC.
- Subclasses (1.16). Three per class, picked in the level-up checklist
  at level 2 (cleric, druid, wizard) or 3, shown beside the class
  everywhere, and changed by the DM from the sheet: the old one's lines
  come out of Special Abilities and the new one's go in. Characters
  already at that level keep the path they were on.
- Character state (1.17). Inspiration, exhaustion and conditions on the
  sheet and in the panel, and counters for what a class does a set
  number of times (Rage, Ki, Channel Divinity...), which the player
  spends and a rest gives back. A long rest takes a level of exhaustion
  off. A character's conditions are their own now: they stay from one
  encounter to the next.
- Faster battles (1.18). Up to 20 of a monster added at once, numbered.
  One button rolls initiative for the monsters and NPCs. A condition can
  be given a number of rounds and then comes off by itself. Hovering
  over a condition shows its rule, and damaging someone who is
  Concentrating shows the save to call for.
- Rolls from the sheet (1.19). A number with a dotted underline rolls
  when clicked, with its bonus: skills, saves, ability checks, attacks,
  damage, spell dice, and a monster's attack, damage and abilities. The
  dice roller has advantage and disadvantage. "Call for a roll" rolls
  one check or save for the whole party. Rolls stay in the browser.
- XP and difficulty (1.20). An encounter shows the DM how hard it is,
  Trivial to Deadly, from the monsters' challenge ratings against the
  characters in it, and what it is worth in XP; one button shares that
  out, once. The sheet and the party overview show XP and when a level
  is due. Levelling up stays the DM's decision.
- Subclasses are optional per campaign (1.21). With "Characters choose a
  subclass" unticked in the campaign's Settings nobody is asked, each
  class follows its first subclass, and none is named.
- Conditions, exhaustion, counters and subclass features are reminders
  for the table: none of them changes a number on the sheet.
- The docs cover all of it, with generated tables of what each subclass
  adds.
- New migrations: a character's subclass; inspiration, exhaustion,
  conditions and spent counters on a character; the rounds left on a
  combatant's conditions; the XP awarded for an encounter; the
  campaign's subclass setting.
- All packages at version 1.21.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:36:27 +02:00

252 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// What each class gains at each level after the first: the "Levelling-Up
// Checklist" tables of the Hollowcrown book (Rules Cheat Sheet), levels 2 to
// 10. These are the features every member of the class gets; what a subclass
// adds is in subclasses.js. Level 1 features are in characterOptions.js and
// are written by the builder.
//
// Each entry becomes a line of the character's special abilities, "Name —
// text". An `upgrade` rewrites the line that already has that name (Sneak
// Attack growing a die); anything else is added as a new line. `{ boon: true }`
// is a Boon: the DM picks one from BOONS below.
//
// None of this feeds the calculation engine: like the level 1 features, these
// are table rules in plain words.
const BOON = { boon: true };
const rage = (uses, damage) => ({
name: 'Rage',
text: `${uses} per day, for one minute: +${damage} melee damage, advantage on Strength checks and saves, and physical damage against you is halved.`,
upgrade: true,
});
const sneakAttack = (dice) => ({
name: 'Sneak Attack',
text: `Once per turn, deal an extra ${dice} damage to a target you have advantage against, or that has one of your allies next to it.`,
upgrade: true,
});
const bardicInspiration = (die) => ({
name: 'Bardic Inspiration',
text: `A few times per day, as a bonus action, give an ally a ${die} to add to one attack, check or save.`,
upgrade: true,
});
const wildShape = (limit) => ({
name: 'Wild Shape',
text: `Twice per short rest, become a beast you have seen of ${limit}, for up to half your level in hours. Use the beast's HP; when it hits 0 you return to your own form.`,
upgrade: true,
});
const EXTRA_ATTACK = { name: 'Extra Attack', text: 'Attack twice when you take the Attack action.' };
const EVASION = { name: 'Evasion', text: 'On a Dexterity save for half damage, take none on a success and half on a failure.' };
const LANDS_STRIDE = { name: 'Land’s Stride', text: 'Ordinary difficult terrain and plants do not slow you.' };
const INVOCATIONS =
'Agonizing Blast (add your Charisma modifier to each Eldritch Blast hit), Devil’s Sight (see normally in darkness, even magical darkness, to 120 ft), Repelling Blast (Eldritch Blast hits push the target 10 ft away), Mask of Many Faces (cast Disguise Self at will), Beguiling Influence (proficiency in Deception and Persuasion, added by hand), Eyes of the Rune Keeper (read any writing)';
const invocations = (total) => ({
name: 'Eldritch Invocations',
text: `Choose one more with your DM (${total} in all). From level 5 the list adds Thirsting Blade (Pact of the Blade: attack twice with your pact weapon) and One with Shadows (become invisible while standing still in dim light or darkness).`,
});
const LEVEL_FEATURES = {
BARBARIAN: {
2: [
{ name: 'Reckless Attack', text: 'On your first attack each turn you may attack with advantage; attacks against you have advantage until your next turn.' },
{ name: 'Danger Sense', text: 'Advantage on Dexterity saves against effects you can see (traps, breath, spells).' },
],
3: [rage('Three times', 2)],
4: [BOON],
5: [EXTRA_ATTACK, { name: 'Fast Movement', text: '+10 ft speed when not in heavy armor (ask your DM to raise your speed).' }],
6: [rage('Four times', 2)],
7: [{ name: 'Feral Instinct', text: 'Advantage on initiative; you can act normally in a surprise round if you rage first.' }],
8: [BOON],
9: [{ name: 'Brutal Critical', text: 'Roll one extra weapon damage die on a critical hit.' }, rage('Four times', 3)],
},
BARD: {
2: [
{ name: 'Jack of All Trades', text: 'Add half your proficiency bonus (round down) to any check you are not proficient in.' },
{ name: 'Song of Rest', text: 'During a short rest, allies who spend Hit Dice heal an extra 1d6.' },
],
3: [
{ name: 'Expertise', text: 'Double proficiency on two proficient skills (add by hand).' },
],
4: [BOON],
5: [bardicInspiration('d8'), { name: 'Font of Inspiration', text: 'Regain Bardic Inspiration on a short rest.' }],
6: [
{ name: 'Countercharm', text: 'Action: allies within 30 ft get advantage on saves against fear and charm until your next turn.' },
],
8: [BOON],
9: [{ name: 'Song of Rest', text: 'During a short rest, allies who spend Hit Dice heal an extra 1d8.', upgrade: true }],
10: [
bardicInspiration('d10'),
{ name: 'Expertise', text: 'Double proficiency on four proficient skills (add by hand).', upgrade: true },
{ name: 'Magical Secrets', text: 'Learn two more spells from any class (the DM grants them within the unlock schedule).' },
],
},
CLERIC: {
2: [
{ name: 'Channel Divinity', text: 'Once per short rest, use Turn Undead or your domain’s Channel Divinity.' },
{ name: 'Turn Undead', text: 'Channel Divinity. Action: undead within 30 ft make a Wisdom save or flee from you for 1 minute.' },
],
4: [BOON],
5: [{ name: 'Destroy Undead', text: 'Undead of CR 1/2 or lower that fail against your Turn Undead are destroyed.' }],
6: [
{ name: 'Channel Divinity', text: 'Twice per short rest, use Turn Undead or your domain’s Channel Divinity.', upgrade: true },
],
8: [
BOON,
{ name: 'Destroy Undead', text: 'Undead of CR 1 or lower that fail against your Turn Undead are destroyed.', upgrade: true },
],
10: [{ name: 'Divine Intervention', text: 'Once per week, call on your god: roll d100; if you roll your level or lower, the god helps (the DM decides how).' }],
},
DRUID: {
2: [
wildShape('CR 1/4 or less (no flying or swimming)'),
],
4: [BOON, wildShape('CR 1/2 or less (it may swim, not fly)')],
8: [BOON, wildShape('CR 1 or less (it may swim or fly)')],
},
FIGHTER: {
2: [{ name: 'Action Surge', text: 'Once per short rest, take one extra action on your turn.' }],
4: [BOON],
5: [EXTRA_ATTACK],
6: [BOON],
8: [BOON],
9: [{ name: 'Indomitable', text: 'Once per long rest, reroll a failed saving throw.' }],
},
MONK: {
2: [
{ name: 'Ki', text: 'Points equal to your level; they come back on a short rest. Ki save DC is 8 + proficiency bonus + Wisdom modifier.' },
{ name: 'Flurry of Blows', text: '1 ki: two unarmed strikes as a bonus action.' },
{ name: 'Patient Defense', text: '1 ki: Dodge as a bonus action.' },
{ name: 'Step of the Wind', text: '1 ki: Dash or Disengage as a bonus action, jump distance doubled.' },
{ name: 'Unarmored Movement', text: '+10 ft speed without armor (ask your DM to raise your speed).' },
],
3: [
{ name: 'Deflect Missiles', text: 'Reaction: reduce ranged weapon damage by 1d10 + Dexterity modifier + your level.' },
],
4: [BOON, { name: 'Slow Fall', text: 'Reaction: reduce falling damage by 5 × your level.' }],
5: [
EXTRA_ATTACK,
{ name: 'Stunning Strike', text: '1 ki after a melee hit: the target makes a Constitution save or is stunned until your next turn.' },
{ name: 'Martial Arts', text: 'Your unarmed strikes deal 1d6 damage and can use Dexterity. After attacking, make one unarmed strike as a bonus action.', upgrade: true },
],
6: [
{ name: 'Ki-Empowered Strikes', text: 'Your unarmed strikes count as magical.' },
{ name: 'Unarmored Movement', text: '+15 ft speed without armor (ask your DM to raise your speed).', upgrade: true },
],
7: [EVASION, { name: 'Stillness of Mind', text: 'Action: end charm or fear on yourself.' }],
8: [BOON],
9: [{ name: 'Unarmored Movement', text: '+15 ft speed without armor (ask your DM to raise your speed). You can run along walls and across water during your move.', upgrade: true }],
10: [
{ name: 'Purity of Body', text: 'Immune to poison and disease.' },
{ name: 'Unarmored Movement', text: '+20 ft speed without armor (ask your DM to raise your speed). You can run along walls and across water during your move.', upgrade: true },
],
},
PALADIN: {
2: [
{ name: 'Divine Smite', text: 'After a melee hit, spend a spell slot to deal +2d8 radiant (+1d8 per slot level above 1st; +1d8 more against undead and fiends).' },
{ name: 'Fighting Style', text: 'Choose one with your DM: +1 AC in armor (the DM raises Extra AC by 1), +2 damage with a one-handed weapon, or protect an adjacent ally (impose disadvantage on an attack as a reaction).' },
{ name: 'Spellcasting', text: 'Paladin spells unlock: you now have spell slots.' },
],
3: [
{ name: 'Divine Health', text: 'Immune to disease.' },
],
4: [BOON],
5: [EXTRA_ATTACK],
6: [{ name: 'Aura of Protection', text: 'You and allies within 10 ft add your Charisma modifier to all saving throws while you are conscious.' }],
8: [BOON],
10: [{ name: 'Aura of Courage', text: 'You and allies within 10 ft can’t be frightened.' }],
},
RANGER: {
2: [
{ name: 'Fighting Style', text: 'Choose one with your DM: +2 to ranged attacks, +1 AC in armor (the DM raises Extra AC by 1), or add your ability modifier again to a paired weapon’s damage (added by hand).' },
{ name: 'Spellcasting', text: 'Ranger spells unlock: you now have spell slots.' },
],
3: [
{ name: 'Primeval Awareness', text: 'Spend a spell slot: sense aberrations, dragons, fey, fiends, undead and elementals within 1 mile for 1 minute per slot level.' },
],
4: [BOON],
5: [EXTRA_ATTACK],
6: [
{ name: 'Favored Enemy', text: 'Choose a second favored enemy with your DM.' },
{ name: 'Natural Explorer', text: 'Choose a second favored terrain with your DM.' },
],
8: [BOON, LANDS_STRIDE],
10: [
{ name: 'Hide in Plain Sight', text: 'Spend 1 minute camouflaging: +10 to Stealth while you stay still.' },
{ name: 'Natural Explorer', text: 'Choose a third favored terrain with your DM.' },
],
},
ROGUE: {
2: [{ name: 'Cunning Action', text: 'Bonus action: Dash, Disengage or Hide.' }],
3: [sneakAttack('2d6')],
4: [BOON],
5: [sneakAttack('3d6'), { name: 'Uncanny Dodge', text: 'Reaction: halve the damage of an attack that hits you.' }],
6: [{ name: 'Expertise', text: 'Double proficiency on two more proficient skills (add by hand).' }],
7: [sneakAttack('4d6'), EVASION],
8: [BOON],
9: [sneakAttack('5d6')],
10: [BOON],
},
SORCERER: {
2: [{ name: 'Font of Magic', text: 'Sorcery points equal to your level; they come back on a long rest. Bonus action: turn points into a spell slot (2 points = 1st, 3 = 2nd, 5 = 3rd, 6 = 4th, 7 = 5th), or a spell slot into points equal to its level.' }],
3: [{ name: 'Metamagic', text: 'Choose two with your DM: Quickened Spell (2 points: cast an action spell as a bonus action), Twinned Spell (points = spell level: hit a second target with a single-target spell), Careful Spell (1 point: allies auto-succeed on your spell’s save), Subtle Spell (1 point: cast without words or gestures), Extended Spell (1 point: double duration), Heightened Spell (3 points: one target has disadvantage on its first save).' }],
4: [BOON],
8: [BOON],
10: [{ name: 'Metamagic', text: 'Choose a third option with your DM.' }],
},
WARLOCK: {
2: [{ name: 'Eldritch Invocations', text: `Choose two with your DM: ${INVOCATIONS}.` }],
3: [{ name: 'Pact Boon', text: 'Choose one with your DM: Pact of the Blade (summon a weapon of your choice as an action; it counts as magical), Pact of the Chain (gain a small familiar spirit: an imp, a sprite or a cat-shaped shadow), or Pact of the Tome (a book of three extra cantrips from any class, which the DM grants).' }],
4: [BOON],
5: [invocations('three')],
7: [invocations('four')],
8: [BOON],
9: [invocations('five')],
},
WIZARD: {
4: [BOON],
8: [BOON],
},
};
// The class tables above stop here; a higher level still raises the numbers.
const MAX_FEATURE_LEVEL = 10;
// From level 5 a damaging cantrip rolls twice the dice the site shows. Written
// onto the sheet of every class that casts, so nobody forgets.
const CANTRIP_LEVEL = 5;
const CANTRIP_FEATURE = {
name: 'Cantrips',
text: 'Double dice. Roll twice the damage dice the site shows for a damaging cantrip (Eldritch Blast becomes two beams, each with its own attack roll).',
};
// Boons stand in for ability score increases, which the site does not have
// (every number on the sheet comes from the scores rolled at the build). Each
// is worth what +2 to a best ability would be. `max` is how often one can be
// taken; `per` is what each taking adds, and `text` the line for that many.
const BOONS = [
{
key: 'WEAPON_MASTERY',
name: 'Weapon Mastery',
max: 2,
per: 1,
text: (amount) => `+${amount} to attack and damage rolls with weapons (add it to the numbers the site shows).`,
},
{
key: 'ARCANE_FOCUS',
name: 'Arcane Focus',
max: 2,
per: 1,
text: (amount) => `+${amount} to spell save DC and spell attack bonus (add it to the numbers the site shows).`,
},
// AC is the most powerful number in the game, so this one is taken once.
{ key: 'TOUGHENED', name: 'Toughened', max: 1, per: 1, text: (amount) => `+${amount} AC (counted in your Extra AC).` },
// The site works max HP out itself, so the extra lives in Temp HP.
{ key: 'HARDY', name: 'Hardy', max: 2, per: 5, text: (amount) => `+${amount} maximum HP: gain ${amount} Temp HP after each long rest.` },
];
module.exports = { LEVEL_FEATURES, MAX_FEATURE_LEVEL, CANTRIP_LEVEL, CANTRIP_FEATURE, BOONS };