``` ~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js ```
286 lines
8.4 KiB
TypeScript
286 lines
8.4 KiB
TypeScript
import { keyBy } from "lodash";
|
|
import React from "react";
|
|
|
|
import { Snippet } from "@dndbeyond/character-components/es";
|
|
import {
|
|
AbilityLookup,
|
|
BaseSpell,
|
|
BaseFeat,
|
|
Choice,
|
|
ChoiceUtils,
|
|
ClassDefinitionContract,
|
|
Constants,
|
|
DataOriginBaseAction,
|
|
DataOriginRefData,
|
|
InfusionChoice,
|
|
LevelScaleContract,
|
|
Option,
|
|
OptionUtils,
|
|
RuleData,
|
|
RuleDataUtils,
|
|
SnippetData,
|
|
CharacterTheme,
|
|
} from "@dndbeyond/character-rules-engine/es";
|
|
|
|
import { Reference } from "~/components/Reference";
|
|
import FeatureSnippetActions from "~/tools/js/CharacterSheet/components/FeatureSnippet/FeatureSnippetActions";
|
|
import FeatureSnippetChoices from "~/tools/js/CharacterSheet/components/FeatureSnippet/FeatureSnippetChoices";
|
|
import FeatureSnippetInfusionChoices from "~/tools/js/CharacterSheet/components/FeatureSnippet/FeatureSnippetInfusionChoices";
|
|
import FeatureSnippetOption from "~/tools/js/CharacterSheet/components/FeatureSnippet/FeatureSnippetOption";
|
|
import FeatureSnippetSpells from "~/tools/js/CharacterSheet/components/FeatureSnippet/FeatureSnippetSpells";
|
|
|
|
import styles from "./styles.module.css";
|
|
|
|
//TODO: tools/js/CharacterSheet/components/FeatureSnipper components still need to be gradually migrated to FC
|
|
interface Props {
|
|
heading: React.ReactNode;
|
|
dataOriginExtra?: string;
|
|
extraMeta: Array<string>;
|
|
className: string;
|
|
snippetData: SnippetData;
|
|
ruleData: RuleData;
|
|
abilityLookup: AbilityLookup;
|
|
sourceId: number | null;
|
|
sourcePage: number | null;
|
|
|
|
spells: Array<BaseSpell>;
|
|
feats: Array<BaseFeat>;
|
|
actions: Array<DataOriginBaseAction>;
|
|
options: Array<Option>;
|
|
choices: Array<Choice>;
|
|
infusionChoices?: Array<InfusionChoice>;
|
|
dataOriginRefData: DataOriginRefData;
|
|
|
|
levelScale?: LevelScaleContract | null;
|
|
classLevel?: number;
|
|
subclass?: ClassDefinitionContract | null;
|
|
|
|
onActionClick?: (action: DataOriginBaseAction) => void;
|
|
onActionUseSet?: (action: DataOriginBaseAction, uses: number) => void;
|
|
onSpellClick?: (spell: BaseSpell) => void;
|
|
onSpellUseSet?: (spell: BaseSpell, uses: number) => void;
|
|
onFeatureClick: () => void;
|
|
onInfusionChoiceClick?: (infusionChoice: InfusionChoice) => void;
|
|
|
|
showHeader?: boolean;
|
|
showDescription?: boolean;
|
|
|
|
isReadonly: boolean;
|
|
proficiencyBonus: number;
|
|
theme: CharacterTheme;
|
|
}
|
|
|
|
export const FeatureSnippet: React.FC<Props> = ({
|
|
heading,
|
|
dataOriginExtra,
|
|
extraMeta = [],
|
|
choices,
|
|
actions,
|
|
spells,
|
|
infusionChoices = [],
|
|
className,
|
|
snippetData,
|
|
sourceId,
|
|
sourcePage,
|
|
classLevel,
|
|
levelScale,
|
|
children,
|
|
abilityLookup,
|
|
ruleData,
|
|
subclass,
|
|
options,
|
|
feats,
|
|
dataOriginRefData,
|
|
onActionUseSet,
|
|
onActionClick,
|
|
onSpellClick,
|
|
onSpellUseSet,
|
|
onInfusionChoiceClick,
|
|
onFeatureClick,
|
|
showHeader = true,
|
|
showDescription = false,
|
|
isReadonly,
|
|
proficiencyBonus,
|
|
theme,
|
|
}) => {
|
|
const handleFeatureClick = (evt: React.MouseEvent): void => {
|
|
evt.stopPropagation();
|
|
evt.nativeEvent.stopImmediatePropagation();
|
|
|
|
if (onFeatureClick) {
|
|
onFeatureClick();
|
|
}
|
|
};
|
|
|
|
const getNonChoiceOptions = (): Array<Option> => {
|
|
let optionLookup = keyBy(options, (option) => OptionUtils.getId(option));
|
|
let usedOptionIds: Array<number> = [];
|
|
|
|
choices.forEach((choice) => {
|
|
const type = ChoiceUtils.getType(choice);
|
|
const optionValue = ChoiceUtils.getOptionValue(choice);
|
|
|
|
switch (type) {
|
|
case Constants.BuilderChoiceTypeEnum.RACIAL_TRAIT_OPTION:
|
|
case Constants.BuilderChoiceTypeEnum.FEAT_OPTION:
|
|
case Constants.BuilderChoiceTypeEnum.FEATURE_OPTION:
|
|
if (optionValue !== null && optionLookup[optionValue]) {
|
|
usedOptionIds.push(optionValue);
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
|
|
return options.filter(
|
|
(option) => !usedOptionIds.includes(OptionUtils.getId(option))
|
|
);
|
|
};
|
|
|
|
const nonChoiceOptions = getNonChoiceOptions();
|
|
const classNames = [styles.snippet, className];
|
|
const metaItems: Array<React.ReactNode> = [];
|
|
|
|
if (levelScale) {
|
|
metaItems.push(
|
|
<span className={styles.levelScale}>{levelScale.description}</span>
|
|
);
|
|
}
|
|
|
|
metaItems.push(...extraMeta);
|
|
|
|
const sourceDataLookup = RuleDataUtils.getSourceDataLookup(ruleData);
|
|
|
|
if (sourceId) {
|
|
metaItems.push(
|
|
<Reference name={sourceDataLookup[sourceId]?.name} page={sourcePage} />
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={classNames.join(" ")} onClick={handleFeatureClick}>
|
|
{showHeader && (
|
|
<div
|
|
className={`${styles.heading} ${
|
|
theme?.isDarkMode ? styles.headingDarkMode : ""
|
|
}`}
|
|
>
|
|
{heading}
|
|
<span className={styles.meta}>
|
|
{metaItems.map((metaItem, idx) => (
|
|
<span
|
|
className={`${styles.metaItem} ${
|
|
theme?.isDarkMode ? styles.metaItemDarkMode : ""
|
|
}`}
|
|
key={idx}
|
|
>
|
|
{metaItem}
|
|
</span>
|
|
))}
|
|
</span>
|
|
{dataOriginExtra && dataOriginExtra !== "Unknown" && (
|
|
<div>
|
|
<span className={styles.origin}>
|
|
<span className={styles.originLabel}>From</span>
|
|
<span className={styles.originName}>{dataOriginExtra}</span>
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
<div className={styles.content}>
|
|
<Snippet
|
|
snippetData={snippetData}
|
|
levelScale={levelScale}
|
|
classLevel={classLevel}
|
|
parseSnippet={!showDescription}
|
|
proficiencyBonus={proficiencyBonus}
|
|
theme={theme}
|
|
>
|
|
{children}
|
|
</Snippet>
|
|
</div>
|
|
{(choices.length > 0 ||
|
|
actions.length > 0 ||
|
|
spells.length > 0 ||
|
|
nonChoiceOptions.length > 0) && (
|
|
<div
|
|
className={styles.extra}
|
|
style={
|
|
theme?.isDarkMode ? { borderColor: theme.themeColor } : undefined
|
|
}
|
|
>
|
|
<FeatureSnippetChoices
|
|
choices={choices}
|
|
options={options}
|
|
onSpellClick={onSpellClick}
|
|
onSpellUseSet={onSpellUseSet}
|
|
onActionUseSet={onActionUseSet}
|
|
onActionClick={onActionClick}
|
|
ruleData={ruleData}
|
|
abilityLookup={abilityLookup}
|
|
subclass={subclass}
|
|
feats={feats}
|
|
sourceDataLookup={sourceDataLookup}
|
|
classLevel={classLevel}
|
|
snippetData={snippetData}
|
|
dataOriginRefData={dataOriginRefData}
|
|
showDescription={showDescription}
|
|
isInteractive={!isReadonly}
|
|
proficiencyBonus={proficiencyBonus}
|
|
theme={theme}
|
|
/>
|
|
<FeatureSnippetActions
|
|
actions={actions}
|
|
abilityLookup={abilityLookup}
|
|
ruleData={ruleData}
|
|
onActionClick={onActionClick}
|
|
onActionUseSet={onActionUseSet}
|
|
isInteractive={!isReadonly}
|
|
proficiencyBonus={proficiencyBonus}
|
|
theme={theme}
|
|
/>
|
|
<FeatureSnippetSpells
|
|
spells={spells}
|
|
abilityLookup={abilityLookup}
|
|
ruleData={ruleData}
|
|
onSpellClick={onSpellClick}
|
|
onSpellUseSet={onSpellUseSet}
|
|
isInteractive={!isReadonly}
|
|
dataOriginRefData={dataOriginRefData}
|
|
proficiencyBonus={proficiencyBonus}
|
|
theme={theme}
|
|
/>
|
|
{nonChoiceOptions.length > 0 && (
|
|
<div className={styles.options}>
|
|
{nonChoiceOptions.map((option) => (
|
|
<FeatureSnippetOption
|
|
key={OptionUtils.getId(option)}
|
|
option={option}
|
|
onSpellClick={onSpellClick}
|
|
onSpellUseSet={onSpellUseSet}
|
|
onActionUseSet={onActionUseSet}
|
|
onActionClick={onActionClick}
|
|
abilityLookup={abilityLookup}
|
|
ruleData={ruleData}
|
|
sourceDataLookup={sourceDataLookup}
|
|
classLevel={classLevel}
|
|
snippetData={snippetData}
|
|
showDescription={showDescription}
|
|
isInteractive={!isReadonly}
|
|
dataOriginRefData={dataOriginRefData}
|
|
proficiencyBonus={proficiencyBonus}
|
|
theme={theme}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
<FeatureSnippetInfusionChoices
|
|
infusionChoices={infusionChoices}
|
|
onInfusionChoiceClick={onInfusionChoiceClick}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|