From c13fb2bcb3173394ba618f0a64755ca6b5a2af1f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 17:50:30 +0000 Subject: [PATCH 1/2] feat: Refactor to support SRD 5.1 and SRD 5.2 rulesets This refactors the D&D character management to support both SRD 5.1 and SRD 5.2 rulesets. - Adds a `ruleset` field to the `Character` schema and a corresponding database migration. - Splits the D&D data into `srd51.ts` and `srd52.ts` files. - Refactors `src/lib/dnd.ts` to be a core rules file with a `getRuleset` function to dynamically load the correct ruleset. - Updates character computation and services to be ruleset-aware, using the dynamically loaded ruleset data. --- ...251014093140_add_ruleset_to_characters.sql | 1 + src/db/characters.ts | 6 +- src/lib/dnd.ts | 884 +----------------- src/lib/dnd/srd51.ts | 860 +++++++++++++++++ src/lib/dnd/srd52.ts | 716 ++++++++++++++ src/services/computeCharacter.ts | 25 +- src/services/computeSpells.ts | 17 +- 7 files changed, 1623 insertions(+), 886 deletions(-) create mode 100644 migrations/20251014093140_add_ruleset_to_characters.sql create mode 100644 src/lib/dnd/srd51.ts create mode 100644 src/lib/dnd/srd52.ts diff --git a/migrations/20251014093140_add_ruleset_to_characters.sql b/migrations/20251014093140_add_ruleset_to_characters.sql new file mode 100644 index 0000000..357d6b3 --- /dev/null +++ b/migrations/20251014093140_add_ruleset_to_characters.sql @@ -0,0 +1 @@ +ALTER TABLE characters ADD COLUMN ruleset TEXT NOT NULL DEFAULT 'srd51'; diff --git a/src/db/characters.ts b/src/db/characters.ts index cf23613..4ad6999 100644 --- a/src/db/characters.ts +++ b/src/db/characters.ts @@ -12,6 +12,7 @@ export const CharacterSchema = z.object({ subrace: z.enum(SubraceNames).nullable().default(null), background: BackgroundNamesSchema, alignment: z.nullish(z.string()), + ruleset: z.enum(["srd51", "srd52"]).default("srd51"), created_at: z.date(), updated_at: z.date(), }); @@ -29,7 +30,7 @@ export async function create(db: SQL, character: CreateCharacter): Promise c.name); -export const RaceNamesSchema = z.enum(RaceNames); -export type RaceNameType = z.infer; - -export const SubraceNames = Races.flatMap(r => r.subraces ? r.subraces.map(sr => sr.name) : []); -export const SubraceNamesSchema = z.enum(SubraceNames); -export type SubraceNameType = z.infer; - -// ===================== -// Types & Interfaces -// ===================== - -export interface Choice { - /** Choose `choose` items from `from` */ - choose: number; - from: T[]; -} - -// Helper “choice pools” -const GamingSets = [ - "dice set", - "playing card set", -] as const; - -const ArtisanTools = [ - "alchemist’s supplies", - "brewer’s supplies", - "calligrapher’s supplies", - "carpenter’s tools", - "cartographer’s tools", - "cobbler’s tools", - "cook’s utensils", - "glassblower’s tools", - "jeweler’s tools", - "leatherworker’s tools", - "mason’s tools", - "painter’s supplies", - "potter’s tools", - "smith’s tools", - "tinker’s tools", - "weaver’s tools", - "woodcarver’s tools", -] as const; - -const Instruments = [ - "bagpipes", - "drum", - "dulcimer", - "flute", - "lute", - "lyre", - "horn", - "pan flute", - "shawm", - "viol", -] as const; - -// ===================== -// Data (Player’s handbook) -// ===================== export const ClassNames = ["barbarian", "bard", "cleric", "druid", "fighter", "monk", "paladin", "ranger", "rogue", "sorcerer", "warlock", "wizard"] as const; export const ClassNamesSchema = z.enum(ClassNames); @@ -238,6 +83,12 @@ export type SpellcastingInfo = { notes?: string} & ({ enabled: false } | { subclasses?: string[]; // Subclasses that grant/modify spellcasting }) +export interface Choice { + /** Choose `choose` items from `from` */ + choose: number; + from: T[]; +} + export interface ClassDef { name: ClassNameType; hitDie: HitDieType; @@ -256,182 +107,11 @@ export interface ClassDef { notes?: string; } -// ===================== -// Data (Player’s Handbook classes, lowercase) -// ===================== -export const Classes: Record = { - barbarian: { - name: "barbarian", - hitDie: 12, - primaryAbilities: ["strength", "constitution"], - savingThrows: ["strength", "constitution"], - armorProficiencies: ["light", "medium", "shields"], - weaponProficiencies: ["simple", "martial"], - toolProficiencies: [], - skillChoices: { - choose: 2, - from: ["animal handling", "athletics", "intimidation", "nature", "perception", "survival"], - }, - subclasses: ["path of the berserker", "path of the totem warrior"], - subclassLevel: 3, - spellcasting: { enabled: false }, - }, - bard: { - name: "bard", - hitDie: 8, - primaryAbilities: ["charisma", "dexterity"], - savingThrows: ["dexterity", "charisma"], - armorProficiencies: ["light"], - weaponProficiencies: ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], - toolProficiencies: [ - { choose: 3, from: ["bagpipes","drum","dulcimer","flute","lute","lyre","horn","pan flute","shawm","viol"] } - ], - skillChoices: { - choose: 3, - from: [ - "acrobatics","animal handling","arcana","athletics","deception","history","insight","intimidation", - "investigation","medicine","nature","perception","performance","persuasion","religion","sleight of hand","stealth","survival" - ] - }, - subclasses: ["college of lore", "college of valor"], - subclassLevel: 3, - spellcasting: { enabled: true, kind: "full", ability: "charisma", changePrepared: "levelup" }, - }, - cleric: { - name: "cleric", - hitDie: 8, - primaryAbilities: ["wisdom"], - savingThrows: ["wisdom", "charisma"], - armorProficiencies: ["light", "medium", "heavy", "shields"], - weaponProficiencies: ["simple"], - toolProficiencies: [], - skillChoices: { choose: 2, from: ["history", "insight", "medicine", "persuasion", "religion"] }, - subclasses: ["knowledge", "life", "light", "nature", "tempest", "trickery", "war"], - subclassLevel: 1, - spellcasting: { enabled: true, kind: "full", ability: "wisdom", changePrepared: "longrest" }, - }, - druid: { - name: "druid", - hitDie: 8, - primaryAbilities: ["wisdom"], - savingThrows: ["intelligence", "wisdom"], - armorProficiencies: ["light (nonmetal)", "medium (nonmetal)", "shields (nonmetal)"], - weaponProficiencies: ["clubs","daggers","darts","javelins","maces","quarterstaffs","scimitars","sickles","slings","spears"], - toolProficiencies: ["herbalism kit"], - skillChoices: { choose: 2, from: ["arcana","animal handling","insight","medicine","nature","perception","religion","survival"] }, - subclasses: ["circle of the land", "circle of the moon"], - subclassLevel: 2, - spellcasting: { enabled: true, kind: "full", ability: "wisdom", changePrepared: "longrest" }, - }, - fighter: { - name: "fighter", - hitDie: 10, - primaryAbilities: ["strength", "dexterity", "constitution"], - savingThrows: ["strength", "constitution"], - armorProficiencies: ["light", "medium", "heavy", "shields"], - weaponProficiencies: ["simple", "martial"], - toolProficiencies: [], - skillChoices: { choose: 2, from: ["acrobatics","animal handling","athletics","history","insight","intimidation","perception","survival"] }, - subclasses: ["champion", "battle master", "eldritch knight"], - subclassLevel: 3, - spellcasting: { enabled: true, kind: "third", subclasses: ["eldritch knight"], ability: "intelligence", changePrepared: "levelup"}, - }, - monk: { - name: "monk", - hitDie: 8, - primaryAbilities: ["dexterity", "wisdom"], - savingThrows: ["strength", "dexterity"], - armorProficiencies: [], - weaponProficiencies: ["simple", "shortsword"], - toolProficiencies: [{ choose: 1, from: ["artisan's tools", "musical instrument"] }], - skillChoices: { choose: 2, from: ["acrobatics","athletics","history","insight","religion","stealth"] }, - subclasses: ["way of the open hand", "way of shadow", "way of the four elements"], - subclassLevel: 3, - spellcasting: { enabled: false }, - }, - paladin: { - name: "paladin", - hitDie: 10, - primaryAbilities: ["strength", "charisma"], - savingThrows: ["wisdom", "charisma"], - armorProficiencies: ["light", "medium", "heavy", "shields"], - weaponProficiencies: ["simple", "martial"], - toolProficiencies: [], - skillChoices: { choose: 2, from: ["athletics","insight","intimidation","medicine","persuasion","religion"] }, - subclasses: ["oath of devotion", "oath of the ancients", "oath of vengeance"], - subclassLevel: 3, - spellcasting: { enabled: true, kind: "half", ability: "charisma", changePrepared: "longrest", notes: "half-caster progression" }, - }, - ranger: { - name: "ranger", - hitDie: 10, - primaryAbilities: ["dexterity", "wisdom"], - savingThrows: ["strength", "dexterity"], - armorProficiencies: ["light", "medium", "shields"], - weaponProficiencies: ["simple", "martial"], - toolProficiencies: [], - skillChoices: { choose: 3, from: ["animal handling","athletics","insight","investigation","nature","perception","stealth","survival"] }, - subclasses: ["hunter", "beast master"], - subclassLevel: 3, - spellcasting: { enabled: true, kind: "half", ability: "wisdom", changePrepared: "levelup", notes: "half-caster progression" }, - }, - rogue: { - name: "rogue", - hitDie: 8, - primaryAbilities: ["dexterity"], - savingThrows: ["dexterity", "intelligence"], - armorProficiencies: ["light"], - weaponProficiencies: ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], - toolProficiencies: ["thieves' tools"], - skillChoices: { choose: 4, from: ["acrobatics","athletics","deception","insight","intimidation","investigation","perception","performance","persuasion","sleight of hand","stealth"] }, - subclasses: ["thief", "assassin", "arcane trickster"], - subclassLevel: 3, - spellcasting: { enabled: true, kind: "third", subclasses: ["arcane trickster"], ability: "intelligence", changePrepared: "levelup"}, - }, - sorcerer: { - name: "sorcerer", - hitDie: 6, - primaryAbilities: ["charisma"], - savingThrows: ["constitution", "charisma"], - armorProficiencies: [], - weaponProficiencies: ["dagger","dart","sling","quarterstaff","light crossbow"], - toolProficiencies: [], - skillChoices: { choose: 2, from: ["arcana","deception","insight","intimidation","persuasion","religion"] }, - subclasses: ["draconic bloodline", "wild magic"], - subclassLevel: 1, - spellcasting: { enabled: true, kind: "full", ability: "charisma", changePrepared: "levelup" }, - }, - warlock: { - name: "warlock", - hitDie: 8, - primaryAbilities: ["charisma"], - savingThrows: ["wisdom", "charisma"], - armorProficiencies: ["light"], - weaponProficiencies: ["simple"], - toolProficiencies: [], - skillChoices: { choose: 2, from: ["arcana","deception","history","intimidation","investigation","nature","religion"] }, - subclasses: ["the archfey", "the fiend", "the great old one"], - subclassLevel: 1, - spellcasting: { enabled: true, kind: "pact", ability: "charisma", changePrepared: "levelup", notes: "pact magic progression" }, - }, - wizard: { - name: "wizard", - hitDie: 6, - primaryAbilities: ["intelligence"], - savingThrows: ["intelligence", "wisdom"], - armorProficiencies: [], - weaponProficiencies: ["dagger","dart","sling","quarterstaff","light crossbow"], - toolProficiencies: [], - skillChoices: { choose: 2, from: ["arcana","history","insight","investigation","medicine","religion"] }, - subclasses: ["school of abjuration","school of conjuration","school of divination","school of enchantment","school of evocation","school of illusion","school of necromancy","school of transmutation"], - subclassLevel: 2, - spellcasting: { enabled: true, kind: "full", ability: "intelligence", changePrepared: "longrest" }, - } -}; - -export const SubclassNames = Object.values(Classes).flatMap(c => c.subclasses ? c.subclasses : []); -export const SubclassNamesSchema = z.enum(SubclassNames); -export type SubclassNameType = z.infer; +export const BackgroundNames = [ + "acolyte", "charlatan", "criminal", "entertainer", "folk hero", "guild artisan", "hermit", "noble", "outlander", "sage", "sailor", "soldier", "urchin", "pirate", +] as const; +export const BackgroundNamesSchema = z.enum(BackgroundNames); +export type BackgroundNameType = z.infer; export interface BackgroundFeature { name: string; @@ -440,549 +120,23 @@ export interface BackgroundFeature { export interface Background { name: BackgroundNameType; - - /** Usually two fixed skills; sometimes represent “choose” for variants/future content */ skillProficiencies: (SkillType | Choice)[]; - - /** Tool proficiencies can be fixed or a choice; include vehicles under tools for simplicity */ toolProficiencies?: (string | Choice)[]; - - /** Languages can be a number (any choices), a fixed array, or a structured Choice */ languageProficiencies?: number | string[] | Choice; - - /** Starting equipment as brief strings */ equipment?: string[]; - feature: BackgroundFeature; + feat?: string; + ability_scores?: AbilityType[]; } -export const BackgroundNames = [ - "acolyte", "charlatan", "criminal", "entertainer", "folk hero", "guild artisan", "hermit", "noble", "outlander", "sage", "sailor", "soldier", "urchin", "pirate", -] as const; -export const BackgroundNamesSchema = z.enum(BackgroundNames); -export type BackgroundNameType = z.infer; - -export const Backgrounds: Record = { - acolyte: { - name: "acolyte", - skillProficiencies: ["insight", "religion"], - languageProficiencies: 2, - equipment: ["holy symbol", "prayer book or prayer wheel", "5 sticks of incense", "vestments", "common clothes", "15 gp"], - feature: { - name: "shelter of the faithful", - summary: "free support and lodging at a temple of your faith; connections to clergy.", - }, - }, - charlatan: { - name: "charlatan", - skillProficiencies: ["deception", "sleight of hand"], - toolProficiencies: ["disguise kit", "forgery kit"], - equipment: ["fine clothes", "disguise kit", "con tools (e.g., signet of a fake identity)", "15 gp"], - feature: { - name: "false identity", - summary: "you maintain a second identity with documentation, acquaintances, and disguises.", - }, - }, - criminal: { - name: "criminal", - skillProficiencies: ["deception", "stealth"], - toolProficiencies: [ - { choose: 1, from: [...GamingSets] as unknown as string[] }, - "thieves’ tools", - ], - equipment: ["crowbar", "dark common clothes with hood", "15 gp"], - feature: { - name: "criminal contact", - summary: "a reliable and trustworthy contact within the criminal underworld.", - }, - }, - entertainer: { - name: "entertainer", - skillProficiencies: ["acrobatics", "performance"], - toolProficiencies: [ - { choose: 1, from: [...Instruments] as unknown as string[] }, - "disguise kit", - ], - equipment: ["musical instrument (one of your choice)", "favor of an admirer", "costume", "15 gp"], - feature: { - name: "by popular demand", - summary: "you can find a place to perform and secure free lodging and modest food.", - }, - }, - "folk hero": { - name: "folk hero", - skillProficiencies: ["animal handling", "survival"], - toolProficiencies: [ - { choose: 1, from: [...ArtisanTools] as unknown as string[] }, - "vehicles (land)", - ], - equipment: ["artisan’s tools (one of your choice)", "shovel", "iron pot", "common clothes", "10 gp"], - feature: { - name: "rustic hospitality", - summary: "common folk will shelter you; you can hide among them.", - }, - }, - "guild artisan": { - name: "guild artisan", - skillProficiencies: ["insight", "persuasion"], - toolProficiencies: [{ choose: 1, from: [...ArtisanTools] as unknown as string[] }], - languageProficiencies: 1, - equipment: ["artisan’s tools (one of your choice)", "letter of introduction from your guild", "traveler’s clothes", "15 gp"], - feature: { - name: "guild membership", - summary: "access to guild facilities, contacts, and legal support (with dues).", - }, - }, - hermit: { - name: "hermit", - skillProficiencies: ["medicine", "religion"], - toolProficiencies: ["herbalism kit"], - languageProficiencies: 1, - equipment: ["scroll case of notes", "winter blanket", "common clothes", "herbalism kit", "5 gp"], - feature: { - name: "discovery", - summary: "you uncovered a unique and powerful insight during seclusion.", - }, - }, - noble: { - name: "noble", - skillProficiencies: ["history", "persuasion"], - toolProficiencies: [{ choose: 1, from: [...GamingSets] as unknown as string[] }], - languageProficiencies: 1, - equipment: ["fine clothes", "signet ring", "scroll of pedigree", "25 gp"], - feature: { - name: "position of privilege", - summary: "high social standing; easier audience with nobles and officials.", - }, - }, - outlander: { - name: "outlander", - skillProficiencies: ["athletics", "survival"], - toolProficiencies: [{ choose: 1, from: [...Instruments] as unknown as string[] }], - languageProficiencies: 1, - equipment: ["staff", "hunting trap", "trophy from an animal", "traveler’s clothes", "10 gp"], - feature: { - name: "wanderer", - summary: "excellent memory for maps and geography; find food and fresh water for your group.", - }, - }, - sage: { - name: "sage", - skillProficiencies: ["arcana", "history"], - languageProficiencies: 2, - equipment: ["bottle of black ink", "quill", "small knife", "letter from a dead colleague with a question you haven’t answered", "common clothes", "10 gp"], - feature: { - name: "researcher", - summary: "you can usually find where to obtain lore; you know how to get answers.", - }, - }, - sailor: { - name: "sailor", - skillProficiencies: ["athletics", "perception"], - toolProficiencies: ["navigator’s tools", "vehicles (water)"], - equipment: ["belaying pin (club)", "50 feet of silk rope", "lucky charm", "common clothes", "10 gp"], - feature: { - name: "ship’s passage", - summary: "secure free passage on a sailing ship for you and companions (with obligations).", - }, - }, - pirate: { - name: "pirate", - skillProficiencies: ["athletics", "perception"], - toolProficiencies: ["navigator’s tools", "vehicles (water)"], - equipment: ["belaying pin (club)", "50 feet of silk rope", "lucky charm", "common clothes", "10 gp"], - feature: { - name: "bad reputation", - summary: "your notoriety lets you get away with minor crimes; people fear you.", - }, - }, - soldier: { - name: "soldier", - skillProficiencies: ["athletics", "intimidation"], - toolProficiencies: [{ choose: 1, from: [...GamingSets] as unknown as string[] }, "vehicles (land)"], - equipment: ["insignia of rank", "trophy from a fallen enemy", "bone dice or deck of cards", "common clothes", "10 gp"], - feature: { - name: "military rank", - summary: "you have a rank; soldiers loyal to your former organization recognize authority.", - }, - }, - urchin: { - name: "urchin", - skillProficiencies: ["sleight of hand", "stealth"], - toolProficiencies: ["disguise kit", "thieves’ tools"], - equipment: ["small knife", "map of city you grew up in", "pet mouse", "token to remember parents", "common clothes", "10 gp"], - feature: { - name: "city secrets", - summary: "you and companions can move through a city twice as fast via alleys and passages.", - }, - }, -} as const; - -// ===================== -// Spell Progression Tables from SRD -// ===================== - -type SpellProgressionTableRow = { - level: number; - cantrips: number; - prepared: number; - slots: number[]; // 1st to 9th level slots - arcanum?: Record; // warlock-only -} - -const SpellProgressionTables: Partial> = { - "bard": [ - {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 1, cantrips: 2, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 2, cantrips: 2, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 3, cantrips: 2, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, - {level: 4, cantrips: 3, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, - {level: 5, cantrips: 3, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, - {level: 6, cantrips: 3, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, - {level: 7, cantrips: 3, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, - {level: 8, cantrips: 3, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, - {level: 9, cantrips: 3, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, - {level: 10, cantrips: 4, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, - {level: 11, cantrips: 4, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, - {level: 12, cantrips: 4, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, - {level: 13, cantrips: 4, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, - {level: 14, cantrips: 4, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, - {level: 15, cantrips: 4, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, - {level: 16, cantrips: 4, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, - {level: 17, cantrips: 4, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, - {level: 18, cantrips: 4, prepared: 20, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, - {level: 19, cantrips: 4, prepared: 21, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, - {level: 20, cantrips: 4, prepared: 22, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, - ], - 'cleric': [ - {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 1, cantrips: 3, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 2, cantrips: 3, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 3, cantrips: 3, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, - {level: 4, cantrips: 4, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, - {level: 5, cantrips: 4, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, - {level: 6, cantrips: 4, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, - {level: 7, cantrips: 4, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, - {level: 8, cantrips: 4, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, - {level: 9, cantrips: 4, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, - {level: 10, cantrips: 5, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, - {level: 11, cantrips: 5, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, - {level: 12, cantrips: 5, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, - {level: 13, cantrips: 5, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, - {level: 14, cantrips: 5, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, - {level: 15, cantrips: 5, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, - {level: 16, cantrips: 5, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, - {level: 17, cantrips: 5, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, - {level: 18, cantrips: 5, prepared: 20, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, - {level: 19, cantrips: 5, prepared: 21, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, - {level: 20, cantrips: 5, prepared: 22, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, - ], - "druid": [ - {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 1, cantrips: 2, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 2, cantrips: 2, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 3, cantrips: 2, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, - {level: 4, cantrips: 3, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, - {level: 5, cantrips: 3, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, - {level: 6, cantrips: 3, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, - {level: 7, cantrips: 3, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, - {level: 8, cantrips: 3, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, - {level: 9, cantrips: 3, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, - {level: 10, cantrips: 4, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, - {level: 11, cantrips: 4, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, - {level: 12, cantrips: 4, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, - {level: 13, cantrips: 4, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, - {level: 14, cantrips: 4, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, - {level: 15, cantrips: 4, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, - {level: 16, cantrips: 4, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, - {level: 17, cantrips: 4, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, - {level: 18, cantrips: 4, prepared: 20, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, - {level: 19, cantrips: 4, prepared: 21, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, - {level: 20, cantrips: 4, prepared: 22, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, - ], - "paladin": [ - {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 1, cantrips: 0, prepared: 2, slots: [2, 0, 0, 0, 0]}, - {level: 2, cantrips: 0, prepared: 3, slots: [2, 0, 0, 0, 0]}, - {level: 3, cantrips: 0, prepared: 4, slots: [3, 0, 0, 0, 0]}, - {level: 4, cantrips: 0, prepared: 5, slots: [3, 0, 0, 0, 0]}, - {level: 5, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, - {level: 6, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, - {level: 7, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, - {level: 8, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, - {level: 9, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, - {level: 10, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, - {level: 11, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, - {level: 12, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, - {level: 13, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, - {level: 14, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, - {level: 15, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, - {level: 16, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, - {level: 17, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, - {level: 18, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, - {level: 19, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, - {level: 20, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, - ], - "ranger": [ - {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 1, cantrips: 0, prepared: 2, slots: [2, 0, 0, 0, 0]}, - {level: 2, cantrips: 0, prepared: 3, slots: [2, 0, 0, 0, 0]}, - {level: 3, cantrips: 0, prepared: 4, slots: [3, 0, 0, 0, 0]}, - {level: 4, cantrips: 0, prepared: 5, slots: [3, 0, 0, 0, 0]}, - {level: 5, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, - {level: 6, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, - {level: 7, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, - {level: 8, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, - {level: 9, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, - {level: 10, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, - {level: 11, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, - {level: 12, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, - {level: 13, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, - {level: 14, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, - {level: 15, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, - {level: 16, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, - {level: 17, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, - {level: 18, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, - {level: 19, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, - {level: 20, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, - ], - "sorcerer": [ - {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 1, cantrips: 4, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 2, cantrips: 4, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 3, cantrips: 4, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, - {level: 4, cantrips: 5, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, - {level: 5, cantrips: 5, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, - {level: 6, cantrips: 5, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, - {level: 7, cantrips: 5, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, - {level: 8, cantrips: 5, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, - {level: 9, cantrips: 5, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, - {level: 10, cantrips: 6, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, - {level: 11, cantrips: 6, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, - {level: 12, cantrips: 6, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, - {level: 13, cantrips: 6, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, - {level: 14, cantrips: 6, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, - {level: 15, cantrips: 6, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, - {level: 16, cantrips: 6, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, - {level: 17, cantrips: 6, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, - {level: 18, cantrips: 6, prepared: 20, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, - {level: 19, cantrips: 6, prepared: 21, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, - {level: 20, cantrips: 6, prepared: 22, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, - ], - "warlock": [ - {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 1, cantrips: 2, prepared: 2, slots: [1, 0, 0, 0, 0], arcanum: {}}, - {level: 2, cantrips: 2, prepared: 3, slots: [2, 0, 0, 0, 0], arcanum: {}}, - {level: 3, cantrips: 2, prepared: 4, slots: [0, 2, 0, 0, 0], arcanum: {}}, - {level: 4, cantrips: 3, prepared: 5, slots: [0, 2, 0, 0, 0], arcanum: {}}, - {level: 5, cantrips: 3, prepared: 6, slots: [0, 0, 2, 0, 0], arcanum: {}}, - {level: 6, cantrips: 3, prepared: 7, slots: [0, 0, 2, 0, 0], arcanum: {}}, - {level: 7, cantrips: 3, prepared: 8, slots: [0, 0, 0, 2, 0], arcanum: {}}, - {level: 8, cantrips: 3, prepared: 9, slots: [0, 0, 0, 2, 0], arcanum: {}}, - {level: 9, cantrips: 3, prepared: 10, slots: [0, 0, 0, 0, 2], arcanum: {}}, - {level: 10, cantrips: 4, prepared: 10, slots: [0, 0, 0, 0, 2], arcanum: {}}, - {level: 11, cantrips: 4, prepared: 11, slots: [0, 0, 0, 0, 3], arcanum: {6: 1}}, - {level: 12, cantrips: 4, prepared: 11, slots: [0, 0, 0, 0, 3], arcanum: {6: 1}}, - {level: 13, cantrips: 4, prepared: 12, slots: [0, 0, 0, 0, 3], arcanum: {6: 1, 7: 1}}, - {level: 14, cantrips: 4, prepared: 12, slots: [0, 0, 0, 0, 3], arcanum: {6: 1, 7: 1}}, - {level: 15, cantrips: 4, prepared: 13, slots: [0, 0, 0, 0, 3], arcanum: {6: 1, 7: 1, 8: 1}}, - {level: 16, cantrips: 4, prepared: 13, slots: [0, 0, 0, 0, 3], arcanum: {6: 1, 7: 1, 8: 1}}, - {level: 17, cantrips: 4, prepared: 14, slots: [0, 0, 0, 0, 4], arcanum: {6: 1, 7: 1, 8: 1, 9: 1}}, - {level: 18, cantrips: 4, prepared: 14, slots: [0, 0, 0, 0, 4], arcanum: {6: 1, 7: 1, 8: 1, 9: 1}}, - {level: 19, cantrips: 4, prepared: 15, slots: [0, 0, 0, 0, 4], arcanum: {6: 1, 7: 1, 8: 1, 9: 1}}, - {level: 20, cantrips: 4, prepared: 15, slots: [0, 0, 0, 0, 4], arcanum: {6: 1, 7: 1, 8: 1, 9: 1}}, - ], - "wizard": [ - {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 1, cantrips: 3, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 2, cantrips: 3, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 3, cantrips: 3, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, - {level: 4, cantrips: 4, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, - {level: 5, cantrips: 4, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, - {level: 6, cantrips: 4, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, - {level: 7, cantrips: 4, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, - {level: 8, cantrips: 4, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, - {level: 9, cantrips: 4, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, - {level: 10, cantrips: 5, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, - {level: 11, cantrips: 5, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, - {level: 12, cantrips: 5, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, - {level: 13, cantrips: 5, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, - {level: 14, cantrips: 5, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, - {level: 15, cantrips: 5, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, - {level: 16, cantrips: 5, prepared: 21, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, - {level: 17, cantrips: 5, prepared: 22, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, - {level: 18, cantrips: 5, prepared: 23, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, - {level: 19, cantrips: 5, prepared: 24, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, - {level: 20, cantrips: 5, prepared: 26, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, - ], -} - -/** Eldritch Knight / Arcane Trickster cantrips known by level (third-caster, starts at level 3) */ -type SpellProgression = number[]; -export const THIRD_CASTER_CANTRIPS_KNOWN: SpellProgression = [ - 0, 0, 0, // 0-2 (unused / not yet a caster) - 2, 2, 2, 2, 2, 2, 2, 3, // 3-10 - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // 11-20 -]; - -/** Eldritch Knight / Arcane Trickster spells known by level (third-caster, starts at level 3) */ -export const THIRD_CASTER_SPELLS_PREPARED: SpellProgression = [ - 0, 0, 0, // 0-2 (unused / not yet a caster) - 3, 4, 4, 5, 6, 6, 7, 8, // 3-10 - 8, 9, 10, 10, 11, 11, 12, 13, 13, 13, // 11-20 -]; - -/** Helper function to get cantrips known for a class at a given level */ -export function maxCantripsKnown(className: ClassNameType, level: number): number { - const classDef = Classes[className]; - if (!classDef.spellcasting.enabled) return 0; - - switch (className) { - case "bard": - case "sorcerer": - case "warlock": - case "cleric": - case "druid": - case "wizard": - return SpellProgressionTables[className]![level]?.cantrips || 0; - case "fighter": // Eldritch Knight - case "rogue": // Arcane Trickster - return THIRD_CASTER_CANTRIPS_KNOWN[level] || 0; - default: return 0; - } -} - -/** Helper function to get spells known for "known caster" classes at a given level */ -export function maxSpellsPrepared(className: ClassNameType, level: number): number | null { - const classDef = Classes[className]; - if (!classDef.spellcasting.enabled) { - return null; // Not a "known" caster - } - - switch (className) { - case "bard": - case "sorcerer": - case "warlock": - case "cleric": - case "druid": - case "wizard": - case "paladin": - case "ranger": - return SpellProgressionTables[className]![level]?.prepared || 0; - case "fighter": // Eldritch Knight - case "rogue": // Arcane Trickster - return THIRD_CASTER_CANTRIPS_KNOWN[level] || 0; - default: return 0; - } -} - -// ===================== -// Spell Slots -// ===================== - - // slots[0] = level 1 slots, slots[1] = level 2 slots, etc. export type SlotProgression = {level: number, slots: number[]}[]; -/** ---------- Full Caster Slots (Bard, Cleric, Druid, Sorcerer, Wizard) ---------- */ -export const FULL_CASTER_SLOTS: SlotProgression = [ - {level: 0, slots: []}, - {level: 1, slots: [ 2 ]}, - {level: 2, slots: [ 3 ]}, - {level: 3, slots: [ 4, 2 ]}, - {level: 4, slots: [ 4, 3 ]}, - {level: 5, slots: [ 4, 3, 2 ]}, - {level: 6, slots: [ 4, 3, 3 ]}, - {level: 7, slots: [ 4, 3, 3, 1 ]}, - {level: 8, slots: [ 4, 3, 3, 2 ]}, - {level: 9, slots: [ 4, 3, 3, 3, 1 ]}, - {level: 10, slots: [ 4, 3, 3, 3, 2 ]}, - {level: 11, slots: [ 4, 3, 3, 3, 2, 1 ]}, - {level: 12, slots: [ 4, 3, 3, 3, 2, 1 ]}, - {level: 13, slots: [ 4, 3, 3, 3, 2, 1, 1 ]}, - {level: 14, slots: [ 4, 3, 3, 3, 2, 1, 1 ]}, - {level: 15, slots: [ 4, 3, 3, 3, 2, 1, 1, 1 ]}, - {level: 16, slots: [ 4, 3, 3, 3, 2, 1, 1, 1 ]}, - {level: 17, slots: [ 4, 3, 3, 3, 2, 1, 1, 1, 1 ]}, - {level: 18, slots: [ 4, 3, 3, 3, 3, 1, 1, 1, 1 ]}, - {level: 19, slots: [ 4, 3, 3, 3, 3, 2, 1, 1, 1 ]}, - {level: 20, slots: [ 4, 3, 3, 3, 3, 2, 2, 1, 1 ]}, -]; - -/** ---------- Half Caster Slots (Paladin, Ranger) ---------- */ -export const HALF_CASTER_SLOTS: SlotProgression = [ - {level: 0, slots: []}, - {level: 1, slots: [ 2 ]}, - {level: 2, slots: [ 2 ]}, - {level: 3, slots: [ 3 ]}, - {level: 4, slots: [ 3 ]}, - {level: 5, slots: [ 4, 2 ]}, - {level: 6, slots: [ 4, 2 ]}, - {level: 7, slots: [ 4, 3 ]}, - {level: 8, slots: [ 4, 3 ]}, - {level: 9, slots: [ 4, 3, 2 ]}, - {level: 10, slots: [ 4, 3, 2 ]}, - {level: 11, slots: [ 4, 3, 3 ]}, - {level: 12, slots: [ 4, 3, 3 ]}, - {level: 13, slots: [ 4, 3, 3, 1 ]}, - {level: 14, slots: [ 4, 3, 3, 1 ]}, - {level: 15, slots: [ 4, 3, 3, 2 ]}, - {level: 16, slots: [ 4, 3, 3, 2 ]}, - {level: 17, slots: [ 4, 3, 3, 3, 1 ]}, - {level: 18, slots: [ 4, 3, 3, 3, 1 ]}, - {level: 19, slots: [ 4, 3, 3, 3, 2 ]}, - {level: 20, slots: [ 4, 3, 3, 3, 2 ]}, -]; - -/** ---------- Third Caster Slots (Eldritch Knight / Arcane Trickster) ---------- */ -export const THIRD_CASTER_SLOTS: SlotProgression = [ - {level: 0, slots: []}, - {level: 1, slots: []}, - {level: 2, slots: []}, - {level: 3, slots: [ 2 ]}, - {level: 4, slots: [ 3 ]}, - {level: 5, slots: [ 3 ]}, - {level: 6, slots: [ 3, 2 ]}, - {level: 7, slots: [ 4, 2 ]}, - {level: 8, slots: [ 4, 2 ]}, - {level: 9, slots: [ 4, 2 ]}, - {level: 10, slots: [ 4, 3 ]}, - {level: 11, slots: [ 4, 3 ]}, - {level: 12, slots: [ 4, 3 ]}, - {level: 13, slots: [ 4, 3, 2 ]}, - {level: 14, slots: [ 4, 3, 2 ]}, - {level: 15, slots: [ 4, 3, 2 ]}, - {level: 16, slots: [ 4, 3, 3 ]}, - {level: 17, slots: [ 4, 3, 3 ]}, - {level: 18, slots: [ 4, 3, 3 ]}, - {level: 19, slots: [ 4, 3, 3, 1 ]}, - {level: 20, slots: [ 4, 3, 3, 1 ]}, -]; - -function slotsFromProgression(progression: number[]): SpellSlotsType { - const slots: number[] = []; - - for (let level = 1; level <= 9; level++) { - const slotsAtLevel = progression[level - 1] || 0; - for (let i = 0; i < slotsAtLevel; i++) { - slots.push(level); - } - } - - return slots as SpellSlotsType; -} +import srd51 from "./dnd/srd51"; +import srd52 from "./dnd/srd52"; -/** Helper function to get spell slots for a class at a given level */ -export function getSlotsFor(casterKind: CasterKindType, level: number): SpellSlotsType { - if (casterKind === "full") { - return slotsFromProgression(FULL_CASTER_SLOTS[level]?.slots || []); - } else if (casterKind === "half") { - return slotsFromProgression(HALF_CASTER_SLOTS[level]?.slots || []); - } else if (casterKind === "third") { - return slotsFromProgression(THIRD_CASTER_SLOTS[level]?.slots || []); - } else if (casterKind === "pact") { - const progression = SpellProgressionTables["warlock"]!; - return slotsFromProgression(progression[level]?.slots || []); - } else { - return [] +export function getRuleset(ruleset: 'srd51' | 'srd52') { + if (ruleset === 'srd52') { + return srd52; } + return srd51; } diff --git a/src/lib/dnd/srd51.ts b/src/lib/dnd/srd51.ts new file mode 100644 index 0000000..d72c383 --- /dev/null +++ b/src/lib/dnd/srd51.ts @@ -0,0 +1,860 @@ +import { z } from "zod"; +import type { + AbilityScoreModifiers, + AbilityType, + Background, + BackgroundFeature, + BackgroundNameType, + CasterKindType, + Choice, + ClassDef, + ClassNameType, + HitDieType, + Race, + SizeType, + SkillType, + SlotProgression, + SpellSlotsType, + Subrace, + SpellcastingInfo, + SpellChangeEventType +} from "../dnd"; + + +const GamingSets = [ + "dice set", + "playing card set", +] as const; + +const ArtisanTools = [ + "alchemist’s supplies", + "brewer’s supplies", + "calligrapher’s supplies", + "carpenter’s tools", + "cartographer’s tools", + "cobbler’s tools", + "cook’s utensils", + "glassblower’s tools", + "jeweler’s tools", + "leatherworker’s tools", + "mason’s tools", + "painter’s supplies", + "potter’s tools", + "smith’s tools", + "tinker’s tools", + "weaver’s tools", + "woodcarver’s tools", +] as const; + +const Instruments = [ + "bagpipes", + "drum", + "dulcimer", + "flute", + "lute", + "lyre", + "horn", + "pan flute", + "shawm", + "viol", +] as const; + + +const Races: Race[] = [ + { + name: "dwarf", + size: "medium", + speed: 25, + ability_score_modifiers: { constitution: 2 }, + subraces: [ + { name: "hill dwarf", ability_score_modifiers: { wisdom: 1 } }, + { name: "mountain dwarf", ability_score_modifiers: { strength: 2 } } + ] + }, + { + name: "elf", + size: "medium", + speed: 30, + ability_score_modifiers: { dexterity: 2 }, + subraces: [ + { name: "high elf", ability_score_modifiers: { intelligence: 1 } }, + { name: "wood elf", ability_score_modifiers: { wisdom: 1 } }, + { name: "drow", ability_score_modifiers: { charisma: 1 } } + ] + }, + { + name: "halfling", + size: "small", + speed: 25, + ability_score_modifiers: { dexterity: 2 }, + subraces: [ + { name: "lightfoot", ability_score_modifiers: { charisma: 1 } }, + { name: "stout", ability_score_modifiers: { constitution: 1 } } + ] + }, + { + name: "human", + size: "medium", + speed: 30, + ability_score_modifiers: { + strength: 1, + dexterity: 1, + constitution: 1, + intelligence: 1, + wisdom: 1, + charisma: 1 + }, + variants: [ + { + name: "strong cute human", + ability_score_modifiers: { + // Typically two ability scores of choice + strength: 1, + charisma: 1, + } + } + ] + }, + { + name: "dragonborn", + size: "medium", + speed: 30, + ability_score_modifiers: { strength: 2, charisma: 1 } + }, + { + name: "gnome", + size: "small", + speed: 25, + ability_score_modifiers: { intelligence: 2 }, + subraces: [ + { name: "forest gnome", ability_score_modifiers: { dexterity: 1 } }, + { name: "rock gnome", ability_score_modifiers: { constitution: 1 } }, + { name: "deep gnome", ability_score_modifiers: { dexterity: 1 } }, + ] + }, + { + name: "half-elf", + size: "medium", + speed: 30, + ability_score_modifiers: { + charisma: 2 + // plus two ability scores of choice + } + }, + { + name: "half-orc", + size: "medium", + speed: 30, + ability_score_modifiers: { strength: 2, constitution: 1 } + }, + { + name: "tiefling", + size: "medium", + speed: 30, + ability_score_modifiers: { charisma: 2, intelligence: 1 } + } +] as const; +const RaceNames = Races.map(c => c.name); +const SubraceNames = Races.flatMap(r => r.subraces ? r.subraces.map(sr => sr.name) : []); + +const ClassNames = ["barbarian", "bard", "cleric", "druid", "fighter", "monk", "paladin", "ranger", "rogue", "sorcerer", "warlock", "wizard"] as const; + +const Classes: Record = { + barbarian: { + name: "barbarian", + hitDie: 12, + primaryAbilities: ["strength", "constitution"], + savingThrows: ["strength", "constitution"], + armorProficiencies: ["light", "medium", "shields"], + weaponProficiencies: ["simple", "martial"], + toolProficiencies: [], + skillChoices: { + choose: 2, + from: ["animal handling", "athletics", "intimidation", "nature", "perception", "survival"], + }, + subclasses: ["path of the berserker", "path of the totem warrior"], + subclassLevel: 3, + spellcasting: { enabled: false }, + }, + bard: { + name: "bard", + hitDie: 8, + primaryAbilities: ["charisma", "dexterity"], + savingThrows: ["dexterity", "charisma"], + armorProficiencies: ["light"], + weaponProficiencies: ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], + toolProficiencies: [ + { choose: 3, from: ["bagpipes","drum","dulcimer","flute","lute","lyre","horn","pan flute","shawm","viol"] } + ], + skillChoices: { + choose: 3, + from: [ + "acrobatics","animal handling","arcana","athletics","deception","history","insight","intimidation", + "investigation","medicine","nature","perception","performance","persuasion","religion","sleight of hand","stealth","survival" + ] + }, + subclasses: ["college of lore", "college of valor"], + subclassLevel: 3, + spellcasting: { enabled: true, kind: "full", ability: "charisma", changePrepared: "levelup" }, + }, + cleric: { + name: "cleric", + hitDie: 8, + primaryAbilities: ["wisdom"], + savingThrows: ["wisdom", "charisma"], + armorProficiencies: ["light", "medium", "heavy", "shields"], + weaponProficiencies: ["simple"], + toolProficiencies: [], + skillChoices: { choose: 2, from: ["history", "insight", "medicine", "persuasion", "religion"] }, + subclasses: ["knowledge", "life", "light", "nature", "tempest", "trickery", "war"], + subclassLevel: 1, + spellcasting: { enabled: true, kind: "full", ability: "wisdom", changePrepared: "longrest" }, + }, + druid: { + name: "druid", + hitDie: 8, + primaryAbilities: ["wisdom"], + savingThrows: ["intelligence", "wisdom"], + armorProficiencies: ["light (nonmetal)", "medium (nonmetal)", "shields (nonmetal)"], + weaponProficiencies: ["clubs","daggers","darts","javelins","maces","quarterstaffs","scimitars","sickles","slings","spears"], + toolProficiencies: ["herbalism kit"], + skillChoices: { choose: 2, from: ["arcana","animal handling","insight","medicine","nature","perception","religion","survival"] }, + subclasses: ["circle of the land", "circle of the moon"], + subclassLevel: 2, + spellcasting: { enabled: true, kind: "full", ability: "wisdom", changePrepared: "longrest" }, + }, + fighter: { + name: "fighter", + hitDie: 10, + primaryAbilities: ["strength", "dexterity", "constitution"], + savingThrows: ["strength", "constitution"], + armorProficiencies: ["light", "medium", "heavy", "shields"], + weaponProficiencies: ["simple", "martial"], + toolProficiencies: [], + skillChoices: { choose: 2, from: ["acrobatics","animal handling","athletics","history","insight","intimidation","perception","survival"] }, + subclasses: ["champion", "battle master", "eldritch knight"], + subclassLevel: 3, + spellcasting: { enabled: true, kind: "third", subclasses: ["eldritch knight"], ability: "intelligence", changePrepared: "levelup"}, + }, + monk: { + name: "monk", + hitDie: 8, + primaryAbilities: ["dexterity", "wisdom"], + savingThrows: ["strength", "dexterity"], + armorProficiencies: [], + weaponProficiencies: ["simple", "shortsword"], + toolProficiencies: [{ choose: 1, from: ["artisan's tools", "musical instrument"] }], + skillChoices: { choose: 2, from: ["acrobatics","athletics","history","insight","religion","stealth"] }, + subclasses: ["way of the open hand", "way of shadow", "way of the four elements"], + subclassLevel: 3, + spellcasting: { enabled: false }, + }, + paladin: { + name: "paladin", + hitDie: 10, + primaryAbilities: ["strength", "charisma"], + savingThrows: ["wisdom", "charisma"], + armorProficiencies: ["light", "medium", "heavy", "shields"], + weaponProficiencies: ["simple", "martial"], + toolProficiencies: [], + skillChoices: { choose: 2, from: ["athletics","insight","intimidation","medicine","persuasion","religion"] }, + subclasses: ["oath of devotion", "oath of the ancients", "oath of vengeance"], + subclassLevel: 3, + spellcasting: { enabled: true, kind: "half", ability: "charisma", changePrepared: "longrest", notes: "half-caster progression" }, + }, + ranger: { + name: "ranger", + hitDie: 10, + primaryAbilities: ["dexterity", "wisdom"], + savingThrows: ["strength", "dexterity"], + armorProficiencies: ["light", "medium", "shields"], + weaponProficiencies: ["simple", "martial"], + toolProficiencies: [], + skillChoices: { choose: 3, from: ["animal handling","athletics","insight","investigation","nature","perception","stealth","survival"] }, + subclasses: ["hunter", "beast master"], + subclassLevel: 3, + spellcasting: { enabled: true, kind: "half", ability: "wisdom", changePrepared: "levelup", notes: "half-caster progression" }, + }, + rogue: { + name: "rogue", + hitDie: 8, + primaryAbilities: ["dexterity"], + savingThrows: ["dexterity", "intelligence"], + armorProficiencies: ["light"], + weaponProficiencies: ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], + toolProficiencies: ["thieves' tools"], + skillChoices: { choose: 4, from: ["acrobatics","athletics","deception","insight","intimidation","investigation","perception","performance","persuasion","sleight of hand","stealth"] }, + subclasses: ["thief", "assassin", "arcane trickster"], + subclassLevel: 3, + spellcasting: { enabled: true, kind: "third", subclasses: ["arcane trickster"], ability: "intelligence", changePrepared: "levelup"}, + }, + sorcerer: { + name: "sorcerer", + hitDie: 6, + primaryAbilities: ["charisma"], + savingThrows: ["constitution", "charisma"], + armorProficiencies: [], + weaponProficiencies: ["dagger","dart","sling","quarterstaff","light crossbow"], + toolProficiencies: [], + skillChoices: { choose: 2, from: ["arcana","deception","insight","intimidation","persuasion","religion"] }, + subclasses: ["draconic bloodline", "wild magic"], + subclassLevel: 1, + spellcasting: { enabled: true, kind: "full", ability: "charisma", changePrepared: "levelup" }, + }, + warlock: { + name: "warlock", + hitDie: 8, + primaryAbilities: ["charisma"], + savingThrows: ["wisdom", "charisma"], + armorProficiencies: ["light"], + weaponProficiencies: ["simple"], + toolProficiencies: [], + skillChoices: { choose: 2, from: ["arcana","deception","history","intimidation","investigation","nature","religion"] }, + subclasses: ["the archfey", "the fiend", "the great old one"], + subclassLevel: 1, + spellcasting: { enabled: true, kind: "pact", ability: "charisma", changePrepared: "levelup", notes: "pact magic progression" }, + }, + wizard: { + name: "wizard", + hitDie: 6, + primaryAbilities: ["intelligence"], + savingThrows: ["intelligence", "wisdom"], + armorProficiencies: [], + weaponProficiencies: ["dagger","dart","sling","quarterstaff","light crossbow"], + toolProficiencies: [], + skillChoices: { choose: 2, from: ["arcana","history","insight","investigation","medicine","religion"] }, + subclasses: ["school of abjuration","school of conjuration","school of divination","school of enchantment","school of evocation","school of illusion","school of necromancy","school of transmutation"], + subclassLevel: 2, + spellcasting: { enabled: true, kind: "full", ability: "intelligence", changePrepared: "longrest" }, + } +}; +const SubclassNames = Object.values(Classes).flatMap(c => c.subclasses ? c.subclasses : []); + +const BackgroundNames = [ + "acolyte", "charlatan", "criminal", "entertainer", "folk hero", "guild artisan", "hermit", "noble", "outlander", "sage", "sailor", "soldier", "urchin", "pirate", +] as const; + +const Backgrounds: Record = { + acolyte: { + name: "acolyte", + skillProficiencies: ["insight", "religion"], + languageProficiencies: 2, + equipment: ["holy symbol", "prayer book or prayer wheel", "5 sticks of incense", "vestments", "common clothes", "15 gp"], + feature: { + name: "shelter of the faithful", + summary: "free support and lodging at a temple of your faith; connections to clergy.", + }, + }, + charlatan: { + name: "charlatan", + skillProficiencies: ["deception", "sleight of hand"], + toolProficiencies: ["disguise kit", "forgery kit"], + equipment: ["fine clothes", "disguise kit", "con tools (e.g., signet of a fake identity)", "15 gp"], + feature: { + name: "false identity", + summary: "you maintain a second identity with documentation, acquaintances, and disguises.", + }, + }, + criminal: { + name: "criminal", + skillProficiencies: ["deception", "stealth"], + toolProficiencies: [ + { choose: 1, from: [...GamingSets] as unknown as string[] }, + "thieves’ tools", + ], + equipment: ["crowbar", "dark common clothes with hood", "15 gp"], + feature: { + name: "criminal contact", + summary: "a reliable and trustworthy contact within the criminal underworld.", + }, + }, + entertainer: { + name: "entertainer", + skillProficiencies: ["acrobatics", "performance"], + toolProficiencies: [ + { choose: 1, from: [...Instruments] as unknown as string[] }, + "disguise kit", + ], + equipment: ["musical instrument (one of your choice)", "favor of an admirer", "costume", "15 gp"], + feature: { + name: "by popular demand", + summary: "you can find a place to perform and secure free lodging and modest food.", + }, + }, + "folk hero": { + name: "folk hero", + skillProficiencies: ["animal handling", "survival"], + toolProficiencies: [ + { choose: 1, from: [...ArtisanTools] as unknown as string[] }, + "vehicles (land)", + ], + equipment: ["artisan’s tools (one of your choice)", "shovel", "iron pot", "common clothes", "10 gp"], + feature: { + name: "rustic hospitality", + summary: "common folk will shelter you; you can hide among them.", + }, + }, + "guild artisan": { + name: "guild artisan", + skillProficiencies: ["insight", "persuasion"], + toolProficiencies: [{ choose: 1, from: [...ArtisanTools] as unknown as string[] }], + languageProficiencies: 1, + equipment: ["artisan’s tools (one of your choice)", "letter of introduction from your guild", "traveler’s clothes", "15 gp"], + feature: { + name: "guild membership", + summary: "access to guild facilities, contacts, and legal support (with dues).", + }, + }, + hermit: { + name: "hermit", + skillProficiencies: ["medicine", "religion"], + toolProficiencies: ["herbalism kit"], + languageProficiencies: 1, + equipment: ["scroll case of notes", "winter blanket", "common clothes", "herbalism kit", "5 gp"], + feature: { + name: "discovery", + summary: "you uncovered a unique and powerful insight during seclusion.", + }, + }, + noble: { + name: "noble", + skillProficiencies: ["history", "persuasion"], + toolProficiencies: [{ choose: 1, from: [...GamingSets] as unknown as string[] }], + languageProficiencies: 1, + equipment: ["fine clothes", "signet ring", "scroll of pedigree", "25 gp"], + feature: { + name: "position of privilege", + summary: "high social standing; easier audience with nobles and officials.", + }, + }, + outlander: { + name: "outlander", + skillProficiencies: ["athletics", "survival"], + toolProficiencies: [{ choose: 1, from: [...Instruments] as unknown as string[] }], + languageProficiencies: 1, + equipment: ["staff", "hunting trap", "trophy from an animal", "traveler’s clothes", "10 gp"], + feature: { + name: "wanderer", + summary: "excellent memory for maps and geography; find food and fresh water for your group.", + }, + }, + sage: { + name: "sage", + skillProficiencies: ["arcana", "history"], + languageProficiencies: 2, + equipment: ["bottle of black ink", "quill", "small knife", "letter from a dead colleague with a question you haven’t answered", "common clothes", "10 gp"], + feature: { + name: "researcher", + summary: "you can usually find where to obtain lore; you know how to get answers.", + }, + }, + sailor: { + name: "sailor", + skillProficiencies: ["athletics", "perception"], + toolProficiencies: ["navigator’s tools", "vehicles (water)"], + equipment: ["belaying pin (club)", "50 feet of silk rope", "lucky charm", "common clothes", "10 gp"], + feature: { + name: "ship’s passage", + summary: "secure free passage on a sailing ship for you and companions (with obligations).", + }, + }, + pirate: { + name: "pirate", + skillProficiencies: ["athletics", "perception"], + toolProficiencies: ["navigator’s tools", "vehicles (water)"], + equipment: ["belaying pin (club)", "50 feet of silk rope", "lucky charm", "common clothes", "10 gp"], + feature: { + name: "bad reputation", + summary: "your notoriety lets you get away with minor crimes; people fear you.", + }, + }, + soldier: { + name: "soldier", + skillProficiencies: ["athletics", "intimidation"], + toolProficiencies: [{ choose: 1, from: [...GamingSets] as unknown as string[] }, "vehicles (land)"], + equipment: ["insignia of rank", "trophy from a fallen enemy", "bone dice or deck of cards", "common clothes", "10 gp"], + feature: { + name: "military rank", + summary: "you have a rank; soldiers loyal to your former organization recognize authority.", + }, + }, + urchin: { + name: "urchin", + skillProficiencies: ["sleight of hand", "stealth"], + toolProficiencies: ["disguise kit", "thieves’ tools"], + equipment: ["small knife", "map of city you grew up in", "pet mouse", "token to remember parents", "common clothes", "10 gp"], + feature: { + name: "city secrets", + summary: "you and companions can move through a city twice as fast via alleys and passages.", + }, + }, +} as const; + +type SpellProgressionTableRow = { + level: number; + cantrips: number; + prepared: number; + slots: number[]; // 1st to 9th level slots + arcanum?: Record; // warlock-only +} + +const SpellProgressionTables: Partial> = { + "bard": [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 2, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 2, cantrips: 2, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 3, cantrips: 2, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, + {level: 4, cantrips: 3, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, + {level: 5, cantrips: 3, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, + {level: 6, cantrips: 3, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, + {level: 7, cantrips: 3, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, + {level: 8, cantrips: 3, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, + {level: 9, cantrips: 3, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, + {level: 10, cantrips: 4, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, + {level: 11, cantrips: 4, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 12, cantrips: 4, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 13, cantrips: 4, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 14, cantrips: 4, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 15, cantrips: 4, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 16, cantrips: 4, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 17, cantrips: 4, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, + {level: 18, cantrips: 4, prepared: 20, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, + {level: 19, cantrips: 4, prepared: 21, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, + {level: 20, cantrips: 4, prepared: 22, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, + ], + 'cleric': [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 3, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 2, cantrips: 3, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 3, cantrips: 3, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, + {level: 4, cantrips: 4, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, + {level: 5, cantrips: 4, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, + {level: 6, cantrips: 4, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, + {level: 7, cantrips: 4, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, + {level: 8, cantrips: 4, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, + {level: 9, cantrips: 4, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, + {level: 10, cantrips: 5, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, + {level: 11, cantrips: 5, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 12, cantrips: 5, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 13, cantrips: 5, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 14, cantrips: 5, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 15, cantrips: 5, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 16, cantrips: 5, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 17, cantrips: 5, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, + {level: 18, cantrips: 5, prepared: 20, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, + {level: 19, cantrips: 5, prepared: 21, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, + {level: 20, cantrips: 5, prepared: 22, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, + ], + "druid": [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 2, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 2, cantrips: 2, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 3, cantrips: 2, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, + {level: 4, cantrips: 3, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, + {level: 5, cantrips: 3, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, + {level: 6, cantrips: 3, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, + {level: 7, cantrips: 3, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, + {level: 8, cantrips: 3, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, + {level: 9, cantrips: 3, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, + {level: 10, cantrips: 4, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, + {level: 11, cantrips: 4, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 12, cantrips: 4, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 13, cantrips: 4, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 14, cantrips: 4, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 15, cantrips: 4, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 16, cantrips: 4, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 17, cantrips: 4, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, + {level: 18, cantrips: 4, prepared: 20, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, + {level: 19, cantrips: 4, prepared: 21, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, + {level: 20, cantrips: 4, prepared: 22, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, + ], + "paladin": [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 0, prepared: 2, slots: [2, 0, 0, 0, 0]}, + {level: 2, cantrips: 0, prepared: 3, slots: [2, 0, 0, 0, 0]}, + {level: 3, cantrips: 0, prepared: 4, slots: [3, 0, 0, 0, 0]}, + {level: 4, cantrips: 0, prepared: 5, slots: [3, 0, 0, 0, 0]}, + {level: 5, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, + {level: 6, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, + {level: 7, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, + {level: 8, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, + {level: 9, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, + {level: 10, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, + {level: 11, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, + {level: 12, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, + {level: 13, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, + {level: 14, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, + {level: 15, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, + {level: 16, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, + {level: 17, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, + {level: 18, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, + {level: 19, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, + {level: 20, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, + ], + "ranger": [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 0, prepared: 2, slots: [2, 0, 0, 0, 0]}, + {level: 2, cantrips: 0, prepared: 3, slots: [2, 0, 0, 0, 0]}, + {level: 3, cantrips: 0, prepared: 4, slots: [3, 0, 0, 0, 0]}, + {level: 4, cantrips: 0, prepared: 5, slots: [3, 0, 0, 0, 0]}, + {level: 5, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, + {level: 6, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, + {level: 7, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, + {level: 8, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, + {level: 9, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, + {level: 10, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, + {level: 11, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, + {level: 12, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, + {level: 13, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, + {level: 14, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, + {level: 15, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, + {level: 16, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, + {level: 17, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, + {level: 18, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, + {level: 19, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, + {level: 20, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, + ], + "sorcerer": [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 4, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 2, cantrips: 4, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 3, cantrips: 4, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, + {level: 4, cantrips: 5, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, + {level: 5, cantrips: 5, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, + {level: 6, cantrips: 5, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, + {level: 7, cantrips: 5, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, + {level: 8, cantrips: 5, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, + {level: 9, cantrips: 5, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, + {level: 10, cantrips: 6, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, + {level: 11, cantrips: 6, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 12, cantrips: 6, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 13, cantrips: 6, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 14, cantrips: 6, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 15, cantrips: 6, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 16, cantrips: 6, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 17, cantrips: 6, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, + {level: 18, cantrips: 6, prepared: 20, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, + {level: 19, cantrips: 6, prepared: 21, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, + {level: 20, cantrips: 6, prepared: 22, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, + ], + "warlock": [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 2, prepared: 2, slots: [1, 0, 0, 0, 0], arcanum: {}}, + {level: 2, cantrips: 2, prepared: 3, slots: [2, 0, 0, 0, 0], arcanum: {}}, + {level: 3, cantrips: 2, prepared: 4, slots: [0, 2, 0, 0, 0], arcanum: {}}, + {level: 4, cantrips: 3, prepared: 5, slots: [0, 2, 0, 0, 0], arcanum: {}}, + {level: 5, cantrips: 3, prepared: 6, slots: [0, 0, 2, 0, 0], arcanum: {}}, + {level: 6, cantrips: 3, prepared: 7, slots: [0, 0, 2, 0, 0], arcanum: {}}, + {level: 7, cantrips: 3, prepared: 8, slots: [0, 0, 0, 2, 0], arcanum: {}}, + {level: 8, cantrips: 3, prepared: 9, slots: [0, 0, 0, 2, 0], arcanum: {}}, + {level: 9, cantrips: 3, prepared: 10, slots: [0, 0, 0, 0, 2], arcanum: {}}, + {level: 10, cantrips: 4, prepared: 10, slots: [0, 0, 0, 0, 2], arcanum: {}}, + {level: 11, cantrips: 4, prepared: 11, slots: [0, 0, 0, 0, 3], arcanum: {6: 1}}, + {level: 12, cantrips: 4, prepared: 11, slots: [0, 0, 0, 0, 3], arcanum: {6: 1}}, + {level: 13, cantrips: 4, prepared: 12, slots: [0, 0, 0, 0, 3], arcanum: {6: 1, 7: 1}}, + {level: 14, cantrips: 4, prepared: 12, slots: [0, 0, 0, 0, 3], arcanum: {6: 1, 7: 1}}, + {level: 15, cantrips: 4, prepared: 13, slots: [0, 0, 0, 0, 3], arcanum: {6: 1, 7: 1, 8: 1}}, + {level: 16, cantrips: 4, prepared: 13, slots: [0, 0, 0, 0, 3], arcanum: {6: 1, 7: 1, 8: 1}}, + {level: 17, cantrips: 4, prepared: 14, slots: [0, 0, 0, 0, 4], arcanum: {6: 1, 7: 1, 8: 1, 9: 1}}, + {level: 18, cantrips: 4, prepared: 14, slots: [0, 0, 0, 0, 4], arcanum: {6: 1, 7: 1, 8: 1, 9: 1}}, + {level: 19, cantrips: 4, prepared: 15, slots: [0, 0, 0, 0, 4], arcanum: {6: 1, 7: 1, 8: 1, 9: 1}}, + {level: 20, cantrips: 4, prepared: 15, slots: [0, 0, 0, 0, 4], arcanum: {6: 1, 7: 1, 8: 1, 9: 1}}, + ], + "wizard": [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 3, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 2, cantrips: 3, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 3, cantrips: 3, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, + {level: 4, cantrips: 4, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, + {level: 5, cantrips: 4, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, + {level: 6, cantrips: 4, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, + {level: 7, cantrips: 4, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, + {level: 8, cantrips: 4, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, + {level: 9, cantrips: 4, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, + {level: 10, cantrips: 5, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, + {level: 11, cantrips: 5, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 12, cantrips: 5, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 13, cantrips: 5, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 14, cantrips: 5, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 15, cantrips: 5, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 16, cantrips: 5, prepared: 21, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 17, cantrips: 5, prepared: 22, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, + {level: 18, cantrips: 5, prepared: 23, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, + {level: 19, cantrips: 5, prepared: 24, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, + {level: 20, cantrips: 5, prepared: 26, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, + ], +} + +type SpellProgression = number[]; +const THIRD_CASTER_CANTRIPS_KNOWN: SpellProgression = [ + 0, 0, 0, // 0-2 (unused / not yet a caster) + 2, 2, 2, 2, 2, 2, 2, 3, // 3-10 + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // 11-20 +]; + +const THIRD_CASTER_SPELLS_PREPARED: SpellProgression = [ + 0, 0, 0, // 0-2 (unused / not yet a caster) + 3, 4, 4, 5, 6, 6, 7, 8, // 3-10 + 8, 9, 10, 10, 11, 11, 12, 13, 13, 13, // 11-20 +]; + +const FULL_CASTER_SLOTS: SlotProgression = [ + {level: 0, slots: []}, + {level: 1, slots: [ 2 ]}, + {level: 2, slots: [ 3 ]}, + {level: 3, slots: [ 4, 2 ]}, + {level: 4, slots: [ 4, 3 ]}, + {level: 5, slots: [ 4, 3, 2 ]}, + {level: 6, slots: [ 4, 3, 3 ]}, + {level: 7, slots: [ 4, 3, 3, 1 ]}, + {level: 8, slots: [ 4, 3, 3, 2 ]}, + {level: 9, slots: [ 4, 3, 3, 3, 1 ]}, + {level: 10, slots: [ 4, 3, 3, 3, 2 ]}, + {level: 11, slots: [ 4, 3, 3, 3, 2, 1 ]}, + {level: 12, slots: [ 4, 3, 3, 3, 2, 1 ]}, + {level: 13, slots: [ 4, 3, 3, 3, 2, 1, 1 ]}, + {level: 14, slots: [ 4, 3, 3, 3, 2, 1, 1 ]}, + {level: 15, slots: [ 4, 3, 3, 3, 2, 1, 1, 1 ]}, + {level: 16, slots: [ 4, 3, 3, 3, 2, 1, 1, 1 ]}, + {level: 17, slots: [ 4, 3, 3, 3, 2, 1, 1, 1, 1 ]}, + {level: 18, slots: [ 4, 3, 3, 3, 3, 1, 1, 1, 1 ]}, + {level: 19, slots: [ 4, 3, 3, 3, 3, 2, 1, 1, 1 ]}, + {level: 20, slots: [ 4, 3, 3, 3, 3, 2, 2, 1, 1 ]}, +]; + +const HALF_CASTER_SLOTS: SlotProgression = [ + {level: 0, slots: []}, + {level: 1, slots: [ 2 ]}, + {level: 2, slots: [ 2 ]}, + {level: 3, slots: [ 3 ]}, + {level: 4, slots: [ 3 ]}, + {level: 5, slots: [ 4, 2 ]}, + {level: 6, slots: [ 4, 2 ]}, + {level: 7, slots: [ 4, 3 ]}, + {level: 8, slots: [ 4, 3 ]}, + {level: 9, slots: [ 4, 3, 2 ]}, + {level: 10, slots: [ 4, 3, 2 ]}, + {level: 11, slots: [ 4, 3, 3 ]}, + {level: 12, slots: [ 4, 3, 3 ]}, + {level: 13, slots: [ 4, 3, 3, 1 ]}, + {level: 14, slots: [ 4, 3, 3, 1 ]}, + {level: 15, slots: [ 4, 3, 3, 2 ]}, + {level: 16, slots: [ 4, 3, 3, 2 ]}, + {level: 17, slots: [ 4, 3, 3, 3, 1 ]}, + {level: 18, slots: [ 4, 3, 3, 3, 1 ]}, + {level: 19, slots: [ 4, 3, 3, 3, 2 ]}, + {level: 20, slots: [ 4, 3, 3, 3, 2 ]}, +]; + +const THIRD_CASTER_SLOTS: SlotProgression = [ + {level: 0, slots: []}, + {level: 1, slots: []}, + {level: 2, slots: []}, + {level: 3, slots: [ 2 ]}, + {level: 4, slots: [ 3 ]}, + {level: 5, slots: [ 3 ]}, + {level: 6, slots: [ 3, 2 ]}, + {level: 7, slots: [ 4, 2 ]}, + {level: 8, slots: [ 4, 2 ]}, + {level: 9, slots: [ 4, 2 ]}, + {level: 10, slots: [ 4, 3 ]}, + {level: 11, slots: [ 4, 3 ]}, + {level: 12, slots: [ 4, 3 ]}, + {level: 13, slots: [ 4, 3, 2 ]}, + {level: 14, slots: [ 4, 3, 2 ]}, + {level: 15, slots: [ 4, 3, 2 ]}, + {level: 16, slots: [ 4, 3, 3 ]}, + {level: 17, slots: [ 4, 3, 3 ]}, + {level: 18, slots: [ 4, 3, 3 ]}, + {level: 19, slots: [ 4, 3, 3, 1 ]}, + {level: 20, slots: [ 4, 3, 3, 1 ]}, +]; + +function slotsFromProgression(progression: number[]): SpellSlotsType { + const slots: number[] = []; + + for (let level = 1; level <= 9; level++) { + const slotsAtLevel = progression[level - 1] || 0; + for (let i = 0; i < slotsAtLevel; i++) { + slots.push(level); + } + } + + return slots as SpellSlotsType; +} + +const srd51 = { + Races, + RaceNames, + SubraceNames, + Classes, + ClassNames, + SubclassNames, + Backgrounds, + BackgroundNames, + SpellProgressionTables, + THIRD_CASTER_CANTRIPS_KNOWN, + THIRD_CASTER_SPELLS_PREPARED, + FULL_CASTER_SLOTS, + HALF_CASTER_SLOTS, + THIRD_CASTER_SLOTS, + + maxCantripsKnown(className: ClassNameType, level: number): number { + const classDef = this.Classes[className]; + if (!classDef.spellcasting.enabled) return 0; + + switch (className) { + case "bard": + case "sorcerer": + case "warlock": + case "cleric": + case "druid": + case "wizard": + return this.SpellProgressionTables[className]![level]?.cantrips || 0; + case "fighter": // Eldritch Knight + case "rogue": // Arcane Trickster + return this.THIRD_CASTER_CANTRIPS_KNOWN[level] || 0; + default: return 0; + } + }, + + maxSpellsPrepared(className: ClassNameType, level: number): number | null { + const classDef = this.Classes[className]; + if (!classDef.spellcasting.enabled) { + return null; // Not a "known" caster + } + + switch (className) { + case "bard": + case "sorcerer": + case "warlock": + case "cleric": + case "druid": + case "wizard": + case "paladin": + case "ranger": + return this.SpellProgressionTables[className]![level]?.prepared || 0; + case "fighter": // Eldritch Knight + case "rogue": // Arcane Trickster + return this.THIRD_CASTER_CANTRIPS_KNOWN[level] || 0; + default: return 0; + } + }, + + getSlotsFor(casterKind: CasterKindType, level: number): SpellSlotsType { + if (casterKind === "full") { + return slotsFromProgression(this.FULL_CASTER_SLOTS[level]?.slots || []); + } else if (casterKind === "half") { + return slotsFromProgression(this.HALF_CASTER_SLOTS[level]?.slots || []); + } else if (casterKind === "third") { + return slotsFromProgression(this.THIRD_CASTER_SLOTS[level]?.slots || []); + } else if (casterKind === "pact") { + const progression = this.SpellProgressionTables["warlock"]!; + return slotsFromProgression(progression[level]?.slots || []); + } else { + return [] + } + } +} + +export default srd51; diff --git a/src/lib/dnd/srd52.ts b/src/lib/dnd/srd52.ts new file mode 100644 index 0000000..53067c3 --- /dev/null +++ b/src/lib/dnd/srd52.ts @@ -0,0 +1,716 @@ +import { z } from "zod"; +import type { + AbilityScoreModifiers, + AbilityType, + Background, + BackgroundFeature, + BackgroundNameType, + CasterKindType, + Choice, + ClassDef, + ClassNameType, + HitDieType, + Race, + SizeType, + SkillType, + SlotProgression, + SpellSlotsType, + Subrace, + SpellcastingInfo, + SpellChangeEventType +} from "../dnd"; + + +const GamingSets = [ + "dice set", + "playing card set", +] as const; + +const ArtisanTools = [ + "alchemist’s supplies", + "brewer’s supplies", + "calligrapher’s supplies", + "carpenter’s tools", + "cartographer’s tools", + "cobbler’s tools", + "cook’s utensils", + "glassblower’s tools", + "jeweler’s tools", + "leatherworker’s tools", + "mason’s tools", + "painter’s supplies", + "potter’s tools", + "smith’s tools", + "tinker’s tools", + "weaver’s tools", + "woodcarver’s tools", +] as const; + +const Instruments = [ + "bagpipes", + "drum", + "dulcimer", + "flute", + "lute", + "lyre", + "horn", + "pan flute", + "shawm", + "viol", +] as const; + + +const SpeciesData: Race[] = [ + { + name: "dragonborn", + size: "medium", + speed: 30, + }, + { + name: "dwarf", + size: "medium", + speed: 30, + }, + { + name: "elf", + size: "medium", + speed: 30, + subraces: [ + { name: "drow" }, + { name: "high elf" }, + { name: "wood elf" }, + ] + }, + { + name: "gnome", + size: "small", + speed: 30, + subraces: [ + { name: "forest gnome" }, + { name: "rock gnome" }, + ] + }, + { + name: "goliath", + size: "medium", + speed: 35, + }, + { + name: "halfling", + size: "small", + speed: 30, + }, + { + name: "human", + size: "medium", // or small + speed: 30, + }, + { + name: "orc", + size: "medium", + speed: 30, + }, + { + name: "tiefling", + size: "medium", // or small + speed: 30, + } +] as const; +const RaceNames = SpeciesData.map(c => c.name); +const SubraceNames = SpeciesData.flatMap(r => r.subraces ? r.subraces.map(sr => sr.name) : []); + +const ClassNames = ["barbarian", "bard", "cleric", "druid", "fighter", "monk", "paladin", "ranger", "rogue", "sorcerer", "warlock", "wizard"] as const; + +const Classes: Record = { + barbarian: { + name: "barbarian", + hitDie: 12, + primaryAbilities: ["strength", "constitution"], + savingThrows: ["strength", "constitution"], + armorProficiencies: ["light", "medium", "shields"], + weaponProficiencies: ["simple", "martial"], + toolProficiencies: [], + skillChoices: { + choose: 2, + from: ["animal handling", "athletics", "intimidation", "nature", "perception", "survival"], + }, + subclasses: ["path of the berserker"], + subclassLevel: 3, + spellcasting: { enabled: false }, + }, + bard: { + name: "bard", + hitDie: 8, + primaryAbilities: ["charisma", "dexterity"], + savingThrows: ["dexterity", "charisma"], + armorProficiencies: ["light"], + weaponProficiencies: ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], + toolProficiencies: [ + { choose: 3, from: ["bagpipes","drum","dulcimer","flute","lute","lyre","horn","pan flute","shawm","viol"] } + ], + skillChoices: { + choose: 3, + from: [ + "acrobatics","animal handling","arcana","athletics","deception","history","insight","intimidation", + "investigation","medicine","nature","perception","performance","persuasion","religion","sleight of hand","stealth","survival" + ] + }, + subclasses: ["college of lore"], + subclassLevel: 3, + spellcasting: { enabled: true, kind: "full", ability: "charisma", changePrepared: "levelup" }, + }, + cleric: { + name: "cleric", + hitDie: 8, + primaryAbilities: ["wisdom"], + savingThrows: ["wisdom", "charisma"], + armorProficiencies: ["light", "medium", "heavy", "shields"], + weaponProficiencies: ["simple"], + toolProficiencies: [], + skillChoices: { choose: 2, from: ["history", "insight", "medicine", "persuasion", "religion"] }, + subclasses: ["life domain"], + subclassLevel: 1, + spellcasting: { enabled: true, kind: "full", ability: "wisdom", changePrepared: "longrest" }, + }, + druid: { + name: "druid", + hitDie: 8, + primaryAbilities: ["wisdom"], + savingThrows: ["intelligence", "wisdom"], + armorProficiencies: ["light (nonmetal)", "medium (nonmetal)", "shields (nonmetal)"], + weaponProficiencies: ["clubs","daggers","darts","javelins","maces","quarterstaffs","scimitars","sickles","slings","spears"], + toolProficiencies: ["herbalism kit"], + skillChoices: { choose: 2, from: ["arcana","animal handling","insight","medicine","nature","perception","religion","survival"] }, + subclasses: ["circle of the land"], + subclassLevel: 2, + spellcasting: { enabled: true, kind: "full", ability: "wisdom", changePrepared: "longrest" }, + }, + fighter: { + name: "fighter", + hitDie: 10, + primaryAbilities: ["strength", "dexterity", "constitution"], + savingThrows: ["strength", "constitution"], + armorProficiencies: ["light", "medium", "heavy", "shields"], + weaponProficiencies: ["simple", "martial"], + toolProficiencies: [], + skillChoices: { choose: 2, from: ["acrobatics","animal handling","athletics","history","insight","intimidation","perception","survival"] }, + subclasses: ["champion"], + subclassLevel: 3, + spellcasting: { enabled: false }, + }, + monk: { + name: "monk", + hitDie: 8, + primaryAbilities: ["dexterity", "wisdom"], + savingThrows: ["strength", "dexterity"], + armorProficiencies: [], + weaponProficiencies: ["simple", "shortsword"], + toolProficiencies: [{ choose: 1, from: ["artisan's tools", "musical instrument"] }], + skillChoices: { choose: 2, from: ["acrobatics","athletics","history","insight","religion","stealth"] }, + subclasses: ["warrior of the open hand"], + subclassLevel: 3, + spellcasting: { enabled: false }, + }, + paladin: { + name: "paladin", + hitDie: 10, + primaryAbilities: ["strength", "charisma"], + savingThrows: ["wisdom", "charisma"], + armorProficiencies: ["light", "medium", "heavy", "shields"], + weaponProficiencies: ["simple", "martial"], + toolProficiencies: [], + skillChoices: { choose: 2, from: ["athletics","insight","intimidation","medicine","persuasion","religion"] }, + subclasses: ["oath of devotion"], + subclassLevel: 3, + spellcasting: { enabled: true, kind: "half", ability: "charisma", changePrepared: "longrest", notes: "half-caster progression" }, + }, + ranger: { + name: "ranger", + hitDie: 10, + primaryAbilities: ["dexterity", "wisdom"], + savingThrows: ["strength", "dexterity"], + armorProficiencies: ["light", "medium", "shields"], + weaponProficiencies: ["simple", "martial"], + toolProficiencies: [], + skillChoices: { choose: 3, from: ["animal handling","athletics","insight","investigation","nature","perception","stealth","survival"] }, + subclasses: ["hunter"], + subclassLevel: 3, + spellcasting: { enabled: true, kind: "half", ability: "wisdom", changePrepared: "levelup", notes: "half-caster progression" }, + }, + rogue: { + name: "rogue", + hitDie: 8, + primaryAbilities: ["dexterity"], + savingThrows: ["dexterity", "intelligence"], + armorProficiencies: ["light"], + weaponProficiencies: ["simple", "hand crossbow", "longsword", "rapier", "shortsword"], + toolProficiencies: ["thieves' tools"], + skillChoices: { choose: 4, from: ["acrobatics","athletics","deception","insight","intimidation","investigation","perception","performance","persuasion","sleight of hand","stealth"] }, + subclasses: ["thief"], + subclassLevel: 3, + spellcasting: { enabled: false }, + }, + sorcerer: { + name: "sorcerer", + hitDie: 6, + primaryAbilities: ["charisma"], + savingThrows: ["constitution", "charisma"], + armorProficiencies: [], + weaponProficiencies: ["dagger","dart","sling","quarterstaff","light crossbow"], + toolProficiencies: [], + skillChoices: { choose: 2, from: ["arcana","deception","insight","intimidation","persuasion","religion"] }, + subclasses: ["draconic sorcery"], + subclassLevel: 1, + spellcasting: { enabled: true, kind: "full", ability: "charisma", changePrepared: "levelup" }, + }, + warlock: { + name: "warlock", + hitDie: 8, + primaryAbilities: ["charisma"], + savingThrows: ["wisdom", "charisma"], + armorProficiencies: ["light"], + weaponProficiencies: ["simple"], + toolProficiencies: [], + skillChoices: { choose: 2, from: ["arcana","deception","history","intimidation","investigation","nature","religion"] }, + subclasses: ["fiend patron"], + subclassLevel: 1, + spellcasting: { enabled: true, kind: "pact", ability: "charisma", changePrepared: "levelup", notes: "pact magic progression" }, + }, + wizard: { + name: "wizard", + hitDie: 6, + primaryAbilities: ["intelligence"], + savingThrows: ["intelligence", "wisdom"], + armorProficiencies: [], + weaponProficiencies: ["dagger","dart","sling","quarterstaff","light crossbow"], + toolProficiencies: [], + skillChoices: { choose: 2, from: ["arcana","history","insight","investigation","medicine","religion"] }, + subclasses: ["evoker"], + subclassLevel: 2, + spellcasting: { enabled: true, kind: "full", ability: "intelligence", changePrepared: "longrest" }, + } +}; +const SubclassNames = Object.values(Classes).flatMap(c => c.subclasses ? c.subclasses : []); + +const BackgroundNames = [ + "acolyte", "criminal", "sage", "soldier" +] as const; + +const Backgrounds: Record = { + acolyte: { + name: "acolyte", + skillProficiencies: ["insight", "religion"], + feat: "Magic Initiate (Cleric)", + ability_scores: ["intelligence", "wisdom", "charisma"], + equipment: ["Calligrapher’s Supplies", "Book (prayers)", "Holy Symbol", "Parchment (10 sheets)", "Robe", "8 GP"], + feature: { + name: "shelter of the faithful", + summary: "free support and lodging at a temple of your faith; connections to clergy.", + }, + }, + criminal: { + name: "criminal", + skillProficiencies: ["deception", "stealth"], + toolProficiencies: ["thieves’ tools"], + feat: "Alert", + ability_scores: ["dexterity", "constitution", "intelligence"], + equipment: ["2 Daggers", "Thieves’ Tools", "Crowbar", "2 Pouches", "Traveler’s Clothes", "16 GP"], + feature: { + name: "criminal contact", + summary: "a reliable and trustworthy contact within the criminal underworld.", + }, + }, + sage: { + name: "sage", + skillProficiencies: ["arcana", "history"], + toolProficiencies: ["calligrapher's supplies"], + feat: "Magic Initiate (Wizard)", + ability_scores: ["constitution", "intelligence", "wisdom"], + equipment: ["Quarterstaff", "Calligrapher’s Supplies", "Book (history)", "Parchment (8 sheets)", "Robe", "8 GP"], + feature: { + name: "researcher", + summary: "you can usually find where to obtain lore; you know how to get answers.", + }, + }, + soldier: { + name: "soldier", + skillProficiencies: ["athletics", "intimidation"], + toolProficiencies: [{ choose: 1, from: [...GamingSets] as unknown as string[] }], + feat: "Savage Attacker", + ability_scores: ["strength", "dexterity", "constitution"], + equipment: ["Spear", "Shortbow", "20 Arrows", "Gaming Set", "Healer’s Kit", "Quiver", "Traveler’s Clothes", "14 GP"], + feature: { + name: "military rank", + summary: "you have a rank; soldiers loyal to your former organization recognize authority.", + }, + }, +} as const; + +type SpellProgressionTableRow = { + level: number; + cantrips: number; + prepared: number; + slots: number[]; // 1st to 9th level slots + arcanum?: Record; // warlock-only +} + +const SpellProgressionTables: Partial> = { + "bard": [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 2, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 2, cantrips: 2, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 3, cantrips: 2, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, + {level: 4, cantrips: 3, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, + {level: 5, cantrips: 3, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, + {level: 6, cantrips: 3, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, + {level: 7, cantrips: 3, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, + {level: 8, cantrips: 3, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, + {level: 9, cantrips: 3, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, + {level: 10, cantrips: 4, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, + {level: 11, cantrips: 4, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 12, cantrips: 4, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 13, cantrips: 4, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 14, cantrips: 4, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 15, cantrips: 4, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 16, cantrips: 4, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 17, cantrips: 4, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, + {level: 18, cantrips: 4, prepared: 20, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, + {level: 19, cantrips: 4, prepared: 21, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, + {level: 20, cantrips: 4, prepared: 22, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, + ], + 'cleric': [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 3, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 2, cantrips: 3, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 3, cantrips: 3, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, + {level: 4, cantrips: 4, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, + {level: 5, cantrips: 4, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, + {level: 6, cantrips: 4, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, + {level: 7, cantrips: 4, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, + {level: 8, cantrips: 4, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, + {level: 9, cantrips: 4, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, + {level: 10, cantrips: 5, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, + {level: 11, cantrips: 5, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 12, cantrips: 5, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 13, cantrips: 5, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 14, cantrips: 5, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 15, cantrips: 5, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 16, cantrips: 5, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 17, cantrips: 5, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, + {level: 18, cantrips: 5, prepared: 20, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, + {level: 19, cantrips: 5, prepared: 21, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, + {level: 20, cantrips: 5, prepared: 22, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, + ], + "druid": [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 2, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 2, cantrips: 2, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 3, cantrips: 2, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, + {level: 4, cantrips: 3, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, + {level: 5, cantrips: 3, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, + {level: 6, cantrips: 3, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, + {level: 7, cantrips: 3, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, + {level: 8, cantrips: 3, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, + {level: 9, cantrips: 3, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, + {level: 10, cantrips: 4, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, + {level: 11, cantrips: 4, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 12, cantrips: 4, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 13, cantrips: 4, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 14, cantrips: 4, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 15, cantrips: 4, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 16, cantrips: 4, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 17, cantrips: 4, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, + {level: 18, cantrips: 4, prepared: 20, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, + {level: 19, cantrips: 4, prepared: 21, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, + {level: 20, cantrips: 4, prepared: 22, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, + ], + "paladin": [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 0, prepared: 2, slots: [2, 0, 0, 0, 0]}, + {level: 2, cantrips: 0, prepared: 3, slots: [2, 0, 0, 0, 0]}, + {level: 3, cantrips: 0, prepared: 4, slots: [3, 0, 0, 0, 0]}, + {level: 4, cantrips: 0, prepared: 5, slots: [3, 0, 0, 0, 0]}, + {level: 5, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, + {level: 6, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, + {level: 7, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, + {level: 8, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, + {level: 9, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, + {level: 10, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, + {level: 11, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, + {level: 12, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, + {level: 13, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, + {level: 14, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, + {level: 15, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, + {level: 16, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, + {level: 17, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, + {level: 18, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, + {level: 19, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, + {level: 20, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, + ], + "ranger": [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 0, prepared: 2, slots: [2, 0, 0, 0, 0]}, + {level: 2, cantrips: 0, prepared: 3, slots: [2, 0, 0, 0, 0]}, + {level: 3, cantrips: 0, prepared: 4, slots: [3, 0, 0, 0, 0]}, + {level: 4, cantrips: 0, prepared: 5, slots: [3, 0, 0, 0, 0]}, + {level: 5, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, + {level: 6, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, + {level: 7, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, + {level: 8, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, + {level: 9, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, + {level: 10, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, + {level: 11, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, + {level: 12, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, + {level: 13, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, + {level: 14, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, + {level: 15, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, + {level: 16, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, + {level: 17, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, + {level: 18, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, + {level: 19, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, + {level: 20, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, + ], + "sorcerer": [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 4, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 2, cantrips: 4, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 3, cantrips: 4, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, + {level: 4, cantrips: 5, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, + {level: 5, cantrips: 5, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, + {level: 6, cantrips: 5, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, + {level: 7, cantrips: 5, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, + {level: 8, cantrips: 5, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, + {level: 9, cantrips: 5, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, + {level: 10, cantrips: 6, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, + {level: 11, cantrips: 6, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 12, cantrips: 6, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 13, cantrips: 6, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 14, cantrips: 6, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 15, cantrips: 6, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 16, cantrips: 6, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 17, cantrips: 6, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, + {level: 18, cantrips: 6, prepared: 20, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, + {level: 19, cantrips: 6, prepared: 21, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, + {level: 20, cantrips: 6, prepared: 22, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, + ], + "warlock": [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 2, prepared: 2, slots: [1, 0, 0, 0, 0], arcanum: {}}, + {level: 2, cantrips: 2, prepared: 3, slots: [2, 0, 0, 0, 0], arcanum: {}}, + {level: 3, cantrips: 2, prepared: 4, slots: [0, 2, 0, 0, 0], arcanum: {}}, + {level: 4, cantrips: 3, prepared: 5, slots: [0, 2, 0, 0, 0], arcanum: {}}, + {level: 5, cantrips: 3, prepared: 6, slots: [0, 0, 2, 0, 0], arcanum: {}}, + {level: 6, cantrips: 3, prepared: 7, slots: [0, 0, 2, 0, 0], arcanum: {}}, + {level: 7, cantrips: 3, prepared: 8, slots: [0, 0, 0, 2, 0], arcanum: {}}, + {level: 8, cantrips: 3, prepared: 9, slots: [0, 0, 0, 2, 0], arcanum: {}}, + {level: 9, cantrips: 3, prepared: 10, slots: [0, 0, 0, 0, 2], arcanum: {}}, + {level: 10, cantrips: 4, prepared: 10, slots: [0, 0, 0, 0, 2], arcanum: {}}, + {level: 11, cantrips: 4, prepared: 11, slots: [0, 0, 0, 0, 3], arcanum: {6: 1}}, + {level: 12, cantrips: 4, prepared: 11, slots: [0, 0, 0, 0, 3], arcanum: {6: 1}}, + {level: 13, cantrips: 4, prepared: 12, slots: [0, 0, 0, 0, 3], arcanum: {6: 1, 7: 1}}, + {level: 14, cantrips: 4, prepared: 12, slots: [0, 0, 0, 0, 3], arcanum: {6: 1, 7: 1}}, + {level: 15, cantrips: 4, prepared: 13, slots: [0, 0, 0, 0, 3], arcanum: {6: 1, 7: 1, 8: 1}}, + {level: 16, cantrips: 4, prepared: 13, slots: [0, 0, 0, 0, 3], arcanum: {6: 1, 7: 1, 8: 1}}, + {level: 17, cantrips: 4, prepared: 14, slots: [0, 0, 0, 0, 4], arcanum: {6: 1, 7: 1, 8: 1, 9: 1}}, + {level: 18, cantrips: 4, prepared: 14, slots: [0, 0, 0, 0, 4], arcanum: {6: 1, 7: 1, 8: 1, 9: 1}}, + {level: 19, cantrips: 4, prepared: 15, slots: [0, 0, 0, 0, 4], arcanum: {6: 1, 7: 1, 8: 1, 9: 1}}, + {level: 20, cantrips: 4, prepared: 15, slots: [0, 0, 0, 0, 4], arcanum: {6: 1, 7: 1, 8: 1, 9: 1}}, + ], + "wizard": [ + {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 1, cantrips: 3, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 2, cantrips: 3, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 3, cantrips: 3, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, + {level: 4, cantrips: 4, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, + {level: 5, cantrips: 4, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, + {level: 6, cantrips: 4, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, + {level: 7, cantrips: 4, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, + {level: 8, cantrips: 4, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, + {level: 9, cantrips: 4, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, + {level: 10, cantrips: 5, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, + {level: 11, cantrips: 5, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 12, cantrips: 5, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 13, cantrips: 5, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 14, cantrips: 5, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 15, cantrips: 5, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 16, cantrips: 5, prepared: 21, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 17, cantrips: 5, prepared: 22, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, + {level: 18, cantrips: 5, prepared: 23, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, + {level: 19, cantrips: 5, prepared: 24, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, + {level: 20, cantrips: 5, prepared: 26, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, + ], +} + +type SpellProgression = number[]; +const THIRD_CASTER_CANTRIPS_KNOWN: SpellProgression = [ + 0, 0, 0, // 0-2 (unused / not yet a caster) + 2, 2, 2, 2, 2, 2, 2, 3, // 3-10 + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // 11-20 +]; + +const THIRD_CASTER_SPELLS_PREPARED: SpellProgression = [ + 0, 0, 0, // 0-2 (unused / not yet a caster) + 3, 4, 4, 5, 6, 6, 7, 8, // 3-10 + 8, 9, 10, 10, 11, 11, 12, 13, 13, 13, // 11-20 +]; + +const FULL_CASTER_SLOTS: SlotProgression = [ + {level: 0, slots: []}, + {level: 1, slots: [ 2 ]}, + {level: 2, slots: [ 3 ]}, + {level: 3, slots: [ 4, 2 ]}, + {level: 4, slots: [ 4, 3 ]}, + {level: 5, slots: [ 4, 3, 2 ]}, + {level: 6, slots: [ 4, 3, 3 ]}, + {level: 7, slots: [ 4, 3, 3, 1 ]}, + {level: 8, slots: [ 4, 3, 3, 2 ]}, + {level: 9, slots: [ 4, 3, 3, 3, 1 ]}, + {level: 10, slots: [ 4, 3, 3, 3, 2 ]}, + {level: 11, slots: [ 4, 3, 3, 3, 2, 1 ]}, + {level: 12, slots: [ 4, 3, 3, 3, 2, 1 ]}, + {level: 13, slots: [ 4, 3, 3, 3, 2, 1, 1 ]}, + {level: 14, slots: [ 4, 3, 3, 3, 2, 1, 1 ]}, + {level: 15, slots: [ 4, 3, 3, 3, 2, 1, 1, 1 ]}, + {level: 16, slots: [ 4, 3, 3, 3, 2, 1, 1, 1 ]}, + {level: 17, slots: [ 4, 3, 3, 3, 2, 1, 1, 1, 1 ]}, + {level: 18, slots: [ 4, 3, 3, 3, 3, 1, 1, 1, 1 ]}, + {level: 19, slots: [ 4, 3, 3, 3, 3, 2, 1, 1, 1 ]}, + {level: 20, slots: [ 4, 3, 3, 3, 3, 2, 2, 1, 1 ]}, +]; + +const HALF_CASTER_SLOTS: SlotProgression = [ + {level: 0, slots: []}, + {level: 1, slots: [ 2 ]}, + {level: 2, slots: [ 2 ]}, + {level: 3, slots: [ 3 ]}, + {level: 4, slots: [ 3 ]}, + {level: 5, slots: [ 4, 2 ]}, + {level: 6, slots: [ 4, 2 ]}, + {level: 7, slots: [ 4, 3 ]}, + {level: 8, slots: [ 4, 3 ]}, + {level: 9, slots: [ 4, 3, 2 ]}, + {level: 10, slots: [ 4, 3, 2 ]}, + {level: 11, slots: [ 4, 3, 3 ]}, + {level: 12, slots: [ 4, 3, 3 ]}, + {level: 13, slots: [ 4, 3, 3, 1 ]}, + {level: 14, slots: [ 4, 3, 3, 1 ]}, + {level: 15, slots: [ 4, 3, 3, 2 ]}, + {level: 16, slots: [ 4, 3, 3, 2 ]}, + {level: 17, slots: [ 4, 3, 3, 3, 1 ]}, + {level: 18, slots: [ 4, 3, 3, 3, 1 ]}, + {level: 19, slots: [ 4, 3, 3, 3, 2 ]}, + {level: 20, slots: [ 4, 3, 3, 3, 2 ]}, +]; + +const THIRD_CASTER_SLOTS: SlotProgression = [ + {level: 0, slots: []}, + {level: 1, slots: []}, + {level: 2, slots: []}, + {level: 3, slots: [ 2 ]}, + {level: 4, slots: [ 3 ]}, + {level: 5, slots: [ 3 ]}, + {level: 6, slots: [ 3, 2 ]}, + {level: 7, slots: [ 4, 2 ]}, + {level: 8, slots: [ 4, 2 ]}, + {level: 9, slots: [ 4, 2 ]}, + {level: 10, slots: [ 4, 3 ]}, + {level: 11, slots: [ 4, 3 ]}, + {level: 12, slots: [ 4, 3 ]}, + {level: 13, slots: [ 4, 3, 2 ]}, + {level: 14, slots: [ 4, 3, 2 ]}, + {level: 15, slots: [ 4, 3, 2 ]}, + {level: 16, slots: [ 4, 3, 3 ]}, + {level: 17, slots: [ 4, 3, 3 ]}, + {level: 18, slots: [ 4, 3, 3 ]}, + {level: 19, slots: [ 4, 3, 3, 1 ]}, + {level: 20, slots: [ 4, 3, 3, 1 ]}, +]; + +function slotsFromProgression(progression: number[]): SpellSlotsType { + const slots: number[] = []; + + for (let level = 1; level <= 9; level++) { + const slotsAtLevel = progression[level - 1] || 0; + for (let i = 0; i < slotsAtLevel; i++) { + slots.push(level); + } + } + + return slots as SpellSlotsType; +} + +const srd52 = { + Races: SpeciesData, + RaceNames, + SubraceNames, + Classes, + ClassNames, + SubclassNames, + Backgrounds, + BackgroundNames, + SpellProgressionTables, + THIRD_CASTER_CANTRIPS_KNOWN, + THIRD_CASTER_SPELLS_PREPARED, + FULL_CASTER_SLOTS, + HALF_CASTER_SLOTS, + THIRD_CASTER_SLOTS, + + maxCantripsKnown(className: ClassNameType, level: number): number { + const classDef = this.Classes[className]; + if (!classDef.spellcasting.enabled) return 0; + + switch (className) { + case "bard": + case "sorcerer": + case "warlock": + case "cleric": + case "druid": + case "wizard": + return this.SpellProgressionTables[className]![level]?.cantrips || 0; + case "fighter": // Eldritch Knight + case "rogue": // Arcane Trickster + return this.THIRD_CASTER_CANTRIPS_KNOWN[level] || 0; + default: return 0; + } + }, + + maxSpellsPrepared(className: ClassNameType, level: number): number | null { + const classDef = this.Classes[className]; + if (!classDef.spellcasting.enabled) { + return null; // Not a "known" caster + } + + switch (className) { + case "bard": + case "sorcerer": + case "warlock": + case "cleric": + case "druid": + case "wizard": + case "paladin": + case "ranger": + return this.SpellProgressionTables[className]![level]?.prepared || 0; + case "fighter": // Eldritch Knight + case "rogue": // Arcane Trickster + return this.THIRD_CASTER_CANTRIPS_KNOWN[level] || 0; + default: return 0; + } + }, + + getSlotsFor(casterKind: CasterKindType, level: number): SpellSlotsType { + if (casterKind === "full") { + return slotsFromProgression(this.FULL_CASTER_SLOTS[level]?.slots || []); + } else if (casterKind === "half") { + return slotsFromProgression(this.HALF_CASTER_SLOTS[level]?.slots || []); + } else if (casterKind === "third") { + return slotsFromProgression(this.THIRD_CASTER_SLOTS[level]?.slots || []); + } else if (casterKind === "pact") { + const progression = this.SpellProgressionTables["warlock"]!; + return slotsFromProgression(progression[level]?.slots || []); + } else { + return [] + } + } +} + +export default srd52; diff --git a/src/services/computeCharacter.ts b/src/services/computeCharacter.ts index e3bfbbf..156641b 100644 --- a/src/services/computeCharacter.ts +++ b/src/services/computeCharacter.ts @@ -6,7 +6,7 @@ import { currentByCharacterId as getCurrentSkills } from "@src/db/char_skills"; import { getHpDelta } from "@src/db/char_hp"; import { findByCharacterId as findHitDiceChanges } from "@src/db/char_hit_dice"; import { findByCharacterId as findSpellSlotChanges } from "@src/db/char_spell_slots"; -import { Races, Classes, Skills, SkillAbilities, type SizeType, type AbilityType, type SkillType, type ProficiencyLevel, type HitDieType, Abilities, getSlotsFor, type ClassNameType, type SpellSlotsType, type SpellLevelType } from "@src/lib/dnd"; +import { getRuleset, Skills, SkillAbilities, type SizeType, type AbilityType, type SkillType, type ProficiencyLevel, type HitDieType, Abilities, type ClassNameType, type SpellSlotsType, type SpellLevelType } from "@src/lib/dnd"; import { computeSpells, type SpellInfoForClass } from "@src/services/computeSpells"; export interface CharacterClass { @@ -70,7 +70,8 @@ export async function computeCharacter(db: SQL, characterId: string): Promise sum + c.level, 0); const proficiencyBonus = Math.floor((totalLevel - 1) / 4) + 2; - const race = Races.find(r => r.name === character.race)!; + const ruleset = getRuleset(character.ruleset); + const race = ruleset.Races.find(r => r.name === character.race)!; // Calculate modifier and saving throw for each ability const calculateModifier = (score: number) => Math.floor((score - 10) / 2); @@ -123,7 +124,7 @@ export async function computeCharacter(db: SQL, characterId: string): Promise 0) { - spellSlots = getSlotsFor('full', fullCasterLevel); + spellSlots = ruleset.getSlotsFor('full', fullCasterLevel); } else if (halfCasterLevel > 0) { - spellSlots = getSlotsFor('half', halfCasterLevel); + spellSlots = ruleset.getSlotsFor('half', halfCasterLevel); } else if (thirdCasterLevel > 0) { - spellSlots = getSlotsFor('third', thirdCasterLevel); + spellSlots = ruleset.getSlotsFor('third', thirdCasterLevel); } } else if (casterTypeCount > 1) { // Multiclassing - use effective caster level with highest tier progression @@ -217,11 +218,11 @@ export async function computeCharacter(db: SQL, characterId: string): Promise 0) { - spellSlots = getSlotsFor('full', effectiveCasterLevel); + spellSlots = ruleset.getSlotsFor('full', effectiveCasterLevel); } else if (halfCasterLevel > 0) { - spellSlots = getSlotsFor('half', effectiveCasterLevel); + spellSlots = ruleset.getSlotsFor('half', effectiveCasterLevel); } else if (thirdCasterLevel > 0) { - spellSlots = getSlotsFor('third', effectiveCasterLevel); + spellSlots = ruleset.getSlotsFor('third', effectiveCasterLevel); } } @@ -253,7 +254,7 @@ export async function computeCharacter(db: SQL, characterId: string): Promise, proficiencyBonus: number, wizardSpellbookSpells: Spell[], allPreparedRecords: CharSpellPrepared[] ): Promise { - const classDef = Classes[charClass.class]; + const classDef = ruleset.Classes[charClass.class]; // Skip non-spellcasting classes if (!classDef.spellcasting.enabled) { @@ -64,10 +65,10 @@ async function computeSpellsForClass( const spellSaveDC = 8 + proficiencyBonus + abilityModifier; // Calculate max spell level this class can cast - const maxSpellLevel = getMaxSpellLevel(classDef, charClass.level); + const maxSpellLevel = getMaxSpellLevel(ruleset, classDef, charClass.level); // Calculate maximums - const maxCantrips = maxCantripsKnown(charClass.class, charClass.level); + const maxCantrips = ruleset.maxCantripsKnown(charClass.class, charClass.level); // Filter prepared records for this class const classPreparedRecords = allPreparedRecords.filter(r => r.class === charClass.class); @@ -93,7 +94,7 @@ async function computeSpellsForClass( knownSpells = classSpells.map(s => s.id); // Includes both cantrips and leveled spells } - const maxPrepared = maxSpellsPrepared(charClass.class, charClass.level) || 0; + const maxPrepared = ruleset.maxSpellsPrepared(charClass.class, charClass.level) || 0; const preparedSlots = createPreparedSlots(leveledSpellRecords, maxPrepared); return { @@ -155,6 +156,7 @@ function createPreparedSlots( */ export async function computeSpells( db: SQL, + ruleset: any, characterId: string, classes: CharacterClass[], abilityScores: Record, @@ -174,6 +176,7 @@ export async function computeSpells( for (const charClass of classes) { const classSpellInfo = await computeSpellsForClass( + ruleset, charClass, abilityScores, proficiencyBonus, @@ -193,12 +196,12 @@ export async function computeSpells( * Get the highest spell level a character can cast based on their class and level * Uses the slot progression tables from dnd.ts */ -export function getMaxSpellLevel(classDef: ClassDef, classLevel: number): number { +export function getMaxSpellLevel(ruleset: any, classDef: ClassDef, classLevel: number): number { if (!classDef.spellcasting.enabled) { return 0; } // For other casters, get slots and find highest level with slots > 0 - const slots = getSlotsFor(classDef.spellcasting.kind, classLevel); + const slots = ruleset.getSlotsFor(classDef.spellcasting.kind, classLevel); return Math.max(...slots) } From a33f8005c4e91ae37bbae3796bc51aa418c63c20 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:02:05 +0000 Subject: [PATCH 2/2] feat: Refactor to support SRD 5.1 and SRD 5.2 rulesets This refactors the D&D character management to support both SRD 5.1 and SRD 5.2 rulesets. - Adds a `ruleset` field to the `Character` schema and a corresponding database migration. - Splits the D&D data into `srd51.ts` and `srd52.ts` files. - Refactors `src/lib/dnd.ts` to be a core rules file with a `getRuleset` function to dynamically load the correct ruleset. - Updates character computation and services to be ruleset-aware, using the dynamically loaded ruleset data. - Corrects spell preparation calculations for SRD 5.1 and cleans up unused imports and data inconsistencies. --- ...51014115526_add_ruleset_to_characters.sql} | 0 src/db/characters.ts | 8 +- src/lib/dnd.ts | 14 +- src/lib/dnd/srd51.ts | 312 ++++++++---------- src/lib/dnd/srd52.ts | 135 +++----- src/services/computeCharacter.ts | 6 +- src/services/computeSpells.ts | 2 +- 7 files changed, 194 insertions(+), 283 deletions(-) rename migrations/{20251014093140_add_ruleset_to_characters.sql => 20251014115526_add_ruleset_to_characters.sql} (100%) diff --git a/migrations/20251014093140_add_ruleset_to_characters.sql b/migrations/20251014115526_add_ruleset_to_characters.sql similarity index 100% rename from migrations/20251014093140_add_ruleset_to_characters.sql rename to migrations/20251014115526_add_ruleset_to_characters.sql diff --git a/src/db/characters.ts b/src/db/characters.ts index 4ad6999..b8207db 100644 --- a/src/db/characters.ts +++ b/src/db/characters.ts @@ -2,15 +2,15 @@ import { ulid } from "ulid"; import { z } from "zod"; import type { SQL } from "bun"; -import { BackgroundNamesSchema, RaceNamesSchema, SubraceNames } from "@src/lib/dnd" +import { z } from "zod"; export const CharacterSchema = z.object({ id: z.string(), user_id: z.string(), name: z.string().min(3), - race: RaceNamesSchema, - subrace: z.enum(SubraceNames).nullable().default(null), - background: BackgroundNamesSchema, + race: z.string(), + subrace: z.string().nullable().default(null), + background: z.string(), alignment: z.nullish(z.string()), ruleset: z.enum(["srd51", "srd52"]).default("srd51"), created_at: z.date(), diff --git a/src/lib/dnd.ts b/src/lib/dnd.ts index b592367..2ca7e37 100644 --- a/src/lib/dnd.ts +++ b/src/lib/dnd.ts @@ -48,18 +48,18 @@ export type AbilityScoreModifiers = { [key in AbilityType]?: number; } -export interface Subrace { +export interface Lineage { name: string; ability_score_modifiers?: AbilityScoreModifiers; } -export interface Race { +export interface Species { name: string; size: SizeType; speed: number; ability_score_modifiers?: AbilityScoreModifiers; - subraces?: Subrace[]; - variants?: Subrace[]; + lineages?: Lineage[]; + variants?: Lineage[]; } @@ -107,11 +107,7 @@ export interface ClassDef { notes?: string; } -export const BackgroundNames = [ - "acolyte", "charlatan", "criminal", "entertainer", "folk hero", "guild artisan", "hermit", "noble", "outlander", "sage", "sailor", "soldier", "urchin", "pirate", -] as const; -export const BackgroundNamesSchema = z.enum(BackgroundNames); -export type BackgroundNameType = z.infer; +export type BackgroundNameType = string; export interface BackgroundFeature { name: string; diff --git a/src/lib/dnd/srd51.ts b/src/lib/dnd/srd51.ts index d72c383..7655fdf 100644 --- a/src/lib/dnd/srd51.ts +++ b/src/lib/dnd/srd51.ts @@ -1,72 +1,31 @@ -import { z } from "zod"; import type { AbilityScoreModifiers, AbilityType, Background, - BackgroundFeature, - BackgroundNameType, CasterKindType, Choice, ClassDef, ClassNameType, HitDieType, - Race, + Species, SizeType, SkillType, SlotProgression, SpellSlotsType, - Subrace, + Lineage, SpellcastingInfo, - SpellChangeEventType + SpellChangeEventType, + BackgroundNameType as CoreBackgroundNameType, } from "../dnd"; -const GamingSets = [ - "dice set", - "playing card set", -] as const; - -const ArtisanTools = [ - "alchemist’s supplies", - "brewer’s supplies", - "calligrapher’s supplies", - "carpenter’s tools", - "cartographer’s tools", - "cobbler’s tools", - "cook’s utensils", - "glassblower’s tools", - "jeweler’s tools", - "leatherworker’s tools", - "mason’s tools", - "painter’s supplies", - "potter’s tools", - "smith’s tools", - "tinker’s tools", - "weaver’s tools", - "woodcarver’s tools", -] as const; - -const Instruments = [ - "bagpipes", - "drum", - "dulcimer", - "flute", - "lute", - "lyre", - "horn", - "pan flute", - "shawm", - "viol", -] as const; - - -const Races: Race[] = [ +const SpeciesData: Species[] = [ { name: "dwarf", size: "medium", speed: 25, ability_score_modifiers: { constitution: 2 }, - subraces: [ + lineages: [ { name: "hill dwarf", ability_score_modifiers: { wisdom: 1 } }, { name: "mountain dwarf", ability_score_modifiers: { strength: 2 } } ] @@ -76,7 +35,7 @@ const Races: Race[] = [ size: "medium", speed: 30, ability_score_modifiers: { dexterity: 2 }, - subraces: [ + lineages: [ { name: "high elf", ability_score_modifiers: { intelligence: 1 } }, { name: "wood elf", ability_score_modifiers: { wisdom: 1 } }, { name: "drow", ability_score_modifiers: { charisma: 1 } } @@ -87,7 +46,7 @@ const Races: Race[] = [ size: "small", speed: 25, ability_score_modifiers: { dexterity: 2 }, - subraces: [ + lineages: [ { name: "lightfoot", ability_score_modifiers: { charisma: 1 } }, { name: "stout", ability_score_modifiers: { constitution: 1 } } ] @@ -126,7 +85,7 @@ const Races: Race[] = [ size: "small", speed: 25, ability_score_modifiers: { intelligence: 2 }, - subraces: [ + lineages: [ { name: "forest gnome", ability_score_modifiers: { dexterity: 1 } }, { name: "rock gnome", ability_score_modifiers: { constitution: 1 } }, { name: "deep gnome", ability_score_modifiers: { dexterity: 1 } }, @@ -154,8 +113,8 @@ const Races: Race[] = [ ability_score_modifiers: { charisma: 2, intelligence: 1 } } ] as const; -const RaceNames = Races.map(c => c.name); -const SubraceNames = Races.flatMap(r => r.subraces ? r.subraces.map(sr => sr.name) : []); +const RaceNames = SpeciesData.map(c => c.name); +const SubraceNames = SpeciesData.flatMap(r => r.lineages ? r.lineages.map(sr => sr.name) : []); const ClassNames = ["barbarian", "bard", "cleric", "druid", "fighter", "monk", "paladin", "ranger", "rogue", "sorcerer", "warlock", "wizard"] as const; @@ -333,6 +292,8 @@ const SubclassNames = Object.values(Classes).flatMap(c => c.subclasses ? c.subcl const BackgroundNames = [ "acolyte", "charlatan", "criminal", "entertainer", "folk hero", "guild artisan", "hermit", "noble", "outlander", "sage", "sailor", "soldier", "urchin", "pirate", ] as const; +export type BackgroundNameType = typeof BackgroundNames[number]; + const Backgrounds: Record = { acolyte: { @@ -359,7 +320,7 @@ const Backgrounds: Record = { name: "criminal", skillProficiencies: ["deception", "stealth"], toolProficiencies: [ - { choose: 1, from: [...GamingSets] as unknown as string[] }, + { choose: 1, from: ["dice set", "playing card set"] }, "thieves’ tools", ], equipment: ["crowbar", "dark common clothes with hood", "15 gp"], @@ -372,7 +333,7 @@ const Backgrounds: Record = { name: "entertainer", skillProficiencies: ["acrobatics", "performance"], toolProficiencies: [ - { choose: 1, from: [...Instruments] as unknown as string[] }, + { choose: 1, from: ["bagpipes", "drum", "dulcimer", "flute", "lute", "lyre", "horn", "pan flute", "shawm", "viol"] }, "disguise kit", ], equipment: ["musical instrument (one of your choice)", "favor of an admirer", "costume", "15 gp"], @@ -385,7 +346,7 @@ const Backgrounds: Record = { name: "folk hero", skillProficiencies: ["animal handling", "survival"], toolProficiencies: [ - { choose: 1, from: [...ArtisanTools] as unknown as string[] }, + { choose: 1, from: ["alchemist’s supplies", "brewer’s supplies", "calligrapher’s supplies", "carpenter’s tools", "cartographer’s tools", "cobbler’s tools", "cook’s utensils", "glassblower’s tools", "jeweler’s tools", "leatherworker’s tools", "mason’s tools", "painter’s supplies", "potter’s tools", "smith’s tools", "tinker’s tools", "weaver’s tools", "woodcarver’s tools"] }, "vehicles (land)", ], equipment: ["artisan’s tools (one of your choice)", "shovel", "iron pot", "common clothes", "10 gp"], @@ -397,7 +358,7 @@ const Backgrounds: Record = { "guild artisan": { name: "guild artisan", skillProficiencies: ["insight", "persuasion"], - toolProficiencies: [{ choose: 1, from: [...ArtisanTools] as unknown as string[] }], + toolProficiencies: [{ choose: 1, from: ["alchemist’s supplies", "brewer’s supplies", "calligrapher’s supplies", "carpenter’s tools", "cartographer’s tools", "cobbler’s tools", "cook’s utensils", "glassblower’s tools", "jeweler’s tools", "leatherworker’s tools", "mason’s tools", "painter’s supplies", "potter’s tools", "smith’s tools", "tinker’s tools", "weaver’s tools", "woodcarver’s tools"] }], languageProficiencies: 1, equipment: ["artisan’s tools (one of your choice)", "letter of introduction from your guild", "traveler’s clothes", "15 gp"], feature: { @@ -419,7 +380,7 @@ const Backgrounds: Record = { noble: { name: "noble", skillProficiencies: ["history", "persuasion"], - toolProficiencies: [{ choose: 1, from: [...GamingSets] as unknown as string[] }], + toolProficiencies: [{ choose: 1, from: ["dice set", "playing card set"] }], languageProficiencies: 1, equipment: ["fine clothes", "signet ring", "scroll of pedigree", "25 gp"], feature: { @@ -430,7 +391,7 @@ const Backgrounds: Record = { outlander: { name: "outlander", skillProficiencies: ["athletics", "survival"], - toolProficiencies: [{ choose: 1, from: [...Instruments] as unknown as string[] }], + toolProficiencies: [{ choose: 1, from: ["bagpipes", "drum", "dulcimer", "flute", "lute", "lyre", "horn", "pan flute", "shawm", "viol"] }], languageProficiencies: 1, equipment: ["staff", "hunting trap", "trophy from an animal", "traveler’s clothes", "10 gp"], feature: { @@ -471,7 +432,7 @@ const Backgrounds: Record = { soldier: { name: "soldier", skillProficiencies: ["athletics", "intimidation"], - toolProficiencies: [{ choose: 1, from: [...GamingSets] as unknown as string[] }, "vehicles (land)"], + toolProficiencies: [{ choose: 1, from: ["dice set", "playing card set"] }, "vehicles (land)"], equipment: ["insignia of rank", "trophy from a fallen enemy", "bone dice or deck of cards", "common clothes", "10 gp"], feature: { name: "military rank", @@ -493,7 +454,8 @@ const Backgrounds: Record = { type SpellProgressionTableRow = { level: number; cantrips: number; - prepared: number; + prepared?: number; // Spells known for "known" casters + prepared_fn?: (level: number, ability_modifier: number) => number; slots: number[]; // 1st to 9th level slots arcanum?: Record; // warlock-only } @@ -524,95 +486,95 @@ const SpellProgressionTables: Partial mod + level, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 2, cantrips: 3, prepared_fn: (level, mod) => mod + level, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 3, cantrips: 3, prepared_fn: (level, mod) => mod + level, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, + {level: 4, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, + {level: 5, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, + {level: 6, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, + {level: 7, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, + {level: 8, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, + {level: 9, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, + {level: 10, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, + {level: 11, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 12, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 13, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 14, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 15, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 16, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 17, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, + {level: 18, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, + {level: 19, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, + {level: 20, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, ], "druid": [ {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 1, cantrips: 2, prepared: 4, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 2, cantrips: 2, prepared: 5, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 3, cantrips: 2, prepared: 6, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, - {level: 4, cantrips: 3, prepared: 7, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, - {level: 5, cantrips: 3, prepared: 9, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, - {level: 6, cantrips: 3, prepared: 10, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, - {level: 7, cantrips: 3, prepared: 11, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, - {level: 8, cantrips: 3, prepared: 12, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, - {level: 9, cantrips: 3, prepared: 14, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, - {level: 10, cantrips: 4, prepared: 15, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, - {level: 11, cantrips: 4, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, - {level: 12, cantrips: 4, prepared: 16, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, - {level: 13, cantrips: 4, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, - {level: 14, cantrips: 4, prepared: 17, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, - {level: 15, cantrips: 4, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, - {level: 16, cantrips: 4, prepared: 18, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, - {level: 17, cantrips: 4, prepared: 19, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, - {level: 18, cantrips: 4, prepared: 20, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, - {level: 19, cantrips: 4, prepared: 21, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, - {level: 20, cantrips: 4, prepared: 22, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, + {level: 1, cantrips: 2, prepared_fn: (level, mod) => mod + level, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 2, cantrips: 2, prepared_fn: (level, mod) => mod + level, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 3, cantrips: 2, prepared_fn: (level, mod) => mod + level, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, + {level: 4, cantrips: 3, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, + {level: 5, cantrips: 3, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, + {level: 6, cantrips: 3, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, + {level: 7, cantrips: 3, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, + {level: 8, cantrips: 3, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, + {level: 9, cantrips: 3, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, + {level: 10, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, + {level: 11, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 12, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 13, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 14, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 15, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 16, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 17, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, + {level: 18, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, + {level: 19, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, + {level: 20, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, ], "paladin": [ {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 1, cantrips: 0, prepared: 2, slots: [2, 0, 0, 0, 0]}, - {level: 2, cantrips: 0, prepared: 3, slots: [2, 0, 0, 0, 0]}, - {level: 3, cantrips: 0, prepared: 4, slots: [3, 0, 0, 0, 0]}, - {level: 4, cantrips: 0, prepared: 5, slots: [3, 0, 0, 0, 0]}, - {level: 5, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, - {level: 6, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, - {level: 7, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, - {level: 8, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, - {level: 9, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, - {level: 10, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, - {level: 11, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, - {level: 12, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, - {level: 13, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, - {level: 14, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, - {level: 15, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, - {level: 16, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, - {level: 17, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, - {level: 18, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, - {level: 19, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, - {level: 20, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, + {level: 1, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [2, 0, 0, 0, 0]}, + {level: 2, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [2, 0, 0, 0, 0]}, + {level: 3, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [3, 0, 0, 0, 0]}, + {level: 4, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [3, 0, 0, 0, 0]}, + {level: 5, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 2, 0, 0, 0]}, + {level: 6, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 2, 0, 0, 0]}, + {level: 7, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 0, 0, 0]}, + {level: 8, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 0, 0, 0]}, + {level: 9, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 2, 0, 0]}, + {level: 10, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 2, 0, 0]}, + {level: 11, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 0, 0]}, + {level: 12, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 0, 0]}, + {level: 13, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 1, 0]}, + {level: 14, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 1, 0]}, + {level: 15, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 2, 0]}, + {level: 16, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 2, 0]}, + {level: 17, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 3, 1]}, + {level: 18, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 3, 1]}, + {level: 19, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 3, 2]}, + {level: 20, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 3, 2]}, ], "ranger": [ {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, - {level: 1, cantrips: 0, prepared: 2, slots: [2, 0, 0, 0, 0]}, - {level: 2, cantrips: 0, prepared: 3, slots: [2, 0, 0, 0, 0]}, - {level: 3, cantrips: 0, prepared: 4, slots: [3, 0, 0, 0, 0]}, - {level: 4, cantrips: 0, prepared: 5, slots: [3, 0, 0, 0, 0]}, - {level: 5, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, - {level: 6, cantrips: 0, prepared: 6, slots: [4, 2, 0, 0, 0]}, - {level: 7, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, - {level: 8, cantrips: 0, prepared: 7, slots: [4, 3, 0, 0, 0]}, - {level: 9, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, - {level: 10, cantrips: 0, prepared: 9, slots: [4, 3, 2, 0, 0]}, - {level: 11, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, - {level: 12, cantrips: 0, prepared: 10, slots: [4, 3, 3, 0, 0]}, - {level: 13, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, - {level: 14, cantrips: 0, prepared: 11, slots: [4, 3, 3, 1, 0]}, - {level: 15, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, - {level: 16, cantrips: 0, prepared: 12, slots: [4, 3, 3, 2, 0]}, - {level: 17, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, - {level: 18, cantrips: 0, prepared: 14, slots: [4, 3, 3, 3, 1]}, - {level: 19, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, - {level: 20, cantrips: 0, prepared: 15, slots: [4, 3, 3, 3, 2]}, + {level: 1, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [2, 0, 0, 0, 0]}, + {level: 2, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [2, 0, 0, 0, 0]}, + {level: 3, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [3, 0, 0, 0, 0]}, + {level: 4, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [3, 0, 0, 0, 0]}, + {level: 5, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 2, 0, 0, 0]}, + {level: 6, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 2, 0, 0, 0]}, + {level: 7, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 0, 0, 0]}, + {level: 8, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 0, 0, 0]}, + {level: 9, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 2, 0, 0]}, + {level: 10, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 2, 0, 0]}, + {level: 11, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 0, 0]}, + {level: 12, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 0, 0]}, + {level: 13, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 1, 0]}, + {level: 14, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 1, 0]}, + {level: 15, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 2, 0]}, + {level: 16, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 2, 0]}, + {level: 17, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 3, 1]}, + {level: 18, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 3, 1]}, + {level: 19, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 3, 2]}, + {level: 20, cantrips: 0, prepared_fn: (level, mod) => mod + Math.floor(level / 2), slots: [4, 3, 3, 3, 2]}, ], "sorcerer": [ {level: 0, cantrips: 0, prepared: 0, slots: [0, 0, 0, 0, 0, 0, 0, 0, 0]}, @@ -662,26 +624,26 @@ const SpellProgressionTables: Partial mod + level, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 2, cantrips: 3, prepared_fn: (level, mod) => mod + level, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 3, cantrips: 3, prepared_fn: (level, mod) => mod + level, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, + {level: 4, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, + {level: 5, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, + {level: 6, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, + {level: 7, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, + {level: 8, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, + {level: 9, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, + {level: 10, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, + {level: 11, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 12, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 13, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 14, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 15, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 16, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 17, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, + {level: 18, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, + {level: 19, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, + {level: 20, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, ], } @@ -784,7 +746,7 @@ function slotsFromProgression(progression: number[]): SpellSlotsType { } const srd51 = { - Races, + Races: SpeciesData, RaceNames, SubraceNames, Classes, @@ -818,27 +780,25 @@ const srd51 = { } }, - maxSpellsPrepared(className: ClassNameType, level: number): number | null { + maxSpellsPrepared(className: ClassNameType, level: number, abilityModifier: number): number | null { const classDef = this.Classes[className]; if (!classDef.spellcasting.enabled) { return null; // Not a "known" caster } - switch (className) { - case "bard": - case "sorcerer": - case "warlock": - case "cleric": - case "druid": - case "wizard": - case "paladin": - case "ranger": - return this.SpellProgressionTables[className]![level]?.prepared || 0; - case "fighter": // Eldritch Knight - case "rogue": // Arcane Trickster - return this.THIRD_CASTER_CANTRIPS_KNOWN[level] || 0; - default: return 0; + const progression = this.SpellProgressionTables[className]; + if (!progression || !progression[level]) { + return 0; } + + const entry = progression[level]; + if (entry.prepared) { + return entry.prepared; + } else if (entry.prepared_fn) { + return entry.prepared_fn(level, abilityModifier); + } + + return 0; }, getSlotsFor(casterKind: CasterKindType, level: number): SpellSlotsType { diff --git a/src/lib/dnd/srd52.ts b/src/lib/dnd/srd52.ts index 53067c3..1f011eb 100644 --- a/src/lib/dnd/srd52.ts +++ b/src/lib/dnd/srd52.ts @@ -1,66 +1,24 @@ -import { z } from "zod"; import type { AbilityScoreModifiers, AbilityType, Background, - BackgroundFeature, - BackgroundNameType, CasterKindType, Choice, ClassDef, ClassNameType, HitDieType, - Race, + Species, SizeType, SkillType, SlotProgression, SpellSlotsType, - Subrace, + Lineage, SpellcastingInfo, - SpellChangeEventType + SpellChangeEventType, + BackgroundNameType as CoreBackgroundNameType, } from "../dnd"; - -const GamingSets = [ - "dice set", - "playing card set", -] as const; - -const ArtisanTools = [ - "alchemist’s supplies", - "brewer’s supplies", - "calligrapher’s supplies", - "carpenter’s tools", - "cartographer’s tools", - "cobbler’s tools", - "cook’s utensils", - "glassblower’s tools", - "jeweler’s tools", - "leatherworker’s tools", - "mason’s tools", - "painter’s supplies", - "potter’s tools", - "smith’s tools", - "tinker’s tools", - "weaver’s tools", - "woodcarver’s tools", -] as const; - -const Instruments = [ - "bagpipes", - "drum", - "dulcimer", - "flute", - "lute", - "lyre", - "horn", - "pan flute", - "shawm", - "viol", -] as const; - - -const SpeciesData: Race[] = [ +const SpeciesData: Species[] = [ { name: "dragonborn", size: "medium", @@ -75,7 +33,7 @@ const SpeciesData: Race[] = [ name: "elf", size: "medium", speed: 30, - subraces: [ + lineages: [ { name: "drow" }, { name: "high elf" }, { name: "wood elf" }, @@ -85,7 +43,7 @@ const SpeciesData: Race[] = [ name: "gnome", size: "small", speed: 30, - subraces: [ + lineages: [ { name: "forest gnome" }, { name: "rock gnome" }, ] @@ -116,8 +74,8 @@ const SpeciesData: Race[] = [ speed: 30, } ] as const; -const RaceNames = SpeciesData.map(c => c.name); -const SubraceNames = SpeciesData.flatMap(r => r.subraces ? r.subraces.map(sr => sr.name) : []); +const SpeciesNames = SpeciesData.map(c => c.name); +const LineageNames = SpeciesData.flatMap(r => r.lineages ? r.lineages.map(sr => sr.name) : []); const ClassNames = ["barbarian", "bard", "cleric", "druid", "fighter", "monk", "paladin", "ranger", "rogue", "sorcerer", "warlock", "wizard"] as const; @@ -295,6 +253,7 @@ const SubclassNames = Object.values(Classes).flatMap(c => c.subclasses ? c.subcl const BackgroundNames = [ "acolyte", "criminal", "sage", "soldier" ] as const; +export type BackgroundNameType = typeof BackgroundNames[number]; const Backgrounds: Record = { acolyte: { @@ -335,7 +294,7 @@ const Backgrounds: Record = { soldier: { name: "soldier", skillProficiencies: ["athletics", "intimidation"], - toolProficiencies: [{ choose: 1, from: [...GamingSets] as unknown as string[] }], + toolProficiencies: [{ choose: 1, from: ["dice set", "playing card set"] }], feat: "Savage Attacker", ability_scores: ["strength", "dexterity", "constitution"], equipment: ["Spear", "Shortbow", "20 Arrows", "Gaming Set", "Healer’s Kit", "Quiver", "Traveler’s Clothes", "14 GP"], @@ -349,7 +308,7 @@ const Backgrounds: Record = { type SpellProgressionTableRow = { level: number; cantrips: number; - prepared: number; + prepared?: number; slots: number[]; // 1st to 9th level slots arcanum?: Record; // warlock-only } @@ -518,26 +477,26 @@ const SpellProgressionTables: Partial mod + level, slots: [2, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 2, cantrips: 3, prepared_fn: (level, mod) => mod + level, slots: [3, 0, 0, 0, 0, 0, 0, 0, 0]}, + {level: 3, cantrips: 3, prepared_fn: (level, mod) => mod + level, slots: [4, 2, 0, 0, 0, 0, 0, 0, 0]}, + {level: 4, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 0, 0, 0, 0, 0, 0, 0]}, + {level: 5, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 2, 0, 0, 0, 0, 0, 0]}, + {level: 6, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 0, 0, 0, 0, 0, 0]}, + {level: 7, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 1, 0, 0, 0, 0, 0]}, + {level: 8, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 2, 0, 0, 0, 0, 0]}, + {level: 9, cantrips: 4, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 1, 0, 0, 0, 0]}, + {level: 10, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 0, 0, 0, 0]}, + {level: 11, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 12, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 0, 0, 0]}, + {level: 13, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 14, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 0, 0]}, + {level: 15, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 16, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 1, 0]}, + {level: 17, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 2, 1, 1, 1, 1]}, + {level: 18, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 3, 1, 1, 1, 1]}, + {level: 19, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 3, 2, 1, 1, 1]}, + {level: 20, cantrips: 5, prepared_fn: (level, mod) => mod + level, slots: [4, 3, 3, 3, 3, 2, 2, 1, 1]}, ], } @@ -641,8 +600,8 @@ function slotsFromProgression(progression: number[]): SpellSlotsType { const srd52 = { Races: SpeciesData, - RaceNames, - SubraceNames, + RaceNames: SpeciesNames, + SubraceNames: LineageNames, Classes, ClassNames, SubclassNames, @@ -674,27 +633,23 @@ const srd52 = { } }, - maxSpellsPrepared(className: ClassNameType, level: number): number | null { + maxSpellsPrepared(className: ClassNameType, level: number, abilityModifier: number): number | null { const classDef = this.Classes[className]; if (!classDef.spellcasting.enabled) { return null; // Not a "known" caster } - switch (className) { - case "bard": - case "sorcerer": - case "warlock": - case "cleric": - case "druid": - case "wizard": - case "paladin": - case "ranger": - return this.SpellProgressionTables[className]![level]?.prepared || 0; - case "fighter": // Eldritch Knight - case "rogue": // Arcane Trickster - return this.THIRD_CASTER_CANTRIPS_KNOWN[level] || 0; - default: return 0; + const progression = this.SpellProgressionTables[className]; + if (!progression || !progression[level]) { + return 0; + } + + const entry = progression[level]; + if (entry.prepared) { + return entry.prepared; } + + return 0; }, getSlotsFor(casterKind: CasterKindType, level: number): SpellSlotsType { diff --git a/src/services/computeCharacter.ts b/src/services/computeCharacter.ts index 156641b..31e11d8 100644 --- a/src/services/computeCharacter.ts +++ b/src/services/computeCharacter.ts @@ -71,7 +71,7 @@ export async function computeCharacter(db: SQL, characterId: string): Promise r.name === character.race)!; + const species = ruleset.Races.find(r => r.name === character.race)!; // Calculate modifier and saving throw for each ability const calculateModifier = (score: number) => Math.floor((score - 10) / 2); @@ -260,8 +260,8 @@ export async function computeCharacter(db: SQL, characterId: string): Promise s.id); // Includes both cantrips and leveled spells } - const maxPrepared = ruleset.maxSpellsPrepared(charClass.class, charClass.level) || 0; + const maxPrepared = ruleset.maxSpellsPrepared(charClass.class, charClass.level, abilityScores[ability].modifier) || 0; const preparedSlots = createPreparedSlots(leveledSpellRecords, maxPrepared); return {