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:
@@ -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)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user