import React, { useState } from "react"; import { Collapsible, CollapsibleHeaderContent, CollapsibleHeading, } from "@dndbeyond/character-components/es"; import { CharacterTheme, CharClass, ClassUtils, Constants, DataOriginRefData, EntityValueLookup, Spell, } from "@dndbeyond/character-rules-engine/es"; import SpellManager from "../SpellManager"; interface Props { charClass: CharClass; activeSpells: Array; isPrepareMaxed: boolean; isCantripsKnownMaxed: boolean; isSpellsKnownMaxed: boolean; hasMultipleSpellClasses: boolean; activeFeatureCantripCount: number; knownFeatureSpellCount: number; knownSpellCount: number; activeCantripsCount: number; preparedSpellCount: number; knownCantripsMax: number | null; knownSpellsMax: number | null; prepareMax: number | null; entityValueLookup: EntityValueLookup; dataOriginRefData: DataOriginRefData; proficiencyBonus: number; theme: CharacterTheme; } interface State { collapsedGroups: { activeSpells: boolean; spellbook: boolean; addSpells: boolean; }; } export default function ClassSpellManager({ charClass, activeSpells, isPrepareMaxed, isCantripsKnownMaxed, isSpellsKnownMaxed, activeFeatureCantripCount, knownFeatureSpellCount, knownSpellCount, activeCantripsCount, preparedSpellCount, knownCantripsMax, knownSpellsMax, prepareMax, entityValueLookup, dataOriginRefData, proficiencyBonus, theme, hasMultipleSpellClasses = false, }: Props) { const isSpellbookSpellcaster = ClassUtils.isSpellbookSpellcaster(charClass); const isSpellbookEmpty = knownSpellCount === 0 && activeCantripsCount === 0; const spellCastingLearningStyle = ClassUtils.getSpellCastingLearningStyle(charClass); const buttonAddText = Constants.SpellCastingLearningStyleAddText[spellCastingLearningStyle]; const removeButtonText = Constants.SpellCastingLearningStyleRemoveText[spellCastingLearningStyle]; const [state, setState] = useState({ collapsedGroups: { activeSpells: hasMultipleSpellClasses || (isSpellbookSpellcaster && isSpellbookEmpty), spellbook: hasMultipleSpellClasses || !isSpellbookSpellcaster || (isSpellbookSpellcaster && !isSpellbookEmpty), addSpells: true, }, }); function handleShowSpellGroup(key: keyof State["collapsedGroups"]): void { let resetState = { activeSpells: true, addSpells: true, spellbook: true, }; setState({ ...state, collapsedGroups: { ...resetState, [key]: !state.collapsedGroups[key], }, }); } function doesAvailableSpellsHaveNotifications(): boolean { if (knownCantripsMax !== null && activeCantripsCount > knownCantripsMax) { return true; } if (prepareMax && preparedSpellCount > prepareMax) { return true; } if (knownSpellsMax && knownSpellCount > knownSpellsMax) { return true; } return false; } function renderSpellListSpellStatus(): React.ReactNode { let featureSpellCount: number = activeFeatureCantripCount + knownFeatureSpellCount; let calloutNode: React.ReactNode; if (featureSpellCount > 0) { calloutNode = "*"; } let cantripsNode: React.ReactNode = ""; let cantripsClsNames: Array = [ "ct-class-spell-manager__info-entry", "ct-class-spell-manager__info-entry--cantrips", ]; if (knownCantripsMax !== null) { if (activeCantripsCount === knownCantripsMax) { cantripsNode = `Cantrips: ${knownCantripsMax}`; } else { cantripsNode = `Cantrips: ${activeCantripsCount}/${knownCantripsMax}`; if (activeCantripsCount > knownCantripsMax) { cantripsClsNames.push("ct-class-spell-manager__info-entry--exceeded"); } } } const spellDisplayListType = spellCastingLearningStyle === Constants.SpellCastingLearningStyle.Prepared ? "Prepared" : "Known"; let spellsNode: React.ReactNode; let spellsExtraNode: React.ReactNode; let spellsClsNames: Array = [ "ct-class-spell-manager__info-entry", "ct-class-spell-manager__info-entry--spells", ]; if (prepareMax) { if (preparedSpellCount === prepareMax) { spellsNode = `Prepared Spells: ${preparedSpellCount}`; } else { spellsNode = `Prepared Spells: ${preparedSpellCount}/${prepareMax}`; if (preparedSpellCount > prepareMax) { spellsClsNames.push("ct-class-spell-manager__info-entry--exceeded"); } } spellsExtraNode = ( ({knownSpellCount} Known) ); } else if (knownSpellsMax) { if (knownSpellCount === knownSpellsMax) { spellsNode = `${spellDisplayListType} Spells: ${knownSpellCount}`; } else { spellsNode = `${spellDisplayListType} Spells: ${knownSpellCount}/${knownSpellsMax}`; if (knownSpellCount > knownSpellsMax) { spellsClsNames.push("ct-class-spell-manager__info-entry--exceeded"); } } } return (
{cantripsNode} {calloutNode}
{spellsNode} {spellsExtraNode} {calloutNode}
{featureSpellCount > 0 && (
*{featureSpellCount} spell {featureSpellCount !== 1 ? "s" : ""} included from class features.
)}
); } function renderEmptyActiveSpells(): React.ReactNode { return (
{ClassUtils.isSpellbookSpellcaster(charClass) && ( You currently have no prepared spells. )} {ClassUtils.isPreparedSpellcaster(charClass) && ( You currently have no prepared spells. Learn and prepare spells from your list of available spells below. )} {ClassUtils.isKnownSpellcaster(charClass) && ( You currently have no known spells. Learn spells from your list of available spells below. )}
); } function renderActiveSpells(): React.ReactNode { return (
); } function renderActiveSpellsGroup(): React.ReactNode { const { collapsedGroups } = state; let heading: React.ReactNode = "Prepared Spells"; if (ClassUtils.isKnownSpellcaster(charClass) && spellCastingLearningStyle !== Constants.SpellCastingLearningStyle.Prepared) { heading = "Known Spells"; } let headingNode: React.ReactNode = ( {heading} ({activeSpells.length}) ); let headerNode: React.ReactNode = ( ); return ( {activeSpells.length > 0 ? renderActiveSpells() : renderEmptyActiveSpells()} ); } function renderSpellbook(): React.ReactNode { const { collapsedGroups } = state; if (!ClassUtils.isSpellbookSpellcaster(charClass)) { return null; } let headerNode: React.ReactNode = ( ); return ( {isSpellbookEmpty && (
You currently have no known spells. Learn spells from your list of available spells below.
)} {!isSpellbookEmpty && ( {renderSpellListSpellStatus()} )}
); } function renderAddSpells(): React.ReactNode { const { collapsedGroups } = state; let heading: React.ReactNode = "Add Spells"; if (ClassUtils.isPreparedSpellcaster(charClass)) { heading = "Known Spells"; } let headingNode: React.ReactNode = ( {heading} {doesAvailableSpellsHaveNotifications() && (
!
)}
); let headerNode: React.ReactNode = ( ); return ( {renderSpellListSpellStatus()} ); } return (
{ClassUtils.getName(charClass)}
{renderAddSpells()} {renderActiveSpellsGroup()} {renderSpellbook()}
); }