diff --git a/app/.meteor/packages b/app/.meteor/packages index c2382499..07c9d939 100644 --- a/app/.meteor/packages +++ b/app/.meteor/packages @@ -52,3 +52,4 @@ bozhao:link-accounts peerlibrary:reactive-publish simple:rest simple:rest-method-mixin +mikowals:batch-insert diff --git a/app/.meteor/versions b/app/.meteor/versions index c21c1581..d8f3f236 100644 --- a/app/.meteor/versions +++ b/app/.meteor/versions @@ -72,6 +72,7 @@ meteorhacks:subs-manager@1.6.4 meteortesting:browser-tests@1.3.3 meteortesting:mocha@1.1.5 meteortesting:mocha-core@7.0.1 +mikowals:batch-insert@1.1.9 minifier-css@1.5.0 minifier-js@2.6.0 minimongo@1.6.0 diff --git a/app/imports/api/campaign/Campaigns.js b/app/imports/api/campaign/Campaigns.js new file mode 100644 index 00000000..2e9b8e49 --- /dev/null +++ b/app/imports/api/campaign/Campaigns.js @@ -0,0 +1,11 @@ +import SimpleSchema from 'simpl-schema'; + +let Campaigns = new Mongo.Collection('campaigns'); + +let CampaignSchema = new SimpleSchema({ + +}); + +Campaigns.attachSchema(CampaignSchema); + +export default Campaigns; diff --git a/app/imports/api/campaign/Encounter.js b/app/imports/api/campaign/Encounter.js index 0b8aed79..679f86ec 100644 --- a/app/imports/api/campaign/Encounter.js +++ b/app/imports/api/campaign/Encounter.js @@ -1,9 +1,51 @@ import SimpleSchema from 'simpl-schema'; -let Encounters = new Mongo.Collection("encounters"); +let Encounters = new Mongo.Collection('encounters'); +const CreatureInitiativeSchema = new SimpleSchema({ + name: { + type: String, + optional: true, + }, + initiativeRoll: { + type: SimpleSchema.Integer, + }, +}); + +const InitiativeSchema = new SimpleSchema({ + // An ordered list of all creatures in the initiative order + creatures: { + type: Array, + defaultValue: [], + }, + 'creatures.$': { + type: CreatureInitiativeSchema, + }, + active: { + type: Boolean, + defaultValue: false, + }, + roundNumber: { + type: SimpleSchema.Integer, + defaultValue: 0, + }, + initiativeNumber: { + type: SimpleSchema.Integer, + optional: true, + }, +}); + +// A creature can be in one ecounter at a time. +// All creatures in an encounter have a shared time and space. let EncounterSchema = new SimpleSchema({ - //an encounter is a single flow of time all parties in an encounter are in-sync time wise + name: { + type: String, + optional: true, + }, + initiative: { + type: InitiativeSchema, + defaultValue: {}, + }, }); Encounters.attachSchema(EncounterSchema); diff --git a/app/imports/api/campaign/Parties.js b/app/imports/api/campaign/Parties.js index 41ce048c..e0f90c60 100644 --- a/app/imports/api/campaign/Parties.js +++ b/app/imports/api/campaign/Parties.js @@ -1,22 +1,21 @@ import SimpleSchema from 'simpl-schema'; -let Parties = new Mongo.Collection("parties"); +let Parties = new Mongo.Collection('parties'); let partySchema = new SimpleSchema({ name: { type: String, - defaultValue: "New Party", + defaultValue: 'New Party', trim: false, optional: true, }, - characters: { + creatures: { type: Array, defaultValue: [], }, - characters: { + 'creatures.$': { type: String, regEx: SimpleSchema.RegEx.Id, - index: 1, }, owner: { type: String, @@ -26,24 +25,4 @@ let partySchema = new SimpleSchema({ Parties.attachSchema(partySchema); -Parties.allow({ - insert: function(userId, doc) { - return userId && doc.owner === userId; - }, - update: function(userId, doc, fields, modifier) { - return userId && doc.owner === userId; - }, - remove: function(userId, doc) { - return userId && doc.owner === userId; - }, - fetch: ["owner"], -}); - -Parties.deny({ - update: function(userId, docs, fields, modifier) { - // can't change owners - return _.contains(fields, "owner"); - } -}); - export default Parties; diff --git a/app/imports/api/creature/actions/applyAction.js b/app/imports/api/creature/actions/applyAction.js new file mode 100644 index 00000000..356d85b7 --- /dev/null +++ b/app/imports/api/creature/actions/applyAction.js @@ -0,0 +1,5 @@ +import spendResources from '/imports/api/creature/actions/spendResources.js' + +export default function applyAction({prop}){ + spendResources(prop); +} diff --git a/app/imports/api/creature/actions/applyBuff.js b/app/imports/api/creature/actions/applyBuff.js new file mode 100644 index 00000000..6b55f617 --- /dev/null +++ b/app/imports/api/creature/actions/applyBuff.js @@ -0,0 +1,61 @@ +import { + setLineageOfDocs, + renewDocIds +} from '/imports/api/parenting/parenting.js'; +import {setDocToLastOrder} from '/imports/api/parenting/order.js'; +import CreatureProperties from '/imports/api/creature/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); +} diff --git a/app/imports/api/creature/actions/applyDamage.js b/app/imports/api/creature/actions/applyDamage.js new file mode 100644 index 00000000..b2634071 --- /dev/null +++ b/app/imports/api/creature/actions/applyDamage.js @@ -0,0 +1,19 @@ +import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js'; + +export default function applyDamage({ + prop, + creature, + targets, + actionContext +}){ + let damageTargets = prop.target === 'self' ? [creature] : targets; + let scope = { + ...creature.variables, + ...actionContext, + }; + let {result, errors} = evaluateString(prop.amount, scope); + if (Meteor.isClient) errors.forEach(e => console.error(e)); + if (Number.isFinite(result)) { + damageTargets.forEach() + } +} diff --git a/app/imports/api/creature/actions/applyProperties.js b/app/imports/api/creature/actions/applyProperties.js new file mode 100644 index 00000000..3831b5a8 --- /dev/null +++ b/app/imports/api/creature/actions/applyProperties.js @@ -0,0 +1,62 @@ +import applyAction from '/imports/api/creature/actions/applyAction.js'; +//import applyDamage from '/imports/api/creature/actions/applyDamage.js'; +import applyBuff from '/imports/api/creature/actions/applyBuff.js'; + +function applyProperty(options){ + let prop = options.prop; + if ( + prop.disabled === true || // ignore disabled props + prop.equipped === false || // ignore unequipped items + prop.toggleResult === false || // ignore untoggled toggles + prop.applied === true // ignore buffs that are already applied + ){ + return false; + } + switch (prop.type){ + case 'action': + case 'spell': + case 'attack': + applyAction(options); + return true; + case 'damage': + // applyDamage(options); + return true; + case 'adjustment': + // applyAdjustment(options); + return true; + case 'buff': + applyBuff(options); + return false; + case 'roll': + // applyRoll(options); + return true; + case 'savingThrow': + // applySavingThrow(options); + return false; + } +} + +export default function applyProperties({ + forest, + creature, + targets, + actionContext +}){ + forest.forEach(child => { + let walkChildren = applyProperty({ + prop: child.node, + children: child.children, + creature, + targets, + actionContext + }); + if (walkChildren){ + applyProperties({ + forest: child.children, + creature, + targets, + actionContext + }); + } + }); +} diff --git a/app/imports/api/creature/actions/doAction.js b/app/imports/api/creature/actions/doAction.js index 85f27da3..c1bead02 100644 --- a/app/imports/api/creature/actions/doAction.js +++ b/app/imports/api/creature/actions/doAction.js @@ -1,88 +1,61 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import CreatureProperties, { getCreature, damagePropertyWork, adjustQuantityWork } from '/imports/api/creature/CreatureProperties.js'; +import CreatureProperties, { getCreature } from '/imports/api/creature/CreatureProperties.js'; import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; import { recomputeCreatureByDoc } from '/imports/api/creature/computation/recomputeCreature.js'; +import { nodesToTree } from '/imports/api/parenting/parenting.js'; +import applyProperties from '/imports/api/creature/actions/applyProperties.js'; const doAction = new ValidatedMethod({ name: 'creatureProperties.doAction', validate: new SimpleSchema({ actionId: SimpleSchema.RegEx.Id, + targetId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + }, }).validator(), mixins: [RateLimiterMixin], rateLimit: { numRequests: 10, timeInterval: 5000, }, - run({actionId}) { + run({actionId, targetId}) { let action = CreatureProperties.findOne(actionId); // Check permissions let creature = getCreature(action); assertEditPermission(creature, this.userId); - doActionWork(action); + let target = undefined; + if (targetId) { + target = getCreature(targetId); + assertEditPermission(target, this.userId); + } + doActionWork({action, creature, target}); // Note this only recomputes the top-level creature, not the nearest one recomputeCreatureByDoc(creature); + if (target){ + recomputeCreatureByDoc(target); + } }, }); -function doActionWork(action){ - spendResources(action); -} - -function spendResources(action){ - // Check Uses - if (action.usesUsed >= action.usesResult){ - throw new Meteor.Error('Insufficient Uses', - 'This action has no uses left'); - } - // Resources - if (action.insufficientResources){ - throw new Meteor.Error('Insufficient Resources', - 'This creature doesn\'t have sufficient resources to perform this action'); - } - // Items - let itemQuantityAdjustments = []; - action.resources.itemsConsumed.forEach(itemConsumed => { - if (!itemConsumed.itemId){ - throw new Meteor.Error('Ammo not selected', - 'No ammo was selected for this action'); - } - let item = CreatureProperties.findOne(itemConsumed.itemId); - if (!item || item.ancestors[0].id !== action.ancestors[0].id){ - throw new Meteor.Error('Ammo not found', - 'The action\'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, - }); +function doActionWork({action, creature, target}){ + let actionContext = {}; + let decendantForest = nodesToTree({ + collection: CreatureProperties, + ancestorId: action._id, }); - // 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 - CreatureProperties.update(action._id, { - $inc: {usesUsed: 1} - }, { - selector: action - }); - // Damage stats - action.resources.attributesConsumed.forEach(attConsumed => { - if (!attConsumed.quantity) return; - let stat = CreatureProperties.findOne(attConsumed.statId); - damagePropertyWork({ - property: stat, - operation: 'increment', - value: attConsumed.quantity, - }); + let startingForest = [{ + node: action, + children: decendantForest, + }]; + applyProperties({ + forest: startingForest, + creature, + target, + actionContext }); } diff --git a/app/imports/api/creature/actions/spendResources.js b/app/imports/api/creature/actions/spendResources.js new file mode 100644 index 00000000..b4409866 --- /dev/null +++ b/app/imports/api/creature/actions/spendResources.js @@ -0,0 +1,57 @@ +import CreatureProperties, { damagePropertyWork, adjustQuantityWork } from '/imports/api/creature/CreatureProperties.js'; + +export default function spendResources(action){ + // Check Uses + if (action.usesUsed >= action.usesResult){ + throw new Meteor.Error('Insufficient Uses', + 'This action has no uses left'); + } + // Resources + if (action.insufficientResources){ + throw new Meteor.Error('Insufficient Resources', + 'This creature doesn\'t have sufficient resources to perform this action'); + } + // Items + let itemQuantityAdjustments = []; + action.resources.itemsConsumed.forEach(itemConsumed => { + if (!itemConsumed.itemId){ + throw new Meteor.Error('Ammo not selected', + 'No ammo was selected for this action'); + } + let item = CreatureProperties.findOne(itemConsumed.itemId); + if (!item || item.ancestors[0].id !== action.ancestors[0].id){ + throw new Meteor.Error('Ammo not found', + 'The action\'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, + }); + }); + // 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 + CreatureProperties.update(action._id, { + $inc: {usesUsed: 1} + }, { + selector: action + }); + // Damage stats + action.resources.attributesConsumed.forEach(attConsumed => { + if (!attConsumed.quantity) return; + let stat = CreatureProperties.findOne(attConsumed.statId); + damagePropertyWork({ + property: stat, + operation: 'increment', + value: attConsumed.quantity, + }); + }); +} diff --git a/app/imports/api/parenting/parenting.js b/app/imports/api/parenting/parenting.js index 58bd8130..6c902d84 100644 --- a/app/imports/api/parenting/parenting.js +++ b/app/imports/api/parenting/parenting.js @@ -130,7 +130,7 @@ export function renewDocIds({docArray, collectionMap}){ const remapReference = ref => { if (idMap[ref.id]){ ref.id = idMap[ref.id]; - ref.collection = collectionMap[ref.collection] || ref.collection; + ref.collection = collectionMap && collectionMap[ref.collection] || ref.collection; } } docArray.forEach(doc => { @@ -204,17 +204,11 @@ export function getName(doc){ } } -export function nodesToTree({collection, ancestorId, filter, options}){ +export function nodeArrayToTree(nodes){ // Store a dict of all the nodes let nodeIndex = {}; let nodeList = []; - if (!options) options = {}; - options.sort = {order: 1}; - collection.find({ - 'ancestors.id': ancestorId, - removed: {$ne: true}, - ...filter, - }, options).forEach( node => { + nodes.forEach( node => { let treeNode = { node: node, children: [], @@ -238,3 +232,14 @@ export function nodesToTree({collection, ancestorId, filter, options}){ }); return forest; } + +export function nodesToTree({collection, ancestorId, filter, options}){ + if (!options) options = {}; + options.sort = {order: 1}; + let nodes = collection.find({ + 'ancestors.id': ancestorId, + removed: {$ne: true}, + ...filter, + }, options); + return nodeArrayToTree(nodes); +} diff --git a/app/imports/ui/properties/forms/BuffForm.vue b/app/imports/ui/properties/forms/BuffForm.vue index 5620d058..513bc15a 100644 --- a/app/imports/ui/properties/forms/BuffForm.vue +++ b/app/imports/ui/properties/forms/BuffForm.vue @@ -13,6 +13,7 @@ :error-messages="errors.description" @change="change('description', ...arguments)" /> + +