``` ~/go/bin/sourcemapper -output ddb -jsurl https://media.dndbeyond.com/character-app/static/js/main.90aa78c5.js ```
918 lines
30 KiB
JavaScript
918 lines
30 KiB
JavaScript
import { isFinite, toNumber } from 'lodash';
|
|
import { TypeScriptUtils } from '../../utils';
|
|
import { AbilityAccessors } from '../Ability';
|
|
import { CharacterDerivers } from '../Character';
|
|
import { DiceRenderers } from '../Dice';
|
|
import { FormatUtils } from '../Format';
|
|
import { LimitedUseDerivers } from '../LimitedUse';
|
|
import { SpellDerivers } from '../Spell';
|
|
import { SnippetAbilityKeyEnum, SnippetContentChunkTypeEnum, SnippetMathOperatorEnum, SnippetPostProcessTypeEnum, SnippetSymbolEnum, SnippetTagDataTypeEnum, SnippetTagValueTypeEnum, SnippetValueModifierTypeEnum, } from './constants';
|
|
const snippetTagValues = Object.keys(SnippetTagValueTypeEnum)
|
|
.map((tagValueType) => SnippetTagValueTypeEnum[tagValueType])
|
|
.join('|')
|
|
.toLowerCase();
|
|
const groupFragmentRegExpr = new RegExp(['\\(([^\\(\\)]*?)\\)(?:', SnippetSymbolEnum.VALUE_MODIFIER, '([^+*\\-\\/]+)?)?'].join(''));
|
|
const tagValueRegExpr = new RegExp([
|
|
'((?:',
|
|
snippetTagValues,
|
|
')(?:\\',
|
|
SnippetSymbolEnum.PARAMETER,
|
|
'[^',
|
|
SnippetSymbolEnum.VALUE_MODIFIER,
|
|
'+*\\-\\/]+)?)(?:',
|
|
SnippetSymbolEnum.VALUE_MODIFIER,
|
|
'([^+*\\-\\/]+)?)?',
|
|
].join(''));
|
|
/**
|
|
*
|
|
* @param key
|
|
*/
|
|
export function validateAbilityKey(key) {
|
|
if (!isSnippetAbilityKeyEnum(key)) {
|
|
throw new Error(`Invalid Ability Key: ${key}`);
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @param key
|
|
*/
|
|
export function isSnippetAbilityKeyEnum(key) {
|
|
switch (key) {
|
|
case SnippetAbilityKeyEnum.STRENGTH:
|
|
case SnippetAbilityKeyEnum.DEXTERITY:
|
|
case SnippetAbilityKeyEnum.CONSTITUTION:
|
|
case SnippetAbilityKeyEnum.INTELLIGENCE:
|
|
case SnippetAbilityKeyEnum.WISDOM:
|
|
case SnippetAbilityKeyEnum.CHARISMA:
|
|
return true;
|
|
default:
|
|
// not implemented
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
*
|
|
* @param key
|
|
*/
|
|
export function isSnippetValueModifierTypeEnum(key) {
|
|
switch (key) {
|
|
case SnippetValueModifierTypeEnum.MAX:
|
|
case SnippetValueModifierTypeEnum.MIN:
|
|
case SnippetValueModifierTypeEnum.ROUND_DOWN:
|
|
case SnippetValueModifierTypeEnum.ROUND_UP:
|
|
return true;
|
|
default:
|
|
// not implemented
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
*
|
|
* @param key
|
|
*/
|
|
export function isSnippetPostProcessTypeEnum(key) {
|
|
switch (key) {
|
|
case SnippetPostProcessTypeEnum.MAX:
|
|
case SnippetPostProcessTypeEnum.MIN:
|
|
case SnippetPostProcessTypeEnum.ROUND_DOWN:
|
|
case SnippetPostProcessTypeEnum.ROUND_UP:
|
|
case SnippetPostProcessTypeEnum.SIGNED_NUMBER:
|
|
case SnippetPostProcessTypeEnum.UNSIGNED_NUMBER:
|
|
return true;
|
|
default:
|
|
// not implemented
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
*
|
|
* @param key
|
|
*/
|
|
export function isSnippetTagValueTypeEnum(key) {
|
|
switch (key) {
|
|
case SnippetTagValueTypeEnum.SCALE_VALUE:
|
|
case SnippetTagValueTypeEnum.LIMITED_USE:
|
|
case SnippetTagValueTypeEnum.CLASS_LEVEL:
|
|
case SnippetTagValueTypeEnum.ABILITY_SCORE:
|
|
case SnippetTagValueTypeEnum.CHARACTER_LEVEL:
|
|
case SnippetTagValueTypeEnum.MAX_HP:
|
|
case SnippetTagValueTypeEnum.PROFICIENCY:
|
|
case SnippetTagValueTypeEnum.SPELL_ATTACK:
|
|
case SnippetTagValueTypeEnum.SAVE_DC:
|
|
case SnippetTagValueTypeEnum.MODIFIER:
|
|
case SnippetTagValueTypeEnum.FIXED_VALUE:
|
|
return true;
|
|
default:
|
|
// not implemented
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
*
|
|
* @param type
|
|
*/
|
|
export function isSignedTagValueType(type) {
|
|
switch (type) {
|
|
case SnippetTagValueTypeEnum.SPELL_ATTACK:
|
|
case SnippetTagValueTypeEnum.MODIFIER:
|
|
return true;
|
|
default:
|
|
// not implemented
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
*
|
|
* @param contextData
|
|
*/
|
|
export function isLevelScaleFixedValue(contextData) {
|
|
if (!contextData.levelScale) {
|
|
return false;
|
|
}
|
|
return contextData.levelScale.fixedValue !== null && contextData.levelScale.dice === null;
|
|
}
|
|
/**
|
|
*
|
|
* @param rawSnippetTag
|
|
* @param contextData
|
|
*/
|
|
export function isValidStringTagValueType(rawSnippetTag, contextData) {
|
|
const tagValueStringTypesRegExp = new RegExp(['^(', [SnippetTagValueTypeEnum.SCALE_VALUE].join('|').toLowerCase(), ')$'].join(''));
|
|
let match = rawSnippetTag.match(tagValueStringTypesRegExp);
|
|
if (match) {
|
|
switch (match[1]) {
|
|
case SnippetTagValueTypeEnum.SCALE_VALUE:
|
|
return !isLevelScaleFixedValue(contextData);
|
|
default:
|
|
// not implemented
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
*
|
|
* @param type
|
|
*/
|
|
export function isLastPostProcessType(type) {
|
|
switch (type) {
|
|
case SnippetPostProcessTypeEnum.SIGNED_NUMBER:
|
|
case SnippetPostProcessTypeEnum.UNSIGNED_NUMBER:
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
*
|
|
* @param numberString
|
|
*/
|
|
export function isParsableNumber(numberString) {
|
|
return !isNaN(toNumber(numberString)) || numberString === '-' || numberString === '.';
|
|
}
|
|
/**
|
|
*
|
|
* @param expression
|
|
* @param operator
|
|
*/
|
|
export function hasOperator(expression, operator) {
|
|
return expression.indexOf(operator) > -1;
|
|
}
|
|
/**
|
|
*
|
|
* @param str
|
|
*/
|
|
export function throwMismatchedParens(str) {
|
|
let level = 0;
|
|
for (let i = 0; i < str.length; i++) {
|
|
let char = str[i];
|
|
if (char === '(') {
|
|
level += 1;
|
|
}
|
|
if (char === ')') {
|
|
if (level === 0) {
|
|
throw new Error('Mismatching parenthesis');
|
|
}
|
|
level -= 1;
|
|
}
|
|
}
|
|
if (level !== 0) {
|
|
throw new Error('Mismatching parenthesis');
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @param str
|
|
*/
|
|
export function throwInvalidSnippetCharacters(str) {
|
|
const keywordMatches = str.match(/[a-z]+/g);
|
|
keywordMatches === null || keywordMatches === void 0 ? void 0 : keywordMatches.forEach((keyword) => {
|
|
if (isSnippetTagValueTypeEnum(keyword) ||
|
|
isSnippetAbilityKeyEnum(keyword) ||
|
|
isSnippetValueModifierTypeEnum(keyword) ||
|
|
isSnippetPostProcessTypeEnum(keyword)) {
|
|
return;
|
|
}
|
|
else {
|
|
throw new Error(`Invalid keyword: ${keyword}`);
|
|
}
|
|
});
|
|
const invalidSymbolMatches = str.match(/[^a-z\d+*\-\/#@:,()]/);
|
|
if (invalidSymbolMatches) {
|
|
throw new Error(`Invalid symbol: ${invalidSymbolMatches[0]}`);
|
|
}
|
|
const invalidValueModifierMatches = str.match(/[^a-z@)]+(@[a-z,:\d]*)/);
|
|
if (invalidValueModifierMatches) {
|
|
throw new Error(`Value modifier cannot be used at current location: ${invalidValueModifierMatches[1]}`);
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @param str
|
|
*/
|
|
export function deriveFormattedTag(str) {
|
|
return str
|
|
.toLowerCase()
|
|
.replace(/\s+/g, '')
|
|
.replace(/-\(/g, '-1*(')
|
|
.replace(/\)\(/g, ')*(')
|
|
.replace(/-/g, '+-')
|
|
.replace(/--/g, '+')
|
|
.replace(/\+\+/g, '+')
|
|
.replace(/\(\+/g, '(')
|
|
.replace(/(\d)\(/g, '$1*(')
|
|
.replace(/^\++/, '');
|
|
}
|
|
/**
|
|
*
|
|
* @param str
|
|
*/
|
|
export function deriveListOptions(str) {
|
|
return str.split(SnippetSymbolEnum.LIST_OPTION);
|
|
}
|
|
/**
|
|
*
|
|
* @param stats
|
|
* @param snippetData
|
|
*/
|
|
export function deriveStatModifierValue(stats, snippetData) {
|
|
const modifierValues = stats
|
|
.map((statKey) => {
|
|
validateAbilityKey(statKey);
|
|
return AbilityAccessors.getModifier(snippetData.abilityKeyLookup[statKey]);
|
|
})
|
|
.filter(TypeScriptUtils.isNotNullOrUndefined);
|
|
return Math.max(...modifierValues);
|
|
}
|
|
/**
|
|
*
|
|
* @param stats
|
|
* @param snippetData
|
|
*/
|
|
export function deriveAbilityScoreValue(stats, snippetData) {
|
|
const modifierValues = stats
|
|
.map((statKey) => {
|
|
validateAbilityKey(statKey);
|
|
return AbilityAccessors.getScore(snippetData.abilityKeyLookup[statKey]);
|
|
})
|
|
.filter(TypeScriptUtils.isNotNullOrUndefined);
|
|
return Math.max(...modifierValues);
|
|
}
|
|
/**
|
|
*
|
|
* @param contextData
|
|
*/
|
|
export function deriveLevelScaleDiceValue(contextData) {
|
|
if (!contextData.levelScale) {
|
|
throw new Error('No level scale data available');
|
|
}
|
|
if (contextData.levelScale.dice === null) {
|
|
throw new Error('Has level scale, but no dice value available');
|
|
}
|
|
return DiceRenderers.renderDice(contextData.levelScale.dice);
|
|
}
|
|
/**
|
|
*
|
|
* @param contextData
|
|
*/
|
|
export function deriveLevelScaleFixedValue(contextData) {
|
|
if (!contextData.levelScale) {
|
|
throw new Error('No level scale data available');
|
|
}
|
|
if (contextData.levelScale.fixedValue === null) {
|
|
throw new Error('Has level scale, but no numeric value available');
|
|
}
|
|
return contextData.levelScale.fixedValue;
|
|
}
|
|
/**
|
|
*
|
|
* @param tagValue
|
|
* @param contextData
|
|
*/
|
|
export function deriveStringTagValue(tagValue, contextData) {
|
|
const { type, params } = tagValue;
|
|
switch (type) {
|
|
case SnippetTagValueTypeEnum.SCALE_VALUE:
|
|
return deriveLevelScaleDiceValue(contextData);
|
|
}
|
|
throw new Error('Unknown string tag');
|
|
}
|
|
/**
|
|
*
|
|
* @param tagValue
|
|
* @param contextData
|
|
* @param snippetData
|
|
* @param proficiencyBonus
|
|
*/
|
|
export function deriveNumericTagValue(tagValue, contextData, snippetData, proficiencyBonus) {
|
|
const { type, params } = tagValue;
|
|
let value = 0;
|
|
switch (type) {
|
|
case SnippetTagValueTypeEnum.SAVE_DC:
|
|
if (params.length < 1) {
|
|
throw new Error('Save DC missing ability key');
|
|
}
|
|
return CharacterDerivers.deriveAttackSaveValue(snippetData.proficiencyBonus, deriveStatModifierValue(deriveListOptions(params[0]), snippetData));
|
|
case SnippetTagValueTypeEnum.SPELL_ATTACK:
|
|
if (params.length < 1) {
|
|
throw new Error('Spell attack missing ability key');
|
|
}
|
|
return SpellDerivers.deriveSpellAttackModifier(snippetData.proficiencyBonus, deriveStatModifierValue(deriveListOptions(params[0]), snippetData));
|
|
case SnippetTagValueTypeEnum.MODIFIER: {
|
|
if (params.length < 1) {
|
|
throw new Error('Modifier missing ability key');
|
|
}
|
|
return deriveStatModifierValue(deriveListOptions(params[0]), snippetData);
|
|
}
|
|
case SnippetTagValueTypeEnum.ABILITY_SCORE: {
|
|
if (params.length < 1) {
|
|
throw new Error('Ability score missing ability key');
|
|
}
|
|
return deriveAbilityScoreValue(deriveListOptions(params[0]), snippetData);
|
|
}
|
|
case SnippetTagValueTypeEnum.SCALE_VALUE:
|
|
return deriveLevelScaleFixedValue(contextData);
|
|
case SnippetTagValueTypeEnum.FIXED_VALUE:
|
|
if (params.length < 1) {
|
|
throw new Error('Fixed value is missing');
|
|
}
|
|
value = toNumber(params[0]);
|
|
if (!isFinite(value)) {
|
|
throw new Error('Fixed value is not a number');
|
|
}
|
|
return value;
|
|
case SnippetTagValueTypeEnum.CLASS_LEVEL: {
|
|
if (!contextData.classLevel) {
|
|
throw new Error('No class level data available');
|
|
}
|
|
return contextData.classLevel;
|
|
}
|
|
case SnippetTagValueTypeEnum.CHARACTER_LEVEL: {
|
|
return snippetData.xpInfo.currentLevel;
|
|
}
|
|
case SnippetTagValueTypeEnum.PROFICIENCY: {
|
|
return snippetData.proficiencyBonus;
|
|
}
|
|
case SnippetTagValueTypeEnum.MAX_HP: {
|
|
return snippetData.hitPointInfo.totalHp;
|
|
}
|
|
case SnippetTagValueTypeEnum.LIMITED_USE: {
|
|
if (!contextData.limitedUse) {
|
|
throw new Error('No limited use data available');
|
|
}
|
|
return LimitedUseDerivers.deriveMaxUses(contextData.limitedUse, snippetData.abilityLookup, snippetData.ruleData, proficiencyBonus);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @param str
|
|
*/
|
|
export function parseSnippetParams(str) {
|
|
if (!str.length) {
|
|
throw new Error('Invalid parameter');
|
|
}
|
|
const [key, ...params] = str.split(SnippetSymbolEnum.PARAMETER);
|
|
if (!key.length) {
|
|
throw new Error('Invalid parameter key');
|
|
}
|
|
return {
|
|
key,
|
|
params: params.filter(Boolean),
|
|
};
|
|
}
|
|
/**
|
|
*
|
|
* @param str
|
|
*/
|
|
export function parseSnippetPostProcessParams(str) {
|
|
const { key, params } = parseSnippetParams(str);
|
|
if (!isSnippetPostProcessTypeEnum(key)) {
|
|
throw new Error(`Unknown post process type: ${key}`);
|
|
}
|
|
return {
|
|
type: key,
|
|
params,
|
|
};
|
|
}
|
|
/**
|
|
*
|
|
* @param str
|
|
*/
|
|
export function parseSnippetTagValue(str) {
|
|
const { key, params } = parseSnippetParams(str);
|
|
if (!isSnippetTagValueTypeEnum(key)) {
|
|
throw new Error(`Unknown value type: ${key}`);
|
|
}
|
|
return {
|
|
type: key,
|
|
params,
|
|
};
|
|
}
|
|
/**
|
|
*
|
|
* @param str
|
|
*/
|
|
export function parseSnippetValueModifierParams(str) {
|
|
const { key, params } = parseSnippetParams(str);
|
|
if (!isSnippetValueModifierTypeEnum(key)) {
|
|
throw new Error(`Unknown value modifier type: ${key}`);
|
|
}
|
|
return {
|
|
type: key,
|
|
params,
|
|
};
|
|
}
|
|
/**
|
|
*
|
|
* @param str
|
|
*/
|
|
export function parseRawTagInfo(str) {
|
|
if (!str.length) {
|
|
throw new Error('Invalid tag');
|
|
}
|
|
const [expression, rawPostProcess] = str.split(SnippetSymbolEnum.POST_PROCESS);
|
|
if (!expression.length) {
|
|
throw new Error('Missing tag expression');
|
|
}
|
|
let postProcess = rawPostProcess;
|
|
if (rawPostProcess === undefined || rawPostProcess === '') {
|
|
postProcess = null;
|
|
}
|
|
return {
|
|
expression,
|
|
postProcess,
|
|
};
|
|
}
|
|
/**
|
|
*
|
|
* @param value
|
|
* @param valueModifierString
|
|
*/
|
|
export function applyValueModifierString(value, valueModifierString) {
|
|
let modifiedValue = value;
|
|
const valueModifiers = deriveListOptions(valueModifierString);
|
|
valueModifiers.forEach((valueModifierString) => {
|
|
const { type, params } = parseSnippetValueModifierParams(valueModifierString);
|
|
modifiedValue = deriveModifiedValue(modifiedValue, type, params);
|
|
});
|
|
return modifiedValue;
|
|
}
|
|
/**
|
|
*
|
|
* @param value
|
|
* @param postProcessesString
|
|
* @param intrinsicSignedNumber
|
|
*/
|
|
export function applyPostProcessString(value, postProcessesString, intrinsicSignedNumber = false) {
|
|
if (typeof value === 'string' && !value) {
|
|
return value;
|
|
}
|
|
let processedValue = value;
|
|
let postProcesses = postProcessesString === null || !postProcessesString.length ? [] : deriveListOptions(postProcessesString);
|
|
if (intrinsicSignedNumber &&
|
|
!postProcesses.includes(SnippetPostProcessTypeEnum.SIGNED_NUMBER) &&
|
|
!postProcesses.includes(SnippetPostProcessTypeEnum.UNSIGNED_NUMBER)) {
|
|
postProcesses = [...postProcesses, SnippetPostProcessTypeEnum.SIGNED_NUMBER];
|
|
}
|
|
postProcesses.forEach((postProcessString, idx) => {
|
|
const { type, params } = parseSnippetPostProcessParams(postProcessString);
|
|
const isLast = idx + 1 === postProcesses.length;
|
|
if (isLastPostProcessType(type) && !isLast) {
|
|
throw new Error(`"${type}" post process must be the last to run`);
|
|
}
|
|
processedValue = derivePostProcessedValue(processedValue, type, params);
|
|
});
|
|
return processedValue;
|
|
}
|
|
/**
|
|
*
|
|
* @param value
|
|
* @param modifierType
|
|
* @param params
|
|
*/
|
|
export function deriveModifiedValue(value, modifierType, params) {
|
|
switch (modifierType) {
|
|
case SnippetValueModifierTypeEnum.MAX:
|
|
if (params.length < 1) {
|
|
throw new Error('Max value modifier is missing a number');
|
|
}
|
|
const maxValue = toNumber(params[0]);
|
|
if (!isFinite(maxValue)) {
|
|
throw new Error('Max value modifier is not a number');
|
|
}
|
|
return Math.min(maxValue, value);
|
|
case SnippetValueModifierTypeEnum.MIN:
|
|
if (params.length < 1) {
|
|
throw new Error('Min value modifier is missing a number');
|
|
}
|
|
const minValue = toNumber(params[0]);
|
|
if (!isFinite(minValue)) {
|
|
throw new Error('Min value modifier is not a number');
|
|
}
|
|
return Math.max(minValue, value);
|
|
case SnippetValueModifierTypeEnum.ROUND_UP:
|
|
return Math.ceil(value);
|
|
case SnippetValueModifierTypeEnum.ROUND_DOWN:
|
|
return Math.floor(value);
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @param value
|
|
* @param type
|
|
* @param params
|
|
*/
|
|
export function derivePostProcessedValue(value, type, params) {
|
|
switch (type) {
|
|
case SnippetPostProcessTypeEnum.MAX:
|
|
if (typeof value !== 'number') {
|
|
throw new Error('Max post process must be used on number value');
|
|
}
|
|
if (params.length < 1) {
|
|
throw new Error('Max post process is missing a number');
|
|
}
|
|
const maxValue = toNumber(params[0]);
|
|
return Math.min(maxValue, value);
|
|
case SnippetPostProcessTypeEnum.MIN:
|
|
if (typeof value !== 'number') {
|
|
throw new Error('Min post process must be used on number value');
|
|
}
|
|
if (params.length < 1) {
|
|
throw new Error('Min post process is missing a number');
|
|
}
|
|
const minValue = toNumber(params[0]);
|
|
return Math.max(minValue, value);
|
|
case SnippetPostProcessTypeEnum.ROUND_DOWN:
|
|
if (typeof value !== 'number') {
|
|
throw new Error('Post process value being rounded is not a number');
|
|
}
|
|
return Math.floor(value);
|
|
case SnippetPostProcessTypeEnum.ROUND_UP:
|
|
if (typeof value !== 'number') {
|
|
throw new Error('Post process value being rounded is not a number');
|
|
}
|
|
return Math.ceil(value);
|
|
case SnippetPostProcessTypeEnum.SIGNED_NUMBER:
|
|
if (typeof value !== 'number') {
|
|
return value;
|
|
}
|
|
return FormatUtils.renderSignedNumber(value);
|
|
case SnippetPostProcessTypeEnum.UNSIGNED_NUMBER:
|
|
return value;
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @param haystack
|
|
* @param operatorIdx
|
|
* @param direction
|
|
*/
|
|
export function deriveExpressionValueFragmentSide(haystack, operatorIdx, direction) {
|
|
const limit = direction == -1 ? 0 : haystack.length;
|
|
let i = operatorIdx + direction;
|
|
let term = '';
|
|
while (i * direction <= limit) {
|
|
if (isParsableNumber(haystack[i])) {
|
|
if (direction == 1) {
|
|
term = term + haystack[i];
|
|
}
|
|
else {
|
|
term = haystack[i] + term;
|
|
}
|
|
i += direction;
|
|
}
|
|
else {
|
|
return term;
|
|
}
|
|
}
|
|
return term;
|
|
}
|
|
/**
|
|
*
|
|
* @param expression
|
|
* @param operator
|
|
*/
|
|
export function deriveExpressionValueFragment(expression, operator) {
|
|
if (hasOperator(expression, operator)) {
|
|
const middleIndex = expression.indexOf(operator);
|
|
const left = deriveExpressionValueFragmentSide(expression, middleIndex, -1);
|
|
const right = deriveExpressionValueFragmentSide(expression, middleIndex, 1);
|
|
const value = deriveExpressionValue(left, right, operator);
|
|
return expression.replace(left + operator + right, value.toString());
|
|
}
|
|
return expression;
|
|
}
|
|
/**
|
|
*
|
|
* @param left
|
|
* @param right
|
|
* @param operator
|
|
*/
|
|
export function deriveExpressionValue(left, right, operator) {
|
|
switch (operator) {
|
|
case SnippetMathOperatorEnum.ADDITION:
|
|
return parseFloat(left) + parseFloat(right);
|
|
case SnippetMathOperatorEnum.MULTIPLICATION:
|
|
return parseFloat(left) * parseFloat(right);
|
|
case SnippetMathOperatorEnum.DIVISION:
|
|
return parseFloat(left) / parseFloat(right);
|
|
case SnippetMathOperatorEnum.SUBTRACTION:
|
|
return parseFloat(left) - parseFloat(right);
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @param expression
|
|
*/
|
|
export function deriveExpression(expression) {
|
|
while (hasOperator(expression, SnippetMathOperatorEnum.MULTIPLICATION) ||
|
|
hasOperator(expression, SnippetMathOperatorEnum.DIVISION)) {
|
|
let multiply = true;
|
|
if (expression.indexOf(SnippetMathOperatorEnum.MULTIPLICATION) <
|
|
expression.indexOf(SnippetMathOperatorEnum.DIVISION)) {
|
|
multiply = hasOperator(expression, SnippetMathOperatorEnum.MULTIPLICATION);
|
|
}
|
|
else {
|
|
multiply = !hasOperator(expression, SnippetMathOperatorEnum.DIVISION);
|
|
}
|
|
if (multiply) {
|
|
expression = deriveExpressionValueFragment(expression, SnippetMathOperatorEnum.MULTIPLICATION);
|
|
}
|
|
else {
|
|
expression = deriveExpressionValueFragment(expression, SnippetMathOperatorEnum.DIVISION);
|
|
}
|
|
}
|
|
while (hasOperator(expression, SnippetMathOperatorEnum.ADDITION)) {
|
|
expression = deriveExpressionValueFragment(expression, SnippetMathOperatorEnum.ADDITION);
|
|
}
|
|
return toNumber(expression);
|
|
}
|
|
/**
|
|
*
|
|
* @param tagExpression
|
|
* @param contextData
|
|
* @param snippetData
|
|
* @param proficiencyBonus
|
|
*/
|
|
export function generateFragmentExpression(tagExpression, contextData, snippetData, proficiencyBonus) {
|
|
let hasSignedValue = false;
|
|
let updatedExpression = tagExpression;
|
|
let match;
|
|
while ((match = updatedExpression.match(tagValueRegExpr))) {
|
|
const tagRaw = match[0];
|
|
const tagValue = match[1];
|
|
const tagValueModifier = match[2];
|
|
const snippetTagValue = parseSnippetTagValue(tagValue);
|
|
if (!hasSignedValue) {
|
|
hasSignedValue = isSignedTagValueType(snippetTagValue.type);
|
|
}
|
|
let value = deriveNumericTagValue(snippetTagValue, contextData, snippetData, proficiencyBonus);
|
|
if (tagValueModifier) {
|
|
value = applyValueModifierString(value, tagValueModifier);
|
|
}
|
|
updatedExpression = updatedExpression.replace(tagRaw, value.toString());
|
|
}
|
|
return {
|
|
original: tagExpression,
|
|
expression: updatedExpression,
|
|
hasSignedValue,
|
|
};
|
|
}
|
|
/**
|
|
*
|
|
* @param rawSnippetTag
|
|
* @param contextData
|
|
* @param snippetData
|
|
* @param proficiencyBonus
|
|
*/
|
|
export function generateSnippetTag(rawSnippetTag, contextData, snippetData, proficiencyBonus) {
|
|
const { expression, postProcess } = parseRawTagInfo(deriveFormattedTag(rawSnippetTag));
|
|
const snippetBaseTag = {
|
|
raw: rawSnippetTag,
|
|
};
|
|
if (isValidStringTagValueType(rawSnippetTag, contextData)) {
|
|
const snippetTagValue = parseSnippetTagValue(rawSnippetTag);
|
|
return Object.assign(Object.assign({}, snippetBaseTag), { type: SnippetTagDataTypeEnum.STRING, value: deriveStringTagValue(snippetTagValue, contextData) });
|
|
}
|
|
throwMismatchedParens(rawSnippetTag);
|
|
throwInvalidSnippetCharacters(rawSnippetTag);
|
|
let hasSignedValue = false;
|
|
let updatedExpression = expression;
|
|
// solve grouped parens first
|
|
let match;
|
|
while ((match = updatedExpression.match(groupFragmentRegExpr))) {
|
|
let groupFragmentRaw = match[0];
|
|
let groupFragmentExpression = match[1];
|
|
let groupFragmentValueModifier = match[2];
|
|
let tagFragmentExpression = generateFragmentExpression(groupFragmentExpression, contextData, snippetData, proficiencyBonus);
|
|
if (!hasSignedValue) {
|
|
hasSignedValue = tagFragmentExpression.hasSignedValue;
|
|
}
|
|
let value = deriveExpression(tagFragmentExpression.expression);
|
|
if (groupFragmentValueModifier) {
|
|
value = applyValueModifierString(value, groupFragmentValueModifier);
|
|
}
|
|
updatedExpression = updatedExpression.replace(groupFragmentRaw, value.toString());
|
|
}
|
|
let fragmentExpression = generateFragmentExpression(updatedExpression, contextData, snippetData, proficiencyBonus);
|
|
let value = deriveExpression(fragmentExpression.expression);
|
|
if (!hasSignedValue) {
|
|
hasSignedValue = fragmentExpression.hasSignedValue;
|
|
}
|
|
value = applyPostProcessString(value, postProcess, hasSignedValue);
|
|
if (typeof value === 'number') {
|
|
return Object.assign(Object.assign({}, snippetBaseTag), { type: SnippetTagDataTypeEnum.NUMBER, value });
|
|
}
|
|
else {
|
|
return Object.assign(Object.assign({}, snippetBaseTag), { type: SnippetTagDataTypeEnum.STRING, value });
|
|
}
|
|
}
|
|
/**
|
|
*
|
|
* @param id
|
|
*/
|
|
export function generateSnippetTagPlaceholder(id) {
|
|
return `__SNIPPET_PLACEHOLDER_${id}__`;
|
|
}
|
|
/**
|
|
*
|
|
* @param snippetTag
|
|
*/
|
|
export function generateSnippetContentTagChunk(snippetTag) {
|
|
return {
|
|
type: SnippetContentChunkTypeEnum.TAG,
|
|
data: snippetTag,
|
|
};
|
|
}
|
|
/**
|
|
*
|
|
* @param str
|
|
*/
|
|
export function generateSnippetContentTextChunk(str) {
|
|
return {
|
|
type: SnippetContentChunkTypeEnum.TEXT,
|
|
data: str,
|
|
};
|
|
}
|
|
/**
|
|
*
|
|
* @param snippetString
|
|
* @param placeholders
|
|
* @param placeholderLookup
|
|
*/
|
|
export function generateSnippetContentChunks(snippetString, placeholders, placeholderLookup) {
|
|
let chunks = [];
|
|
placeholders.forEach((placeholder) => {
|
|
let strIdx = snippetString.indexOf(placeholder);
|
|
if (strIdx > -1) {
|
|
let prePlaceholder = snippetString.slice(0, strIdx);
|
|
let postPlaceholder = snippetString.slice(strIdx + placeholder.length);
|
|
if (prePlaceholder) {
|
|
chunks = [...generateSnippetContentChunks(prePlaceholder, placeholders, placeholderLookup)];
|
|
}
|
|
chunks = [...chunks, generateSnippetContentTagChunk(placeholderLookup[placeholder])];
|
|
if (postPlaceholder) {
|
|
chunks = [...chunks, ...generateSnippetContentChunks(postPlaceholder, placeholders, placeholderLookup)];
|
|
}
|
|
}
|
|
});
|
|
if (!chunks.length) {
|
|
chunks.push(generateSnippetContentTextChunk(snippetString));
|
|
}
|
|
return chunks;
|
|
}
|
|
/**
|
|
*
|
|
* @param snippetString
|
|
* @param contextData
|
|
* @param snippetData
|
|
* @param proficiencyBonus
|
|
*/
|
|
export function generateSnippetChunks(snippetString, contextData, snippetData, proficiencyBonus) {
|
|
let match;
|
|
let placeholderSnippetString = snippetString;
|
|
let placeholders = [];
|
|
let rawSnippetTags = [];
|
|
// find all snippet tags and put placeholders
|
|
while ((match = placeholderSnippetString.match(/\{\{([^\}]*)\}\}/))) {
|
|
const rawSnippetTag = match[0];
|
|
const rawSnippetTagContent = match[1];
|
|
const placeholder = generateSnippetTagPlaceholder(rawSnippetTags.length);
|
|
rawSnippetTags.push(rawSnippetTagContent);
|
|
placeholders.push(placeholder);
|
|
placeholderSnippetString = placeholderSnippetString.split(rawSnippetTag).join(placeholder);
|
|
}
|
|
// parse all snippet tags that were found
|
|
let placeholderLookup = {};
|
|
rawSnippetTags.forEach((rawSnippetTag, idx) => {
|
|
const key = generateSnippetTagPlaceholder(idx);
|
|
try {
|
|
placeholderLookup[key] = generateSnippetTag(rawSnippetTag, contextData, snippetData, proficiencyBonus);
|
|
}
|
|
catch (error) {
|
|
const errorSnippetTag = {
|
|
type: SnippetTagDataTypeEnum.ERROR,
|
|
raw: rawSnippetTag,
|
|
value: error.message,
|
|
};
|
|
placeholderLookup[key] = errorSnippetTag;
|
|
}
|
|
});
|
|
// break apart the original string into chunks
|
|
return generateSnippetContentChunks(placeholderSnippetString, placeholders, placeholderLookup);
|
|
}
|
|
/**
|
|
*
|
|
* @param snippetString
|
|
* @param contextData
|
|
* @param snippetData
|
|
* @param outputHtml
|
|
* @param proficiencyBonus
|
|
* @deprecated this will be moving to character component Snippet
|
|
*
|
|
*/
|
|
export function convertSnippetToHtml(snippetString, contextData, snippetData, outputHtml = true, proficiencyBonus) {
|
|
if (snippetString === null) {
|
|
return null;
|
|
}
|
|
const snippetChunks = generateSnippetChunks(snippetString, contextData, snippetData, proficiencyBonus);
|
|
const htmlChunkString = snippetChunks
|
|
.map((contentChunk) => {
|
|
switch (contentChunk.type) {
|
|
case SnippetContentChunkTypeEnum.TAG:
|
|
switch (contentChunk.data.type) {
|
|
case SnippetTagDataTypeEnum.ERROR:
|
|
const errorMessage = `${contentChunk.data.raw} - ${contentChunk.data.value}`;
|
|
return `<span class="ddbc-snippet__tag--error" title="${contentChunk.data.raw}">${errorMessage}</span>`;
|
|
case SnippetTagDataTypeEnum.NUMBER:
|
|
return `<Tooltip title="${contentChunk.data.raw}"><span class="ddbc-snippet__tag">${contentChunk.data.value.toLocaleString()}</span></Tooltip>`;
|
|
case SnippetTagDataTypeEnum.STRING:
|
|
default:
|
|
return `<Tooltip title="${contentChunk.data.raw}"><span class="ddbc-snippet__tag">${contentChunk.data.value}</span></Tooltip>`;
|
|
}
|
|
case SnippetContentChunkTypeEnum.TEXT:
|
|
default:
|
|
return contentChunk.data;
|
|
}
|
|
})
|
|
.join('');
|
|
return `<p>${htmlChunkString.replace(/\s*\n+?\s*\n*/g, '</p><p>')}</p>`;
|
|
}
|
|
/**
|
|
*
|
|
* @param snippetString
|
|
* @param contextData
|
|
* @param snippetData
|
|
* @param proficiencyBonus
|
|
*/
|
|
export function convertSnippetToText(snippetString, contextData, snippetData, proficiencyBonus) {
|
|
if (snippetString === null) {
|
|
return null;
|
|
}
|
|
const snippetChunks = generateSnippetChunks(snippetString, contextData, snippetData, proficiencyBonus);
|
|
return snippetChunks
|
|
.map((contentChunk) => {
|
|
switch (contentChunk.type) {
|
|
case SnippetContentChunkTypeEnum.TAG:
|
|
switch (contentChunk.data.type) {
|
|
case SnippetTagDataTypeEnum.ERROR:
|
|
let messageParts = [];
|
|
if (contentChunk.data.raw.trim()) {
|
|
messageParts.push(contentChunk.data.raw);
|
|
}
|
|
messageParts.push(contentChunk.data.value);
|
|
return messageParts.join(' - ');
|
|
case SnippetTagDataTypeEnum.NUMBER:
|
|
return contentChunk.data.value.toLocaleString();
|
|
case SnippetTagDataTypeEnum.STRING:
|
|
default:
|
|
return contentChunk.data.value;
|
|
}
|
|
case SnippetContentChunkTypeEnum.TEXT:
|
|
return contentChunk.data;
|
|
default:
|
|
// not implemented
|
|
}
|
|
})
|
|
.join('');
|
|
}
|