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,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>
);
};