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

209 lines
5.9 KiB
TypeScript

import clsx from "clsx";
import { FC, HTMLAttributes, useEffect, useMemo, useState } from "react";
import {
CharacterUtils,
FormatUtils,
HelperUtils,
HtmlSelectOption,
RuleDataUtils,
} from "@dndbeyond/character-rules-engine";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import XpBar from "~/tools/js/smartComponents/XpBar";
import { Select } from "~/tools/js/smartComponents/legacy";
import { Button } from "../Button";
import styles from "./styles.module.css";
const XP_CHANGE_TYPE = {
ADD: "ADD",
REMOVE: "REMOVE",
};
interface XpManagerProps extends HTMLAttributes<HTMLDivElement> {
handleXpUpdates: (newXpTotal: number) => void;
shouldReset: boolean;
}
export const XpManager: FC<XpManagerProps> = ({
handleXpUpdates,
shouldReset,
...props
}) => {
const { experienceInfo: xpInfo, ruleData } = useCharacterEngine();
const [xpNew, setXpNew] = useState<number | null>(null);
const [xpChange, setXpChange] = useState<number | null>(null);
const [levelChosen, setLevelChosen] = useState<number | null>(null);
const [changeType, setChangeType] = useState<string>(XP_CHANGE_TYPE.ADD);
const [newXpTotal, setNewXpTotal] = useState<number>(xpInfo.currentLevelXp);
useEffect(() => {
let xpDiff: number = xpChange ?? 0;
if (changeType === XP_CHANGE_TYPE.REMOVE) {
xpDiff *= -1;
}
let newXpTotal: number = xpInfo.currentLevelXp;
if (xpDiff) {
newXpTotal += xpDiff;
} else if (levelChosen !== null) {
newXpTotal = CharacterUtils.deriveCurrentLevelXp(levelChosen, ruleData);
} else if (xpNew !== null) {
newXpTotal = xpNew;
}
newXpTotal = Math.min(
Math.max(0, newXpTotal),
CharacterUtils.deriveMaxXp(ruleData)
);
setNewXpTotal((prev) => {
if (newXpTotal !== prev) {
handleXpUpdates(newXpTotal);
}
return newXpTotal;
});
}, [
xpChange,
changeType,
levelChosen,
xpNew,
xpInfo,
ruleData,
handleXpUpdates,
]);
useEffect(() => {
if (shouldReset) {
setXpNew(null);
setXpChange(null);
setLevelChosen(null);
setChangeType(XP_CHANGE_TYPE.ADD);
setNewXpTotal(xpInfo.currentLevelXp);
}
}, [shouldReset, xpInfo]);
const levelOptions = useMemo(() => {
let levelOptions: Array<HtmlSelectOption> = [];
for (let i = 1; i <= RuleDataUtils.getMaxCharacterLevel(ruleData); i++) {
levelOptions.push({
label: `${i}`,
value: i,
});
}
return levelOptions;
}, [ruleData]);
const displayCurrentLevelXp = useMemo(() => {
return xpInfo.currentLevel === ruleData.maxCharacterLevel
? CharacterUtils.deriveCurrentLevelXp(
ruleData.maxCharacterLevel - 1,
ruleData
)
: xpInfo.currentLevelXp;
}, [xpInfo, ruleData]);
const handleChooseLevel = (value: string): void => {
setXpNew(null);
setXpChange(null);
setLevelChosen(HelperUtils.parseInputInt(value));
};
const handleXpChange = (evt: React.ChangeEvent<HTMLInputElement>): void => {
setXpNew(null);
setXpChange(HelperUtils.parseInputInt(evt.target.value));
setLevelChosen(null);
};
const handleXpSet = (evt: React.ChangeEvent<HTMLInputElement>): void => {
setXpNew(HelperUtils.parseInputInt(evt.target.value));
setXpChange(null);
setLevelChosen(null);
};
return (
<div {...props}>
<h2 className={styles.levelTotal}>
Current XP Total:{" "}
{FormatUtils.renderLocaleNumber(xpInfo.currentLevelXp)} (Level{" "}
{xpInfo.currentLevel})
</h2>
<div className={styles.xpBar}>
<XpBar
xp={xpInfo.currentLevelXp}
showCurrentMarker={true}
ruleData={ruleData}
/>
</div>
<div className={styles.currentLevelAmounts}>
<div>{FormatUtils.renderLocaleNumber(displayCurrentLevelXp)}</div>
<div>{FormatUtils.renderLocaleNumber(xpInfo.nextLevelXp)}</div>
</div>
<div className={clsx([styles.controls, styles.withDivider])}>
<div className={styles.control}>
<label id="set-level" className={styles.label}>
Set Level
</label>
<Select
aria-labelledby="set-level"
options={levelOptions}
onChange={handleChooseLevel}
value={levelChosen}
placeholder={"--"}
/>
</div>
<div className={clsx([styles.control, styles.setXp])}>
<label id="set-xp" className={styles.label}>
Set XP
</label>
<input
aria-labelledby="set-xp"
type="number"
value={xpNew === null ? "" : xpNew}
onChange={handleXpSet}
/>
</div>
<div className={styles.adjustGroup}>
<div className={styles.adjustTabs}>
<Button
variant="text"
size="xx-small"
className={clsx([
styles.tab,
changeType === XP_CHANGE_TYPE.ADD && styles.active,
])}
onClick={() => setChangeType(XP_CHANGE_TYPE.ADD)}
>
Add Xp
</Button>
<Button
variant="text"
size="xx-small"
className={clsx([
styles.tab,
changeType === XP_CHANGE_TYPE.REMOVE && styles.active,
])}
onClick={() => setChangeType(XP_CHANGE_TYPE.REMOVE)}
>
Remove Xp
</Button>
</div>
<input
type="number"
value={xpChange === null ? "" : xpChange}
onChange={handleXpChange}
placeholder="Type XP Value"
/>
</div>
</div>
<h2 className={clsx([styles.levelTotal, styles.withDivider])}>
New XP Total: {FormatUtils.renderLocaleNumber(newXpTotal)} (Level{" "}
{CharacterUtils.deriveXpLevel(newXpTotal, ruleData)})
</h2>
</div>
);
};