Settling on a data structure to balance compatibility

with not being wrong
This commit is contained in:
Thaum Rystra
2023-11-09 16:08:04 +02:00
parent 6ce7542c4b
commit 9e5b6b11e1
22 changed files with 312 additions and 338 deletions

View File

@@ -1,23 +1,44 @@
import evaluateCalculation from '../../utility/evaluateCalculation.js';
import call from '/imports/parser/parseTree/call.js';
import constant from '/imports/parser/parseTree/constant.js';
import operator from '/imports/parser/parseTree/operator.js';
import parenthesis from '/imports/parser/parseTree/parenthesis.js';
import { toString } from '/imports/parser/resolve.js';
import resolve, { toPrimitiveOrString } from '/imports/parser/resolve.js';
export default function computeCalculation(computation, node) {
const calcObj = node.data;
evaluateCalculation(calcObj, computation.scope);
if (calcObj.effects || calcObj.proficiencies) {
calcObj.unaffected = calcObj.value;
calcObj.displayUnaffected = toString(calcObj.unaffected);
// 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);
calcObj.displayValue = toString(calcObj.value);
// 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.effectIds;
computation.dependencyGraph.forEachLinkedNode(
@@ -36,107 +57,110 @@ function aggregateCalculationEffects(node, computation) {
},
true // enumerate only outbound links
);
if (calcObj.effectIds) {
// dictionary of {[operation]: parseNode}
const aggregator = {};
// Store all effect values
calcObj.effects.forEach(effect => {
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.value);
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) + sum(...add) ) * mul(...mul)
* min(...min, x)
* max(...max, x)
* set(last(...set))a
*/
// Set
// If we do set, return early, nothing else matters
if (aggregator.set) {
calcObj.value = aggregator.set[aggregator.set.length - 1];
return;
}
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
}
// Base value
if (aggregator.base) {
calcObj.value = call.create({
functionName: 'max',
args: [calcObj.value, aggregator.base]
});
/**
* 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
});
}
// Add
aggregator.add?.forEach(node => {
calcObj.value = operator.create({
left: calcObj.value,
// Append all multiplications
aggregator.mul.forEach(node => {
calcObj.valueNode = operator.create({
left: calcObj.valueNode,
right: node,
operator: '+'
operator: '*'
});
});
// Multiply
if (aggregator.mul) {
// Wrap the previous node in brackets if it's another operator
if (calcObj.parseType === 'operator') {
calcObj.value = parenthesis.create({
content: calcObj.value
});
}
// Append all multiplications
aggregator.mul.forEach(node => {
calcObj.value = operator.create({
left: calcObj.value,
right: node,
operator: '*'
});
});
}
// Min
if (aggregator.min) {
calcObj.value = call.create({
functionName: 'max',
args: [calcObj.value, aggregator.min]
});
}
// Max
if (aggregator.max) {
calcObj.value = call.create({
functionName: 'min',
args: [calcObj.value, aggregator.max]
});
}
}
// 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(
@@ -148,53 +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 = operator.create({
left: calcObj.value,
right: constant.create({ value: calcObj.proficiencyBonus }),
operator: '+'
});
}
}
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: '+'
});
}

View File

@@ -1,5 +1,4 @@
import { has } from 'lodash';
import evaluateCalculation from '../../utility/evaluateCalculation.js';
export default function computePointBuy(computation, node) {
const prop = node.data;

View File

@@ -24,14 +24,7 @@ export default function aggregateDefinition({ node, linkedNode, link }) {
}
// Aggregate the base value due to the defining properties
let propBaseValue = undefined;
const valueNode = prop.baseValue?.value;
if (
valueNode?.parseType === 'constant'
&& valueNode?.valueType === 'number'
) {
propBaseValue = valueNode.value;
}
let propBaseValue = prop.baseValue?.value;
// Point buy rows use prop.value instead of prop.baseValue
if (prop.type === 'pointBuyRow') {
propBaseValue = prop.value;
@@ -39,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: propBaseValue,
type: 'pointBuy',
});
node.data.effectIds.push(prop.tableId);
} else {
node.data.effects.push({
_id: prop._id,
name: prop.name,
operation: 'base',
amount: propBaseValue,
type: prop.type,
});
node.data.effectIds.push(prop._id);
}
if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue) {
node.data.baseValue = propBaseValue;

View File

@@ -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,30 +17,14 @@ export default function aggregateEffect({ node, linkedNode, link }) {
rollBonus: [],
};
// Store a summary of the effect itself
node.data.effects = node.data.effects || [];
// Store either just
node.data.effects.push({
_id: linkedNode.data._id,
name: linkedNode.data.name,
operation: linkedNode.data.operation,
amount: linkedNode.data.amount.displayValue,
type: linkedNode.data.type,
text: linkedNode.data.text,
// ancestors: linkedNode.data.ancestors,
});
// 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;
// Get the result of the effect
let result = undefined;
const valueNode = linkedNode.data.amount?.value;
if (
valueNode?.parseType === 'constant'
&& valueNode?.valueType === 'number'
) {
result = valueNode.value;
}
let result = linkedNode.data.amount?.value;
if (typeof result !== 'number') result = undefined;
// Aggregate the effect based on its operation

View File

@@ -49,5 +49,5 @@ export default function computeVariableAsAttribute(computation, node, prop) {
undefined
// Store effects
prop.effects = node.data.effects;
prop.effectIds = node.data.effectIds;
}

View File

@@ -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) {

View File

@@ -3,7 +3,7 @@ import { assert } from 'chai';
import computeCreatureComputation from '../../computeCreatureComputation.js';
import clean from '../../utility/cleanProp.testFn.js';
export default function(){
export default function () {
const computation = buildComputationFromProps(testProperties);
computeCreatureComputation(computation);
const prop = id => computation.propsById[id];

View File

@@ -15,8 +15,8 @@ export default function () {
prop('strengthId').modifier, -1,
'The proficiency bonus should not change the strength modifier'
);
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(