Backend work to support actions consuming their resources on use
This commit is contained in:
@@ -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})
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
89
app/imports/api/creature/actions/doAction.js
Normal file
89
app/imports/api/creature/actions/doAction.js
Normal file
@@ -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;
|
||||
@@ -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){
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -17,6 +17,10 @@ const ItemConsumedSchema = new SimpleSchema({
|
||||
type: Number,
|
||||
defaultValue: 1,
|
||||
},
|
||||
itemId: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default ItemConsumedSchema;
|
||||
|
||||
Reference in New Issue
Block a user