Completed first pass at action system re-write. Untested

This commit is contained in:
Stefan Zermatten
2021-10-10 19:44:02 +02:00
parent 0cdec4a429
commit 51d3fbbcb7
21 changed files with 232 additions and 840 deletions

View File

@@ -35,6 +35,9 @@ export default function applyAction(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.rollBonus, scope, log);
@@ -54,18 +57,22 @@ function applyAttackWithoutTarget({prop, scope, log}){
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.rollBonus, scope, log);
const value = rollDice(1, 20)[0];
scope['$attackRoll'] = {value};
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.rollBonus.value;
scope['$toHit'] = {value: result};
scope['$attackRoll'] = {value: result};
if (target.variables.armor){
const armor = target.variables.armor.value;
const name = criticalHit ? 'Critical Hit!' :

View File

@@ -1,51 +1,32 @@
// import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import applyProperty from '../applyProperty.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';
import recalculateCalculation from './shared/recalculateCalculation.js';
import { Context } from '/imports/parser/resolve.js';
export default function applyDamage({
prop,
creature,
targets,
actionContext,
log,
export default function applyDamage(node, {
creature, targets, scope, log
}){
let damageTargets = prop.target === 'self' ? [creature] : targets;
let scope = {
...creature.variables,
...actionContext,
const applyChildren = function(){
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
};
// Add the target's variables to the scope
if (targets.length === 1){
scope.target = targets[0].variables;
}
const prop = node.node;
let damageTargets = prop.target === 'self' ? [creature] : targets;
// Determine if the hit is critical
let criticalHit = !!(
actionContext.criticalHit &&
actionContext.criticalHit.value &&
let criticalHit = scope['$criticalHit']?.value &&
prop.damageType !== 'healing' // Can't critically heal
);
;
// Double the damage rolls if the hit is critical
let context = new CompilationContext({
doubleRolls: criticalHit,
let context = new Context({
options: {doubleRolls: criticalHit},
});
recalculateCalculation(prop.amount, scope, log, context);
// 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;
}
// If we didn't end up with a finite amount, give up
if (!isFinite(prop.amount?.value)) return applyChildren();
// Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : ' ') +
@@ -57,28 +38,11 @@ export default function applyDamage({
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,
amount: prop.amount.value,
});
// Log the damage done
@@ -109,7 +73,8 @@ export default function applyDamage({
// There are no targets, just log the result
log.content.push({
name: prop.damageType === 'healing' ? 'Healing' : 'Damage',
value: result.toString() + suffix,
value: prop.amount.value + suffix,
});
}
return applyChildren();
}

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, context);
if (isFinite(prop.roll.value)){
scope[prop.variableName] = prop.roll.value;
}
log.content.push({
name: prop.name,
value: prop.variableName + ' = ' + prop.roll + ' = ' + 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}){
let saveTargets = prop.target === 'self' ? [creature] : targets;
const prop = node.node;
recalculateCalculation(prop.dc, scope, log, context);
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

@@ -1,9 +1,9 @@
import evaluateCalculation from '../utility/evaluateCalculation.js';
import logErrors from './logErrors.js';
export default function recalculateCalculation(calc, scope, log){
export default function recalculateCalculation(calc, scope, log, context){
if (!calc.parseNode) return;
calc._parseLevel = 'reduce';
evaluateCalculation(calc, scope);
evaluateCalculation(calc, scope, context);
logErrors(calc.errors, log);
}

View File

@@ -1,15 +1,90 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
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 { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
import applyProperty from './applyProperty.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
export default function doAction({actionId, targetIds, method}){
const doAction = 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);
// 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);
});
// Fetch all the action's ancestor creatureProperties
const ancestorIds = [];
action.ancestors.forEach(ref => {
if (ref.collection === 'creatureProperties') {
ancestorIds.push(ref.id);
}
});
// Get cursor of ancestors
const ancestors = CreatureProperties.find({
_id: {$in: ancestorIds},
}, {
sort: {order: 1},
});
// 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
Meteor.defer(() => computeCreature(creature._id));
targets.forEach(target => {
Meteor.defer(() => computeCreature(target._id));
});
},
});
export default doAction;
export function doActionWork({
creature, targets, properties, ancestors, method
}){
// get the docs
const {
creature, targets, properties, ancestors
} = fetchActionDocs(actionId, targetIds);
const ancestorScope = getAncestorScope(ancestors);
const propertyForest = nodeArrayToTree(properties);
if (propertyForest.length !== 1){
@@ -37,60 +112,6 @@ export default function doAction({actionId, targetIds, method}){
// Insert the log
insertCreatureLogWork({log, creature, method});
// Recompute the creature and targets
Meteor.defer(() => computeCreature(creature._id));
targetIds.forEach(targetId => {
Meteor.defer(() => computeCreature(targetId));
});
}
function fetchActionDocs(actionId, targetIds){
// Fetch the action with ancestors only
const action = CreatureProperties.findOne({
_id: actionId,
removed: {$ne: true},
}, {
fields: {ancestors: 1}
});
if (!action) throw new Meteor.Error('The specified action was not found');
// Fetch all the action's ancestor creatureProperties
const ancestorIds = [];
action.ancestors.forEach(ref => {
if (ref.collection === 'creatureProperties') {
ancestorIds.push(ref.id);
}
});
// Get cursor of ancestors
const ancestors = CreatureProperties.find({
_id: {$in: ancestorIds},
}, {
sort: {order: 1},
});
// Fetch the action's top level ancestor creature
const creature = Creatures.findOne(action.ancestors[0].id, {
fields: {variables: 1},
});
if (!creature) throw new Meteor.Error('The creature for this action was not found');
// Fetch all the target creatures
const targets = Creatures.find({
_id: targetIds,
}, {
fields: {variables: 1},
}).fetch();
// Get cursor of the properties
const properties = CreatureProperties.find({
$or: [{_id: actionId}, {'ancestors.id': actionId}],
removed: {$ne: true},
}, {
sort: {order: 1},
});
return {action, creature, targets, properties, ancestors}
}
// Assumes ancestors are in tree order already

View File

@@ -3,22 +3,19 @@ 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 { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import nodesToTree from '/imports/api/parenting/nodesToTree.js';
import applyProperties from '/imports/api/creature/actions/applyProperties.js';
import getAncestorContext from '/imports/api/creature/actions/getAncestorContext.js';
import doAction from '../doAction.js';
const doAction = new ValidatedMethod({
const commitAction = new ValidatedMethod({
name: 'creatureProperties.doAction',
validate: new SimpleSchema({
actionId: SimpleSchema.RegEx.Id,
targetIds: {
type: Array,
defaultValue: [],
maxCount: 10,
maxCount: 20,
optional: true,
},
'targetIds.$': {
@@ -36,9 +33,6 @@ const doAction = new ValidatedMethod({
// Check permissions
let creature = getRootCreatureAncestor(action);
// Build ancestor context
let actionContext = getAncestorContext(action);
assertEditPermission(creature, this.userId);
let targets = [];
targetIds.forEach(targetId => {
@@ -46,7 +40,7 @@ const doAction = new ValidatedMethod({
assertEditPermission(target, this.userId);
targets.push(target);
});
doActionWork({action, creature, targets, actionContext, method: this});
doAction({action, creature, targets, method: this});
// recompute creatures
computeCreature(creature._id);
@@ -57,35 +51,4 @@ const doAction = new ValidatedMethod({
},
});
export function doActionWork({
action,
creature,
targets,
actionContext = {},
method
}){
// 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,
creature,
targets,
log,
});
insertCreatureLogWork({log, creature, method});
}
export default doAction;
export default commitAction;

View File

@@ -1,10 +1,10 @@
import resolve, { toString } from '/imports/parser/resolve.js';
export default function evaluateCalculation(calculation, scope){
export default function evaluateCalculation(calculation, scope, givenContext){
const parseNode = calculation.parseNode;
const fn = calculation._parseLevel;
const calculationScope = {...calculation._localScope, ...scope};
const {result: resultNode, context} = resolve(fn, parseNode, calculationScope);
const {result: resultNode, context} = resolve(fn, parseNode, calculationScope, givenContext);
calculation.errors = context.errors;
if (resultNode?.parseType === 'constant'){
calculation.value = resultNode.value;

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 rollDice from '/imports/parser/rollDice.js';
export default function applyAttack({
prop,
log,
actionContext,
creature,
}){
let value = rollDice(1, 20)[0];
actionContext.attackRoll = {value};
let criticalHitTarget = creature.variables.criticalHitTarget &&
creature.variables.criticalHitTarget.value || 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,81 +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(args){
let prop = args.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':
if (prop.attackRoll && prop.attackRoll.calculation){
applyAttack(args)
}
applyAction(args);
break;
case 'damage':
applyDamage(args);
break;
case 'adjustment':
applyAdjustment(args);
break;
case 'buff':
applyBuff(args);
return false;
case 'toggle':
return applyToggle(args);
case 'roll':
applyRoll(args);
break;
case 'savingThrow':
return applySave(args);
}
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 computeCreature from '/imports/api/engine/computeCreature.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.value){
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

@@ -1,68 +0,0 @@
import SimpleSchema from 'simpl-schema';
import {
ItemConsumedSchema,
ComputedOnlyItemConsumedSchema,
ComputedItemConsumedSchema
} from '/imports/api/properties/subSchemas/ItemConsumedSchema.js';
import {
AttributeConsumedSchema,
ComputedOnlyAttributeConsumedSchema,
ComputedAttributeConsumedSchema
} from '/imports/api/properties/subSchemas/AttributeConsumedSchema.js';
const ResourcesSchema = new SimpleSchema({
itemsConsumed: {
type: Array,
defaultValue: [],
},
'itemsConsumed.$': {
type: ItemConsumedSchema,
},
attributesConsumed: {
type: Array,
defaultValue: [],
},
'attributesConsumed.$': {
type: AttributeConsumedSchema,
},
});
const ResourcesComputedOnlySchema = new SimpleSchema({
itemsConsumed: {
type: Array,
defaultValue: [],
},
'itemsConsumed.$': {
type: ComputedOnlyItemConsumedSchema,
},
attributesConsumed: {
type: Array,
defaultValue: [],
},
'attributesConsumed.$': {
type: ComputedOnlyAttributeConsumedSchema,
},
});
const ResourcesComputedSchema = new SimpleSchema({
itemsConsumed: {
type: Array,
defaultValue: [],
},
'itemsConsumed.$': {
type: ComputedItemConsumedSchema,
},
attributesConsumed: {
type: Array,
defaultValue: [],
},
'attributesConsumed.$': {
type: ComputedAttributeConsumedSchema,
},
});
export {
ResourcesSchema,
ResourcesComputedOnlySchema,
ResourcesComputedSchema,
};