import { visuallyHidden } from "@mui/utils"; import React from "react"; import { connect, DispatchProp } from "react-redux"; import { ConcentrationIcon, RitualIcon, } from "@dndbeyond/character-components/es"; import { AbilityLookup, ApiRequestHelpers, characterActions, CharacterTheme, ClassUtils, Constants, DataOriginRefData, ExperienceInfo, FormatUtils, Hack__BaseCharClass, HelperUtils, InventoryLookup, InventoryManager, ItemManager, RuleData, rulesEngineSelectors, ScaledSpell, SpellCasterInfo, SpellSlotContract, SpellUtils, } from "@dndbeyond/character-rules-engine/es"; import { IRollContext } from "@dndbeyond/dice"; import { TabFilter } from "~/components/TabFilter"; import { PaneInfo, useSidebar } from "~/contexts/Sidebar/Sidebar"; import { PaneComponentEnum } from "~/subApps/sheet/components/Sidebar/types"; import SpellsLevel from "../../../Shared/components/SpellsLevel"; import SpellsLevelCasting from "../../../Shared/components/SpellsLevelCasting"; import { ThemeButton } from "../../../Shared/components/common/Button"; import { appEnvSelectors, characterRollContextSelectors, } from "../../../Shared/selectors"; import { PaneIdentifierUtils } from "../../../Shared/utils"; import ContentGroup from "../../components/ContentGroup"; import SpellsFilter from "../../components/SpellsFilter"; import { SheetAppState } from "../../typings"; import styles from "./styles.module.css"; const ALL_LEVELS: number = -1; function filterLevelSpells( levelSpells: Array>, testFunc: (spell: ScaledSpell) => boolean ): Array> { return levelSpells.map((spells) => spells.filter((spell) => testFunc(spell))); } function hasFilteredLevelSpells( levelSpells: Array>, testFunc: (spell: ScaledSpell) => boolean ): boolean { return levelSpells.reduce((acc, spells) => { return ( acc || spells.reduce((spellAcc, spell) => { return spellAcc || testFunc(spell); }, false) ); }, false); } interface Props extends DispatchProp { levelSpells: Array>; spellCasterInfo: SpellCasterInfo; ruleData: RuleData; abilityLookup: AbilityLookup; xpInfo: ExperienceInfo; isReadonly: boolean; spellSlots: Array; pactMagicSlots: Array; showNotes: boolean; theme: CharacterTheme; diceEnabled: boolean; dataOriginRefData: DataOriginRefData; proficiencyBonus: number; characterRollContext: IRollContext; inventoryManager: InventoryManager; inventoryLookup: InventoryLookup; partyInventoryLookup: InventoryLookup; paneHistoryStart: PaneInfo["paneHistoryStart"]; } interface StateFilterData { filteredLevelSpells: Array>; showAdvancedFilters: boolean; isFiltering: boolean; } interface State { filterData: StateFilterData; ritualLevelSpells: Array>; concentrationLevelSpells: Array>; hasRitualSpells: boolean; hasConcentrationSpells: boolean; } class Spells extends React.PureComponent { static defaultProps = { showNotes: true, }; constructor(props) { super(props); this.state = { filterData: { filteredLevelSpells: [], showAdvancedFilters: false, isFiltering: false, }, ritualLevelSpells: filterLevelSpells( props.levelSpells, SpellUtils.isRitual ), concentrationLevelSpells: filterLevelSpells( props.levelSpells, SpellUtils.getConcentration ), hasRitualSpells: hasFilteredLevelSpells( props.levelSpells, SpellUtils.isRitual ), hasConcentrationSpells: hasFilteredLevelSpells( props.levelSpells, SpellUtils.getConcentration ), }; } componentDidUpdate(prevProps: Props, prevState: State): void { if (this.props.levelSpells !== prevProps.levelSpells) { this.setState({ ritualLevelSpells: filterLevelSpells( this.props.levelSpells, SpellUtils.isRitual ), concentrationLevelSpells: filterLevelSpells( this.props.levelSpells, SpellUtils.getConcentration ), hasRitualSpells: hasFilteredLevelSpells( this.props.levelSpells, SpellUtils.isRitual ), hasConcentrationSpells: hasFilteredLevelSpells( this.props.levelSpells, SpellUtils.getConcentration ), }); } } handleSpellSlotSet = (level: number, used: number): void => { const { dispatch } = this.props; const spellLevelSpellSlotRequestsDataKey = ApiRequestHelpers.getSpellLevelSpellSlotRequestsDataKey(level); if (spellLevelSpellSlotRequestsDataKey !== null) { dispatch( characterActions.spellLevelSpellSlotsSet({ [spellLevelSpellSlotRequestsDataKey]: used, }) ); } }; handleSpellSlotChange = (level: number, changeAmount: number): void => { const { spellSlots } = this.props; const usedAmount = this.getUsedSpellSlotLevelAmount( level, changeAmount, spellSlots ); if (usedAmount !== null) { this.handleSpellSlotSet(level, usedAmount); } }; handlePactSlotSet = (level: number, used: number): void => { const { dispatch } = this.props; const spellLevelPactMagicRequestsDataKey = ApiRequestHelpers.getSpellLevelPactMagicRequestsDataKey(level); if (spellLevelPactMagicRequestsDataKey !== null) { dispatch( characterActions.spellLevelPactMagicSlotsSet({ [spellLevelPactMagicRequestsDataKey]: used, }) ); } }; handlePactSlotChange = (level: number, changeAmount: number): void => { const { pactMagicSlots } = this.props; const usedAmount = this.getUsedSpellSlotLevelAmount( level, changeAmount, pactMagicSlots ); if (usedAmount !== null) { this.handlePactSlotSet(level, usedAmount); } }; getUsedSpellSlotLevelAmount = ( level: number, changeAmount: number, spellSlots: Array ): number | null => { const foundSlotLevel = spellSlots.find( (spellSlot) => spellSlot.level === level ); if (!foundSlotLevel) { return null; } const usedAmount: number = foundSlotLevel.used + changeAmount; const maxAmount: number = foundSlotLevel.available; return HelperUtils.clampInt(usedAmount, 0, maxAmount); }; handleSpellLimitedUseSet = ( mappingId: number, mappingTypeId: number, uses: number, dataOrigin: Constants.DataOriginTypeEnum ): void => { const { dispatch } = this.props; dispatch( characterActions.spellUseSet(mappingId, mappingTypeId, uses, dataOrigin) ); }; handleItemLimitedUseSet = ( mappingId: number, mappingTypeId: number, uses: number ): void => { let item: ItemManager | null = null; item = ItemManager.getItem(mappingId); if (item) { item.handleItemLimitedUseSet(uses); } }; handleSpellDetailShow = (spell: ScaledSpell, castLevel: number): void => { const { paneHistoryStart } = this.props; const mappingId = SpellUtils.getMappingId(spell); if (mappingId !== null) { const dataOrigin = SpellUtils.getDataOrigin(spell); const dataOriginType = SpellUtils.getDataOriginType(spell); switch (dataOriginType) { case Constants.DataOriginTypeEnum.CLASS: paneHistoryStart( PaneComponentEnum.CLASS_SPELL_DETAIL, PaneIdentifierUtils.generateClassSpell( ClassUtils.getMappingId( dataOrigin.primary as Hack__BaseCharClass ), mappingId, castLevel ) ); break; default: paneHistoryStart( PaneComponentEnum.CHARACTER_SPELL_DETAIL, PaneIdentifierUtils.generateCharacterSpell(mappingId, castLevel) ); } } }; handleManageSpellsOpen = (): void => { const { paneHistoryStart } = this.props; paneHistoryStart(PaneComponentEnum.SPELL_MANAGE); }; handleFilterUpdate = (filterData: StateFilterData): void => { this.setState({ filterData, }); }; renderEmptySpellLevel = (level: number): React.ReactNode => { let spellAbbr = FormatUtils.renderSpellLevelAbbreviation(level); return (
You do not have any {spellAbbr}-level spells or spells that scale to{" "} {spellAbbr} level available, but you can cast lower level spells using your {spellAbbr}-level spell slots.
); }; renderManageButton = (): React.ReactNode => { const { isReadonly } = this.props; if (isReadonly) { return null; } return ( Manage Spells ); }; renderSpellLevels = ( levelSpells: Array>, showLevels: Array, filterRitual: boolean = false, filterConcentration: boolean = false ): React.ReactNode => { const { filterData, ritualLevelSpells, concentrationLevelSpells } = this.state; const { spellCasterInfo, ruleData, xpInfo, abilityLookup, isReadonly, showNotes, diceEnabled, theme, dataOriginRefData, proficiencyBonus, characterRollContext, } = this.props; return ( {levelSpells.map((spells, level) => { if (!showLevels.includes(ALL_LEVELS) && !showLevels.includes(level)) { return null; } let levelHasSlots = spellCasterInfo.availablePactMagicLevels.includes(level) || spellCasterInfo.availableSpellLevels.includes(level); if (filterRitual) { spells = ritualLevelSpells[level]; } if (filterConcentration) { spells = concentrationLevelSpells[level]; } if ((filterRitual || filterConcentration) && !spells.length) { return null; } if (!spells.length && !levelHasSlots) { return null; } let filteredSpells = filterData.filteredLevelSpells[level]; if (filteredSpells && filterRitual) { filteredSpells = filteredSpells.filter((spell) => SpellUtils.isRitual(spell) ); } if (filteredSpells && filterConcentration) { filteredSpells = filteredSpells.filter((spell) => SpellUtils.getConcentration(spell) ); } if (!filteredSpells) { return null; } let extraNode: React.ReactNode = ( ); return ( {spells.length ? ( ) : ( this.renderEmptySpellLevel(level) )} ); })} ); }; render() { const { filterData, hasConcentrationSpells, hasRitualSpells } = this.state; const { ruleData, abilityLookup, levelSpells, spellCasterInfo, theme } = this.props; return (

Spells

{!filterData.showAdvancedFilters && (
{ let levelHasSlots = spellCasterInfo.availablePactMagicLevels.includes(level) || spellCasterInfo.availableSpellLevels.includes(level); if (!spells.length && !levelHasSlots) return { label: "", content: null }; let filterBadgeText: number | null = null; const filteredSpells = filterData.filteredLevelSpells[level]; if (filterData.isFiltering && filteredSpells.length) { filterBadgeText = filteredSpells.length; } return { label: `${FormatUtils.renderSpellLevelAbbreviation(level)}`, badge: filterBadgeText ?? undefined, content: this.renderSpellLevels(levelSpells, [level]), }; }), ...(hasConcentrationSpells ? [ { label: ( ), content: this.renderSpellLevels( levelSpells, [ALL_LEVELS], false, true ), }, ] : []), ...(hasRitualSpells ? [ { label: ( ), content: this.renderSpellLevels( levelSpells, [ALL_LEVELS], true ), }, ] : []), ]} />
)}
); } } function mapStateToProps(state: SheetAppState) { return { levelSpells: rulesEngineSelectors.getLevelSpells(state), spellCasterInfo: rulesEngineSelectors.getSpellCasterInfo(state), abilityLookup: rulesEngineSelectors.getAbilityLookup(state), ruleData: rulesEngineSelectors.getRuleData(state), xpInfo: rulesEngineSelectors.getExperienceInfo(state), spellSlots: rulesEngineSelectors.getSpellSlots(state), pactMagicSlots: rulesEngineSelectors.getPactMagicSlots(state), theme: rulesEngineSelectors.getCharacterTheme(state), isReadonly: appEnvSelectors.getIsReadonly(state), diceEnabled: appEnvSelectors.getDiceEnabled(state), dataOriginRefData: rulesEngineSelectors.getDataOriginRefData(state), proficiencyBonus: rulesEngineSelectors.getProficiencyBonus(state), characterRollContext: characterRollContextSelectors.getCharacterRollContext(state), }; } const SpellsContainer = (props) => { const { pane: { paneHistoryStart }, } = useSidebar(); return ; }; export default connect(mapStateToProps)(SpellsContainer);