import clsx from "clsx"; import { FC, HTMLAttributes, ReactNode } from "react"; import { useDispatch } from "react-redux"; import { characterActions, CharacterUtils, CharClass, ClassUtils, Constants, FormatUtils, HelperUtils, HtmlSelectOption, } from "@dndbeyond/character-rules-engine"; import CloseIcon from "@dndbeyond/fontawesome-cache/svgs/solid/x.svg"; import { Button } from "~/components/Button"; import { useCharacterEngine } from "~/hooks/useCharacterEngine"; import { useModalManager } from "~/subApps/builder/contexts/ModalManager"; import { Select } from "~/tools/js/smartComponents/legacy"; import styles from "./styles.module.css"; export interface ClassHeaderProps extends HTMLAttributes { charClass: CharClass; isMulticlass: boolean; levelsRemaining: number; } export const ClassHeader: FC = ({ charClass, isMulticlass, levelsRemaining, className, ...props }) => { const dispatch = useDispatch(); const { preferences, ruleData, classes, currentLevel, totalClassLevel } = useCharacterEngine(); const { createModal } = useModalManager(); const portraitAvatarUrl = ClassUtils.getPortraitUrl(charClass); const isStartingClass = ClassUtils.isStartingClass(charClass); const level = ClassUtils.getLevel(charClass); const name = ClassUtils.getName(charClass); const subclassDefinition = ClassUtils.getSubclass(charClass); const classId = ClassUtils.getActiveId(charClass); const levelsDiff: number = currentLevel - totalClassLevel; const isTypeMilestone: boolean = preferences.progressionType === Constants.PreferenceProgressionTypeEnum.MILESTONE; const isTypeXp: boolean = preferences.progressionType === Constants.PreferenceProgressionTypeEnum.XP; const hasLevelXpDiff: boolean = levelsDiff !== 0 && isTypeXp; let levelOptions: Array = []; for (let i = 1; i <= level + levelsRemaining; i++) { levelOptions.push({ label: "" + i, value: i, }); } const handleLevelChangePromise = ( newLevel: string, oldLevel: string, accept: () => void, reject: () => void ): void => { const newLevelValue = HelperUtils.parseInputInt(newLevel, 0); const oldLevelValue = HelperUtils.parseInputInt(oldLevel, 0); const newTotalClassLevel: number = classes.reduce( (acc: number, oldClass) => (acc += oldClass.id === charClass.id ? newLevelValue : oldClass.level), 0 ); const isLevelUp: boolean = newLevelValue > oldLevelValue; const isLevelDown: boolean = newLevelValue < oldLevelValue; // Modals to handle milestone if (isLevelUp && isTypeMilestone) { dispatch(characterActions.classLevelSetRequest(classId, newLevelValue)); accept(); } if (isLevelDown && isTypeMilestone) { createModal({ content: (
{renderModalIntro(currentLevel, newTotalClassLevel)}

Are you sure you want to level down in the {name} class?

Your hit points will be reduced by the fixed amount and class feature choices you have made for the higher levels will be lost.

), props: { heading: "Level Down", color: "secondary", confirmButtonText: "Level Down", size: "fit-content", onConfirm: () => { dispatch( characterActions.classLevelSetRequest(classId, newLevelValue) ); accept(); }, onClose: () => { reject(); }, }, }); } // Modals to handle progression if (isLevelUp && isTypeXp) { if (newTotalClassLevel <= currentLevel) { dispatch(characterActions.classLevelSetRequest(classId, newLevelValue)); accept(); } else { createModal({ content: (
{renderModalIntro(currentLevel, newTotalClassLevel)}

Are you sure you want to level up in the {name} class?

Your XP total will be increased to{" "} {FormatUtils.renderLocaleNumber( CharacterUtils.deriveCurrentLevelXp( newTotalClassLevel, ruleData ) )}{" "} to match your new level.

), props: { heading: "Level Up", confirmButtonText: "Level Up", size: "fit-content", onConfirm: () => { dispatch( characterActions.classLevelSetRequest( classId, newLevelValue, CharacterUtils.deriveCurrentLevelXp( newTotalClassLevel, ruleData ) ) ); accept(); }, onClose: () => { reject(); }, }, }); } } if (isLevelDown && isTypeXp) { createModal({ content: (
{renderModalIntro(currentLevel, newTotalClassLevel)}

Are you sure you want to level down in the {name} class?

Your hit points will be reduced by the fixed amount and class feature choices you have made for the higher levels will be lost.

Your XP total will be decreased to{" "} {FormatUtils.renderLocaleNumber( CharacterUtils.deriveCurrentLevelXp( newTotalClassLevel, ruleData ) )}{" "} to match your new level.

), props: { heading: "Level Down", confirmButtonText: "Level Down", color: "secondary", size: "fit-content", onConfirm: () => { dispatch( characterActions.classLevelSetRequest( classId, newLevelValue, CharacterUtils.deriveCurrentLevelXp( newTotalClassLevel, ruleData ) ) ); accept(); }, onClose: () => { reject(); }, }, }); } }; const handleRemoveClass = (): void => { const newTotalClassLevel: number = classes.reduce( (acc: number, oldClass) => (acc += oldClass.id === charClass.id ? 0 : oldClass.level), 0 ); createModal({ content: (
{renderModalIntro()}

Are you sure you want to remove all your levels in the {name} class?

Your hit points will be reduced by the fixed amount and class feature choices you have made for the higher levels will be lost.

{isTypeXp && (

Your XP total will be decreased to match your new level.

)}
), props: { heading: "Remove Class", variant: "remove", size: "fit-content", confirmButtonText: "Remove", onConfirm: () => { let newCharacterXp: number | null = isTypeXp ? CharacterUtils.deriveCurrentLevelXp(newTotalClassLevel, ruleData) : null; dispatch( characterActions.classRemoveRequest(charClass.id, newCharacterXp) ); }, onClose: () => {}, //Do nothing }, }); }; const renderModalIntro = ( currentLevel: number | null = null, newLevel: number | null = null ): React.ReactNode => { return (
{classPortraitName} {currentLevel !== null && newLevel !== null && (
Current Level: {currentLevel}
New Level: {newLevel}
)}
); }; const classPortraitName: ReactNode = ( <> {`${name}
{subclassDefinition && (
{subclassDefinition.name}
)}
{name}
); return (
{isMulticlass && isStartingClass && (
Starting Class
)} {classPortraitName}