Merge branch 'develop' into feature-nested-sets

This commit is contained in:
ThaumRystra
2023-11-11 10:01:34 +02:00
40 changed files with 562 additions and 394 deletions

View File

@@ -1,11 +1,18 @@
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs';
import {
getCreature, getVariables, getPropertiesOfType
getCreature, getVariables, getPropertiesOfType, replaceLinkedVariablesWithProps
} from '/imports/api/engine/loadCreatures';
import { groupBy, remove } from 'lodash';
export default class ActionContext {
constructor(creatureId, targetIds = [], method) {
creature: any;
log: any;
scope: any;
targets: Array<any>;
triggers: Array<any>;
method: any;
constructor(creatureId, targetIds: string[] = [], method) {
// Get the creature
this.creature = getCreature(creatureId)
@@ -20,6 +27,7 @@ export default class ActionContext {
// Get the variables of the acting creature
this.creature.variables = getVariables(creatureId);
replaceLinkedVariablesWithProps(this.creature.variables);
delete this.creature.variables._id;
delete this.creature.variables._creatureId;
// Alias as scope
@@ -52,10 +60,10 @@ export default class ActionContext {
// Group the triggers into triggers.<event>.<timing> or
// triggers.doActionProperty.<propertyType>.<timing>
this.triggers = groupBy(this.triggers, 'event');
for (let event in this.triggers) {
for (const event in this.triggers) {
if (event === 'doActionProperty') {
this.triggers[event] = groupBy(this.triggers[event], 'actionPropertyType');
for (let propertyType in this.triggers[event]) {
for (const propertyType in this.triggers[event]) {
this.triggers[event][propertyType] = groupBy(this.triggers[event][propertyType], 'timing');
}
} else {

View File

@@ -1,13 +1,12 @@
import {
setLineageOfDocs,
renewDocIds
renewDocIds,
} from '/imports/api/parenting/parentingFunctions';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex';
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey';
import { get } from 'lodash';
import resolve, { map, toString } from '/imports/parser/resolve';
import symbol from '/imports/parser/parseTree/symbol';
import accessor from '/imports/parser/parseTree/accessor';
import logErrors from './shared/logErrors';
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53';
@@ -25,7 +24,7 @@ export default function applyBuff(node, actionContext) {
// Then copy the descendants of the buff to the targets
let propList = [prop];
function addChildrenToPropList(children, { skipCrystalize } = {}) {
function addChildrenToPropList(children, { skipCrystalize } = { skipCrystalize: false }) {
children.forEach(child => {
if (skipCrystalize) child.node._skipCrystalize = true;
propList.push(child.node);
@@ -40,13 +39,20 @@ export default function applyBuff(node, actionContext) {
crystalizeVariables({ propList, actionContext });
}
let oldParent = {
id: prop.parent.id,
collection: prop.parent.collection,
};
buffTargets.forEach(target => {
const targetPropList = EJSON.clone(propList);
// Move the properties to the target by replacing the old subtree parent and root with the '
// target id
renewDocIds({
docArray: targetPropList,
idMap: {
[prop.parentId]: target._id,
[prop.root.id]: target._id,
},
collectionMap: { [prop.root.collection]: 'creatures' }
});
// Apply the buff
copyNodeListToTarget(propList, target, oldParent);
CreatureProperties.batchInsert(targetPropList);
//Log the buff
let logValue = prop.description?.value
@@ -81,25 +87,6 @@ export default function applyBuff(node, actionContext) {
// Don't apply the children of the buff, they get copied to the target instead
}
function copyNodeListToTarget(propList, target, oldParent) {
let ancestry = [{ collection: 'creatures', id: target._id }];
setLineageOfDocs({
docArray: propList,
newAncestry: ancestry,
oldParent,
});
renewDocIds({
docArray: propList,
});
/*
setDocToLastOrder({
collection: CreatureProperties,
doc: propList[0],
});
*/
CreatureProperties.batchInsert(propList);
}
/**
* Replaces all variables with their resolved values
* except variables of the form `~target.thing.total` become `thing.total`
@@ -118,7 +105,7 @@ function crystalizeVariables({ propList, actionContext }) {
calcObj.parseNode = map(calcObj.parseNode, node => {
// Skip nodes that aren't symbols or accessors
if (
node.parseType !== 'accessor' && node.parseType !== 'symbol'
node.parseType !== 'accessor'
) return node;
// Handle variables
if (node.name === '~target') {
@@ -126,7 +113,7 @@ function crystalizeVariables({ propList, actionContext }) {
if (node.parseType === 'accessor') {
node.name = node.path.shift();
if (!node.path.length) {
return symbol.create({ name: node.name })
return accessor.create({ name: node.name })
}
} else {
// Can't strip symbols

View File

@@ -3,7 +3,7 @@ import applyChildren from '/imports/api/engine/actions/applyPropertyByType/share
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs';
import resolve, { Context, toString } from '/imports/parser/resolve';
import logErrors from './shared/logErrors';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode';
import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation'
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty';
import {
getPropertiesOfType
@@ -37,8 +37,8 @@ export default function applyDamage(node, actionContext) {
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.amount, actionContext);
const { result: rolled } = resolve('roll', prop.amount.parseNode, scope, context);
recalculateCalculation(prop.amount, actionContext, undefined, 'compile');
const { result: rolled } = resolve('roll', prop.amount.valueNode, scope, context);
if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled));
}
@@ -88,8 +88,8 @@ export default function applyDamage(node, actionContext) {
let damageOnSave, saveNode, saveRoll;
if (prop.save) {
if (prop.save.damageFunction?.calculation) {
applyEffectsToCalculationParseNode(prop.save.damageFunction, actionContext);
let { result: saveDamageRolled } = resolve('roll', prop.save.damageFunction.parseNode, scope, context);
recalculateCalculation(prop.save.damageFunction, actionContext, undefined, 'compile');
let { result: saveDamageRolled } = resolve('roll', prop.save.damageFunction.valueNode, scope, context);
saveRoll = toString(saveDamageRolled);
let { result: saveDamageResult } = resolve('reduce', saveDamageRolled, scope, context);
// If we didn't end up with a constant of finite amount, give up
@@ -167,7 +167,7 @@ export default function applyDamage(node, actionContext) {
creatureId: target._id,
content: [{
name,
value: `Recieved **${damageDealt}** ${suffix}`,
value: `Received **${damageDealt}** ${suffix}`,
}],
}
});

View File

@@ -1,6 +1,6 @@
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren';
import logErrors from './shared/logErrors';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode';
import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation';
import resolve, { toString } from '/imports/parser/resolve';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers';
@@ -12,8 +12,8 @@ export default function applyRoll(node, actionContext) {
const logValue = [];
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.roll, actionContext);
const { result: rolled, context } = resolve('roll', prop.roll.parseNode, actionContext.scope);
recalculateCalculation(prop.roll, actionContext, undefined, 'compile');
const { result: rolled, context } = resolve('roll', prop.roll.valueNode, actionContext.scope);
if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled));
}

View File

@@ -1,46 +0,0 @@
import operator from '/imports/parser/parseTree/operator';
import { parse } from '/imports/parser/parser';
import logErrors from './logErrors';
export default function applyEffectsToCalculationParseNode(calcObj, actionContext) {
calcObj.effects?.forEach(effect => {
if (effect.operation !== 'add') return;
if (!effect.amount) return;
if (effect.amount.value === null) return;
let effectParseNode;
try {
effectParseNode = parse(effect.amount.value.toString());
calcObj.parseNode = operator.create({
left: calcObj.parseNode,
right: effectParseNode,
operator: '+',
fn: 'add'
});
} catch (e) {
logErrors([e], actionContext)
}
});
// Add the highest proficiency as well
let highestProficiency;
calcObj.proficiencies?.forEach(proficiency => {
if (
proficiency.value > highestProficiency
|| (highestProficiency === undefined && Number.isFinite(proficiency.value))
) {
highestProficiency = proficiency.value;
}
});
if (highestProficiency) {
try {
let profParseNode = parse(highestProficiency.toString());
calcObj.parseNode = operator.create({
left: calcObj.parseNode,
right: profParseNode,
operator: '+',
fn: 'add'
});
} catch (e) {
logErrors([e], actionContext)
}
}
}

View File

@@ -1,11 +1,59 @@
import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode';
import logErrors from './logErrors';
import { toPrimitiveOrString } from '/imports/parser/resolve';
import {
aggregateCalculationEffects,
aggregateCalculationProficiencies,
resolveCalculationNode,
} from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation';
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
import resolve from '/imports/parser/resolve';
export default function recalculateCalculation(calc, actionContext, context) {
if (!calc?.parseNode) return;
calc._parseLevel = 'reduce';
applyEffectsToCalculationParseNode(calc, actionContext);
evaluateCalculation(calc, actionContext.scope, context);
logErrors(calc.errors, actionContext);
// Redo the work of imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js
// But in the action scope
export default function recalculateCalculation(calcObj, actionContext, context, parseLevel = 'reduce') {
if (!calcObj?.parseNode) return;
calcObj._parseLevel = parseLevel;
// Re-resolve the parse node
resolveCalculationNode(calcObj, calcObj.parseNode, actionContext.scope, context);
// store the unaffected value
if (calcObj.effectIds || calcObj.proficiencyIds) {
calcObj.unaffected = toPrimitiveOrString(calcObj.valueNode);
}
// Apply all the effects and proficiencies
aggregateCalculationEffects(
calcObj,
id => getSingleProperty(actionContext.creature._id, id)
);
aggregateCalculationProficiencies(
calcObj,
id => getSingleProperty(actionContext.creature._id, id),
actionContext.scope['proficiencyBonus']?.value || 0
);
// Resolve the modified valueNode
resolveCalculationNode(calcObj, calcObj.valueNode, actionContext.scope, context);
// Store the primitive value
calcObj.value = toPrimitiveOrString(calcObj.valueNode);
logErrors(calcObj.errors, actionContext);
}
export function rollAndReduceCalculation(calcObj, actionContext, context) {
// Compile
recalculateCalculation(calcObj, actionContext, context, 'compile');
const compiled = calcObj.valueNode;
const compileErrors = context.errors;
// Roll
context.errors = [];
const { result: rolled } = resolve('roll', calcObj.valueNode, actionContext.scope, context);
const rollErrors = context.errors;
// Reduce
context.errors = [];
const { result: reduced } = resolve('reduce', rolled, actionContext.scope, context);
const reduceErrors = context.errors;
// Return
return { compiled, compileErrors, rolled, rollErrors, reduced, reduceErrors };
}

View File

@@ -39,7 +39,7 @@ export function applyTrigger(trigger, prop, actionContext) {
// Prevent triggers from firing if their condition is false
if (trigger.condition?.parseNode) {
recalculateCalculation(trigger.condition, actionContext);
if (!trigger.condition.value) return;
if (!trigger.condition.value?.value) return;
}
// Prevent triggers from firing themselves in a loop

View File

@@ -7,7 +7,7 @@ import rollDice from '/imports/parser/rollDice';
import numberToSignedString from '/imports/api/utility/numberToSignedString';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers';
import ActionContext from '/imports/api/engine/actions/ActionContext';
import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation';
import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation';
const doCheck = new ValidatedMethod({
name: 'creatureProperties.doCheck',
@@ -77,7 +77,7 @@ function rollCheck(prop, actionContext) {
let rollModifierText = numberToSignedString(rollModifier, true);
const { effectBonus, effectString } = applyUnresolvedEffects(prop, scope)
const { effectBonus, effectString } = applyUnresolvedEffects(prop, actionContext)
rollModifierText += effectString;
rollModifier += effectBonus;
@@ -117,7 +117,8 @@ function rollCheck(prop, actionContext) {
});
}
export function applyUnresolvedEffects(prop, scope) {
// TODO replace this with recalculating and then rolling/reducing the value node
export function applyUnresolvedEffects(prop, actionContext) {
let effectBonus = 0;
let effectString = '';
if (!prop.effects) {
@@ -126,8 +127,7 @@ export function applyUnresolvedEffects(prop, scope) {
prop.effects.forEach(effect => {
if (!effect.amount?.parseNode) return;
if (effect.operation !== 'add') return;
effect.amount._parseLevel = 'reduce';
evaluateCalculation(effect.amount, scope);
recalculateCalculation(effect.amount, actionContext, context, 'reduce');
if (typeof effect.amount?.value !== 'number') return;
effectBonus += effect.amount.value;
effectString += ` ${effect.amount.value < 0 ? '-' : '+'} [${effect.amount.calculation}] ${Math.abs(effect.amount.value)}`

View File

@@ -12,8 +12,8 @@ export default function linkCalculationDependencies(dependencyGraph, prop, { pro
// Skip empty calculations that aren't targeted by anything
if (
!calcObj.calculation
&& !calcObj.effects
&& !calcObj.proficiencies
&& !calcObj.effectIds
&& !calcObj.proficiencyIds
) return;
dependencyGraph.addNode(calcNodeId, calcObj);

View File

@@ -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: '+'
});
}

View File

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

View File

@@ -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;

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,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;

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

@@ -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(

View File

@@ -1,19 +0,0 @@
import resolve, { toString } from '/imports/parser/resolve';
export default function evaluateCalculation(calculation, scope, givenContext) {
const parseNode = calculation.parseNode;
const fn = calculation._parseLevel;
const calculationScope = { ...calculation._localScope, ...scope };
const { result: resultNode, context } = resolve(fn, parseNode, calculationScope, givenContext);
calculation.errors = context.errors;
if (resultNode?.parseType === 'constant') {
calculation.value = resultNode.value;
} else if (resultNode?.parseType === 'error') {
calculation.value = null;
} else {
calculation.value = toString(resultNode);
}
// remove the working fields
delete calculation._parseLevel;
delete calculation._localScope;
}

View File

@@ -19,6 +19,12 @@ export default function writeScope(creatureId, computation) {
// Mongo can't handle keys that start with a dollar sign
if (key[0] === '$' || key[0] === '_') continue;
// Remove empty objects
if (Object.keys(scope[key]).length === 0) {
delete scope[key];
continue;
}
// Remove large properties that aren't likely to be accessed
delete scope[key].parent;
@@ -29,6 +35,11 @@ export default function writeScope(creatureId, computation) {
}
}
// If this is a creature property, replace the property with a link
if (scope[key]._id && scope[key].type) {
scope[key] = { _propId: scope[key]._id };
}
// Only update changed fields
if (!EJSON.equals(variables[key], scope[key])) {
if (!$set) $set = {};

View File

@@ -114,6 +114,14 @@ export function getVariables(creatureId) {
return variables;
}
export function replaceLinkedVariablesWithProps(variables) {
for (const key in variables) {
const propId = variables[key]?._propId;
if (!propId) continue;
variables[key] = getSingleProperty(variables._creatureId, propId);
}
}
export function getPropertyAncestors(creatureId: string, propertyId: string) {
const prop = getSingleProperty(creatureId, propertyId);
if (!prop) return [];