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

254 lines
7.6 KiB
TypeScript

import clsx from "clsx";
import { FC, HTMLAttributes, useMemo, useState } from "react";
import { useDispatch } from "react-redux";
import {
characterActions,
ClassUtils,
Constants,
DiceUtils,
FormatUtils,
HelperUtils,
RuleDataUtils,
} from "@dndbeyond/character-rules-engine";
import { ConfirmModal, ConfirmModalProps } from "~/components/ConfirmModal";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import {
HP_BASE_MAX_VALUE,
HP_BONUS_VALUE,
HP_OVERRIDE_MAX_VALUE,
} from "~/subApps/sheet/constants";
import { FormInputField } from "~/tools/js/Shared/components/common/FormInputField";
import styles from "./styles.module.css";
export interface HpManageModalProps
extends Omit<ConfirmModalProps, "onConfirm"> {}
export const HpManageModal: FC<HpManageModalProps> = ({
onClose,
...props
}) => {
const dispatch = useDispatch();
const { hpInfo, preferences, ruleData } = useCharacterEngine();
const [baseHp, setBaseHp] = useState(hpInfo.baseHp);
const [bonusHp, setBonusHp] = useState(hpInfo.bonusHp);
const [overrideHp, setOverrideHp] = useState(hpInfo.overrideHp);
const reset = () => {
setBaseHp(hpInfo.baseHp);
setBonusHp(hpInfo.bonusHp);
setOverrideHp(hpInfo.overrideHp);
};
const handleClose = () => {
onClose();
reset();
};
const onConfirm = () => {
if (baseHp !== hpInfo.baseHp) {
dispatch(characterActions.baseHitPointsSet(baseHp));
}
if (bonusHp !== hpInfo.bonusHp) {
dispatch(characterActions.bonusHitPointsSet(bonusHp));
}
if (overrideHp !== hpInfo.overrideHp) {
dispatch(characterActions.overrideHitPointsSet(overrideHp));
}
onClose();
reset();
};
const transformBaseHp = (value: string): number => {
let parsedNumber = HelperUtils.parseInputInt(
value,
RuleDataUtils.getMinimumHpTotal(ruleData)
);
return HelperUtils.clampInt(
parsedNumber,
RuleDataUtils.getMinimumHpTotal(ruleData),
HP_BASE_MAX_VALUE
);
};
const transformBonusHp = (value: string): number | null => {
let parsedNumber = HelperUtils.parseInputInt(value);
if (parsedNumber === null) {
return parsedNumber;
}
return HelperUtils.clampInt(
parsedNumber,
HP_BONUS_VALUE.MIN,
HP_BONUS_VALUE.MAX
);
};
const transformOverrideHp = (value: string): number | null => {
let parsedNumber = HelperUtils.parseInputInt(value, null);
if (parsedNumber === null) {
return parsedNumber;
}
return HelperUtils.clampInt(
parsedNumber,
RuleDataUtils.getMinimumHpTotal(ruleData)
);
};
const handleOverrideHpUpdate = (value: number | null): void => {
const newOverride =
value === null
? null
: HelperUtils.clampInt(
value,
RuleDataUtils.getMinimumHpTotal(ruleData),
HP_OVERRIDE_MAX_VALUE
);
setOverrideHp(newOverride);
};
const totalHp = useMemo(() => {
let totalHp: number =
baseHp + hpInfo.totalHitPointSources + (bonusHp === null ? 0 : bonusHp);
if (overrideHp !== null) {
totalHp = overrideHp;
}
return Math.max(RuleDataUtils.getMinimumHpTotal(ruleData), totalHp);
}, [baseHp, bonusHp, overrideHp, hpInfo, ruleData]);
return (
<ConfirmModal
onClose={handleClose}
heading="Manage Hit Points"
onConfirm={onConfirm}
confirmButtonText="Apply"
useMobileFullScreen
{...props}
>
{/* Total Max Hit Points */}
<div className={styles.hpManageModal}>
<p className={styles.total}>
<span className={styles.totalLabel}>Maximum Hit Points</span>
<span className={styles.totalValue}>{totalHp}</span>
</p>
{/* Hit Point Controls */}
<div className={styles.controls}>
{preferences.hitPointType ===
Constants.PreferenceHitPointTypeEnum.FIXED ? (
<p className={styles.baseHp}>
<span className={clsx([styles.baseHpLabel])}>Fixed HP</span>
<span className={styles.baseHpValue}>{baseHp}</span>
</p>
) : (
<FormInputField
label="Rolled HP"
initialValue={baseHp}
type="number"
onBlur={(value) => setBaseHp(value as number)}
transformValueOnBlur={transformBaseHp}
/>
)}
<FormInputField
label="HP Modifier"
initialValue={bonusHp}
type="number"
placeholder={"--"}
onBlur={(value) => setBonusHp(value as number)}
transformValueOnBlur={transformBonusHp}
/>
<FormInputField
inputAttributes={
{
min: RuleDataUtils.getMinimumHpTotal(ruleData),
max: HP_OVERRIDE_MAX_VALUE,
} as HTMLAttributes<HTMLInputElement>
}
label="Override HP"
initialValue={overrideHp}
type="number"
placeholder={"--"}
onBlur={handleOverrideHpUpdate}
transformValueOnBlur={transformOverrideHp}
/>
</div>
{/* Hit Point Bonus Sources */}
{hpInfo.hitPointSources.length > 0 && (
<div className={styles.hpSources}>
<h3>Hit Point Bonuses</h3>
{hpInfo.hitPointSources.map((hitPointSource, idx) => (
<p key={`${idx}:${hitPointSource.source}`}>
{FormatUtils.renderSignedNumber(hitPointSource.amount)} from{" "}
{hitPointSource.source}
</p>
))}
</div>
)}
{/* Hit Dice & Potential Values */}
<div className={styles.info}>
<div className={styles.infoGroup}>
<h3>Hit Dice</h3>
{hpInfo.classesHitDice.map((classHitDice) => (
<div key={ClassUtils.getId(classHitDice.charClass)}>
<span className={styles.infoLabel}>
{ClassUtils.getName(classHitDice.charClass)}:
</span>
<span>{DiceUtils.renderDie(classHitDice.dice)}</span>
</div>
))}
</div>
<div className={styles.infoGroup}>
<h3>Potential Values</h3>
<div>
<span className={styles.infoLabel}>Total Fixed Value HP:</span>
<span>{hpInfo.totalFixedValueHp}</span>
</div>
<div>
<span className={styles.infoLabel}>Total Average HP:</span>
<span>{hpInfo.totalAverageHp}</span>
</div>
<div>
<span className={styles.infoLabel}>Total Possible HP:</span>
<span>{hpInfo.possibleMaxHitPoints}</span>
</div>
</div>
</div>
{/* HP Help */}
<div className={styles.help}>
<h3>Max Hit Points</h3>
<p>
Your hit point maximum is determined by the number you roll for your
hit dice each level or a fixed value determined by your hit dice and
your Constitution modifier
</p>
<h3>Bonus Hit Points</h3>
<p>
Use this field to record any miscellaneous bonus hit points you want
to add to your normal hit point maximum. These hit points are
different from temporary hit points, which you can add on your
character sheet during play.
</p>
<h3>Override Hit Points</h3>
<p>
Use this field to override your typical hit point maximum. The
number you enter here will display as your hit point maximum on your
character sheet.
</p>
</div>
</div>
</ConfirmModal>
);
};