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,606 @@
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
FC,
|
||||
Fragment,
|
||||
HTMLAttributes,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleHeaderCallout,
|
||||
CollapsibleHeaderContent,
|
||||
CreatureName,
|
||||
InfusionPreview,
|
||||
} from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
characterActions,
|
||||
Constants,
|
||||
Creature,
|
||||
CreatureUtils,
|
||||
InfusionUtils,
|
||||
serviceDataActions,
|
||||
SourceUtils,
|
||||
ValueUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { Button } from "~/components/Button";
|
||||
import { EditableName } from "~/components/EditableName";
|
||||
import { HtmlContent } from "~/components/HtmlContent";
|
||||
import { Reference } from "~/components/Reference";
|
||||
import { TagGroup } from "~/components/TagGroup";
|
||||
import { useSidebar } from "~/contexts/Sidebar";
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { CreatureBlock } from "~/subApps/sheet/components/CreatureBlock";
|
||||
import { Header } from "~/subApps/sheet/components/Sidebar/components/Header";
|
||||
import { Heading } from "~/subApps/sheet/components/Sidebar/components/Heading";
|
||||
import { Preview } from "~/subApps/sheet/components/Sidebar/components/Preview";
|
||||
import {
|
||||
PaneComponentEnum,
|
||||
PaneIdentifiersCreature,
|
||||
} from "~/subApps/sheet/components/Sidebar/types";
|
||||
|
||||
import CustomizeDataEditor from "../../../../../../tools/js/Shared/components/CustomizeDataEditor";
|
||||
import EditorBox from "../../../../../../tools/js/Shared/components/EditorBox";
|
||||
import ValueEditor from "../../../../../../tools/js/Shared/components/ValueEditor";
|
||||
import { RemoveButton } from "../../../../../../tools/js/Shared/components/common/Button";
|
||||
import { appEnvSelectors } from "../../../../../../tools/js/Shared/selectors";
|
||||
import { PaneIdentifierUtils } from "../../../../../../tools/js/Shared/utils";
|
||||
import { DeathSummary } from "../../../HitPointsBox/DeathSummary";
|
||||
import { HitPointsSummary } from "../../../HitPointsBox/HitPointsSummary";
|
||||
import { PaneInitFailureContent } from "../../components/PaneInitFailureContent";
|
||||
import { QuickActions } from "../../components/QuickActions";
|
||||
import { HitPointsAdjuster } from "../HitPointsManagePane/HitPointsAdjuster";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
identifiers: PaneIdentifiersCreature | null;
|
||||
}
|
||||
|
||||
export const CreaturePane: FC<Props> = ({ identifiers, ...props }) => {
|
||||
const { pane } = useSidebar();
|
||||
|
||||
const {
|
||||
ruleData,
|
||||
creatures,
|
||||
entityValueLookup,
|
||||
snippetData,
|
||||
infusionChoiceLookup,
|
||||
proficiencyBonus,
|
||||
characterTheme: theme,
|
||||
hpInfo,
|
||||
deathCause,
|
||||
} = useCharacterEngine();
|
||||
const isReadonly = useSelector(appEnvSelectors.getIsReadonly);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getCreature = useCallback((): Creature | null => {
|
||||
return (
|
||||
creatures.find(
|
||||
(creature) => identifiers?.id === CreatureUtils.getMappingId(creature)
|
||||
) ?? null
|
||||
);
|
||||
}, [creatures, identifiers]);
|
||||
|
||||
const [creature, setCreature] = useState<Creature | null>(getCreature());
|
||||
const [isCustomizeClosed, setIsCustomizeClosed] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const foundCreature = getCreature();
|
||||
|
||||
setCreature(foundCreature);
|
||||
}, [getCreature]);
|
||||
|
||||
const getData = (): Record<string, any> => {
|
||||
if (!creature) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
name: CreatureUtils.getName(creature),
|
||||
description: creature.description,
|
||||
groupId: CreatureUtils.getGroupId(creature),
|
||||
};
|
||||
};
|
||||
|
||||
const handleToggleCustomize = () => {
|
||||
setIsCustomizeClosed(!isCustomizeClosed);
|
||||
};
|
||||
|
||||
const dispatchTempHp = (value: number): void => {
|
||||
if (!creature) {
|
||||
return;
|
||||
}
|
||||
|
||||
const useOwnerHp = CreatureUtils.getUseOwnerHp(creature);
|
||||
const id = CreatureUtils.getMappingId(creature);
|
||||
const hitPointInfo = useOwnerHp
|
||||
? hpInfo
|
||||
: CreatureUtils.getHitPointInfo(creature);
|
||||
|
||||
dispatch(
|
||||
useOwnerHp
|
||||
? characterActions.hitPointsSet(hitPointInfo.removedHp, value)
|
||||
: characterActions.creatureHitPointsSet(
|
||||
id,
|
||||
hitPointInfo.removedHp,
|
||||
value
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleQuickTempAction = (value: number): void => {
|
||||
dispatchTempHp(value);
|
||||
};
|
||||
|
||||
const handleSaveProperties = (properties: Record<string, any>): void => {
|
||||
if (!creature) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = getData();
|
||||
const prevData: Record<string, any> = data || {};
|
||||
|
||||
const newProperties: Record<string, any> = {
|
||||
...prevData,
|
||||
...properties,
|
||||
};
|
||||
|
||||
dispatch(
|
||||
characterActions.creatureDataSet(
|
||||
CreatureUtils.getMappingId(creature),
|
||||
newProperties as any
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleDataUpdate = (data: Record<string, any>): void => {
|
||||
if (!data.name) {
|
||||
data.name = null;
|
||||
}
|
||||
|
||||
handleSaveProperties(data);
|
||||
};
|
||||
|
||||
const handleCustomDataUpdate = (
|
||||
key: number,
|
||||
value: string,
|
||||
source: string | null
|
||||
): void => {
|
||||
if (!creature) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
characterActions.valueSet(
|
||||
key,
|
||||
value,
|
||||
source,
|
||||
ValueUtils.hack__toString(CreatureUtils.getMappingId(creature)),
|
||||
ValueUtils.hack__toString(
|
||||
CreatureUtils.getMappingEntityTypeId(creature)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveCustomizations = (): void => {
|
||||
if (creature) {
|
||||
dispatch(
|
||||
characterActions.creatureCustomizationsDelete(
|
||||
CreatureUtils.getMappingId(creature),
|
||||
CreatureUtils.getMappingEntityTypeId(creature)
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (): void => {
|
||||
if (!creature) {
|
||||
return;
|
||||
}
|
||||
|
||||
pane.paneHistoryStart(PaneComponentEnum.EXTRA_MANAGE);
|
||||
dispatch(
|
||||
characterActions.creatureRemove(CreatureUtils.getMappingId(creature))
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveInfusion = (): void => {
|
||||
if (!creature) {
|
||||
return;
|
||||
}
|
||||
|
||||
const infusion = CreatureUtils.getInfusion(creature);
|
||||
if (infusion) {
|
||||
const infusionId = InfusionUtils.getId(infusion);
|
||||
if (infusionId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const choiceKey = InfusionUtils.getChoiceKey(infusion);
|
||||
if (choiceKey !== null) {
|
||||
pane.paneHistoryStart(
|
||||
PaneComponentEnum.INFUSION_CHOICE,
|
||||
PaneIdentifierUtils.generateInfusionChoice(choiceKey)
|
||||
);
|
||||
}
|
||||
dispatch(
|
||||
serviceDataActions.infusionMappingDestroy(
|
||||
infusionId,
|
||||
InfusionUtils.getInventoryMappingId(infusion)
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInfusionClick = (): void => {
|
||||
if (!creature) {
|
||||
return;
|
||||
}
|
||||
|
||||
const infusion = CreatureUtils.getInfusion(creature);
|
||||
if (infusion) {
|
||||
const choiceKey = InfusionUtils.getChoiceKey(infusion);
|
||||
if (choiceKey !== null) {
|
||||
pane.paneHistoryPush(
|
||||
PaneComponentEnum.INFUSION_CHOICE,
|
||||
PaneIdentifierUtils.generateInfusionChoice(choiceKey)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeathSummaryClick = (evt: React.MouseEvent): void => {
|
||||
if (!isReadonly) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
pane.paneHistoryPush(PaneComponentEnum.HEALTH_MANAGE);
|
||||
}
|
||||
};
|
||||
|
||||
const renderHealthAdjuster = (): React.ReactNode => {
|
||||
if (!creature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const creatureHpInfo = CreatureUtils.getHitPointInfo(creature);
|
||||
|
||||
// Key to check if the creature has useOwnerHp set to true
|
||||
const creatureGroupName = CreatureUtils.getGroupInfo(creature)?.name;
|
||||
const useOwnerHp = CreatureUtils.getUseOwnerHp(creature);
|
||||
const initialTempHp = CreatureUtils.getInitialTempHp(creature);
|
||||
|
||||
const hp = useOwnerHp ? hpInfo : creatureHpInfo;
|
||||
|
||||
const tempHp = hp.tempHp ?? 0;
|
||||
|
||||
const extraNode: React.ReactNode = (
|
||||
<>
|
||||
<span className={clsx([tempHp > 0 && styles.hasTempHp])}>
|
||||
{hp.remainingHp + tempHp}
|
||||
</span>
|
||||
<span className={styles.valueSep}>/</span>
|
||||
<span className={clsx([tempHp > 0 && styles.hasTempHp])}>
|
||||
{hp.totalHp + tempHp}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
const headerCalloutNode: React.ReactNode = (
|
||||
<CollapsibleHeaderCallout extra={extraNode} value={null} />
|
||||
);
|
||||
|
||||
const headerNode: React.ReactNode = (
|
||||
<CollapsibleHeaderContent
|
||||
heading="Hit Points"
|
||||
callout={headerCalloutNode}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={clsx([styles.adjuster, styles.hasSeparator])}>
|
||||
<Collapsible header={headerNode}>
|
||||
{useOwnerHp &&
|
||||
(deathCause === Constants.DeathCauseEnum.CONDITION ||
|
||||
hpInfo.remainingHp <= 0) ? (
|
||||
<DeathSummary
|
||||
className={styles.deathSummary}
|
||||
onClick={handleDeathSummaryClick}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.hitPointsContainer}>
|
||||
{creatureGroupName === Constants.DB_STRING_WILDSHAPE_2024 &&
|
||||
useOwnerHp && (
|
||||
<p className={clsx([styles.wildshapeInfo, styles.block])}>
|
||||
Wild Shape modifies your character's HP. Your sheet will
|
||||
reflect these changes.
|
||||
</p>
|
||||
)}
|
||||
<HitPointsSummary
|
||||
hpInfo={hp}
|
||||
creature={useOwnerHp ? undefined : creature}
|
||||
showPermanentInputs
|
||||
/>
|
||||
{!isReadonly && (
|
||||
<HitPointsAdjuster
|
||||
hpInfo={hp}
|
||||
creature={useOwnerHp ? undefined : creature}
|
||||
/>
|
||||
)}
|
||||
{creatureGroupName === Constants.DB_STRING_WILDSHAPE_2024 &&
|
||||
initialTempHp !== 0 && (
|
||||
<p className={styles.wildshapeInfo}>
|
||||
When you assume a Wild Shape form, you gain{" "}
|
||||
<b>{initialTempHp}</b> Temporary HP.
|
||||
</p>
|
||||
)}
|
||||
{!isReadonly && renderQuickActions()}
|
||||
</div>
|
||||
)}
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderQuickActions = (): React.ReactNode => {
|
||||
if (!creature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If initialTempHp exists and is not 0, render a button to add the initial temp hp to the creature.
|
||||
const initialTempHp = CreatureUtils.getInitialTempHp(creature);
|
||||
if (!initialTempHp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const actions = [] as {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
disabled: boolean;
|
||||
}[];
|
||||
|
||||
actions.push({
|
||||
label: `Set Temp HP to ${initialTempHp}`,
|
||||
onClick: () => handleQuickTempAction(initialTempHp),
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
return <QuickActions actions={actions}></QuickActions>;
|
||||
};
|
||||
|
||||
const renderDescription = (
|
||||
label: React.ReactNode,
|
||||
description: string | null
|
||||
): React.ReactNode => {
|
||||
if (!description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.description}>
|
||||
{label && <Heading>{label}</Heading>}
|
||||
<HtmlContent html={description} withoutTooltips />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTags = (): React.ReactNode => {
|
||||
if (!creature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tags = CreatureUtils.getTags(creature);
|
||||
const envTags = CreatureUtils.getEnvironmentTags(creature);
|
||||
|
||||
return (
|
||||
<>
|
||||
{tags.length > 0 && <TagGroup label="Tags" tags={tags} />}
|
||||
{envTags.length > 0 && <TagGroup label="Habitats" tags={envTags} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCustomize = (): React.ReactNode => {
|
||||
if (isReadonly || !creature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const useOwnerHp = CreatureUtils.getUseOwnerHp(creature);
|
||||
|
||||
const optionRestrictions: Record<number, Array<number> | null> = {};
|
||||
const groupInfo = CreatureUtils.getGroupInfo(creature);
|
||||
if (groupInfo?.monsterTypes?.length) {
|
||||
optionRestrictions[Constants.AdjustmentTypeEnum.CREATURE_TYPE_OVERRIDE] =
|
||||
groupInfo.monsterTypes;
|
||||
}
|
||||
|
||||
const isCustomized = CreatureUtils.isCustomized(creature);
|
||||
|
||||
const valueEditors = [
|
||||
Constants.AdjustmentTypeEnum.CREATURE_SIZE,
|
||||
Constants.AdjustmentTypeEnum.CREATURE_TYPE_OVERRIDE,
|
||||
Constants.AdjustmentTypeEnum.CREATURE_ALIGNMENT,
|
||||
Constants.AdjustmentTypeEnum.CREATURE_AC,
|
||||
Constants.AdjustmentTypeEnum.CREATURE_HIT_POINTS,
|
||||
Constants.AdjustmentTypeEnum.CREATURE_NOTES,
|
||||
];
|
||||
// Filter out hitpoints if the creature has useOwnerHp set to true
|
||||
if (useOwnerHp) {
|
||||
valueEditors.splice(
|
||||
valueEditors.indexOf(Constants.AdjustmentTypeEnum.CREATURE_HIT_POINTS),
|
||||
1
|
||||
);
|
||||
}
|
||||
const labelOverrides = {
|
||||
[Constants.AdjustmentTypeEnum.CREATURE_AC]: "Armor Class",
|
||||
[Constants.AdjustmentTypeEnum.CREATURE_ALIGNMENT]: "Alignment",
|
||||
[Constants.AdjustmentTypeEnum.CREATURE_HIT_POINTS]: "Max HP",
|
||||
[Constants.AdjustmentTypeEnum.CREATURE_SIZE]: "Size",
|
||||
[Constants.AdjustmentTypeEnum.CREATURE_TYPE_OVERRIDE]: "Type",
|
||||
[Constants.AdjustmentTypeEnum.CREATURE_NOTES]: "Notes",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.hasSeparator}>
|
||||
<Collapsible
|
||||
layoutType={"minimal"}
|
||||
header={`Customize${isCustomized ? "*" : ""}`}
|
||||
collapsed={isCustomizeClosed}
|
||||
onChangeHandler={handleToggleCustomize}
|
||||
>
|
||||
<EditorBox className={styles.editor}>
|
||||
<CustomizeDataEditor
|
||||
data={getData()}
|
||||
enableName={true}
|
||||
onDataUpdate={handleDataUpdate}
|
||||
/>
|
||||
<ValueEditor
|
||||
dataLookup={ValueUtils.getEntityData(
|
||||
entityValueLookup,
|
||||
ValueUtils.hack__toString(CreatureUtils.getMappingId(creature)),
|
||||
ValueUtils.hack__toString(
|
||||
CreatureUtils.getMappingEntityTypeId(creature)
|
||||
)
|
||||
)}
|
||||
onDataUpdate={handleCustomDataUpdate}
|
||||
valueEditors={valueEditors}
|
||||
labelOverrides={labelOverrides}
|
||||
layoutType={"standard"}
|
||||
optionRestrictions={optionRestrictions}
|
||||
ruleData={ruleData}
|
||||
/>
|
||||
<RemoveButton
|
||||
enableConfirm={true}
|
||||
size="medium"
|
||||
style="filled"
|
||||
disabled={!isCustomized}
|
||||
isInteractive={isCustomized}
|
||||
onClick={handleRemoveCustomizations}
|
||||
>
|
||||
{isCustomized ? "Remove" : "No"} Customizations
|
||||
</RemoveButton>
|
||||
</EditorBox>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderInfusionPreview = (): React.ReactNode => {
|
||||
if (!creature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const infusion = CreatureUtils.getInfusion(creature);
|
||||
if (infusion) {
|
||||
return (
|
||||
<div className={styles.hasSeparator}>
|
||||
<InfusionPreview
|
||||
infusion={infusion}
|
||||
snippetData={snippetData}
|
||||
ruleData={ruleData}
|
||||
infusionChoiceLookup={infusionChoiceLookup}
|
||||
onClick={handleInfusionClick}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const key: string = creature ? CreatureUtils.getUniqueKey(creature) : "";
|
||||
const groupInfo = creature ? CreatureUtils.getGroupInfo(creature) : null;
|
||||
const largeAvatarUrl = creature
|
||||
? CreatureUtils.getLargeAvatarUrl(creature)
|
||||
: null;
|
||||
|
||||
const infusion = creature ? CreatureUtils.getInfusion(creature) : null;
|
||||
|
||||
return (
|
||||
<div key={key} {...props}>
|
||||
{!creature ? (
|
||||
<>
|
||||
<Header>Missing Creature</Header>
|
||||
<PaneInitFailureContent />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Header
|
||||
parent={groupInfo?.name || null}
|
||||
preview={
|
||||
<Preview imageUrl={CreatureUtils.getAvatarUrl(creature)} />
|
||||
}
|
||||
>
|
||||
<EditableName onClick={handleToggleCustomize}>
|
||||
<CreatureName theme={theme} creature={creature} />
|
||||
</EditableName>
|
||||
</Header>
|
||||
{CreatureUtils.isHomebrew(creature) ? (
|
||||
<Reference isDarkMode={theme.isDarkMode} name="Homebrew" />
|
||||
) : (
|
||||
SourceUtils.getSourceFullNames(
|
||||
CreatureUtils.getSources(creature),
|
||||
ruleData
|
||||
).map((source, idx) => {
|
||||
return (
|
||||
<Fragment key={source}>
|
||||
{idx > 0 ? " /" : ""}{" "}
|
||||
<Reference isDarkMode={theme.isDarkMode} name={source} />
|
||||
</Fragment>
|
||||
);
|
||||
})
|
||||
)}
|
||||
{renderCustomize()}
|
||||
{renderHealthAdjuster()}
|
||||
<div className={styles.block}>
|
||||
<CreatureBlock
|
||||
variant="default"
|
||||
creature={creature}
|
||||
ruleData={ruleData}
|
||||
/>
|
||||
</div>
|
||||
{largeAvatarUrl && (
|
||||
<img
|
||||
className={styles.img}
|
||||
src={largeAvatarUrl}
|
||||
alt={CreatureUtils.getDefinitionName(creature)}
|
||||
/>
|
||||
)}
|
||||
{renderTags()}
|
||||
{renderDescription(
|
||||
null,
|
||||
CreatureUtils.getCharacteristicsDescription(creature)
|
||||
)}
|
||||
{renderDescription(
|
||||
"Lair",
|
||||
CreatureUtils.getLairDescription(creature)
|
||||
)}
|
||||
{renderInfusionPreview()}
|
||||
{!isReadonly && (
|
||||
<div className={clsx([styles.hasSeparator, styles.removeButton])}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xx-small"
|
||||
themed
|
||||
onClick={() => {
|
||||
if (infusion) {
|
||||
handleRemoveInfusion();
|
||||
} else {
|
||||
handleRemove();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{infusion ? "Remove Infusion" : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user