Merge branch 'develop' into feature-nested-sets
This commit is contained in:
@@ -1,19 +1,46 @@
|
||||
import evaluateCalculation from '../../utility/evaluateCalculation';
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES';
|
||||
import call from '/imports/parser/parseTree/call';
|
||||
import constant from '/imports/parser/parseTree/constant';
|
||||
import operator from '/imports/parser/parseTree/operator';
|
||||
import parenthesis from '/imports/parser/parseTree/parenthesis';
|
||||
import resolve, { toPrimitiveOrString } from '/imports/parser/resolve';
|
||||
|
||||
export default function computeCalculation(computation, node) {
|
||||
const calcObj = node.data;
|
||||
evaluateCalculation(calcObj, computation.scope);
|
||||
if (calcObj.effects || calcObj.proficiencies) {
|
||||
calcObj.baseValue = calcObj.value;
|
||||
if (!calcObj) return;
|
||||
// resolve the parse node into the initial value
|
||||
resolveCalculationNode(calcObj, calcObj.parseNode, computation.scope);
|
||||
// Store the unaffected value
|
||||
if (calcObj.effectIds || calcObj.proficiencyIds) {
|
||||
calcObj.unaffected = toPrimitiveOrString(calcObj.valueNode);
|
||||
}
|
||||
aggregateCalculationEffects(node, computation);
|
||||
aggregateCalculationProficiencies(node, computation);
|
||||
// link and aggregate the effects and proficiencies
|
||||
linkCalculationEffects(node, computation);
|
||||
aggregateCalculationEffects(calcObj, id => computation.propsById[id]);
|
||||
linkCalculationProficiencies(node, computation)
|
||||
aggregateCalculationProficiencies(calcObj, id => computation.propsById[id], computation.scope['proficiencyBonus']?.value || 0);
|
||||
|
||||
// Resolve the valueNode after effects and proficiencies have been applied to it
|
||||
resolveCalculationNode(calcObj, calcObj.valueNode, computation.scope);
|
||||
|
||||
// Store the value as a primitive
|
||||
calcObj.value = toPrimitiveOrString(calcObj.valueNode);
|
||||
|
||||
// remove the working fields
|
||||
delete calcObj._parseLevel;
|
||||
delete calcObj._localScope;
|
||||
}
|
||||
|
||||
function aggregateCalculationEffects(node, computation) {
|
||||
export function resolveCalculationNode(calculation, parseNode, scope) {
|
||||
const fn = calculation._parseLevel;
|
||||
const calculationScope = { ...calculation._localScope, ...scope };
|
||||
const { result: resultNode, context } = resolve(fn, parseNode, calculationScope);
|
||||
calculation.errors = context.errors;
|
||||
calculation.valueNode = resultNode;
|
||||
}
|
||||
|
||||
function linkCalculationEffects(node, computation) {
|
||||
const calcObj = node.data;
|
||||
delete calcObj.effects;
|
||||
delete calcObj.effectIds;
|
||||
computation.dependencyGraph.forEachLinkedNode(
|
||||
node.id,
|
||||
(linkedNode, link) => {
|
||||
@@ -25,35 +52,115 @@ function aggregateCalculationEffects(node, computation) {
|
||||
if (linkedNode.data.inactive) return;
|
||||
|
||||
// Collate effects
|
||||
calcObj.effects = calcObj.effects || [];
|
||||
calcObj.effects.push({
|
||||
_id: linkedNode.data._id,
|
||||
name: linkedNode.data.name,
|
||||
operation: linkedNode.data.operation,
|
||||
amount: linkedNode.data.amount && {
|
||||
value: linkedNode.data.amount.value,
|
||||
},
|
||||
});
|
||||
calcObj.effectIds = calcObj.effectIds || [];
|
||||
calcObj.effectIds.push(linkedNode.data._id);
|
||||
},
|
||||
true // enumerate only outbound links
|
||||
);
|
||||
if (calcObj.effects && typeof calcObj.value === 'number') {
|
||||
calcObj.effects.forEach(effect => {
|
||||
if (
|
||||
effect.operation === 'add' &&
|
||||
effect.amount && typeof effect.amount.value === 'number'
|
||||
) {
|
||||
calcObj.value += effect.amount.value
|
||||
}
|
||||
}
|
||||
|
||||
export function aggregateCalculationEffects(calcObj, getEffectFromId) {
|
||||
// dictionary of {[operation]: parseNode}
|
||||
const aggregator = {};
|
||||
// Store all effect values
|
||||
calcObj.effectIds?.forEach(effectId => {
|
||||
const effect = getEffectFromId(effectId);
|
||||
const op = effect.operation;
|
||||
switch (op) {
|
||||
case undefined:
|
||||
break;
|
||||
// Conditionals stored as a list of text
|
||||
case 'conditional':
|
||||
if (!aggregator[op]) aggregator[op] = [];
|
||||
aggregator[op].push(effect.text);
|
||||
break;
|
||||
// Adv/Dis and Fails just count instances
|
||||
case 'advantage':
|
||||
case 'disadvantage':
|
||||
case 'fail':
|
||||
if (calcObj[op] === undefined) calcObj[op] = 0;
|
||||
calcObj[op]++;
|
||||
break;
|
||||
// Math functions store value parseNodes
|
||||
case 'base':
|
||||
case 'add':
|
||||
case 'mul':
|
||||
case 'min':
|
||||
case 'max':
|
||||
case 'set':
|
||||
if (!aggregator[op]) aggregator[op] = [];
|
||||
aggregator[op].push(effect.amount.valueNode);
|
||||
break;
|
||||
// No case for passiveAdd, it doesn't make sense in this context
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Aggregate the effects in a parse tree like so
|
||||
* x = max(...base, unaffectedValue)
|
||||
* x = x + sum(...add)
|
||||
* x = x * mul(...mul)
|
||||
* x = min(...min, x)
|
||||
* x = max(...max, x)
|
||||
* x = set(last(...set))a
|
||||
*/
|
||||
// Set
|
||||
// If we do set, return early, nothing else matters
|
||||
if (aggregator.set) {
|
||||
calcObj.valueNode = aggregator.set[aggregator.set.length - 1];
|
||||
return;
|
||||
}
|
||||
// Base value
|
||||
if (aggregator.base) {
|
||||
calcObj.valueNode = call.create({
|
||||
functionName: 'max',
|
||||
args: [calcObj.valueNode, aggregator.base]
|
||||
});
|
||||
}
|
||||
// Add
|
||||
aggregator.add?.forEach(node => {
|
||||
calcObj.valueNode = operator.create({
|
||||
left: calcObj.valueNode,
|
||||
right: node,
|
||||
operator: '+'
|
||||
});
|
||||
});
|
||||
// Multiply
|
||||
if (aggregator.mul) {
|
||||
// Wrap the previous node in brackets if it's another operator
|
||||
if (calcObj.parseType === 'operator') {
|
||||
calcObj.valueNode = parenthesis.create({
|
||||
content: calcObj.valueNode
|
||||
});
|
||||
}
|
||||
// Append all multiplications
|
||||
aggregator.mul.forEach(node => {
|
||||
calcObj.valueNode = operator.create({
|
||||
left: calcObj.valueNode,
|
||||
right: node,
|
||||
operator: '*'
|
||||
});
|
||||
});
|
||||
}
|
||||
// Min
|
||||
if (aggregator.min) {
|
||||
calcObj.valueNode = call.create({
|
||||
functionName: 'max',
|
||||
args: [calcObj.valueNode, aggregator.min]
|
||||
});
|
||||
}
|
||||
// Max
|
||||
if (aggregator.max) {
|
||||
calcObj.valueNode = call.create({
|
||||
functionName: 'min',
|
||||
args: [calcObj.valueNode, aggregator.max]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function aggregateCalculationProficiencies(node, computation) {
|
||||
function linkCalculationProficiencies(node, computation) {
|
||||
const calcObj = node.data;
|
||||
delete calcObj.proficiencies;
|
||||
delete calcObj.proficiencyIds;
|
||||
delete calcObj.proficiency;
|
||||
let profBonus = computation.scope['proficiencyBonus']?.value || 0;
|
||||
|
||||
// Go through all the links and collect them on the calculation
|
||||
computation.dependencyGraph.forEachLinkedNode(
|
||||
@@ -65,49 +172,52 @@ function aggregateCalculationProficiencies(node, computation) {
|
||||
if (!linkedNode.data) return;
|
||||
// Ignoring inactive props
|
||||
if (linkedNode.data.inactive) return;
|
||||
// Compute the proficiency and value
|
||||
let proficiency, value;
|
||||
if (linkedNode.data.type === 'proficiency') {
|
||||
proficiency = linkedNode.data.value || 0;
|
||||
// Multiply the proficiency bonus by the actual proficiency
|
||||
if (proficiency === 0.49) {
|
||||
// Round down proficiency bonus in the special case
|
||||
value = Math.floor(profBonus * 0.5);
|
||||
} else {
|
||||
value = Math.ceil(profBonus * proficiency);
|
||||
}
|
||||
} else if (linkedNode.data.type === 'skill') {
|
||||
value = linkedNode.data.value || 0;
|
||||
proficiency = linkedNode.data.proficiency || 0;
|
||||
}
|
||||
// Collate proficiencies
|
||||
calcObj.proficiencies = calcObj.proficiencies || [];
|
||||
calcObj.proficiencies.push({
|
||||
_id: linkedNode.data._id,
|
||||
name: linkedNode.data.name,
|
||||
type: linkedNode.data.type,
|
||||
proficiency,
|
||||
value,
|
||||
});
|
||||
calcObj.proficiencyIds = calcObj.proficiencyIds || [];
|
||||
calcObj.proficiencyIds.push(linkedNode.data._id);
|
||||
},
|
||||
true // enumerate only outbound links
|
||||
);
|
||||
|
||||
// Apply the highest proficiency, marking all others as overridden
|
||||
if (calcObj.proficiencies && typeof calcObj.value === 'number') {
|
||||
calcObj.proficiency = 0;
|
||||
calcObj.proficiencyBonus = 0;
|
||||
let currentProf;
|
||||
calcObj.proficiencies.forEach(prof => {
|
||||
if (prof.value > calcObj.proficiencyBonus) {
|
||||
if (currentProf) currentProf.overridden = true;
|
||||
calcObj.proficiencyBonus = prof.value;
|
||||
calcObj.proficiency = prof.proficiency;
|
||||
currentProf = prof;
|
||||
} else {
|
||||
prof.overridden = true;
|
||||
}
|
||||
});
|
||||
calcObj.value += calcObj.proficiencyBonus;
|
||||
}
|
||||
}
|
||||
|
||||
export function aggregateCalculationProficiencies(calcObj, getProficiencyFromId, profBonus) {
|
||||
if (!calcObj.proficiencyIds) return;
|
||||
// Apply the highest proficiency, marking all others as overridden
|
||||
calcObj.proficiency = 0;
|
||||
calcObj.proficiencyBonus = 0;
|
||||
let currentProf;
|
||||
calcObj.proficiencyIds.forEach(profId => {
|
||||
const profProp = getProficiencyFromId(profId)
|
||||
if (!profProp) {
|
||||
console.warn('proficiency linked but not found ', profId);
|
||||
}
|
||||
// Compute the proficiency and value
|
||||
let proficiency, value;
|
||||
if (profProp.type === 'proficiency') {
|
||||
proficiency = profProp.value || 0;
|
||||
// Multiply the proficiency bonus by the actual proficiency
|
||||
if (proficiency === 0.49) {
|
||||
// Round down proficiency bonus in the special case
|
||||
value = Math.floor(profBonus * 0.5);
|
||||
} else {
|
||||
value = Math.ceil(profBonus * proficiency);
|
||||
}
|
||||
} else if (profProp.type === 'skill') {
|
||||
value = profProp.value || 0;
|
||||
proficiency = profProp.proficiency || 0;
|
||||
}
|
||||
if (value > calcObj.proficiencyBonus) {
|
||||
if (currentProf) currentProf.overridden = true;
|
||||
calcObj.proficiencyBonus = value;
|
||||
calcObj.proficiency = proficiency;
|
||||
currentProf = profProp;
|
||||
} else {
|
||||
profProp.overridden = true;
|
||||
}
|
||||
});
|
||||
calcObj.valueNode = operator.create({
|
||||
left: calcObj.valueNode,
|
||||
right: constant.create({ value: calcObj.proficiencyBonus }),
|
||||
operator: '+'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { has } from 'lodash';
|
||||
import evaluateCalculation from '../../utility/evaluateCalculation';
|
||||
import { resolveCalculationNode } from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation';
|
||||
|
||||
export default function computePointBuy(computation, node) {
|
||||
const prop = node.data;
|
||||
@@ -26,7 +26,9 @@ export default function computePointBuy(computation, node) {
|
||||
}
|
||||
// Evaluate the cost function
|
||||
if (!costFunction) return;
|
||||
evaluateCalculation(costFunction, { ...computation.scope, value: row.value });
|
||||
resolveCalculationNode(costFunction, costFunction.parseNode, {
|
||||
...computation.scope, value: row.value
|
||||
});
|
||||
// Write calculation errors
|
||||
costFunction.errors?.forEach(error => {
|
||||
if (error?.message) {
|
||||
|
||||
@@ -32,23 +32,11 @@ export default function aggregateDefinition({ node, linkedNode, link }) {
|
||||
|
||||
if (propBaseValue === undefined) return;
|
||||
// Store a summary of the definition as a base value effect
|
||||
node.data.effects = node.data.effects || [];
|
||||
node.data.effectIds = node.data.effectIds || [];
|
||||
if (prop.type === 'pointBuyRow') {
|
||||
node.data.effects.push({
|
||||
_id: prop.tableId,
|
||||
name: prop.tableName,
|
||||
operation: 'base',
|
||||
amount: { value: propBaseValue },
|
||||
type: 'pointBuy',
|
||||
});
|
||||
node.data.effectIds.push(prop.tableId);
|
||||
} else {
|
||||
node.data.effects.push({
|
||||
_id: prop._id,
|
||||
name: prop.name,
|
||||
operation: 'base',
|
||||
amount: { value: propBaseValue },
|
||||
type: prop.type,
|
||||
});
|
||||
node.data.effectIds.push(prop._id);
|
||||
}
|
||||
if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue) {
|
||||
node.data.baseValue = propBaseValue;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { pick } from 'lodash';
|
||||
|
||||
export default function aggregateEffect({ node, linkedNode, link }) {
|
||||
if (link.data !== 'effect') return;
|
||||
// store the effect aggregator, its presence indicates that the variable is
|
||||
@@ -19,27 +17,9 @@ export default function aggregateEffect({ node, linkedNode, link }) {
|
||||
rollBonus: [],
|
||||
};
|
||||
|
||||
// Store a summary of the effect itself
|
||||
node.data.effects = node.data.effects || [];
|
||||
// Store either just
|
||||
let effectAmount;
|
||||
if (!linkedNode.data.amount) {
|
||||
effectAmount = undefined;
|
||||
} else if (typeof linkedNode.data.amount.value === 'string') {
|
||||
effectAmount = pick(linkedNode.data.amount, [
|
||||
'calculation', 'parseNode', 'parseError', 'value'
|
||||
]);
|
||||
} else {
|
||||
effectAmount = pick(linkedNode.data.amount, ['value']);
|
||||
}
|
||||
node.data.effects.push({
|
||||
_id: linkedNode.data._id,
|
||||
name: linkedNode.data.name,
|
||||
operation: linkedNode.data.operation,
|
||||
amount: effectAmount,
|
||||
type: linkedNode.data.type,
|
||||
text: linkedNode.data.text,
|
||||
});
|
||||
// Store a link to the effect
|
||||
node.data.effectIds = node.data.effectIds || [];
|
||||
node.data.effectIds.push(linkedNode.data._id);
|
||||
|
||||
// get a shorter reference to the aggregator document
|
||||
const aggregator = node.data.effectAggregator;
|
||||
|
||||
@@ -49,5 +49,5 @@ export default function computeVariableAsAttribute(computation, node, prop) {
|
||||
undefined
|
||||
|
||||
// Store effects
|
||||
prop.effects = node.data.effects;
|
||||
prop.effectIds = node.data.effectIds;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function computeVariableAsSkill(computation, node, prop) {
|
||||
const aggregatorBase = aggregator?.base || 0;
|
||||
|
||||
// Store effects
|
||||
prop.effects = node.data.effects;
|
||||
prop.effectIds = node.data.effectIds;
|
||||
|
||||
// If there is no aggregator, determine if the prop can hide, then exit
|
||||
if (!aggregator) {
|
||||
|
||||
@@ -17,12 +17,8 @@ export default function () {
|
||||
prop('strengthId').modifier, -1,
|
||||
'The proficiency bonus should not change the strength modifier'
|
||||
);
|
||||
assert.isTrue(
|
||||
!!hasLink('actionId.attackRoll', 'tagTargetedProficiency'),
|
||||
'There should be a link from the proficiency to the attack roll'
|
||||
);
|
||||
assert.exists(prop('actionId').attackRoll.proficiencies, 'The proficiency aggregator should be here')
|
||||
assert.exists(prop('actionId').attackRoll.proficiencies[0], 'The proficiency should be here')
|
||||
assert.exists(prop('actionId').attackRoll.proficiencyIds, 'The proficiency aggregator should be here')
|
||||
assert.exists(prop('actionId').attackRoll.proficiencyIds[0], 'The proficiency should be here')
|
||||
// attack roll = strength.mod + proficiencyBonus/2 rounded down
|
||||
// = -1 + 13/2 = -1 + 6 = 5
|
||||
assert.equal(
|
||||
|
||||
Reference in New Issue
Block a user