2025-05-28 15:36:51 -07:00

212 lines
5.9 KiB
TypeScript

import React from "react";
import { Tooltip } from "@dndbeyond/character-common-components/es";
import {
ClassUtils,
SpellCasterCastingEntry,
SpellCasterInfo,
SpellSlotContract,
} from "@dndbeyond/character-rules-engine/es";
import { NumberDisplay } from "~/components/NumberDisplay";
import SlotManager from "../SlotManager";
interface Props {
level: number;
spellCasterInfo: SpellCasterInfo;
onSpellSlotSet?: (level: number, uses: number) => void;
onPactSlotSet?: (level: number, uses: number) => void;
showSlots: boolean;
showCastingInfo: boolean;
isInteractive: boolean;
isDarkMode?: boolean;
}
export default class SpellsLevelCasting extends React.PureComponent<Props> {
static defaultProps = {
level: 1,
showSlots: true,
showCastingInfo: true,
isInteractive: true,
};
handleSpellSlotSet = (uses: number): void => {
const { onSpellSlotSet, level } = this.props;
if (onSpellSlotSet) {
onSpellSlotSet(level, uses);
}
};
handlePactSlotSet = (uses: number): void => {
const { onPactSlotSet, level } = this.props;
if (onPactSlotSet) {
onPactSlotSet(level, uses);
}
};
getSpellSlotInfo = (): SpellSlotContract | null => {
const { spellCasterInfo, level } = this.props;
const { spellSlots } = spellCasterInfo;
let spellSlotInfo = spellSlots.find(
(spellSlotGroup) => spellSlotGroup.level === level
);
return spellSlotInfo ? spellSlotInfo : null;
};
getPactSlotInfo = (): SpellSlotContract | null => {
const { spellCasterInfo, level } = this.props;
const { pactMagicSlots } = spellCasterInfo;
let spellSlotInfo = pactMagicSlots.find(
(pactSlotGroup) => pactSlotGroup.level === level
);
return spellSlotInfo ? spellSlotInfo : null;
};
hasLevelSlots = (): boolean => {
let spellSlotLevel = this.getSpellSlotInfo();
let pactMagicLevel = this.getPactSlotInfo();
return !!(spellSlotLevel || pactMagicLevel);
};
hasCastingInfo = (): boolean => {
const { spellCasterInfo } = this.props;
const { castingInfo } = spellCasterInfo;
return !!(
castingInfo.modifiers.length ||
castingInfo.spellAttacks.length ||
castingInfo.saveDcs.length
);
};
renderLevelSlots = (): React.ReactNode => {
const { isInteractive, isDarkMode } = this.props;
let spellSlotLevel = this.getSpellSlotInfo();
let pactMagicLevel = this.getPactSlotInfo();
if (!this.hasLevelSlots()) {
return null;
}
return (
<div className="ct-spells-level-casting__slot-groups">
{pactMagicLevel && (
<div className="ct-spells-level-casting__slot-group ct-spells-level-casting__slot-group--pact">
<div className="ct-spells-level-casting__slot-group-manager">
<SlotManager
onSet={this.handlePactSlotSet}
available={pactMagicLevel.available}
used={pactMagicLevel.used}
size={"small"}
isInteractive={isInteractive}
/>
</div>
<div className="ct-spells-level-casting__slot-group-label">
Pact
</div>
</div>
)}
{spellSlotLevel && (
<div className="ct-spells-level-casting__slot-group ct-spells-level-casting__slot-group--spells">
<div className="ct-spells-level-casting__slot-group-manager">
<SlotManager
onSet={this.handleSpellSlotSet}
available={spellSlotLevel.available}
used={spellSlotLevel.used}
size={"small"}
isInteractive={isInteractive}
/>
</div>
<div
className={`ct-spells-level-casting__slot-group-label ${
isDarkMode ? "ct-spells-level-casting--dark-mode" : ""
}`}
>
Slots
</div>
</div>
)}
</div>
);
};
renderCastingInfoGroup = (
label: React.ReactNode,
entries: Array<SpellCasterCastingEntry>,
isSignedNumber: boolean = true
): React.ReactNode => {
const { isDarkMode } = this.props;
return (
<div className="ct-spells-level-casting__info-group">
<div className="ct-spells-level-casting__info-items">
{entries.map((entry) => {
if (entry.value === null) {
return null;
}
let tooltip = entry.sources
.map((charClass) => ClassUtils.getName(charClass))
.join(", ");
return (
<Tooltip
title={tooltip}
className="ct-spells-level-casting__info-item"
key={entry.value}
tippyOpts={{ dynamicTitle: true }}
isDarkMode={isDarkMode}
>
{isSignedNumber ? (
<NumberDisplay type="signed" number={entry.value} />
) : (
entry.value
)}
</Tooltip>
);
})}
</div>
<div className="ct-spells-level-casting__info-label">{label}</div>
</div>
);
};
renderCastingInfo = (): React.ReactNode => {
const { spellCasterInfo } = this.props;
const { castingInfo } = spellCasterInfo;
if (!this.hasCastingInfo()) {
return null;
}
return (
<div className="ct-spells-level-casting__info">
{this.renderCastingInfoGroup("Modifier", castingInfo.modifiers)}
{this.renderCastingInfoGroup("Spell Attack", castingInfo.spellAttacks)}
{this.renderCastingInfoGroup("Save DC", castingInfo.saveDcs, false)}
</div>
);
};
render() {
const { showSlots, showCastingInfo } = this.props;
if (!this.hasCastingInfo() && !this.hasLevelSlots()) {
return null;
}
return (
<div className="ct-spells-level-casting">
{showSlots && this.renderLevelSlots()}
{showCastingInfo && this.renderCastingInfo()}
</div>
);
}
}