Refactored actions and let actions apply buffs to self
This commit is contained in:
@@ -52,3 +52,4 @@ bozhao:link-accounts
|
||||
peerlibrary:reactive-publish
|
||||
simple:rest
|
||||
simple:rest-method-mixin
|
||||
mikowals:batch-insert
|
||||
|
||||
@@ -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
|
||||
|
||||
11
app/imports/api/campaign/Campaigns.js
Normal file
11
app/imports/api/campaign/Campaigns.js
Normal file
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
5
app/imports/api/creature/actions/applyAction.js
Normal file
5
app/imports/api/creature/actions/applyAction.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import spendResources from '/imports/api/creature/actions/spendResources.js'
|
||||
|
||||
export default function applyAction({prop}){
|
||||
spendResources(prop);
|
||||
}
|
||||
61
app/imports/api/creature/actions/applyBuff.js
Normal file
61
app/imports/api/creature/actions/applyBuff.js
Normal file
@@ -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);
|
||||
}
|
||||
19
app/imports/api/creature/actions/applyDamage.js
Normal file
19
app/imports/api/creature/actions/applyDamage.js
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
62
app/imports/api/creature/actions/applyProperties.js
Normal file
62
app/imports/api/creature/actions/applyProperties.js
Normal file
@@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
57
app/imports/api/creature/actions/spendResources.js
Normal file
57
app/imports/api/creature/actions/spendResources.js
Normal file
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
:error-messages="errors.description"
|
||||
@change="change('description', ...arguments)"
|
||||
/>
|
||||
<!-- Duration not implemented yet
|
||||
<text-field
|
||||
label="Duration"
|
||||
hint="How long the buff lasts"
|
||||
@@ -20,6 +21,16 @@
|
||||
:error-messages="errors.duration"
|
||||
@change="change('duration', ...arguments)"
|
||||
/>
|
||||
-->
|
||||
<smart-select
|
||||
label="Target"
|
||||
:hint="targetOptionHint"
|
||||
:items="targetOptions"
|
||||
:value="model.target"
|
||||
:error-messages="errors.target"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="change('target', ...arguments)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user