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:
2025-05-28 11:50:03 -07:00
commit 8df9031d27
3592 changed files with 319051 additions and 0 deletions
@@ -0,0 +1,104 @@
import clsx from "clsx";
import { FC, HTMLAttributes } from "react";
import { Constants } from "@dndbeyond/character-rules-engine";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import styles from "./styles.module.css";
interface Props extends HTMLAttributes<HTMLDivElement> {}
export const DeathSummary: FC<Props> = ({ className, onClick, ...props }) => {
const { deathSaveInfo, ruleData, ruleDataUtils, deathCause, characterTheme } =
useCharacterEngine();
const renderDeathSavesSummaryMarkGroup = (
key: string,
label: string,
activeCount: number,
totalCount: number,
isDarkMode: boolean
): React.ReactNode => {
let marks: Array<React.ReactNode> = [];
for (let i = 0; i < totalCount; i++) {
marks.push(
<span
key={`${key}-${i}`}
className={clsx([
styles.mark,
i < activeCount ? styles[`activeMark-${key}`] : styles.inactiveMark,
isDarkMode &&
i < activeCount &&
styles[`activeMarkDarkMode-${key}`],
])}
/>
);
}
return (
<div className={styles.group}>
<h2 className={styles.label}>{label}</h2>
<span className={styles.marks}>{marks}</span>
</div>
);
};
return (
<>
{/* If the death cause is exhaustion, render the exhaustion summary. Otherwise, render the death saves summary. */}
{deathCause === Constants.DeathCauseEnum.CONDITION ? (
<div
className={clsx([styles.container, className])}
onClick={onClick}
{...props}
>
<div className={styles.exhaustion}>
<div
className={clsx([
styles.icon,
characterTheme.isDarkMode && styles.iconDarkMode,
])}
/>
<h2 className={clsx([styles.label, styles.exhaustionLabel])}>
Exhaustion Level 6
</h2>
</div>
<h1 className={styles.deathLabel}>Death</h1>
</div>
) : (
<div
className={clsx([styles.container, className])}
onClick={onClick}
{...props}
>
<div className={styles.deathSaves}>
<div
className={clsx([
styles.icon,
characterTheme.isDarkMode && styles.iconDarkMode,
])}
/>
<div className={styles.markGroupsContainer}>
{renderDeathSavesSummaryMarkGroup(
"fail",
"Failure",
deathSaveInfo.failCount,
ruleDataUtils.getMaxDeathsavesFail(ruleData),
characterTheme.isDarkMode
)}
{renderDeathSavesSummaryMarkGroup(
"success",
"Success",
deathSaveInfo.successCount,
ruleDataUtils.getMaxDeathsavesSuccess(ruleData),
characterTheme.isDarkMode
)}
</div>
</div>
<h1 className={styles.deathLabel}>Death Saves</h1>
</div>
)}
</>
);
};
@@ -0,0 +1,62 @@
import clsx from "clsx";
import { FC, HTMLAttributes } from "react";
import { useSelector } from "react-redux";
import {
BeveledBoxSvg317x89,
BoxBackground,
} from "@dndbeyond/character-components/es";
import { Constants } from "@dndbeyond/character-rules-engine";
import { useSidebar } from "~/contexts/Sidebar/Sidebar";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
import { PaneComponentEnum } from "../Sidebar/types";
import { DeathSummary } from "./DeathSummary";
import { HitPointsQuickAdjust } from "./HitPointsQuickAdjust";
import { HitPointsSummary } from "./HitPointsSummary";
import styles from "./styles.module.css";
interface Props extends HTMLAttributes<HTMLDivElement> {}
export const HitPointsBox: FC<Props> = ({ className, ...props }) => {
const {
pane: { paneHistoryStart },
} = useSidebar();
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
const { hpInfo, deathCause, characterTheme } = useCharacterEngine();
const onClick = (evt: React.MouseEvent): void => {
if (!isReadonly) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
paneHistoryStart(PaneComponentEnum.HEALTH_MANAGE);
}
};
return (
<div
className={clsx([styles.hitPointsBox, className])}
onClick={onClick}
{...props}
>
<BoxBackground
StyleComponent={BeveledBoxSvg317x89}
theme={characterTheme}
/>
{deathCause === Constants.DeathCauseEnum.CONDITION ||
hpInfo.remainingHp <= 0 ? (
<DeathSummary />
) : (
<div className={styles.content}>
<h1 className={styles.label}>Hit Points</h1>
<HitPointsQuickAdjust />
<HitPointsSummary hpInfo={hpInfo} />
</div>
)}
</div>
);
};
@@ -0,0 +1,136 @@
import clsx from "clsx";
import { FC, HTMLAttributes, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Button } from "~/components/Button";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import { useUnpropagatedClick } from "~/hooks/useUnpropagatedClick";
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
import { HP_DAMAGE_TAKEN_VALUE } from "../../../constants";
import styles from "./styles.module.css";
interface Props extends HTMLAttributes<HTMLDivElement | HTMLButtonElement> {}
export const HitPointsQuickAdjust: FC<Props> = ({ className, ...props }) => {
const dispatch = useDispatch();
const { hpInfo, characterActions, characterUtils, helperUtils } =
useCharacterEngine();
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
const [adjustmentValue, setAdjustmentValue] = useState<number | null>(null);
const [isFocused, setIsFocused] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const adjustHitPoints = (adjustmentSign: number): void => {
const difference = adjustmentSign * (adjustmentValue ?? 0);
const { newTemp, newRemovedHp } = characterUtils.calculateHitPoints(
hpInfo,
difference
);
dispatch(characterActions.hitPointsSet(newRemovedHp, newTemp));
setAdjustmentValue(null);
setIsHovered(false);
};
const onHealClick = useUnpropagatedClick(() => {
adjustHitPoints(1);
});
const onDamageClick = useUnpropagatedClick(() => {
adjustHitPoints(-1);
});
const onFocus = (): void => {
setIsFocused(true);
};
const onMouseEnter = (): void => {
setIsHovered(true);
};
const onMouseLeave = (): void => {
setIsHovered(false);
};
const onBlur = (evt: React.FocusEvent<HTMLInputElement>): void => {
setAdjustmentValue(helperUtils.parseInputInt(evt.target.value));
setIsFocused(false);
};
const onChange = (evt: React.ChangeEvent<HTMLInputElement>): void => {
setAdjustmentValue(helperUtils.parseInputInt(evt.target.value));
};
const onWheel = (evt: WheelEvent): void => {
if (isHovered || isFocused) {
evt.preventDefault();
let direction = evt.deltaY < 0 ? 1 : -1;
if (!isReadonly) {
let newValue = (adjustmentValue ?? 0) + direction;
newValue = helperUtils.clampInt(newValue ?? 0, 0, hpInfo.totalHp * 2);
setAdjustmentValue(newValue);
}
}
};
const onScroll = (evt: Event): void => {
if (isHovered || isFocused) {
evt.preventDefault();
}
};
useEffect(() => {
window.addEventListener("wheel", onWheel, { passive: false });
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("wheel", onWheel);
window.removeEventListener("scroll", onScroll);
};
});
return (
<div className={clsx([styles.container, className])} {...props}>
<Button
size="xx-small"
variant="outline"
themed
disabled={isReadonly}
onClick={onHealClick}
className={clsx([styles.button, styles.heal])}
>
Heal
</Button>
<input
className={styles.input}
data-testid="hp-adjust-input"
type="number"
aria-label="Hit Points Adjustment"
value={adjustmentValue ?? ""}
min={HP_DAMAGE_TAKEN_VALUE.MIN}
max={HP_DAMAGE_TAKEN_VALUE.MAX}
onClick={useUnpropagatedClick()}
onFocus={onFocus}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onChange={onChange}
onBlur={onBlur}
readOnly={isReadonly}
/>
<Button
size="xx-small"
variant="outline"
themed
disabled={isReadonly}
onClick={onDamageClick}
className={clsx([styles.button, styles.damage])}
>
Damage
</Button>
</div>
);
};
@@ -0,0 +1,353 @@
import clsx from "clsx";
import { FC, HTMLAttributes, useEffect, useState, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { v4 as uuidv4 } from "uuid";
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
import accessibility from "~/styles/accessibility.module.css";
import { HP_TEMP_VALUE } from "~/subApps/sheet/constants";
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
import { CharacterHitPointInfo, Creature, HitPointInfo } from "~/types";
import styles from "./styles.module.css";
interface Props extends HTMLAttributes<HTMLDivElement> {
hpInfo: CharacterHitPointInfo | HitPointInfo;
creature?: Creature;
showOriginalMax?: boolean;
showPermanentInputs?: boolean;
}
export const HitPointsSummary: FC<Props> = ({
hpInfo,
creature,
showOriginalMax = false,
showPermanentInputs = false,
className,
...props
}) => {
// TODO This hitpoints summary doesn't work as a generic component for vehicles yet. Works for creatures and characters.
const { characterActions, creatureUtils, helperUtils } = useCharacterEngine();
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
const [isCurrentEditorVisible, setIsCurrentEditorVisible] = useState(false);
const [isTempEditorVisible, setIsTempEditorVisible] = useState(false);
const [currentValue, setCurrentValue] = useState<number | null>(
hpInfo.remainingHp
);
const [tempValue, setTempValue] = useState<number | null>(hpInfo.tempHp);
const [isMaxDifferent, setIsMaxDifferent] = useState<boolean>(false);
const currentEditorRef = useRef<HTMLInputElement | null>(null);
const tempEditorRef = useRef<HTMLInputElement | null>(null);
const currentId = uuidv4();
const tempId = uuidv4();
const dispatch = useDispatch();
/* --- Current hit point functions --- */
const onCurrentInputChange = (
evt: React.ChangeEvent<HTMLInputElement>
): void => {
setCurrentValue(helperUtils.parseInputInt(evt.target.value));
};
const onCurrentInputClick = (
evt: React.MouseEvent<HTMLInputElement>
): void => {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
};
const onCurrentClick = (evt: React.MouseEvent<HTMLButtonElement>): void => {
if (!isReadonly) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
setIsCurrentEditorVisible(true);
}
};
const dispatchCurrentHp = (value: number | null) => {
if (value === null) {
value = hpInfo.remainingHp;
} else {
let newRemovedAmount = hpInfo.totalHp - value;
newRemovedAmount = helperUtils.clampInt(
newRemovedAmount,
0,
hpInfo.totalHp
);
value = hpInfo.totalHp - newRemovedAmount;
if (newRemovedAmount !== hpInfo.removedHp) {
creature
? dispatch(
characterActions.creatureHitPointsSet(
creatureUtils.getMappingId(creature),
newRemovedAmount,
hpInfo.tempHp ?? HP_TEMP_VALUE.MIN
)
)
: dispatch(
characterActions.hitPointsSet(
newRemovedAmount,
hpInfo.tempHp ?? HP_TEMP_VALUE.MIN
)
);
}
}
setCurrentValue(value);
setIsCurrentEditorVisible(false);
};
const onCurrentInputBlur = (
evt: React.FocusEvent<HTMLInputElement>
): void => {
let current: number | null = helperUtils.parseInputInt(evt.target.value);
dispatchCurrentHp(current);
};
const onCurrentInputEnter = (
evt: React.KeyboardEvent<HTMLInputElement>
): void => {
if (evt.key === "Enter") {
let current: number | null = helperUtils.parseInputInt(
evt.currentTarget.value
);
dispatchCurrentHp(current);
}
};
/* --- Temp hit point functions --- */
const onTempInputClick = (evt: React.MouseEvent<HTMLInputElement>): void => {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
};
const onTempClick = (evt: React.MouseEvent<HTMLButtonElement>): void => {
if (!isReadonly) {
evt.stopPropagation();
evt.nativeEvent.stopImmediatePropagation();
setIsTempEditorVisible(true);
}
};
const onTempInputChange = (
evt: React.ChangeEvent<HTMLInputElement>
): void => {
setTempValue(helperUtils.parseInputInt(evt.target.value));
};
const dispatchTempHp = (value: number | null) => {
if (value === null) {
value = hpInfo.tempHp;
} else {
value = helperUtils.clampInt(value, HP_TEMP_VALUE.MIN, HP_TEMP_VALUE.MAX);
if (value !== hpInfo.tempHp) {
creature
? dispatch(
characterActions.creatureHitPointsSet(
creatureUtils.getMappingId(creature),
hpInfo.removedHp,
value
)
)
: dispatch(characterActions.hitPointsSet(hpInfo.removedHp, value));
}
}
setTempValue(value);
setIsTempEditorVisible(false);
};
const onTempInputBlur = (evt: React.FocusEvent<HTMLInputElement>): void => {
let temp: number | null = helperUtils.parseInputInt(evt.target.value);
dispatchTempHp(temp);
};
const onTempInputEnter = (
evt: React.KeyboardEvent<HTMLInputElement>
): void => {
if (evt.key === "Enter") {
let temp: number | null = helperUtils.parseInputInt(
evt.currentTarget.value
);
dispatchTempHp(temp);
}
};
/* --- useEffects --- */
useEffect(() => {
if (isTempEditorVisible) {
tempEditorRef.current?.focus();
}
}, [isTempEditorVisible]);
useEffect(() => {
if (isCurrentEditorVisible) {
currentEditorRef.current?.focus();
}
}, [isCurrentEditorVisible]);
useEffect(() => {
setCurrentValue(hpInfo.remainingHp);
}, [hpInfo.remainingHp]);
useEffect(() => {
setTempValue(hpInfo.tempHp);
}, [hpInfo.tempHp]);
useEffect(() => {
if (showOriginalMax && (hpInfo.overrideHp || hpInfo.bonusHp)) {
setIsMaxDifferent(true);
} else {
setIsMaxDifferent(false);
}
}, [hpInfo.overrideHp, hpInfo.bonusHp, showOriginalMax]);
/* --- Render Functions --- */
const renderTempHitPoints = (): React.ReactNode => {
if (isTempEditorVisible || showPermanentInputs) {
return (
<div>
<input
data-testid="temp-hp-input"
id={tempId}
ref={tempEditorRef}
className={styles.input}
type="number"
value={tempValue ?? ""}
min={HP_TEMP_VALUE.MIN}
max={HP_TEMP_VALUE.MAX}
onChange={onTempInputChange}
onBlur={onTempInputBlur}
onClick={onTempInputClick}
onKeyDown={onTempInputEnter}
/>
</div>
);
}
if (!tempValue) {
return (
<button
id={tempId}
className={clsx([
styles.valueButton,
styles.tempEmpty,
styles.number,
])}
onClick={onTempClick}
>
--
</button>
);
}
return (
<button
id={tempId}
className={clsx([styles.valueButton, styles.number])}
onClick={onTempClick}
>
{tempValue}
</button>
);
};
return (
<div className={clsx([styles.container, className])} {...props}>
<div className={styles.innerContainer}>
<div className={styles.item}>
<label
htmlFor={currentId}
className={styles.label}
aria-label={`Current hit points ${currentValue}, change current hit points`}
>
Current
</label>
{isCurrentEditorVisible || showPermanentInputs ? (
<div>
<input
data-testid="current-hp-input"
id={currentId}
ref={currentEditorRef}
className={styles.input}
type="number"
value={currentValue ?? ""}
min={0}
max={hpInfo.totalHp}
onChange={onCurrentInputChange}
onBlur={onCurrentInputBlur}
onClick={onCurrentInputClick}
onKeyDown={onCurrentInputEnter}
/>
</div>
) : (
<button
id={currentId}
className={clsx([styles.valueButton, styles.number])}
onClick={onCurrentClick}
>
{currentValue}
</button>
)}
</div>
<div className={styles.item}>
<div className={styles.spacer} />
<span className={clsx([styles.number, styles.slash])}>/</span>
</div>
<div className={styles.item}>
<span className={styles.label} aria-hidden="true">
Max
</span>
<span
className={accessibility.screenreaderOnly}
>{`Max hit points`}</span>
<div className={clsx([styles.number, styles.maxContainer])}>
<span
data-testid="max-hp"
className={clsx([
styles.max,
isMaxDifferent &&
"baseTotalHp" in hpInfo &&
(hpInfo.totalHp - hpInfo.baseTotalHp > 0
? styles.positive
: styles.negative),
])}
>
{hpInfo.totalHp}
</span>
{isMaxDifferent && "baseTotalHp" in hpInfo && (
<span
data-testid="origin-max-hp"
className={clsx([styles.originalMax])}
>
({hpInfo.baseTotalHp})
</span>
)}
</div>
</div>
</div>
<div className={clsx([styles.item, styles.temp])}>
<label
htmlFor={tempId}
aria-label={
!tempValue
? "Temporary hit points 0, set temporary hit points"
: `Temporary hit points ${tempValue}, change temporary hit points`
}
className={styles.label}
>
Temp
</label>
{renderTempHitPoints()}
</div>
</div>
);
};