diff --git a/app/imports/api/creature/CreatureProperties.js b/app/imports/api/creature/CreatureProperties.js index 1104ab68..c6c6e446 100644 --- a/app/imports/api/creature/CreatureProperties.js +++ b/app/imports/api/creature/CreatureProperties.js @@ -20,6 +20,8 @@ import { import {setDocToLastOrder} from '/imports/api/parenting/order.js'; import { storedIconsSchema } from '/imports/api/icons/Icons.js'; +import '/imports/api/creature/actions/doAction.js'; + let CreatureProperties = new Mongo.Collection('creatureProperties'); let CreaturePropertySchema = new SimpleSchema({ @@ -56,7 +58,7 @@ for (let key in propertySchemasIndex){ }); } -function getCreature(property){ +export function getCreature(property){ if (!property) throw new Meteor.Error('No property provided'); let creature = Creatures.findOne(property.ancestors[0].id); if (!creature) throw new Meteor.Error('Creature does not exist'); @@ -231,6 +233,37 @@ const updateProperty = new ValidatedMethod({ }, }); +export function damagePropertyWork({property, operation, value}){ + if (operation === 'set'){ + let 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 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 + }); + } else if (operation === 'increment'){ + let currentValue = property.value - (property.damage || 0); + let currentDamage = property.damage; + let increment = value; + // Can't increase damage above the remaining value + if (increment > currentValue) increment = currentValue; + // Can't decrease damage below zero + if (-increment > currentDamage) increment = -currentDamage; + CreatureProperties.update(property._id, { + $inc: {damage: increment} + }, { + selector: property + }); + } +} + const damageProperty = new ValidatedMethod({ name: 'creatureProperties.damage', validate: new SimpleSchema({ @@ -258,38 +291,39 @@ const damageProperty = new ValidatedMethod({ `Property of type "${currentProperty.type}" can't be damaged` ); } - if (operation === 'set'){ - let currentValue = currentProperty.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 can't exceed total value - if (damage > currentValue) damage = currentValue; - // Damage must be positive - if (damage < 0) damage = 0; - CreatureProperties.update(_id, { - $set: {damage} - }, { - selector: currentProperty - }); - } else if (operation === 'increment'){ - let currentValue = currentProperty.value - (currentProperty.damage || 0); - let currentDamage = currentProperty.damage; - let increment = value; - // Can't increase damage above the remaining value - if (increment > currentValue) increment = currentValue; - // Can't decrease damage below zero - if (-increment > currentDamage) increment = -currentDamage; - CreatureProperties.update(_id, { - $inc: {damage: increment} - }, { - selector: currentProperty - }); - } + damagePropertyWork({property: currentProperty, operation, value}) recomputeCreatures(currentProperty); }, }); +export function adjustQuantityWork({property, operation, value}){ + // Check if property has quantity + let schema = CreatureProperties.simpleSchema(property); + if (!schema.allowsKey('quantity')){ + throw new Meteor.Error( + 'Adjust quantity failed', + `Property of type "${property.type}" doesn't have a quantity` + ); + } + if (operation === 'set'){ + CreatureProperties.update(property._id, { + $set: {quantity: value} + }, { + selector: property + }); + } else if (operation === 'increment'){ + // value here is 'damage' + value = -value; + let currentQuantity = property.quantity; + if (currentQuantity + value < 0) value = -currentQuantity; + CreatureProperties.update(property._id, { + $inc: {quantity: value} + }, { + selector: property + }); + } +} + const adjustQuantity = new ValidatedMethod({ name: 'creatureProperties.adjustQuantity', validate: new SimpleSchema({ @@ -309,31 +343,7 @@ const adjustQuantity = new ValidatedMethod({ let currentProperty = CreatureProperties.findOne(_id); // Check permissions assertPropertyEditPermission(currentProperty, this.userId); - // Check if property can take damage - let schema = CreatureProperties.simpleSchema(currentProperty); - if (!schema.allowsKey('quantity')){ - throw new Meteor.Error( - 'Adjust quantity failed', - `Property of type "${currentProperty.type}" doesn't have a quantity` - ); - } - if (operation === 'set'){ - CreatureProperties.update(_id, { - $set: {quantity: value} - }, { - selector: currentProperty - }); - } else if (operation === 'increment'){ - // value here is 'damage' - value = -value; - let currentQuantity = currentProperty.quantity; - if (currentQuantity + value < 0) value = -currentQuantity; - CreatureProperties.update(_id, { - $inc: {quantity: value} - }, { - selector: currentProperty - }); - } + adjustQuantityWork({property: currentProperty, operation, value}) }, }); diff --git a/app/imports/api/creature/actions/doAction.js b/app/imports/api/creature/actions/doAction.js new file mode 100644 index 00000000..85f27da3 --- /dev/null +++ b/app/imports/api/creature/actions/doAction.js @@ -0,0 +1,89 @@ +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 { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; +import { recomputeCreatureByDoc } from '/imports/api/creature/computation/recomputeCreature.js'; + +const doAction = new ValidatedMethod({ + name: 'creatureProperties.doAction', + validate: new SimpleSchema({ + actionId: SimpleSchema.RegEx.Id, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 10, + timeInterval: 5000, + }, + run({actionId}) { + let action = CreatureProperties.findOne(actionId); + // Check permissions + let creature = getCreature(action); + assertEditPermission(creature, this.userId); + doActionWork(action); + // Note this only recomputes the top-level creature, not the nearest one + recomputeCreatureByDoc(creature); + }, +}); + +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, + }); + }); + // 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, + }); + }); +} + +export default doAction; diff --git a/app/imports/api/creature/computation/computeEndStepProperty.js b/app/imports/api/creature/computation/computeEndStepProperty.js index 81a2096f..3241815c 100644 --- a/app/imports/api/creature/computation/computeEndStepProperty.js +++ b/app/imports/api/creature/computation/computeEndStepProperty.js @@ -20,6 +20,7 @@ export default function computeEndStepProperty(prop, memo){ } function computeAction(prop, memo){ + // Uses let {value, errors} = evaluateCalculation(prop.uses, memo); prop.usesResult = value; if (errors.length){ @@ -27,10 +28,28 @@ function computeAction(prop, memo){ } else { delete prop.usesErrors; } - // TODO compute resources.$.$.available and insufficientResources + prop.insufficientResources = undefined; + if (prop.usesUsed >= prop.usesResult){ + prop.insufficientResources = true; + } + // Attributes consumed + prop.resources.attributesConsumed.forEach((attConsumed, i) => { + if (attConsumed.variableName){ + let stat = memo.statsByVariableName[attConsumed.variableName]; + prop.resources.attributesConsumed[i].statId = stat && stat._id; + let available = stat && stat.currentValue || 0; + prop.resources.attributesConsumed[i].available = available; + if (available < attConsumed.quantity){ + prop.insufficientResources = true; + } + } + }); + // Items consumed + // TODO } function computeAttack(prop, memo){ + // Roll bonus let {value, errors} = evaluateCalculation(prop.rollBonus, memo); prop.rollBonusResult = value; if (errors.length){ diff --git a/app/imports/api/creature/computation/recomputeCreature.js b/app/imports/api/creature/computation/recomputeCreature.js index e0fb59af..b2bb007e 100644 --- a/app/imports/api/creature/computation/recomputeCreature.js +++ b/app/imports/api/creature/computation/recomputeCreature.js @@ -91,7 +91,7 @@ export function recomputeCreatureById(creatureId){ * - Mark the stat as computed * - Write the computed results back to the database */ -function recomputeCreatureByDoc(creature){ +export function recomputeCreatureByDoc(creature){ const creatureId = creature._id; let props = getActiveProperties({ ancestorId: creatureId, diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index 5981a2d8..7938f22d 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -1,5 +1,4 @@ import SimpleSchema from 'simpl-schema'; -import ResourcesSchema from '/imports/api/properties/subSchemas/ResourcesSchema.js' import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; /* @@ -41,13 +40,60 @@ let ActionSchema = new SimpleSchema({ 'tags.$': { type: String, }, + // Duplicate the ResourceSchema here so we can extend it elegantly. resources: { - type: ResourcesSchema, + type: Object, defaultValue: {}, + }, + 'resources.itemsConsumed': { + type: Array, + defaultValue: [], + }, + 'resources.itemsConsumed.$': { + type: Object, + }, + 'resources.itemsConsumed.$._id': { + type: String, + regEx: SimpleSchema.RegEx.Id, + autoValue(){ + if (!this.isSet) return Random.id(); + } + }, + 'resources.itemsConsumed.$.tag': { + type: String, + optional: true, + }, + 'resources.itemsConsumed.$.quantity': { + type: Number, + defaultValue: 1, + }, + 'resources.itemsConsumed.$.itemId': { + type: String, + optional: true, + }, + 'resources.attributesConsumed': { + type: Array, + defaultValue: [], + }, + 'resources.attributesConsumed.$': { + type: Object, + }, + 'resources.attributesConsumed.$._id': { + type: String, + regEx: SimpleSchema.RegEx.Id, + autoValue(){ + if (!this.isSet) return Random.id(); + } + }, + 'resources.attributesConsumed.$.variableName': { + type: String, + optional: true, + }, + 'resources.attributesConsumed.$.quantity': { + type: Number, + defaultValue: 1, }, // Calculation of how many times this action can be used - // Only set if this action tracks its own uses, rather than adjusting - // resources uses: { type: String, optional: true, @@ -84,12 +130,23 @@ const ComputedOnlyActionSchema = new SimpleSchema({ type: Number, optional: true, }, + // This appears both in the computed and uncomputed schema because it can be + // set by both a computation or a form + 'resources.itemsConsumed.$.itemId': { + type: String, + optional: true, + }, 'resources.attributesConsumed': Array, 'resources.attributesConsumed.$': Object, 'resources.attributesConsumed.$.available': { type: Number, optional: true, }, + 'resources.attributesConsumed.$.statId': { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + }, insufficientResources: { type: Boolean, optional: true, diff --git a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js index ecfe56b1..8545b6d1 100644 --- a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js +++ b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js @@ -17,6 +17,10 @@ const ItemConsumedSchema = new SimpleSchema({ type: Number, defaultValue: 1, }, + itemId: { + type: String, + optional: true, + }, }); export default ItemConsumedSchema; diff --git a/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue b/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue index 132e77f1..15c4751e 100644 --- a/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue +++ b/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue @@ -247,24 +247,15 @@
- - - Actions - - - +
{ if (error) console.warn(error); ack && ack(error && error.reason || error); diff --git a/app/imports/ui/properties/components/actions/ActionCard.vue b/app/imports/ui/properties/components/actions/ActionCard.vue new file mode 100644 index 00000000..60476b6e --- /dev/null +++ b/app/imports/ui/properties/components/actions/ActionCard.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/app/imports/ui/properties/components/actions/ActionListTile.vue b/app/imports/ui/properties/components/actions/ActionListTile.vue index a7ffe8f5..9e978bf4 100644 --- a/app/imports/ui/properties/components/actions/ActionListTile.vue +++ b/app/imports/ui/properties/components/actions/ActionListTile.vue @@ -3,12 +3,19 @@ class="ability-list-tile" v-on="hasClickListener ? {click} : {}" > - - {{ rollBonus }} - + + {{ rollBonus }} + + {{ model.name }} @@ -20,7 +27,7 @@ - {{ model.usesResult - (model.usesUsed) }}/{{ model.usesResult }} + {{ usesLeft }}/{{ totalUses }} @@ -29,6 +36,7 @@ diff --git a/app/imports/ui/properties/forms/ResourcesForm.vue b/app/imports/ui/properties/forms/ResourcesForm.vue index 60337f0e..17811b83 100644 --- a/app/imports/ui/properties/forms/ResourcesForm.vue +++ b/app/imports/ui/properties/forms/ResourcesForm.vue @@ -83,6 +83,7 @@ this.addResourceLoading = false; }, addAttributesConsumed(){ + console.log(AttributeConsumedSchema.clean({})); this.addResourceLoading = true; this.$emit('push', { path: ['attributesConsumed'],