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
+25
View File
@@ -0,0 +1,25 @@
import { useContext, useEffect, useState } from "react";
import {
AbilityManager,
FeaturesManager,
} from "@dndbeyond/character-rules-engine/es";
import { AttributesManagerContext } from "~/tools/js/Shared/managers/AttributesManagerContext";
export function useAbilities() {
const { attributesManager } = useContext(AttributesManagerContext);
const [abilities, setAbilities] = useState<Array<AbilityManager>>([]);
useEffect(() => {
async function onUpdate() {
const abilities = await attributesManager.getAbilities();
setAbilities(abilities);
}
return FeaturesManager.subscribeToUpdates({ onUpdate });
}, [attributesManager, setAbilities]);
return abilities;
}
+48
View File
@@ -0,0 +1,48 @@
import { useCallback, useState } from "react";
const identity = (x) => x;
/**
* Hook that encapsulates loading/error state setting when calling an API promise
* Usage:
* const [
* doThing, // callback to invoke to initiate the API call
* isDoingThing, // boolean indicating if the API call is in progress
* errorDoingThing, // error object indicating if there was an error caught (whatever param is passed to the .catch() callback)
* ] = useApiCall(MyApiUtils.doThing);
* @param {function} apiFunc The API function to use for setting loading/error state - Must return a promise!
* @param {function} [onSuccess] Optional callback invoked if the apiFunc promise succeeds
* @returns {[function, boolean, any]} An array containing a callback to initiate the API call, a isLoading boolean, and an error object
*/
const useApiCall = (apiFunc, onSuccess = identity) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const makeCall = useCallback(
(...args) => {
setIsLoading(true);
setError(null);
apiFunc(...args)
.then(onSuccess)
.then(() => {
setIsLoading(false);
})
.catch((err) => {
setIsLoading(false);
setError(err);
});
},
[apiFunc, onSuccess]
);
return [
makeCall as (...args: any[]) => void,
isLoading as boolean,
error as any,
];
};
export default useApiCall;
+428
View File
@@ -0,0 +1,428 @@
import { useSelector } from "react-redux";
import {
AbilityUtils as abilityUtils,
AccessUtils as accessUtils,
ActionUtils as actionUtils,
ActivationUtils as activationUtils,
ApiAdapterUtils as apiAdapterUtils,
BackgroundUtils as backgroundUtils,
CampaignUtils as campaignUtils,
CampaignSettingUtils as campaignSettingUtils,
CharacterUtils as characterUtils,
ChoiceUtils as choiceUtils,
ClassFeatureUtils as classFeatureUtils,
ClassUtils as classUtils,
ConditionUtils as conditionUtils,
ConditionLevelUtils as conditionLevelUtils,
ContainerUtils as containerUtils,
CoreUtils as coreUtils,
CreatureUtils as creatureUtils,
CreatureRuleUtils as creatureRuleUtils,
DataOriginUtils as dataOriginUtils,
DecorationUtils as decorationUtils,
DefinitionUtils as definitionUtils,
DefinitionPoolUtils as definitionPoolUtils,
DiceUtils as diceUtils,
DurationUtils as durationUtils,
EntityUtils as entityUtils,
ExtraUtils as extraUtils,
FeatUtils as featUtils,
FeatureFlagInfoUtils as featureFlagInfoUtils,
FeatureUtils as featureUtils,
FormatUtils as formatUtils,
HelperUtils as helperUtils,
InfusionChoiceUtils as infusionChoiceUtils,
InfusionUtils as infusionUtils,
ItemUtils as itemUtils,
KnownInfusionUtils as knownInfusionUtils,
LimitedUseUtils as limitedUseUtils,
ModifierUtils as modifierUtils,
NoteUtils as noteUtils,
OptionalClassFeatureUtils as optionalClassFeatureUtils,
OptionalOriginUtils as optionalOriginUtils,
OptionUtils as optionUtils,
OrganizationUtils as organizationUtils,
PdfUtils as pdfUtils,
PrerequisiteUtils as prerequisiteUtils,
RaceUtils as raceUtils,
RacialTraitUtils as racialTraitUtils,
RuleDataUtils as ruleDataUtils,
RuleDataPoolUtils as ruleDataPoolUtils,
SkillUtils as skillUtils,
SnippetUtils as snippetUtils,
SourceUtils as sourceUtils,
SpellUtils as spellUtils,
StartingEquipmentUtils as startingEquipmentUtils,
ValueUtils as valueUtils,
VehicleUtils as vehicleUtils,
VehicleComponentUtils as vehicleComponentUtils,
apiCreatorSelectors as api,
rulesEngineSelectors as s,
FeatList,
characterSelectors as char,
characterActions,
serviceDataSelectors as sds,
serviceDataActions,
} from "@dndbeyond/character-rules-engine";
/**
* A hook that provides access to the character rules engine state without
* having to import rules engine all over the place in your components. This
* reduces touchpoints to the rules engine and makes it easier to refactor the
* rules engine in the future.
*/
export const useCharacterEngine = () => ({
// Character Actions
characterActions,
serviceDataActions,
// Character Selectors that aren't passed thru to Rules Engine Selectors
characterStatusSlug: useSelector(char.getStatusSlug),
// Rules Engine Selector Values
abilities: useSelector(s.getAbilities),
abilityLookup: useSelector(s.getAbilityLookup),
abilityKey: useSelector(s.getAbilityKeyLookup),
actionLookup: useSelector(s.getActionLookup),
acAdjustments: useSelector(s.getAcAdjustments),
acSuppliers: useSelector(s.getArmorClassSuppliers),
acTotal: useSelector(s.getAcTotal),
actions: useSelector(s.getActions),
activatables: useSelector(s.getActivatables),
activeCharacterSpells: useSelector(s.getActiveCharacterSpells),
activeClassSpells: useSelector(s.getActiveClassSpells),
activeConditions: useSelector(s.getActiveConditions),
activeGroupedImmunities: useSelector(s.getActiveGroupedImmunities),
activeGroupedResistances: useSelector(s.getActiveGroupedResistances),
activeGroupedVulnerabilities: useSelector(s.getActiveGroupedVulnerabilities),
activeImmunities: useSelector(s.getActiveImmunities),
activeResistances: useSelector(s.getActiveResistances),
activeVulnerabilities: useSelector(s.getActiveVulnerabilities),
activeSource: useSelector(s.getActiveSourceLookup),
activeSourceCategories: useSelector(s.getActiveSourceCategories),
activeSources: useSelector(s.getActiveSources),
activeSpellAttackList: useSelector(s.getActiveSpellAttackList),
advantageSavingThrowModifiers: useSelector(
s.getAdvantageSavingThrowModifiers
),
alignment: useSelector(s.getAlignment),
allInventoryItems: useSelector(s.getAllInventoryItems),
armorSpeedAdjustmentAmount: useSelector(s.getArmorSpeedAdjustmentAmount),
attacks: useSelector(s.getAttacks),
attacksPerActionInfo: useSelector(s.getAttacksPerActionInfo),
attunedItems: useSelector(s.getAttunedItems),
attunedItemCountMax: useSelector(s.getAttunedItemCountMax),
attunedSlots: useSelector(s.getAttunedSlots),
availableInfusionChoices: useSelector(s.getAvailableInfusionChoices),
availablePactMagicSlotLevels: useSelector(s.getAvailablePactMagicSlotLevels),
availableSpellSlotLevels: useSelector(s.getAvailableSpellSlotLevels),
backgroundChoice: useSelector(s.getBackgroundChoiceLookup),
backgroundInfo: useSelector(s.getBackgroundInfo),
backgroundModifier: useSelector(s.getBackgroundModifierLookup),
backgroundModifiers: useSelector(s.getBackgroundModifiers),
baseClass: useSelector(s.getBaseClassLookup),
baseFeat: useSelector(s.getBaseFeatLookup),
baseFeats: useSelector(s.getBaseFeats),
bonusSavingThrowModifiers: useSelector(s.getBonusSavingThrowModifiers),
campaign: useSelector(s.getCampaign),
campaignSettings: useSelector(sds.getCampaignSettings),
carryingCapacity: useSelector(s.getCarryCapacity),
castableSpellSlotLevels: useSelector(s.getCastableSpellSlotLevels),
castablePactMagicSlotLevels: useSelector(s.getCastablePactMagicSlotLevels),
characterConfig: useSelector(s.getCharacterConfiguration),
characterGender: useSelector(s.getGender),
characterId: useSelector(s.getId),
characterInventoryContainers: useSelector(s.getCharacterInventoryContainers),
characterName: useSelector(s.getName),
characterNotes: useSelector(s.getCharacterNotes),
characterSpells: useSelector(s.getCharacterSpells),
characterTheme: useSelector(s.getCharacterTheme),
characterTraits: useSelector(s.getCharacterTraits),
choiceInfo: useSelector(s.getChoiceInfo),
classAction: useSelector(s.getClassActionLookup),
classAlwaysKnownSpells: useSelector(sds.getClassAlwaysKnownSpells),
classAlwaysPreparedSpells: useSelector(sds.getClassAlwaysPreparedSpells),
classChoice: useSelector(s.getClassChoiceLookup),
classCreatureRules: useSelector(s.getClassCreatureRules),
classFeature: useSelector(s.getClassFeatureLookup),
classMappingId: useSelector(s.getClassMappingIdLookupByActiveId),
classModifier: useSelector(s.getClassModifierLookup),
classOption: useSelector(s.getClassOptionLookup),
classSpell: useSelector(s.getClassSpellLookup),
classSpells: useSelector(s.getClassSpells),
classSpellInfo: useSelector(s.getClassSpellInfoLookup),
classSpellLists: useSelector(s.getClassSpellLists),
classSpellListSpells: useSelector(s.getClassSpellListSpellsLookup),
classes: useSelector(s.getClasses),
classesModifiers: useSelector(s.getClassesModifiers),
combinedMaxSpellSlotLevel: useSelector(s.getCombinedMaxSpellSlotLevel),
combinedSpellSlots: useSelector(s.getCombinedSpellSlots),
conModifier: useSelector(s.getConModifier),
conditionImmunityData: useSelector(s.getConditionImmunityData),
conditionModifier: useSelector(s.getConditionModifierLookup),
conditionModifiers: useSelector(s.getConditionModifiers),
container: useSelector(s.getContainerLookup),
containerCoinWeight: useSelector(s.getCointainerCoinWeight),
containerItemWeight: useSelector(s.getContainerItemWeight),
containerLookup: useSelector(s.getContainerLookup),
creature: useSelector(s.getCreatureLookup),
creatureGroupRules: useSelector(s.getCreatureGroupRulesLookup),
creatureInfusion: useSelector(s.getCreatureInfusionLookup),
creatureOwnerData: useSelector(s.getCreatureOwnerData),
creatureRules: useSelector(s.getCreatureRules),
creatures: useSelector(s.getCreatures),
currentCarriedWeightSpeed: useSelector(s.getCurrentCarriedWeightSpeed),
currentCarriedWeightType: useSelector(s.getCurrentCarriedWeightType),
currentLevel: useSelector(s.getCurrentLevel),
currentXp: useSelector(s.getCurrentXp),
currentLevelRacialTraits: useSelector(s.getCurrentLevelRacialTraits),
customActions: useSelector(s.getCustomActions),
customConditionAdjustments: useSelector(s.getCustomConditionAdjustments),
customDamageAdjustments: useSelector(s.getCustomDamageAdjustments),
customItems: useSelector(s.getCustomItems),
customImmunityDamageAdjustments: useSelector(
s.getCustomImmunityDamageAdjustments
),
customResistanceDamageAdjustments: useSelector(
s.getCustomResistanceDamageAdjustments
),
customVulnerabilityDamageAdjustments: useSelector(
s.getCustomVulnerabilityDamageAdjustments
),
customSkills: useSelector(s.getCustomSkills),
customSkillProficiencies: useSelector(s.getCustomSkillProficiencies),
damageImmunityData: useSelector(s.getDamageImmunityData),
deathSaveInfo: useSelector(s.getDeathSaveInfo),
deathCause: useSelector(s.getDeathCause),
decorationInfo: useSelector(s.getDecorationInfo),
dedicatedWeaponEnabled: useSelector(s.getDedicatedWeaponEnabled),
definitionPool: useSelector(sds.getDefinitionPool),
dexModifier: useSelector(s.getDexModifier),
disadvantageSavingThrowModifiers: useSelector(
s.getDisadvantageSavingThrowModifiers
),
encumberedWeight: useSelector(s.getEncumberedWeight),
entityRestrictionData: useSelector(s.getEntityRestrictionData),
entityValueLookup: useSelector(s.getCharacterValueLookupByEntity),
equippedItems: useSelector(s.getEquippedItems),
experienceInfo: useSelector(s.getExperienceInfo),
expertiseModifiers: useSelector(s.getExpertiseModifiers),
extras: useSelector(s.getExtras),
featLookup: useSelector(s.getFeatLookup),
feats: useSelector(s.getFeats),
featAction: useSelector(s.getFeatActionLookup),
featCreatureRules: useSelector(s.getFeatCreatureRules),
featModifier: useSelector(s.getFeatModifierLookup),
featModifiers: useSelector(s.getFeatModifiers),
featOption: useSelector(s.getFeatOptionLookup),
featChoice: useSelector(s.getFeatChoiceLookup),
gearWeaponItems: useSelector(s.getGearWeaponItems),
globalBackgroundSpellListIds: useSelector(s.getGlobalBackgroundSpellListIds),
globalRaceSpellListIds: useSelector(s.getGlobalRaceSpellListIds),
globalSpellListIds: useSelector(s.getGlobalSpellListIds),
hasInitiativeAdvantage: useSelector(s.getHasInitiativeAdvantage),
hasMaxAttunedItems: useSelector(s.hasMaxAttunedItems),
hasSpells: useSelector(s.hasSpells),
heavilyEncumberedWeight: useSelector(s.getHeavilyEncumberedWeight),
hexWeaponEnabled: useSelector(s.getHexWeaponEnabled),
highestEquippedArmorAC: useSelector(s.getHighestAcEquippedArmor),
highestEquippedShieldAC: useSelector(s.getHighestAcEquippedShield),
hpInfo: useSelector(s.getHitPointInfo),
improvedPactWeaponEnabled: useSelector(s.getImprovedPactWeaponEnabled),
infusions: useSelector(s.getInfusions),
infusionChoices: useSelector(s.getInfusionChoices),
infusionChoiceLookup: useSelector(s.getInfusionChoiceLookup),
infusionChoiceInfusion: useSelector(s.getInfusionChoiceInfusionLookup),
infusionMappings: useSelector(sds.getInfusionsMappings),
innateNaturalActions: useSelector(s.getInnateNaturalActions),
inventory: useSelector(s.getInventory),
inventoryContainers: useSelector(s.getInventoryContainers),
inventoryLookup: useSelector(s.getInventoryLookup),
inventoryInfusion: useSelector(s.getInventoryInfusionLookup),
isSheetReady: useSelector(s.isCharacterSheetReady),
isDead: useSelector(s.isDead),
isMulticlassCharacter: useSelector(s.isMulticlassCharacter),
itemAttacks: useSelector(s.getItemAttacks),
itemModifier: useSelector(s.getItemModifierLookup),
itemSpell: useSelector(s.getItemSpellLookup),
kenseiModifiers: useSelector(s.getKenseiModifiers),
knownInfusion: useSelector(s.getKnownInfusionLookup),
knownInfusionByChoice: useSelector(s.getKnownInfusionLookupByChoiceKey),
knownInfusions: useSelector(s.getKnownInfusions),
knownInfusionMappings: useSelector(sds.getKnownInfusionsMappings),
knownReplicatedItems: useSelector(s.getKnownReplicatedItems),
languages: useSelector(s.getLanguages),
languageModifiers: useSelector(s.getLanguageModifiers),
levelSpells: useSelector(s.getLevelSpells),
lifestyle: useSelector(s.getLifestyle),
loadSpecies: useSelector(api.makeLoadAvailableRaces),
maxPactMagicSlotLevel: useSelector(s.getMaxPactMagicSlotLevel),
maxSpellSlotLevel: useSelector(s.getMaxSpellSlotLevel),
miscModifiers: useSelector(s.getMiscModifiers),
modifierData: useSelector(s.getModifierData),
optionalClassFeature: useSelector(s.getOptionalClassFeatureLookup),
optionalClassFeatures: useSelector(s.getOptionalClassFeatures),
optionalOrigin: useSelector(s.getOptionalOriginLookup),
optionalOrigins: useSelector(s.getOptionalOrigins),
originRef: useSelector(s.getDataOriginRefData),
originRefBackground: useSelector(s.getDataOriginRefBackgroundData),
originRefClass: useSelector(s.getDataOriginRefClassData),
originRefCondition: useSelector(s.getDataOriginRefConditionData),
originRefFeat: useSelector(s.getDataOriginRefFeatData),
originRefFeatList: useSelector(s.getDataOriginRefFeatListData),
originRefItem: useSelector(s.getDataOriginRefItemData),
originRefRace: useSelector(s.getDataOriginRefRaceData),
originRefVehicle: useSelector(s.getDataOriginRefVehicleData),
overallSpellInfo: useSelector(s.getOverallSpellInfo),
overridePassiveInsight: useSelector(s.getOverridePassiveInsight),
overridePassiveInvestigation: useSelector(s.getOverridePassiveInvestigation),
overridePassivePerception: useSelector(s.getOverridePassivePerception),
pactMagicConditiones: useSelector(s.getPactMagicClasses),
pactMagicSlots: useSelector(s.getPactMagicSlots),
pactWeaponEnabled: useSelector(s.getPactWeaponEnabled),
partyInfo: useSelector(sds.getPartyInfo),
partyInventory: useSelector(s.getPartyInventory),
partyInventoryContainers: useSelector(s.getPartyInventoryContainers),
partyInventoryItem: useSelector(s.getPartyInventoryLookup),
passiveInsight: useSelector(s.getPassiveInsight),
passiveInvestigation: useSelector(s.getPassiveInvestigation),
passivePerception: useSelector(s.getPassivePerception),
pdfBucket1: useSelector(s.getPdfDataBucket1),
pdfBucket2: useSelector(s.getPdfDataBucket2),
pdfBucket3: useSelector(s.getPdfDataBucket3),
pdfBucket4: useSelector(s.getPdfDataBucket4),
pdfBucket5: useSelector(s.getPdfDataBucket5),
pdfBucket6: useSelector(s.getPdfDataBucket6),
pdfExportData: useSelector(s.getPdfExportData),
playerId: useSelector(s.getUserId),
playerName: useSelector(s.getUsername),
preferences: useSelector(s.getCharacterPreferences),
prerequisiteData: useSelector(s.getPrerequisiteData),
processedInitiative: useSelector(s.getProcessedInitiative),
proficiency: useSelector(s.getProficiencyLookup),
proficiencyBonus: useSelector(s.getProficiencyBonus),
proficiencyGroups: useSelector(s.getProficiencyGroups),
proficiencyModifiers: useSelector(s.getProficiencyModifiers),
protectionSuppliers: useSelector(s.getProtectionSuppliers),
pushDragLiftWeight: useSelector(s.getPushDragLiftWeight),
race: useSelector(s.getRace),
raceAction: useSelector(s.getRaceActionLookup),
raceChoice: useSelector(s.getRaceChoiceLookup),
raceCreatureRules: useSelector(s.getRaceCreatureRules),
raceModifier: useSelector(s.getRaceModifierLookup),
raceModifiers: useSelector(s.getRaceModifiers),
raceOption: useSelector(s.getRaceOptionLookup),
raceSpell: useSelector(s.getRaceSpellLookup),
requiredCharacterServiceParams: useSelector(
s.getRequiredCharacterServiceParams
),
requiredGameDataServiceParams: useSelector(
s.getRequiredGameDataServiceParams
),
resistanceData: useSelector(s.getResistanceData),
restrictedBonusSavingThrowModifiers: useSelector(
s.getRestrictedBonusSavingThrowModifiers
),
ritualSpells: useSelector(s.getRitualSpells),
ruleData: useSelector(s.getRuleData),
ruleDataPool: useSelector(sds.getRuleDataPool),
savingThrowDiceAdjustments: useSelector(s.getSavingThrowDiceAdjustments),
senseInfo: useSelector(s.getSenseInfo),
situationalBonusSavingThrowsLookup: useSelector(
s.getSituationalBonusSavingThrowsLookup
),
situationalBonusSavingThrows: useSelector(s.getSituationalBonusSavingThrows),
size: useSelector(s.getSize),
skill: useSelector(s.getSkillLookup),
skills: useSelector(s.getSkills),
snippetData: useSelector(s.getSnippetData),
socialImageData: useSelector(s.getSocialImageData),
specialWeaponPropertiesEnabled: useSelector(
s.hack__getSpecialWeaponPropertiesEnabled
),
speeds: useSelector(s.getSpeeds),
spellClasses: useSelector(s.getSpellClasses),
spellcasterInfo: useSelector(s.getSpellCasterInfo),
spellcastingClasses: useSelector(s.getSpellcastingClasses),
spellListDataOrigin: useSelector(s.getSpellListDataOriginLookup),
spellSlots: useSelector(s.getSpellSlots),
startingClass: useSelector(s.getStartingClass),
strScore: useSelector(s.getStrScore),
totalCarriedWeight: useSelector(s.getTotalCarriedWeight),
totalClassLevel: useSelector(s.getTotalClassLevel),
uniqueProficiencyModifiers: useSelector(s.getUniqueProficiencyModifiers),
validEquipmentModifiers: useSelector(s.getValidEquipmentModifiers),
validGlobalModifiers: useSelector(s.getValidGlobalModifiers),
validImmunityModifiers: useSelector(s.getValidImmunityModifiers),
validResistanceModifiers: useSelector(s.getValidResistanceModifiers),
validVulnerabilityModifiers: useSelector(s.getValidVulnerabilityModifiers),
valueLookup: useSelector(s.getCharacterValueLookup),
vehicle: useSelector(s.getVehicleLookup),
vehicles: useSelector(s.getVehicles),
vehicleComponent: useSelector(s.getVehicleComponentLookup),
vehicleComponentMappings: useSelector(sds.getVehicleComponentMappings),
vehicleMappings: useSelector(sds.getVehicleMappings),
vulnerabilityData: useSelector(s.getVulnerabilityData),
weaponSpellDamageGroups: useSelector(s.getWeaponSpellDamageGroups),
weightSpeeds: useSelector(s.getWeightSpeeds),
// Utility Functions
abilityUtils,
accessUtils,
actionUtils,
activationUtils,
apiAdapterUtils,
backgroundUtils,
campaignUtils,
campaignSettingUtils,
characterUtils,
choiceUtils,
classFeatureUtils,
classUtils,
conditionUtils,
conditionLevelUtils,
containerUtils,
coreUtils,
creatureUtils,
creatureRuleUtils,
dataOriginUtils,
decorationUtils,
definitionUtils,
definitionPoolUtils,
diceUtils,
durationUtils,
entityUtils,
extraUtils,
featUtils,
featureFlagInfoUtils,
featureUtils,
formatUtils,
helperUtils,
infusionChoiceUtils,
infusionUtils,
itemUtils,
knownInfusionUtils,
limitedUseUtils,
modifierUtils,
noteUtils,
optionalClassFeatureUtils,
optionalOriginUtils,
optionUtils,
organizationUtils,
pdfUtils,
prerequisiteUtils,
raceUtils,
racialTraitUtils,
ruleDataUtils,
ruleDataPoolUtils,
skillUtils,
snippetUtils,
sourceUtils,
spellUtils,
startingEquipmentUtils,
valueUtils,
vehicleUtils,
vehicleComponentUtils,
// Misc
FeatList,
});
+77
View File
@@ -0,0 +1,77 @@
import { useCallback, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { characterSelectors } from "@dndbeyond/character-rules-engine/es";
import { toastMessageActions } from "~/tools/js/Shared/actions";
import {
claimCharacter as claimCharacterApi,
joinCampaign as joinCampaignApi,
} from "../helpers/characterServiceApi";
interface UseClaimCharacterProps {
campaignJoinCode: string | null;
isAssigned: boolean;
}
export const useClaimCharacter = ({
campaignJoinCode,
isAssigned,
}: UseClaimCharacterProps) => {
const [isFinished, setIsFinished] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [newCharacterId, setNewCharacterId] = useState<number | null>(null);
const [campaignId, setCampaignId] = useState<number | null>(null);
const characterId = useSelector(characterSelectors.getId);
const dispatch = useDispatch();
const claimCharacter = useCallback(async () => {
try {
setIsLoading(true);
let clonedId;
const claimResponse = await claimCharacterApi(characterId, isAssigned);
if (claimResponse.ok) {
const data = await claimResponse.json();
clonedId = data.id;
setNewCharacterId(clonedId);
} else {
throw new Error(`There was an error claiming character ${characterId}`);
}
if (campaignJoinCode) {
const joinResponse = await joinCampaignApi(campaignJoinCode, clonedId);
if (joinResponse.ok) {
const data = await joinResponse.json();
setCampaignId(data.campaignId);
} else {
throw new Error(
`There was an error adding character ${clonedId} to campaign join code ${campaignJoinCode}`
);
}
}
} catch (error) {
console.error(error);
dispatch(
toastMessageActions.toastError("Error", "An unexpected error occurred.")
);
} finally {
setIsLoading(false);
setIsFinished(true);
}
}, [campaignJoinCode, characterId, dispatch, isAssigned]);
return [
claimCharacter,
isLoading,
isFinished,
newCharacterId,
campaignId,
] as const;
};
@@ -0,0 +1,33 @@
import { HTMLAttributes, useState } from "react";
import styles from '../../styles/errors.module.css';
export interface ErrorHandlerOptions {
initialState: boolean;
errMsg: string;
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>;
};
export interface ErrorHandler {
showError: boolean;
setShowError: (value: boolean) => void;
ErrorMessage: () => JSX.Element;
};
export const useErrorHandling = (
initialState: boolean,
errMsg: string,
errorMessageAttributes: HTMLAttributes<HTMLDivElement> | null = null
): ErrorHandler => {
const [showError, setShowError] = useState(initialState);
const ErrorMessage = (): JSX.Element => (
<div className={styles.inputError} {...errorMessageAttributes}>
{errMsg}
</div>
);
return {
showError,
setShowError,
ErrorMessage,
};
};
@@ -0,0 +1,49 @@
import { HTMLAttributes } from "react";
import { useErrorHandling } from "./useErrorHandling";
export interface MaxLengthErrorHandler {
handleMaxLengthErrorMsg: (value: string) => void;
hideError: () => void;
MaxLengthErrorMessage: () => JSX.Element;
};
export const useMaxLengthErrorHandling = (
initialState: boolean,
maxLength: number | null,
errMsg: string = "",
errorMessageAttributes?: HTMLAttributes<HTMLDivElement>
): MaxLengthErrorHandler => {
errMsg ||= `The max length is ${maxLength} characters.`;
const {
showError,
setShowError,
ErrorMessage,
} = useErrorHandling(initialState, errMsg, errorMessageAttributes);
const handleMaxLengthErrorMsg = (value: string): void => {
if (!maxLength) {
// Skip if maxLength is 0 or not set.
return;
}
const isTooLong = (value?.length ?? 0) >= (maxLength ?? 0);
if (isTooLong !== showError) {
setShowError(isTooLong);
}
};
const hideError = () => {
setShowError(false);
}
const MaxLengthErrorMessage = (): JSX.Element => showError && errMsg
? (<ErrorMessage />)
: <></>;
return {
handleMaxLengthErrorMsg,
MaxLengthErrorMessage,
hideError
};
}
+26
View File
@@ -0,0 +1,26 @@
import { useContext, useEffect, useState } from "react";
import {
ExtraManager,
FeaturesManager,
} from "@dndbeyond/character-rules-engine/es";
import { ExtrasManagerContext } from "~/tools/js/Shared/managers/ExtrasManagerContext";
//TODO: need to look at subscribeToUpdates for how many times its being called on state updates
export function useExtras() {
const { extrasManager } = useContext(ExtrasManagerContext);
const [extras, setExtras] = useState<Array<ExtraManager>>(
extrasManager.getCharacterExtraManagers()
);
useEffect(() => {
const onUpdate = () => {
setExtras(extrasManager.getCharacterExtraManagers());
};
return FeaturesManager.subscribeToUpdates({ onUpdate });
}, [extrasManager]);
return extras;
}
+59
View File
@@ -0,0 +1,59 @@
import {
useState,
useEffect,
useCallback,
Dispatch,
SetStateAction,
} from "react";
import { tryGet, tryRemove, trySet } from "../helpers/localStorageUtils";
//got most of the typing for this at https://usehooks-ts.com/react-hook/use-local-storage
type SetValue<T> = Dispatch<SetStateAction<T>>;
type RemoveValue<T> = Dispatch<SetStateAction<T>>;
function useLocalStorage<T>(
key: string,
defaultValue: T
): [T, SetValue<T>, RemoveValue<T>] {
const getValue = useCallback(() => {
const savedValue = tryGet(key);
return savedValue === null
? (defaultValue as T)
: (parseJSON(savedValue) as T);
}, [key, defaultValue]);
const [value, setValue] = useState(getValue());
useEffect(() => {
setValue(getValue());
}, [getValue]);
const set = useCallback(
(newValue) => {
setValue(newValue);
trySet(key, newValue);
},
[key, setValue]
);
const remove = () => {
setValue(defaultValue as T);
tryRemove(key);
};
return [value, set, remove];
}
export default useLocalStorage;
// A wrapper for "JSON.parse()"" to support "undefined" value
function parseJSON<T>(value: any): T | undefined {
try {
return value === "undefined" ? undefined : JSON.parse(value ?? "");
} catch (error) {
console.log("parsing error on", { value });
return value;
}
}
+58
View File
@@ -0,0 +1,58 @@
import { useCallback, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
ApiAdapterUtils,
ApiRequests,
characterActions,
rulesEngineSelectors,
syncTransactionActions,
} from "@dndbeyond/character-rules-engine";
import { toastMessageActions } from "~/tools/js/Shared/actions";
export interface ExportPdfData {
exportPdf: () => void;
pdfUrl: string | null;
isLoading: boolean;
isFinished: boolean;
}
export const usePdfExport = (): ExportPdfData => {
const [isFinished, setIsFinished] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const pdfData = useSelector(rulesEngineSelectors.getPdfExportData);
const dispatch = useDispatch();
const exportPdf = useCallback(async () => {
try {
setIsLoading(true);
let syncActivationId: string = "PDF-SYNC";
dispatch(syncTransactionActions.activate(syncActivationId));
//This will update the character's spell data before export is created
//(in case a user is in the builder and has not updated their class spells)
dispatch(characterActions.loadLazyCharacterData());
const response = await ApiRequests.postCharacterPdf({
exportData: JSON.stringify(pdfData),
});
const pdfUrl = ApiAdapterUtils.getResponseData(response);
if (pdfUrl) {
setPdfUrl(pdfUrl);
dispatch(syncTransactionActions.deactivate());
}
} catch (error) {
dispatch(
toastMessageActions.toastError("Error", "An unexpected error occurred.")
);
} finally {
setIsLoading(false);
setIsFinished(true);
}
}, []);
return { exportPdf, pdfUrl, isLoading, isFinished };
};
+62
View File
@@ -0,0 +1,62 @@
import { useSelector } from "react-redux";
import { useSidebar } from "~/contexts/Sidebar";
import {
SidebarAlignmentEnum,
SidebarPlacementEnum,
SidebarPositionInfo,
} from "~/subApps/sheet/components/Sidebar/types";
import { SheetPositioningInfo } from "~/tools/js/CharacterSheet/typings";
import { appEnvSelectors } from "~/tools/js/Shared/selectors";
export const usePositioning = () => {
const {
sidebar: { isVisible, placement, alignment, width },
} = useSidebar();
const getSheetPositioning = (): SheetPositioningInfo => {
let offset: number = 0;
if (!isVisible) {
return { offset, left: offset };
}
if (placement === SidebarPlacementEnum.FIXED) {
offset = width / 2;
}
return {
offset,
left: alignment === SidebarAlignmentEnum.LEFT ? offset : -1 * offset,
};
};
const getSidebarPositioning = (): SidebarPositionInfo => {
const sheetDimensions = useSelector(appEnvSelectors.getDimensions);
const sheetPosition = getSheetPositioning();
const { sheet, window } = sheetDimensions;
let posX: number = 0;
let sidebarGutter: number = 15;
let screenGutterSize: number = (window.width - sheet.width) / 2;
if (placement === SidebarPlacementEnum.FIXED) {
posX = screenGutterSize + sheetPosition.offset - sidebarGutter - width;
} else {
screenGutterSize -= sidebarGutter;
let overflowOffset: number =
width > screenGutterSize ? width - screenGutterSize : 0;
posX = screenGutterSize - width + overflowOffset;
}
return {
left: alignment === SidebarAlignmentEnum.LEFT ? posX : "auto",
right: alignment === SidebarAlignmentEnum.RIGHT ? posX : "auto",
};
};
return {
getSheetPositioning,
getSidebarPositioning,
};
};
+104
View File
@@ -0,0 +1,104 @@
import { useSelector } from "react-redux";
import {
RuleDataUtils as ruleDataUtils,
ruleDataSelectors as s,
} from "@dndbeyond/character-rules-engine";
/**
* A hook that provides access to the rule data state without having to
* import rules engine all over the place in your components. This reduces
* touchpoints to the rules engine and makes it easier to refactor the rules
* engine in the future.
*/
export const useRuleData = () => ({
// Export all data coming from the RuleData selectors
allData: useSelector(s.getAllData),
abilityScoreDisplayTypes: useSelector(s.getAbilityScoreDisplayTypes),
abilitySkills: useSelector(s.getAbilitySkills),
activationTypes: useSelector(s.getActivationTypes),
additionalLevelTypes: useSelector(s.getAdditionalLevelTypes),
adjustmentDataTypes: useSelector(s.getAdjustmentDataTypes),
adjustmentTypes: useSelector(s.getAdjustmentTypes),
alignments: useSelector(s.getAlignments),
aoeTypes: useSelector(s.getAoeTypes),
armor: useSelector(s.getArmor),
basicActions: useSelector(s.getBasicActions),
baseWeaponReach: useSelector(s.getBaseWeaponReach),
basicMaxStatScore: useSelector(s.getBasicMaxStatScore),
challengeRatings: useSelector(s.getChallengeRatings),
conditionTypes: useSelector(s.getConditionTypes),
conditions: useSelector(s.getConditions),
creatureGroupCategories: useSelector(s.getCreatureGroupCategories),
creatureGroupFlags: useSelector(s.getCreatureGroupFlags),
creatureGroups: useSelector(s.getCreatureGroups),
creatureSizes: useSelector(s.getCreatureSizes),
currencyData: useSelector(s.getCurrencyData),
damageAdjustments: useSelector(s.getDamageAdjustments),
damageTypes: useSelector(s.getDamageTypes),
defaultArmorImageUrl: useSelector(s.getDefaultArmorImageUrl),
defaultAttunedItemCountMax: useSelector(s.getDefaultAttunedItemCountMax),
defaultGearImageUrl: useSelector(s.getDefaultGearImageUrl),
defaultRacePortraitUrl: useSelector(s.getDefaultRacePortraitUrl),
defaultWeaponImageUrl: useSelector(s.getDefaultWeaponImageUrl),
diceValues: useSelector(s.getDiceValues),
environments: useSelector(s.getEnvironments),
initiativeScore: useSelector(s.getInitiativeScore),
languageTypeId: useSelector(s.getLanguageTypeId),
languages: useSelector(s.getLanguages),
levelExperiencePoints: useSelector(s.getLevelExperiencePoints),
levelProficiencyBonuses: useSelector(s.getLevelProficiencyBonuses),
lifestyles: useSelector(s.getLifestyles),
limitedUseResetTypes: useSelector(s.getLimitedUseResetTypes),
longRestMinHitDiceUsedRecovered: useSelector(
s.getLongRestMinimumHitDiceUsedRecovered
),
maxAttunedItemCountMax: useSelector(s.getMaxAttunedItemCountMax),
maxCharacterLevel: useSelector(s.getMaxCharacterLevel),
maxDeathsavesFail: useSelector(s.getMaxDeathsavesFail),
maxDeathsavesSuccess: useSelector(s.getMaxDeathsavesSuccess),
maxSpellLevel: useSelector(s.getMaxSpellLevel),
maxStatScore: useSelector(s.getMaxStatScore),
minAttunedItemCountMax: useSelector(s.getMinAttunedItemCountMax),
minStatScore: useSelector(s.getMinStatScore),
minimumHpTotal: useSelector(s.getMinimumHpTotal),
minimumLimitedUseMaxUse: useSelector(s.getMinimumLimitedUseMaxUse),
monsterSubTypes: useSelector(s.getMonsterSubTypes),
monsterTypes: useSelector(s.getMonsterTypes),
movements: useSelector(s.getMovements),
multiClassSpellSlots: useSelector(s.getMultiClassSpellSlots),
naturalActions: useSelector(s.getNaturalActions),
noArmorAcAmount: useSelector(s.getNoArmorAcAmount),
pactMagicMultiClassSpellSlots: useSelector(
s.getPactMagicMultiClassSpellSlots
),
privacyTypes: useSelector(s.getPrivacyTypes),
proficiencyGroups: useSelector(s.getProficiencyGroups),
raceGroups: useSelector(s.getRaceGroups),
rangeTypes: useSelector(s.getRangeTypes),
ritualCastingTimeMinuteAddition: useSelector(
s.getRitualCastingTimeMinuteAddition
),
rules: useSelector(s.getRules),
senses: useSelector(s.getSenses),
sharingTypes: useSelector(s.getSharingTypes),
sourceCategories: useSelector(s.getSourceCategories),
sources: useSelector(s.getSources),
spellComponents: useSelector(s.getSpellComponents),
spellConditionTypes: useSelector(s.getSpellConditionTypes),
spellRangeTypes: useSelector(s.getSpellRangeTypes),
statModifiers: useSelector(s.getStatModifiers),
stats: useSelector(s.getStats),
stealthCheckTypes: useSelector(s.getStealthCheckTypes),
stringMartialArts: useSelector(s.getStringMartialArts),
stringPactMagic: useSelector(s.getStringPactMagic),
stringSpellCasting: useSelector(s.getStringSpellCasting),
stringSpellEldritchBlast: useSelector(s.getStringSpellEldritchBlast),
weapons: useSelector(s.getWeapons),
weaponCategories: useSelector(s.getWeaponCategories),
weaponProperties: useSelector(s.getWeaponProperties),
weaponPropertyReachDistance: useSelector(s.getWeaponPropertyReachDistance),
// Export all utils from RuleDataUtils
ruleDataUtils,
});
+179
View File
@@ -0,0 +1,179 @@
import { orderBy } from "lodash";
import {
BuilderChoiceOptionContract,
EntityRestrictionData,
HelperUtils,
HtmlSelectOptionGroup,
RuleDataUtils,
SimpleSourcedDefinitionContract,
SourceMappingContract,
SourceUtils,
} from "@dndbeyond/character-rules-engine";
import { TypeScriptUtils } from "~/tools/js/Shared/utils";
import { useCharacterEngine } from "./useCharacterEngine";
import { useRuleData } from "./useRuleData";
interface SourceCategoryGroup<T> {
name: string;
id: number;
items: Array<T>;
sortOrder: number | undefined;
}
type SourceCategoryKey =
| "avatarUrl"
| "description"
| "id"
| "isEnabledByDefault"
| "isHideable"
| "isPartneredContent"
| "isToggleable"
| "name";
export const useSource = () => {
const {
ruleData,
helperUtils: { lookupDataOrFallback },
ruleDataUtils: { getSourceCategoryLookup },
} = useCharacterEngine();
const {
ruleDataUtils: { getSourceDataLookup },
} = useRuleData();
const allSources = getSourceDataLookup(ruleData);
const allSourceCategories = getSourceCategoryLookup(ruleData);
// Returns the source ID from a given source
const getSourceId = (source: SourceMappingContract) => source?.sourceId;
const getSource = (sourceId: number | string) =>
lookupDataOrFallback(allSources, sourceId);
const getSourceName = (sourceId: number | string) =>
lookupDataOrFallback(allSources, sourceId)?.name;
const getSourceDescription = (sourceId: number | string) =>
lookupDataOrFallback(allSources, sourceId)?.description;
// Source Category direct access with category id
const getSourceCategory = (sourceCategoryId: number) =>
lookupDataOrFallback(allSourceCategories, sourceCategoryId);
const getSourceCategoryDescription = (sourceCategoryId: number) =>
getSourceCategory(sourceCategoryId)?.description;
// Takes an array of any entity that contains a sources key and returns an array of sorted SourceCategoryGroup representing each Source Category with the category data and an array of the entities that belong to that category.
const getSourceCategoryGroups = <
T extends {
sources: SourceMappingContract[] | null;
}
>(
items: Array<T>
): Array<SourceCategoryGroup<T>> => {
let groups: Array<SourceCategoryGroup<T>> = [];
items.forEach((item) => {
const sourceContracts = item.sources ?? [];
//get the source data for each source contract
const sources = sourceContracts
.map((source) =>
HelperUtils.lookupDataOrFallback(
RuleDataUtils.getSourceDataLookup(ruleData),
source.sourceId
)
)
.filter(TypeScriptUtils.isNotNullOrUndefined);
//get the source category name for the first source contract or default to Homebrew
const sourceCategoryName =
sources.length > 0 &&
sources[0].sourceCategory &&
sources[0].sourceCategory.name
? sources[0].sourceCategory.name
: "Homebrew";
// Search the availableGroupedOptions array for the source category
const index = groups.findIndex(
(element) => element.name === sourceCategoryName
);
// If the source category wasn't found, add it as a new option group.
if (index < 0) {
groups.push({
name: sourceCategoryName,
id: sources[0]?.sourceCategory?.id || 0,
sortOrder: sources[0]?.sourceCategory?.sortOrder,
items: [item],
});
// Otherwise, add the option to the existing source category's array of options.
} else {
groups[index].items.push(item);
}
});
return orderBy(groups, "sortOrder");
};
// Takes an array of any entity's DefinitionContract and returns an array of sorted HtmlSelectOptionGroup representing each Source Category and a sorted array of options.
const getGroupedOptionsBySourceCategory = <
T extends {
sources: SourceMappingContract[] | null;
name: string | null;
id: number;
description?: string | null;
}
>(
items: Array<T>,
optionValue?: number | null,
entityRestrictionData?: EntityRestrictionData,
labelFallback?: string
): HtmlSelectOptionGroup[] => {
return SourceUtils.getGroupedOptionsBySourceCategory(
items,
ruleData,
optionValue,
entityRestrictionData,
labelFallback
);
};
//Given an array of choiceOptionContracts, return an array of SimpleSourcedDefinitionContracts (these are used to create grouped options in a select dropdown)
const getSimpleSourcedDefinitionContracts = (
choiceOptionContracts: Array<BuilderChoiceOptionContract>
): Array<SimpleSourcedDefinitionContract> => {
return SourceUtils.getSimpleSourcedDefinitionContracts(
choiceOptionContracts
);
};
// Compares a source's category to a given match and returns a boolean value
const matchSourceCategory = (
sourceId: number,
match: string,
key: SourceCategoryKey = "name"
) => {
if (!sourceId) return false;
const source = lookupDataOrFallback(allSources, sourceId);
if (!source) return false;
if (source.sourceCategory?.[key] === match) return true;
};
return {
allSources,
getSource,
getSourceId,
getSourceCategory,
getSourceCategoryDescription,
getSourceDescription,
getSourceName,
matchSourceCategory,
getGroupedOptionsBySourceCategory,
getSimpleSourcedDefinitionContracts,
getSourceCategoryGroups,
};
};
+26
View File
@@ -0,0 +1,26 @@
import { useState, useEffect } from "react";
import { getSubscriptionTier } from "../helpers/userApi";
export const FREE_TIER = "free";
export const HERO_TIER = "hero";
export const MASTER_TIER = "master";
export const LEGENDARY_TIER = "legendary";
const useSubscriptionTier = (defaultSubscription?: string) => {
const [subscriptionTier, setSubscriptionTier] = useState<string | undefined>(
defaultSubscription
);
useEffect(() => {
getSubscriptionTier().then((tier) => {
if (tier) {
setSubscriptionTier(tier as unknown as string);
}
});
}, []);
return (subscriptionTier || FREE_TIER).toLowerCase();
};
export default useSubscriptionTier;
+21
View File
@@ -0,0 +1,21 @@
import { MouseEvent, useCallback } from "react";
/**
* Prevents click events from propagating to parent elements.
* @param onClick The function to call when the element is clicked.
* @returns A function that will prevent the click event from propagating.
*/
export const useUnpropagatedClick = (
onClick?: (event: MouseEvent, ...args: unknown[]) => void
) => {
const handleClick = useCallback(
(event: MouseEvent, ...args: unknown[]) => {
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
onClick?.(event, ...args);
},
[onClick]
);
return handleClick;
};
+42
View File
@@ -0,0 +1,42 @@
import { useState, useEffect } from "react";
import type { User } from "@dndbeyond/authentication-lib-js";
import { getUser } from "../helpers/userApi";
export interface AppUser extends User {
displayName: string;
id: string;
name: string;
roles: Array<string>;
subscription: string;
subscriptionTier: string;
}
/**
* AppUserState has three states:
* - undefined: We currently don't know if the user is authenticated or not. ie loading state
* - null: user is not authenticated
* - User: user is authenticated and is a registered user
*/
const useUserInitialState = undefined;
type UseUserUnauthenticated = null;
export type AppUserState =
| AppUser
| UseUserUnauthenticated
| typeof useUserInitialState;
const useUser = () => {
const [user, setUser] = useState<AppUserState>(useUserInitialState);
useEffect(() => {
getUser()
.then(setUser)
.catch(() => {
setUser(null);
});
}, []);
return user;
};
export default useUser;
+17
View File
@@ -0,0 +1,17 @@
import { useState, useEffect } from "react";
import { getUserId } from "../helpers/userApi";
const useUserId = (defaultUserId?: number) => {
const [userId, setUserId] = useState<number | undefined>(defaultUserId);
useEffect(() => {
getUserId().then((user) => {
setUserId(user as unknown as number);
});
}, []);
return userId;
};
export default useUserId;
+19
View File
@@ -0,0 +1,19 @@
import { useState, useEffect } from "react";
import { getUserDisplayName } from "../helpers/userApi";
const useUserName = () => {
const [userName, setUserName] = useState<string | null>("");
useEffect(() => {
getUserDisplayName()
.then(setUserName)
.catch(() => {
setUserName(null);
});
}, []);
return userName;
};
export default useUserName;
+19
View File
@@ -0,0 +1,19 @@
import { useState, useEffect } from "react";
import { getRoles } from "../helpers/userApi";
const useUserRoles = () => {
const [userRoles, setUserRoles] = useState<string[]>([]);
useEffect(() => {
getRoles()
.then(setUserRoles)
.catch(() => {
setUserRoles([]);
});
}, []);
return userRoles;
};
export default useUserRoles;