Start of action system re-write

This commit is contained in:
Stefan Zermatten
2021-10-09 12:36:06 +02:00
parent 0097696cc8
commit 0cdec4a429
41 changed files with 783 additions and 119 deletions

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 { computeCreatureDependencyGroup } from '/imports/api/engine/computeCreature.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) computeCreatureDependencyGroup(lastProperty);
}
});

View File

@@ -45,21 +45,17 @@ const damageProperty = new ValidatedMethod({
});
export function damagePropertyWork({property, operation, value}){
let damage, newValue;
if (operation === 'set'){
let currentValue = property.value;
const currentValue = property.value;
// 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 = currentValue - value;
// Damage can't exceed total value
if (damage > currentValue) damage = currentValue;
// 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 currentDamage = property.damage;
@@ -68,13 +64,16 @@ export function damagePropertyWork({property, operation, 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
});
}
export default damageProperty;

View File

@@ -125,7 +125,7 @@ export function insertCreatureLogWork({log, creature, method}){
// Insert it
let id = CreatureLogs.insert(log);
if (Meteor.isServer){
method.unblock();
method?.unblock();
removeOldLogs(creature._id);
logWebhook({log, creature});
}

View File

@@ -0,0 +1,15 @@
const applyPropertyByType = {
action,
branch,
buff,
damage,
roll,
savingThrow,
spell,
toggle,
};
export default function applyProperty(node, ...args){
return applyPropertyByType[node.node.type]?.(node, ...args);
}

View File

@@ -0,0 +1,200 @@
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];
const failed = spendResources({prop, log, scope});
if (failed) return;
let content = { name: prop.name };
if (prop.summary?.text){
recalculateInlineCalculations(prop.summary, scope, log);
content.value = prop.summary.value;
}
log.content.push(content);
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});
}
}
}
function applyAttackWithoutTarget({prop, scope, log}){
delete scope['$attackHit'];
delete scope['$attackMiss'];
recalculateCalculation(prop.rollBonus, 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};
let result = value + prop.rollBonus.value;
scope['$toHit'] = {value: result};
log.content.push({
name: criticalHit ? 'Critical Hit!' : 'To Hit',
value: `1d20 {${value}} + ${prop.rollBonus.value} = ` + result,
});
}
function applyAttackToTarget({prop, target, scope, log}){
delete scope['$attackHit'];
delete scope['$attackMiss'];
recalculateCalculation(prop.rollBonus, scope, log);
const value = rollDice(1, 20)[0];
scope['$attackRoll'] = {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};
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.rollBonus.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.rollBonus.value} = ` + result,
});
}
}
function applyChildren(node, args){
node.children.forEach(child => applyProperty(child, args));
}
function spendResources({prop, log, scope}){
// Check Uses
if (prop.usesUsed >= prop.uses?.value){
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 => {
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) 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);
}
});
} 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.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 = scope[attConsumed.variableName];
if (!stat) return;
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,61 @@
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);
prop.amount.errors?.forEach(error => {
if (error.type !== 'info'){
log.content.push({name: 'Error', value: error.message});
}
});
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;
}
damagePropertyWork({
property: stat,
operation: prop.operation,
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,42 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.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 '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,95 @@
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
prop.applied = true;
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,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,9 @@
import evaluateCalculation from '../utility/evaluateCalculation.js';
import logErrors from './logErrors.js';
export default function recalculateCalculation(calc, scope, log){
if (!calc.parseNode) return;
calc._parseLevel = 'reduce';
evaluateCalculation(calc, scope);
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

@@ -0,0 +1,103 @@
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 { 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}){
// get the docs
const {
creature, targets, properties, ancestors
} = fetchActionDocs(actionId, targetIds);
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,
});
// 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});
// 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
function getAncestorScope(ancestors){
let scope = {};
ancestors.forEach(prop => {
scope[`#${prop.type}`] = prop;
});
return scope;
}

View File

@@ -27,7 +27,6 @@ function computeResources(computation, node){
const att = computation.scope[attConsumed.variableName];
if (!att._id) return;
attConsumed.available = att.value;
attConsumed.statId = att._id;
attConsumed.statName = att.name;
});
}

View File

@@ -1,5 +1,5 @@
import resolve, { toString } from '/imports/parser/resolve.js';
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
import embedInlineCalculations from '../utility/embedInlineCalculations.js';
import evaluateCalculation from '../utility/evaluateCalculation.js';
export default function computeCalculations(computation, node){
if (!node.data) return;
@@ -11,32 +11,3 @@ export default function computeCalculations(computation, node){
embedInlineCalculations(inlineCalcObj);
});
}
function evaluateCalculation(calculation, scope){
const parseNode = calculation.parseNode;
const fn = calculation._parseLevel;
const calculationScope = {...calculation._localScope, ...scope};
const {result: resultNode, context} = resolve(fn, parseNode, calculationScope);
calculation.errors = context.errors;
if (resultNode?.parseType === 'constant'){
calculation.value = resultNode.value;
} else if (resultNode?.parseType === 'error'){
calculation.value = null;
} else {
calculation.value = toString(resultNode);
}
// remove the working fields
delete calculation._parseLevel;
delete calculation._localScope;
}
function embedInlineCalculations(inlineCalcObj){
const string = inlineCalcObj.text;
const calculations = inlineCalcObj.inlineCalculations;
if (!string || !calculations) return;
let index = 0;
inlineCalcObj.value = string.replace(INLINE_CALCULATION_REGEX, substring => {
let calc = calculations[index++];
return (calc && 'value' in calc) ? calc.value : substring;
});
}

View File

@@ -0,0 +1,12 @@
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
export default function embedInlineCalculations(inlineCalcObj){
const string = inlineCalcObj.text;
const calculations = inlineCalcObj.inlineCalculations;
if (!string || !calculations) return;
let index = 0;
inlineCalcObj.value = string.replace(INLINE_CALCULATION_REGEX, substring => {
let calc = calculations[index++];
return (calc && 'value' in calc) ? calc.value : substring;
});
}

View File

@@ -0,0 +1,19 @@
import resolve, { toString } from '/imports/parser/resolve.js';
export default function evaluateCalculation(calculation, scope){
const parseNode = calculation.parseNode;
const fn = calculation._parseLevel;
const calculationScope = {...calculation._localScope, ...scope};
const {result: resultNode, context} = resolve(fn, parseNode, calculationScope);
calculation.errors = context.errors;
if (resultNode?.parseType === 'constant'){
calculation.value = resultNode.value;
} else if (resultNode?.parseType === 'error'){
calculation.value = null;
} else {
calculation.value = toString(resultNode);
}
// remove the working fields
delete calculation._parseLevel;
delete calculation._localScope;
}

View File

@@ -0,0 +1,5 @@
import Creatures from '/imports/api/creature/creatures/Creatures.js';
export default function writeScope(creatureId, scope){
Creatures.update(creatureId, {$set: {variables: scope}});
}

View File

@@ -1,11 +1,13 @@
import buildCreatureComputation from './computation/buildCreatureComputation.js';
import computeCreatureComputation from './computation/computeCreatureComputation.js';
import writeAlteredProperties from './computation/writeComputation/writeAlteredProperties.js';
import writeScope from './computation/writeComputation/writeScope.js';
export default function computeCreature(creatureId){
const computation = buildCreatureComputation(creatureId);
computeCreatureComputation(computation);
writeAlteredProperties(computation);
writeScope(creatureId, computation.scope);
}
// For now just recompute the whole creature, TODO only recompute a single

View File

@@ -1,13 +0,0 @@
import spendResources from '/imports/api/creature/actions/spendResources.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,4 +1,4 @@
import roll from '/imports/parser/roll.js';
import rollDice from '/imports/parser/rollDice.js';
export default function applyAttack({
prop,
@@ -6,7 +6,7 @@ export default function applyAttack({
actionContext,
creature,
}){
let value = roll(1, 20)[0];
let value = rollDice(1, 20)[0];
actionContext.attackRoll = {value};
let criticalHitTarget = creature.variables.criticalHitTarget &&
creature.variables.criticalHitTarget.value || 20;

View File

@@ -7,8 +7,8 @@ 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;
function applyProperty(args){
let prop = args.prop;
if (prop.type === 'buff'){
// ignore only applied buffs, don't apply them again
if (prop.applied === true){
@@ -26,28 +26,27 @@ function applyProperty(options){
switch (prop.type){
case 'action':
case 'spell':
applyAction(options);
break;
case 'attack':
applyAction(options);
applyAttack(options);
if (prop.attackRoll && prop.attackRoll.calculation){
applyAttack(args)
}
applyAction(args);
break;
case 'damage':
applyDamage(options);
applyDamage(args);
break;
case 'adjustment':
applyAdjustment(options);
applyAdjustment(args);
break;
case 'buff':
applyBuff(options);
applyBuff(args);
return false;
case 'toggle':
return applyToggle(options);
return applyToggle(args);
case 'roll':
applyRoll(options);
applyRoll(args);
break;
case 'savingThrow':
return applySave(options);
return applySave(args);
}
return true;
}

View File

@@ -204,12 +204,6 @@ const ComputedOnlyActionSchema = createPropertySchema({
optional: true,
removeBeforeCompute: true,
},
'resources.attributesConsumed.$.statId': {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
removeBeforeCompute: true,
},
'resources.attributesConsumed.$.statName': {
type: String,
optional: true,

View File

@@ -14,11 +14,10 @@ const AdjustmentSchema = createPropertySchema({
// Who this adjustment applies to
target: {
type: String,
defaultValue: 'every',
defaultValue: 'target',
allowedValues: [
'self', // the character who took the Adjustment
'each', // rolled once for `each` target
'every', // rolled once and applied to `every` target
'self',
'target',
],
},
// The stat this rolls applies to

View File

@@ -0,0 +1,49 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
let BranchSchema = createPropertySchema({
branchType: {
type: String,
allowedValues: [
// Uses the condition field to determine whether to apply children
'if',
// Attack
'hit',
'miss',
// Save
'failedSave',
'successfulSave',
// Iterate through targets
'eachTarget',
// if it has option children, asks to select one
// Otherwise presents its own text with yes/no
//'choice',
//'option',
],
},
text: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
condition: {
type: 'fieldToCompute',
optional: true,
parseLevel: 'compile',
},
});
let ComputedOnlyBranchSchema = createPropertySchema({
condition: {
type: 'computedOnlyField',
optional: true,
parseLevel: 'compile',
},
});
const ComputedBranchSchema = new SimpleSchema()
.extend(BranchSchema)
.extend(ComputedOnlyBranchSchema);
export { BranchSchema, ComputedBranchSchema, ComputedOnlyBranchSchema }

View File

@@ -25,9 +25,8 @@ let BuffSchema = createPropertySchema({
target: {
type: String,
allowedValues: [
'self', // the character who took the buff
'each', // rolled once for `each` target
'every', // rolled once and applied to `every` target
'self',
'target',
],
defaultValue: 'every',
},

View File

@@ -16,9 +16,8 @@ const DamageSchema = createPropertySchema({
type: String,
defaultValue: 'every',
allowedValues: [
'self', // the character who took the action
'each', // rolled once for `each` target
'every', // rolled once and applied to `every` target
'self',
'target',
],
},
damageType: {

View File

@@ -20,9 +20,8 @@ let SavingThrowSchema = createPropertySchema({
type: String,
defaultValue: 'every',
allowedValues: [
'self', // the character who took the action
'each', // rolled once for `each` target
'every', // rolled once and applied to `every` target
'self',
'target',
],
},
// The variable name of save to roll

View File

@@ -71,6 +71,7 @@ const transformsByPropType = {
'action': actionTransforms,
'adjustment': [
...getComputedPropertyTransforms('amount'),
{from: 'target', to: 'target', up: simplifyTarget},
],
'attack': [
...actionTransforms,
@@ -89,6 +90,7 @@ const transformsByPropType = {
...getComputedPropertyTransforms('duration'),
...getInlineComputationTransforms('description'),
{from: 'value', to: 'total', up: nanToNull},
{from: 'target', to: 'target', up: simplifyTarget},
],
'classLevel': [
...getInlineComputationTransforms('description'),
@@ -98,6 +100,7 @@ const transformsByPropType = {
],
'damage': [
...getComputedPropertyTransforms('amount'),
{from: 'target', to: 'target', up: simplifyTarget},
],
'effect': [
{from: 'calculation', to: 'amount.calculation'},
@@ -128,6 +131,7 @@ const transformsByPropType = {
],
'savingThrow': [
...getComputedPropertyTransforms('dc'),
{from: 'target', to: 'target', up: simplifyTarget},
],
'skill': [
...getComputedPropertyTransforms('baseValue'),
@@ -193,6 +197,14 @@ function stripZero(val){
}
}
function simplifyTarget(val){
if (val === 'self'){
return val;
} else {
return 'target';
}
}
function trimErrors(arr){
if(!arr) return arr;
arr.forEach(e => {

View File

@@ -1,4 +1,4 @@
import resolve, { toString, traverse } from '../resolve.js';
import resolve, { toString, traverse, map } from '../resolve.js';
import constant from './constant.js';
const array = {
@@ -41,6 +41,13 @@ const array = {
fn(node);
node.values.forEach(value => traverse(value, fn));
},
map(node, fn){
const resultingNode = fn(node);
if (resultingNode === node){
node.values = node.values.map(value => map(value, fn));
}
return resultingNode;
},
}
export default array;

View File

@@ -1,7 +1,7 @@
import error from './error.js';
import constant from './constant.js';
import functions from '/imports/parser/functions.js';
import resolve, { toString, traverse } from '../resolve.js';
import resolve, { toString, traverse, map } from '../resolve.js';
const call = {
create({functionName, args}) {
@@ -104,6 +104,13 @@ const call = {
fn(node);
node.args.forEach(arg => traverse(arg, fn));
},
map(node, fn){
const resultingNode = fn(node);
if (resultingNode === node){
node.args = node.args.map(arg => map(arg, fn));
}
return resultingNode;
},
checkArugments({node, fn, argumentsExpected, resolvedArgs, context}){
// Check that the number of arguments matches the number expected
if (

View File

@@ -1,4 +1,4 @@
import resolve, { traverse, toString } from '../resolve';
import resolve, { traverse, toString, map } from '../resolve';
const ifNode = {
create({condition, consequent, alternative}){
@@ -39,6 +39,15 @@ const ifNode = {
traverse(node.consequent, fn);
traverse(node.alternative, fn);
},
map(node, fn){
const resultingNode = fn(node);
if (resultingNode === node){
node.condition = map(node.condition, fn);
node.consequent = map(node.consequent, fn);
node.alternative = map(node.alternative, fn);
}
return resultingNode;
},
}
export default ifNode;

View File

@@ -1,4 +1,4 @@
import resolve, { traverse, toString } from '../resolve';
import resolve, { traverse, toString, map } from '../resolve';
import error from './error';
const indexNode = {
@@ -68,6 +68,14 @@ const indexNode = {
traverse(node.array, fn);
traverse(node.index, fn);
},
map(node, fn){
const resultingNode = fn(node);
if (resultingNode === node){
node.array = map(node.array, fn);
node.index = map(node.index, fn);
}
return resultingNode;
},
}
export default indexNode;

View File

@@ -1,4 +1,4 @@
import resolve, { toString, traverse } from '../resolve.js';
import resolve, { toString, traverse, map } from '../resolve.js';
import constant from './constant.js';
const not = {
@@ -31,7 +31,14 @@ const not = {
traverse(node, fn){
fn(node);
traverse(node.right, fn);
}
},
map(node, fn){
const resultingNode = fn(node);
if (resultingNode === node){
node.right = map(node.right, fn);
}
return resultingNode;
},
}
export default not;

View File

@@ -1,4 +1,4 @@
import resolve, { toString, traverse } from '../resolve.js';
import resolve, { toString, traverse, map } from '../resolve.js';
import constant from './constant.js';
const operator = {
@@ -71,6 +71,14 @@ const operator = {
traverse(node.left, fn);
traverse(node.right, fn);
},
map(node, fn){
const resultingNode = fn(node);
if (resultingNode === node){
node.left = map(node.left, fn);
node.right = map(node.right, fn);
}
return resultingNode;
},
}
export default operator;

View File

@@ -1,4 +1,4 @@
import resolve, { toString, traverse } from '../resolve.js';
import resolve, { toString, traverse, map } from '../resolve.js';
const parenthesis = {
create({content}) {
@@ -28,7 +28,14 @@ const parenthesis = {
traverse(node, fn){
fn(node);
traverse(node.content, fn);
}
},
map(node, fn){
const resultingNode = fn(node);
if (resultingNode === node){
node.content = map(node.content, fn);
}
return resultingNode;
},
}
export default parenthesis;

View File

@@ -1,7 +1,7 @@
import resolve, { toString, traverse } from '../resolve.js';
import resolve, { toString, traverse, map } from '../resolve.js';
import error from './error.js';
import rollArray from './rollArray.js';
import roll from '/imports/parser/roll.js';
import rollDice from '/imports/parser/rollDice.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const rollNode = {
@@ -39,7 +39,7 @@ const rollNode = {
return errorResult('Dice size is not an integer', node, context);
}
let number = left.value;
if (context.doubleRolls){
if (context.options.doubleRolls){
number *= 2;
}
if (number > STORAGE_LIMITS.diceRollValuesCount){
@@ -47,7 +47,7 @@ const rollNode = {
return errorResult(message, node, context);
}
let diceSize = right.value;
let values = roll(number, diceSize);
let values = rollDice(number, diceSize);
if (context){
context.storeRoll({number, diceSize, values});
}
@@ -69,6 +69,14 @@ const rollNode = {
traverse(node.left, fn);
traverse(node.right, fn);
},
map(node, fn){
const resultingNode = fn(node);
if (resultingNode === node){
node.left = map(node.left, fn);
node.right = map(node.right, fn);
}
return resultingNode;
},
}
function errorResult(message, node, context){

View File

@@ -27,9 +27,6 @@ const rollArray = {
context,
};
},
traverse(node, fn){
return fn(node);
}
}
export default rollArray;

View File

@@ -1,4 +1,4 @@
import resolve, { toString, traverse } from '../resolve.js';
import resolve, { toString, traverse, map } from '../resolve.js';
import constant from './constant.js';
const unaryOperator = {
@@ -41,6 +41,13 @@ const unaryOperator = {
fn(node);
traverse(node.right, fn);
},
map(node, fn){
const resultingNode = fn(node);
if (resultingNode === node){
node.right = map(node.right, fn);
}
return resultingNode;
},
};
export default unaryOperator;

View File

@@ -43,11 +43,24 @@ export function traverse(node, fn){
return fn(node);
}
export function map(node, fn){
if (!node) return;
let type = nodeTypeIndex[node.parseType];
if (!type){
console.error(node);
throw new Meteor.Error('Not valid parse node');
}
if (type.map){
return type.map(node, fn);
}
return fn(node);
}
export class Context {
constructor({errors = [], rolls = [], doubleRolls} = {}){
constructor({errors = [], rolls = [], options = {}} = {}){
this.errors = errors;
this.rolls = rolls;
this.doubleRolls = doubleRolls;
this.options = options;
}
error(e){
if (!e) return;

View File

@@ -1,4 +1,4 @@
export default function roll(number, diceSize){
export default function rollDice(number, diceSize){
let values = [];
let randomSrc = DDP.randomStream('diceRoller');
for (let i = 0; i < number; i++){

View File

@@ -28,6 +28,7 @@
</div>
<smart-select
label="Type"
clearable
:items="skillTypes"
:value="model.skillType"
:error-messages="errors.skillType"