Completed first pass at action system re-write. Untested
This commit is contained in:
@@ -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!' :
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
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 recalculateCalculation from './shared/recalculateCalculation.js';
|
||||
import { Context } from '/imports/parser/resolve.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;
|
||||
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},
|
||||
});
|
||||
recalculateCalculation(prop.amount, scope, log, context);
|
||||
|
||||
// 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 ' : ' ') +
|
||||
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';
|
||||
|
||||
// Deal the damage to the target
|
||||
let damageDealt = dealDamage.call({
|
||||
creatureId: target._id,
|
||||
damageType: prop.damageType,
|
||||
amount: prop.amount.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: prop.amount.value + suffix,
|
||||
});
|
||||
}
|
||||
return applyChildren();
|
||||
}
|
||||
@@ -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
|
||||
}));
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
54
app/imports/api/engine/actions/methods/commitAction.js
Normal file
54
app/imports/api/engine/actions/methods/commitAction.js
Normal 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;
|
||||
Reference in New Issue
Block a user