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,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>
);
}
}
@@ -0,0 +1,4 @@
import ClassFeatureSnippet from "./ClassFeatureSnippet";
export default ClassFeatureSnippet;
export { ClassFeatureSnippet };
@@ -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 };
@@ -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>
);
}
}
@@ -0,0 +1,4 @@
import FeatureSnippetAction from "./FeatureSnippetAction";
export default FeatureSnippetAction;
export { FeatureSnippetAction };
@@ -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>
);
}
}
@@ -0,0 +1,4 @@
import FeatureSnippetActions from "./FeatureSnippetActions";
export default FeatureSnippetActions;
export { FeatureSnippetActions };
@@ -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>
);
}
}
@@ -0,0 +1,4 @@
import FeatureSnippetChoices from "./FeatureSnippetChoices";
export default FeatureSnippetChoices;
export { FeatureSnippetChoices };
@@ -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>
);
}
}
@@ -0,0 +1,4 @@
import FeatureSnippetInfusionChoices from "./FeatureSnippetInfusionChoices";
export default FeatureSnippetInfusionChoices;
export { FeatureSnippetInfusionChoices };
@@ -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>
);
}
}
@@ -0,0 +1,4 @@
import FeatureSnippetLimitedUse from "./FeatureSnippetLimitedUse";
export default FeatureSnippetLimitedUse;
export { FeatureSnippetLimitedUse };
@@ -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>
);
}
}
@@ -0,0 +1,4 @@
import FeatureSnippetOption from "./FeatureSnippetOption";
export default FeatureSnippetOption;
export { FeatureSnippetOption };
@@ -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>
);
}
}
@@ -0,0 +1,4 @@
import FeatureSnippetSpells from "./FeatureSnippetSpells";
export default FeatureSnippetSpells;
export { FeatureSnippetSpells };
@@ -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}
/>
))}
</>
);
}
}
@@ -0,0 +1,4 @@
import SpeciesTraitFeatureSnippet from "./SpeciesTraitFeatureSnippet";
export default SpeciesTraitFeatureSnippet;
export { SpeciesTraitFeatureSnippet };