Merge branch 'version-2-dev' into version-2

This commit is contained in:
Stefan Zermatten
2022-02-22 12:07:10 +02:00
401 changed files with 12276 additions and 7292 deletions

View File

@@ -3,44 +3,34 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
accounts-password
random
dburles:collection-helpers
reactive-var
underscore
momentjs:moment
accounts-password@2.2.0
random@1.2.0
underscore@1.0.10
dburles:mongo-collection-instances
accounts-google
email
meteorhacks:subs-manager
chuangbo:marked
meteor-base
mobile-experience
mongo
session
tracker
logging
reload
ejson
check
standard-minifier-js
shell-server
templates:array
ecmascript
es5-shim
reactive-dict
accounts-google@1.4.0
email@2.2.0
meteor-base@1.5.1
mobile-experience@1.1.0
mongo@1.14.0
session@1.2.0
tracker@1.2.0
logging@1.3.1
reload@1.3.1
ejson@1.1.1
check@1.3.1
standard-minifier-js@2.8.0
shell-server@0.5.0
ecmascript@0.16.1
es5-shim@4.8.0
percolate:synced-cron
ongoworks:speakingurl
service-configuration
dynamic-import
ddp-rate-limiter
rate-limit
service-configuration@1.3.0
dynamic-import@0.7.2
ddp-rate-limiter@1.1.0
rate-limit@1.0.9
mdg:validated-method
akryum:vue-router2
static-html
static-html@1.3.2
aldeed:collection2
aldeed:schema-index
zer0th:meteor-vuetify-loader
accounts-patreon
bozhao:link-accounts
peerlibrary:reactive-publish
@@ -49,5 +39,11 @@ simple:rest-method-mixin
mikowals:batch-insert
peerlibrary:subscription-data
seba:minifiers-autoprefixer
zer0th:meteor-vuetify-loader
akryum:vue-component
akryum:vue-sass
akryum:vue-router2
percolate:migrations
meteortesting:mocha
ostrio:files
simple:rest-bearer-token-parser
simple:rest-json-error-handler

View File

@@ -1 +1 @@
METEOR@2.2.1
METEOR@2.6

View File

@@ -1,19 +1,18 @@
accounts-base@1.9.0
accounts-google@1.3.3
accounts-oauth@1.2.0
accounts-password@1.7.1
accounts-base@2.2.1
accounts-google@1.4.0
accounts-oauth@1.4.0
accounts-password@2.2.0
accounts-patreon@0.1.0
akryum:npm-check@0.1.2
akryum:vue-component@0.15.2
akryum:vue-component-dev-client@0.4.7
akryum:vue-component-dev-server@0.1.4
akryum:vue-router2@0.2.3
akryum:vue-sass@0.1.2
aldeed:collection2@3.4.1
aldeed:collection2@3.5.0
aldeed:schema-index@3.0.0
allow-deny@1.1.0
autoupdate@1.7.0
babel-compiler@7.6.2
allow-deny@1.1.1
autoupdate@1.8.0
babel-compiler@7.8.0
babel-runtime@1.5.0
base64@1.0.12
binary-heap@1.0.11
@@ -22,66 +21,63 @@ boilerplate-generator@1.7.1
bozhao:link-accounts@2.4.0
caching-compiler@1.2.2
caching-html-compiler@1.2.1
callback-hook@1.3.0
callback-hook@1.4.0
check@1.3.1
chuangbo:marked@0.3.5_1
coffeescript@2.4.1
coffeescript-compiler@2.4.1
dburles:collection-helpers@1.1.0
dburles:mongo-collection-instances@0.3.5
dburles:mongo-collection-instances@0.3.6
ddp@1.4.0
ddp-client@2.4.1
ddp-client@2.5.0
ddp-common@1.4.0
ddp-rate-limiter@1.0.9
ddp-server@2.3.3
deps@1.0.12
ddp-rate-limiter@1.1.0
ddp-server@2.5.0
diff-sequence@1.1.1
dynamic-import@0.6.0
ecmascript@0.15.1
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.11.1
ecmascript-runtime-server@0.10.1
dynamic-import@0.7.2
ecmascript@0.16.1
ecmascript-runtime@0.8.0
ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0
ejson@1.1.1
email@2.0.0
email@2.2.0
es5-shim@4.8.0
fetch@0.1.1
geojson-utils@1.0.10
google-oauth@1.3.0
google-oauth@1.4.1
hot-code-push@1.0.4
html-tools@1.1.2
htmljs@1.1.1
http@1.4.4
http@2.0.0
id-map@1.1.1
inter-process-messaging@0.1.1
lai:collection-extensions@0.2.1_1
launch-screen@1.2.1
livedata@1.0.18
lai:collection-extensions@0.3.0
launch-screen@1.3.0
localstorage@1.2.0
logging@1.2.0
logging@1.3.1
mdg:validated-method@1.2.0
meteor@1.9.3
meteor-base@1.4.0
meteorhacks:subs-manager@1.6.4
mikowals:batch-insert@1.2.0
minifier-css@1.5.4
minifier-js@2.6.1
minimongo@1.6.2
meteor@1.10.0
meteor-base@1.5.1
meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3
meteortesting:mocha-core@8.1.2
mikowals:batch-insert@1.3.0
minifier-css@1.6.0
minifier-js@2.7.3
minimongo@1.8.0
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.5
modules@0.16.0
modern-browsers@0.1.7
modules@0.18.0
modules-runtime@0.12.0
momentjs:moment@2.29.1
mongo@1.11.1
mongo@1.14.4
mongo-decimal@0.1.2
mongo-dev-server@1.1.0
mongo-id@1.0.8
npm-bcrypt@0.9.4
npm-mongo@3.9.0
oauth@1.3.2
oauth2@1.3.0
ongoworks:speakingurl@9.0.0
npm-mongo@4.3.1
oauth@2.1.1
oauth2@1.3.1
ordered-dict@1.1.0
ostrio:cookies@2.7.0
ostrio:files@2.0.1
patreon-oauth@0.1.0
peerlibrary:assert@0.3.0
peerlibrary:check-extension@0.7.0
@@ -89,41 +85,42 @@ peerlibrary:computed-field@0.10.0
peerlibrary:data-lookup@0.3.0
peerlibrary:extend-publish@0.6.0
peerlibrary:fiber-utils@0.10.0
peerlibrary:reactive-mongo@0.4.0
peerlibrary:reactive-mongo@0.4.1
peerlibrary:reactive-publish@0.10.0
peerlibrary:server-autorun@0.8.0
peerlibrary:subscription-data@0.8.0
percolate:migrations@1.0.3
percolate:synced-cron@1.3.2
promise@0.11.2
promise@0.12.0
raix:eventemitter@1.0.0
random@1.2.0
rate-limit@1.0.9
react-fast-refresh@0.1.1
react-fast-refresh@0.2.2
reactive-dict@1.3.0
reactive-var@1.0.11
reload@1.3.1
retry@1.1.0
routepolicy@1.1.0
routepolicy@1.1.1
seba:minifiers-autoprefixer@2.0.1
service-configuration@1.0.11
service-configuration@1.3.0
session@1.2.0
sha@1.0.9
shell-server@0.5.0
simple:json-routes@2.1.0
simple:rest@1.1.1
simple:rest-method-mixin@1.0.1
socket-stream-client@0.3.3
simple:json-routes@2.3.1
simple:rest@1.2.1
simple:rest-bearer-token-parser@1.1.1
simple:rest-json-error-handler@1.1.1
simple:rest-method-mixin@1.1.0
socket-stream-client@0.4.0
spacebars-compiler@1.3.0
srp@1.1.0
standard-minifier-js@2.6.1
standard-minifier-js@2.8.0
static-html@1.3.2
templates:array@1.0.3
templating-tools@1.2.1
tmeasday:check-npm-versions@1.0.2
tracker@1.2.0
typescript@4.2.2
typescript@4.4.1
underscore@1.0.10
url@1.3.2
webapp@1.10.1
webapp@1.13.0
webapp-hashing@1.1.0
zer0th:meteor-vuetify-loader@0.1.30
zer0th:meteor-vuetify-loader@0.1.41

View File

@@ -1,4 +1,7 @@
import '/imports/api/simpleSchemaConfig.js';
import '/imports/ui/vueSetup.js';
import '/imports/ui/styles/stylesIndex.js';
import '/imports/client/config.js';
import '/imports/client/serviceWorker.js';
import 'ngraph.graph';

View File

@@ -1,13 +0,0 @@
import spendResources from '/imports/api/creature/actions/spendResources.js'
import embedInlineCalculations from '/imports/api/creature/computation/afterComputation/embedInlineCalculations.js';
export default function applyAction({prop, log}){
let content = { name: prop.name };
if (prop.summary){
content.value = embedInlineCalculations(
prop.summary, prop.summaryCalculations
);
}
log.content.push(content);
spendResources({prop, log});
}

View File

@@ -1,55 +0,0 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import damagePropertiesByName from '/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js';
export default function applyAdjustment({
prop,
creature,
targets,
actionContext,
log
}){
let damageTargets = prop.target === 'self' ? [creature] : targets;
let scope = {
...creature.variables,
...actionContext,
};
var {result, context} = evaluateString({
string: prop.amount,
scope,
fn: 'reduce'
});
context.errors.forEach(e => {
log.content.push({
name: 'Attribute damage error',
value: e.message || e.toString(),
});
});
if (damageTargets) {
damageTargets.forEach(target => {
if (prop.target === 'each'){
({result} = evaluateString({
string: prop.amount,
scope,
fn: 'reduce'
}));
}
damagePropertiesByName.call({
creatureId: target._id,
variableName: prop.stat,
operation: prop.operation || 'increment',
value: result.value,
});
log.content.push({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${result.isNumber ? -result.value : result.toString()}`,
});
});
} else {
log.content.push({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${result.isNumber ? -result.value : result.toString()}`,
});
}
}

View File

@@ -1,22 +0,0 @@
import roll from '/imports/parser/roll.js';
export default function applyAttack({
prop,
log,
actionContext,
creature,
}){
let value = roll(1, 20)[0];
actionContext.attackRoll = {value};
let criticalHitTarget = creature.variables.criticalHitTarget &&
creature.variables.criticalHitTarget.currentValue || 20;
let criticalHit = value >= criticalHitTarget;
if (criticalHit) actionContext.criticalHit = {value: true};
let result = value + prop.rollBonusResult;
actionContext.toHit = {value: result};
log.content.push({
name: criticalHit ? 'Critical Hit!' : 'To Hit',
value: `1d20 [${value}] + ${prop.rollBonusResult} = ` + result,
});
}

View File

@@ -1,61 +0,0 @@
import {
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import {setDocToLastOrder} from '/imports/api/parenting/order.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function applyBuff({
prop,
children,
creature,
targets = [],
//actionContext,
}){
let buffTargets = prop.target === 'self' ? [creature] : targets;
//let scope = {
// ...creature.variables,
// ...actionContext,
//};
// TODO
// If the target is not self, walk through all decendants and replace
// variables in calculations with their values from the creature scope
// If the target is self, replace all the target.x references with just x
// Then copy the decendants of the buff to the targets
prop.applied = true;
let propList = [prop];
function addChildrenToPropList(children){
children.forEach(child => {
propList.push(child.node);
addChildrenToPropList(child.children);
});
}
addChildrenToPropList(children);
let oldParent = {
id: prop.parent.id,
collection: prop.parent.collection,
};
buffTargets.forEach(target => {
copyNodeListToTarget(propList, target, oldParent);
});
}
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);
}

View File

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

View File

@@ -1,82 +0,0 @@
import applyAction from '/imports/api/creature/actions/applyAction.js';
import applyAdjustment from '/imports/api/creature/actions/applyAdjustment.js';
import applyAttack from '/imports/api/creature/actions/applyAttack.js';
import applyBuff from '/imports/api/creature/actions/applyBuff.js';
import applyDamage from '/imports/api/creature/actions/applyDamage.js';
import applyRoll from '/imports/api/creature/actions/applyRoll.js';
import applyToggle from '/imports/api/creature/actions/applyToggle.js';
import applySave from '/imports/api/creature/actions/applySave.js';
function applyProperty(options){
let prop = options.prop;
if (prop.type === 'buff'){
// ignore only applied buffs, don't apply them again
if (prop.applied === true){
return false;
}
// Only ignore toggles if they wont be computed
} else if (prop.type === 'toggle') {
if (prop.disabled) return false;
if (prop.enabled) return true;
if (!prop.condition) return false;
// Ignore inactive props of other types
} else if (prop.deactivatedBySelf === true){
return false;
}
switch (prop.type){
case 'action':
case 'spell':
applyAction(options);
break;
case 'attack':
applyAction(options);
applyAttack(options);
break;
case 'damage':
applyDamage(options);
break;
case 'adjustment':
applyAdjustment(options);
break;
case 'buff':
applyBuff(options);
return false;
case 'toggle':
return applyToggle(options);
case 'roll':
applyRoll(options);
break;
case 'savingThrow':
return applySave(options);
}
return true;
}
function applyPropertyAndWalkChildren({prop, children, targets, ...options}){
let shouldKeepWalking = applyProperty({ prop, children, targets, ...options });
if (shouldKeepWalking){
applyProperties({ forest: children, targets, ...options,});
}
}
export default function applyProperties({ forest, targets, ...options}){
forest.forEach(node => {
let prop = node.node;
options.actionContext[`#${prop.type}`] = prop;
let children = node.children;
if (shouldSplit(prop) && targets.length){
targets.forEach(target => {
let targets = [target]
applyPropertyAndWalkChildren({ targets, prop, children, ...options});
});
} else {
applyPropertyAndWalkChildren({prop, children, targets, ...options});
}
});
}
function shouldSplit(prop){
if (prop.target === 'each'){
return true;
}
}

View File

@@ -1,25 +0,0 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
export default function applyRoll({
prop,
creature,
actionContext,
log,
}){
let scope = {
...creature.variables,
...actionContext,
};
var {result} = evaluateString({
string: prop.roll,
scope,
fn: 'reduce'
});
if (result.isNumber){
actionContext[prop.variableName] = result.value;
}
log.content.push({
name: prop.name,
value: prop.variableName + ' = ' + prop.roll + ' = ' + result.toString(),
});
}

View File

@@ -1,76 +0,0 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import CreaturesProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import roll from '/imports/parser/roll.js';
export default function applySave({
prop,
creature,
actionContext,
log,
}){
let scope = {
...creature.variables,
...actionContext,
};
try {
// Calculate the DC
var {result} = evaluateString({
string: prop.dc,
scope,
fn: 'reduce'
});
let dc = result.value;
log.content.push({
name: prop.name,
value: ' DC ' + result.toString(),
});
if (prop.target === 'self'){
let save = CreaturesProperties.findOne({
'ancestors.id': creature._id,
type: 'skill',
skillType: 'save',
variableName: prop.stat,
removed: {$ne: true},
inactive: {$ne: true},
});
if (!save){
log.content.push({
name: 'Saving throw error',
value: 'No saving throw found: ' + prop.stat,
});
return;
}
let value, values, resultPrefix;
if (save.advantage === 1){
values = roll(2, 20).sort().reverse();
value = values[0];
resultPrefix = `Advantage: 1d20 [${values[0]},~~${values[1]}~~] + ${save.value} = `
} else if (save.advantage === -1){
values = roll(2, 20).sort();
value = values[0];
resultPrefix = `Disadvantage: 1d20 [${values[0]},~~${values[1]}~~] + ${save.value} = `
} else {
values = roll(1, 20);
value = values[0];
resultPrefix = `1d20 [${value}] + ${save.value} = `
}
actionContext.savingThrowRoll = {value};
let result = value + save.value;
actionContext.savingThrow = {value: result};
let saveSuccess = result >= dc;
log.content.push({
name: 'Save',
value: resultPrefix + result + (saveSuccess ? 'Passed' : 'Failed')
});
return !saveSuccess;
} else {
// TODO
return true;
}
} catch (e){
log.content.push({
name: 'Save error',
value: e.toString(),
});
}
}

View File

@@ -1,33 +0,0 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
export default function applyToggle({
prop,
creature,
actionContext,
log,
}){
let scope = {
...creature.variables,
...actionContext,
};
if (Number.isFinite(+prop.condition)){
return !!+prop.condition;
}
var {result} = evaluateString({
string: prop.condition,
scope,
fn: 'reduce'
});
if (result.constructor.name === 'ErrorNode') {
log.content.push({
name: 'Toggle error',
value: result.toString(),
});
return false;
}
log.content.push({
name: prop.name || 'Toggle',
value: prop.condition + ' = ' + result.toString(),
});
return !!result.value;
}

View File

@@ -1,92 +0,0 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { doActionWork } from '/imports/api/creature/actions/doAction.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import getAncestorContext from '/imports/api/creature/actions/getAncestorContext.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties';
const castSpellWithSlot = new ValidatedMethod({
name: 'creatureProperties.castSpellWithSlot',
validate: new SimpleSchema({
spellId: SimpleSchema.RegEx.Id,
slotId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
targetId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({spellId, slotId, targetId}) {
let spell = CreatureProperties.findOne(spellId);
// Check permissions
let creature = getRootCreatureAncestor(spell);
assertEditPermission(creature, this.userId);
let target = undefined;
if (targetId) {
target = Creatures.findOne(targetId);
assertEditPermission(target, this.userId);
}
let slotLevel = spell.level || 0;
if (slotLevel !== 0){
let slot = CreatureProperties.findOne(slotId);
if (!slot){
throw new Meteor.Error('No slot',
'Slot not found to cast spell');
}
if (!slot.currentValue){
throw new Meteor.Error('No slot',
'Slot depleted');
}
if (!(slot.spellSlotLevelValue >= spell.level)){
throw new Meteor.Error('Slot too small',
'Slot is not large enough to cast spell');
}
slotLevel = slot.spellSlotLevelValue;
damagePropertyWork({
property: slot,
operation: 'increment',
value: 1,
});
}
let actionContext = getAncestorContext(spell);
doActionWork({
action: spell,
actionContext: {slotLevel, ...actionContext},
creature,
targets: target ? [target] : [],
method: this,
});
// Note these lines only recompute the top-level creature, not the nearest one
// The acting creature might have a new item
recomputeInventory(creature._id);
// The spell might add properties which need to be activated
recomputeInactiveProperties(creature._id);
recomputeCreatureByDoc(creature);
if (target){
recomputeInventory(target._id);
recomputeInactiveProperties(target._id);
recomputeCreatureByDoc(target);
}
},
});
export default castSpellWithSlot;

View File

@@ -1,56 +0,0 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import roll from '/imports/parser/roll.js';
const doCheck = new ValidatedMethod({
name: 'creature.doCheck',
validate: new SimpleSchema({
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
attributeName: {
type: String,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({creatureId, attributeName}) {
let creature = Creatures.findOne(creatureId);
assertEditPermission(creature, this.userId);
let bonus = getAttributeValue({creature, attributeName})
return doCheckWork({bonus});
},
});
function getAttributeValue({creature, attributeName}){
let att = creature.variables[attributeName];
if (!att) throw new Meteor.Error('No such attribute',
`This creature does not have a ${attributeName} property`);
let bonus = att.attributeType === 'ability'? att.modifier : att.value;
return bonus || 0;
}
export function doCheckWork({bonus, advantage = 0}){
let rolls = roll(2,20);
let chosenRoll;
if (advantage === 1){
chosenRoll = Math.max.apply(rolls);
} else if (advantage === -1){
chosenRoll = Math.min.apply(rolls);
} else {
chosenRoll = rolls[0];
}
let result = chosenRoll + bonus;
return {rolls, bonus, chosenRoll, result};
}
export default doCheck;

View File

@@ -1,15 +0,0 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function getAncestorContext(prop){
// Build ancestor context
const actionContext = {};
let ancestorIds = prop.ancestors.map(ref => ref.id);
CreatureProperties.find({
_id: {$in: ancestorIds}
}, {
sort: {order: 1},
}).forEach(ancestor => {
actionContext[`#${ancestor.type}`] = ancestor;
});
return actionContext;
}

View File

@@ -1,93 +0,0 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
export default function spendResources({prop, log}){
// Check Uses
if (prop.usesUsed >= prop.usesResult){
throw new Meteor.Error('Insufficient Uses',
'This prop has no uses left');
}
// Resources
if (prop.insufficientResources){
throw new Meteor.Error('Insufficient Resources',
'This creature doesn\'t have sufficient resources to perform this prop');
}
// Items
let itemQuantityAdjustments = [];
let spendLog = [];
let gainLog = [];
prop.resources.itemsConsumed.forEach(itemConsumed => {
if (!itemConsumed.itemId){
throw new Meteor.Error('Ammo not selected',
'No ammo was selected for this prop');
}
let item = CreatureProperties.findOne(itemConsumed.itemId);
if (!item || item.ancestors[0].id !== prop.ancestors[0].id){
throw new Meteor.Error('Ammo not found',
'The prop\'s ammo was not found on the creature');
}
if (!item.equipped){
throw new Meteor.Error('Ammo not equipped',
'The selected ammo is not equipped');
}
if (!itemConsumed.quantity) return;
itemQuantityAdjustments.push({
property: item,
operation: 'increment',
value: itemConsumed.quantity,
});
let logName = item.name;
if (itemConsumed.quantity > 1 || itemConsumed.quantity < -1){
logName = item.plural || logName;
}
if (itemConsumed.quantity > 0){
spendLog.push(logName + ': ' + itemConsumed.quantity);
} else if (itemConsumed.quantity < 0){
gainLog.push(logName + ': ' + -itemConsumed.quantity);
}
});
// No more errors should be thrown after this line
// Now that we have confirmed that there are no errors, do actual work
//Items
itemQuantityAdjustments.forEach(adjustQuantityWork);
// Use uses
if (prop.usesResult){
CreatureProperties.update(prop._id, {
$inc: {usesUsed: 1}
}, {
selector: prop
});
log.content.push({
name: 'Uses left',
value: prop.usesResult - (prop.usesUsed || 0) - 1,
});
}
// Damage stats
prop.resources.attributesConsumed.forEach(attConsumed => {
if (!attConsumed.quantity) return;
let stat = CreatureProperties.findOne(attConsumed.statId);
damagePropertyWork({
property: stat,
operation: 'increment',
value: attConsumed.quantity,
});
if (attConsumed.quantity > 0){
spendLog.push(stat.name + ': ' + attConsumed.quantity);
} else if (attConsumed.quantity < 0){
gainLog.push(stat.name + ': ' + -attConsumed.quantity);
}
});
// Log all the spending
if (gainLog.length) log.content.push({
name: 'Gained',
value: gainLog.join('\n'),
});
if (spendLog.length) log.content.push({
name: 'Spent',
value: spendLog.join('\n'),
});
}

View File

@@ -0,0 +1,17 @@
import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
const ArchiveCreatureFiles = createS3FilesCollection({
collectionName: 'archiveCreatureFiles',
storagePath: '/DiceCloud/archiveCreatures/',
onBeforeUpload(file) {
// Allow upload files under 10MB, and only in json format
if (file.size > 10485760) {
return 'Please upload with size equal or less than 10MB';
}
if (!/json/i.test(file.extension)){
return 'Please upload only a JSON file';
}
}
});
export default ArchiveCreatureFiles;

View File

@@ -0,0 +1,78 @@
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
export function getArchiveObj(creatureId){
// Build the archive document
const creature = Creatures.findOne(creatureId);
const properties = CreatureProperties.find({'ancestors.id': creatureId}).fetch();
const experiences = Experiences.find({creatureId}).fetch();
const logs = CreatureLogs.find({creatureId}).fetch();
let archiveCreature = {
meta: {
type: 'DiceCloud V2 Creature Archive',
schemaVersion: SCHEMA_VERSION,
archiveDate: new Date(),
},
creature,
properties,
experiences,
logs,
};
return archiveCreature;
}
export function archiveCreature(creatureId){
const archive = getArchiveObj(creatureId);
const buffer = Buffer.from(JSON.stringify(archive, null, 2));
ArchiveCreatureFiles.write(buffer, {
fileName: `${archive.creature.name || archive.creature._id}.json`,
type: 'application/json',
userId: archive.creature.owner,
meta: {
schemaVersion: SCHEMA_VERSION,
creatureId: archive.creature._id,
creatureName: archive.creature.name,
},
}, (error) => {
if (error){
throw error;
} else {
removeCreatureWork(creatureId);
}
}, true);
}
const archiveCreatureToFile = new ValidatedMethod({
name: 'Creatures.methods.archiveCreatureToFile',
validate: new SimpleSchema({
'creatureId': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
async run({creatureId}) {
assertOwnership(creatureId, this.userId);
if (Meteor.isServer){
archiveCreature(creatureId);
} else {
removeCreatureWork(creatureId);
}
},
});
export default archiveCreatureToFile;

View File

@@ -1,66 +0,0 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js';
function archiveCreature(creatureId){
// Build the archive document
const creature = Creatures.findOne(creatureId);
const properties = CreatureProperties.find({'ancestors.id': creatureId}).fetch();
const experiences = Experiences.find({creatureId}).fetch();
const logs = CreatureLogs.find({creatureId}).fetch();
let archiveCreature = {
owner: creature.owner,
archiveDate: new Date(),
creature,
properties,
experiences,
logs,
};
// Insert it
let id = ArchivedCreatures.insert(archiveCreature);
// Remove the original creature
removeCreatureWork(creatureId);
return id;
}
const archiveCreatures = new ValidatedMethod({
name: 'Creatures.methods.archiveCreatures',
validate: new SimpleSchema({
creatureIds: {
type: Array,
max: 10,
},
'creatureIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 5000,
},
run({creatureIds}) {
for (let id of creatureIds){
assertOwnership(id, this.userId)
}
let archivedIds = [];
for (let id of creatureIds){
let archivedId = archiveCreature(id);
archivedIds.push(archivedId);
}
return archivedIds;
},
});
export default archiveCreatures;

View File

@@ -1,2 +1,4 @@
import '/imports/api/creature/archive/methods/archiveCreatures.js';
// import '/imports/api/creature/archive/methods/archiveCreatures.js';
import '/imports/api/creature/archive/methods/archiveCreatureToFile.js';
import '/imports/api/creature/archive/methods/restoreCreatures.js';
import '/imports/api/creature/archive/methods/restoreCreatureFromFile.js';

View File

@@ -0,0 +1,82 @@
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
let migrateArchive;
if (Meteor.isServer){
migrateArchive = require('/imports/migrations/server/migrateArchive.js').default;
}
function restoreCreature(archive){
if (SCHEMA_VERSION < archive.meta.schemaVersion){
throw new Meteor.Error('Incompatible',
'The archive file is from a newer version. Update required to read.')
}
// Migrate and verify the archive meets the current schema
migrateArchive(archive);
// Insert the creature sub documents
// They still have their original _id's
Creatures.insert(archive.creature);
try {
// Add all the properties
if (archive.properties && archive.properties.length){
CreatureProperties.batchInsert(archive.properties);
}
if (archive.experiences && archive.experiences.length){
Experiences.batchInsert(archive.experiences);
}
if (archive.logs && archive.logs.length){
CreatureLogs.batchInsert(archive.logs);
}
} catch (e) {
// If the above fails, delete the inserted creature
removeCreatureWork(archive.creature._id);
throw e;
}
}
const restoreCreaturefromFile = new ValidatedMethod({
name: 'Creatures.methods.restoreCreaturefromFile',
validate: new SimpleSchema({
'fileId': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
async run({fileId}) {
// fetch the file
const file = ArchiveCreatureFiles.findOne({_id: fileId}).get();
if (!file){
throw new Meteor.Error('File not found',
'The requested creature archive does not exist');
}
// Assert ownership
const userId = file?.userId;
if (!userId || userId !== this.userId){
throw new Meteor.Error('Permission denied',
'You can only restore creatures you own');
}
if (Meteor.isServer){
// Read the file data
const archive = await ArchiveCreatureFiles.readJSONFile(file);
restoreCreature(archive);
}
//Remove the archive once the restore succeeded
ArchiveCreatureFiles.remove({_id: fileId});
},
});
export default restoreCreaturefromFile;

View File

@@ -9,7 +9,7 @@ import Experiences from '/imports/api/creature/experience/Experiences.js';
import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
function restoreCreature(archiveId){
export function restoreCreature(archiveId){
// Get the archive
const archivedCreature = ArchivedCreatures.findOne(archiveId);

View File

@@ -1,11 +0,0 @@
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
export default function embedInlineCalculations(string, calculations){
if (!string) return '';
if (!calculations) return string;
let index = 0;
return string.replace(INLINE_CALCULATION_REGEX, substring => {
let comp = calculations && calculations[index++];
return (comp && 'result' in comp) ? comp.result : substring;
});
}

View File

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

View File

@@ -1,97 +0,0 @@
import {computeCreature} from "./recomputeCreature.js";
import assert from "assert";
const makeEffect = function(operation, value){
let effect = {computed: false, result: 0, operation}
if (_.isFinite(value)){
effect.value = +value;
} else {
effect.calculation = value;
}
return effect;
}
describe('computeCreature', function () {
it('computes an aritrary creature', function () {
let char = {
atts: {
attribute1: {
computed: false,
busyComputing: false,
type: "attribute",
attributeType: "ability",
result: 0,
mod: 0, // The resulting modifier if this is an ability
base: 0,
add: 0,
mul: 1,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
effects: [
makeEffect("base", 10),
makeEffect("add", 5),
makeEffect("mul", 2),
],
},
attribute2: {
computed: false,
busyComputing: false,
type: "attribute",
result: 0,
mod: 0, // The resulting modifier if this is an ability
base: 0,
add: 0,
mul: 1,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
effects: [
makeEffect("base", "attribute1"),
makeEffect("max", 2),
],
},
},
skills: {
skill1: {
computed: false,
busyComputing: false,
type: "skill",
ability: "attribute1",
result: 0,
proficiency: 0,
add: 0,
mul: 1,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
advantage: 0,
disadvantage: 0,
passiveAdd: 0,
fail: 0,
conditional: 0,
effects: [],
proficiencies: [],
},
},
dms: {
dm1: {
computed: false,
busyComputing: false,
type: "damageMultiplier",
result: 0,
immunityCount: 0,
ressistanceCount: 0,
vulnerabilityCount: 0,
effects: [],
}
},
classes: {
Barbarian: {
level: 5,
},
},
level: 5,
};
char = computeCreature(char);
console.log(char);
assert(true);
});
});

View File

@@ -1,289 +0,0 @@
import { includes, cloneDeep } from 'lodash';
import findAncestorByType from '/imports/api/creature/computation/engine/findAncestorByType.js';
// The computation memo is an in-memory data structure used only during the
// computation process
export default class ComputationMemo {
constructor(props, creature){
this.statsByVariableName = {};
this.constantsByVariableName = {};
this.constantsById = {};
this.extraStatsByVariableName = {};
this.statsById = {};
this.originalPropsById = {};
this.propsById = {};
this.skillsByAbility = {};
this.unassignedEffects = [];
this.classLevelsById = {};
this.classes = {};
this.togglesById = {};
this.toggleIds = new Set();
// Equipped items that might be used as ammo
this.equipmentById = {};
// Properties that have calculations, but don't impact other properties
this.endStepPropsById = {};
// First note all the ids of all the toggles
props.forEach((prop) => {
if (
prop.type === 'toggle'
) {
this.toggleIds.add(prop._id);
}
});
props.filter((prop) => {
if (
prop.type === 'toggle'
) {
this.addToggle(prop);
} else {
return true;
}
}).filter((prop) => {
if (
prop.type === 'attribute' ||
prop.type === 'skill'
) {
// Add all the stats
this.addStat(prop);
} else if (
prop.type === 'item'
) {
this.addEquipment(prop);
} else {
return true;
}
}).forEach((prop) => {
// Now add everything else
if (prop.type === 'effect'){
this.addEffect(prop);
} else if (prop.type === 'proficiency') {
this.addProficiency(prop);
} else if (prop.type === 'classLevel'){
this.addClassLevel(prop);
} else if (prop.type === 'constant'){
this.addConstant(prop);
} else {
this.addEndStepProp(prop);
}
});
for (let name in creature.denormalizedStats){
if (!this.statsByVariableName[name]){
this.statsByVariableName[name] = {
variableName: name,
value: creature.denormalizedStats[name],
computationDetails: propDetailsByType.denormalizedStat(),
}
}
}
}
addConstant(prop){
prop = this.registerProperty(prop);
this.constantsById[prop._id] = prop;
}
registerProperty(prop){
this.originalPropsById[prop._id] = cloneDeep(prop);
this.propsById[prop._id] = prop;
prop.dependencies = [];
prop.computationDetails = propDetails(prop);
prop.ancestors.forEach(ancestor => {
if (this.toggleIds.has(ancestor.id)){
prop.computationDetails.toggleAncestors.push(ancestor.id);
}
});
return prop;
}
addToggle(prop){
prop = this.registerProperty(prop);
this.togglesById[prop._id] = prop;
}
addClassLevel(prop){
prop = this.registerProperty(prop);
this.classLevelsById[prop._id] = prop;
}
addStat(prop){
let variableName = prop.variableName;
if (!variableName) return;
let existingStat = this.statsByVariableName[variableName];
prop = this.registerProperty(prop);
if (existingStat){
existingStat.computationDetails.idsOfSameName.push(prop._id);
} else {
this.statsById[prop._id] = prop;
this.statsByVariableName[variableName] = prop;
if (
prop.type === 'skill' &&
isSkillCheck(prop) &&
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 => {
if (target.computationDetails && target.computationDetails.effects){
target.computationDetails.effects.push(prop);
}
});
if (!targets.size){
this.unassignedEffects.push(prop);
}
}
getEffectTargets(prop){
let targets = new Set();
if (!prop.stats) return targets;
prop.stats.forEach((statName) => {
let target;
if (statName[0] === '#'){
target = findAncestorByType({
type: statName.slice(1),
prop,
memo: this
});
} else {
target = this.statsByVariableName[statName];
}
if (!target) return;
targets.add(target);
if (isSkillOperation(prop) && isAbility(target)){
let extras = this.skillsByAbility[statName] || [];
extras.forEach(ex =>{
// Only pass on ability effects to skills and checks
if (ex.skillType === 'skill' || ex.skillType === 'check'){
targets.add(ex)
}
});
}
});
return targets;
}
addProficiency(prop){
prop = this.registerProperty(prop);
let targets = this.getProficiencyTargets(prop);
targets.forEach(target => {
if(target.computationDetails.proficiencies){
target.computationDetails.proficiencies.push(prop);
}
});
}
getProficiencyTargets(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 (isAbility(target)) {
let extras = this.skillsByAbility[statName] || [];
extras.forEach(ex =>{
// Only pass on ability proficiencies to skills and checks
if (ex.skillType === 'skill' || ex.skillType === 'check'){
targets.add(ex)
}
});
}
});
return targets;
}
addEquipment(prop){
prop = this.registerProperty(prop);
this.equipmentById[prop._id] = prop;
}
addEndStepProp(prop){
prop = this.registerProperty(prop);
this.endStepPropsById[prop._id] = prop;
}
}
function isAbility(prop){
return prop.type === 'attribute' &&
prop.attributeType === 'ability'
}
function isSkillCheck(prop){
return includes(['skill', 'check', 'save', 'utility'], prop.skillType);
}
const skillOperations = [
'advantage',
'disadvantage',
'passiveAdd',
'fail',
'conditional',
'rollBonus',
];
function isSkillOperation(prop){
return skillOperations.includes(prop.operation);
}
function propDetails(prop){
return propDetailsByType[prop.type] && propDetailsByType[prop.type]() ||
propDetailsByType.default();
}
const propDetailsByType = {
default(){
return {
toggleAncestors: [],
};
},
toggle(){
return {
computed: false,
busyComputing: false,
toggleAncestors: [],
};
},
attribute(){
return {
computed: false,
busyComputing: false,
effects: [],
proficiencies: [],
toggleAncestors: [],
idsOfSameName: [],
};
},
skill(){
return {
computed: false,
busyComputing: false,
effects: [],
proficiencies: [],
toggleAncestors: [],
idsOfSameName: [],
};
},
effect(){
return {
computed: false,
busyComputing: false,
toggleAncestors: [],
};
},
classLevel(){
return {
computed: true,
toggleAncestors: [],
};
},
proficiency(){
return {
toggleAncestors: [],
};
},
denormalizedStat(){
return {
toggleAncestors: [],
};
}
}

View File

@@ -1,78 +0,0 @@
export default class EffectAggregator{
constructor(){
this.base = undefined;
this.add = 0;
this.mul = 1;
this.min = Number.NEGATIVE_INFINITY;
this.max = Number.POSITIVE_INFINITY;
this.advantage = 0;
this.disadvantage = 0;
this.passiveAdd = undefined;
this.fail = 0;
this.set = undefined;
this.conditional = [];
this.rollBonus = [];
this.hasNoEffects = true;
}
addEffect(effect){
let result = effect.result;
if (this.hasNoEffects) this.hasNoEffects = false;
switch(effect.operation){
case 'base':
// Take the largest base value
if (Number.isFinite(result)){
if(Number.isFinite(this.base)){
this.base = Math.max(this.base, result);
} else {
this.base = result;
}
}
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 'set':
// Take the highest set value
this.set = this.set === undefined || result > this.set ? result : this.set;
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
if (this.passiveAdd === undefined) this.passiveAdd = 0;
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;
}
}
}

View File

@@ -1,25 +0,0 @@
import computeToggle from '/imports/api/creature/computation/engine/computeToggle.js';
import { union } from 'lodash';
export default function applyToggles(prop, memo){
// If it used to be inactive delete those fields
if (prop.inactive) prop.inactive = undefined;
if (prop.deactivatedByAncestor) prop.deactivatedByAncestor = undefined;
if (prop.deactivatedByToggle) prop.deactivatedByToggle = undefined;
// Iterate through the toggle ancestors from oldest to nearest
prop.computationDetails.toggleAncestors.forEach(toggleId => {
let toggle = memo.togglesById[toggleId];
computeToggle(toggle, memo);
prop.dependencies = union(
prop.dependencies,
[toggle._id],
toggle.dependencies,
);
// Deactivate if the toggle is false
if (!toggle.toggleResult){
prop.inactive = true;
prop.deactivatedByAncestor = true;
prop.deactivatedByToggle = true;
}
});
}

View File

@@ -1,197 +0,0 @@
import computeStat from '/imports/api/creature/computation/engine/computeStat.js';
import computeProficiency from '/imports/api/creature/computation/engine/computeProficiency.js';
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import stripFloatingPointOddities from '/imports/ui/utility/stripFloatingPointOddities.js';
import { union } from 'lodash';
export default function combineStat(stat, aggregator, memo){
if (stat.type === 'attribute'){
combineAttribute(stat, aggregator, memo);
} else if (stat.type === 'skill'){
combineSkill(stat, aggregator, memo);
} else if (stat.type === 'damageMultiplier'){
combineDamageMultiplier(stat, memo);
}
}
function getAggregatorResult(stat, aggregator){
let base;
if (!Number.isFinite(aggregator.base)){
base = stat.baseValue || 0;
} else if (!Number.isFinite(stat.baseValue)){
base = aggregator.base || 0;
} else {
base = Math.max(aggregator.base, stat.baseValue);
}
let result = (base + aggregator.add) * aggregator.mul;
if (result < aggregator.min) {
result = aggregator.min;
}
if (result > aggregator.max) {
result = aggregator.max;
}
if (aggregator.set !== undefined) {
result = aggregator.set;
}
if (!stat.decimal && Number.isFinite(result)){
result = Math.floor(result);
} else if (Number.isFinite(result)){
result = stripFloatingPointOddities(result);
}
return result;
}
function combineAttribute(stat, aggregator, memo){
stat.value = getAggregatorResult(stat, aggregator);
if (stat.attributeType === 'spellSlot'){
let {
result,
context,
dependencies
} = evaluateCalculation({
string: stat.spellSlotLevelCalculation,
memo,
prop: stat,
});
stat.spellSlotLevelValue = result.value;
stat.spellSlotLevelErrors = context.errors;
stat.dependencies = union(stat.dependencies, dependencies);
}
stat.currentValue = stat.value - (stat.damage || 0);
// Ability scores get modifiers
if (stat.attributeType === 'ability') {
stat.modifier = Math.floor((stat.currentValue - 10) / 2);
} else {
stat.modifier = undefined;
}
// Hit dice get constitution modifiers
stat.constitutionMod = undefined;
if (stat.attributeType === 'hitDice') {
let conStat = memo.statsByVariableName['constitution'];
if (conStat && 'modifier' in conStat){
stat.constitutionMod = conStat.modifier;
stat.dependencies = union(
stat.dependencies,
[conStat._id],
conStat.dependencies,
);
}
}
// Stats that have no effects can be hidden based on a sheet setting
stat.hide = aggregator.hasNoEffects &&
stat.baseValue === undefined ||
undefined
}
function combineSkill(stat, aggregator, memo){
// Skills are based on some ability Modifier
let ability = stat.ability && memo.statsByVariableName[stat.ability]
if (stat.ability && ability){
computeStat(ability, memo);
stat.abilityMod = ability.modifier;
stat.dependencies = union(
stat.dependencies,
[ability._id],
ability.dependencies,
);
} else {
stat.abilityMod = 0;
}
// Combine all the child proficiencies
stat.proficiency = 0;
for (let i in stat.computationDetails.proficiencies){
let prof = stat.computationDetails.proficiencies[i];
computeProficiency(prof, memo);
if (
!prof.deactivatedByToggle &&
prof.value > stat.proficiency
){
stat.proficiency = prof.value;
stat.dependencies = union(
stat.dependencies,
[prof._id],
prof.dependencies,
);
}
}
// Get the character's proficiency bonus to apply
let profBonusStat = memo.statsByVariableName['proficiencyBonus'];
let profBonus = profBonusStat && profBonusStat.value;
if (profBonusStat){
stat.dependencies = union(
stat.dependencies,
[profBonusStat._id],
profBonusStat.dependencies,
);
}
if (typeof profBonus !== 'number' && memo.statsByVariableName['level']){
let levelProp = memo.statsByVariableName['level'];
let level = levelProp.value;
profBonus = Math.ceil(level / 4) + 1;
if (levelProp._id){
stat.dependencies = union(stat.dependencies, [levelProp._id]);
}
if (levelProp.dependencies){
stat.dependencies = union(stat.dependencies, levelProp.dependencies);
}
}
// Multiply the proficiency bonus by the actual proficiency
if(stat.proficiency === 0.49){
// Round down proficiency bonus in the special case
profBonus = Math.floor(profBonus * 0.5);
} else {
profBonus = Math.ceil(profBonus * stat.proficiency);
}
// Combine everything to get the final result
let base = aggregator.base || 0;
let result = (base + stat.abilityMod + profBonus + aggregator.add) * aggregator.mul;
if (result < aggregator.min) result = aggregator.min;
if (result > aggregator.max) result = aggregator.max;
if (aggregator.set !== undefined) {
result = aggregator.set;
}
if (Number.isFinite(result)){
result = Math.floor(result);
}
stat.value = result;
// Advantage/disadvantage
if (aggregator.advantage && !aggregator.disadvantage){
stat.advantage = 1;
} else if (aggregator.disadvantage && !aggregator.advantage){
stat.advantage = -1;
} else {
stat.advantage = 0;
}
// Passive bonus
stat.passiveBonus = aggregator.passiveAdd;
// conditional benefits
stat.conditionalBenefits = aggregator.conditional;
// Roll bonuses
stat.rollBonus = aggregator.rollBonus;
// Forced to fail
stat.fail = aggregator.fail;
// Rollbonus
stat.rollBonuses = aggregator.rollBonus;
// Hide
stat.hide = aggregator.hasNoEffects &&
stat.baseValue === undefined &&
stat.proficiency == 0 ||
undefined;
}
function combineDamageMultiplier(stat){
if (stat.immunityCount) return 0;
let result;
if (stat.ressistanceCount && !stat.vulnerabilityCount){
result = 0.5;
} else if (!stat.ressistanceCount && stat.vulnerabilityCount){
result = 2;
} else {
result = 1;
}
stat.value = result;
}

View File

@@ -1,12 +0,0 @@
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
export default function computeConstant(constant, memo){
// Apply any toggles
applyToggles(constant, memo);
if (constant.deactivatedByToggle) return;
if (
!memo.constantsByVariableName[constant.variableName]
){
memo.constantsByVariableName[constant.variableName] = constant
}
}

View File

@@ -1,55 +0,0 @@
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
import { union } from 'lodash';
export default function computeEffect(effect, memo){
if (effect.computationDetails.computed) return;
if (effect.computationDetails.busyComputing){
// Trying to compute this effect again while it is already computing.
// We must be in a dependency loop.
effect.computationDetails.computed = true;
effect.result = NaN;
effect.computationDetails.busyComputing = false;
effect.computationDetails.error = 'dependencyLoop';
if (Meteor.isClient) console.warn('dependencyLoop', effect);
return;
}
// Before doing any work, mark this effect as busy
effect.computationDetails.busyComputing = true;
// Apply any toggles
applyToggles(effect, memo);
// Determine result of effect calculation
delete effect.errors;
if (!effect.calculation){
if(effect.operation === 'add' || effect.operation === 'base'){
effect.result = 0;
} else {
delete effect.result
}
} else if (Number.isFinite(+effect.calculation)){
effect.result = +effect.calculation;
} else if(effect.operation === 'conditional' || effect.operation === 'rollBonus'){
effect.result = effect.calculation;
} else if(_.contains(['advantage', 'disadvantage', 'fail'], effect.operation)){
effect.result = 1;
} else {
let {
result,
context,
dependencies,
} = evaluateCalculation({
string: effect.calculation,
prop: effect,
memo
});
effect.result = result.value;
effect.dependencies = union(effect.dependencies, dependencies);
if (context.errors.length){
effect.errors = context.errors;
}
}
effect.computationDetails.computed = true;
effect.computationDetails.busyComputing = false;
}

View File

@@ -1,129 +0,0 @@
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
import { union } from 'lodash';
export default function computeEndStepProperty(prop, memo){
applyToggles(prop, memo);
switch (prop.type){
case 'action':
case 'spell':
computeAction(prop, memo);
break;
case 'adjustment':
case 'damage':
computePropertyField(prop, memo, 'amount', 'compile');
break;
case 'attack':
computeAction(prop, memo);
computePropertyField(prop, memo, 'rollBonus');
break;
case 'savingThrow':
computePropertyField(prop, memo, 'dc');
break;
case 'spellList':
computePropertyField(prop, memo, 'maxPrepared');
computePropertyField(prop, memo, 'attackRollBonus');
computePropertyField(prop, memo, 'dc');
break;
case 'propertySlot':
computePropertyField(prop, memo, 'quantityExpected');
computePropertyField(prop, memo, 'slotCondition');
break;
case 'roll':
computePropertyField(prop, memo, 'roll', 'compile');
break;
}
}
function computeAction(prop, memo){
// Uses
let {
result,
context,
dependencies,
} = evaluateCalculation({ string: prop.uses, prop, memo});
prop.usesResult = result.value;
prop.dependencies = union(prop.dependencies, dependencies);
if (context.errors.length){
prop.usesErrors = context.errors;
} else {
delete prop.usesErrors;
}
prop.insufficientResources = undefined;
if (prop.usesUsed >= prop.usesResult){
prop.insufficientResources = true;
}
if (!prop.resources) return;
// Attributes consumed
prop.resources.attributesConsumed.forEach((attConsumed, i) => {
if (attConsumed.variableName){
let stat = memo.statsByVariableName[attConsumed.variableName];
prop.resources.attributesConsumed[i].statId = stat && stat._id;
prop.resources.attributesConsumed[i].statName = stat && stat.name;
let available = stat && stat.currentValue || 0;
prop.resources.attributesConsumed[i].available = available;
if (available < attConsumed.quantity){
prop.insufficientResources = true;
}
if (stat){
prop.dependencies = union(
prop.dependencies,
[stat._id],
stat.dependencies
);
}
}
});
// Items consumed
prop.resources.itemsConsumed.forEach((itemConsumed, i) => {
let item = itemConsumed.itemId ?
memo.equipmentById[itemConsumed.itemId] :
undefined;
let available = item ? item.quantity : 0;
prop.resources.itemsConsumed[i].available = available;
if (!item || available < itemConsumed.quantity){
prop.insufficientResources = true;
}
if (item){
prop.resources.itemsConsumed[i].itemId = item._id;
let name = item.name;
if (item.quantity !== 1 && item.plural){
name = item.plural;
}
if (name) prop.resources.itemsConsumed[i].itemName = name;
if (item.icon) prop.resources.itemsConsumed[i].itemIcon = item.icon;
if (item.color) prop.resources.itemsConsumed[i].itemColor = item.color;
prop.dependencies = union(
prop.dependencies,
[item._id],
item.dependencies
);
} else {
delete prop.resources.itemsConsumed[i].itemId;
delete prop.resources.itemsConsumed[i].itemName;
delete prop.resources.itemsConsumed[i].itemIcon;
delete prop.resources.itemsConsumed[i].itemColor;
}
});
}
function computePropertyField(prop, memo, fieldName, fn){
let {
result,
context,
dependencies,
} = evaluateCalculation({string: prop[fieldName], prop, memo, fn});
if (result instanceof ConstantNode){
prop[`${fieldName}Result`] = result.value;
} else {
prop[`${fieldName}Result`] = result.toString();
}
prop.dependencies = union(prop.dependencies, dependencies);
if (context.errors.length){
prop[`${fieldName}Errors`] = context.errors;
} else {
delete prop[`${fieldName}Errors`];
}
}

View File

@@ -1,40 +0,0 @@
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
import { union } from 'lodash';
export default function computeInlineCalculations(prop, memo){
if (prop.summary){
computeInlineCalcsForField(prop, memo, 'summary');
}
if (prop.description){
computeInlineCalcsForField(prop, memo, 'description');
}
}
function computeInlineCalcsForField(prop, memo, field){
let string = prop[field];
let inlineComputations = [];
let matches = string.matchAll(INLINE_CALCULATION_REGEX);
for (let match of matches){
let calculation = match[1];
let {
result,
context,
dependencies,
} = evaluateCalculation({string: calculation, prop, memo, fn: 'compile'});
if (result instanceof ErrorNode){
result = '`Calculation Error`';
}
let computation = {
calculation,
result: result && result.toString(),
};
if (context.errors.length){
computation.errors = context.errors;
}
inlineComputations.push(computation);
prop.dependencies = union(prop.dependencies, dependencies);
}
prop[`${field}Calculations`] = inlineComputations;
}

View File

@@ -1,66 +0,0 @@
import { forOwn, has, union } from 'lodash';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
export default function computeLevels(memo){
computeClassLevels(memo);
computeTotalLevel(memo);
}
function computeClassLevels(memo){
forOwn(memo.classLevelsById, classLevel => {
applyToggles(classLevel, memo);
// class levels are mutually dependent
classLevel.dependencies = union(
classLevel.dependencies,
Object.keys(memo.classLevelsById)
);
if (classLevel.deactivatedByToggle) return;
let name = classLevel.variableName;
let stat = memo.statsByVariableName[name];
if (!stat){
memo.statsByVariableName[name] = classLevel;
memo.classes[name] = classLevel;
} else if (!has(stat, 'level')){
// Stat is overriden by an attribute
return;
} else if (stat.level < classLevel.level) {
memo.statsByVariableName[name] = classLevel;
memo.classes[name] = classLevel;
}
});
}
function computeTotalLevel(memo){
let currentLevel = memo.statsByVariableName['level'];
if (!currentLevel || currentLevel.deactivatedByToggle){
currentLevel = {
value: 0,
dependencies: [],
computationDetails: {
builtIn: true,
computed: true,
}
};
memo.statsByVariableName['level'] = currentLevel;
}
// bail out if overriden by an attribute
if (!currentLevel.computationDetails.builtIn) return;
let level = 0;
for (let name in memo.classes){
let cls = memo.classes[name];
level += cls.level || 0;
if (cls._id){
currentLevel.dependencies = union(
currentLevel.dependencies,
[cls._id]
)
}
if (cls.dependencies){
currentLevel.dependencies = union(
currentLevel.dependencies,
cls.dependencies,
)
}
}
currentLevel.value = level;
}

View File

@@ -1,37 +0,0 @@
import { each, forOwn } from 'lodash';
import computeLevels from '/imports/api/creature/computation/engine/computeLevels.js';
import computeStat from '/imports/api/creature/computation/engine/computeStat.js';
import computeEffect from '/imports/api/creature/computation/engine/computeEffect.js';
import computeToggle from '/imports/api/creature/computation/engine/computeToggle.js';
import computeEndStepProperty from '/imports/api/creature/computation/engine/computeEndStepProperty.js';
import computeInlineCalculations from '/imports/api/creature/computation/engine/computeInlineCalculations.js';
import computeConstant from '/imports/api/creature/computation/engine/computeConstant.js';
export default function computeMemo(memo){
// Compute level
computeLevels(memo);
// Compute all constants that could be used
forOwn(memo.constantsById, constant => {
computeConstant (constant, memo);
});
// Compute all stats, even if they are overriden
forOwn(memo.statsById, stat => {
computeStat (stat, memo);
});
// Compute effects which didn't end up targeting a stat
each(memo.unassignedEffects, effect => {
computeEffect(effect, memo);
});
// Compute toggles which didn't already get computed by dependencies
forOwn(memo.togglesById, toggle => {
computeToggle(toggle, memo);
});
// Compute end step properties
forOwn(memo.endStepPropsById, prop => {
computeEndStepProperty(prop, memo);
});
// Compute inline calculations
forOwn(memo.propsById, prop => {
computeInlineCalculations(prop, memo);
});
}

View File

@@ -1,23 +0,0 @@
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
export default function computeEffect(proficiency, memo){
if (proficiency.computationDetails.computed) return;
if (proficiency.computationDetails.busyComputing){
// Trying to compute this proficiency again while it is already computing.
// We must be in a dependency loop.
proficiency.computationDetails.computed = true;
proficiency.result = NaN;
proficiency.computationDetails.busyComputing = false;
proficiency.computationDetails.error = 'dependencyLoop';
if (Meteor.isClient) console.warn('dependencyLoop', proficiency);
return;
}
// Before doing any work, mark this proficiency as busy
proficiency.computationDetails.busyComputing = true;
// Apply any toggles
applyToggles(proficiency, memo);
proficiency.computationDetails.computed = true;
proficiency.computationDetails.busyComputing = false;
}

View File

@@ -1,162 +0,0 @@
import combineStat from '/imports/api/creature/computation/engine/combineStat.js';
import computeEffect from '/imports/api/creature/computation/engine/computeEffect.js';
import EffectAggregator from '/imports/api/creature/computation/engine/EffectAggregator.js';
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
import { each, union, without } 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';
if (Meteor.isClient) console.warn('dependencyLoop', stat);
return;
}
// Before doing any work, mark this stat as busy
stat.computationDetails.busyComputing = true;
let effects = stat.computationDetails.effects || [];
let proficiencies = stat.computationDetails.proficiencies || [];
// Get references to all the stats that share the variable name
let sameNameStats
if (stat.computationDetails.idsOfSameName){
sameNameStats = stat.computationDetails.idsOfSameName.map(
id => memo.propsById[id]
);
} else {
sameNameStats = [];
}
let allStats = [stat, ...sameNameStats];
// Decide which stat is the last active stat
// The last active stat is considered the cannonical stat
let lastActiveStat;
allStats.forEach(candidateStat => {
applyToggles(candidateStat, memo);
if (!candidateStat.inactive) lastActiveStat = candidateStat;
candidateStat.overridden = undefined;
});
if (!lastActiveStat){
delete memo.statsByVariableName[stat.variableName];
return;
}
// Make sure the active stat has all the effects and proficiencies
lastActiveStat.computationDetails.effects = effects;
lastActiveStat.computationDetails.proficiencies = proficiencies;
// Update the memo's stat with the chosen stat
memo.statsByVariableName[stat.variableName] = lastActiveStat;
// Recreate list of the non-cannonical stats
sameNameStats = without(allStats, lastActiveStat);
sameNameStats.forEach(statInstance => {
// Mark the non-cannonical stats as overridden
statInstance.overridden = true;
// Apply the cannonical damage
statInstance.damage = lastActiveStat.damage;
});
let baseDependencies = [];
allStats.forEach(statInstance => {
// Add this stat and its deps to the dependencies
baseDependencies = union(
baseDependencies,
[statInstance._id],
statInstance.dependencies,
);
// Apply all the base proficiencies
if (statInstance.baseProficiency && !statInstance.inactive){
proficiencies.push({
value: statInstance.baseProficiency,
stats: [statInstance.variableName],
type: 'proficiency',
dependencies: statInstance.overridden ?
union(statInstance.dependencies, [statInstance._id]) :
[],
computationDetails: {
computed: true,
}
});
}
// Compute each active stat's baseValue calculation and apply it
if (!statInstance.inactive) {
delete statInstance.baseValueErrors;
let {
result,
context,
dependencies
} = evaluateCalculation({
string: statInstance.baseValueCalculation,
prop: statInstance,
memo
});
result.value = +result.value;
if (!isNaN(result.value)){
statInstance.baseValue = result.value;
} else {
statInstance.baseValue = undefined;
}
statInstance.dependencies = union(statInstance.dependencies, dependencies);
if (context.errors.length){
statInstance.baseValueErrors = context.errors;
}
// Apply all the base values
if (Number.isFinite(statInstance.baseValue)){
effects.push({
operation: 'base',
calculation: statInstance.baseValueCalculation,
result: statInstance.baseValue,
stats: [statInstance.variableName],
dependencies: statInstance.overridden ?
union(statInstance.dependencies, [statInstance._id]) :
[],
computationDetails: {
computed: true,
},
});
}
}
});
// Compute and aggregate all the effects
let aggregator = new EffectAggregator();
let effectDeps = [];
each(effects, (effect) => {
// Compute
computeEffect(effect, memo);
if (effect.deactivatedByToggle) return;
// dependencies
if (effect._id) effectDeps = union(effectDeps, [effect._id]);
effectDeps = union(effectDeps, effect.dependencies);
// Add computed effect to aggregator
aggregator.addEffect(effect);
});
// Combine the effects into the stats
allStats.forEach(statInstance => {
// Conglomerate all the effects to compute the final stat values
combineStat(statInstance, aggregator, memo);
// Mark the stats as computed
statInstance.computationDetails.computed = true;
statInstance.computationDetails.busyComputing = false;
// Only the active stat instance depeneds on the effects
if (!statInstance.overridden){
statInstance.dependencies = union(statInstance.dependencies, effectDeps);
}
});
}

View File

@@ -1,55 +0,0 @@
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
import { union } from 'lodash';
export default function computeToggle(toggle, memo){
if (toggle.computationDetails.computed) return;
if (toggle.computationDetails.busyComputing){
// Trying to compute this effect again while it is already computing.
// We must be in a dependency loop.
toggle.computationDetails.computed = true;
toggle.result = false;
toggle.computationDetails.busyComputing = false;
toggle.computationDetails.error = 'dependencyLoop';
if (Meteor.isClient) console.warn('dependencyLoop', toggle);
return;
}
// Before doing any work, mark this toggle as busy
toggle.computationDetails.busyComputing = true;
// Apply any parent toggles
applyToggles(toggle, memo);
// Do work
delete toggle.errors;
if (toggle.enabled){
toggle.toggleResult = true;
} else if (toggle.disabled){
toggle.toggleResult = false;
} else if (!toggle.condition){
toggle.toggleResult = false;
} else if (Number.isFinite(+toggle.condition)){
toggle.toggleResult = !!+toggle.condition;
} else {
let {
result,
context,
dependencies,
} = evaluateCalculation({string: toggle.condition, prop: toggle, memo});
toggle.toggleResult = !!result.value;
toggle.dependencies = union(
toggle.dependencies,
dependencies,
);
if (context.errors.length){
toggle.errors = context.errors;
}
}
if (!toggle.toggleResult){
toggle.inactive = true;
toggle.deactivatedBySelf = true;
toggle.deactivatedByToggle = true;
}
toggle.computationDetails.computed = true;
toggle.computationDetails.busyComputing = false;
}

View File

@@ -1,137 +0,0 @@
import computeStat from '/imports/api/creature/computation/engine/computeStat.js';
import { prettifyParseError, parse, CompilationContext } from '/imports/parser/parser.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
import AccessorNode from '/imports/parser/parseTree/AccessorNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
import findAncestorByType from '/imports/api/creature/computation/engine/findAncestorByType.js';
import { union } from 'lodash';
/* Convert a calculation into a constant output and errors*/
export default function evaluateCalculation({
string,
prop,
memo,
fn = 'reduce',
}){
let dependencies = [];
let context = new CompilationContext();
if (!string) return {
result: new ConstantNode({value: string, type: 'string'}),
context,
dependencies,
};
if (typeof string !== 'string'){
string = string.toString();
}
// Parse the string
let calc;
try {
calc = parse(string);
} catch (e) {
let error = prettifyParseError(e);
return {
result: new ErrorNode({context, error}),
context,
dependencies,
};
}
// Replace constants with their parsed constant
let replaceResults = replaceConstants({
calc, memo, prop, dependencies, context
});
dependencies = replaceResults.dependencies;
calc = replaceResults.calc;
if (replaceResults.failed){
return {
result: new ConstantNode({value: string, type: 'string'}),
context,
dependencies,
};
}
// Ensure all symbol nodes are defined and computed
dependencies = computeSymbols({calc, memo, prop, dependencies})
// Evaluate
let result = calc[fn](memo.statsByVariableName, context);
return {result, context, dependencies};
}
// Replace constants in the calc with the right ParseNodes
function replaceConstants({calc, memo, prop, dependencies, context}){
let constFailed = [];
calc = calc.replaceNodes(node => {
if (!(node instanceof SymbolNode)) return;
let stat, constant;
if (node.name[0] !== '#'){
stat = memo.statsByVariableName[node.name]
constant = memo.constantsByVariableName[node.name];
} else if (node.name === '#constant'){
constant = findAncestorByType({type: 'constant', prop, memo});
}
// replace constants that aren't overridden by stats or disabled by a toggle
if (constant && !constant.deactivatedByToggle && !stat){
dependencies = union(dependencies, [
constant._id,
...constant.dependencies
]);
// Fail if the constant has errors
if (constant.errors && constant.errors.length){
constFailed.push(node.name);
return;
}
let parsedConstantNode;
try {
parsedConstantNode = parse(constant.calculation);
} catch(e){
constFailed.push(node.name);
return;
}
if (!parsedConstantNode) constFailed.push(node.name);
return parsedConstantNode;
}
});
constFailed.forEach(name => {
context.storeError({
type: 'error',
message: `${name} is a constant property with parsing errors`
});
});
let failed = !!constFailed.length;
if (failed){
calc = new ErrorNode({error: 'Failed to replace constants'});
}
return { failed, dependencies, calc };
}
// Ensure all symbol nodes are defined and computed
function computeSymbols({calc, memo, prop, dependencies}){
calc.traverse(node => {
if (node instanceof SymbolNode || node instanceof AccessorNode){
let stat;
// References up the tree start with #
if (node.name[0] === '#'){
stat = findAncestorByType({type: node.name.slice(1), prop, memo});
memo.statsByVariableName[node.name] = stat;
} else {
stat = memo.statsByVariableName[node.name];
}
if (stat && stat.computationDetails && !stat.computationDetails.computed){
computeStat(stat, memo);
}
if (stat){
if (stat.dependencies){
dependencies = union(dependencies, [
stat._id || node.name,
...stat.dependencies
]);
} else {
dependencies = union(dependencies, [stat._id || node.name]);
}
}
}
});
return dependencies;
}

View File

@@ -1,24 +0,0 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function getComputationProperties(creatureId){
// Find all the relevant properties
return CreatureProperties.find({
'ancestors.id': creatureId,
removed: {$ne: true},
$or: [
// All active properties
{inactive: {$ne: true}},
// Unless they were deactivated because of a toggle
{deactivatedByToggle: true},
]
}, {
// Filter out fields never used by calculations
fields: {
icon: 0,
},
// Obey tree order
sort: {
order: 1,
}
}).fetch();
}

View File

@@ -1,51 +0,0 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { union } from 'lodash';
export default function getDependentProperties({
creatureId,
propertyIds,
propertiesDependedAponIds,
}){
// find ids of all dependant toggles that have conditions, even if inactive
let toggleIds = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'toggle',
removed: {$ne: true},
condition: { $exists: true },
dependencies: {$in: propertyIds},
}, {
fields: {_id: 1},
}).map(t => t._id);
// Find all the dependant properties
let props = CreatureProperties.find({
'ancestors.id': creatureId,
removed: {$ne: true},
dependencies: {$in: propertyIds},
$or: [
// All active properties
{inactive: {$ne: true}},
// All active and inactive toggles with conditions
// Same as {$in: toggleIds}, but should be slightly faster
{type: 'toggle', condition: { $exists: true }},
// All decendents of the above toggles
{'ancestors.id': {$in: toggleIds}},
]
}, { fields: {_id: 1, dependencies: 1} }).fetch();
// Add all the properties that changing props depend on, but haven't yet been
// included to make an array of every property we need
let allConnectedPropIds = [...propertyIds, ...propertiesDependedAponIds];
props.forEach(prop => {
allConnectedPropIds = union(
allConnectedPropIds,
prop.dependencies,
[prop._id]);
});
// Add on all the properties and the objects they depend apon
return CreatureProperties.find({
_id: {$in: allConnectedPropIds}
}, {
// Ignore fields not used in computations
fields: {icon: 0},
sort: {order: 1},
}).fetch();
}

View File

@@ -1,59 +0,0 @@
import { pick, forOwn } from 'lodash';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import VERSION from '/imports/constants/VERSION.js';
export default function writeCreatureVariables(memo, creatureId, fullRecompute = true) {
const fields = [
'ability',
'abilityMod',
'advantage',
'attributeType',
'baseProficiency',
'baseValue',
'calculation',
'conditionalBenefits',
'currentValue',
'damage',
'decimal',
'fail',
'level',
'modifier',
'name',
'passiveBonus',
'proficiency',
'reset',
'resetMultiplier',
'rollBonuses',
'skillType',
'spellSlotLevelValue',
'type',
'value',
];
if (fullRecompute){
memo.creatureVariables = {};
forOwn(memo.statsByVariableName, (stat, variableName) => {
// Don't save context variables
if (variableName[0] === '#') return;
let condensedStat = pick(stat, fields);
memo.creatureVariables[variableName] = condensedStat;
});
forOwn(memo.constantsByVariableName, (stat, variableName) => {
let condensedStat = pick(stat, fields);
if (!memo.creatureVariables[variableName]){
memo.creatureVariables[variableName] = condensedStat;
}
});
Creatures.update(creatureId, {$set: {
variables: memo.creatureVariables,
computeVersion: VERSION,
}});
} else {
let $set = {};
forOwn(memo.statsByVariableName, (stat, variableName) => {
let condensedStat = pick(stat, fields);
$set[`variables.${variableName}`] = condensedStat;
});
Creatures.update(creatureId, {$set});
}
}

View File

@@ -1,120 +0,0 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import ComputationMemo from '/imports/api/creature/computation/engine/ComputationMemo.js';
import getComputationProperties from '/imports/api/creature/computation/engine/getComputationProperties.js';
import computeMemo from '/imports/api/creature/computation/engine/computeMemo.js';
import writeAlteredProperties from '/imports/api/creature/computation/engine/writeAlteredProperties.js';
import writeCreatureVariables from '/imports/api/creature/computation/engine/writeCreatureVariables.js';
import { recomputeDamageMultipliersById } from '/imports/api/creature/denormalise/recomputeDamageMultipliers.js';
import recomputeSlotFullness from '/imports/api/creature/denormalise/recomputeSlotFullness.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import getDependentProperties from '/imports/api/creature/computation/engine/getDependentProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
export const recomputeCreature = new ValidatedMethod({
name: 'creatures.recomputeCreature',
validate: new SimpleSchema({
charId: { type: String }
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({charId}) {
let creature = Creatures.findOne(charId);
// Permission
assertEditPermission(creature, 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);
},
});
export function recomputeCreatureById(creatureId){
let creature = Creatures.findOne(creatureId);
recomputeCreatureByDoc(creature);
}
/**
* 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 recomputeCreatureByDoc(creature){
const creatureId = creature._id;
let props = getComputationProperties(creatureId);
let computationMemo = new ComputationMemo(props, creature);
computeMemo(computationMemo);
writeAlteredProperties(computationMemo);
writeCreatureVariables(computationMemo, creatureId);
recomputeDamageMultipliersById(creatureId);
recomputeSlotFullness(creatureId);
return computationMemo;
}
export function recomputePropertyDependencies(property){
let creature = getRootCreatureAncestor(property);
recomputeCreatureByDependencies({
creature,
propertyIds: [property._id],
propertiesDependedAponIds: property.dependencies,
});
}
export function recomputeCreatureByDependencies({
creature,
propertyIds,
propertiesDependedAponIds
}){
let props = getDependentProperties({
creatureId: creature._id,
propertyIds,
propertiesDependedAponIds,
});
let computationMemo = new ComputationMemo(props, creature);
computeMemo(computationMemo);
writeAlteredProperties(computationMemo);
writeCreatureVariables(computationMemo, creature._id, false)
recomputeInactiveProperties(creature._id);
return computationMemo;
}

View File

@@ -10,6 +10,14 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let CreatureProperties = new Mongo.Collection('creatureProperties');
let CreaturePropertySchema = new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
_migrationError: {
type: String,
optional: true,
},
type: {
type: String,
allowedValues: Object.keys(propertySchemasIndex),
@@ -38,12 +46,16 @@ let CreaturePropertySchema = new SimpleSchema({
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
});
const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
// Denormalised flag if this property is inactive on the sheet for any reason
// Including being disabled, or a decendent of a disabled property
inactive: {
type: Boolean,
optional: true,
index: 1,
removeBeforeCompute: true,
},
// Denormalised flag if this property was made inactive by an inactive
// ancestor. True if this property has an inactive ancestor even if this
@@ -52,6 +64,7 @@ let CreaturePropertySchema = new SimpleSchema({
type: Boolean,
optional: true,
index: 1,
removeBeforeCompute: true,
},
// Denormalised flag if this property was made inactive because of its own
// state
@@ -59,6 +72,7 @@ let CreaturePropertySchema = new SimpleSchema({
type: Boolean,
optional: true,
index: 1,
removeBeforeCompute: true,
},
// Denormalised flag if this property was made inactive because of a toggle
// calculation. Either an ancestor toggle calculation or its own.
@@ -66,18 +80,12 @@ let CreaturePropertySchema = new SimpleSchema({
type: Boolean,
optional: true,
index: 1,
},
// Denormalised list of all properties or creatures this property depends on
dependencies: {
type: Array,
defaultValue: [],
index: 1,
},
'dependencies.$': {
type: String,
removeBeforeCompute: true,
},
});
CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema);
for (let key in propertySchemasIndex){
let schema = new SimpleSchema({});
schema.extend(propertySchemasIndex[key]);
@@ -91,10 +99,11 @@ for (let key in propertySchemasIndex){
}
import '/imports/api/creature/creatureProperties/methods/index.js';
import '/imports/api/creature/actions/doAction.js';
import '/imports/api/creature/actions/castSpellWithSlot.js';
//import '/imports/api/creature/actions/doAction.js';
//import '/imports/api/creature/actions/castSpellWithSlot.js';
export default CreatureProperties;
export {
DenormalisedOnlyCreaturePropertySchema,
CreaturePropertySchema,
};

View File

@@ -4,8 +4,7 @@ import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const adjustQuantity = new ValidatedMethod({
name: 'creatureProperties.adjustQuantity',
@@ -33,8 +32,7 @@ const adjustQuantity = new ValidatedMethod({
// Changing quantity does not change dependencies, but recomputing the
// inventory changes many deps at once, so recompute fully
recomputeCreatureByDoc(rootCreature);
recomputeInventory(rootCreature._id);
computeCreature(rootCreature._id);
},
});

View File

@@ -5,7 +5,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { recomputePropertyDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js';
const damagePropertiesByName = new ValidatedMethod({
name: 'CreatureProperties.damagePropertiesByName',
@@ -29,14 +28,13 @@ const damagePropertiesByName = new ValidatedMethod({
// Check permissions
let creature = Creatures.findOne(creatureId, {
fields: {
damageMultipliers: 1,
variables: 1,
owner: 1,
readers: 1,
writers: 1,
},
});
assertEditPermission(creature, this.userId);
let lastProperty;
CreatureProperties.find({
'ancestors.id': creatureId,
variableName,
@@ -48,9 +46,7 @@ const damagePropertiesByName = new ValidatedMethod({
if (!schema.allowsKey('damage')) return;
// Damage the property
damagePropertyWork({property, operation, value});
lastProperty = property;
});
if (lastProperty) recomputePropertyDependencies(lastProperty);
}
});

View File

@@ -4,7 +4,7 @@ import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { recomputePropertyDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { computeCreatureDependencyGroup } from '/imports/api/engine/computeCreature.js';
const damageProperty = new ValidatedMethod({
name: 'creatureProperties.damage',
@@ -39,42 +39,42 @@ const damageProperty = new ValidatedMethod({
}
let result = damagePropertyWork({property, operation, value});
// Dependencies can't be changed through damage, only recompute deps
recomputePropertyDependencies(property);
computeCreatureDependencyGroup(property);
return result;
},
});
export function damagePropertyWork({property, operation, value}){
let damage, newValue;
if (operation === 'set'){
let currentValue = property.value;
const total = property.total;
// Set represents what we want the value to be after damage
// So we need the actual damage to get to that value
let damage = currentValue - value;
damage = total - value;
// Damage can't exceed total value
if (damage > currentValue) damage = currentValue;
if (damage > total) damage = total;
// Damage must be positive
if (damage < 0) damage = 0;
CreatureProperties.update(property._id, {
$set: {damage}
}, {
selector: property
});
return currentValue - damage;
newValue = property.total - damage;
} else if (operation === 'increment'){
let currentValue = property.value - (property.damage || 0);
let currentValue = property.value;
let currentDamage = property.damage;
let increment = value;
// Can't increase damage above the remaining value
if (increment > currentValue) increment = currentValue;
// Can't decrease damage below zero
if (-increment > currentDamage) increment = -currentDamage;
CreatureProperties.update(property._id, {
$inc: {damage: increment}
}, {
selector: property
});
return increment;
damage = currentDamage + increment;
newValue = property.total - damage;
}
// Write the results
CreatureProperties.update(property._id, {
$set: {damage, value: newValue}
}, {
selector: property
});
return damage;
}
export default damageProperty;

View File

@@ -5,7 +5,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { recomputeCreatureByDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const dealDamage = new ValidatedMethod({
name: 'creatureProperties.dealDamage',
@@ -25,7 +25,6 @@ const dealDamage = new ValidatedMethod({
// permissions
let creature = Creatures.findOne(creatureId, {
fields: {
damageMultipliers: 1,
owner: 1,
readers: 1,
writers: 1,
@@ -33,41 +32,41 @@ const dealDamage = new ValidatedMethod({
});
assertEditPermission(creature, this.userId);
// Get all the health bars and do damage to them
let healthBars = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'attribute',
attributeType:'healthBar',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: -1},
});
let multiplier = creature.damageMultipliers[damageType];
if (multiplier === undefined) multiplier = 1;
let totalDamage = Math.floor(amount * multiplier);
let damageLeft = totalDamage;
if (damageType === 'healing') damageLeft = -totalDamage;
let propertyIds = [];
let propertiesDependedAponIds = [];
healthBars.forEach(healthBar => {
if (damageLeft === 0) return;
let damageAdded = damagePropertyWork({
property: healthBar,
operation: 'increment',
value: damageLeft,
});
damageLeft -= damageAdded;
propertyIds.push(healthBar._id);
propertiesDependedAponIds.push(...healthBar.dependencies);
});
recomputeCreatureByDependencies({
creature,
propertyIds,
propertiesDependedAponIds,
});
const totalDamage = dealDamageWork({creature, damageType, amount})
computeCreature(creatureId);
return totalDamage;
},
});
export function dealDamageWork({creature, damageType, amount}){
// Get all the health bars and do damage to them
let healthBars = CreatureProperties.find({
'ancestors.id': creature._id,
type: 'attribute',
attributeType:'healthBar',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: -1},
});
//let multiplier = creature.damageMultipliers[damageType];
//if (multiplier === undefined) multiplier = 1;
//let totalDamage = Math.floor(amount * multiplier);
const totalDamage = amount;
let damageLeft = totalDamage;
if (damageType === 'healing') damageLeft = -totalDamage;
let propertyIds = [];
healthBars.forEach(healthBar => {
if (damageLeft === 0) return;
let damageAdded = damagePropertyWork({
property: healthBar,
operation: 'increment',
value: damageLeft,
});
damageLeft -= damageAdded;
propertyIds.push(healthBar._id);
});
return totalDamage;
}
export default dealDamage;

View File

@@ -8,10 +8,8 @@ import {
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
var snackbar;
if (Meteor.isClient){
snackbar = require(
@@ -89,14 +87,8 @@ const duplicateProperty = new ValidatedMethod({
ancestorId: property.ancestors[0].id,
});
// Inserting the active status of the property needs to be denormalised
recomputeInactiveProperties(creature._id);
// Recompute the inventory
recomputeInventory(creature._id);
// Inserting a creature property invalidates dependencies: full recompute
recomputeCreatureByDoc(creature);
computeCreature(creature._id);
return propertyId;
},

View File

@@ -4,9 +4,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
@@ -49,9 +47,7 @@ const equipItem = new ValidatedMethod({
skipRecompute: true,
});
recomputeInactiveProperties(creature._id);
recomputeInventory(creature._id);
recomputeCreatureByDoc(creature);
computeCreature(creature._id);
},
});

View File

@@ -0,0 +1,48 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const flipToggle = new ValidatedMethod({
name: 'creatureProperties.flipToggle',
validate({_id}){
if (!_id) throw new Meteor.Error('No _id', '_id is required');
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
// Permission
let property = CreatureProperties.findOne(_id, {
fields: {type: 1, ancestors: 1, enabled: 1, disabled: 1}
});
if (property.type !== 'toggle'){
throw new Meteor.Error('wrong property',
'This method can only be applied to toggles');
}
if (!property.enabled && !property.disabled){
throw new Meteor.Error('Computed toggle',
'Can\'t flip a toggle that is computed')
}
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
// Invert the current value, disabled is the canonical store of value
const currentValue = !property.disabled;
CreatureProperties.update(_id, {$set: {
enabled: !currentValue,
disabled: currentValue,
}}, {
selector: {type: 'toggle'},
});
// Updating a toggle is likely to change the whole tree, do a full recompute
computeCreature(rootCreature._id);
},
});
export default flipToggle;

View File

@@ -12,3 +12,4 @@ import '/imports/api/creature/creatureProperties/methods/restoreProperty.js';
import '/imports/api/creature/creatureProperties/methods/selectAmmoItem.js';
import '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
import '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';
import '/imports/api/creature/creatureProperties/methods/flipToggle.js';

View File

@@ -5,9 +5,7 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
import SimpleSchema from 'simpl-schema';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import { getAncestry } from '/imports/api/parenting/parenting.js';
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
@@ -140,15 +138,8 @@ export function insertPropertyWork({property, creature}){
collection: CreatureProperties,
ancestorId: creature._id,
});
// Inserting the active status of the property needs to be denormalised
recomputeInactiveProperties(creature._id);
// Recompute the inventory if it has changed
if (property.type === 'item' || property.type === 'container'){
recomputeInventory(creature._id);
}
// Inserting a creature property invalidates dependencies: full recompute
recomputeCreatureByDoc(creature);
computeCreature(creature._id);
return _id;
}

View File

@@ -5,8 +5,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import {
setLineageOfDocs,
@@ -15,7 +14,6 @@ import {
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
const insertPropertyFromLibraryNode = new ValidatedMethod({
@@ -74,12 +72,8 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
ancestorId: rootCreature._id,
});
// The library properties need to denormalise which of them are inactive
recomputeInactiveProperties(rootCreature._id);
// Some of the library properties may be items or containers
recomputeInventory(rootCreature._id);
// Inserting a creature property invalidates dependencies: full recompute
recomputeCreatureByDoc(rootCreature);
computeCreature(rootCreature._id);
// Return the docId of the last property, the inserted root property
return rootId;
},
@@ -116,7 +110,7 @@ function insertPropertyFromNode(nodeId, ancestors, order){
nodes = [node, ...nodes];
// set libraryNodeIds
storeLibraryNodeReferences(nodes, nodeId);
storeLibraryNodeReferences(nodes);
// re-map all the ancestors
setLineageOfDocs({
@@ -149,6 +143,7 @@ function insertPropertyFromNode(nodeId, ancestors, order){
function storeLibraryNodeReferences(nodes){
nodes.forEach(node => {
if (node.libraryNodeId) return;
node.libraryNodeId = node._id;
});
}
@@ -162,20 +157,11 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
// Filter out the reference nodes we replace
let resultingNodes = nodes.filter(node => {
// We have already visited this ref and replaced it
if (visitedRefs.has(node._id)) return false;
// Already replaced an ancestor node
for (let i; i < node.ancestors.length; i++){
if (visitedRefs.has(node.ancestors[i].id)) return false;
}
// This isn't a reference node, continue as normal
if (node.type !== 'reference') return true;
// We have gone too deep, keep the reference node as an error
if (depth > 10){
if (depth >= 10){
if (Meteor.isClient) console.warn('Reference depth limit exceeded');
node.cache = {error: 'Reference depth limit exceeded'};
return true;
@@ -211,26 +197,31 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
oldParent: referencedNode.parent,
});
// Remove all the looped references and descendents from the new nodes
// We can't rely on the reify recursion to do this, since the IDs are
// getting renewed before it is called
addedNodes = addedNodes.filter(node => {
// Exclude removed referenced
if (visitedRefs.has(node._id)) return false;
// Exclude descendants of removed references
for (let i; i < node.ancestors.length; i++){
if (visitedRefs.has(node.ancestors[i].id)) return false;
// Filter all the looped references
addedNodes = addedNodes.filter(addedNode => {
// Add all non-reference nodes
if (addedNode.type !== 'reference'){
return true;
}
// If this exact reference has already been resolved before, filter it out
if (visitedRefs.has(addedNode._id)){
return false;
} else {
// Otherwise mark it as visited, and keep it
visitedRefs.add(addedNode._id);
return true;
}
return true;
});
// TODO: Force the referencedNode to take the old id of the reference
// such that the reference's children can be kept
// Before renewing Ids make sure the library node reference is stored
storeLibraryNodeReferences(addedNodes);
// Give the new referenced sub-tree new ids
// The referenced node must get the id of the ref node so that the
// descendants of the ref node keep their ancestry intact
renewDocIds({
docArray: addedNodes,
idMap: { [referencedNode._id]: node._id },
});
// Reify the subtree as well with recursion

View File

@@ -3,7 +3,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const pullFromProperty = new ValidatedMethod({
name: 'creatureProperties.pull',
@@ -28,7 +28,7 @@ const pullFromProperty = new ValidatedMethod({
});
// TODO figure out if this method can change deps or not
recomputeCreatureByDoc(rootCreature);
computeCreature(rootCreature._id);
// recomputePropertyDependencies(property);
}
});

View File

@@ -3,7 +3,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import { get } from 'lodash';
const pushToProperty = new ValidatedMethod({
@@ -45,8 +45,7 @@ const pushToProperty = new ValidatedMethod({
});
// TODO figure out if this method can change deps or not
recomputeCreatureByDoc(rootCreature);
// recomputePropertyDependencies(property);
computeCreature(rootCreature._id);
}
});

View File

@@ -5,9 +5,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { restore } from '/imports/api/parenting/softRemove.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const restoreProperty = new ValidatedMethod({
name: 'creatureProperties.restore',
@@ -28,12 +26,8 @@ const restoreProperty = new ValidatedMethod({
// Do work
restore({_id, collection: CreatureProperties});
// Items and containers might be restored
recomputeInventory(rootCreature._id);
// Parents active status may have changed while it was deleted
recomputeInactiveProperties(rootCreature._id);
// Changes dependency tree by restoring children
recomputeCreatureByDoc(rootCreature);
computeCreature(rootCreature._id);
}
});

View File

@@ -4,7 +4,7 @@ import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const selectAmmoItem = new ValidatedMethod({
name: 'creatureProperties.selectAmmoItem',
@@ -45,7 +45,7 @@ const selectAmmoItem = new ValidatedMethod({
// Changing the linked item does change the dependency tree
// TODO: We can predict exactly which deps will be affected instead of
// recomputing the entire creature
recomputeCreatureByDoc(rootCreature);
computeCreature(rootCreature._id);
},
});

View File

@@ -5,8 +5,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { softRemove } from '/imports/api/parenting/softRemove.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const softRemoveProperty = new ValidatedMethod({
name: 'creatureProperties.softRemove',
@@ -27,10 +26,8 @@ const softRemoveProperty = new ValidatedMethod({
// Do work
softRemove({_id, collection: CreatureProperties});
// Potentially changes items and containers
recomputeInventory(rootCreature._id);
// Changes dependency tree by removing children
recomputeCreatureByDoc(rootCreature);
computeCreature(rootCreature._id);
}
});

View File

@@ -3,9 +3,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const updateCreatureProperty = new ValidatedMethod({
name: 'creatureProperties.update',
@@ -47,20 +45,9 @@ const updateCreatureProperty = new ValidatedMethod({
selector: {type: property.type},
});
// Some updates might cause other properties to become inactive
if ([
'applied', 'equipped', 'prepared', 'alwaysPrepared', 'disabled'
].includes(path[0])){
recomputeInactiveProperties(rootCreature._id);
}
if (property.type === 'item' || property.type === 'container'){
// Potentially changes items and containers
recomputeInventory(rootCreature._id);
}
// Updating a property is likely to change dependencies, do a full recompute
// denormalised stats might change, so fetch the creature again
recomputeCreatureById(rootCreature._id);
computeCreature(rootCreature._id);
},
});

View File

@@ -1,4 +1,4 @@
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
/**
* Recomputes all ancestor creatures of this property
@@ -6,7 +6,7 @@ import { recomputeCreatureById } from '/imports/api/creature/computation/methods
export default function recomputeCreaturesByProperty(property){
for (let ref of property.ancestors){
if (ref.collection === 'creatures') {
recomputeCreatureById.call(ref.id);
computeCreature.call(ref.id);
}
}
}

View File

@@ -100,35 +100,6 @@ let CreatureSchema = new SimpleSchema({
type: SimpleSchema.Integer,
defaultValue: 0,
},
// Inventory
'denormalizedStats.weightTotal': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.weightEquipment': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.weightCarried': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.valueTotal': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.valueEquipment': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.valueCarried': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.itemsAttuned': {
type: Number,
defaultValue: 0,
},
// Version of computation engine that was last used to compute this creature
computeVersion: {
type: String,
@@ -175,6 +146,7 @@ Creatures.attachSchema(CreatureSchema);
import '/imports/api/creature/creatures/methods/index.js';
import '/imports/api/engine/actions/doAction.js';
export default Creatures;
export { CreatureSchema };

View File

@@ -10,10 +10,10 @@ export default function defaultCharacterProperties(creatureId){
{
type: 'propertySlot',
name: 'Ruleset',
description: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base, your sheet will be empty.',
description: {text: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base, your sheet will be empty.'},
slotTags: ['base'],
tags: [],
quantityExpected: 1,
quantityExpected: {calculation: '1'},
hideWhenFull: true,
spaceLeft: 1,
totalFilled: 0,

View File

@@ -4,7 +4,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const restCreature = new ValidatedMethod({
name: 'creature.methods.longRest',
@@ -109,7 +109,7 @@ const restCreature = new ValidatedMethod({
});
});
}
recomputeCreatureById(creatureId);
computeCreature(creatureId);
},
});

View File

@@ -1,78 +0,0 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export const recomputeDamageMultipliers = new ValidatedMethod({
name: 'creatures.recomputeDamageMultipliers',
validate: new SimpleSchema({
creatureId: { type: String }
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({creatureId}) {
// Permission
assertEditPermission(creatureId, this.userId);
// Work, call this direcly if you are already in a method that has checked
// for permission to edit a given character
recomputeDamageMultipliersById(creatureId);
},
});
export function recomputeDamageMultipliersById(creatureId){
if (!creatureId) throw 'Creature ID is required';
let props = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'damageMultiplier',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1}
});
// Count of how many weakness, resistances and immunities each damage type has
let multipliersByName = {};
props.forEach(dm => {
dm.damageTypes.forEach(damageType => {
if (!multipliersByName[damageType]){
multipliersByName[damageType] = {
weaknesses: 0,
resistances: 0,
immunities: 0,
};
}
if (dm.value === 0){
multipliersByName[damageType].immunities++;
} else if (dm.value === 0.5){
multipliersByName[damageType].resistances++;
} else if (dm.value === 2){
multipliersByName[damageType].weaknesses++;
}
});
});
// Make an Object with keys of all the damage types that have a resulting
// immunity, weakness, or resistance
let damageMultipliers = {};
for (let damageType in multipliersByName){
let multiplier = multipliersByName[damageType];
if (multiplier.immunities){
damageMultipliers[damageType] = 0;
} else if (multiplier.resistances && !multiplier.weaknesses){
damageMultipliers[damageType] = 0.5;
} else if (multiplier.weaknesses && !multiplier.resistances){
damageMultipliers[damageType] = 2;
}
}
// Store the Object on the creature document
Creatures.update(creatureId, {$set: {damageMultipliers}});
}

View File

@@ -1,75 +0,0 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function recomputeInactiveProperties(ancestorId){
let disabledFilter = {
'ancestors.id': ancestorId,
$or: [
{disabled: true}, // Everything can be disabled
{type: 'buff', applied: false}, // Buffs can be applied
{type: 'item', equipped: {$ne: true}},
{type: 'spell', prepared: {$ne: true}, alwaysPrepared: {$ne: true}},
],
};
let disabledIds = CreatureProperties.find(disabledFilter, {
fields: {_id: 1},
}).map(prop => prop._id);
// Deactivate relevant properties
// Inactive properties
CreatureProperties.update({
'ancestors.id': ancestorId,
'_id': {$in: disabledIds},
$or: [
{inactive: {$ne: true}},
{deactivatedBySelf: {$ne: true}},
{deactivatedByAncestor: true},
],
}, {
$set: {
inactive: true,
deactivatedBySelf: true,
},
$unset: {deactivatedByAncestor: 1},
}, {
multi: true,
selector: {type: 'any'},
});
// Decendants of inactive properties
CreatureProperties.update({
'ancestors.id': {$eq: ancestorId, $in: disabledIds},
$or: [
{inactive: {$ne: true}},
{deactivatedByAncestor: {$ne: true}},
],
}, {
$set: {
inactive: true,
deactivatedByAncestor: true,
},
}, {
multi: true,
selector: {type: 'any'},
});
// Remove inactive from all the properties that are inactive but shouldn't be
CreatureProperties.update({
'ancestors.id': {$eq: ancestorId, $nin: disabledIds},
'_id': {$nin: disabledIds},
// if it was a toggle responsible, we leave it alone
deactivatedByToggle: {$ne: true},
$or: [
{inactive: true},
{deactivatedByAncestor: true},
{deactivatedBySelf: true}
],
}, {
$unset: {
inactive: 1,
deactivatedByAncestor: 1,
deactivatedBySelf: 1,
},
}, {
multi: true,
selector: {type: 'any'},
});
}

View File

@@ -1,111 +0,0 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import nodesToTree from '/imports/api/parenting/nodesToTree.js';
export default function recomputeInventory(creatureId){
let inventoryForest = nodesToTree({
collection: CreatureProperties,
ancestorId: creatureId,
filter: {
type: {$in: ['container', 'item']},
},
deactivatedByAncestor: {$ne: true},
});
let containersToWrite = [];
let data = getChildrenInventoryData(inventoryForest, containersToWrite);
containersToWrite.forEach(container => {
CreatureProperties.update(container._id, {$set: {
contentsWeight: container.contentsWeight,
contentsValue: container.contentsValue,
}}, {selector: {type: 'container'}});
});
Creatures.update(creatureId, {$set: {
'denormalizedStats.weightTotal': data.weightTotal,
'denormalizedStats.weightEquipment': data.weightEquipment,
'denormalizedStats.weightCarried': data.weightCarried,
'denormalizedStats.valueTotal': data.valueTotal,
'denormalizedStats.valueEquipment': data.valueEquipment,
'denormalizedStats.valueCarried': data.valueCarried,
'denormalizedStats.itemsAttuned': data.itemsAttuned,
}});
return data;
}
function getChildrenInventoryData(forest, containersToWrite){
let data = {
weightTotal: 0,
weightEquipment: 0,
weightCarried: 0,
valueTotal: 0,
valueEquipment: 0,
valueCarried: 0,
itemsAttuned: 0,
}
forest.forEach(tree => {
let treeData = getInventoryData(tree, containersToWrite);
for (let key in data){
data[key] += treeData[key] || 0;
}
});
return data;
}
function getInventoryData(tree, containersToWrite){
let data = {
weightTotal: 0,
weightEquipment: 0,
weightCarried: 0,
valueTotal: 0,
valueEquipment: 0,
valueCarried: 0,
itemsAttuned: 0,
}
let childData = getChildrenInventoryData(tree.children, containersToWrite);
let node = tree.node;
if (node.type === 'container'){
data.weightTotal += node.weight || 0;
data.valueTotal += node.value || 0;
data.weightCarried += node.weight || 0;
data.valueCarried += node.value || 0;
storeContentsData(node, childData, containersToWrite);
} else if (node.type === 'item'){
data.weightTotal += (node.weight * node.quantity) || 0;
data.valueTotal += (node.value * node.quantity) || 0;
data.weightCarried += (node.weight * node.quantity) || 0;
data.valueCarried += (node.value * node.quantity) || 0;
if (node.equipped){
data.weightEquipment += (node.weight * node.quantity) || 0;
data.valueEquipment += (node.value * node.quantity) || 0;
}
if (node.attuned){
data.itemsAttuned += 1;
}
}
for (let key in data){
data[key] += childData[key];
}
if (node.contentsWeightless){
data.weightCarried = node.weight;
}
if (node.carried === false){
data.weightCarried = 0;
data.valueCarried = 0;
}
return data
}
function storeContentsData(node, childData, containersToWrite){
let newContentsWeight = childData.weightCarried
if (node.contentsWeight !== newContentsWeight){
node.contentsWeight = newContentsWeight;
node.contentsWeightChanged = true;
}
let newContentsValue = childData.valueCarried;
if (node.contentsValue !== newContentsValue){
node.contentsValue = newContentsValue;
node.contentsValueChanged = true;
}
if (node.contentsWeightChanged || node.contentsValueChanged){
containersToWrite.push(node);
}
}

View File

@@ -1,43 +0,0 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
// n + 1 database queries + n potential updates for n slots. Could be sped up.
export default function recomputeSlotFullness(ancestorId){
CreatureProperties.find({
'ancestors.id': ancestorId,
type: 'propertySlot',
}).forEach(slot => {
let children = CreatureProperties.find({
'parent.id': slot._id,
removed: {$ne: true},
}, {
fields: {
slotQuantityFilled: 1,
type: 1
}
}).fetch();
let totalFilled = 0;
children.forEach(child => {
if (child.type === 'slotFiller'){
totalFilled += child.slotQuantityFilled;
} else {
totalFilled++;
}
});
let spaceLeft;
let expected = slot.quantityExpectedResult;
if (typeof expected !== 'number'){
expected = 1;
}
if (expected === 0){
spaceLeft = null;
} else {
spaceLeft = expected - totalFilled;
}
if (slot.totalFilled !== totalFilled || slot.spaceLeft !== spaceLeft){
CreatureProperties.update(slot._id, {
$set: {totalFilled, spaceLeft},
}, {
selector: {type: 'propertySlot'}
});
}
});
}

View File

@@ -3,7 +3,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let Experiences = new Mongo.Collection('experiences');
@@ -175,7 +175,7 @@ const recomputeExperiences = new ValidatedMethod({
'denormalizedStats.xp': xp,
'denormalizedStats.milestoneLevels': milestoneLevels
}});
recomputeCreatureById(creatureId);
computeCreature(creatureId);
},
});

View File

@@ -4,11 +4,8 @@ import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import {assertEditPermission} from '/imports/api/creature/creatures/creaturePermissions.js';
import {
parse,
CompilationContext,
prettifyParseError
} from '/imports/parser/parser.js';
import {parse, prettifyParseError} from '/imports/parser/parser.js';
import resolve, { toString } from '/imports/parser/resolve.js';
const PER_CREATURE_LOG_LIMIT = 100;
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
@@ -121,11 +118,12 @@ export function insertCreatureLogWork({log, creature, method}){
if (typeof log === 'string'){
log = {content: [{value: log}]};
}
if (!log.content?.length) return;
log.date = new Date();
// Insert it
let id = CreatureLogs.insert(log);
if (Meteor.isServer){
method.unblock();
method?.unblock();
removeOldLogs(creature._id);
logWebhook({log, creature});
}
@@ -174,26 +172,29 @@ const logRoll = new ValidatedMethod({
logContent.push({name: 'Parse Error', value: error});
}
if (parsedResult) try {
let rollContext = new CompilationContext();
let compiled = parsedResult.compile(creature.variables, rollContext);
let compiledString = compiled.toString();
let {
result: compiled,
context
} = resolve('compile', parsedResult, creature.variables);
const compiledString = toString(compiled);
if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({
value: roll
});
logContent.push({
value: compiledString
});
let rolled = compiled.roll(creature.variables, rollContext);
let rolledString = rolled.toString();
let {result: rolled} = resolve('roll', compiled, creature.variables, context);
let rolledString = toString(rolled);
if (rolledString !== compiledString) logContent.push({
value: rolled.toString()
});
let result = rolled.reduce(creature.variables, rollContext);
let resultString = result.toString();
let {result} = resolve('reduce', rolled, creature.variables, context);
let resultString = toString(result);
if (resultString !== rolledString) logContent.push({
value: resultString
});
} catch (e){
console.error(e);
logContent = [{name: 'Calculation error'}];
}
const log = {

View File

@@ -1,4 +1,4 @@
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
export default function recomputeCreatureMixin(methodOptions){
let runFunc = methodOptions.run;
@@ -10,7 +10,7 @@ export default function recomputeCreatureMixin(methodOptions){
) {
return result;
}
recomputeCreatureById(charId);
computeCreature(charId);
return result;
};
return methodOptions;

View File

@@ -0,0 +1,27 @@
import action from './applyPropertyByType/applyAction.js';
import adjustment from './applyPropertyByType/applyAdjustment.js';
import branch from './applyPropertyByType/applyBranch.js';
import buff from './applyPropertyByType/applyBuff.js';
import damage from './applyPropertyByType/applyDamage.js';
import note from './applyPropertyByType/applyNote.js';
import roll from './applyPropertyByType/applyRoll.js';
import savingThrow from './applyPropertyByType/applySavingThrow.js';
import toggle from './applyPropertyByType/applyToggle.js';
const applyPropertyByType = {
action,
adjustment,
branch,
buff,
damage,
note,
roll,
savingThrow,
spell: action,
toggle,
};
export default function applyProperty(node, opts, ...rest){
opts.scope[`#${node.node.type}`] = node.node;
return applyPropertyByType[node.node.type]?.(node, opts, ...rest);
}

View File

@@ -0,0 +1,240 @@
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import rollDice from '/imports/parser/rollDice.js';
import applyProperty from '../applyProperty.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
export default function applyAction(node, {creature, targets, scope, log}){
const prop = node.node;
if (prop.target === 'self') targets = [creature];
// Log the name and description
let content = { name: prop.name };
if (prop.description?.text){
recalculateInlineCalculations(prop.description, scope, log);
content.value = prop.description.value;
}
if (content.name || content.value){
log.content.push(content);
}
// Spend the resources
const failed = spendResources({prop, log, scope});
if (failed) return;
// Attack if there is an attack roll
if (prop.attackRoll && prop.attackRoll.calculation){
if (targets.length){
targets.forEach(target => {
applyAttackToTarget({prop, target, scope, log});
// Apply the children, but only to the current target
applyChildren(node, {targets: [target], scope, log});
});
} else {
applyAttackWithoutTarget({prop, scope, log});
applyChildren(node, {creature, targets, scope, log});
}
} else {
applyChildren(node, {creature, targets, scope, log});
}
}
function applyAttackWithoutTarget({prop, scope, log}){
delete scope['$attackHit'];
delete scope['$attackMiss'];
delete scope['$criticalHit'];
delete scope['$criticalMiss'];
delete scope['$attackRoll'];
recalculateCalculation(prop.attackRoll, scope, log);
let value = rollDice(1, 20)[0];
scope['$attackRoll'] = {value};
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
let criticalHit = value >= criticalHitTarget;
if (criticalHit){
scope['$criticalHit'] = {value: true};
scope['$attackHit'] = {value: true};
} else {
let criticalMiss = value === 1;
if (criticalMiss){
scope['$criticalMiss'] = 1;
log.content.push({
name: 'Critical Miss!',
});
scope['$attackMiss'] = {value: true};
} else {
// Untargeted attacks hit by default
scope['$attackHit'] = {value: true}
}
}
let result = value + prop.attackRoll.value;
scope['$attackRoll'] = {value: result};
log.content.push({
name: criticalHit ? 'Critical Hit!' : 'To Hit',
value: `1d20 [${value}] + ${prop.attackRoll.value} = ` + result,
});
}
function applyAttackToTarget({prop, target, scope, log}){
delete scope['$attackHit'];
delete scope['$attackMiss'];
delete scope['$criticalHit'];
delete scope['$criticalMiss'];
delete scope['$attackDiceRoll'];
delete scope['$attackRoll'];
recalculateCalculation(prop.attackRoll, scope, log);
const value = rollDice(1, 20)[0];
scope['$attackDiceRoll'] = {value};
const criticalHitTarget = scope.criticalHitTarget?.value || 20;
const criticalHit = value >= criticalHitTarget;
const criticalMiss = value === 1;
if (criticalHit) scope['$criticalHit'] = {value: true};
if (criticalMiss) scope['$criticalMiss'] = {value: true};
const result = value + prop.attackRoll.value;
scope['$attackRoll'] = {value: result};
if (target.variables.armor){
const armor = target.variables.armor.value;
const name = criticalHit ? 'Critical Hit!' :
criticalMiss ? 'Critical miss!' :
result > armor ? 'Hit!' :
'Miss!'
log.content.push({
name,
value: `1d20 {${value}} + ${prop.attackRoll.value} = ` + result,
});
if ((result > armor) || (criticalHit)){
scope['$attackHit'] = true;
} else {
scope['$attackMiss'] = true;
}
} else {
log.content.push({
name: 'Error',
value:'Target has no `armor`',
});
log.content.push({
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical miss!' : 'To Hit',
value: `1d20 {${value}} + ${prop.attackRoll.value} = ` + result,
});
}
}
function applyChildren(node, args){
node.children.forEach(child => applyProperty(child, args));
}
function spendResources({prop, log, scope}){
// Check Uses
if (prop.usesLeft < 0){
log.content.push({
name: 'Error',
value: `${prop.name || 'action'} does not have enough uses left`,
});
return true;
}
// Resources
if (prop.insufficientResources){
log.content.push({
name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action',
});
return true;
}
// Items
let itemQuantityAdjustments = [];
let spendLog = [];
let gainLog = [];
try {
prop.resources.itemsConsumed.forEach(itemConsumed => {
recalculateCalculation(itemConsumed.quantity, scope, log);
if (!itemConsumed.itemId){
throw 'No ammo was selected for this prop';
}
let item = CreatureProperties.findOne(itemConsumed.itemId);
if (!item || item.ancestors[0].id !== prop.ancestors[0].id){
throw 'The prop\'s ammo was not found on the creature';
}
if (!item.equipped){
throw 'The selected ammo is not equipped';
}
if (
!itemConsumed.quantity.value ||
!isFinite(itemConsumed.quantity.value)
) return;
itemQuantityAdjustments.push({
property: item,
operation: 'increment',
value: itemConsumed.quantity.value,
});
let logName = item.name;
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1){
logName = item.plural || logName;
}
if (itemConsumed.quantity.value > 0){
spendLog.push(logName + ': ' + itemConsumed.quantity.value);
} else if (itemConsumed.quantity.value < 0){
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
}
});
} catch (e){
log.content.push({
name: 'Error',
value: e,
});
return true;
}
// No more errors should be thrown after this line
// Now that we have confirmed that there are no errors, do actual work
//Items
itemQuantityAdjustments.forEach(adjustQuantityWork);
// Use uses
if (prop.usesLeft){
CreatureProperties.update(prop._id, {
$inc: {usesUsed: 1}
}, {
selector: prop
});
log.content.push({
name: 'Uses left',
value: prop.usesLeft - (prop.usesUsed || 0) - 1,
});
}
// Damage stats
prop.resources.attributesConsumed.forEach(attConsumed => {
recalculateCalculation(attConsumed.quantity, scope, log);
if (!attConsumed.quantity?.value) return;
let stat = scope[attConsumed.variableName];
if (!stat){
spendLog.push(stat.name + ': ' + ' not found');
return;
}
damagePropertyWork({
property: stat,
operation: 'increment',
value: attConsumed.quantity.value,
});
if (attConsumed.quantity.value > 0){
spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
} else if (attConsumed.quantity.value < 0){
gainLog.push(stat.name + ': ' + -attConsumed.quantity.value);
}
});
// Log all the spending
if (gainLog.length) log.content.push({
name: 'Gained',
value: gainLog.join('\n'),
});
if (spendLog.length) log.content.push({
name: 'Spent',
value: spendLog.join('\n'),
});
}

View File

@@ -0,0 +1,57 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
export default function applyAdjustment(node, {
creature, targets, scope, log
}){
const prop = node.node;
const damageTargets = prop.target === 'self' ? [creature] : targets;
if (!prop.amount) {
return applyChildren(node, {creature, targets, scope, log});
}
// Evaluate the amount
recalculateCalculation(prop.amount, scope, log);
const value = +prop.amount.value;
if (!isFinite(value)) {
return applyChildren(node, {creature, targets, scope, log});
}
if (damageTargets?.length) {
damageTargets.forEach(target => {
let stat = target.variables[prop.stat];
if (!stat) {
log({
name: 'Error',
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
});
return applyChildren(node, {creature, targets, scope, log});
}
damagePropertyWork({
property: stat,
operation: prop.operation,
value: value,
});
log.content.push({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`,
});
});
} else {
log.content.push({
name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`,
});
}
return applyChildren(node, {creature, targets, scope, log});
}
function applyChildren(node, args){
node.children.forEach(child => applyProperty(child, args));
}

View File

@@ -0,0 +1,51 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import rollDice from '/imports/parser/rollDice.js';
export default function applyBranch(node, {
creature, targets, scope, log
}){
const applyChildren = function(){
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
};
const prop = node.node;
switch(prop.branchType){
case 'if':
recalculateCalculation(prop.condition, scope, log);
if (prop.condition?.value) applyChildren();
break;
case 'hit':
if (scope['$attackHit']?.value) applyChildren();
break;
case 'miss':
if (scope['$attackMiss']?.value) applyChildren();
break;
case 'failedSave':
if (scope['$saveFailed']?.value) applyChildren();
break;
case 'successfulSave':
if (scope['$saveSucceeded']?.value) applyChildren();
break;
case 'random':
if (node.children.length){
let index = rollDice(1, node.children.length)[0] - 1;
applyProperty(node.children[index], {
creature, targets, scope, log
});
}
break;
case 'eachTarget':
if (targets.length){
targets.forEach(target => {
node.children.forEach(child => applyProperty(child, {
creature, targets: [target], scope, log
}));
});
} else {
applyChildren();
}
break;
}
}

View File

@@ -0,0 +1,94 @@
import {
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import {setDocToLastOrder} from '/imports/api/parenting/order.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
import { get } from 'lodash';
import resolve, { map } from '/imports/parser/resolve.js';
import logErrors from './shared/logErrors.js';
export default function applyBuff(node, {creature, targets, scope, log}){
const prop = node.node;
let buffTargets = prop.target === 'self' ? [creature] : targets;
// Then copy the decendants of the buff to the targets
let propList = [prop];
function addChildrenToPropList(children){
children.forEach(child => {
propList.push(child.node);
addChildrenToPropList(child.children);
});
}
addChildrenToPropList(node.children);
crystalizeVariables({propList, scope, log});
let oldParent = {
id: prop.parent.id,
collection: prop.parent.collection,
};
buffTargets.forEach(target => {
copyNodeListToTarget(propList, target, oldParent);
});
// 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`
*/
function crystalizeVariables({propList, scope, log}){
propList.forEach(prop => {
computedSchemas[prop.type].computedFields().forEach( calcKey => {
applyFnToKey(prop, calcKey, (prop, key) => {
const calcObj = get(prop, key);
if (!calcObj?.parseNode) return;
map(calcObj.parseNode, node => {
// Skip nodes that aren't symbols or accessors
if (
node.parseType !== 'accessor' && node.parseType !== 'symbol'
) return node;
// Handle variables
if (node.name === '$target'){
// strip $target
if (node.parseType === 'accessor'){
node.name = node.path.shift();
} else {
// Can't strip symbols
log.content.push({
name: 'Error',
value: 'Variable `$target` should not be used without a property: $target.property'
});
}
return node;
} else {
// Resolve all other variables
const {result, context} = resolve('reduce', node, scope);
logErrors(context.errors, log);
return result;
}
});
});
});
});
}

View File

@@ -0,0 +1,115 @@
import applyProperty from '../applyProperty.js';
import { dealDamageWork } from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import resolve, { Context, toString } from '/imports/parser/resolve.js';
import logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
export default function applyDamage(node, {
creature, targets, scope, log
}){
const applyChildren = function(){
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
};
const prop = node.node;
// Skip if there is no parse node to work with
if (!prop.amount.parseNode) return;
// Choose target
let damageTargets = prop.target === 'self' ? [creature] : targets;
// Determine if the hit is critical
let criticalHit = scope['$criticalHit']?.value &&
prop.damageType !== 'healing' // Can't critically heal
;
// Double the damage rolls if the hit is critical
let context = new Context({
options: {doubleRolls: criticalHit},
});
// Gather all the lines we need to log into an array
const logValue = [];
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// Compile the dice roll and store that string first
// const {result: compiled} = resolve('compiled', prop.amount.parseNode, scope, context);
// logValue.push(toString(compiled));
// logErrors(context.errors, log);
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.amount, log);
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
logValue.push(toString(rolled));
logErrors(context.errors, log);
// Reset the errors so we don't log the same errors twice
context.errors = [];
// Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, scope, context);
logErrors(context.errors, log);
// Store the result
if (reduced.parseType === 'constant'){
prop.amount.value = reduced.value;
} else if (reduced.parseType === 'error'){
prop.amount.value = null;
} else {
prop.amount.value = toString(reduced);
}
const damage = +reduced.value;
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' && !isFinite(reduced.value)){
return applyChildren();
}
// Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : ' ') +
prop.damageType +
(prop.damageType !== 'healing' ? ' damage ': '');
if (damageTargets && damageTargets.length) {
// Iterate through all the targets
damageTargets.forEach(target => {
// Deal the damage to the target
let damageDealt = dealDamageWork({
creature: target,
damageType: prop.damageType,
amount: damage,
});
// Log the damage done
if (target._id === creature._id){
// Target is same as self, log damage as such
logValue.push(damageDealt + suffix + ' to self');
} else {
logValue.push('Dealt ' + damageDealt + suffix + ` ${target.name && ' to '}${target.name}`);
// Log the damage received on that creature's log as well
insertCreatureLog.call({
log: {
creatureId: target._id,
content: [{
name,
value: 'Recieved ' + damageDealt + suffix,
}],
}
});
}
});
} else {
// There are no targets, just log the result
logValue.push(damage + suffix);
}
log.content.push({
name: logName,
value: logValue.join('\n'),
});
return applyChildren();
}

View File

@@ -0,0 +1,25 @@
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
import applyProperty from '../applyProperty.js';
export default function applyNote(node, {creature, targets, scope, log}){
const prop = node.node;
// Log Name, summary
let content = { name: prop.name };
if (prop.summary?.text){
recalculateInlineCalculations(prop.summary, scope, log);
content.value = prop.summary.value;
}
if (content.name || content.value){
log.content.push(content);
}
// Log description
if (prop.description?.text){
recalculateInlineCalculations(prop.description, scope, log);
log.content.push({value: prop.description.value});
}
// Apply children
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
}

View File

@@ -0,0 +1,21 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
export default function applyRoll(node, {creature, targets, scope, log}){
const prop = node.node;
if (prop.roll?.calculation){
recalculateCalculation(prop.roll, scope, log);
if (isFinite(prop.roll.value)){
scope[prop.variableName] = prop.roll.value;
}
log.content.push({
name: prop.name,
value: prop.variableName + ' = ' + prop.roll.calculation + ' = ' + prop.roll.value,
});
}
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
}

View File

@@ -0,0 +1,78 @@
import rollDice from '/imports/parser/rollDice.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import applyProperty from '../applyProperty.js';
export default function applySavingThrow(node, {creature, targets, scope, log}){
const prop = node.node;
let saveTargets = prop.target === 'self' ? [creature] : targets;
recalculateCalculation(prop.dc, scope, log);
const dc = (prop.dc?.value);
if (!isFinite(dc)){
log.content.push({
name: 'Error',
value: 'Saving throw requires a DC',
});
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
}
log.content.push({
name: prop.name,
value: ' DC ' + dc,
});
saveTargets.forEach(target => {
delete scope['$saveFailed'];
delete scope['$saveSucceeded'];
delete scope['$saveDiceRoll'];
delete scope['$saveRoll'];
const applyChildren = function(){
node.children.forEach(child => applyProperty(child, {
creature, targets: [target], scope, log
}));
};
const save = target.variables[prop.stat];
if (!save){
log.content.push({
name: 'Saving throw error',
value: 'No saving throw found: ' + prop.stat,
});
return applyChildren();
}
let value, values, resultPrefix;
if (save.advantage === 1){
values = rollDice(2, 20).sort().reverse();
value = values[0];
resultPrefix = `Advantage: 1d20 [${values[0]},~~${values[1]}~~] + ${save.value} = `
} else if (save.advantage === -1){
values = rollDice(2, 20).sort();
value = values[0];
resultPrefix = `Disadvantage: 1d20 [${values[0]},~~${values[1]}~~] + ${save.value} = `
} else {
values = rollDice(1, 20);
value = values[0];
resultPrefix = `1d20 [${value}] + ${save.value} = `
}
scope['$saveDiceRoll'] = {value};
const result = value + save.value || 0;
scope['$saveRoll'] = {value: result};
const saveSuccess = result >= dc;
if (saveSuccess){
scope['$saveSucceeded'] = {value: true};
} else {
scope['$saveFailed'] = {value: true};
}
log.content.push({
name: 'Save',
value: resultPrefix + result + (saveSuccess ? 'Passed' : 'Failed')
});
return applyChildren();
});
}

View File

@@ -0,0 +1,14 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
export default function applyToggle(node, {
creature, targets, scope, log
}){
const prop = node.node;
recalculateCalculation(prop.condition, scope, log);
if (prop.condition?.value) {
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
}
}

View File

@@ -0,0 +1,24 @@
import operator from '/imports/parser/parseTree/operator.js';
import { parse } from '/imports/parser/parser.js';
import logErrors from './logErrors.js';
export default function applyEffectsToCalculationParseNode(calcObj, log){
if (!calcObj.effects) return;
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], log)
}
});
}

View File

@@ -0,0 +1,7 @@
export default function logErrors(errors, log){
errors?.forEach(error => {
if (error.type !== 'info'){
log.content.push({name: 'Error', value: error.message});
}
});
}

View File

@@ -0,0 +1,11 @@
import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
import logErrors from './logErrors.js';
export default function recalculateCalculation(calc, scope, log, context){
if (!calc?.parseNode) return;
calc._parseLevel = 'reduce';
applyEffectsToCalculationParseNode(calc, log);
evaluateCalculation(calc, scope, context);
logErrors(calc.errors, log);
}

View File

@@ -0,0 +1,13 @@
import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations.js';
import recalculateCalculation from './recalculateCalculation.js'
export default function recalculateInlineCalculations(inlineCalcObj, scope, log){
// Skip if there are no calculations
if (!inlineCalcObj?.calculations?.length) return;
// Recalculate each calculation with the current scope
inlineCalcObj.inlineCalculations.forEach(calc => {
recalculateCalculation(calc, scope, log);
});
// Embed the new calculated values
embedInlineCalculations(inlineCalcObj);
}

View File

@@ -1,17 +1,14 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import nodesToTree from '/imports/api/parenting/nodesToTree.js';
import applyProperties from '/imports/api/creature/actions/applyProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import getAncestorContext from '/imports/api/creature/actions/getAncestorContext.js';
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
import applyProperty from './applyProperty.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const doAction = new ValidatedMethod({
name: 'creatureProperties.doAction',
@@ -20,7 +17,7 @@ const doAction = new ValidatedMethod({
targetIds: {
type: Array,
defaultValue: [],
maxCount: 10,
maxCount: 20,
optional: true,
},
'targetIds.$': {
@@ -38,62 +35,90 @@ const doAction = new ValidatedMethod({
// Check permissions
let creature = getRootCreatureAncestor(action);
// Build ancestor context
let actionContext = getAncestorContext(action);
assertEditPermission(creature, this.userId);
// Get all the targets and make sure we can edit them
let targets = [];
targetIds.forEach(targetId => {
let target = Creatures.findOne(targetId);
assertEditPermission(target, this.userId);
targets.push(target);
});
doActionWork({action, creature, targets, actionContext, method: this});
// The acting creature might have used ammo
recomputeInventory(creature._id);
// Fetch all the action's ancestor creatureProperties
const ancestorIds = [];
action.ancestors.forEach(ref => {
if (ref.collection === 'creatureProperties') {
ancestorIds.push(ref.id);
}
});
// The action might add properties which need to be activated
recomputeInactiveProperties(creature._id);
// Get cursor of ancestors
const ancestors = CreatureProperties.find({
_id: {$in: ancestorIds},
}, {
sort: {order: 1},
});
// recompute creatures
recomputeCreatureByDoc(creature);
// Get cursor of the properties
const properties = CreatureProperties.find({
$or: [{_id: action._id}, {'ancestors.id': action._id}],
removed: {$ne: true},
}, {
sort: {order: 1},
});
// Do the action
doActionWork({creature, targets, properties, ancestors, method: this});
// Recompute all involved creatures
computeCreature(creature._id);
targets.forEach(target => {
recomputeInactiveProperties(target._id);
recomputeCreatureByDoc(target);
computeCreature(target._id);
});
},
});
export default doAction;
export function doActionWork({
action,
creature,
targets,
actionContext = {},
method
creature, targets, properties, ancestors, method
}){
// get the docs
const ancestorScope = getAncestorScope(ancestors);
const propertyForest = nodeArrayToTree(properties);
if (propertyForest.length !== 1){
throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`);
}
// Create the log
let log = CreatureLogSchema.clean({
creatureId: creature._id,
creatureName: creature.name,
});
let decendantForest = nodesToTree({
collection: CreatureProperties,
ancestorId: action._id,
});
let startingForest = [{
node: action,
children: decendantForest,
}];
applyProperties({
forest: startingForest,
actionContext,
// Apply the top level property, it is responsible for applying its children
// recursively
const scope = {
...creature.variables,
...ancestorScope,
}
applyProperty(propertyForest[0], {
creature,
targets,
scope,
log,
});
// Insert the log
insertCreatureLogWork({log, creature, method});
}
export default doAction;
// Assumes ancestors are in tree order already
function getAncestorScope(ancestors){
let scope = {};
ancestors.forEach(prop => {
scope[`#${prop.type}`] = prop;
});
return scope;
}

View File

@@ -0,0 +1,11 @@
import '/imports/api/simpleSchemaConfig.js';
//import testTypes from './testTypes/index.js';
import { doActionWork } from './doAction.js';
import createAction from './tests/createAction.testFn.js';
describe('Do Action', function(){
it('Does an empty action', function(){
doActionWork(createAction({properties: [{type: 'action'}]}));
});
//testTypes.forEach(test => it(test.text, test.fn));
});

View File

@@ -0,0 +1,54 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import doAction from '../doAction.js';
const commitAction = new ValidatedMethod({
name: 'creatureProperties.doAction',
validate: new SimpleSchema({
actionId: SimpleSchema.RegEx.Id,
targetIds: {
type: Array,
defaultValue: [],
maxCount: 20,
optional: true,
},
'targetIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({actionId, targetIds = []}) {
let action = CreatureProperties.findOne(actionId);
// Check permissions
let creature = getRootCreatureAncestor(action);
assertEditPermission(creature, this.userId);
let targets = [];
targetIds.forEach(targetId => {
let target = Creatures.findOne(targetId);
assertEditPermission(target, this.userId);
targets.push(target);
});
doAction({action, creature, targets, method: this});
// recompute creatures
computeCreature(creature._id);
targets.forEach(target => {
computeCreature(target._id);
});
},
});
export default commitAction;

View File

@@ -0,0 +1,26 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
export default function createAction({
creature = {_id: 'creatureId'},
targets = [],
properties = [],
ancestors = [],
method
} = {}){
properties = properties.map(cleanProp);
ancestors = ancestors.map(cleanProp);
creature = cleanCreature(creature);
ancestors = ancestors.map(cleanCreature);
return {creature, targets, properties, ancestors, method};
}
function cleanProp(prop){
let schema = CreatureProperties.simpleSchema(prop);
return schema.clean(prop);
}
function cleanCreature(creature){
let schema = Creatures.simpleSchema(creature);
return schema.clean(creature);
}

View File

@@ -0,0 +1,6 @@
import applyAction from './applyAction.testFn.js';
export default [{
text: 'Applies actions',
fn: applyAction,
},];

View File

@@ -0,0 +1,37 @@
import { EJSON } from 'meteor/ejson';
import createGraph from 'ngraph.graph';
export default class CreatureComputation {
constructor(properties){
// Set up fields
this.originalPropsById = {};
this.propsById = {};
this.propsWithTag = {};
this.scope = {};
this.props = properties;
this.dependencyGraph = createGraph();
// Store properties for easy access later
properties.forEach(prop => {
// Store a copy of the unmodified prop
// EJSON clone is ~4x faster than lodash cloneDeep for EJSONable objects
this.originalPropsById[prop._id] = EJSON.clone(prop);
// Store by id
this.propsById[prop._id] = prop;
// Store sets of ids in each tag
if (prop.tags){
prop.tags.forEach(tag => {
if (this.propsWithTag[tag]){
this.propsWithTag[tag].push(prop._id);
} else {
this.propsWithTag[tag] = [prop._id];
}
});
}
// Store the prop in the dependency graph
this.dependencyGraph.addNode(prop._id, prop);
});
}
}

View File

@@ -0,0 +1,42 @@
import walkDown from '/imports/api/engine/computation/utility/walkdown.js';
export default function computeInactiveStatus(node){
const prop = node.node;
if (!isActive(prop)){
// Mark prop inactive due to self
prop.inactive = true;
prop.deactivatedBySelf = true;
}
if(!childrenActive(prop)){
// Mark children as inactive due to ancestor
walkDown(node.children, child => {
child.node.inactive = true;
child.node.deactivatedByAncestor = true;
});
}
}
function isActive(prop){
if (prop.disabled) return false;
switch (prop.type){
// Unprepared spells are inactive
case 'spell': return !!prop.prepared || !!prop.alwaysPrepared;
default: return true;
}
}
function childrenActive(prop){
// Children of disabled properties are always inactive
if (prop.disabled) return false;
switch (prop.type){
// Only equipped items have active children
case 'item': return !!prop.equipped;
// The children of actions are always inactive
case 'action': return false;
case 'spell': return false;
// The children of notes are always inactive
case 'note': return false;
// Other children are active
default: return true;
}
}

View File

@@ -0,0 +1,21 @@
/**
* Only computes `totalFilled`, need to compute `quantityExpected.value`
* before `spacesLeft` can be computed
*/
export default function computeSlotQuantityFilled(node, dependencyGraph){
let slot = node.node;
if (slot.type !== 'propertySlot') return;
slot.totalFilled = 0;
node.children.forEach(child => {
let childProp = child.node;
dependencyGraph.addLink(slot._id, childProp._id, 'slotFill');
if (
childProp.type === 'slotFiller' &&
Number.isFinite(childProp.slotQuantityFilled)
){
slot.totalFilled += childProp.slotQuantityFilled;
} else {
slot.totalFilled++;
}
});
}

View File

@@ -0,0 +1,17 @@
import walkDown from '/imports/api/engine/computation/utility/walkdown.js';
export default function computeToggleDependencies(node, dependencyGraph){
const prop = node.node;
// Only for toggles that aren't inactive and aren't set to enabled or disabled
if (
prop.inactive ||
prop.type !== 'toggle' ||
prop.disabled ||
prop.enabled
) return;
walkDown(node.children, child => {
child.node._computationDetails.toggleAncestors.push(prop);
// The child nodes depend on the toggle condition compuation
dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
});
}

View File

@@ -0,0 +1,51 @@
import findAncestorByType from '/imports/api/engine/computation/utility/findAncestorByType.js';
import { traverse } from '/imports/parser/resolve.js';
export default function linkCalculationDependencies(dependencyGraph, prop, {propsById}){
prop._computationDetails.calculations.forEach(calcObj => {
// Store resolved ancestors
const memo = {
// ancestors: {} //this gets added if there are resolved ancestors
};
// Add this calculation to the dependency graph
const calcNodeId = `${prop._id}.${calcObj._key}`;
dependencyGraph.addNode(calcNodeId, calcObj);
// Traverse the parsed calculation looking for variable names
traverse(calcObj.parseNode, node => {
// Skip nodes that aren't symbols or accessors
if (node.parseType !== 'symbol' && node.parseType !== 'accessor') return;
// Link ancestor references as direct property dependencies
if (node.name[0] === '#'){
let ancestorProp = getAncestorProp(
node.name.slice(1), memo, prop, propsById
);
if (!ancestorProp) return;
// Link the ancestor prop as a direct dependency
dependencyGraph.addLink(
calcNodeId, ancestorProp._id, 'ancestorReference'
);
} else {
// Link variable name references as variable dependencies
dependencyGraph.addLink(
calcNodeId, node.name, 'variableReference'
);
}
});
// Store the resolved ancestors in this calculation's local scope
if (memo.ancestors) {
calcObj._localScope = { ...calcObj._localScope, ...memo.ancestors};
}
});
}
function getAncestorProp(type, memo, prop, propsById){
if (memo.ancestors && memo.ancestors['#' + type]){
return memo.ancestors['#' + type];
} else {
var ancestorProp = findAncestorByType( prop, type, propsById );
if (!memo.ancestors) memo.ancestors = {};
memo.ancestors['#' + type] = ancestorProp;
return ancestorProp;
}
}

View File

@@ -0,0 +1,62 @@
/**
* Performs a depth first traversal of the character tree, summing the container
* and inventory contents on the way up the tree
*/
export default function linkInventory(forest, dependencyGraph){
// The stack of properties to still navigate
const stack = [...forest];
// The current containers we are inside of
const containerStack = [];
while(stack.length){
const top = stack[stack.length - 1];
const prop = top.node;
if (prop._computationDetails.inventoryChildrenVisited){
if (prop.type === 'container') containerStack.pop();
stack.pop();
handleProp(prop, containerStack, dependencyGraph);
} else {
// Add all containers to the stack when we first visit them
if (prop.type === 'container'){
containerStack.push(top.node);
}
// Push children onto the stack and mark this as children are visited
stack.push(...top.children);
prop._computationDetails.inventoryChildrenVisited = true;
}
}
}
function handleProp(prop, containerStack, dependencyGraph){
// Skip props that aren't part of the inventory
if (prop.type !== 'item' && prop.type !== 'container') return;
// Determine if this property is carried, items are carried by default
let carried = prop.type === 'container' ? prop.carried : true;
// Item-specific links
if (prop.type === 'item'){
if (prop.attuned){
dependencyGraph.addLink('itemsAttuned', prop._id, 'attunedItem');
}
if (prop.equipped){
dependencyGraph.addLink('weightEquipment', prop._id, 'equippedItem');
dependencyGraph.addLink('valueEquipment', prop._id, 'equippedItem');
}
}
// Get the parent container
const container = containerStack[containerStack.length - 1];
if (container){
// The container depends on this prop for its contents data
dependencyGraph.addLink(container._id, prop._id, 'containerContents');
} else {
// There is no parent container, the character totals depend on this prop
dependencyGraph.addLink('weightTotal', prop._id, 'inventoryStats');
dependencyGraph.addLink('valueTotal', prop._id, 'inventoryStats');
if (carried){
dependencyGraph.addLink('weightCarried', prop._id, 'inventoryStats');
dependencyGraph.addLink('valueCarried', prop._id, 'inventoryStats');
}
}
}

View File

@@ -0,0 +1,262 @@
import { get, intersection, difference } from 'lodash';
const linkDependenciesByType = {
action: linkAction,
adjustment: linkAdjustment,
attribute: linkAttribute,
branch: linkBranch,
buff: linkBuff,
class: linkVariableName,
classLevel: linkClassLevel,
constant: linkVariableName,
damage: linkDamage,
damageMultiplier: linkDamageMultiplier,
effect: linkEffects,
proficiency: linkProficiencies,
roll: linkRoll,
slot: linkSlot,
skill: linkSkill,
spell: linkAction,
spellList: linkSpellList,
savingThrow: linkSavingThrow,
toggle: linkToggle,
}
export default function linkTypeDependencies(dependencyGraph, prop, computation){
linkDependenciesByType[prop.type]?.(dependencyGraph, prop, computation);
}
function dependOnCalc({dependencyGraph, prop, key}){
let calc = get(prop, key);
if (!calc) return;
if (calc.type !== '_calculation'){
console.log(calc);
throw `Expected calculation got ${calc.type}`
}
dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation');
}
function linkAction(dependencyGraph, prop, {propsById}){
// The action depends on its attack roll and uses calculations
dependOnCalc({dependencyGraph, prop, key: 'attackRoll'});
dependOnCalc({dependencyGraph, prop, key: 'uses'});
// Link the resources the action uses
if (!prop.resources) return;
// Link items consumed
prop.resources.itemsConsumed.forEach((itemConsumed, index) => {
if (!itemConsumed.itemId) return;
const item = propsById[itemConsumed.itemId];
if (!item || item.inactive){
// Unlink if the item doesn't exist or is inactive
itemConsumed.itemId = undefined;
return;
}
// none of these dependencies are computed, we can use them immediately
itemConsumed.available = item.quantity;
itemConsumed.itemName = item.name;
itemConsumed.itemIcon = item.icon;
itemConsumed.itemColor = item.color;
dependencyGraph.addLink(prop._id, item._id, 'inventory');
// Link the property to its resource quantity calculation
dependOnCalc({
dependencyGraph,
prop,
key: `${prop._id}.resources.itemsConsumed.${index}.quantity`,
});
});
// Link attributes consumed
prop.resources.attributesConsumed.forEach((attConsumed, index) => {
if (!attConsumed.variableName) return;
dependencyGraph.addLink(prop._id, attConsumed.variableName, 'resource');
// Link the property to its resource quantity calculation
dependOnCalc({
dependencyGraph,
prop,
key: `${prop._id}.resources.attributesConsumed.${index}.quantity`,
});
});
}
function linkAdjustment(dependencyGraph, prop){
// Adjustment depends on its amount
dependOnCalc({dependencyGraph, prop, key: 'amount'});
}
function linkAttribute(dependencyGraph, prop){
linkVariableName(dependencyGraph, prop);
// Depends on spellSlotLevel
dependOnCalc({dependencyGraph, prop, key: 'spellSlotLevel'});
// Depends on base value
dependOnCalc({dependencyGraph, prop, key: 'baseValue'});
// hit dice depend on constitution
if (prop.attributeType === 'hitDice'){
dependencyGraph.addLink(prop._id, 'constitution', 'hitDiceConMod');
}
}
function linkBranch(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'condition'});
}
function linkBuff(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'duration'});
}
function linkClassLevel(dependencyGraph, prop){
// The variableName of the prop depends on the prop
if (prop.variableName && prop.level){
dependencyGraph.addLink(prop.variableName, prop._id, 'classLevel');
// The level variable depends on the class variableName variable
let existingLevelLink = dependencyGraph.getLink('level', prop.variableName);
if (!existingLevelLink){
dependencyGraph.addLink('level', prop.variableName, 'level');
}
}
}
function linkDamage(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'amount'});
}
function linkEffects(dependencyGraph, prop, computation){
// The effect depends on its amount calculation
dependOnCalc({dependencyGraph, prop, key: 'amount'});
// The stats depend on the effect
if (prop.targetByTags){
getEffectTagTargets(prop, computation).forEach(targetId => {
const targetProp = computation.propsById[targetId];
const key = prop.targetField || getDefaultCalculationField(targetProp);
const calcObj = get(targetProp, key);
if (calcObj && calcObj.calculation){
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id , 'effect');
}
});
} else {
prop.stats.forEach(statName => {
if (!statName) return;
dependencyGraph.addLink(statName, prop._id, 'effect');
});
}
}
// Returns an array of IDs of the properties the effect targets
function getEffectTagTargets(effect, computation){
const targets = getTargetListFromTags(effect.targetTags, computation);
const notIds = [];
if (effect.extraTags){
effect.extraTags.forEach(ex => {
if (ex.operation === 'OR'){
targets.push(...getTargetListFromTags(ex.tags, computation));
} else if (ex.operation === 'NOT'){
ex.tags.forEach(tag => {
const idList = computation.propsWithTag[tag];
if (idList) notIds.push(...computation.propsWithTag[tag])
});
}
});
}
return difference(targets, notIds);
}
function getTargetListFromTags(tags, computation){
const targetTagIdLists = [];
if (!tags) return [];
tags.forEach(tag => {
const idList = computation.propsWithTag[tag];
if (idList) targetTagIdLists.push(idList);
});
const targets = intersection(...targetTagIdLists);
return targets;
}
function getDefaultCalculationField(prop){
switch (prop.type){
case 'action': return 'attackRoll';
case 'adjustment': return 'amount';
case 'attribute': return 'baseValue';
case 'branch': return 'condition';
case 'buff': return 'duration';
case 'class': return null;
case 'classLevel': return null;
case 'constant': return null;
case 'container': return null;
case 'damageMultiplier': return null;
case 'damage': return 'amount';
case 'effect': return 'amount';
case 'feature': return null;
case 'folder': return null;
case 'item': return null;
case 'note': return null;
case 'proficiency': return null;
case 'reference': return null;
case 'roll': return 'roll';
case 'savingThrow': return 'dc';
case 'skill': return 'baseValue';
case 'slotFiller': return null;
case 'slot': return 'quantityExpected';
case 'spellList': return 'attackRollBonus';
case 'spell': return null;
case 'toggle': return 'condition';
}
}
function linkRoll(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'roll'});
}
function linkVariableName(dependencyGraph, prop){
// The variableName of the prop depends on the prop
if (prop.variableName){
dependencyGraph.addLink(prop.variableName, prop._id, 'definition');
}
}
function linkDamageMultiplier(dependencyGraph, prop){
prop.damageTypes.forEach(damageType => {
// Remove all non-letter characters from the damage name
const damageName = damageType.replace(/[^a-z]/gi, '')
dependencyGraph.addLink(`${damageName}Multiplier`, prop._id, prop.type);
});
}
function linkProficiencies(dependencyGraph, prop){
// The stats depend on the proficiency
prop.stats.forEach(statName => {
if (!statName) return;
dependencyGraph.addLink(statName, prop._id, prop.type);
});
}
function linkSavingThrow(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'dc'});
}
function linkSkill(dependencyGraph, prop){
linkVariableName(dependencyGraph, prop);
// The prop depends on the variable references as the ability
if (prop.ability){
dependencyGraph.addLink(prop._id, prop.ability, 'skillAbilityScore');
}
// Skills depend on the creature's proficiencyBonus
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
}
function linkSlot(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'quantityExpected'});
dependOnCalc({dependencyGraph, prop, key: 'slotCondition'});
}
function linkSpellList(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'maxPrepared'});
dependOnCalc({dependencyGraph, prop, key: 'attackRollBonus'});
dependOnCalc({dependencyGraph, prop, key: 'dc'});
}
function linkToggle(dependencyGraph, prop){
linkVariableName(dependencyGraph, prop);
dependOnCalc({dependencyGraph, prop, key: 'condition'});
}

View File

@@ -0,0 +1,105 @@
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
import { prettifyParseError, parse } from '/imports/parser/parser.js';
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
import { get, unset } from 'lodash';
import errorNode from '/imports/parser/parseTree/error.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
export default function parseCalculationFields(prop, schemas){
discoverInlineCalculationFields(prop, schemas);
parseAllCalculationFields(prop, schemas);
}
function discoverInlineCalculationFields(prop, schemas){
// For each key in the schema
schemas[prop.type].inlineCalculationFields().forEach( calcKey => {
// That ends in .inlineCalculations
applyFnToKey(prop, calcKey, (prop, key) => {
const inlineCalcObj = get(prop, key);
if (!inlineCalcObj) return;
// Store a reference to all the inline calculations
prop._computationDetails.inlineCalculations.push(inlineCalcObj);
// Extract the calculations and store them on the property
let string = inlineCalcObj.text;
// If there is no text, delete the whole field
if (!string){
unset(prop, calcKey);
return;
}
// Set the value to the uncomputed string for use in calculations
inlineCalcObj.value = string;
// Has the text, if it matches the existing hash, stop
const inlineCalcHash = cyrb53(inlineCalcObj.text);
if (inlineCalcHash === inlineCalcObj.hash){
return;
}
inlineCalcObj.hash = inlineCalcHash;
inlineCalcObj.inlineCalculations = [];
// It will be re set including the embedded calculation at the end of
// the computation
let matches = string.matchAll(INLINE_CALCULATION_REGEX);
for (let match of matches){
let calculation = match[1];
inlineCalcObj.inlineCalculations.push({
calculation,
});
}
});
});
}
function parseAllCalculationFields(prop, schemas){
// For each computed key in the schema
schemas[prop.type].computedFields().forEach( calcKey => {
// Determine the level the calculation should compute down to
let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel || 'reduce';
// Special case of effects, when targeting by tags compile
if (prop.type === 'effect' && prop.targetByTags) parseLevel = 'compile';
// For all fields matching they keys
// supports `keys.$.with.$.arrays`
applyFnToKey(prop, calcKey, (prop, key) => {
const calcObj = get(prop, key);
if (!calcObj) return;
// Delete the whole calculation object if the calculation string isn't set
if (!calcObj.calculation){
unset(prop, calcKey);
return;
}
// Store a reference to all the calculations
prop._computationDetails.calculations.push(calcObj);
// Store the level to compute down to later
calcObj._parseLevel = parseLevel;
// Store the key
calcObj._key = key;
// Set a type
calcObj.type = '_calculation';
// Parse the calculation
parseCalculation(calcObj);
});
});
}
function parseCalculation(calcObj){
const calcHash = cyrb53(calcObj.calculation);
// If the cached parse calculation is equal to the calculation, skip
if (calcHash === calcObj.hash){
return;
}
calcObj.hash = calcHash;
try {
calcObj.parseNode = parse(calcObj.calculation);
delete calcObj.parseError;
} catch (e) {
let error = {
type: 'evaluation',
message: prettifyParseError(e),
};
calcObj.parseError = error;
calcObj.parseNode = errorNode.create({error});
}
}

Some files were not shown because too many files have changed in this diff Show More