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,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>
|
||||
);
|
||||
};
|
||||
+136
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user