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:
+135
@@ -0,0 +1,135 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
AbilityLookup,
|
||||
BaseFeat,
|
||||
CharClass,
|
||||
ClassFeature,
|
||||
ClassFeatureUtils,
|
||||
ClassUtils,
|
||||
DataOriginBaseAction,
|
||||
DataOriginRefData,
|
||||
DataOriginSpell,
|
||||
InfusionChoice,
|
||||
RuleData,
|
||||
SnippetData,
|
||||
AccessUtils,
|
||||
CharacterTheme,
|
||||
CharacterFeaturesManager,
|
||||
FeatManager,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { DataOriginTypeEnum } from "~/constants";
|
||||
import { FeatureSnippet } from "~/subApps/sheet/components/FeatureSnippet";
|
||||
|
||||
import FeatFeatureSnippet from "../FeatFeatureSnippet";
|
||||
|
||||
interface Props {
|
||||
feature: ClassFeature;
|
||||
charClass: CharClass;
|
||||
extraMeta: Array<string>;
|
||||
onActionClick: (action: DataOriginBaseAction) => void;
|
||||
onActionUseSet: (action: DataOriginBaseAction, uses: number) => void;
|
||||
onSpellClick: (spell: DataOriginSpell) => void;
|
||||
onSpellUseSet: (spell: DataOriginSpell, uses: number) => void;
|
||||
onFeatureClick: (feat: ClassFeature, charClass: CharClass) => void;
|
||||
onInfusionChoiceClick: (infusionChoice: InfusionChoice) => void;
|
||||
|
||||
showHeader?: boolean;
|
||||
showDescription?: boolean;
|
||||
|
||||
feats: Array<BaseFeat>;
|
||||
snippetData: SnippetData;
|
||||
ruleData: RuleData;
|
||||
abilityLookup: AbilityLookup;
|
||||
dataOriginRefData: DataOriginRefData;
|
||||
isReadonly: boolean;
|
||||
proficiencyBonus: number;
|
||||
theme: CharacterTheme;
|
||||
onFeatClick: (feat: FeatManager) => void;
|
||||
featuresManager: CharacterFeaturesManager;
|
||||
}
|
||||
export default class ClassFeatureSnippet extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
extraMeta: [],
|
||||
showDescription: false,
|
||||
};
|
||||
|
||||
handleFeatureClick = (): void => {
|
||||
const { feature, charClass, onFeatureClick } = this.props;
|
||||
|
||||
if (onFeatureClick) {
|
||||
onFeatureClick(feature, charClass);
|
||||
}
|
||||
};
|
||||
|
||||
renderDescription = (): React.ReactNode => {
|
||||
const { feature, showDescription } = this.props;
|
||||
|
||||
const description: string | null = AccessUtils.isAccessible(
|
||||
ClassFeatureUtils.getAccessType(feature)
|
||||
)
|
||||
? ClassFeatureUtils.getDescription(feature)
|
||||
: "Check out the Marketplace to unlock this Class Feature.";
|
||||
|
||||
return showDescription
|
||||
? description
|
||||
: ClassFeatureUtils.getSnippet(feature);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
feature,
|
||||
charClass,
|
||||
onFeatureClick,
|
||||
showDescription,
|
||||
featuresManager,
|
||||
onFeatClick,
|
||||
...restProps
|
||||
} = this.props;
|
||||
|
||||
const feats = [
|
||||
// Both primary and parent. Feats from the Granted Feat system have the class feature as a parent.
|
||||
...featuresManager.getDataOriginOnlyFeatsByPrimary(
|
||||
DataOriginTypeEnum.CLASS_FEATURE,
|
||||
`${ClassFeatureUtils.getId(feature)}`
|
||||
),
|
||||
...featuresManager.getDataOriginOnlyFeatsByParent(
|
||||
DataOriginTypeEnum.CLASS_FEATURE,
|
||||
`${ClassFeatureUtils.getId(feature)}`
|
||||
),
|
||||
];
|
||||
|
||||
// When the class feature grants feats, show those rather than the class feature.
|
||||
return feats.length ? (
|
||||
feats.map((feat) => (
|
||||
<FeatFeatureSnippet
|
||||
{...restProps}
|
||||
key={feat.getId()}
|
||||
feat={feat}
|
||||
onFeatureClick={onFeatClick}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<FeatureSnippet
|
||||
{...restProps}
|
||||
heading={ClassFeatureUtils.getName(feature)}
|
||||
className="ct-feature-snippet--class"
|
||||
levelScale={ClassFeatureUtils.getLevelScale(feature)}
|
||||
actions={ClassFeatureUtils.getActions(feature)}
|
||||
options={ClassFeatureUtils.getOptions(feature)}
|
||||
choices={ClassFeatureUtils.getChoices(feature)}
|
||||
infusionChoices={ClassFeatureUtils.getInfusionChoices(feature)}
|
||||
classLevel={ClassUtils.getLevel(charClass)}
|
||||
subclass={ClassUtils.getSubclass(charClass)}
|
||||
spells={ClassFeatureUtils.getSpells(feature)}
|
||||
sourceId={ClassFeatureUtils.getSourceId(feature)}
|
||||
sourcePage={ClassFeatureUtils.getSourcePage(feature)}
|
||||
onFeatureClick={this.handleFeatureClick}
|
||||
showDescription={showDescription}
|
||||
>
|
||||
{this.renderDescription()}
|
||||
</FeatureSnippet>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import ClassFeatureSnippet from "./ClassFeatureSnippet";
|
||||
|
||||
export default ClassFeatureSnippet;
|
||||
export { ClassFeatureSnippet };
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import {
|
||||
AbilityLookup,
|
||||
Action,
|
||||
CharacterTheme,
|
||||
CharacterUtils,
|
||||
DataOriginRefData,
|
||||
FeatManager,
|
||||
RuleData,
|
||||
SnippetData,
|
||||
SourceMappingContract,
|
||||
Spell,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { useCharacterEngine } from "~/hooks/useCharacterEngine";
|
||||
import { FeatureSnippet } from "~/subApps/sheet/components/FeatureSnippet";
|
||||
|
||||
import { CharacterFeaturesManagerContext } from "../../../../Shared/managers/CharacterFeaturesManagerContext";
|
||||
|
||||
interface Props {
|
||||
feat: FeatManager;
|
||||
extraMeta?: Array<string>;
|
||||
onActionClick?: (action: Action) => void;
|
||||
onActionUseSet?: (action: Action, uses: number) => void;
|
||||
onSpellClick?: (spell: Spell) => void;
|
||||
onSpellUseSet?: (spell: Spell, uses: number) => void;
|
||||
onFeatureClick?: (feat: FeatManager) => void;
|
||||
|
||||
showHeader?: boolean;
|
||||
showDescription?: boolean;
|
||||
|
||||
snippetData: SnippetData;
|
||||
ruleData: RuleData;
|
||||
abilityLookup: AbilityLookup;
|
||||
dataOriginRefData: DataOriginRefData;
|
||||
isReadonly: boolean;
|
||||
proficiencyBonus: number;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
export const FeatFeatureSnippet: React.FC<Props> = ({
|
||||
feat,
|
||||
onFeatureClick,
|
||||
snippetData,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
dataOriginRefData,
|
||||
proficiencyBonus,
|
||||
theme,
|
||||
isReadonly,
|
||||
onActionClick,
|
||||
onSpellClick,
|
||||
onSpellUseSet,
|
||||
onActionUseSet,
|
||||
showHeader,
|
||||
extraMeta = [],
|
||||
showDescription = false,
|
||||
}) => {
|
||||
const { characterFeaturesManager } = useContext(
|
||||
CharacterFeaturesManagerContext
|
||||
);
|
||||
const currentFeats = characterFeaturesManager.getFeats();
|
||||
const { entityUtils } = useCharacterEngine();
|
||||
|
||||
let sourceId: number | null = null;
|
||||
let sourcePage: number | null = null;
|
||||
let filteredSources = feat
|
||||
.getSources()
|
||||
.filter(CharacterUtils.isPrimarySource);
|
||||
let dataOrigin = feat.getDataOrigin();
|
||||
const dataOriginExtra = entityUtils.getDataOriginName(
|
||||
dataOrigin,
|
||||
"Unknown",
|
||||
true
|
||||
);
|
||||
|
||||
if (filteredSources.length) {
|
||||
let primarySource: SourceMappingContract = filteredSources[0];
|
||||
sourceId = primarySource.sourceId;
|
||||
sourcePage = primarySource.pageNumber;
|
||||
}
|
||||
|
||||
return (
|
||||
<FeatureSnippet
|
||||
snippetData={snippetData}
|
||||
ruleData={ruleData}
|
||||
abilityLookup={abilityLookup}
|
||||
dataOriginRefData={dataOriginRefData}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
theme={theme}
|
||||
isReadonly={isReadonly}
|
||||
extraMeta={extraMeta}
|
||||
onActionClick={onActionClick}
|
||||
onActionUseSet={onActionUseSet}
|
||||
onSpellClick={onSpellClick}
|
||||
onSpellUseSet={onSpellUseSet}
|
||||
showHeader={showHeader}
|
||||
feats={currentFeats.map((feat) => feat.feat)}
|
||||
dataOriginExtra={dataOriginExtra}
|
||||
heading={feat.getName()}
|
||||
className="ct-feature-snippet--feat"
|
||||
actions={feat.getActions()}
|
||||
options={feat.getOptions()}
|
||||
choices={feat.getChoices()}
|
||||
spells={feat.getSpells()}
|
||||
sourceId={sourceId}
|
||||
sourcePage={sourcePage}
|
||||
onFeatureClick={() => {
|
||||
if (onFeatureClick) {
|
||||
onFeatureClick(feat);
|
||||
}
|
||||
}}
|
||||
showDescription={showDescription}
|
||||
>
|
||||
{showDescription ? feat.getDescription() : feat.getSnippet()}
|
||||
</FeatureSnippet>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatFeatureSnippet;
|
||||
@@ -0,0 +1,4 @@
|
||||
import FeatFeatureSnippet from "./FeatFeatureSnippet";
|
||||
|
||||
export default FeatFeatureSnippet;
|
||||
export { FeatFeatureSnippet };
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
AbilityLookup,
|
||||
ActionUtils,
|
||||
ActivationUtils,
|
||||
Constants,
|
||||
DataOriginBaseAction,
|
||||
LimitedUseUtils,
|
||||
RuleData,
|
||||
CharacterTheme,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { FeatureSnippetLimitedUse } from "../FeatureSnippetLimitedUse";
|
||||
|
||||
interface Props {
|
||||
action: DataOriginBaseAction;
|
||||
|
||||
onActionUseSet?: (action: DataOriginBaseAction, uses: number) => void;
|
||||
onActionClick?: (action: DataOriginBaseAction) => void;
|
||||
|
||||
ruleData: RuleData;
|
||||
abilityLookup: AbilityLookup;
|
||||
isInteractive: boolean;
|
||||
proficiencyBonus: number;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
export default class FeatureSnippetAction extends React.PureComponent<Props> {
|
||||
handleActionUseSet = (action: DataOriginBaseAction, uses: number): void => {
|
||||
const { onActionUseSet } = this.props;
|
||||
|
||||
if (onActionUseSet) {
|
||||
onActionUseSet(action, uses);
|
||||
}
|
||||
};
|
||||
|
||||
handleActionClick = (evt: React.MouseEvent): void => {
|
||||
const { action, onActionClick } = this.props;
|
||||
|
||||
if (onActionClick) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
onActionClick(action);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
action,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
isInteractive,
|
||||
proficiencyBonus,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
let limitedUse = ActionUtils.getLimitedUse(action);
|
||||
let name = ActionUtils.getName(action);
|
||||
let activation = ActionUtils.getActivation(action);
|
||||
let limitedUseLabel: string | null = null;
|
||||
if (limitedUse) {
|
||||
let maxUses = LimitedUseUtils.deriveMaxUses(
|
||||
limitedUse,
|
||||
abilityLookup,
|
||||
ruleData,
|
||||
proficiencyBonus
|
||||
);
|
||||
limitedUseLabel = maxUses > 1 ? "Uses:" : "";
|
||||
}
|
||||
let activationDisplay: string = "(No Action)";
|
||||
if (activation) {
|
||||
switch (activation.activationType) {
|
||||
case Constants.ActivationTypeEnum.ACTION:
|
||||
case Constants.ActivationTypeEnum.REACTION:
|
||||
case Constants.ActivationTypeEnum.BONUS_ACTION:
|
||||
activationDisplay = ActivationUtils.renderActivation(
|
||||
activation,
|
||||
ruleData
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// not implemented
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ct-feature-snippet__action"
|
||||
onClick={this.handleActionClick}
|
||||
>
|
||||
<div
|
||||
className={`ct-feature-snippet__action-summary ${
|
||||
theme?.isDarkMode
|
||||
? "ct-feature-snippet__action-summary--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{name}: {activationDisplay}
|
||||
</div>
|
||||
{limitedUse && (
|
||||
<div className="ct-feature-snippet__action-limited">
|
||||
{limitedUseLabel && (
|
||||
<div
|
||||
className={`ct-feature-snippet__action-limited-label ${
|
||||
theme?.isDarkMode
|
||||
? "ct-feature-snippet__action-limited-label--dark-mode"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{limitedUseLabel}
|
||||
</div>
|
||||
)}
|
||||
<FeatureSnippetLimitedUse
|
||||
component={action}
|
||||
limitedUse={limitedUse}
|
||||
onUseSet={this.handleActionUseSet}
|
||||
abilityLookup={abilityLookup}
|
||||
ruleData={ruleData}
|
||||
isInteractive={isInteractive}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import FeatureSnippetAction from "./FeatureSnippetAction";
|
||||
|
||||
export default FeatureSnippetAction;
|
||||
export { FeatureSnippetAction };
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
AbilityLookup,
|
||||
ActionUtils,
|
||||
DataOriginBaseAction,
|
||||
RuleData,
|
||||
CharacterTheme,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import FeatureSnippetAction from "../FeatureSnippetAction";
|
||||
|
||||
interface Props {
|
||||
actions: Array<DataOriginBaseAction>;
|
||||
|
||||
onActionUseSet?: (action: DataOriginBaseAction, uses: number) => void;
|
||||
onActionClick?: (action: DataOriginBaseAction) => void;
|
||||
|
||||
ruleData: RuleData;
|
||||
abilityLookup: AbilityLookup;
|
||||
isInteractive: boolean;
|
||||
proficiencyBonus: number;
|
||||
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
export default class FeatureSnippetActions extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const {
|
||||
actions,
|
||||
onActionUseSet,
|
||||
onActionClick,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
isInteractive,
|
||||
proficiencyBonus,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
if (!actions.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ct-feature-snippet__actions">
|
||||
{actions.map((action) => (
|
||||
<FeatureSnippetAction
|
||||
key={ActionUtils.getUniqueKey(action)}
|
||||
action={action}
|
||||
onActionUseSet={onActionUseSet}
|
||||
onActionClick={onActionClick}
|
||||
ruleData={ruleData}
|
||||
abilityLookup={abilityLookup}
|
||||
isInteractive={isInteractive}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import FeatureSnippetActions from "./FeatureSnippetActions";
|
||||
|
||||
export default FeatureSnippetActions;
|
||||
export { FeatureSnippetActions };
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
AbilityLookup,
|
||||
BaseFeat,
|
||||
CharacterTheme,
|
||||
Choice,
|
||||
ClassDefinitionContract,
|
||||
Constants,
|
||||
DataOriginBaseAction,
|
||||
DataOriginRefData,
|
||||
FeatUtils,
|
||||
Option,
|
||||
OptionUtils,
|
||||
RuleData,
|
||||
SnippetData,
|
||||
SourceData,
|
||||
Spell,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import FeatureSnippetOption from "../FeatureSnippetOption";
|
||||
|
||||
interface Props {
|
||||
choices?: Array<Choice>;
|
||||
options?: Array<Option>;
|
||||
|
||||
onActionClick?: (action: DataOriginBaseAction) => void;
|
||||
onActionUseSet?: (action: DataOriginBaseAction, uses: number) => void;
|
||||
onSpellUseSet?: (spell: Spell, uses: number) => void;
|
||||
onSpellClick?: (spell: Spell) => void;
|
||||
|
||||
feats?: Array<BaseFeat>;
|
||||
snippetData: SnippetData;
|
||||
ruleData: RuleData;
|
||||
abilityLookup: AbilityLookup;
|
||||
sourceDataLookup: Record<number, SourceData>;
|
||||
dataOriginRefData: DataOriginRefData;
|
||||
|
||||
classLevel?: number;
|
||||
subclass?: ClassDefinitionContract | null;
|
||||
|
||||
showDescription: boolean;
|
||||
isInteractive: boolean;
|
||||
proficiencyBonus: number;
|
||||
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
export default class FeatureSnippetChoices extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
showDescription: false,
|
||||
isInteractive: true,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
options,
|
||||
choices,
|
||||
subclass,
|
||||
feats,
|
||||
snippetData,
|
||||
classLevel,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
sourceDataLookup,
|
||||
dataOriginRefData,
|
||||
onActionUseSet,
|
||||
onActionClick,
|
||||
onSpellClick,
|
||||
onSpellUseSet,
|
||||
showDescription,
|
||||
isInteractive,
|
||||
proficiencyBonus,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
if (!choices || !choices.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ct-feature-snippet__choices">
|
||||
{choices.map((choice, idx) => {
|
||||
const {
|
||||
id,
|
||||
type,
|
||||
optionValue,
|
||||
subType,
|
||||
options: choiceOptions,
|
||||
} = choice;
|
||||
|
||||
let displayType: React.ReactNode;
|
||||
let selectedValue: React.ReactNode;
|
||||
switch (type) {
|
||||
case Constants.BuilderChoiceTypeEnum.RACIAL_TRAIT_OPTION:
|
||||
case Constants.BuilderChoiceTypeEnum.FEAT_OPTION:
|
||||
case Constants.BuilderChoiceTypeEnum.FEATURE_OPTION:
|
||||
displayType = "Feature Option";
|
||||
let selectedFeatureOption = options
|
||||
? options.find((opt) => OptionUtils.getId(opt) === optionValue)
|
||||
: undefined;
|
||||
if (selectedFeatureOption) {
|
||||
selectedValue = (
|
||||
<FeatureSnippetOption
|
||||
option={selectedFeatureOption}
|
||||
onSpellClick={onSpellClick}
|
||||
onSpellUseSet={onSpellUseSet}
|
||||
onActionClick={onActionClick}
|
||||
onActionUseSet={onActionUseSet}
|
||||
snippetData={snippetData}
|
||||
classLevel={classLevel}
|
||||
ruleData={ruleData}
|
||||
abilityLookup={abilityLookup}
|
||||
dataOriginRefData={dataOriginRefData}
|
||||
sourceDataLookup={sourceDataLookup}
|
||||
showDescription={showDescription}
|
||||
isInteractive={isInteractive}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
selectedValue = "Could not find selected feature option";
|
||||
}
|
||||
break;
|
||||
|
||||
case Constants.BuilderChoiceTypeEnum.FEAT_CHOICE_OPTION:
|
||||
displayType = "Feat";
|
||||
let selectedFeat = feats
|
||||
? feats.find((feat) => FeatUtils.getId(feat) === optionValue)
|
||||
: undefined;
|
||||
selectedValue = selectedFeat
|
||||
? FeatUtils.getName(selectedFeat)
|
||||
: "Could not find selected feat";
|
||||
break;
|
||||
|
||||
case Constants.BuilderChoiceTypeEnum.ENTITY_SPELL_OPTION:
|
||||
//The actual rendering spells scenario is being handled in FeatureSnippetSpells only IF the choice is not null
|
||||
//This is a fix to show "No Choice Made" when FeatureSnippetSpells is not rendering anything
|
||||
displayType = "Spell";
|
||||
if (optionValue === null) selectedValue = "No Choice Made";
|
||||
else return null;
|
||||
|
||||
break;
|
||||
|
||||
case Constants.BuilderChoiceTypeEnum.SUB_CLASS_OPTION:
|
||||
displayType = "Subclass";
|
||||
selectedValue =
|
||||
subclass && subclass.name
|
||||
? subclass.name
|
||||
: "Could not find selected subclass";
|
||||
break;
|
||||
|
||||
case Constants.BuilderChoiceTypeEnum.ONE_MODIFIER_TYPE_CHOICE:
|
||||
displayType = "One Modifier Type Choice";
|
||||
let selectedOneModifier = choiceOptions
|
||||
? choiceOptions.find((opt) => opt.id === optionValue)
|
||||
: undefined;
|
||||
selectedValue = selectedOneModifier
|
||||
? selectedOneModifier.label
|
||||
: "Could not find selected one modifier type choice";
|
||||
break;
|
||||
|
||||
case Constants.BuilderChoiceTypeEnum.MODIFIER_SUB_CHOICE:
|
||||
let selectedOption = choiceOptions
|
||||
? choiceOptions.find((opt) => opt.id === optionValue)
|
||||
: undefined;
|
||||
selectedValue = selectedOption
|
||||
? selectedOption.label
|
||||
: "could not find selected choice";
|
||||
switch (subType) {
|
||||
case Constants.BuilderChoiceSubtypeEnum.PROFICIENCY:
|
||||
displayType = "Proficiency";
|
||||
break;
|
||||
case Constants.BuilderChoiceSubtypeEnum.LANGUAGE:
|
||||
displayType = "Language";
|
||||
break;
|
||||
case Constants.BuilderChoiceSubtypeEnum.KENSEI:
|
||||
displayType = "Kensei Option";
|
||||
break;
|
||||
case Constants.BuilderChoiceSubtypeEnum.EXPERTISE:
|
||||
displayType = "Expertise";
|
||||
break;
|
||||
case Constants.BuilderChoiceSubtypeEnum
|
||||
.EXPERTISE_NO_REQUIREMENT:
|
||||
displayType = "Expertise";
|
||||
break;
|
||||
case Constants.BuilderChoiceSubtypeEnum.ABILITY_SCORE: {
|
||||
displayType = "Ability Score";
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown BUILDER_CHOICE_TYPE: ${type}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ct-feature-snippet__choice ${
|
||||
theme?.isDarkMode ? "ct-feature-snippet__choice--dark-mode" : ""
|
||||
}`}
|
||||
key={`${id}-${idx}`}
|
||||
>
|
||||
{optionValue === null
|
||||
? "No Choice Made"
|
||||
: selectedValue
|
||||
? selectedValue
|
||||
: "could not find selected value"}
|
||||
</div>
|
||||
);
|
||||
//`
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import FeatureSnippetChoices from "./FeatureSnippetChoices";
|
||||
|
||||
export default FeatureSnippetChoices;
|
||||
export { FeatureSnippetChoices };
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
InfusionChoice,
|
||||
InfusionChoiceUtils,
|
||||
InfusionUtils,
|
||||
KnownInfusionUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
type OnInfusionChoiceClick = (infusionChoice: InfusionChoice) => void;
|
||||
interface Props {
|
||||
infusionChoices: Array<InfusionChoice>;
|
||||
|
||||
onInfusionChoiceClick?: OnInfusionChoiceClick;
|
||||
}
|
||||
|
||||
export default class FeatureSnippetInfusionChoices extends React.PureComponent<Props> {
|
||||
handleInfusionChoiceClick = (
|
||||
infusionChoice: InfusionChoice,
|
||||
evt: React.MouseEvent
|
||||
) => {
|
||||
const { onInfusionChoiceClick } = this.props;
|
||||
|
||||
if (onInfusionChoiceClick) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
onInfusionChoiceClick(infusionChoice);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { infusionChoices } = this.props;
|
||||
|
||||
let availableInfusionChoices = infusionChoices.filter(
|
||||
InfusionChoiceUtils.validateIsAvailable
|
||||
);
|
||||
|
||||
if (!availableInfusionChoices.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let classNames: Array<string> = ["ct-feature-snippet__infusion-choices"];
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
{availableInfusionChoices.map((infusionChoice, idx) => {
|
||||
const knownInfusion =
|
||||
InfusionChoiceUtils.getKnownInfusion(infusionChoice);
|
||||
|
||||
let nameNode: React.ReactNode = "No Infusion Choice Made";
|
||||
let onClick: OnInfusionChoiceClick | undefined;
|
||||
if (knownInfusion) {
|
||||
const simulatedInfusion =
|
||||
KnownInfusionUtils.getSimulatedInfusion(knownInfusion);
|
||||
if (simulatedInfusion !== null) {
|
||||
nameNode = (
|
||||
<React.Fragment>
|
||||
{InfusionUtils.getName(simulatedInfusion)}
|
||||
</React.Fragment>
|
||||
);
|
||||
onClick = this.handleInfusionChoiceClick.bind(
|
||||
this,
|
||||
infusionChoice
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const choiceKey = InfusionChoiceUtils.getKey(infusionChoice);
|
||||
|
||||
//TODO this onclick handler needs to be converted to a new sub component to
|
||||
// get away from the need for binding as it messes with typing it
|
||||
return (
|
||||
<div
|
||||
className="ct-feature-snippet__infusion-choice"
|
||||
key={choiceKey === null ? "" : choiceKey}
|
||||
>
|
||||
<div
|
||||
className="ct-feature-snippet__infusion-choice-summary"
|
||||
onClick={onClick as any}
|
||||
>
|
||||
{nameNode}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import FeatureSnippetInfusionChoices from "./FeatureSnippetInfusionChoices";
|
||||
|
||||
export default FeatureSnippetInfusionChoices;
|
||||
export { FeatureSnippetInfusionChoices };
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
AbilityLookup,
|
||||
DataOriginBaseAction,
|
||||
DataOriginSpell,
|
||||
Constants,
|
||||
EntityLimitedUseContract,
|
||||
LimitedUseUtils,
|
||||
RuleData,
|
||||
RuleDataUtils,
|
||||
CharacterTheme,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import SlotManager from "../../../../Shared/components/SlotManager";
|
||||
import SlotManagerLarge from "../../../../Shared/components/SlotManagerLarge";
|
||||
|
||||
interface Props {
|
||||
component: DataOriginBaseAction | DataOriginSpell;
|
||||
limitedUse: EntityLimitedUseContract | null;
|
||||
onUseSet?: (
|
||||
component: DataOriginBaseAction | DataOriginSpell,
|
||||
uses: number
|
||||
) => void;
|
||||
|
||||
ruleData: RuleData;
|
||||
abilityLookup: AbilityLookup;
|
||||
isInteractive: boolean;
|
||||
largeLimitedUseAmount: number;
|
||||
proficiencyBonus: number;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
export default class FeatureSnippetLimitedUse extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
largeLimitedUseAmount: 8,
|
||||
};
|
||||
|
||||
handleUseSet = (uses: number): void => {
|
||||
const { onUseSet, component } = this.props;
|
||||
|
||||
if (onUseSet) {
|
||||
onUseSet(component, uses);
|
||||
}
|
||||
};
|
||||
|
||||
renderSmallAmountSlotPool = (): React.ReactNode => {
|
||||
const {
|
||||
limitedUse,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
isInteractive,
|
||||
proficiencyBonus,
|
||||
} = this.props;
|
||||
|
||||
if (!limitedUse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const numberUsed = LimitedUseUtils.getNumberUsed(limitedUse);
|
||||
const maxUses = LimitedUseUtils.deriveMaxUses(
|
||||
limitedUse,
|
||||
abilityLookup,
|
||||
ruleData,
|
||||
proficiencyBonus
|
||||
);
|
||||
|
||||
if (!maxUses) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SlotManager
|
||||
used={numberUsed}
|
||||
available={maxUses}
|
||||
size={"small"}
|
||||
onSet={this.handleUseSet}
|
||||
isInteractive={isInteractive}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderLargeAmountSlotPool = (): React.ReactNode => {
|
||||
const {
|
||||
limitedUse,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
isInteractive,
|
||||
proficiencyBonus,
|
||||
} = this.props;
|
||||
|
||||
if (!limitedUse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const numberUsed = LimitedUseUtils.getNumberUsed(limitedUse);
|
||||
const maxUses = LimitedUseUtils.deriveMaxUses(
|
||||
limitedUse,
|
||||
abilityLookup,
|
||||
ruleData,
|
||||
proficiencyBonus
|
||||
);
|
||||
|
||||
if (!maxUses) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SlotManagerLarge
|
||||
label="Current:"
|
||||
available={maxUses}
|
||||
used={numberUsed}
|
||||
onSet={this.handleUseSet}
|
||||
isInteractive={isInteractive}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
limitedUse,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
largeLimitedUseAmount,
|
||||
proficiencyBonus,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
if (!limitedUse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let resetType = LimitedUseUtils.getResetType(limitedUse);
|
||||
let maxUses = LimitedUseUtils.deriveMaxUses(
|
||||
limitedUse,
|
||||
abilityLookup,
|
||||
ruleData,
|
||||
proficiencyBonus
|
||||
);
|
||||
let numberUsed = LimitedUseUtils.getNumberUsed(limitedUse);
|
||||
let totalSlots = Math.max(maxUses, numberUsed);
|
||||
|
||||
let usagesNode: React.ReactNode;
|
||||
let extraNode: React.ReactNode;
|
||||
if (totalSlots >= largeLimitedUseAmount) {
|
||||
usagesNode = maxUses;
|
||||
extraNode = this.renderLargeAmountSlotPool();
|
||||
} else if (maxUses === -1) {
|
||||
usagesNode = "Unlimited";
|
||||
} else {
|
||||
usagesNode = this.renderSmallAmountSlotPool();
|
||||
}
|
||||
|
||||
let resetNode: React.ReactNode;
|
||||
if (
|
||||
resetType !== Constants.LimitedUseResetTypeEnum.SHORT_REST &&
|
||||
resetType !== Constants.LimitedUseResetTypeEnum.LONG_REST
|
||||
) {
|
||||
resetNode = "Special";
|
||||
} else {
|
||||
resetNode = RuleDataUtils.getLimitedUseResetTypeName(resetType, ruleData);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div
|
||||
className={`ct-feature-snippet__limited-use ${
|
||||
theme.isDarkMode ? "ct-feature-snippet__limited-use--dark-mode" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="ct-feature-snippet__limited-use-usages">
|
||||
{usagesNode}
|
||||
</div>
|
||||
<div className="ct-feature-snippet__limited-use-sep">/</div>
|
||||
<div className="ct-feature-snippet__limited-use-reset">
|
||||
{resetNode}
|
||||
</div>
|
||||
</div>
|
||||
{extraNode && (
|
||||
<div className="ct-feature-snippet__limited-use-extra">
|
||||
{extraNode}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import FeatureSnippetLimitedUse from "./FeatureSnippetLimitedUse";
|
||||
|
||||
export default FeatureSnippetLimitedUse;
|
||||
export { FeatureSnippetLimitedUse };
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
import React from "react";
|
||||
|
||||
import { Tooltip } from "@dndbeyond/character-common-components/es";
|
||||
import { Snippet } from "@dndbeyond/character-components/es";
|
||||
import {
|
||||
AbilityLookup,
|
||||
CharacterTheme,
|
||||
DataOriginBaseAction,
|
||||
DataOriginRefData,
|
||||
Option,
|
||||
OptionUtils,
|
||||
RuleData,
|
||||
SnippetData,
|
||||
SourceData,
|
||||
Spell,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { FeatureSnippetActions } from "../FeatureSnippetActions";
|
||||
import { FeatureSnippetSpells } from "../FeatureSnippetSpells";
|
||||
|
||||
interface Props {
|
||||
option: Option;
|
||||
|
||||
onActionClick?: (action: DataOriginBaseAction) => void;
|
||||
onActionUseSet?: (action: DataOriginBaseAction, uses: number) => void;
|
||||
onSpellClick?: (spell: Spell) => void;
|
||||
onSpellUseSet?: (spell: Spell, uses: number) => void;
|
||||
onOptionClick?: () => void;
|
||||
|
||||
ruleData: RuleData;
|
||||
abilityLookup: AbilityLookup;
|
||||
sourceDataLookup: Record<number, SourceData>;
|
||||
dataOriginRefData: DataOriginRefData;
|
||||
|
||||
snippetData: SnippetData;
|
||||
classLevel?: number;
|
||||
|
||||
showDescription: boolean;
|
||||
isInteractive: boolean;
|
||||
proficiencyBonus: number;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
export default class FeatureSnippetOption extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
showDescription: false,
|
||||
};
|
||||
|
||||
handleOptionClick = (evt: React.MouseEvent): void => {
|
||||
const { onOptionClick } = this.props;
|
||||
|
||||
if (onOptionClick) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
onOptionClick();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
option,
|
||||
snippetData,
|
||||
classLevel,
|
||||
abilityLookup,
|
||||
ruleData,
|
||||
sourceDataLookup,
|
||||
dataOriginRefData,
|
||||
onActionUseSet,
|
||||
onActionClick,
|
||||
onSpellClick,
|
||||
onSpellUseSet,
|
||||
showDescription,
|
||||
isInteractive,
|
||||
proficiencyBonus,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
let sourceId = OptionUtils.getSourceId(option);
|
||||
let sourcePage = OptionUtils.getSourcePage(option);
|
||||
let name = OptionUtils.getName(option);
|
||||
let content: string | null = showDescription
|
||||
? OptionUtils.getDescription(option)
|
||||
: OptionUtils.getSnippet(option);
|
||||
|
||||
const levelScale = OptionUtils.getLevelScale(option);
|
||||
const actions = OptionUtils.getActions(option);
|
||||
const spells = OptionUtils.getSpells(option);
|
||||
|
||||
let metaItems: Array<React.ReactNode> = [];
|
||||
let sourceNode: React.ReactNode;
|
||||
if (sourceId && sourcePage) {
|
||||
let source = sourceDataLookup[sourceId];
|
||||
// TODO should use GameRulesSourceAbbr
|
||||
sourceNode = (
|
||||
<span className="ct-feature-snippet__source">
|
||||
<Tooltip
|
||||
isDarkMode={theme.isDarkMode}
|
||||
title={source.description ? source.description : ""}
|
||||
>
|
||||
<span className="ct-feature-snippet__heading-source-abbr">
|
||||
{source.name}
|
||||
</span>
|
||||
{sourcePage && (
|
||||
<span className="ct-feature-snippet__heading-source-page">
|
||||
{sourcePage}
|
||||
</span>
|
||||
)}
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
metaItems.push(sourceNode);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ct-feature-snippet__option"
|
||||
onClick={this.handleOptionClick}
|
||||
>
|
||||
<div className="ct-feature-snippet__heading">
|
||||
{name}
|
||||
<span className="ct-feature-snippet__meta">
|
||||
{metaItems.map((metaItem, idx) => (
|
||||
<span className="ct-feature-snippet__meta-item" key={idx}>
|
||||
{metaItem}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
{content && (
|
||||
<div className="ct-feature-snippet__option-content">
|
||||
<Snippet
|
||||
snippetData={snippetData}
|
||||
classLevel={classLevel}
|
||||
parseSnippet={!showDescription}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
theme={theme}
|
||||
levelScale={levelScale}
|
||||
>
|
||||
{content}
|
||||
</Snippet>
|
||||
</div>
|
||||
)}
|
||||
{(actions.length > 0 || spells.length > 0) && (
|
||||
<div className="ct-feature-snippet__extra">
|
||||
<FeatureSnippetActions
|
||||
actions={actions}
|
||||
abilityLookup={abilityLookup}
|
||||
ruleData={ruleData}
|
||||
onActionUseSet={onActionUseSet}
|
||||
onActionClick={onActionClick}
|
||||
isInteractive={isInteractive}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
theme={theme}
|
||||
/>
|
||||
<FeatureSnippetSpells
|
||||
spells={spells}
|
||||
abilityLookup={abilityLookup}
|
||||
ruleData={ruleData}
|
||||
onSpellUseSet={onSpellUseSet}
|
||||
onSpellClick={onSpellClick}
|
||||
isInteractive={isInteractive}
|
||||
dataOriginRefData={dataOriginRefData}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import FeatureSnippetOption from "./FeatureSnippetOption";
|
||||
|
||||
export default FeatureSnippetOption;
|
||||
export { FeatureSnippetOption };
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
import { orderBy } from "lodash";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
AbilityLookup,
|
||||
BaseSpell,
|
||||
CharacterTheme,
|
||||
DataOriginRefData,
|
||||
RuleData,
|
||||
SpellUtils,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
import { SpellName } from "~/components/SpellName";
|
||||
|
||||
import { FeatureSnippetLimitedUse } from "../FeatureSnippetLimitedUse";
|
||||
|
||||
interface Props {
|
||||
spells?: Array<BaseSpell>;
|
||||
layoutType: "list" | "compact";
|
||||
|
||||
onSpellUseSet?: (spell: BaseSpell, uses: number) => void;
|
||||
onSpellClick?: (spell: BaseSpell) => void;
|
||||
|
||||
dataOriginRefData: DataOriginRefData;
|
||||
ruleData: RuleData;
|
||||
abilityLookup: AbilityLookup;
|
||||
isInteractive: boolean;
|
||||
proficiencyBonus: number;
|
||||
theme: CharacterTheme;
|
||||
}
|
||||
export default class FeatureSnippetSpells extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
layoutType: "list",
|
||||
};
|
||||
|
||||
handleSpellUseSet = (spell: BaseSpell, uses: number): void => {
|
||||
const { onSpellUseSet } = this.props;
|
||||
|
||||
if (onSpellUseSet) {
|
||||
onSpellUseSet(spell, uses);
|
||||
}
|
||||
};
|
||||
|
||||
handleSpellClick = (spell: BaseSpell, evt: React.MouseEvent): void => {
|
||||
const { onSpellClick } = this.props;
|
||||
|
||||
if (onSpellClick) {
|
||||
evt.stopPropagation();
|
||||
evt.nativeEvent.stopImmediatePropagation();
|
||||
onSpellClick(spell);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
spells,
|
||||
ruleData,
|
||||
abilityLookup,
|
||||
layoutType,
|
||||
isInteractive,
|
||||
dataOriginRefData,
|
||||
proficiencyBonus,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
if (!spells || !spells.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let orderedSpells = orderBy(
|
||||
spells,
|
||||
[
|
||||
(spell) => SpellUtils.getLevel(spell),
|
||||
(spell) => SpellUtils.getName(spell),
|
||||
],
|
||||
["asc", "asc"]
|
||||
);
|
||||
|
||||
let classNames: Array<string> = [
|
||||
"ct-feature-snippet__spells",
|
||||
`ct-feature-snippet__spells--layout-${layoutType}`,
|
||||
];
|
||||
if (theme?.isDarkMode) {
|
||||
classNames.push(`ct-feature-snippet__spells--dark-mode`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames.join(" ")}>
|
||||
{orderedSpells.map((spell, idx) => (
|
||||
<div
|
||||
className="ct-feature-snippet__spell"
|
||||
key={SpellUtils.getUniqueKey(spell)}
|
||||
>
|
||||
<div
|
||||
className="ct-feature-snippet__spell-summary"
|
||||
onClick={this.handleSpellClick.bind(this, spell)}
|
||||
>
|
||||
<SpellName
|
||||
spell={spell}
|
||||
showLegacy={true}
|
||||
dataOriginRefData={dataOriginRefData}
|
||||
/>
|
||||
</div>
|
||||
{layoutType !== "compact" && !SpellUtils.isCantrip(spell) && (
|
||||
<FeatureSnippetLimitedUse
|
||||
component={spell}
|
||||
theme={theme}
|
||||
limitedUse={SpellUtils.getLimitedUse(spell)}
|
||||
onUseSet={this.handleSpellUseSet}
|
||||
abilityLookup={abilityLookup}
|
||||
ruleData={ruleData}
|
||||
isInteractive={isInteractive}
|
||||
proficiencyBonus={proficiencyBonus}
|
||||
/>
|
||||
)}
|
||||
{layoutType === "compact" && idx + 1 < orderedSpells.length && (
|
||||
<span className="ct-feature-snippet__spell-sep">,</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import FeatureSnippetSpells from "./FeatureSnippetSpells";
|
||||
|
||||
export default FeatureSnippetSpells;
|
||||
export { FeatureSnippetSpells };
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import React from "react";
|
||||
import { RacialTraitAccessors } from "@dndbeyond/character-rules-engine/es/engine/RacialTrait";
|
||||
import {
|
||||
AbilityLookup,
|
||||
BaseFeat,
|
||||
DataOriginBaseAction,
|
||||
DataOriginRefData,
|
||||
DataOriginSpell,
|
||||
RacialTrait,
|
||||
RacialTraitUtils,
|
||||
RuleData,
|
||||
SnippetData,
|
||||
AccessUtils,
|
||||
CharacterTheme,
|
||||
FeatManager,
|
||||
CharacterFeaturesManager,
|
||||
} from "@dndbeyond/character-rules-engine/es";
|
||||
|
||||
|
||||
import { DataOriginTypeEnum } from "~/constants";
|
||||
import { FeatureSnippet } from "~/subApps/sheet/components/FeatureSnippet";
|
||||
|
||||
import FeatFeatureSnippet from "../FeatFeatureSnippet";
|
||||
|
||||
interface Props {
|
||||
speciesTrait: RacialTrait;
|
||||
extraMeta: Array<string>;
|
||||
onActionClick?: (action: DataOriginBaseAction) => void;
|
||||
onActionUseSet?: (action: DataOriginBaseAction, uses: number) => void;
|
||||
onSpellClick?: (spell: DataOriginSpell) => void;
|
||||
onSpellUseSet?: (spell: DataOriginSpell, uses: number) => void;
|
||||
onFeatureClick?: (feature: RacialTrait) => void;
|
||||
showHeader?: boolean;
|
||||
showDescription?: boolean;
|
||||
feats: Array<BaseFeat>;
|
||||
snippetData: SnippetData;
|
||||
ruleData: RuleData;
|
||||
abilityLookup: AbilityLookup;
|
||||
dataOriginRefData: DataOriginRefData;
|
||||
isReadonly: boolean;
|
||||
proficiencyBonus: number;
|
||||
theme: CharacterTheme;
|
||||
onFeatClick: (feat: FeatManager) => void;
|
||||
featuresManager: CharacterFeaturesManager;
|
||||
}
|
||||
export default class SpeciesTraitFeatureSnippet extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
extraMeta: [],
|
||||
showDescription: false,
|
||||
};
|
||||
|
||||
handleFeatureClick = (): void => {
|
||||
const { speciesTrait, onFeatureClick } = this.props;
|
||||
|
||||
if (onFeatureClick) {
|
||||
onFeatureClick(speciesTrait);
|
||||
}
|
||||
};
|
||||
|
||||
renderDescription = (): React.ReactNode => {
|
||||
const { speciesTrait, showDescription } = this.props;
|
||||
|
||||
const description: string | null = AccessUtils.isAccessible(
|
||||
RacialTraitUtils.getAccessType(speciesTrait)
|
||||
)
|
||||
? RacialTraitUtils.getDescription(speciesTrait)
|
||||
: "Check out the Marketplace to unlock this Species Trait.";
|
||||
|
||||
return showDescription
|
||||
? description
|
||||
: RacialTraitUtils.getSnippet(speciesTrait);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
speciesTrait,
|
||||
onFeatureClick,
|
||||
showDescription,
|
||||
featuresManager,
|
||||
onFeatClick,
|
||||
...restProps
|
||||
} = this.props;
|
||||
|
||||
const feats = featuresManager.getDataOriginOnlyFeatsByPrimary(
|
||||
DataOriginTypeEnum.RACE,
|
||||
`${RacialTraitAccessors.getId(speciesTrait)}`
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FeatureSnippet
|
||||
{...restProps}
|
||||
heading={RacialTraitUtils.getName(speciesTrait)}
|
||||
className="ct-feature-snippet--racial-trait"
|
||||
actions={RacialTraitUtils.getActions(speciesTrait)}
|
||||
options={RacialTraitUtils.getOptions(speciesTrait)}
|
||||
choices={RacialTraitUtils.getChoices(speciesTrait)}
|
||||
spells={RacialTraitUtils.getSpells(speciesTrait)}
|
||||
sourceId={RacialTraitUtils.getSourceId(speciesTrait)}
|
||||
sourcePage={RacialTraitUtils.getSourcePage(speciesTrait)}
|
||||
onFeatureClick={this.handleFeatureClick}
|
||||
showDescription={showDescription}
|
||||
>
|
||||
{this.renderDescription()}
|
||||
</FeatureSnippet>
|
||||
{feats.map((feat) => (
|
||||
<FeatFeatureSnippet
|
||||
{...restProps}
|
||||
key={feat.getId()}
|
||||
feat={feat}
|
||||
onFeatureClick={onFeatClick}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import SpeciesTraitFeatureSnippet from "./SpeciesTraitFeatureSnippet";
|
||||
|
||||
export default SpeciesTraitFeatureSnippet;
|
||||
export { SpeciesTraitFeatureSnippet };
|
||||
Reference in New Issue
Block a user