Refactored computations again, split into multiple files, lots still to do
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import ChildSchema, { RefSchema } from '/imports/api/parenting/ChildSchema.js';
|
import ChildSchema, { RefSchema } from '/imports/api/parenting/ChildSchema.js';
|
||||||
import { recomputeCreature } from '/imports/api/creature/creatureComputation.js';
|
import { recomputeCreature } from '/imports/api/creature/computation/creatureComputation.js';
|
||||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import { softRemove } from '/imports/api/parenting/softRemove.js';
|
import { softRemove } from '/imports/api/parenting/softRemove.js';
|
||||||
|
|||||||
148
app/imports/api/creature/computation/ComputationMemo.js
Normal file
148
app/imports/api/creature/computation/ComputationMemo.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { includes, cloneDeep } from 'lodash';
|
||||||
|
|
||||||
|
export default class ComputationMemo {
|
||||||
|
constructor(props){
|
||||||
|
this.statsByVariableName = {};
|
||||||
|
this.originalPropsById = {};
|
||||||
|
this.propsById = {};
|
||||||
|
this.skillsByAbility = {};
|
||||||
|
this.unassignedEffects = [];
|
||||||
|
props.filter((prop) => {
|
||||||
|
// skip effects and proficiencies for the next pass
|
||||||
|
if (prop.type === 'effect' || prop.type === 'proficiency') return true;
|
||||||
|
// Add all the stats
|
||||||
|
this.addStat(prop);
|
||||||
|
}).forEach((prop) => {
|
||||||
|
// Now add all effects and proficiencies
|
||||||
|
if (prop.type === 'effect'){
|
||||||
|
this.addEffect(prop);
|
||||||
|
} else if (prop.type === 'proficiency') {
|
||||||
|
this.addProficiency(prop);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
registerProperty(prop){
|
||||||
|
this.originalPropsById[prop._id] = cloneDeep(prop);
|
||||||
|
this.propsById[prop._id] = prop;
|
||||||
|
prop.computationDetails = propDetails(prop);
|
||||||
|
return prop;
|
||||||
|
}
|
||||||
|
addStat(prop){
|
||||||
|
prop = this.registerProperty(prop);
|
||||||
|
let variableName = prop.variableName;
|
||||||
|
if (!variableName) return;
|
||||||
|
if (this.statsByVariableName[variableName]){
|
||||||
|
prop.value = NaN;
|
||||||
|
prop.computationDetails.error = 'variableNameCollision';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.statsByVariableName[variableName] = prop;
|
||||||
|
if (
|
||||||
|
prop.type === 'skill' &&
|
||||||
|
includes(['skill', 'check'], prop.skillType) &&
|
||||||
|
prop.ability
|
||||||
|
){
|
||||||
|
this.addSkillToAbility(prop, prop.ability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addSkillToAbility(prop, ability){
|
||||||
|
if (!this.skillsByAbility[ability]){
|
||||||
|
this.skillsByAbility[ability] = [];
|
||||||
|
}
|
||||||
|
this.skillsByAbility[ability].push(prop);
|
||||||
|
}
|
||||||
|
addEffect(prop){
|
||||||
|
prop = this.registerProperty(prop);
|
||||||
|
let targets = this.getEffectTargets(prop);
|
||||||
|
targets.forEach(target => {
|
||||||
|
target.computationDetails.effects.push(prop);
|
||||||
|
});
|
||||||
|
if (!targets.length){
|
||||||
|
this.unassignedEffects.push(prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getEffectTargets(prop){
|
||||||
|
let targets = new Set();
|
||||||
|
if (!prop.stats) return targets;
|
||||||
|
prop.stats.forEach((statName) => {
|
||||||
|
let target = this.statsByVariableName[statName];
|
||||||
|
if (!target) return;
|
||||||
|
targets.add(target);
|
||||||
|
if (isSkillOperation(prop) && isAbility(target)){
|
||||||
|
let extras = this.skillsByAbility[statName] || [];
|
||||||
|
targets.add(...extras)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
addProficiency(prop){
|
||||||
|
prop = this.registerProperty(prop);
|
||||||
|
let targets = getProficiencyTargets(prop);
|
||||||
|
targets.forEach(target => {
|
||||||
|
target.computationDetails.proficiencies.push(prop);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getProficiencyTargets(prop){
|
||||||
|
let targets = new Set();
|
||||||
|
if (!prop.stats) return targets;
|
||||||
|
proficiency.stats.forEach(statName => {
|
||||||
|
let target = this.statsByVariableName[statName];
|
||||||
|
if (!target) return;
|
||||||
|
targets.add(target);
|
||||||
|
if (isAbility(target) && isSkillCheck(prop)) {
|
||||||
|
let extras = this.skillsByAbility[statName] || [];
|
||||||
|
targets.add(...extras)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillOperations = [
|
||||||
|
'advantage',
|
||||||
|
'disadvantage',
|
||||||
|
'passiveAdd',
|
||||||
|
'fail',
|
||||||
|
'conditional',
|
||||||
|
'rollBonus',
|
||||||
|
];
|
||||||
|
|
||||||
|
function isAbility(prop){
|
||||||
|
return prop.type === 'attribute' &&
|
||||||
|
prop.attributeType === 'ability'
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSkillCheck(prop){
|
||||||
|
return includes(['skill', 'check'], prop.skillType);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSkillOperation(prop){
|
||||||
|
return skillOperations.includes(prop.operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
function propDetails(prop){
|
||||||
|
return propDetailsByType[prop.type]() || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const propDetailsByType = {
|
||||||
|
attribute(){
|
||||||
|
return {
|
||||||
|
computed: false,
|
||||||
|
busyComputing: false,
|
||||||
|
effects: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
skill(){
|
||||||
|
return {
|
||||||
|
computed: false,
|
||||||
|
busyComputing: false,
|
||||||
|
effects: [],
|
||||||
|
proficiencies: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
effect(){
|
||||||
|
return {
|
||||||
|
computed: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
64
app/imports/api/creature/computation/EffectAggregator.js
Normal file
64
app/imports/api/creature/computation/EffectAggregator.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
export default class EffectAggregator{
|
||||||
|
constructor(stat){
|
||||||
|
this.base = stat.baseValue || 0;
|
||||||
|
this.add = 0;
|
||||||
|
this.mul = 1;
|
||||||
|
this.min = Number.NEGATIVE_INFINITY;
|
||||||
|
this.max = Number.POSITIVE_INFINITY;
|
||||||
|
this.advantage = 0;
|
||||||
|
this.disadvantage = 0;
|
||||||
|
this.passiveAdd = 0;
|
||||||
|
this.fail = 0;
|
||||||
|
this.conditional = [];
|
||||||
|
this.rollBonus = [];
|
||||||
|
}
|
||||||
|
addEffect(effect){
|
||||||
|
let result = effect.result;
|
||||||
|
switch(effect.operation){
|
||||||
|
case "base":
|
||||||
|
// Take the largest base value
|
||||||
|
this.base = result > this.base ? result : this.base;
|
||||||
|
break;
|
||||||
|
case "add":
|
||||||
|
// Add all adds together
|
||||||
|
this.add += result;
|
||||||
|
break;
|
||||||
|
case "mul":
|
||||||
|
// Multiply the muls together
|
||||||
|
this.mul *= result;
|
||||||
|
break;
|
||||||
|
case "min":
|
||||||
|
// Take the largest min value
|
||||||
|
this.min = result > this.min ? result : this.min;
|
||||||
|
break;
|
||||||
|
case "max":
|
||||||
|
// Take the smallest max value
|
||||||
|
this.max = result < this.max ? result : this.max;
|
||||||
|
break;
|
||||||
|
case "advantage":
|
||||||
|
// Sum number of advantages
|
||||||
|
this.advantage++;
|
||||||
|
break;
|
||||||
|
case "disadvantage":
|
||||||
|
// Sum number of disadvantages
|
||||||
|
this.disadvantage++;
|
||||||
|
break;
|
||||||
|
case "passiveAdd":
|
||||||
|
// Add all passive adds together
|
||||||
|
this.passiveAdd += result;
|
||||||
|
break;
|
||||||
|
case "fail":
|
||||||
|
// Sum number of fails
|
||||||
|
this.fail++;
|
||||||
|
break;
|
||||||
|
case "conditional":
|
||||||
|
// Store array of conditionals
|
||||||
|
this.conditional.push(result);
|
||||||
|
break;
|
||||||
|
case "rollBonus":
|
||||||
|
// Store array of roll bonuses
|
||||||
|
this.rollBonus.push(result);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/imports/api/creature/computation/combineStat.js
Normal file
69
app/imports/api/creature/computation/combineStat.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import computeStat from '/imports/api/creature/computation/computeStat.js';
|
||||||
|
import computedValueOfVariableName from '/imports/api/creature/computation/computedValueOfVariableName.js'
|
||||||
|
|
||||||
|
|
||||||
|
export default function combineStat(stat, aggregator, memo){
|
||||||
|
if (stat.type === "attribute"){
|
||||||
|
combineAttribute(stat, aggregator);
|
||||||
|
} else if (stat.type === "skill"){
|
||||||
|
combineSkill(stat, aggregator, memo);
|
||||||
|
} else if (stat.type === "damageMultiplier"){
|
||||||
|
combineDamageMultiplier(stat, memo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function combineAttribute(stat, aggregator){
|
||||||
|
let result = (aggregator.base + aggregator.add) * aggregator.mul;
|
||||||
|
if (result < aggregator.min) result = aggregator.min;
|
||||||
|
if (result > aggregator.max) result = aggregator.max;
|
||||||
|
if (!stat.decimal) result = Math.floor(result);
|
||||||
|
stat.value = result;
|
||||||
|
if (stat.attributeType === "ability") {
|
||||||
|
stat.mod = Math.floor((result - 10) / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function combineSkill(stat, aggregator, memo){
|
||||||
|
// Skills are based on some ability Modifier
|
||||||
|
let abilityMod = 0;
|
||||||
|
let ability = memo.statsByVariableName[stat.ability]
|
||||||
|
if (stat.ability && ability){
|
||||||
|
if (!ability.computationDetails.computed){
|
||||||
|
computeStat(ability, memo);
|
||||||
|
}
|
||||||
|
stat.abilityMod = ability.mod;
|
||||||
|
}
|
||||||
|
// Combine all the child proficiencies
|
||||||
|
for (let i in stat.proficiencies){
|
||||||
|
let prof = stat.proficiencies[i];
|
||||||
|
if (prof.value > stat.proficiency) stat.proficiency = prof.value;
|
||||||
|
}
|
||||||
|
// Get the character's proficiency bonus to apply
|
||||||
|
let profBonus = computedValueOfVariableName('proficiencyBonus', memo);
|
||||||
|
/** TODO level needs to be on the memo somewhere
|
||||||
|
if (typeof profBonus !== "number"){
|
||||||
|
profBonus = Math.floor(char.level / 4 + 1.75);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
// Multiply the proficiency bonus by the actual proficiency
|
||||||
|
profBonus *= stat.proficiency;
|
||||||
|
// Combine everything to get the final result
|
||||||
|
let result = (abilityMod + profBonus + stat.add) * stat.mul;
|
||||||
|
if (result < stat.min) result = stat.min;
|
||||||
|
if (result > stat.max) result = stat.max;
|
||||||
|
result = Math.floor(result);
|
||||||
|
if (stat.base > result) result = stat.base;
|
||||||
|
stat.value = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function combineDamageMultiplier(stat){
|
||||||
|
if (stat.immunityCount) return 0;
|
||||||
|
if (stat.ressistanceCount && !stat.vulnerabilityCount){
|
||||||
|
result = 0.5;
|
||||||
|
} else if (!stat.ressistanceCount && stat.vulnerabilityCount){
|
||||||
|
result = 2;
|
||||||
|
} else {
|
||||||
|
result = 1;
|
||||||
|
}
|
||||||
|
stat.value = result;
|
||||||
|
}
|
||||||
15
app/imports/api/creature/computation/computeEffect.js
Normal file
15
app/imports/api/creature/computation/computeEffect.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import evaluateCalculation from '/imports/api/creature/computation/evaluateCalculation.js';
|
||||||
|
|
||||||
|
export default function computeEffect(effect, memo){
|
||||||
|
if (effect.computed) return;
|
||||||
|
if (_.isFinite(effect.calculation)){
|
||||||
|
effect.result = +effect.calculation;
|
||||||
|
} else if(effect.operation === "conditional" || effect.operation === "rollBonuses"){
|
||||||
|
effect.result = effect.calculation;
|
||||||
|
} else if(_.contains(["advantage", "disadvantage", "fail"], effect.operation)){
|
||||||
|
effect.result = 1;
|
||||||
|
} else {
|
||||||
|
effect.result = evaluateCalculation(effect.calculation, memo);
|
||||||
|
}
|
||||||
|
effect.computationDetails.computed = true;
|
||||||
|
}
|
||||||
12
app/imports/api/creature/computation/computeMemo.js
Normal file
12
app/imports/api/creature/computation/computeMemo.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { each, forOwn } from 'lodash';
|
||||||
|
import computeStat from '/imports/api/creature/computation/computeStat.js';
|
||||||
|
import computeEffect from '/imports/api/creature/computation/computeEffect.js';
|
||||||
|
|
||||||
|
export default function computeMemo(memo){
|
||||||
|
forOwn(memo.statsByVariableName, (stat) => {
|
||||||
|
computeStat (stat, memo);
|
||||||
|
});
|
||||||
|
each(memo.unassignedEffects, (effect) => {
|
||||||
|
computeEffect(effect, memo);
|
||||||
|
});
|
||||||
|
}
|
||||||
29
app/imports/api/creature/computation/computeStat.js
Normal file
29
app/imports/api/creature/computation/computeStat.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import combineStat from '/imports/api/creature/computation/combineStat.js';
|
||||||
|
import computeEffect from '/imports/api/creature/computation/computeEffect.js';
|
||||||
|
import EffectAggregator from '/imports/api/creature/computation/EffectAggregator.js';
|
||||||
|
import { each } from 'lodash';
|
||||||
|
|
||||||
|
export default function computeStat(stat, memo){
|
||||||
|
// If the stat is already computed, skip it
|
||||||
|
if (stat.computationDetails.computed) return;
|
||||||
|
if (stat.computationDetails.busyComputing){
|
||||||
|
// Trying to compute this stat again while it is already computing.
|
||||||
|
// We must be in a dependency loop.
|
||||||
|
stat.computationDetails.computed = true;
|
||||||
|
stat.value = NaN;
|
||||||
|
stat.computationDetails.busyComputing = false;
|
||||||
|
stat.computationDetails.error = 'dependencyLoop';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Compute and aggregate all the effects
|
||||||
|
let aggregator = new EffectAggregator(stat)
|
||||||
|
each(stat.computationDetails.effects, (effect) => {
|
||||||
|
computeEffect(effect, memo);
|
||||||
|
aggregator.addEffect(effect);
|
||||||
|
});
|
||||||
|
// Conglomerate all the effects to compute the final stat values
|
||||||
|
combineStat(stat, aggregator, memo);
|
||||||
|
// Mark the attribute as computed
|
||||||
|
stat.computationDetails.computed = true;
|
||||||
|
stat.computationDetails.busyComputing = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import computeStat from '/imports/api/creature/computation/computeStat.js';
|
||||||
|
|
||||||
|
export default function computedValueOfVariableName(sub, memo){
|
||||||
|
const stat = memo.statsByVariableName[sub];
|
||||||
|
if (!stat) return null;
|
||||||
|
if (!stat.computationDetails.computed){
|
||||||
|
computeStat(stat, char);
|
||||||
|
}
|
||||||
|
return stat.result;
|
||||||
|
}
|
||||||
76
app/imports/api/creature/computation/creatureComputation.js
Normal file
76
app/imports/api/creature/computation/creatureComputation.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// TODO allow abilities to get advantage/disadvantage, making all skills that are based
|
||||||
|
// on them disadvantaged as well
|
||||||
|
|
||||||
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
|
||||||
|
import ComputationMemo from '/imports/api/creature/computation/ComputationMemo.js';
|
||||||
|
import computeMemo from '/imports/api/creature/computation/computeMemo.js';
|
||||||
|
import getCalculationProperties from '/imports/api/creature/computation/getCalculationProperties.js';
|
||||||
|
import logAlterations from '/imports/api/creature/computation/logAlterations.js';
|
||||||
|
import * as math from 'mathjs';
|
||||||
|
|
||||||
|
export const recomputeCreature = new ValidatedMethod({
|
||||||
|
|
||||||
|
name: "Creatures.methods.recomputeCreature",
|
||||||
|
|
||||||
|
validate: new SimpleSchema({
|
||||||
|
charId: { type: String }
|
||||||
|
}).validator(),
|
||||||
|
|
||||||
|
run({charId}) {
|
||||||
|
// Permission
|
||||||
|
assertEditPermission(charId, this.userId);
|
||||||
|
// Work, call this direcly if you are already in a method that has checked
|
||||||
|
// for permission to edit a given character
|
||||||
|
recomputeCreatureById(charId);
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is the heart of DiceCloud. It recomputes a creature's stats,
|
||||||
|
* distilling down effects and proficiencies into the final stats that make up
|
||||||
|
* a creature.
|
||||||
|
*
|
||||||
|
* Essentially this is a depth first tree traversal algorithm that computes
|
||||||
|
* stats' dependencies before computing stats themselves, while detecting
|
||||||
|
* dependency loops.
|
||||||
|
*
|
||||||
|
* At the moment it makes no effort to limit recomputation to just what was
|
||||||
|
* changed.
|
||||||
|
*
|
||||||
|
* Attempting to implement dependency management to limit recomputation to just
|
||||||
|
* change affected stats should only happen as a last resort, when this function
|
||||||
|
* can no longer be performed more efficiently, and server resources can not be
|
||||||
|
* expanded to meet demand.
|
||||||
|
*
|
||||||
|
* A brief overview:
|
||||||
|
* - Fetch the stats of the creature and add them to
|
||||||
|
* an object for quick lookup
|
||||||
|
* - Fetch the effects and proficiencies which apply to each stat and store them with the stat
|
||||||
|
* - Fetch the class levels and store them as well
|
||||||
|
* - Mark each stat and effect as uncomputed
|
||||||
|
* - Iterate over each stat in order and compute it
|
||||||
|
* - If the stat is already computed, skip it
|
||||||
|
* - If the stat is busy being computed, we are in a dependency loop, make it NaN and mark computed
|
||||||
|
* - Mark the stat as busy computing
|
||||||
|
* - Iterate over each effect which applies to the attribute
|
||||||
|
* - If the effect is not computed compute it
|
||||||
|
* - If the effect relies on another attribute, get its computed value
|
||||||
|
* - Recurse if that attribute is uncomputed
|
||||||
|
* - apply the effect to the attribute
|
||||||
|
* - Conglomerate all the effects to compute the final stat values
|
||||||
|
* - Mark the stat as computed
|
||||||
|
* - Write the computed results back to the database
|
||||||
|
*/
|
||||||
|
export function recomputeCreatureById(creatureId){
|
||||||
|
let props = getCalculationProperties(creatureId);
|
||||||
|
let computationMemo = new ComputationMemo(props);
|
||||||
|
console.log({toCompute: computationMemo});
|
||||||
|
computeMemo(computationMemo);
|
||||||
|
console.log({computed: computationMemo});
|
||||||
|
logAlterations(computationMemo);
|
||||||
|
//writeAlteredProps(computationMemo);
|
||||||
|
return computationMemo;
|
||||||
|
}
|
||||||
30
app/imports/api/creature/computation/evaluateCalculation.js
Normal file
30
app/imports/api/creature/computation/evaluateCalculation.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import computedValueOfVariableName from '/imports/api/creature/computation/computedValueOfVariableName.js'
|
||||||
|
|
||||||
|
export default function evaluateCalculation(string, memo){
|
||||||
|
if (!string) return string;
|
||||||
|
// Parse the string using mathjs
|
||||||
|
let calc;
|
||||||
|
try {
|
||||||
|
calc = math.parse(string);
|
||||||
|
} catch (e) {
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
// Replace all symbols with known values
|
||||||
|
let substitutedCalc = calc.transform(node => {
|
||||||
|
if (node.isSymbolNode) {
|
||||||
|
let val = computedValueOfVariableName(node.name, memo);
|
||||||
|
if (val === null) return node;
|
||||||
|
return new math.expression.node.ConstantNode(val);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Evaluate the expression to a number or return with substitutions
|
||||||
|
try {
|
||||||
|
return substitutedCalc.eval();
|
||||||
|
} catch (e){
|
||||||
|
return substitutedCalc.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import Creatures from "/imports/api/creature/Creatures.js";
|
||||||
|
import CreatureProperties from "/imports/api/creature/CreatureProperties.js";
|
||||||
|
|
||||||
|
export default function getCalculationProperties(creatureId){
|
||||||
|
// First get ids of disabled properties and unequiped items
|
||||||
|
let disabledAncestorIds = CreatureProperties.find({
|
||||||
|
'ancestors.id': creatureId,
|
||||||
|
$or: [
|
||||||
|
{disabled: true},
|
||||||
|
{equipped: false},
|
||||||
|
],
|
||||||
|
}, {
|
||||||
|
fields: {_id: 1},
|
||||||
|
}).map(prop => prop._id);
|
||||||
|
|
||||||
|
// Then get the ids of creatures that are children of this creature
|
||||||
|
// to isolate their decendent properties from this calculation
|
||||||
|
Creatures.find({
|
||||||
|
'ancestors.id': creatureId,
|
||||||
|
}, {
|
||||||
|
fields: {_id: 1},
|
||||||
|
}).forEach(prop => {
|
||||||
|
disabledAncestorIds.push(prop._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all the properties that aren't from the excluded decendents
|
||||||
|
return CreatureProperties.find({
|
||||||
|
'ancestors.id': {
|
||||||
|
$eq: creatureId,
|
||||||
|
$nin: disabledAncestorIds,
|
||||||
|
},
|
||||||
|
type: {$in: [
|
||||||
|
'attribute',
|
||||||
|
'skill',
|
||||||
|
'damageMultiplier',
|
||||||
|
'effect',
|
||||||
|
'proficiency',
|
||||||
|
]},
|
||||||
|
}).fetch();
|
||||||
|
}
|
||||||
16
app/imports/api/creature/computation/logAlterations.js
Normal file
16
app/imports/api/creature/computation/logAlterations.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { isEqual, forOwn } from 'lodash';
|
||||||
|
import { ComputedOnlySkilLSchema } from '/imports/api/properties/Skills.js';
|
||||||
|
|
||||||
|
export default function logAlterations(memo){
|
||||||
|
forOwn(memo.originalPropsById, old => {
|
||||||
|
let changed = memo.propsById[old._id];
|
||||||
|
delete changed.computationDetails;
|
||||||
|
|
||||||
|
if (!isEqual(old, changed)){
|
||||||
|
console.log({change: {old, changed}})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO use this as a starting point to write only computed fields that have
|
||||||
|
// changed
|
||||||
98
app/imports/api/creature/computation/writeCreature.js
Normal file
98
app/imports/api/creature/computation/writeCreature.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
|
||||||
|
function writeCreature(char) {
|
||||||
|
//TODO these functions don't filter the stats before trying to write
|
||||||
|
writeAttributes(char);
|
||||||
|
writeSkills(char);
|
||||||
|
writeDamageMultipliers(char);
|
||||||
|
writeEffects(char);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Write all the attributes from the in-memory char object to the Attirbute docs
|
||||||
|
*/
|
||||||
|
function writeAttributes(char) {
|
||||||
|
let bulkWriteOps = _.map(char.atts, (att, variableName) => {
|
||||||
|
let op = {
|
||||||
|
updateMany: {
|
||||||
|
filter: {'ancestors.id': char.id, variableName},
|
||||||
|
update: {'$set': {
|
||||||
|
value: att.result,
|
||||||
|
rollBonuses: skill.rollBonus,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (typeof att.mod === 'number'){
|
||||||
|
op.updateMany.update.$set.mod = att.mod;
|
||||||
|
} else {
|
||||||
|
op.updateMany.update.$unset = {mod: 1};
|
||||||
|
}
|
||||||
|
return op;
|
||||||
|
});
|
||||||
|
bulkWriteProperties({bulkWriteOps, selectorType: 'attribute'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeSkills(char) {
|
||||||
|
let bulkWriteOps = _.map(char.skills, (skill, variableName) => {
|
||||||
|
let op = {
|
||||||
|
updateMany: {
|
||||||
|
filter: {'ancestors.id': char.id, variableName},
|
||||||
|
update: {$set: {
|
||||||
|
value: skill.result,
|
||||||
|
abilityMod: skill.abilityMod,
|
||||||
|
advantage: skill.advantage,
|
||||||
|
passiveBonus: skill.passiveAdd,
|
||||||
|
proficiency: skill.proficiency,
|
||||||
|
conditionalBenefits: skill.conditional,
|
||||||
|
rollBonuses: skill.rollBonus,
|
||||||
|
fail: skill.fail,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return op;
|
||||||
|
});
|
||||||
|
bulkWriteProperties({bulkWriteOps, selectorType: 'skill'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeDamageMultipliers(char) {
|
||||||
|
let bulkWriteOps = _.map(char.dms, (dm, variableName) => {
|
||||||
|
let op = {
|
||||||
|
updateMany: {
|
||||||
|
filter: {'ancestors.id': char.id, variableName},
|
||||||
|
update: {$set: {
|
||||||
|
value: dm.result,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return op;
|
||||||
|
});
|
||||||
|
bulkWriteProperties({bulkWriteOps, selectorType: 'damageMultiplier'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeEffects(char){
|
||||||
|
let bulkWriteOps = _.map(char.computedEffects, effect => ({
|
||||||
|
updateOne: {
|
||||||
|
filter: {_id: effect._id},
|
||||||
|
update: {$set: {
|
||||||
|
result: effect.result,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
if (!bulkWriteOps.length) return;
|
||||||
|
bulkWriteProperties({bulkWriteOps, selectorType: 'effect'});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bulkWriteProperties({bulkWriteOps, selectorType}){
|
||||||
|
if (!bulkWriteOps.length) return;
|
||||||
|
if (Meteor.isServer){
|
||||||
|
CreatureProperties.rawCollection().bulkWrite(bulkWriteOps, {ordered : false}, function(e){
|
||||||
|
if (e) console.error(e);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_.each(bulkWriteOps, op => {
|
||||||
|
CreatureProperties.update(op.updateMany.filter, op.updateMany.update, {
|
||||||
|
multi: true,
|
||||||
|
selector: {type: selectorType}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,623 +0,0 @@
|
|||||||
// TODO allow abilities to get advantage/disadvantage, making all skills that are based
|
|
||||||
// on them disadvantaged as well
|
|
||||||
|
|
||||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
|
||||||
import SimpleSchema from 'simpl-schema';
|
|
||||||
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
|
|
||||||
import Creatures from "/imports/api/creature/Creatures.js";
|
|
||||||
import CreatureProperties from "/imports/api/creature/CreatureProperties.js";
|
|
||||||
import * as math from 'mathjs';
|
|
||||||
import { includes } from 'lodash';
|
|
||||||
|
|
||||||
export const recomputeCreature = new ValidatedMethod({
|
|
||||||
|
|
||||||
name: "Creatures.methods.recomputeCreature",
|
|
||||||
|
|
||||||
validate: new SimpleSchema({
|
|
||||||
charId: { type: String }
|
|
||||||
}).validator(),
|
|
||||||
|
|
||||||
run({charId}) {
|
|
||||||
// Permission
|
|
||||||
assertEditPermission(charId, this.userId);
|
|
||||||
// Work, call this direcly if you are already in a method that has checked
|
|
||||||
// for permission to edit a given character
|
|
||||||
recomputeCreatureById(charId);
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is the heart of DiceCloud. It recomputes a creature's stats,
|
|
||||||
* distilling down effects and proficiencies into the final stats that make up
|
|
||||||
* a creature.
|
|
||||||
*
|
|
||||||
* Essentially this is a depth first tree traversal algorithm that computes
|
|
||||||
* stats' dependencies before computing stats themselves, while detecting
|
|
||||||
* dependency loops.
|
|
||||||
*
|
|
||||||
* At the moment it makes no effort to limit recomputation to just what was
|
|
||||||
* changed.
|
|
||||||
*
|
|
||||||
* Attempting to implement dependency management to limit recomputation to just
|
|
||||||
* change affected stats should only happen as a last resort, when this function
|
|
||||||
* can no longer be performed more efficiently, and server resources can not be
|
|
||||||
* expanded to meet demand.
|
|
||||||
*
|
|
||||||
* A brief overview:
|
|
||||||
* - Fetch the stats of the creature and add them to
|
|
||||||
* an object for quick lookup
|
|
||||||
* - Fetch the effects and proficiencies which apply to each stat and store them with the stat
|
|
||||||
* - Fetch the class levels and store them as well
|
|
||||||
* - Mark each stat and effect as uncomputed
|
|
||||||
* - Iterate over each stat in order and compute it
|
|
||||||
* - If the stat is already computed, skip it
|
|
||||||
* - If the stat is busy being computed, we are in a dependency loop, make it NaN and mark computed
|
|
||||||
* - Mark the stat as busy computing
|
|
||||||
* - Iterate over each effect which applies to the attribute
|
|
||||||
* - If the effect is not computed compute it
|
|
||||||
* - If the effect relies on another attribute, get its computed value
|
|
||||||
* - Recurse if that attribute is uncomputed
|
|
||||||
* - apply the effect to the attribute
|
|
||||||
* - Conglomerate all the effects to compute the final stat values
|
|
||||||
* - Mark the stat as computed
|
|
||||||
* - Write the computed results back to the database
|
|
||||||
*/
|
|
||||||
export function recomputeCreatureById(creatureId){
|
|
||||||
let char = buildCreature(creatureId);
|
|
||||||
char = computeCreature(char);
|
|
||||||
writeCreature(char);
|
|
||||||
return char;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load creature details into memory from database
|
|
||||||
function buildCreature(creatureId){
|
|
||||||
let char = {
|
|
||||||
id: creatureId,
|
|
||||||
stats: {},
|
|
||||||
skillsByAbility: {},
|
|
||||||
unassignedEffects: [],
|
|
||||||
computedEffects: [],
|
|
||||||
};
|
|
||||||
// Get all relevant properties and store them on the in-memory creature
|
|
||||||
getCreatureProps(creatureId).filter(prop => {
|
|
||||||
const key = prop.variableName;
|
|
||||||
switch(prop.type){
|
|
||||||
case 'attribute':
|
|
||||||
case 'skill':
|
|
||||||
let skill = statObject(prop);
|
|
||||||
// Store the skill as a stat on the in-memory creature
|
|
||||||
if (!char.stats[key]) char.stats[key] = skill;
|
|
||||||
// Store a reference to the skill under the relevant ability as well
|
|
||||||
if (skill.ability){
|
|
||||||
if (!char.skillsByAbility[skill.ability]){
|
|
||||||
char.skillsByAbility[skill.ability] = [];
|
|
||||||
}
|
|
||||||
char.skillsByAbility[skill.ability].push(skill);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
case 'damageMultiplier':
|
|
||||||
if (!char.stats[key]) char.stats[key] = damageMultiplierObject();
|
|
||||||
return false;
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}).forEach(prop => {
|
|
||||||
// In a second pass through the props, assign effects and proficiencies to
|
|
||||||
// their matched attributes and skills
|
|
||||||
switch(prop.type){
|
|
||||||
case 'effect':
|
|
||||||
let effect = effectObject(prop);
|
|
||||||
// Assign the effect to all the stats it directly targets
|
|
||||||
let targets = getEffectTargets(char, effect);
|
|
||||||
targets.forEach(target =>
|
|
||||||
target.effects.push(effect)
|
|
||||||
);
|
|
||||||
if (!targets.length){
|
|
||||||
char.unassignedEffects.push(effect);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'proficiency':
|
|
||||||
let proficiency = proficiencyObject(prop);
|
|
||||||
getProficiencyTargets(char, proficiency).forEach(target =>
|
|
||||||
target.proficiencies.push(proficiency)
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Add direct properties from creature to variable list
|
|
||||||
const fields = { xp: 1, weightCarried: 1, level: 1};
|
|
||||||
const creature = Creatures.findOne(creatureId, {fields});
|
|
||||||
for (let key in fields){
|
|
||||||
if (!char.stats[key]){
|
|
||||||
char.stats[key] = {
|
|
||||||
computed: true,
|
|
||||||
type: 'creatureProperty',
|
|
||||||
result: creature[key] || 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return char;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCreatureProps(creatureId){
|
|
||||||
// First get ids of disabled properties and unequiped items
|
|
||||||
let disabledAncestorIds = CreatureProperties.find({
|
|
||||||
'ancestors.id': creatureId,
|
|
||||||
$or: [
|
|
||||||
{disabled: true},
|
|
||||||
{equipped: false},
|
|
||||||
],
|
|
||||||
}, {
|
|
||||||
fields: {_id: 1},
|
|
||||||
}).map(prop => prop._id);
|
|
||||||
|
|
||||||
// Then get the ids of creatures that are children of this creature
|
|
||||||
// to isolate their decendent properties from this calculation
|
|
||||||
Creatures.find({
|
|
||||||
'ancestors.id': creatureId,
|
|
||||||
}, {
|
|
||||||
fields: {_id: 1},
|
|
||||||
}).forEach(prop => {
|
|
||||||
disabledAncestorIds.push(prop._id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get all the properties that aren't from the excluded decendents
|
|
||||||
return CreatureProperties.find({
|
|
||||||
'ancestors.id': {
|
|
||||||
$eq: creatureId,
|
|
||||||
$nin: disabledAncestorIds,
|
|
||||||
},
|
|
||||||
type: {$in: [
|
|
||||||
'attribute',
|
|
||||||
'skill',
|
|
||||||
'damageMultiplier',
|
|
||||||
'effect',
|
|
||||||
'proficiency',
|
|
||||||
]},
|
|
||||||
}).fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProficiencyTargets(char, proficiency){
|
|
||||||
let extraTargets = [];
|
|
||||||
if (!proficiency.stats) return [];
|
|
||||||
let targets = proficiency.stats.map(targetStat => {
|
|
||||||
let target = char[targetStat];
|
|
||||||
// Proficiencies targeting ability scores apply to 'skill' and check' skills
|
|
||||||
// based on that ability as well
|
|
||||||
if (
|
|
||||||
target &&
|
|
||||||
target.type === 'attribute' &&
|
|
||||||
target.attributeType === 'ability' &&
|
|
||||||
char.skillsByAbility[targetStat]
|
|
||||||
) {
|
|
||||||
extraTargets.push(
|
|
||||||
...char.skillsByAbility[targetStat].filter(skill =>
|
|
||||||
includes(['skill', 'check'], skill.skillType)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return target;
|
|
||||||
}).filter(target => !!target);
|
|
||||||
return targets.concat(extraTargets);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEffectTargets(char, effect){
|
|
||||||
const skillOperations = [
|
|
||||||
'advantage',
|
|
||||||
'disadvantage',
|
|
||||||
'passiveAdd',
|
|
||||||
'fail',
|
|
||||||
'conditional',
|
|
||||||
'rollBonus',
|
|
||||||
];
|
|
||||||
let extraTargets = [];
|
|
||||||
if (!effect.stats) return [];
|
|
||||||
let targets = effect.stats.map(targetStat => {
|
|
||||||
let target = char[targetStat];
|
|
||||||
// Certain effects targeting ability scores apply to skills
|
|
||||||
// based on that ability as well
|
|
||||||
if (
|
|
||||||
includes(skillOperations, effect.operation) &&
|
|
||||||
target &&
|
|
||||||
target.type === 'attribute' &&
|
|
||||||
target.attributeType === 'ability' &&
|
|
||||||
char.skillsByAbility[targetStat]
|
|
||||||
) {
|
|
||||||
extraTargets.push(...char.skillsByAbility[targetStat]);
|
|
||||||
}
|
|
||||||
return target;
|
|
||||||
});
|
|
||||||
return targets.concat(extraTargets);
|
|
||||||
}
|
|
||||||
|
|
||||||
function statObject(prop){
|
|
||||||
return {
|
|
||||||
computed: false,
|
|
||||||
busyComputing: false,
|
|
||||||
type: prop.type,
|
|
||||||
attributeType: prop.attributeType,
|
|
||||||
skillType: prop.skillType,
|
|
||||||
ability: prop.ability,
|
|
||||||
base: prop.baseValue || 0,
|
|
||||||
proficiency: prop.baseProficiency || 0,
|
|
||||||
decimal: prop.decimal,
|
|
||||||
result: 0,
|
|
||||||
mod: 0, // The resulting modifier if this is an ability
|
|
||||||
add: 0,
|
|
||||||
mul: 1,
|
|
||||||
min: Number.NEGATIVE_INFINITY,
|
|
||||||
max: Number.POSITIVE_INFINITY,
|
|
||||||
advantage: 0,
|
|
||||||
disadvantage: 0,
|
|
||||||
passiveAdd: 0,
|
|
||||||
fail: 0,
|
|
||||||
conditional: 0,
|
|
||||||
rollBonuses: 0,
|
|
||||||
effects: [],
|
|
||||||
proficiencies: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function damageMultiplierObject(){
|
|
||||||
return {
|
|
||||||
computed: false,
|
|
||||||
busyComputing: false,
|
|
||||||
type: "damageMultiplier",
|
|
||||||
result: 0,
|
|
||||||
immunityCount: 0,
|
|
||||||
ressistanceCount: 0,
|
|
||||||
vulnerabilityCount: 0,
|
|
||||||
effects: [],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function effectObject(prop){
|
|
||||||
return {
|
|
||||||
_id: prop._id,
|
|
||||||
computed: false,
|
|
||||||
result: 0,
|
|
||||||
operation: prop.operation,
|
|
||||||
calculation: prop.calculation,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function proficiencyObject(prop){
|
|
||||||
return {
|
|
||||||
value: prop.value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeCreature(char){
|
|
||||||
for (let statName in char.stats){
|
|
||||||
let stat = char.stats[statName];
|
|
||||||
computeStat (stat, char);
|
|
||||||
}
|
|
||||||
for (let effect of char.unassignedEffects){
|
|
||||||
computeEffect(effect, char);
|
|
||||||
}
|
|
||||||
return char;
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeStat(stat, char){
|
|
||||||
|
|
||||||
// If the stat is already computed, skip it
|
|
||||||
if (stat.computed) return;
|
|
||||||
|
|
||||||
// If the stat is busy being computed, make it NaN and mark computed
|
|
||||||
if (stat.busyComputing){
|
|
||||||
// Trying to compute this stat again while it is already computing.
|
|
||||||
// We must be in a dependency loop.
|
|
||||||
stat.computed = true;
|
|
||||||
stat.result = NaN;
|
|
||||||
stat.busyComputing = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate over each effect which applies to the stat
|
|
||||||
for (let i in stat.effects){
|
|
||||||
computeEffect(stat.effects[i], char);
|
|
||||||
// apply the effect to the stat
|
|
||||||
applyEffect(stat.effects[i], stat);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conglomerate all the effects to compute the final stat values
|
|
||||||
combineStat(stat, char);
|
|
||||||
|
|
||||||
// Mark the attribute as computed
|
|
||||||
stat.computed = true;
|
|
||||||
stat.busyComputing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute a the result of a single effect
|
|
||||||
*/
|
|
||||||
function computeEffect(effect, char){
|
|
||||||
if (effect.computed) return;
|
|
||||||
if (_.isFinite(effect.calculation)){
|
|
||||||
effect.result = +effect.calculation;
|
|
||||||
} else if(effect.operation === "conditional" || effect.operation === "rollBonuses"){
|
|
||||||
effect.result = effect.calculation;
|
|
||||||
} else if(_.contains(["advantage", "disadvantage", "fail"], effect.operation)){
|
|
||||||
effect.result = 1;
|
|
||||||
} else {
|
|
||||||
effect.result = evaluateCalculation(effect.calculation, char);
|
|
||||||
}
|
|
||||||
effect.computed = true;
|
|
||||||
char.computedEffects.push(effect);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply a computed effect to its stat
|
|
||||||
*/
|
|
||||||
function applyEffect(effect, stat){
|
|
||||||
if (!_.has(stat, effect.operation)){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch(effect.operation){
|
|
||||||
case "base":
|
|
||||||
// Take the largest base value
|
|
||||||
stat.base = effect.result > stat.base ? effect.result : stat.base;
|
|
||||||
break;
|
|
||||||
case "add":
|
|
||||||
// Add all adds together
|
|
||||||
stat.add += effect.result;
|
|
||||||
break;
|
|
||||||
case "mul":
|
|
||||||
// Multiply the muls together
|
|
||||||
stat.mul *= effect.result;
|
|
||||||
break;
|
|
||||||
case "min":
|
|
||||||
// Take the largest min value
|
|
||||||
stat.min = effect.result > stat.min ? effect.result : stat.min;
|
|
||||||
break;
|
|
||||||
case "max":
|
|
||||||
// Take the smallest max value
|
|
||||||
stat.max = effect.result < stat.max ? effect.result : stat.max;
|
|
||||||
break;
|
|
||||||
case "advantage":
|
|
||||||
// Sum number of advantages
|
|
||||||
stat.advantage++;
|
|
||||||
break;
|
|
||||||
case "disadvantage":
|
|
||||||
// Sum number of disadvantages
|
|
||||||
stat.disadvantage++;
|
|
||||||
break;
|
|
||||||
case "passiveAdd":
|
|
||||||
// Add all passive adds together
|
|
||||||
stat.passiveAdd += effect.result;
|
|
||||||
break;
|
|
||||||
case "fail":
|
|
||||||
// Sum number of fails
|
|
||||||
stat.fail++;
|
|
||||||
break;
|
|
||||||
case "conditional":
|
|
||||||
// Sum number of conditionals
|
|
||||||
stat.conditional++;
|
|
||||||
break;
|
|
||||||
case "rollBonus":
|
|
||||||
// Sum number of roll bonuses
|
|
||||||
stat.rollBonus++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combine the results of multiple effects to get the result of the stat
|
|
||||||
*/
|
|
||||||
function combineStat(stat, char){
|
|
||||||
if (stat.type === "attribute"){
|
|
||||||
combineAttribute(stat, char);
|
|
||||||
} else if (stat.type === "skill"){
|
|
||||||
combineSkill(stat, char);
|
|
||||||
} else if (stat.type === "damageMultiplier"){
|
|
||||||
combineDamageMultiplier(stat, char);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* combineAttribute - Combine attributes's results into final values
|
|
||||||
*/
|
|
||||||
function combineAttribute(stat, char){
|
|
||||||
stat.result = (stat.base + stat.add) * stat.mul;
|
|
||||||
if (stat.result < stat.min) stat.result = stat.min;
|
|
||||||
if (stat.result > stat.max) stat.result = stat.max;
|
|
||||||
if (!stat.decimal) stat.result = Math.floor(stat.result);
|
|
||||||
if (stat.attributeType === "ability") {
|
|
||||||
stat.mod = Math.floor((stat.result - 10) / 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combine skills results into final values
|
|
||||||
*/
|
|
||||||
function combineSkill(stat, char){
|
|
||||||
// Skills are based on some ability Modifier
|
|
||||||
let abilityMod = 0;
|
|
||||||
let ability = char.stats[stat.ability]
|
|
||||||
if (stat.ability && ability){
|
|
||||||
if (!ability.computed){
|
|
||||||
computeStat(ability, char);
|
|
||||||
}
|
|
||||||
stat.abilityMod = ability.mod;
|
|
||||||
}
|
|
||||||
// Combine all the child proficiencies
|
|
||||||
for (let i in stat.proficiencies){
|
|
||||||
let prof = stat.proficiencies[i];
|
|
||||||
if (prof.value > stat.proficiency) stat.proficiency = prof.value;
|
|
||||||
}
|
|
||||||
// Get the character's proficiency bonus to apply
|
|
||||||
let profBonus;
|
|
||||||
if (char.stats.proficiencyBonus){
|
|
||||||
if (!char.stats.proficiencyBonus.computed){
|
|
||||||
computeStat(char.stats.proficiencyBonus, char);
|
|
||||||
}
|
|
||||||
profBonus = char.stats.proficiencyBonus.result;
|
|
||||||
} else {
|
|
||||||
profBonus = Math.floor(char.level / 4 + 1.75);
|
|
||||||
}
|
|
||||||
// Multiply the proficiency bonus by the actual proficiency
|
|
||||||
profBonus *= stat.proficiency;
|
|
||||||
// Combine everything to get the final result
|
|
||||||
stat.result = (abilityMod + profBonus + stat.add) * stat.mul;
|
|
||||||
if (stat.result < stat.min) stat.result = stat.min;
|
|
||||||
if (stat.result > stat.max) stat.result = stat.max;
|
|
||||||
stat.result = Math.floor(stat.result);
|
|
||||||
if (stat.base > stat.result) stat.result = stat.base;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combine damageMultiplier's results into final values
|
|
||||||
*/
|
|
||||||
function combineDamageMultiplier(stat){
|
|
||||||
if (stat.immunityCount) return 0;
|
|
||||||
if (stat.ressistanceCount && !stat.vulnerabilityCount){
|
|
||||||
stat.result = 0.5;
|
|
||||||
} else if (!stat.ressistanceCount && stat.vulnerabilityCount){
|
|
||||||
stat.result = 2;
|
|
||||||
} else {
|
|
||||||
stat.result = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the value of a key, compute it if necessary
|
|
||||||
*/
|
|
||||||
function getComputedValueOfKey(sub, char){
|
|
||||||
const stat = char.stats[sub];
|
|
||||||
if (!stat) return null;
|
|
||||||
if (!stat.computed){
|
|
||||||
computeStat(stat, char);
|
|
||||||
}
|
|
||||||
return stat.result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluate a string computation in the context of a char
|
|
||||||
*/
|
|
||||||
function evaluateCalculation(string, char){
|
|
||||||
if (!string) return string;
|
|
||||||
// Parse the string using mathjs
|
|
||||||
let calc;
|
|
||||||
try {
|
|
||||||
calc = math.parse(string);
|
|
||||||
} catch (e) {
|
|
||||||
return string;
|
|
||||||
}
|
|
||||||
// Replace all symbols with known values
|
|
||||||
let substitutedCalc = calc.transform(node => {
|
|
||||||
if (node.isSymbolNode) {
|
|
||||||
let val = getComputedValueOfKey(node.name, char);
|
|
||||||
if (val === null) return node;
|
|
||||||
return new math.expression.node.ConstantNode(val);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Evaluate the expression to a number or return with substitutions
|
|
||||||
try {
|
|
||||||
return substitutedCalc.eval();
|
|
||||||
} catch (e){
|
|
||||||
return substitutedCalc.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeCreature(char) {
|
|
||||||
//TODO these functions don't filter the stats before trying to write
|
|
||||||
writeAttributes(char);
|
|
||||||
writeSkills(char);
|
|
||||||
writeDamageMultipliers(char);
|
|
||||||
writeEffects(char);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Write all the attributes from the in-memory char object to the Attirbute docs
|
|
||||||
*/
|
|
||||||
function writeAttributes(char) {
|
|
||||||
let bulkWriteOps = _.map(char.atts, (att, variableName) => {
|
|
||||||
let op = {
|
|
||||||
updateMany: {
|
|
||||||
filter: {'ancestors.id': char.id, variableName},
|
|
||||||
update: {'$set': {
|
|
||||||
value: att.result,
|
|
||||||
rollBonuses: skill.rollBonus,
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (typeof att.mod === 'number'){
|
|
||||||
op.updateMany.update.$set.mod = att.mod;
|
|
||||||
} else {
|
|
||||||
op.updateMany.update.$unset = {mod: 1};
|
|
||||||
}
|
|
||||||
return op;
|
|
||||||
});
|
|
||||||
bulkWriteProperties({bulkWriteOps, selectorType: 'attribute'});
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeSkills(char) {
|
|
||||||
let bulkWriteOps = _.map(char.skills, (skill, variableName) => {
|
|
||||||
let op = {
|
|
||||||
updateMany: {
|
|
||||||
filter: {'ancestors.id': char.id, variableName},
|
|
||||||
update: {$set: {
|
|
||||||
value: skill.result,
|
|
||||||
abilityMod: skill.abilityMod,
|
|
||||||
advantage: skill.advantage,
|
|
||||||
passiveBonus: skill.passiveAdd,
|
|
||||||
proficiency: skill.proficiency,
|
|
||||||
conditionalBenefits: skill.conditional,
|
|
||||||
rollBonuses: skill.rollBonus,
|
|
||||||
fail: skill.fail,
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return op;
|
|
||||||
});
|
|
||||||
bulkWriteProperties({bulkWriteOps, selectorType: 'skill'});
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeDamageMultipliers(char) {
|
|
||||||
let bulkWriteOps = _.map(char.dms, (dm, variableName) => {
|
|
||||||
let op = {
|
|
||||||
updateMany: {
|
|
||||||
filter: {'ancestors.id': char.id, variableName},
|
|
||||||
update: {$set: {
|
|
||||||
value: dm.result,
|
|
||||||
}},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return op;
|
|
||||||
});
|
|
||||||
bulkWriteProperties({bulkWriteOps, selectorType: 'damageMultiplier'});
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeEffects(char){
|
|
||||||
let bulkWriteOps = _.map(char.computedEffects, effect => ({
|
|
||||||
updateOne: {
|
|
||||||
filter: {_id: effect._id},
|
|
||||||
update: {$set: {
|
|
||||||
result: effect.result,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
if (!bulkWriteOps.length) return;
|
|
||||||
bulkWriteProperties({bulkWriteOps, selectorType: 'effect'});
|
|
||||||
}
|
|
||||||
|
|
||||||
function bulkWriteProperties({bulkWriteOps, selectorType}){
|
|
||||||
if (!bulkWriteOps.length) return;
|
|
||||||
if (Meteor.isServer){
|
|
||||||
CreatureProperties.rawCollection().bulkWrite(bulkWriteOps, {ordered : false}, function(e){
|
|
||||||
if (e) console.error(e);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_.each(bulkWriteOps, op => {
|
|
||||||
CreatureProperties.update(op.updateMany.filter, op.updateMany.update, {
|
|
||||||
multi: true,
|
|
||||||
selector: {type: selectorType}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { recomputeCreatureById } from '/imports/api/creature/creatureComputation.js';
|
import { recomputeCreatureById } from '/imports/api/creature/computation/creatureComputation.js';
|
||||||
|
|
||||||
export default function recomputeCreatureMixin(methodOptions){
|
export default function recomputeCreatureMixin(methodOptions){
|
||||||
let runFunc = methodOptions.run;
|
let runFunc = methodOptions.run;
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ let SkillSchema = new SimpleSchema({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let ComputedOnlySkillSchema = new SimpleSchema({
|
||||||
let ComputedSkillSchema = new SimpleSchema({
|
|
||||||
// Computed value of skill to be added to skill rolls
|
// Computed value of skill to be added to skill rolls
|
||||||
value: {
|
value: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -80,16 +79,29 @@ let ComputedSkillSchema = new SimpleSchema({
|
|||||||
allowedValues: [0, 0.5, 1, 2],
|
allowedValues: [0, 0.5, 1, 2],
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
// Computed number of total conditional benefits
|
// Computed number of total conditional benefits
|
||||||
conditionalBenefits: {
|
conditionalBenefits: {
|
||||||
type: SimpleSchema.Integer,
|
type: Array,
|
||||||
optional: true,
|
optional: true,
|
||||||
|
},
|
||||||
|
'conditionalBenefits.$': {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
// Computed number of things forcing this skill to fail
|
||||||
|
rollBonuses: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
'rollBonuses.$': {
|
||||||
|
type: String,
|
||||||
},
|
},
|
||||||
// Computed number of things forcing this skill to fail
|
// Computed number of things forcing this skill to fail
|
||||||
fail: {
|
fail: {
|
||||||
type: SimpleSchema.Integer,
|
type: SimpleSchema.Integer,
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
}).extend(SkillSchema);
|
})
|
||||||
|
|
||||||
export { SkillSchema, ComputedSkillSchema };
|
let ComputedSkillSchema = ComputedOnlySkillSchema.extend(SkillSchema);
|
||||||
|
|
||||||
|
export { SkillSchema, ComputedSkillSchema, ComputedOnlySkillSchema };
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
|
||||||
|
|
||||||
let SpellListSchema = new SimpleSchema({
|
let SpellListSchema = new SimpleSchema({
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
// The technical, lowercase, single-word name used in formulae
|
|
||||||
variableName: {
|
|
||||||
type: String,
|
|
||||||
regEx: VARIABLE_NAME_REGEX,
|
|
||||||
min: 3,
|
|
||||||
},
|
|
||||||
description: {
|
description: {
|
||||||
type: String,
|
type: String,
|
||||||
optional: true,
|
optional: true,
|
||||||
|
|||||||
7
app/imports/constants/RESERVED_VARIABLE_NAMES.js
Normal file
7
app/imports/constants/RESERVED_VARIABLE_NAMES.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const RESERVED_VARIABLE_NAMES = Object.freeze([
|
||||||
|
'allChecks',
|
||||||
|
'allSaves',
|
||||||
|
'attackRolls',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default RESERVED_VARIABLE_NAMES;
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
import SpellsTab from '/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue';
|
import SpellsTab from '/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue';
|
||||||
import PersonaTab from '/imports/ui/creature/character/characterSheetTabs/PersonaTab.vue';
|
import PersonaTab from '/imports/ui/creature/character/characterSheetTabs/PersonaTab.vue';
|
||||||
import TreeTab from '/imports/ui/creature/character/characterSheetTabs/TreeTab.vue';
|
import TreeTab from '/imports/ui/creature/character/characterSheetTabs/TreeTab.vue';
|
||||||
import { recomputeCreature } from '/imports/api/creature/creatureComputation.js';
|
import { recomputeCreature } from '/imports/api/creature/computation/creatureComputation.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import "/imports/server/publications/index.js";
|
import "/imports/server/publications/index.js";
|
||||||
import "/imports/api/creature/creatureComputation.js";
|
import "/imports/api/creature/computation/creatureComputation.js";
|
||||||
import "/imports/api/parenting/deleteRemovedDocuments.js";
|
import "/imports/api/parenting/deleteRemovedDocuments.js";
|
||||||
import "/imports/server/config/simpleSchemaDebug.js";
|
import "/imports/server/config/simpleSchemaDebug.js";
|
||||||
import "/imports/api/parenting/organizeMethods.js";
|
import "/imports/api/parenting/organizeMethods.js";
|
||||||
|
|||||||
Reference in New Issue
Block a user