Grabbed dndbeyond's source code
``` ~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js ```
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
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<HTMLDivElement> {
|
||||
charClass: CharClass;
|
||||
isMulticlass: boolean;
|
||||
levelsRemaining: number;
|
||||
}
|
||||
|
||||
export const ClassHeader: FC<ClassHeaderProps> = ({
|
||||
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<HtmlSelectOption> = [];
|
||||
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: (
|
||||
<div>
|
||||
{renderModalIntro(currentLevel, newTotalClassLevel)}
|
||||
<p>Are you sure you want to level down in the {name} class?</p>
|
||||
<p>
|
||||
Your hit points will be reduced by the fixed amount and class
|
||||
feature choices you have made for the higher levels will be lost.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
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: (
|
||||
<div>
|
||||
{renderModalIntro(currentLevel, newTotalClassLevel)}
|
||||
<p>Are you sure you want to level up in the {name} class?</p>
|
||||
<p>
|
||||
Your XP total will be increased to{" "}
|
||||
{FormatUtils.renderLocaleNumber(
|
||||
CharacterUtils.deriveCurrentLevelXp(
|
||||
newTotalClassLevel,
|
||||
ruleData
|
||||
)
|
||||
)}{" "}
|
||||
to match your new level.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
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: (
|
||||
<div>
|
||||
{renderModalIntro(currentLevel, newTotalClassLevel)}
|
||||
<p>Are you sure you want to level down in the {name} class?</p>
|
||||
<p>
|
||||
Your hit points will be reduced by the fixed amount and class
|
||||
feature choices you have made for the higher levels will be lost.
|
||||
</p>
|
||||
<p>
|
||||
Your XP total will be decreased to{" "}
|
||||
{FormatUtils.renderLocaleNumber(
|
||||
CharacterUtils.deriveCurrentLevelXp(
|
||||
newTotalClassLevel,
|
||||
ruleData
|
||||
)
|
||||
)}{" "}
|
||||
to match your new level.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
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: (
|
||||
<div>
|
||||
{renderModalIntro()}
|
||||
<p>
|
||||
Are you sure you want to remove all your levels in the {name} class?
|
||||
</p>
|
||||
<p>
|
||||
Your hit points will be reduced by the fixed amount and class
|
||||
feature choices you have made for the higher levels will be lost.
|
||||
</p>
|
||||
{isTypeXp && (
|
||||
<p>Your XP total will be decreased to match your new level.</p>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
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 (
|
||||
<div className={styles.modalIntro}>
|
||||
{classPortraitName}
|
||||
{currentLevel !== null && newLevel !== null && (
|
||||
<div className={styles.modalLevels}>
|
||||
<div className={styles.modalLevel}>
|
||||
Current Level: {currentLevel}
|
||||
</div>
|
||||
<div className={styles.modalLevel}>New Level: {newLevel}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const classPortraitName: ReactNode = (
|
||||
<>
|
||||
<img
|
||||
className={styles.classImg}
|
||||
src={portraitAvatarUrl}
|
||||
alt={`${name} Class avatar`}
|
||||
/>
|
||||
<div className={styles.nameGroup}>
|
||||
{subclassDefinition && (
|
||||
<div className={styles.subclassName}>{subclassDefinition.name}</div>
|
||||
)}
|
||||
<div className={styles.name} data-testid="class-name">
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx([styles.classHeader, styles.container, className])}
|
||||
{...props}
|
||||
>
|
||||
{isMulticlass && isStartingClass && (
|
||||
<div className={styles.startingClass}>Starting Class</div>
|
||||
)}
|
||||
{classPortraitName}
|
||||
<div className={styles.container}>
|
||||
<div className={styles.levelManager}>
|
||||
<label
|
||||
className={styles.levelLabel}
|
||||
htmlFor={`class-level-${classId}`}
|
||||
>
|
||||
{hasLevelXpDiff && <span className={styles.todoIcon}>!</span>}
|
||||
Level
|
||||
</label>
|
||||
<Select
|
||||
id={`class-level-${classId}`}
|
||||
value={level}
|
||||
options={levelOptions}
|
||||
initialOptionRemoved={true}
|
||||
onChangePromise={handleLevelChangePromise}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className={styles.removeClassButton}
|
||||
size="x-small"
|
||||
variant="text"
|
||||
onClick={handleRemoveClass}
|
||||
forceThemeMode="light"
|
||||
>
|
||||
<CloseIcon className={styles.icon} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user