Made constants work in calculations performed after recomputation

This commit is contained in:
Stefan Zermatten
2021-03-01 13:27:48 +02:00
parent 1276f872a0
commit a97be2f93a
8 changed files with 177 additions and 113 deletions

View File

@@ -13,42 +13,43 @@ export default function applyAdjustment({
...creature.variables, ...creature.variables,
...actionContext, ...actionContext,
}; };
try { var {result, context} = evaluateString({
var {result, errors} = evaluateString(prop.amount, scope, 'reduce'); string: prop.amount,
if (typeof result !== 'number') { scope,
log.content.push({ fn: 'reduce'
name: 'Attribute damage', });
error: errors.join(', ') || 'Something went wrong', context.errors.forEach(e => {
});
}
} catch (e){
log.content.push({ log.content.push({
name: 'Attribute damage', name: 'Attribute damage error',
error: e.toString(), error: e.message || e.toString(),
}); });
} });
if (damageTargets) { if (damageTargets) {
damageTargets.forEach(target => { damageTargets.forEach(target => {
if (prop.target === 'each'){ if (prop.target === 'each'){
result = evaluateString(prop.amount, scope, 'reduce'); ({result} = evaluateString({
string: prop.amount,
scope,
fn: 'reduce'
}));
} }
damagePropertiesByName.call({ damagePropertiesByName.call({
creatureId: target._id, creatureId: target._id,
variableName: prop.stat, variableName: prop.stat,
operation: prop.operation || 'increment', operation: prop.operation || 'increment',
value: result value: result.value,
}); });
log.content.push({ log.content.push({
name: 'Attribute damage', name: 'Attribute damage',
resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`, resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`,
result: `${-result}`, result: `${result.isNumber ? -result.value : result.toString()}`,
}); });
}); });
} else { } else {
log.content.push({ log.content.push({
name: 'Attribute damage', name: 'Attribute damage',
resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`, resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`,
result: `${-result}`, result: `${result.isNumber ? -result.value : result.toString()}`,
}); });
} }
} }

View File

@@ -15,49 +15,79 @@ export default function applyDamage({
...creature.variables, ...creature.variables,
...actionContext, ...actionContext,
}; };
// Add the target's variables to the scope
if (targets.length === 1){ if (targets.length === 1){
scope.target = targets[0].variables; scope.target = targets[0].variables;
} }
// Determine if the hit is critical
let criticalHit = !!( let criticalHit = !!(
actionContext.criticalHit && actionContext.criticalHit &&
actionContext.criticalHit.value && actionContext.criticalHit.value &&
prop.damageType !== 'healing' // Can't critically heal prop.damageType !== 'healing' // Can't critically heal
); );
// Double the damage rolls if the hit is critical
let context = new CompilationContext({ let context = new CompilationContext({
doubleRolls: criticalHit, doubleRolls: criticalHit,
}); });
try {
var {result, errors} = evaluateString(prop.amount, scope, 'reduce', context); // Compute the roll the first time, logging any errors
if (typeof result !== 'number') { var {result} = evaluateString({
log.content.push({ string: prop.amount,
error: errors.join(', '), scope,
}); fn: 'reduce',
} context
} catch (e){ });
// If the result is an error bail out now
if (result.constructor.name === 'ErrorNode'){
log.content.push({ log.content.push({
error: e.toString(), name: 'Damage error',
error: result.toString(),
}); });
return;
} }
// Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : '') + let suffix = (criticalHit ? ' critical ' : '') +
prop.damageType + prop.damageType +
(prop.damageType !== 'healing' ? ' damage': ''); (prop.damageType !== 'healing' ? ' damage': '');
if (damageTargets && damageTargets.length) { if (damageTargets && damageTargets.length) {
// Iterate through all the targets
damageTargets.forEach(target => { damageTargets.forEach(target => {
let name = prop.damageType === 'healing' ? 'Healing' : 'Damage'; let name = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// Reroll the damage if needed
if (prop.target === 'each'){ if (prop.target === 'each'){
result = evaluateString(prop.amount, scope, 'reduce'); ({result, context} = evaluateString({
string: prop.amount,
scope,
fn: 'reduce'
}));
} }
// If the result is an error or not a number bail out now
if (result.constructor.name === 'ErrorNode' || !result.isNumber){
log.content.push({
name: 'Damage error',
error: result.toString(),
});
return;
}
// Deal the damage to the target
let damageDealt = dealDamage.call({ let damageDealt = dealDamage.call({
creatureId: target._id, creatureId: target._id,
damageType: prop.damageType, damageType: prop.damageType,
amount: result, amount: result.value,
}); });
// Log the damage done
if (target._id === creature._id){ if (target._id === creature._id){
// Target is same as self, log damage as such
log.content.push({ log.content.push({
name, name,
result: damageDealt, result: damageDealt,
details: suffix + 'to self', details: suffix + ' to self',
}); });
} else { } else {
log.content.push({ log.content.push({
@@ -66,6 +96,7 @@ export default function applyDamage({
result: damageDealt, result: damageDealt,
details: suffix + `${target.name && ' to '}${target.name}`, details: suffix + `${target.name && ' to '}${target.name}`,
}); });
// Log the damage received on that creature's log as well
insertCreatureLog.call({ insertCreatureLog.call({
log: { log: {
content: [{ content: [{
@@ -80,9 +111,10 @@ export default function applyDamage({
} }
}); });
} else { } else {
// There are no targets, just log the result
log.content.push({ log.content.push({
name: prop.damageType === 'healing' ? 'Healing' : 'Damage', name: prop.damageType === 'healing' ? 'Healing' : 'Damage',
result, result: result.toString(),
details: suffix, details: suffix,
}); });
} }

View File

@@ -10,23 +10,17 @@ export default function applyRoll({
...creature.variables, ...creature.variables,
...actionContext, ...actionContext,
}; };
try { var {result} = evaluateString({
var {result, errors} = evaluateString(prop.roll, scope, 'reduce'); string: prop.roll,
actionContext[prop.variableName] = result; scope,
log.content.push({ fn: 'reduce'
name: prop.name, });
resultPrefix: prop.variableName + ' = ' + prop.roll + ' = ', if (result.isNumber){
result, actionContext[prop.variableName] = result.value;
});
if (errors.length) {
log.content.push({
error: errors.join(', '),
});
}
} catch (e){
log.content.push({
error: e.toString(),
});
} }
log.content.push({
name: prop.name,
resultPrefix: prop.variableName + ' = ' + prop.roll + ' = ',
result: result.toString(),
});
} }

View File

@@ -14,19 +14,17 @@ export default function applySave({
}; };
try { try {
// Calculate the DC // Calculate the DC
var {result, errors} = evaluateString(prop.dc, scope, 'reduce'); var {result} = evaluateString({
let dc = result; string: prop.dc,
scope,
fn: 'reduce'
});
let dc = result.value;
log.content.push({ log.content.push({
name: prop.name, name: prop.name,
resultPrefix: ' DC ', resultPrefix: ' DC ',
result, result: result.toString(),
}); });
if (errors.length) {
log.content.push({
error: errors.join(', '),
});
return false;
}
if (prop.target === 'self'){ if (prop.target === 'self'){
let save = CreaturesProperties.findOne({ let save = CreaturesProperties.findOne({
'ancestors.id': creature._id, 'ancestors.id': creature._id,

View File

@@ -13,23 +13,22 @@ export default function applyToggle({
if (Number.isFinite(+prop.condition)){ if (Number.isFinite(+prop.condition)){
return !!+prop.condition; return !!+prop.condition;
} }
try { var {result} = evaluateString({
var {result, errors} = evaluateString(prop.condition, scope, 'reduce'); string: prop.condition,
if (typeof result !== 'number' && typeof result !== 'boolean') { scope,
log.content.push({ fn: 'reduce'
error: errors.join(', '), });
}); if (result.constructor.name === 'ErrorNode') {
return false;
}
log.content.push({ log.content.push({
name: prop.name, name: 'Toggle error',
resultPrefix: prop.condition + ' = ', error: result.toString(),
result,
});
return !!result;
} catch (e){
log.content.push({
error: e.toString(),
}); });
return false;
} }
log.content.push({
name: prop.name || 'Toggle',
resultPrefix: prop.condition + ' = ',
result: result.toString(),
});
return !!result.value;
} }

View File

@@ -1,32 +1,67 @@
import { parse, CompilationContext } from '/imports/parser/parser.js'; import { parse, CompilationContext } from '/imports/parser/parser.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
//TODO replace constants with their parsed node //TODO replace constants with their parsed node
export default function evaluateString(string, scope, fn = 'compile', context){ export default function evaluateString({string, scope, fn = 'compile', context}){
let errors = []; if (!context){
context = new CompilationContext({});
}
if (!string){ if (!string){
errors.push('No string provided'); context.storeError('No string provided');
return {result: string, errors}; return {result: {value: string}, context};
} }
if (!scope) errors.push('No scope provided'); if (!scope) context.storeError('No scope provided');
// Parse the string using mathjs // Parse the string using mathjs
let node; let node;
try { try {
node = parse(string); node = parse(string);
} catch (e) { } catch (e) {
errors.push(e); context.storeError(e);
return {result: string, errors}; return {result: {value: string}, context};
}
if (!context){
context = new CompilationContext({});
} }
node = replaceConstants({calc: node, context, scope});
let result = node[fn](scope, context); let result = node[fn](scope, context);
if (result instanceof ConstantNode){ return {result, context};
return {result: result.value, errors: context.errors} }
} else {
return {result: result.toString(), errors: context.errors}; // Replace constants in the calc with the right ParseNodes
} function replaceConstants({calc, context, scope}){
let constFailed = [];
calc = calc.replaceNodes(node => {
if (!(node instanceof SymbolNode)) return;
let constant = scope[node.name];
// replace constants that aren't overridden by stats or disabled by a toggle
if (constant && constant.type === 'constant'){
// Fail if the constant has errors
if (constant.errors && constant.errors.length){
constFailed.push(node.name);
return;
}
let parsedConstantNode;
try {
parsedConstantNode = parse(constant.calculation);
} catch(e){
constFailed.push(node.name);
return;
}
if (!parsedConstantNode) constFailed.push(node.name);
return parsedConstantNode;
}
});
constFailed.forEach(name => {
context.storeError({
type: 'error',
message: `${name} is a constant property with parsing errors`
});
});
let failed = !!constFailed.length;
if (failed){
calc = new ErrorNode({error: 'Failed to replace constants'});
}
return calc;
} }

View File

@@ -4,28 +4,30 @@ import VERSION from '/imports/constants/VERSION.js';
export default function writeCreatureVariables(memo, creatureId, fullRecompute = true) { export default function writeCreatureVariables(memo, creatureId, fullRecompute = true) {
const fields = [ const fields = [
'name',
'attributeType',
'baseValue',
'spellSlotLevelValue',
'damage',
'decimal',
'reset',
'resetMultiplier',
'value',
'currentValue',
'modifier',
'ability', 'ability',
'skillType',
'baseProficiency',
'abilityMod', 'abilityMod',
'advantage', 'advantage',
'passiveBonus', 'attributeType',
'proficiency', 'baseProficiency',
'baseValue',
'calculation',
'conditionalBenefits', 'conditionalBenefits',
'rollBonuses', 'currentValue',
'damage',
'decimal',
'fail', 'fail',
'level', 'level',
'modifier',
'name',
'passiveBonus',
'proficiency',
'reset',
'resetMultiplier',
'rollBonuses',
'skillType',
'spellSlotLevelValue',
'type',
'value',
]; ];
if (fullRecompute){ if (fullRecompute){
@@ -34,6 +36,12 @@ export default function writeCreatureVariables(memo, creatureId, fullRecompute =
let condensedStat = pick(stat, fields); let condensedStat = pick(stat, fields);
memo.creatureVariables[variableName] = condensedStat; memo.creatureVariables[variableName] = condensedStat;
}); });
forOwn(memo.constantsByVariableName, (stat, variableName) => {
let condensedStat = pick(stat, fields);
if (!memo.creatureVariables[variableName]){
memo.creatureVariables[variableName] = condensedStat;
}
});
Creatures.update(creatureId, {$set: { Creatures.update(creatureId, {$set: {
variables: memo.creatureVariables, variables: memo.creatureVariables,
computeVersion: VERSION, computeVersion: VERSION,

View File

@@ -143,10 +143,10 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue'; import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js'; import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import { parse, CompilationContext } from '/imports/parser/parser.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js'; import PROPERTIES from '/imports/constants/PROPERTIES.js';
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue'; import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue' import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue'
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
export default { export default {
components: { components: {
@@ -243,15 +243,12 @@ export default {
// the quantity to fill // the quantity to fill
nodes = nodes.filter(node => { nodes = nodes.filter(node => {
if (node.slotFillerCondition){ if (node.slotFillerCondition){
let context = new CompilationContext(); let {result} = evaluateString({
let conditionResult; string: node.slotFillerCondition,
try { scope: this.creature.variables,
conditionResult = parse(node.slotFillerCondition) fn: 'reduce',
.reduce(this.creature.variables, context); });
} catch (e){ if (!result.value) return false;
console.warn(e);
}
if (conditionResult && !conditionResult.value) return false;
} }
if ( if (
node.type === 'slotFiller' && node.type === 'slotFiller' &&