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,7 @@
import * as HttpStatusCodes from "../../constants/HttpStatusCodes";
export const COALESCED_ERROR_CODES = [
HttpStatusCodes.UNAUTHORIZED,
HttpStatusCodes.FORBIDDEN,
HttpStatusCodes.NOT_FOUND,
];
@@ -0,0 +1,227 @@
import axios from "axios";
import { merge } from "lodash";
import {
ApiAdapterDataException,
ApiAdapterException,
ApiAdapterUrlException,
ApiException,
AuthException,
AuthMissingException,
Constants,
} from "@dndbeyond/character-rules-engine/es";
import { appEnvActions } from "../../actions";
import { appInfoActions } from "../../actions/appInfo";
import AppErrorTypeEnum from "../../constants/AppErrorTypeEnum";
import * as HttpStatusCodes from "../../constants/HttpStatusCodes";
import { appEnvSelectors } from "../../selectors";
import { StateStoreUtils } from "../../stores";
import { AppNotificationUtils, ErrorCustomTags, ErrorUtils } from "../../utils";
import { COALESCED_ERROR_CODES } from "./constants";
let hasDispatchedAuthException: boolean = false;
/**
*
* @param url
*/
export function processApiUrl(url: string): string {
let match = url.match(/^(.*\/character\/v\d+\/character)\/\d+$/);
if (match) {
return [match[1], "{id}"].join("/");
}
return url;
}
/**
*
* @param error
* @param extraContextData
*/
export function getApiAdapterUrlExceptionContextData(
error: ApiAdapterUrlException,
extraContextData: Record<string, any>
): Record<string, any> {
let errorContextData: Record<string, any> = error.contextData ?? {};
let errorMessageContextData: Record<string, any> = {
errorMessage: error.message,
errorUrl: error.url,
errorMethod: error.method,
};
return merge({}, errorContextData, errorMessageContextData, extraContextData);
}
/**
*
* @param error
*/
export function getApiAdapterUrlExceptionCustomTags(
error: ApiAdapterUrlException
): ErrorCustomTags {
return {
serverErrorId: error.contextData?.serverErrorData?.errorCode ?? null,
};
}
/**
*
* @param error
* @param messagePrefix
*/
export function getApiAdapterUrlExceptionMessage(
error: ApiAdapterUrlException,
messagePrefix: string
): string {
if (error.errorCode === null) {
return "Unknown Client Request Failure";
}
if (COALESCED_ERROR_CODES.includes(error.errorCode)) {
return [messagePrefix, error.errorCode].join(" - ");
}
return [
messagePrefix,
error.errorCode,
error.method ?? "UNKNOWN Method",
error.url ? processApiUrl(error.url) : "UNKNOWN URL",
].join(" - ");
}
/**
*
* @param error
*/
export function getApiAdapterUrlExceptionAppErrorType(
error: ApiAdapterUrlException
): AppErrorTypeEnum {
if (error.errorCode === HttpStatusCodes.FORBIDDEN) {
return AppErrorTypeEnum.ACCESS_DENIED;
}
if (error.errorCode === HttpStatusCodes.NOT_FOUND) {
return AppErrorTypeEnum.NOT_FOUND;
}
return AppErrorTypeEnum.API_FAIL;
}
/**
*
* @param error
* @param contextData
*/
export function logError(
error: Error,
contextData: Record<string, any> = {}
): null {
// This function returns null because the Rules Engine config expects a log method
// that returns string | null. See packages/rules-engine/src/config/typings.ts
if (process.env.NODE_ENV !== "production") {
console.log("***************");
console.log("The following error happened:");
console.log(error);
console.log("***************");
}
if (axios.isCancel(error)) {
// only do something if not canceled, cancellation should be fine
return null;
}
let appErrorType: AppErrorTypeEnum = AppErrorTypeEnum.GENERIC;
let message: string | null = null;
let shouldLogError: boolean = true;
let customTags: ErrorCustomTags = {};
if (error instanceof ApiException) {
appErrorType = getApiAdapterUrlExceptionAppErrorType(error);
contextData = getApiAdapterUrlExceptionContextData(error, contextData);
customTags = getApiAdapterUrlExceptionCustomTags(error);
message = getApiAdapterUrlExceptionMessage(error, "API");
} else if (error instanceof ApiAdapterDataException) {
appErrorType = getApiAdapterUrlExceptionAppErrorType(error);
contextData = getApiAdapterUrlExceptionContextData(error, contextData);
customTags = getApiAdapterUrlExceptionCustomTags(error);
message = getApiAdapterUrlExceptionMessage(error, "API Data");
} else if (error instanceof AuthException) {
appErrorType = AppErrorTypeEnum.AUTH_FAIL;
message = "Auth Failure";
// We could emit multiple auth exceptions when we make parallel requests
// AuthException isn't recoverable, so it should be fine to only log the first one
if (hasDispatchedAuthException) {
shouldLogError = false;
} else {
hasDispatchedAuthException = true;
}
} else if (error instanceof AuthMissingException) {
appErrorType = AppErrorTypeEnum.AUTH_MISSING;
shouldLogError = false;
}
if (shouldLogError) {
if (message === null) {
ErrorUtils.dispatchException(error, null, customTags);
} else {
ErrorUtils.dispatchError(
message,
contextData,
customTags,
Constants.LogMessageType.ERROR
);
}
}
const store = StateStoreUtils.getAppStore();
if (store !== null) {
const diceUrl = appEnvSelectors.getDiceFeatureConfiguration(
store.getState()
).apiEndpoint;
if (error instanceof ApiException && error?.url?.includes(diceUrl)) {
store.dispatch(
appEnvActions.dataSet({
diceEnabled: false,
})
);
AppNotificationUtils.dispatchError(
"Dice Service Error",
"The dice service is currently experiencing issues. We have disabled it temporarily as a result."
);
} else {
store.dispatch(appInfoActions.errorSet(appErrorType));
}
}
return null;
}
/**
*
* @param message
* @param type
* @param contextData
*/
export function logMessage(
message: string,
type: Constants.LogMessageType,
contextData: Record<string, any> = {}
): null {
ErrorUtils.dispatchError(message, contextData, null, type);
return null;
}
/**
*
* @param error
*/
export function handleAdhocApiError(error: Error): void {
// ApiAdapterException errors are handled by the AppApiAdapter
if (!(error instanceof ApiAdapterException)) {
logError(error);
}
}
@@ -0,0 +1,78 @@
import {
CharClass,
ClassUtils,
Extra,
ExtraUtils,
Item,
ItemUtils,
Spell,
SpellUtils,
} from "@dndbeyond/character-rules-engine/es";
import { AppNotificationUtils } from "../../utils";
export function handleItemAddAccepted(item: Item, amount: number): void {
if (ItemUtils.isPack(item)) {
AppNotificationUtils.dispatchSuccess(
"Equipment Pack Added",
`${ItemUtils.getName(item)} items added to your inventory`
);
} else {
if (amount >= 1) {
let itemName = ItemUtils.getDefinitionName(item);
let displayName = itemName ? `"${itemName}"` : "unknown";
let toastMessage = `Added ${displayName} item`;
if (amount > 1) {
toastMessage = `Added ${amount} ${displayName} items`;
}
AppNotificationUtils.dispatchSuccess("Equipment Added", toastMessage);
}
}
}
export function handleItemAddRejected(item: Item, amount: number): void {
AppNotificationUtils.dispatchError(
"Equipment Not Added",
`${ItemUtils.getName(item)} could not be added to your inventory`
);
}
export function handleStartingEquipmentAccepted(): void {
AppNotificationUtils.dispatchSuccess(
"Equipment Chosen",
"Starting Equipment Added to Inventory"
);
}
export function handleStartingEquipmentRejected(): void {
AppNotificationUtils.dispatchSuccess(
"Starting Equipment Skipped",
"No items added to inventory"
);
}
export function handleStartingGoldAccepted(): void {
AppNotificationUtils.dispatchSuccess(
"Gold Added",
"Starting Gold Added to Currencies Section"
);
}
export function handleSpellCreateAccepted(
spell: Spell,
charClass: CharClass
): void {
AppNotificationUtils.dispatchSuccess(
"Spell Added",
`Added ${SpellUtils.getName(spell)} spell to your ${ClassUtils.getName(
charClass
)}`
);
}
//TODO Extras - move into manager somehow?
export function handleExtraCreateAccepted(extra: Extra): void {
AppNotificationUtils.dispatchSuccess(
`${ExtraUtils.getExtraType(extra)} Added`,
`Added "${ExtraUtils.getName(extra)}"`
);
}
@@ -0,0 +1,52 @@
import { Constants } from "@dndbeyond/character-rules-engine/es";
import { toastMessageActions } from "../../actions/toastMessage";
import { StateStoreUtils } from "../../stores";
/**
*
* @param title
* @param message
* @param notificationType
*/
export function dispatchNotification(
title: string,
message: string,
notificationType: Constants.NotificationTypeEnum
): void {
const store = StateStoreUtils.getAppStore();
if (store) {
switch (notificationType) {
case Constants.NotificationTypeEnum.ERROR:
case Constants.NotificationTypeEnum.CRITICAL:
store.dispatch(toastMessageActions.toastError(title, message));
break;
default:
store.dispatch(toastMessageActions.toastSuccess(title, message));
}
}
}
/**
*
* @param title
* @param message
*/
export function dispatchSuccess(title: string, message: string): void {
const store = StateStoreUtils.getAppStore();
if (store) {
store.dispatch(toastMessageActions.toastSuccess(title, message));
}
}
/**
*
* @param title
* @param message
*/
export function dispatchError(title: string, message: string): void {
const store = StateStoreUtils.getAppStore();
if (store) {
store.dispatch(toastMessageActions.toastError(title, message));
}
}
@@ -0,0 +1,44 @@
/**
* sourced: https://stackoverflow.com/questions/400212/how-do-i-copy-to-the-clipboard-in-javascript
* @param text
*/
export function copyTextToClipboard(text: string): boolean {
let textArea = document.createElement("textarea");
// Place in top-left corner of screen regardless of scroll position.
textArea.style.position = "fixed";
textArea.style.top = "0";
textArea.style.left = "0";
// Ensure it has a small width and height. Setting to 1px / 1em
// doesn't work as this gives a negative w/h on some browsers.
textArea.style.width = "2em";
textArea.style.height = "2em";
// We don't need padding, reducing the size if it does flash render.
textArea.style.padding = "0";
// Clean up any borders.
textArea.style.border = "none";
textArea.style.outline = "none";
textArea.style.boxShadow = "none";
// Avoid flash of white box if rendered for any reason.
textArea.style.background = "transparent";
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
let successful: boolean;
try {
successful = document.execCommand("copy");
} catch (err) {
return false;
}
document.body.removeChild(textArea);
return successful;
}
@@ -0,0 +1,73 @@
/**
* Convert Hex code to RGB before getting ratio
* Return whether or not the colors meet the
* minimum requirements for Accessibility
*/
export const checkContrast = (color1, color2) => {
let [luminance1, luminance2] = [color1, color2].map((color) => {
/* Remove the last 2 characters if 8 digits */
color = color.length > 7 ? color.slice(0, 6) : color;
/* Remove the leading hash sign if it exists */
color = color.startsWith("#") ? color.slice(1) : color;
let r = parseInt(color.slice(0, 2), 16);
let g = parseInt(color.slice(2, 4), 16);
let b = parseInt(color.slice(4, 6), 16);
return luminance(r, g, b);
});
const ratio = contrastRatio(luminance1, luminance2);
const { didPass } = meetsMinimumRequirements(ratio);
return didPass;
};
export const contrastRatio = (luminance1, luminance2) => {
let lighterLum = Math.max(luminance1, luminance2);
let darkerLum = Math.min(luminance1, luminance2);
return (lighterLum + 0.05) / (darkerLum + 0.05);
};
/**
* Function to determine relative luminance from RGB values
*/
export const luminance = (r: number, g: number, b: number) => {
let [lumR, lumG, lumB] = [r, g, b].map((component) => {
let proportion = component / 255;
return proportion <= 0.03928
? proportion / 12.92
: Math.pow((proportion + 0.055) / 1.055, 2.4);
});
return 0.2126 * lumR + 0.7152 * lumG + 0.0722 * lumB;
};
/**
* Determine whether the given contrast ratio meets WCAG
* requirements at any level (AA Large, AA, or AAA). In the return
* value, `isPass` is true if the ratio meets or exceeds the minimum
* of at least one level, and `maxLevel` is the strictest level that
* the ratio passes.
*/
const WCAG_MINIMUM_RATIOS = [
["AA Large", 3],
["AA", 4.5],
["AAA", 7],
];
export const meetsMinimumRequirements = (ratio) => {
let didPass = false;
let maxLevel: string | number = "";
for (const [level, minRatio] of WCAG_MINIMUM_RATIOS) {
if (ratio < minRatio) break;
didPass = true;
maxLevel = level;
}
return { didPass, maxLevel };
};
@@ -0,0 +1,614 @@
import {
Action,
ActionUtils,
ConditionUtils,
Constants,
DiceUtils,
FormatUtils,
HelperUtils,
RuleData,
RuleDataUtils,
SourceData,
Vehicle,
VehicleComponent,
VehicleComponentArmorClassInfo,
VehicleComponentSpeedInfo,
VehicleComponentUtils,
VehicleFeatureData,
VehicleManager,
VehicleUtils,
} from "@dndbeyond/character-rules-engine/es";
import { TypeScriptUtils } from "../index";
import {
generateVehicleActionStationProps,
generateVehicleBlockComponentProps,
generateVehiclePrimaryComponentProps,
} from "./generators";
import {
GD_VehicleBlockActionStationProps,
GD_VehicleBlockComponentProps,
GD_VehicleBlockPrimaryComponentProps,
VehicleActionSummaryProps,
VehicleBlockActionProp,
VehicleComponentActionInfoProp,
VehicleComponentCargoCapacityProp,
VehicleComponentHitPointInfoProps,
VehicleComponentTravelPaceInfoProp,
} from "./typings";
/**
*
* @param vehicle
* @param ruleData
*/
export function deriveVehicleTypeNameProp(
vehicle: Vehicle,
ruleData: RuleData
): string | null {
const type = VehicleUtils.getType(vehicle);
if (type === null) {
return null;
}
return RuleDataUtils.getObjectTypeName(type, ruleData);
}
/**
*
* @param vehicle
*/
export function deriveVehicleSizeNameProp(vehicle: Vehicle): string | null {
const sizeInfo = VehicleUtils.getSizeInfo(vehicle);
if (sizeInfo === null) {
return null;
}
return sizeInfo.name;
}
/**
*
* @param vehicle
*/
export function deriveVehicleComponentCargoCapacityProp(
vehicle: Vehicle
): VehicleComponentCargoCapacityProp {
return {
weight: VehicleUtils.getCargoCapacity(vehicle),
description: VehicleUtils.getCargoCapacityDescription(vehicle),
};
}
/**
*
* @param vehicle
*/
export function deriveVehicleDamageImmunitiesProps(
vehicle: Vehicle
): Array<string> {
return VehicleUtils.getDamageImmunityInfos(vehicle)
.map((immunityInfo) => immunityInfo.name)
.filter(TypeScriptUtils.isNotNullOrUndefined);
}
/**
*
* @param vehicle
*/
export function deriveVehicleConditionImmunitiesProps(
vehicle: Vehicle
): Array<string> {
return VehicleUtils.getConditionImmunityInfos(vehicle).map((conditionInfo) =>
ConditionUtils.getName(conditionInfo)
);
}
/**
*
* @param vehicle
*/
export function deriveVehicleComponentTravelPaceInfoProp(
vehicle: Vehicle
): VehicleComponentTravelPaceInfoProp | null {
const enableTravelPace = VehicleUtils.getConfigurationValue(
Constants.VehicleConfigurationKeyEnum.ENABLE_TRAVEL_PACE,
vehicle
);
if (!enableTravelPace) {
return null;
}
const pace = VehicleUtils.getTravelPace(vehicle);
const effectiveHours = VehicleUtils.getTravelPaceEffectiveHours(vehicle);
let travelPaceInfo: VehicleComponentTravelPaceInfoProp | null = null;
if (pace !== null) {
travelPaceInfo = {
pace,
effectiveHours,
};
}
return travelPaceInfo;
}
/**
*
* @param vehicle
*/
export function deriveVehicleComponentPrimaryPropertiesProps(
vehicle: VehicleManager
): GD_VehicleBlockPrimaryComponentProps | null {
const primaryComponentManageType = vehicle.getPrimaryComponentManageType();
if (
primaryComponentManageType ===
Constants.VehicleConfigurationPrimaryComponentManageTypeEnum.VEHICLE
) {
return generateVehiclePrimaryComponentProps(vehicle);
}
return null;
}
/**
*
* @param vehicle
*/
export function deriveVehicleBlockPrimaryHitPointInfoProps(
vehicle: Vehicle
): VehicleComponentHitPointInfoProps | null {
const primaryManageType = VehicleUtils.getConfigurationValue(
Constants.VehicleConfigurationKeyEnum.PRIMARY_COMPONENT_MANAGE_TYPE,
vehicle
);
if (
primaryManageType ===
Constants.VehicleConfigurationPrimaryComponentManageTypeEnum.COMPONENT
) {
return null;
}
return deriveVehicleComponentHitPointInfoProps(
VehicleUtils.getPrimaryComponent(vehicle),
vehicle
);
}
/**
*
* @param component
* @param vehicle
*/
export function deriveVehicleBlockComponentHitPointInfoProps(
component: VehicleComponent,
vehicle: Vehicle
): VehicleComponentHitPointInfoProps | null {
const isPrimaryComponent = VehicleComponentUtils.getIsPrimary(component);
const primaryManageType = VehicleUtils.getConfigurationValue(
Constants.VehicleConfigurationKeyEnum.PRIMARY_COMPONENT_MANAGE_TYPE,
vehicle
);
if (
isPrimaryComponent &&
primaryManageType ===
Constants.VehicleConfigurationPrimaryComponentManageTypeEnum.VEHICLE
) {
return null;
}
const enableComponentHitPoints = VehicleUtils.getConfigurationValue(
Constants.VehicleConfigurationKeyEnum.ENABLE_COMPONENT_HIT_POINTS,
vehicle
);
if (!enableComponentHitPoints) {
return null;
}
return deriveVehicleComponentHitPointInfoProps(component, vehicle);
}
/**
*
* @param component
* @param vehicle
*/
export function deriveVehicleComponentHitPointInfoProps(
component: VehicleComponent,
vehicle: Vehicle
): VehicleComponentHitPointInfoProps {
let damageThreshold: number | null = null;
if (
VehicleUtils.getConfigurationValue(
Constants.VehicleConfigurationKeyEnum.ENABLE_COMPONENT_DAMAGE_THRESHOLD,
vehicle
)
) {
damageThreshold = VehicleComponentUtils.getDamageThreshold(component);
}
let mishapThreshold: number | null = null;
if (
VehicleUtils.getConfigurationValue(
Constants.VehicleConfigurationKeyEnum.ENABLE_COMPONENT_MISHAP_THRESHOLD,
vehicle
)
) {
mishapThreshold = VehicleComponentUtils.getMishapThreshold(component);
}
let componentHitPointInfo = VehicleComponentUtils.getHitPointInfo(component);
const definitionHitPoints = VehicleComponentUtils.getHitPoints(component);
return {
remainingHp:
componentHitPointInfo !== null
? componentHitPointInfo.remainingHp
: definitionHitPoints,
tempHp:
componentHitPointInfo !== null ? componentHitPointInfo.tempHp : null,
totalHp:
componentHitPointInfo !== null
? componentHitPointInfo.totalHp
: definitionHitPoints,
removedHp:
componentHitPointInfo !== null ? componentHitPointInfo.removedHp : 0,
hitPointSpeedAdjustments:
VehicleComponentUtils.getHitPointSpeedAdjustments(component),
damageThreshold,
mishapThreshold,
};
}
/**
*
* @param vehicle
* @param ruleData
*/
export function deriveVehicleBlockActionsSummariesProps(
vehicle: Vehicle,
ruleData: RuleData
): Array<VehicleActionSummaryProps> {
return VehicleUtils.getActionSummaries(vehicle).map((action) => {
let sourceInfo: SourceData | null = null;
if (action.sourceId !== null) {
sourceInfo = RuleDataUtils.getSourceDataInfo(action.sourceId, ruleData);
}
return {
...action,
sourceName: sourceInfo ? sourceInfo.name : null,
sourceFullName: sourceInfo ? sourceInfo.description : null,
};
});
}
/**
*
* @param vehicle
*/
export function deriveFeaturesProps(
vehicle: Vehicle
): Array<VehicleFeatureData> {
const enableFeatures = VehicleUtils.getConfigurationValue(
Constants.VehicleConfigurationKeyEnum.ENABLE_FEATURES,
vehicle
);
return enableFeatures ? VehicleUtils.getFeatures(vehicle) : [];
}
/**
*
* @param activationType
* @param actions
*/
export function deriveVehicleBlockActionsActions(
activationType: Constants.ActivationTypeEnum,
actions: Array<Action>
): Array<VehicleBlockActionProp> {
return actions
.filter((action) =>
ActionUtils.validateIsActivationType(action, activationType)
)
.map(deriveVehicleActionProps);
}
/**
*
* @param action
*/
function deriveVehicleActionProps(action: Action): VehicleBlockActionProp {
return {
name: ActionUtils.getName(action),
description: ActionUtils.getDescription(action),
key: deriveVehicleActionUniqueKey(action),
ammo: ActionUtils.getAmmunition(action),
};
}
function deriveVehicleActionUniqueKey(action: Action): string {
//TODO use ActionUtils.getUniqueKey once vehicle action have real id and entityTypeIds, need this for react keys
return `${ActionUtils.getName(action)}-${ActionUtils.getUniqueKey(action)}`;
}
/**
*
* @param vehicle
* @param ruleData
*/
export function deriveVehicleActionStationsProps(
vehicle: Vehicle,
ruleData: RuleData
): Array<GD_VehicleBlockActionStationProps> {
return VehicleUtils.getActionStations(vehicle).map((station) =>
generateVehicleActionStationProps(station, vehicle, ruleData)
);
}
/**
*
* @param vehicle
* @param ruleData
*/
export function deriveVehicleComponentsProps(
vehicle: Vehicle,
ruleData: RuleData
): Array<GD_VehicleBlockComponentProps> {
return VehicleUtils.getComponents(vehicle).map((component) =>
generateVehicleBlockComponentProps(component, vehicle, ruleData)
);
}
/**
*
* @param component
*/
export function deriveVehicleComponentActionsProps(
component: VehicleComponent
): Array<VehicleBlockActionProp> {
return VehicleComponentUtils.getActions(component).map((action) =>
deriveVehicleActionProps(action)
);
}
/**
*
* @param component
* @param vehicle
* @param ruleData
*/
export function deriveVehicleComponentCoverTypeProp(
component: VehicleComponent,
vehicle: Vehicle,
ruleData: RuleData
): string | null {
const enableComponentCover = VehicleUtils.getConfigurationValue(
Constants.VehicleConfigurationKeyEnum.ENABLE_COMPONENT_COVER,
vehicle
);
if (!enableComponentCover) {
return null;
}
const coverType = VehicleComponentUtils.getCoverType(component);
if (coverType === null) {
return null;
}
const coverTypeLookup = RuleDataUtils.getCoverTypeLookup(ruleData);
const cover = HelperUtils.lookupDataOrFallback(coverTypeLookup, coverType);
return cover ? cover.name : null;
}
/**
*
* @param component
* @param vehicle
*/
export function deriveVehicleComponentRequiredCrew(
component: VehicleComponent,
vehicle: Vehicle
): number | null {
const enableRequiredCrew = VehicleUtils.getConfigurationValue(
Constants.VehicleConfigurationKeyEnum.ENABLE_COMPONENT_CREW_REQUIREMENTS,
vehicle
);
return enableRequiredCrew
? VehicleComponentUtils.getRequiredCrew(component)
: null;
}
/**
* TODO move this one?
* @param component
* @param vehicle
*/
function allowComponentProperty(
component: VehicleComponent,
vehicle: Vehicle
): boolean {
const primaryManageType = VehicleUtils.getConfigurationValue(
Constants.VehicleConfigurationKeyEnum.PRIMARY_COMPONENT_MANAGE_TYPE,
vehicle
);
const isPrimaryComponent = VehicleComponentUtils.getIsPrimary(component);
return !(
primaryManageType ===
Constants.VehicleConfigurationPrimaryComponentManageTypeEnum.VEHICLE &&
isPrimaryComponent
);
}
/**
*
* @param component
* @param vehicle
*/
export function deriveVehicleComponentArmorClassInfo(
component: VehicleComponent,
vehicle: Vehicle
): VehicleComponentArmorClassInfo | null {
const enableArmorClass = VehicleUtils.getConfigurationValue(
Constants.VehicleConfigurationKeyEnum.ENABLE_COMPONENT_ARMOR_CLASS,
vehicle
);
if (enableArmorClass && allowComponentProperty(component, vehicle)) {
return VehicleComponentUtils.getArmorClassInfo(component);
}
return null;
}
/**
*
* @param component
* @param vehicle
*/
export function deriveVehicleComponentSpeedInfos(
component: VehicleComponent,
vehicle: Vehicle
): Array<VehicleComponentSpeedInfo> {
const enableSpeeds = VehicleUtils.getConfigurationValue(
Constants.VehicleConfigurationKeyEnum.ENABLE_COMPONENT_SPEEDS,
vehicle
);
if (enableSpeeds && allowComponentProperty(component, vehicle)) {
return VehicleComponentUtils.getSpeedInfos(component);
}
return [];
}
/**
* Set up this function to derive more possibilities of component management
* @param component
* @param vehicle
*/
export function deriveEnableComponentManagement(
component: VehicleComponent,
vehicle: Vehicle
): boolean {
if (
deriveVehicleBlockComponentHitPointInfoProps(component, vehicle) !== null
) {
return true;
}
return false;
}
/**
* preserving how to derive ActionInfoProps for when we can get rid of dangerouslySetHtml in Vehicle Action.descriptions
* @param action
*/
export function deriveVehicleComponentActionInfo(
action: Action
): VehicleComponentActionInfoProp {
const actionTypeId = ActionUtils.getActionTypeId(action);
const toHit = ActionUtils.getToHit(action);
const rangeInfo = ActionUtils.getRange(action);
const numOfTargets = ActionUtils.getNumberOfTargets(action);
const attackRangeTypeId = ActionUtils.getAttackRangeId(action);
let actionTypeDisplayText: Array<string> = [];
if (attackRangeTypeId === Constants.AttackTypeRangeEnum.RANGED) {
actionTypeDisplayText.push("Ranged");
} else if (attackRangeTypeId === Constants.AttackTypeRangeEnum.MELEE) {
actionTypeDisplayText.push("Melee");
}
if (actionTypeId === Constants.ActionTypeEnum.WEAPON) {
actionTypeDisplayText.push("Weapon");
} else if (actionTypeId === Constants.ActionTypeEnum.SPELL) {
actionTypeDisplayText.push("Spell");
}
if (actionTypeId !== null) {
actionTypeDisplayText.push("Attack");
} else {
actionTypeDisplayText.push("Action");
}
let actionDescriptionDisplay: Array<string> = [];
if (toHit !== null) {
actionDescriptionDisplay.push(
`${FormatUtils.renderSignedNumber(toHit)} to hit`
);
}
if (rangeInfo) {
const { minimumRange, range, longRange } = rangeInfo;
let rangeText: Array<string> = [];
let rangeString = `range ${range}`;
if (longRange !== null) {
rangeString = `range ${range}/${FormatUtils.renderDistance(longRange)}`;
}
rangeText.push(rangeString);
if (minimumRange !== null) {
rangeText.push(
`(can't hit targets within ${FormatUtils.renderDistance(
minimumRange
)} of it)`
);
}
actionDescriptionDisplay.push(rangeText.join(" "));
}
if (numOfTargets) {
let numOfTargetText: string =
numOfTargets === 1 ? "one target" : `${numOfTargets} targets`;
actionDescriptionDisplay.push(numOfTargetText);
}
return {
label: `${actionTypeDisplayText.join(" ")}`,
snippet: actionDescriptionDisplay.length
? `${actionDescriptionDisplay.join(", ")}`
: null,
};
}
/**
* preserving how to derive DamageInfoProps for when we can get rid of dangerouslySetHtml in Vehicle Action.descriptions
* @param action
*/
export function deriveVehicleComponentActionDamageInfo(
action: Action
): VehicleComponentActionInfoProp {
const diceInfo = ActionUtils.getDice(action);
let avgDiceValue: number | null = null;
if (diceInfo !== null) {
avgDiceValue = DiceUtils.getAverageDiceValue(diceInfo);
}
const damageType = ActionUtils.getDamage(action).type;
let snippet: Array<string | number> = [];
if (avgDiceValue !== null) {
snippet.push(avgDiceValue);
}
if (diceInfo !== null) {
snippet.push(`(${DiceUtils.renderDie(diceInfo)})`);
}
if (damageType !== null && damageType.name !== null) {
snippet.push(`${damageType.name.toLowerCase()} damage`);
}
return {
label: "Hit",
snippet: snippet.length ? `${snippet.join(" ")}` : null,
};
}
@@ -0,0 +1,284 @@
import {
Constants,
RuleData,
Vehicle,
VehicleComponent,
VehicleComponentUtils,
VehicleManager,
VehicleUtils,
} from "@dndbeyond/character-rules-engine/es";
import {
deriveEnableComponentManagement,
deriveVehicleBlockActionsActions,
deriveVehicleBlockComponentHitPointInfoProps,
deriveVehicleComponentActionsProps,
deriveVehicleComponentArmorClassInfo,
deriveVehicleComponentCoverTypeProp,
deriveVehicleComponentPrimaryPropertiesProps,
deriveVehicleComponentRequiredCrew,
deriveVehicleComponentSpeedInfos,
} from "./derivers";
import {
GD_VehicleBlockActionsProps,
GD_VehicleBlockActionStationListProps,
GD_VehicleBlockActionStationProps,
GD_VehicleBlockActionSummariesProps,
GD_VehicleBlockComponentListProps,
GD_VehicleBlockComponentProps,
GD_VehicleBlockFeaturesProps,
GD_VehicleBlockHeaderProps,
GD_VehicleBlockPrimaryComponentProps,
GD_VehicleBlockPrimaryProps,
GD_VehicleBlockProps,
GD_VehicleBlockShellProps,
} from "./typings";
/**
*
* @param vehicle
* @param ruleData
*/
export function generateVehicleBlockProps(
vehicle: VehicleManager
): GD_VehicleBlockProps {
return {
shellProps: generateVehicleShellProps(vehicle),
headerProps: generateVehicleBlockHeaderProps(vehicle),
primaryProps: generateVehicleBlockPrimaryProps(vehicle),
actionSummariesProps: vehicle.generateVehicleActionsSummaries(),
featuresProps: generateVehicleFeaturesProps(vehicle),
actionStationsProps: generateVehicleBlockActionStationListProps(vehicle),
componentsProps: generateVehicleBlockComponentListProps(vehicle),
actionsProps: vehicle.generateVehicleBlockActionsInfo(),
};
}
/**
*
* @param vehicle
*/
export function generateVehicleShellProps(
vehicle: VehicleManager
): GD_VehicleBlockShellProps {
return {
displayType: vehicle.getDisplayType(),
};
}
/**
*
* @param vehicle
* @param ruleData
*/
export function generateVehicleBlockHeaderProps(
vehicle: VehicleManager
): GD_VehicleBlockHeaderProps {
return {
name: vehicle.getName(),
typeName: vehicle.getVehicleTypeName(),
displayType: vehicle.getDisplayType(),
weight: vehicle.getWeight(),
length: vehicle.getLength(),
width: vehicle.getWidth(),
sizeName: vehicle.getVehicleSizeName(),
};
}
/**
*
* @param vehicle
*/
export function generateVehicleBlockPrimaryProps(
vehicle: VehicleManager
): GD_VehicleBlockPrimaryProps {
return {
cargoCapacityInfo: vehicle.getVehicleCargoCapacityInfo(),
conditionImmunities: vehicle.generateVehicleConditionImmunityNames(),
creatureCapacityDescriptions: vehicle.getCreatureCapacityDescriptions(),
damageImmunities: vehicle.generateVehicleDamageImmunityNames(),
displayType: vehicle.getDisplayType(),
primaryProperties: deriveVehicleComponentPrimaryPropertiesProps(vehicle),
stats: vehicle.getStats(),
travelPaceInfo: vehicle.generateVehicleComponentTravelPaceInfo(),
};
}
/**
*
* @param vehicle
* @param ruleData
*/
export function generateVehicleBlockActionSummariesProps(
vehicle: VehicleManager,
ruleData: RuleData
): GD_VehicleBlockActionSummariesProps {
return {
actionsText: vehicle.getActionsText(),
actionsSummaries: vehicle.generateVehicleBlockActionsSummaries(),
};
}
/**
*
* @param vehicle
*/
export function generateVehicleFeaturesProps(
vehicle: VehicleManager
): GD_VehicleBlockFeaturesProps {
return {
features: vehicle.generateFeatures(),
};
}
/**
*
* @param vehicle
* @param ruleData
*/
export function generateVehicleBlockActionStationListProps(
vehicle: VehicleManager
): GD_VehicleBlockActionStationListProps {
const enableActionStations = vehicle.getEnableActionStations();
return {
actionStations: enableActionStations
? vehicle.generateVehicleActionStationsInfo()
: [],
};
}
/**
*
* @param station
* @param vehicle
* @param ruleData
*/
export function generateVehicleActionStationProps(
station: VehicleComponent,
vehicle: Vehicle,
ruleData: RuleData
): GD_VehicleBlockActionStationProps {
//share componentProps for now.
return generateVehicleBlockComponentProps(station, vehicle, ruleData);
}
/**
*
* @param vehicle
*/
export function generateVehiclePrimaryComponentProps(
vehicle: VehicleManager
): GD_VehicleBlockPrimaryComponentProps | null {
const primaryComponent = vehicle.getPrimaryComponent();
if (primaryComponent === null) {
return null;
}
return {
armorClassInfo: primaryComponent.getArmorClassInfo(),
hitPointInfo: vehicle.generateVehicleBlockPrimaryHitPointInfo(),
speedInfos: primaryComponent.getSpeedInfos(),
costInfos: primaryComponent.getCosts(),
displayType: vehicle.getDisplayType(),
width: vehicle.getWidth(),
length: vehicle.getLength(),
isPrimaryComponent: primaryComponent.getIsPrimary(),
};
}
/**
*
* @param vehicle
*/
export function generateVehicleBlockActionsProps(
vehicle: VehicleManager
): GD_VehicleBlockActionsProps {
const allActions = vehicle.getAllActions();
return {
reactions: deriveVehicleBlockActionsActions(
Constants.ActivationTypeEnum.REACTION,
allActions
),
bonusActions: deriveVehicleBlockActionsActions(
Constants.ActivationTypeEnum.BONUS_ACTION,
allActions
),
special: deriveVehicleBlockActionsActions(
Constants.ActivationTypeEnum.SPECIAL,
allActions
),
};
}
/**
*
* @param vehicle
* @param ruleData
*/
export function generateVehicleBlockComponentListProps(
vehicle: VehicleManager
): GD_VehicleBlockComponentListProps {
const enableComponents = vehicle.getEnableComponents();
return {
components: enableComponents
? vehicle.generateVehicleComponentsBlockInfo()
: [],
};
}
/**
*
* @param component
* @param vehicle
* @param ruleData
*/
export function generateVehicleBlockComponentProps(
component: VehicleComponent,
vehicle: Vehicle,
ruleData: RuleData
): GD_VehicleBlockComponentProps {
const uniqueKey = VehicleComponentUtils.getUniqueKey(component);
return {
actions: deriveVehicleComponentActionsProps(component),
count: 1,
description: VehicleComponentUtils.getDescription(component),
displayOrder: VehicleComponentUtils.getDisplayOrder(component),
isPrimaryComponent: VehicleComponentUtils.getIsPrimary(component),
isRemovable: VehicleComponentUtils.getIsRemovable(component),
name: VehicleComponentUtils.getName(component),
typeNames: VehicleComponentUtils.getTypeNames(component),
coverType: deriveVehicleComponentCoverTypeProp(
component,
vehicle,
ruleData
),
requiredCrew: deriveVehicleComponentRequiredCrew(component, vehicle),
key: uniqueKey,
uniqueKey,
uniquenessFactor: VehicleComponentUtils.getUniquenessFactor(component),
id: VehicleComponentUtils.getMappingId(component),
vehicleId: VehicleUtils.getMappingId(vehicle),
displayType: VehicleUtils.getConfigurationValue(
Constants.VehicleConfigurationKeyEnum.DISPLAY_TYPE,
vehicle
),
armorClassInfo: deriveVehicleComponentArmorClassInfo(component, vehicle),
speedInfos: deriveVehicleComponentSpeedInfos(component, vehicle),
hitPointInfo: deriveVehicleBlockComponentHitPointInfoProps(
component,
vehicle
),
enableComponentManagement: deriveEnableComponentManagement(
component,
vehicle
),
costInfos: VehicleComponentUtils.getCosts(component),
width: VehicleUtils.getWidth(vehicle),
length: VehicleUtils.getLength(vehicle),
};
}
@@ -0,0 +1 @@
export let NO_ERROR_CODE = "NO_ERROR_CODE";
@@ -0,0 +1,64 @@
import { LogMessageType } from "@dndbeyond/character-rules-engine/es/logger/constants";
import { ErrorClientConfig, ErrorCustomTags } from "./typings";
// Historical note: This logging util used to be more fully featured,
// when we integrated with Sentry.io. Now it just logs to the
// console if debug is enabled.
let clientConfig: ErrorClientConfig | null = null;
/**
* Logs an exception when debug is enabled in the client config
* @param error The error object
* @param contextData Any context data related to the exception
* @param customTags
*/
export function dispatchException(
error: Error,
contextData: Record<string, any> | null = null,
customTags: ErrorCustomTags | null = null
): void {
if (clientConfig?.debug) {
// eslint-disable-next-line no-console
console.error("EXCEPTION", error, contextData, customTags);
}
}
/**
* Logs a message when debug is enabled in the client config
* @param message The message to send
* @param contextData Any context data related to the message
* @param customTags
* @param severity
*/
export function dispatchError(
message: string,
contextData: Record<string, any> | null,
customTags: ErrorCustomTags | null,
severity: LogMessageType
): void {
if (clientConfig?.debug) {
// eslint-disable-next-line no-console
console.error(severity, message, contextData, customTags);
}
}
/**
* Initializes client error config
* @param config config object
*/
export function initReporting(config: ErrorClientConfig): void {
clientConfig = config;
}
/**
* Reports a React error to logger
* @param error The error to report
* @param componentInfo Information about the component in which the error occurred
*/
export function reportReactError(error: Error, componentInfo: any): void {
dispatchException(error, {
componentInfo,
});
}
@@ -0,0 +1,105 @@
/**
*
* @param input
* @param startIdx
* @param pieceIdx
* @param pieceCharIdx
* @param pieces
*/
export function checkQueryPiece(
input: string,
startIdx: number,
pieceIdx: number,
pieceCharIdx: number,
pieces: Array<string>
): boolean {
let i: number;
let ch: string;
let length: number = input.length;
for (i = startIdx; i < length; i++) {
ch = input.charAt(i);
switch (ch) {
case " ":
case "-":
case ".":
case '"':
continue; //ignore spaces, dashes, periods, and double quotes
default:
if (pieceIdx < pieces.length) {
let piece = pieces[pieceIdx];
if (
pieceCharIdx < piece.length &&
ch === piece.charAt(pieceCharIdx)
) {
//if current character matches, check next character in piece
if (
checkQueryPiece(input, i + 1, pieceIdx, pieceCharIdx + 1, pieces)
) {
return true;
}
//if path for next character doesn't match, try path from start of next piece
if (checkQueryPiece(input, i + 1, pieceIdx + 1, 0, pieces)) {
return true;
}
}
if (pieceCharIdx === 0) {
//if first character of piece doesn't match, allow skipping that piece
if (checkQueryPiece(input, i, pieceIdx + 1, 0, pieces)) {
return true;
}
}
}
return false; //no match if no path above reaches end of input string
}
}
return true; //match if reached end of input string
}
/**
*
* @param query
* @param primaryText
* @param tags
*/
export function doesQueryMatchData(
query: string,
primaryText: string | null,
tags: Array<string> = []
): boolean {
let input: string = query.toLowerCase().trim();
if (!input) {
// if there is no query don't filter anything
return true;
}
let processedPrimaryText: string = primaryText
? primaryText.replace(/["+]/g, "").toLowerCase().trim()
: "";
let processedTags: Array<string> =
tags && tags.length
? tags.map((tag) => (tag ? tag.toLowerCase().trim() : tag))
: [];
let pieces: Array<string> = processedPrimaryText.split(/[ -]/);
if (checkQueryPiece(input, 0, 0, 0, pieces)) {
//don't filter out item if heading matches
return true;
}
// if heading isn't a match, see if input is an exact match for associated tags
if (!processedTags.length) {
// filter out item if no tags specified
return false;
}
let tagMatch: boolean = false;
processedTags.forEach((tag) => {
if (tagMatch || !tag) {
return;
}
let pieces: Array<string> = tag.split(/[ -]/);
tagMatch = checkQueryPiece(input, 0, 0, 0, pieces);
});
return tagMatch;
}
@@ -0,0 +1,13 @@
import * as messageTypes from "./messageTypes";
import { ShowCharacterSheetMessage } from "./typings";
export function createShowCharacterSheetMessage(
characterId: number
): ShowCharacterSheetMessage {
return {
type: messageTypes.SHOW_CHARACTER_SHEET,
payload: {
characterId,
},
};
}
@@ -0,0 +1 @@
export const SHOW_CHARACTER_SHEET = "show-character-sheet";
@@ -0,0 +1,19 @@
import { MobileMessage } from "./typings";
/**
*
* @param message
*/
export function sendMessage(message: MobileMessage): void {
if ((window as any)?.webkit?.messageHandlers?.mobileApp?.postMessage) {
// This should not be stringify'd because iOS is fine with objects
// https://github.com/DnDBeyond/ddb-character-tools-client/pull/306
(window as any).webkit.messageHandlers.mobileApp.postMessage(message);
return;
}
if ((window as any)?.mobileApp?.postMessage) {
(window as any).mobileApp.postMessage(JSON.stringify(message));
return;
}
}
@@ -0,0 +1,384 @@
import {
AbilityManager,
Constants,
} from "@dndbeyond/character-rules-engine/es";
import {
PaneIdentifiersAction,
PaneIdentifiersClassSpell,
PaneIdentifiersNote,
PaneIdentifiersClassFeature,
PaneIdentifiersRacialTrait,
PaneIdentifiersItem,
PaneIdentifiersBasicAction,
PaneIdentifiersCharacterSpell,
PaneIdentifiersFeat,
PaneIdentifiersCustomAction,
PaneIdentifiersCustomItem,
PaneIdentifiersCustomSkill,
PaneIdentifiersAbility,
PaneIdentifiersSkill,
PaneIdentifiersAbilitySavingThrow,
PaneIdentifiersPreferenceHitPointConfirm,
PaneIdentifiersPreferenceProgressionConfirm,
PaneIdentifiersCreature,
PaneIdentifiersVehicle,
PaneIdentifiersVehicleComponent,
PaneIdentifiersVehicleActionStation,
PaneIdentifiersInfusionChoice,
PaneIdentifiersPreferenceOptionalClassFeaturesConfirm,
PaneIdentifiersPreferenceOptionalOriginsConfirm,
PaneIdentifiersContainer,
PaneIdentifiersCurrencyContext,
PaneIdentifiersBlessing,
PaneIdentifiersTrait,
} from "~/subApps/sheet/components/Sidebar/types";
//TODO move this to subapps Sidebar
/**
*
* @param mappingId
* @param mappingEntityTypeId
*/
export function generateAction(
mappingId: string,
mappingEntityTypeId: string | null
): PaneIdentifiersAction {
return {
id: mappingId,
entityTypeId: mappingEntityTypeId,
};
}
/**
*
* @param id
*/
export function generateBasicAction(id: number): PaneIdentifiersBasicAction {
return {
id,
};
}
/**
*
* @param mappingId
*/
export function generateItem(mappingId: number): PaneIdentifiersItem {
return {
id: mappingId,
};
}
/**
*
* @param mappingId
*/
export function generateCharacterSpell(
mappingId: number,
castLevel?: number
): PaneIdentifiersCharacterSpell {
return {
id: mappingId,
castLevel,
};
}
/**
*
* @param classMappingId
* @param spellMappingId
*/
export function generateClassSpell(
classMappingId: number,
spellMappingId: number,
castLevel?: number
): PaneIdentifiersClassSpell {
return {
classId: classMappingId,
spellId: spellMappingId,
castLevel,
};
}
/**
*
* @param noteType
*/
export function generateNote(noteType: string): PaneIdentifiersNote {
return {
noteType,
};
}
/**
*
* @param classFeatureId
* @param classMappingId
*/
export function generateClassFeature(
classFeatureId: number,
classMappingId: number
): PaneIdentifiersClassFeature {
return {
classMappingId,
id: classFeatureId,
};
}
/**
*
* @param racialTraitId
*/
export function generateRacialTrait(
racialTraitId: number
): PaneIdentifiersRacialTrait {
return {
id: racialTraitId,
};
}
/**
*
* @param id
*/
export function generateFeat(id: number): PaneIdentifiersFeat {
return {
id,
};
}
/**
*
* @param id
*/
export function generateBlessing(id: string): PaneIdentifiersBlessing {
return {
id,
};
}
/**
*
* @param id
*/
export function generateCustomAction(id: string): PaneIdentifiersCustomAction {
return {
id,
};
}
/**
*
* @param type
*/
export function generateTrait(
type: Constants.TraitTypeEnum
): PaneIdentifiersTrait {
return {
type,
};
}
/**
*
* @param mappingId
*/
export function generateCustomItem(
mappingId: number
): PaneIdentifiersCustomItem {
return {
id: mappingId,
};
}
/**
*
* @param mappingId
*/
export function generateCustomSkill(
mappingId: number
): PaneIdentifiersCustomSkill {
return {
id: mappingId,
};
}
/**
*
* @param id
*/
export function generateAbility(
ability: AbilityManager
): PaneIdentifiersAbility {
return {
ability,
};
}
/**
*
* @param id
*/
export function generateSkill(id: number): PaneIdentifiersSkill {
return {
id,
};
}
/**
*
* @param abilityId
*/
export function generateAbilitySavingThrows(
abilityId: number
): PaneIdentifiersAbilitySavingThrow {
return {
id: abilityId,
};
}
/**
*
* @param hitPointType
*/
export function generatePreferenceHitPointConfirm(
hitPointType: number
): PaneIdentifiersPreferenceHitPointConfirm {
return {
id: hitPointType,
};
}
/**
*
* @param progressionType
*/
export function generatePreferenceProgressionConfirm(
progressionType: number
): PaneIdentifiersPreferenceProgressionConfirm {
return {
id: progressionType,
};
}
/**
*
* @param spellListIds
* @param newIsEnabled
*/
export function generatePreferenceOptionalClassFeaturesConfirm(
spellListIds: Array<number>,
newIsEnabled: boolean
): PaneIdentifiersPreferenceOptionalClassFeaturesConfirm {
return {
spellListIds,
newIsEnabled,
};
}
/**
*
* @param spellListIds
* @param newIsEnabled
*/
export function generatePreferenceOptionalOriginsConfirm(
spellListIds: Array<number>,
newIsEnabled: boolean
): PaneIdentifiersPreferenceOptionalOriginsConfirm {
return {
spellListIds,
newIsEnabled,
};
}
/**
*
* @param creatureMappingId
*/
export function generateCreature(
creatureMappingId: number
): PaneIdentifiersCreature {
return {
id: creatureMappingId,
};
}
/**
*
* @param vehicleMappingId
*/
export function generateVehicle(vehicleMappingId): PaneIdentifiersVehicle {
return {
id: vehicleMappingId,
};
}
/**
*
* @param componentId
* @param vehicleId
*/
export function generateVehicleComponent(
componentId: number,
vehicleId: number
): PaneIdentifiersVehicleComponent {
return {
vehicleId,
id: componentId,
};
}
/**
*
* @param stationId
* @param vehicleId
*/
export function generateVehicleActionStation(
stationId: number,
vehicleId: number
): PaneIdentifiersVehicleActionStation {
return {
vehicleId,
id: stationId,
};
}
/**
*
* @param id
*/
export function generateInfusionChoice(
id: string
): PaneIdentifiersInfusionChoice {
return {
id,
};
}
/**
*
* @param containerDefinitionKey
*/
export function generateContainer(
containerDefinitionKey: string,
showAddItems?: boolean
): PaneIdentifiersContainer {
return {
containerDefinitionKey,
showAddItems,
};
}
/**
*
* @param containerDefinitionKeyContext
*/
export function generateCurrencyContext(
containerDefinitionKeyContext: string | null
): PaneIdentifiersCurrencyContext {
return {
containerDefinitionKeyContext,
};
}
@@ -0,0 +1,24 @@
import { RollParentKeyInfoLookup, RollResultGroupsLookup } from "./typings";
/**
*
* @param rollResultGroupsLookup
*/
export function generateRollParentKeyInfoLookup(
rollResultGroupsLookup: RollResultGroupsLookup
): RollParentKeyInfoLookup {
let lookup: RollParentKeyInfoLookup = {};
Object.keys(rollResultGroupsLookup).forEach((componentKey) => {
rollResultGroupsLookup[componentKey].forEach((rollGroupContract) => {
rollGroupContract.rollResults.forEach((rollResult) => {
lookup[rollResult.rollKey] = {
componentKey,
groupKey: rollGroupContract.groupKey,
};
});
});
});
return lookup;
}
@@ -0,0 +1,278 @@
import { groupBy, keyBy } from "lodash";
import {
HelperUtils,
RollGroupContract,
RollResultContract,
} from "@dndbeyond/character-rules-engine/es";
import { RollResultGroupsLookup } from "./typings";
/**
*
* @param componentKey
* @param rollResultGroupsLookup
*/
export function getGroupsByComponentKey(
componentKey: string,
rollResultGroupsLookup: RollResultGroupsLookup
): Array<RollGroupContract> {
return HelperUtils.lookupDataOrFallback(
rollResultGroupsLookup,
componentKey,
[]
);
}
/**
*
* @param componentKey
* @param rollResultGroupsLookup
*/
export function getComponentOrderedGroups(
componentKey: string,
rollResultGroupsLookup: RollResultGroupsLookup
): Array<RollGroupContract> {
return getOrderedLinkedEntries(
getGroupsByComponentKey(componentKey, rollResultGroupsLookup)
);
}
/**
*
* @param rollResults
*/
export function getGroupOrderedRollResults(
rollResults: Array<RollResultContract>
): Array<RollResultContract> {
return getOrderedLinkedEntries(rollResults);
}
/**
*
* @param linkedEntries
*/
export function getOrderedLinkedEntries<T extends RollGroupContract>(
linkedEntries: Array<T>
): Array<T>;
export function getOrderedLinkedEntries<T extends RollResultContract>(
linkedEntries: Array<T>
): Array<T>;
export function getOrderedLinkedEntries<T extends any>(
linkedEntries: Array<T>
) {
// @ts-ignore
return helper__getOrderedLinkedEntries(
linkedEntries,
getLinkedEntryKey as (entry: T) => string | null,
getLinkedEntryNextKey as (entry: T) => string | null
);
}
//probs in HelperUtils
export function helper__getOrderedLinkedEntries<T extends any>(
linkedEntries: Array<T>,
getLinkedEntryKey: (entry: T) => string | null,
getLinkedEntryNextKey: (entry: T) => string | null
): Array<T> {
const entryByNextKeyLookup = keyBy(
linkedEntries,
(entry) => getLinkedEntryNextKey(entry) ?? "null"
);
let orderedEntries: Array<T> = [];
let currentEntry: T | null = HelperUtils.lookupDataOrFallback(
entryByNextKeyLookup,
"null"
);
// Ordering fallback if no current entry is found
if (!currentEntry) {
// Find entry by nextKey not being included in list of current keys
const includedKeys = linkedEntries.map(
(entry) => getLinkedEntryKey(entry) ?? "null"
);
currentEntry =
linkedEntries.find(
(entry) =>
!includedKeys.includes(getLinkedEntryNextKey(entry) ?? "null")
) ?? null;
}
let counter: number = 0;
while (counter <= linkedEntries.length && currentEntry) {
orderedEntries.push(currentEntry);
const nextEntryKey = getLinkedEntryKey(currentEntry);
currentEntry = null;
if (nextEntryKey) {
const nextEntry = HelperUtils.lookupDataOrFallback(
entryByNextKeyLookup,
nextEntryKey
);
if (nextEntry) {
currentEntry = nextEntry;
}
}
counter++;
}
return orderedEntries.reverse();
}
/**
*
* @param linkedEntry
*/
export function getLinkedEntryNextKey(
linkedEntry: RollResultContract | RollGroupContract
): string | null {
if (isLinkedEntryRollGroupContract(linkedEntry)) {
return linkedEntry.nextGroupKey;
} else if (isLinkedEntryRollResultContract(linkedEntry)) {
return linkedEntry.nextRollKey;
} else {
return null;
}
}
/**
*
* @param linkedEntry
*/
export function getLinkedEntryKey(
linkedEntry: RollResultContract | RollGroupContract
): string | null {
if (isLinkedEntryRollGroupContract(linkedEntry)) {
return linkedEntry.groupKey;
} else if (isLinkedEntryRollResultContract(linkedEntry)) {
return linkedEntry.rollKey;
} else {
return null;
}
}
/**
*
* @param linkedEntry
*/
export function isLinkedEntryRollGroupContract(
linkedEntry: RollResultContract | RollGroupContract
): linkedEntry is RollGroupContract {
return (linkedEntry as RollGroupContract).groupKey !== undefined;
}
/**
*
* @param linkedEntry
*/
export function isLinkedEntryRollResultContract(
linkedEntry: RollResultContract | RollGroupContract
): linkedEntry is RollResultContract {
return (linkedEntry as RollResultContract).rollKey !== undefined;
}
/**
*
* @param componentKey
* @param rollResultGroupsLookup
*/
export function generateComponentOrderedGroups(
componentKey: string,
rollResultGroupsLookup: RollResultGroupsLookup
): Array<RollGroupContract> {
const groups = getGroupsByComponentKey(componentKey, rollResultGroupsLookup);
const groupByGroupKeyLookup = keyBy(groups, (group) => group.groupKey);
const nextGroupKeyGroupsLookup = groupBy(
groups,
(group) => group.nextGroupKey
);
let orderedGroups: Array<RollGroupContract> = [];
//process any groups with nextGroupKey pointing to non-existent group
groups.forEach((group) => {
if (group.nextGroupKey) {
const nextGroup = HelperUtils.lookupDataOrFallback(
groupByGroupKeyLookup,
group.nextGroupKey
);
if (!nextGroup) {
orderedGroups.push(
...getOrderedGroupsByEndKey(
group.nextGroupKey,
groups,
groupByGroupKeyLookup,
nextGroupKeyGroupsLookup
)
);
}
}
});
//process any groups with null nextGroupKey
const nullTierGroups = getOrderedGroupsByEndKey(
"null",
groups,
groupByGroupKeyLookup,
nextGroupKeyGroupsLookup
);
return [...orderedGroups, ...nullTierGroups];
}
/**
*
* @param endKey
* @param groups
* @param groupByGroupKeyLookup
* @param nextGroupKeyGroupsLookup
*/
function getOrderedGroupsByEndKey(
endKey: string,
groups: Array<RollGroupContract>,
groupByGroupKeyLookup: Record<
RollGroupContract["groupKey"],
RollGroupContract
>,
nextGroupKeyGroupsLookup: Record<string, Array<RollGroupContract>>
): Array<RollGroupContract> {
const orderedGroups: Array<RollGroupContract> = [];
let currentTierGroups: Array<RollGroupContract> = [];
const endTierGroups = HelperUtils.lookupDataOrFallback(
nextGroupKeyGroupsLookup,
endKey
);
if (endTierGroups) {
currentTierGroups.push(...endTierGroups);
}
//find each tier of groups by nextGroupKey
let counter: number = 0;
while (
counter <= groups.length &&
currentTierGroups &&
currentTierGroups.length > 0
) {
const nextTierGroups: Array<RollGroupContract> = [];
currentTierGroups.forEach((group) => {
const foundGroups = HelperUtils.lookupDataOrFallback(
nextGroupKeyGroupsLookup,
group.groupKey
);
if (foundGroups) {
nextTierGroups.push(...foundGroups);
}
});
orderedGroups.push(...currentTierGroups);
currentTierGroups = nextTierGroups;
counter++;
}
return orderedGroups.reverse();
}
@@ -0,0 +1,17 @@
import { AxiosError } from "axios";
/**
* https://github.com/Microsoft/TypeScript/issues/16069#issuecomment-369374214
* Used to get around array filter function that errors because it doesn't recognize the filter
* is removing nulls
* @param input
*/
export function isNotNullOrUndefined<T extends Object>(
input: null | undefined | T
): input is T {
return input !== null;
}
export function isAxiosError(error: Error): error is AxiosError {
return (error as AxiosError).response !== undefined;
}
@@ -0,0 +1,8 @@
import {
CampaignDataContract,
CampaignUtils,
} from "@dndbeyond/character-rules-engine/es";
export function getLaunchGameUrl(campaign: CampaignDataContract): string {
return `/games/${CampaignUtils.getId(campaign)}`;
}