Skip to content
3 Death Saves

Wizard Spellbook Management

Wizards maintain a dedicated spellbook alongside the regular spell-preparation workflow. A spell must be learned into the spellbook before it can be prepared or cast. Wizard handling applies to the class whose identifier is wizard and to any class with Force Wizard Mode enabled in Class Rules.

Each wizard-enabled class is treated independently: its own Learn tab, journal-backed spellbook, copied-spell metadata flag, and counters.


Wizard Learn tab with spellbook entries and scroll sources

A Learn tab is rendered for every wizard-enabled class on the actor, alongside that class’s regular Prepare tab. The tab label reads “<Class> Spell Book” and carries the module icon.

The tab lists:

  • Spells from the class’s assigned spell list not yet in the spellbook.
  • Eligible spell scrolls detected in the actor’s inventory (see Scroll Scanner).

Spells above the actor’s maximum castable level for that class are filtered out.

The sidebar carries an Affordable Only filter that hides spells the character cannot pay for. It appears only while spell learning costs are deducted.

Each spell row on the Learn tab resolves to one of three states:

StateControlBehavior
Available from the spell listLearn buttonFree copies are added immediately. Paid copies open a confirmation dialog quoting the gold cost and time from WizardBook.getCopyingCost() and WizardBook.getCopyingMinutes().
Available from a scrollLearn button with scroll iconCalls WizardBook.learnFromScroll. While Consume Scrolls When Learning is enabled, the scroll’s quantity is decremented; the scroll is deleted when quantity hits zero.
Already in the spellbookSource-labeled badgeShows a source icon and label (see Learning Sources). Hovering reveals an eraser-icon Unlearn Spell affordance; clicking removes the spell from the spellbook immediately, with no confirmation prompt.

The Learn tab footer displays:

  • current/max Spells. Current is the spellbook’s total learned count, max is getMaxSpellsAllowed().
  • N Free. Remaining free-pool slots from getRemainingFreeSpells(). When this reaches zero, any further non-scroll copies require gold and time.
  • The character’s total wealth, refreshed alongside the counters.

Clicking Learn queries WizardBook.getCopyingCost(actor, classId, spell):

  • If the result is free (cantrip or free pool not yet depleted), the spell is added directly without a dialog.
  • Otherwise a confirmation dialog shows the gold cost and time. On confirm, if Deduct Spell Learning Costs is enabled, the gold is deducted through the system’s currency manager, and only then is the spell added with source copied.

While Deduct Spell Learning Costs is enabled, every learnable spell shows its copying cost beside the Learn button, a free spell shows a free badge instead, and a spell the character cannot pay for shows its cost in a red state with its Learn button disabled and the shortfall as the button tooltip. With that setting off, no cost badge is shown.

Scroll rows route to WizardBook.learnFromScroll. The spell is added with source scroll and consumes no free-pool slot. Enabling Charge Cost for Scroll Copying applies the normal copying cost to a scroll copy. While Consume Scrolls When Learning is on, the scroll item’s quantity is decremented, and the item is deleted when quantity would drop below 1.

The in-spellbook eraser-icon action calls WizardBook.removeSpellFromSpellbook(actor, classId, uuid):

  • Removes the UUID from the spellbook journal page’s system.spells Set.
  • Removes the matching metadata entry from the wizardCopiedSpells_<classId> actor flag.

cost = spellLevel * spellLearningCostMultiplier

Cantrips and free copies always cost 0 GP.

minutes = spellLevel * spellLearningTimeMultiplier

Cantrips always take 1 minute. Copying time is displayed in the largest whole unit that fits the duration.

Both multipliers are configurable per class in Class Rules.


max = startingSpells + max(0, classLevel - 1) * spellsPerLevel

startingSpells and spellsPerLevel are configurable per class in Class Rules. Class level reads from actor.spellcastingClasses[classId].system.levels.

A spell occupies a free slot unless it was learned via copied or scroll, recorded in the wizardCopiedSpells_<classId> flag.

  • Used free = spellbook entries with no matching entry in the copied-spells flag.
  • Remaining free = max(0, capacity − used).

Cantrips are always treated as free regardless of remaining pool.

getMaxSpellsAllowed() returns the same formula and defines both the free-pool ceiling and the total spellbook capacity shown in the footer.


Each wizard-enabled class gets a dedicated JournalEntry in the spell-book.custom-spell-lists compendium pack, placed in the Actor Spellbooks folder. Access goes through WizardBook._getOrCreateSpellbookJournal(actor, classId).

The journal has one page of type spells; its system.spells Set holds the learned UUIDs.

LocationValue
Journal flags (spell-book)actorId, classIdentifier, isActorSpellbook: true, creationDate
Page system.identifier<slug(actor.name)>-<classId>-spellbook
Page system.type'actor-spellbook'
OwnershipMirrors the actor’s owners: everyone with ownership level 3 retains ownership on the journal and page.

Journal name is the actor’s name for the default wizard class, or <actor> (<className>) for other wizard-enabled classes.

Spells learned as copied or scroll append an entry to the actor.flags.spell-book.wizardCopiedSpells_<classId> flag:

FieldDescription
spellUuidSpell UUID
dateCopiedDate.now() timestamp
costGold paid (0 for scroll)
timeSpentDuration of the copy, in minutes
fromScrolltrue for scroll learns, false for paid copies

Unlearning removes the entry. Spells learned via free have no flag entry; absence of an entry is what marks a spell as a free-pool occupant. The initial and levelUp source constants are defined but produce no flag entry and are currently unused in any write path.


With GM Approval for Spell Copying enabled, a player’s copy attempt from the class spell list is stored on the character as a pending request instead of being applied. Scroll-based learning bypasses this check entirely, even when Charge Cost for Scroll Copying applies a gold cost to it. See Spell Copy Approval for the GM side of the workflow.


Every spell in the spellbook resolves to one of the following sources, defined in WIZARD_SPELL_SOURCE:

SourceMeaningBadge labelIcon
initialDefined constant; no grant logic is currently implementedLevel-upbook
freeCopied from the class spell list while the free pool still had capacityLevel-upbook
copiedPaid gold-copy from the class spell listPurchasedbook
scrollLearned by consuming/reading a scrollScrollscroll
levelUpDefined constant; no grant logic is currently implementedLevel-upbook

WizardBook.getSpellLearningSource() infers from the wizardCopiedSpells_<classId> flag: entries with fromScroll: true resolve to scroll, other entries to copied, and missing entries fall back to free. initial and levelUp produce no flag entry and are therefore indistinguishable from free at runtime. The badge template checks the source only against scroll; every other source renders the same book icon, distinguished instead by badge color and label text.


scanForScrollSpells(actor, maxSpellLevel) in scripts/data/scroll-processor.mjs builds the scroll section of the Learn tab.

  1. Filters actor.items to entries with type === 'consumable' and system.type.value === 'scroll'.
  2. For each scroll, extracts a spell UUID from, in order:
    • Any activity with activity.spell.uuid.
    • Any activity effect reference whose corresponding effect has an origin UUID.
    • Fallback: parse the scroll name ("Spell Scroll: <Name>") and search item packs for a spell matching that name, preferring system.level === scroll.flags.dnd5e.spellLevel.base.
  3. Rejects spells above maxSpellLevel. Cantrips always pass.

Scrolls whose contained spell UUID is already in the wizard’s spellbook are excluded from the Learn tab; the spell is already known.


Set per-class ritual behavior in Class Rules under Ritual Casting. Three modes:

ModeLabelBehavior
noneNo Ritual CastingRituals are prepared like any other spell.
preparedCast Rituals Only If PreparedRituals can be cast without consuming a slot, but only while prepared.
alwaysCast Any Known RitualOn save, spells in the wizard’s spellbook flagged as ritual are auto-injected into the actor as ritual-mode items (system.method: 'ritual', system.prepared: 0), even when not prepared.

SpellManager marks each auto-injected ritual item with an isModuleRitual item flag, which identifies module-managed ritual copies on subsequent saves. On each save:

  • Items flagged from a previous mode that no longer apply are cleaned up.
  • Current ritual spells from the spellbook are inserted or refreshed.

Switching a class away from always clears the ritual items it generated.

See Ruleset Types and Meanings for the full ruleset table.


WizardBook._ensureFlagsInitialized runs the first time a wizard-enabled class is processed for an actor. It creates an empty wizardCopiedSpells_<classId> flag array on the actor if one is not already present. It does not add spell UUIDs to the spellbook and does not reference WIZARD_DEFAULTS.STARTING_SPELLS.

WIZARD_SPELL_SOURCE.INITIAL and WIZARD_SPELL_SOURCE.LEVEL_UP are defined constants with no corresponding write path; no automatic grant of initial or level-up spells is implemented. The free-pool capacity formula in getMaxSpellsAllowed() uses startingSpells and spellsPerLevel as ceiling inputs only. The GM or player must manually learn the spells the calculated capacity makes available.


When the Chris’s Premades module is active and the CPR compatibility world setting is enabled, every preparation save triggers an actor refresh, re-evaluating that module’s item replacements against the new spell preparation state.


Class Rules expose a per-class Force Wizard Mode toggle. When on, the class is treated as a wizard for all purposes:

  • A Learn tab is rendered for it.
  • Its spells are managed through a dedicated wizard journal and copied-spells flag.
  • Cost, time, free pool, and ritual integration apply using that class’s own rules.

Use it for homebrew classes, variant classes, or subclasses that copy spells into a book.

Wizard detection comes from ClassManager.getWizardEnabledClasses(), which returns every spellcasting class whose identifier is wizard or has forceWizardMode === true. Each returned class gets independent state; there is no priority between them.


A character with multiple wizard-enabled classes has:

  • One Learn tab per wizard-enabled class.
  • One spellbook journal per class in custom-spell-lists.
  • Independent wizardCopiedSpells_<classId> flags.
  • Independent free-pool, max-spell, and ritual-mode state driven by each class’s own level and rules.

No spells are shared between spellbooks.


Key static methods on WizardBook (scripts/managers/wizard-book.mjs):

MethodPurpose
getWizardSpellbook(actor, classId)Returns the UUID array from the journal page
isSpellInSpellbook(actor, classId, uuid)Membership check
addSpellToSpellbook(actor, classId, uuid, source, metadata)Add a spell and optionally record copied metadata
removeSpellFromSpellbook(actor, classId, uuid)Remove a spell and clean up metadata
copySpell(actor, classId, uuid, cost, time, source)Add with optional currency deduction
learnFromScroll(actor, classId, scroll, options)Learn the spell a scroll is linked to and consume the scroll
getCopyingCost(actor, classId, spell){ cost, isFree }
getCopyingMinutes(actor, classId, spell)Copying duration in minutes
formatCopyingTime(minutes)Display string for a duration in minutes
getTotalWealth(actor)Actor currency summed into base currency units
canAfford(actor, cost)Whether the actor can pay a cost
getMaxSpellsAllowed(actor, classId)Capacity formula result
getUsedFreeSpells(actor, classId)Count of non-paid spells in the spellbook
getRemainingFreeSpells(actor, classId)max - used, floored at zero
isSpellFree(actor, classId, spell)Whether the next copy would be free
getSpellLearningSource(actor, classId, uuid)Resolves to free / copied / scroll from the flag
invalidateCache(actor)Drop journal and spellbook caches

Related modules:

  • scripts/apps/player-spell-book.mjs: Learn tab render (#buildRenderContext with isLearn), #onLearnSpell, #onLearnSpellFromScroll, #onUnlearnSpell.
  • scripts/managers/spell-manager.mjs: save flow and ritual auto-injection.
  • scripts/data/scroll-processor.mjs: scanForScrollSpells.
  • scripts/constants.mjs: WIZARD_DEFAULTS, WIZARD_SPELL_SOURCE, FLAGS.WIZARD_COPIED_SPELLS.
  • templates/apps/player/tab-learn.hbs, templates/components/spell-item.hbs: Learn tab and per-row wizard action block.