255 lines
7.4 KiB
TypeScript

import clsx from "clsx";
import { FC, FocusEvent, 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 { InputField } from "../InputField";
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 handleBaseHp = (e: FocusEvent<HTMLInputElement>) => {
let parsedNumber = HelperUtils.parseInputInt(
e.target.value,
RuleDataUtils.getMinimumHpTotal(ruleData)
);
const clampedValue = HelperUtils.clampInt(
parsedNumber,
RuleDataUtils.getMinimumHpTotal(ruleData),
HP_BASE_MAX_VALUE
);
setBaseHp(clampedValue);
};
const handleBonusHp = (e: FocusEvent<HTMLInputElement>) => {
let parsedNumber = HelperUtils.parseInputInt(e.target.value);
if (parsedNumber === null) return parsedNumber;
const clampedValue = HelperUtils.clampInt(
parsedNumber,
HP_BONUS_VALUE.MIN,
HP_BONUS_VALUE.MAX
);
setBonusHp(clampedValue);
};
const handleOverrideHp = (e: FocusEvent<HTMLInputElement>) => {
let parsedNumber = HelperUtils.parseInputInt(e.target.value, null);
if (parsedNumber === null) {
setOverrideHp(null);
return parsedNumber;
}
const clampedValue = HelperUtils.clampInt(
parsedNumber,
RuleDataUtils.getMinimumHpTotal(ruleData)
);
const clampedOverride = HelperUtils.clampInt(
clampedValue,
RuleDataUtils.getMinimumHpTotal(ruleData),
HP_OVERRIDE_MAX_VALUE
);
setOverrideHp(clampedOverride);
};
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>
) : (
<InputField
className={styles.inputField}
label="Rolled HP"
initialValue={baseHp}
type="number"
onBlur={handleBaseHp}
/>
)}
<InputField
className={styles.inputField}
label="HP Modifier"
initialValue={bonusHp}
type="number"
placeholder="--"
onBlur={handleBonusHp}
/>
<InputField
className={styles.inputField}
label="Override HP"
initialValue={overrideHp}
type="number"
placeholder={"--"}
inputProps={{
min: RuleDataUtils.getMinimumHpTotal(ruleData),
max: HP_OVERRIDE_MAX_VALUE,
}}
onBlur={handleOverrideHp}
/>
</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>
);
};