Merge branch 'develop' into feature-nested-sets
This commit is contained in:
@@ -11,7 +11,7 @@ accounts-google@1.4.0
|
||||
email@2.2.5
|
||||
meteor-base@1.5.1
|
||||
mobile-experience@1.1.0
|
||||
mongo@1.16.6
|
||||
mongo@1.16.7
|
||||
session@1.2.1
|
||||
tracker@1.3.2
|
||||
logging@1.3.2
|
||||
|
||||
@@ -1 +1 @@
|
||||
METEOR@2.12
|
||||
METEOR@2.13.3
|
||||
|
||||
@@ -30,7 +30,7 @@ ddp@1.4.1
|
||||
ddp-client@2.6.1
|
||||
ddp-common@1.4.0
|
||||
ddp-rate-limiter@1.2.0
|
||||
ddp-server@2.6.1
|
||||
ddp-server@2.6.2
|
||||
diff-sequence@1.1.2
|
||||
dynamic-import@0.7.3
|
||||
ecmascript@0.16.7
|
||||
@@ -42,7 +42,7 @@ email@2.2.5
|
||||
es5-shim@4.8.0
|
||||
fetch@0.1.3
|
||||
geojson-utils@1.0.11
|
||||
google-oauth@1.4.3
|
||||
google-oauth@1.4.4
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.1.3
|
||||
htmljs@1.1.1
|
||||
@@ -55,7 +55,7 @@ littledata:synced-cron@1.5.1
|
||||
localstorage@1.2.0
|
||||
logging@1.3.2
|
||||
mdg:validated-method@1.3.0
|
||||
meteor@1.11.2
|
||||
meteor@1.11.3
|
||||
meteor-base@1.5.1
|
||||
meteortesting:browser-tests@1.4.2
|
||||
meteortesting:mocha@2.1.0
|
||||
@@ -70,7 +70,7 @@ mobile-status-bar@1.1.0
|
||||
modern-browsers@0.1.9
|
||||
modules@0.19.0
|
||||
modules-runtime@0.13.1
|
||||
mongo@1.16.6
|
||||
mongo@1.16.7
|
||||
mongo-decimal@0.1.3
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.8
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
|
||||
|
||||
//set up the collection for creature variables
|
||||
let CreatureVariables = new Mongo.Collection('creatureVariables');
|
||||
|
||||
@@ -7,15 +9,29 @@ if (Meteor.isServer) {
|
||||
}
|
||||
|
||||
/** No schema because the structure isn't known until compute time
|
||||
* Expect documents to looke like:
|
||||
* Expect documents to look like:
|
||||
* {
|
||||
* _id: "nE8Ngd6K4L4jSxLY2",
|
||||
* _creatureId: "nE8Ngd6K4L4jSxLY2", // indexed reference to the creature
|
||||
* explicitlyDefinedVariableName: {...some creatureProperty}
|
||||
* explicitlyDefinedVariableName: {...some creatureProperty},
|
||||
* // Must be found in CreatureProperties before using:
|
||||
* linkedProperty: { _propId: "nE8Ngd6K1234SxLY2" }
|
||||
* implicitVariableName: {value: 10},
|
||||
* undefinedVariableName: {},
|
||||
* }
|
||||
* Where top level fields that don't start with `_` are variables on the sheet
|
||||
**/
|
||||
|
||||
/**
|
||||
* Get the property from the given scope, respecting properties that are just a link to the actual
|
||||
* property document
|
||||
*/
|
||||
export function getFromScope(name, scope) {
|
||||
let value = scope?.[name];
|
||||
if (value?._propId) {
|
||||
value = getSingleProperty(scope._creatureId, value._propId);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export default CreatureVariables;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`,
|
||||
}],
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}`
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
@@ -199,14 +199,21 @@ let ComputedOnlyAttributeSchema = createPropertySchema({
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
// A list of effect ids targeting this attribute
|
||||
effects: {
|
||||
'effectIds': {
|
||||
type: Array,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
'effects.$': {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
'effectIds.$': {
|
||||
type: String,
|
||||
},
|
||||
'proficiencyIds': {
|
||||
type: Array,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
'proficiencyIds.$': {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -134,14 +134,21 @@ let ComputedOnlySkillSchema = createPropertySchema({
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
// A list of effect ids targeting this skill
|
||||
effects: {
|
||||
'effectIds': {
|
||||
type: Array,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
'effects.$': {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
'effectIds.$': {
|
||||
type: String,
|
||||
},
|
||||
'proficiencyIds': {
|
||||
type: Array,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
'proficiencyIds.$': {
|
||||
type: String,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -29,20 +29,40 @@ function fieldToCompute(field) {
|
||||
|
||||
function computedOnlyField(field) {
|
||||
const schemaObj = {
|
||||
// The value (or calculation string) before any effects/proficiencies are applied or rolls made
|
||||
[`${field}.unaffected`]: {
|
||||
type: SimpleSchema.oneOf(String, Number),
|
||||
optional: true,
|
||||
blackbox: true,
|
||||
},
|
||||
// The value (or calculation string) after applying all effects
|
||||
[`${field}.value`]: {
|
||||
type: SimpleSchema.oneOf(String, Number),
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
blackbox: true,
|
||||
},
|
||||
// A list of effects targeting this calculation
|
||||
[`${field}.effects`]: {
|
||||
// The value as a parse node, after applying all effects
|
||||
[`${field}.valueNode`]: {
|
||||
type: SimpleSchema.oneOf(String, Number),
|
||||
optional: true,
|
||||
blackbox: true,
|
||||
},
|
||||
// A list of effect Ids targeting this calculation
|
||||
[`${field}.effectIds`]: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
[`${field}.effects.$`]: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
[`${field}.effectIds.$`]: {
|
||||
type: String,
|
||||
},
|
||||
[`${field}.proficiencyIds`]: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
[`${field}.proficiencyIds.$`]: {
|
||||
type: String,
|
||||
},
|
||||
// A cache of the parse result of the calculation
|
||||
[`${field}.parseNode`]: {
|
||||
@@ -69,6 +89,30 @@ function computedOnlyField(field) {
|
||||
[`${field}.errors.$`]: {
|
||||
type: ErrorSchema,
|
||||
},
|
||||
// Effect aggregations
|
||||
[`${field}.advantage`]: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
[`${field}.disadvantage`]: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
[`${field}.fail`]: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
[`${field}.conditional`]: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
[`${field}.conditional.$`]: {
|
||||
type: String,
|
||||
},
|
||||
}
|
||||
includeParentFields(field, schemaObj);
|
||||
return new SimpleSchema(schemaObj);
|
||||
|
||||
@@ -85,21 +85,15 @@
|
||||
<script lang="js">
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES';
|
||||
import numberToSignedString from '/imports/api/utility/numberToSignedString';
|
||||
import AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
|
||||
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
|
||||
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
||||
import MarkdownText from '/imports/client/ui/components/MarkdownText.vue';
|
||||
import TreeNodeList from '/imports/client/ui/components/tree/TreeNodeList.vue';
|
||||
import { docsToForest as nodeArrayToTree } from '/imports/api/parenting/parentingFunctions';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
import { some } from 'lodash';
|
||||
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode';
|
||||
import resolve, { Context, toString } from '/imports/parser/resolve';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AttributeConsumedView,
|
||||
ItemConsumedView,
|
||||
MarkdownText,
|
||||
PropertyIcon,
|
||||
TreeNodeList,
|
||||
@@ -167,16 +161,15 @@ export default {
|
||||
if (this.children[0].children[0]?.node?.type !== 'damage') return;
|
||||
if (this.children[0].children[0].children?.length !== 0) return;
|
||||
const damage = this.children[0].children[0]?.node;
|
||||
applyEffectsToCalculationParseNode(damage.amount);
|
||||
const { result } = resolve('compile', damage.amount.parseNode, {});
|
||||
return {
|
||||
damage: toString(result),
|
||||
damage: damage.value,
|
||||
suffix: damage.damageType + (damage.damageType !== 'healing' ? ' damage ' : '')
|
||||
};
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
children() {
|
||||
throw 'TODO: rewrite with nested sets'
|
||||
const indicesOfTerminatingProps = [];
|
||||
const decendants = CreatureProperties.find({
|
||||
'ancestors.id': this.model._id,
|
||||
|
||||
@@ -91,7 +91,11 @@ export default {
|
||||
name: 'Vibes',
|
||||
title: 'Kell of Nothing',
|
||||
avatar: 'vibes'
|
||||
}
|
||||
}, {
|
||||
name: 'ßlue',
|
||||
title: 'Embodiment of Greed',
|
||||
avatar: 'blue'
|
||||
},
|
||||
],
|
||||
}},
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<script lang="js">
|
||||
import propertyFormMixin from '/imports/client/ui/properties/forms/shared/propertyFormMixin';
|
||||
import CalculationErrorList from '/imports/client/ui/properties/forms/shared/CalculationErrorList.vue';
|
||||
import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation';
|
||||
import { resolveCalculationNode } from '/imports/api/engine/computation/computeComputation/computeByType/computeCalculation';
|
||||
import { Tracker } from 'meteor/tracker'
|
||||
|
||||
export default {
|
||||
@@ -120,7 +120,7 @@ export default {
|
||||
const costFunction = EJSON.clone(row.cost || this.model.cost);
|
||||
if (!costFunction?.parseNode) return;
|
||||
if (costFunction) costFunction.parseLevel = 'reduce';
|
||||
evaluateCalculation(costFunction, { value });
|
||||
resolveCalculationNode(costFunction, costFunction.parseNode, { value });
|
||||
if (Number.isFinite(costFunction.value)) {
|
||||
newSpent += costFunction.value;
|
||||
if (this.useEstimate) this.estimatedCost = newSpent;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Generated automatically by nearley, version 2.16.0
|
||||
// Generated automatically by nearley, version 2.20.1
|
||||
// http://github.com/Hardmath123/nearley
|
||||
function id(x) { return x[0]; }
|
||||
|
||||
@@ -127,7 +127,7 @@ let ParserRules = [
|
||||
{ "name": "valueExpression", "symbols": ["string"], "postprocess": id },
|
||||
{ "name": "valueExpression", "symbols": ["boolean"], "postprocess": id },
|
||||
{ "name": "number", "symbols": [(lexer.has("number") ? { type: "number" } : number)], "postprocess": d => node.constant.create({ value: +d[0].value }) },
|
||||
{ "name": "name", "symbols": [(lexer.has("name") ? { type: "name" } : name)], "postprocess": d => node.symbol.create({ name: d[0].value }) },
|
||||
{ "name": "name", "symbols": [(lexer.has("name") ? { type: "name" } : name)], "postprocess": d => node.accessor.create({ name: d[0].value }) },
|
||||
{ "name": "string", "symbols": [(lexer.has("string") ? { type: "string" } : string)], "postprocess": d => node.constant.create({ value: d[0].value }) },
|
||||
{ "name": "boolean", "symbols": [{ "literal": "true" }], "postprocess": d => node.constant.create({ value: true }) },
|
||||
{ "name": "boolean", "symbols": [{ "literal": "false" }], "postprocess": d => node.constant.create({ value: false }) },
|
||||
|
||||
@@ -159,7 +159,7 @@ number ->
|
||||
%number {% d => node.constant.create({value: +d[0].value}) %}
|
||||
|
||||
name ->
|
||||
%name {% d => node.symbol.create({name: d[0].value}) %}
|
||||
%name {% d => node.accessor.create({name: d[0].value}) %}
|
||||
|
||||
string ->
|
||||
%string {% d => node.constant.create({value: d[0].value}) %}
|
||||
|
||||
@@ -10,7 +10,6 @@ import operator from './operator';
|
||||
import parenthesis from './parenthesis';
|
||||
import roll from './roll';
|
||||
import rollArray from './rollArray';
|
||||
import symbol from './symbol';
|
||||
import unaryOperator from './unaryOperator';
|
||||
|
||||
export default Object.freeze({
|
||||
@@ -26,6 +25,7 @@ export default Object.freeze({
|
||||
parenthesis,
|
||||
roll,
|
||||
rollArray,
|
||||
symbol,
|
||||
// What used to be symbols are now just treated as accessors without a path
|
||||
symbol: accessor,
|
||||
unaryOperator,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import constant from './constant';
|
||||
// import array from './array';
|
||||
import { toString } from '../resolve';
|
||||
import array from './array';
|
||||
import resolve from '../resolve';
|
||||
import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables';
|
||||
|
||||
const accessor = {
|
||||
create({ name, path }) {
|
||||
@@ -11,62 +12,75 @@ const accessor = {
|
||||
};
|
||||
},
|
||||
compile(node, scope, context) {
|
||||
let value = scope && scope[node.name];
|
||||
// For objects, get their value
|
||||
node.path.forEach(name => {
|
||||
let value = getFromScope(node.name, scope);
|
||||
// Get the value from the given path
|
||||
node.path?.forEach(name => {
|
||||
if (value === undefined) return;
|
||||
value = value[name];
|
||||
});
|
||||
let valueType = Array.isArray(value) ? 'array' : typeof value;
|
||||
// If the accessor returns an objet, get the object's value instead
|
||||
let valueType = getType(value);
|
||||
// If the accessor returns an object, get the object's value instead
|
||||
while (valueType === 'object') {
|
||||
value = value.value;
|
||||
valueType = Array.isArray(value) ? 'array' : typeof value;
|
||||
// Prefer the valueNode over the value
|
||||
if (value.valueNode) {
|
||||
value = value.valueNode;
|
||||
} else {
|
||||
value = value.value;
|
||||
}
|
||||
valueType = getType(value);
|
||||
}
|
||||
// Return a parse node based on the type returned
|
||||
// Return a discovered parse node
|
||||
if (valueType === 'parseNode') {
|
||||
return {
|
||||
result: value,
|
||||
context,
|
||||
};
|
||||
}
|
||||
// Return a parse node based on the constant type returned
|
||||
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') {
|
||||
return {
|
||||
result: constant.create({ value }),
|
||||
context,
|
||||
};
|
||||
}
|
||||
// Return a parser array
|
||||
if (valueType === 'array') {
|
||||
// If the first value is a parse node, assume all the values are
|
||||
if (getType(value[0]) === 'parseNode') {
|
||||
return {
|
||||
result: array.create({
|
||||
values: value,
|
||||
}),
|
||||
context,
|
||||
};
|
||||
}
|
||||
// Create the array from js primitives instead
|
||||
return {
|
||||
result: array.fromConstantArray(value),
|
||||
context,
|
||||
};
|
||||
}
|
||||
if (valueType === 'undefined') {
|
||||
// Undefined defaults to zero
|
||||
return {
|
||||
result: constant.create({
|
||||
value,
|
||||
valueType
|
||||
value: 0,
|
||||
}),
|
||||
context,
|
||||
};
|
||||
}
|
||||
/* Can't access #object.tags until this is fixed
|
||||
* If we activate this, the array node expects values to be an array of
|
||||
* parse nodes, so it will break unless the values are coerced here or at
|
||||
* in the array node's code to be parse nodes, not raw js
|
||||
else if (valueType === 'array') {
|
||||
return {
|
||||
result: array.create({
|
||||
values: value,
|
||||
}),
|
||||
context,
|
||||
};
|
||||
}
|
||||
*/
|
||||
else if (valueType === 'undefined') {
|
||||
return {
|
||||
result: accessor.create({
|
||||
name: node.name,
|
||||
path: node.path,
|
||||
}),
|
||||
context,
|
||||
};
|
||||
} else {
|
||||
context.error(`Accessing ${accessor.toString(node)} is not supported yet`);
|
||||
return {
|
||||
result: accessor.create({
|
||||
name: node.name,
|
||||
path: node.path,
|
||||
}),
|
||||
context,
|
||||
context
|
||||
};
|
||||
}
|
||||
context.error(`Accessing ${accessor.toString(node)} is not supported yet`);
|
||||
return {
|
||||
result: accessor.create({
|
||||
name: node.name,
|
||||
path: node.path,
|
||||
}),
|
||||
context,
|
||||
};
|
||||
},
|
||||
reduce(node, scope, context) {
|
||||
let { result } = accessor.compile(node, scope, context);
|
||||
({ result } = resolve('reduce', result, scope, context));
|
||||
if (result.parseType === 'accessor') {
|
||||
return {
|
||||
result: constant.create({
|
||||
@@ -79,8 +93,16 @@ const accessor = {
|
||||
}
|
||||
},
|
||||
toString(node) {
|
||||
if (!node.path) return `${node.name}`;
|
||||
return `${node.name}.${node.path.join('.')}`;
|
||||
}
|
||||
}
|
||||
|
||||
function getType(val) {
|
||||
if (!val) return typeof val;
|
||||
if (Array.isArray(val)) return 'array';
|
||||
if (val.parseType) return 'parseNode';
|
||||
return typeof val;
|
||||
}
|
||||
|
||||
export default accessor;
|
||||
|
||||
@@ -19,7 +19,9 @@ const array = {
|
||||
) {
|
||||
return constant.create({ value, valueType });
|
||||
} else {
|
||||
throw `Unexpected type in constant array: ${valueType}`
|
||||
// Gracefully create an empty spot in the array for unsupported types
|
||||
return undefined;
|
||||
// throw `Unexpected type in constant array: ${valueType}`
|
||||
}
|
||||
});
|
||||
return array.create({ values });
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
const error = {
|
||||
create({node, error}) {
|
||||
create({ node, error }) {
|
||||
return {
|
||||
parseType: 'error',
|
||||
node,
|
||||
error,
|
||||
}
|
||||
},
|
||||
compile(node, scope, context){
|
||||
return {result: node, context};
|
||||
compile(node, scope, context) {
|
||||
return { result: node, context };
|
||||
},
|
||||
toString(node){
|
||||
return node.error.toString();
|
||||
toString(node) {
|
||||
return `${node.error.type} error: ${node.error.message}`;
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,12 @@ import resolve, { toString, traverse, map } from '../resolve';
|
||||
import constant from './constant';
|
||||
|
||||
const operator = {
|
||||
create({ left, right, operator, fn }) {
|
||||
create({ left, right, operator }) {
|
||||
return {
|
||||
parseType: 'operator',
|
||||
left,
|
||||
right,
|
||||
operator,
|
||||
fn
|
||||
};
|
||||
},
|
||||
resolve(fn, node, scope, context) {
|
||||
@@ -21,7 +20,6 @@ const operator = {
|
||||
left: leftNode,
|
||||
right: rightNode,
|
||||
operator: node.operator,
|
||||
fn: node.fn,
|
||||
}),
|
||||
context,
|
||||
};
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import resolve, { toString } from '../resolve';
|
||||
import constant from './constant';
|
||||
|
||||
const symbol = {
|
||||
create({ name }) {
|
||||
return {
|
||||
parseType: 'symbol',
|
||||
name,
|
||||
};
|
||||
},
|
||||
toString(node) {
|
||||
return `${node.name}`
|
||||
},
|
||||
compile(node, scope, context, calledFromReduce = false) {
|
||||
let value = scope && scope[node.name];
|
||||
let type = typeof value;
|
||||
// For objects, default to their .value
|
||||
if (type === 'object') {
|
||||
value = value.value;
|
||||
type = typeof value;
|
||||
}
|
||||
// For parse nodes, compile and return
|
||||
if (value?.parseType) {
|
||||
if (calledFromReduce) {
|
||||
return resolve('reduce', value, scope, context);
|
||||
} else {
|
||||
return resolve('compile', value, scope, context);
|
||||
}
|
||||
}
|
||||
if (type === 'string' || type === 'number' || type === 'boolean') {
|
||||
return {
|
||||
result: constant.create({ value }),
|
||||
context,
|
||||
};
|
||||
} else if (type === 'undefined') {
|
||||
return {
|
||||
result: symbol.create({ name: node.name }),
|
||||
context,
|
||||
};
|
||||
} else {
|
||||
throw new Meteor.Error(`Unexpected case: ${node.name} resolved to ${value}`);
|
||||
}
|
||||
},
|
||||
reduce(node, scope, context) {
|
||||
let { result } = symbol.compile(node, scope, context, true);
|
||||
if (result.parseType === 'symbol') {
|
||||
return {
|
||||
result: constant.create({ value: 0 }),
|
||||
context,
|
||||
};
|
||||
} else {
|
||||
return { result, context };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default symbol;
|
||||
@@ -30,6 +30,13 @@ export function toString(node) {
|
||||
return type.toString(node);
|
||||
}
|
||||
|
||||
export function toPrimitiveOrString(node) {
|
||||
if (!node) return '';
|
||||
if (node.parseType === 'constant') return node.value;
|
||||
if (node.parseType === 'error') return null;
|
||||
return toString(node);
|
||||
}
|
||||
|
||||
export function traverse(node, fn) {
|
||||
if (!node) return;
|
||||
let type = nodeTypeIndex[node.parseType];
|
||||
|
||||
@@ -1,11 +1,60 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
|
||||
import { JsonRoutes } from 'meteor/simple:json-routes';
|
||||
import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions';
|
||||
import computeCreature from '/imports/api/engine/computeCreature';
|
||||
import VERSION from '/imports/constants/VERSION';
|
||||
import { getCreature, getProperties, getVariables } from '/imports/api/engine/loadCreatures';
|
||||
|
||||
JsonRoutes.add('get', 'api/creature/:id', function (req, res) {
|
||||
const creatureId = req.params.id;
|
||||
|
||||
// Validate the creature ID
|
||||
try {
|
||||
new SimpleSchema({
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
}).validate({ creatureId });
|
||||
} catch (e) {
|
||||
const error = new Meteor.Error('invalid-id', 'Invalid creature ID provided');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
const creature = getCreature(creatureId);
|
||||
const userId = req.userId;
|
||||
try {
|
||||
assertViewPermission(creature, userId)
|
||||
} catch (e) {
|
||||
e.statusCode = 403;
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Compute the creature first if need be
|
||||
if (creature.computeVersion !== VERSION) {
|
||||
try {
|
||||
computeCreature(creatureId)
|
||||
} catch (e) {
|
||||
e.statusCode = 500;
|
||||
console.error(e)
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Send the results
|
||||
JsonRoutes.sendResult(res, {
|
||||
data: {
|
||||
creatures: [creature],
|
||||
creatureProperties: getProperties(creatureId),
|
||||
creatureVariables: getVariables(creatureId),
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/*
|
||||
Meteor.publish('api-creature', function (creatureId) {
|
||||
try {
|
||||
new SimpleSchema({
|
||||
@@ -15,6 +64,7 @@ Meteor.publish('api-creature', function (creatureId) {
|
||||
},
|
||||
}).validate({ creatureId });
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error(e);
|
||||
return;
|
||||
}
|
||||
@@ -26,6 +76,7 @@ Meteor.publish('api-creature', function (creatureId) {
|
||||
try {
|
||||
assertViewPermission(creature, userId)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.error(e);
|
||||
return;
|
||||
}
|
||||
@@ -48,3 +99,4 @@ Meteor.publish('api-creature', function (creatureId) {
|
||||
}, {
|
||||
url: 'api/creature/:0'
|
||||
});
|
||||
*/
|
||||
|
||||
BIN
app/public/images/paragons/blue.png
Normal file
BIN
app/public/images/paragons/blue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 909 B |
Reference in New Issue
Block a user