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 { hpInfo: CharacterHitPointInfo | HitPointInfo; creature?: Creature; showOriginalMax?: boolean; showPermanentInputs?: boolean; } export const HitPointsSummary: FC = ({ 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( hpInfo.remainingHp ); const [tempValue, setTempValue] = useState(hpInfo.tempHp); const [isMaxDifferent, setIsMaxDifferent] = useState(false); const currentEditorRef = useRef(null); const tempEditorRef = useRef(null); const currentId = uuidv4(); const tempId = uuidv4(); const dispatch = useDispatch(); /* --- Current hit point functions --- */ const onCurrentInputChange = ( evt: React.ChangeEvent ): void => { setCurrentValue(helperUtils.parseInputInt(evt.target.value)); }; const onCurrentInputClick = ( evt: React.MouseEvent ): void => { evt.stopPropagation(); evt.nativeEvent.stopImmediatePropagation(); }; const onCurrentClick = (evt: React.MouseEvent): 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 ): void => { let current: number | null = helperUtils.parseInputInt(evt.target.value); dispatchCurrentHp(current); }; const onCurrentInputEnter = ( evt: React.KeyboardEvent ): 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): void => { evt.stopPropagation(); evt.nativeEvent.stopImmediatePropagation(); }; const onTempClick = (evt: React.MouseEvent): void => { if (!isReadonly) { evt.stopPropagation(); evt.nativeEvent.stopImmediatePropagation(); setIsTempEditorVisible(true); } }; const onTempInputChange = ( evt: React.ChangeEvent ): 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): void => { let temp: number | null = helperUtils.parseInputInt(evt.target.value); dispatchTempHp(temp); }; const onTempInputEnter = ( evt: React.KeyboardEvent ): 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 ( ); } if (!tempValue) { return ( -- ); } return ( {tempValue} ); }; return ( Current {isCurrentEditorVisible || showPermanentInputs ? ( ) : ( {currentValue} )} / Max {`Max hit points`} 0 ? styles.positive : styles.negative), ])} > {hpInfo.totalHp} {isMaxDifferent && "baseTotalHp" in hpInfo && ( ({hpInfo.baseTotalHp}) )} Temp {renderTempHitPoints()} ); };