lowercased all model directories
This commit is contained in:
42
app/imports/model/creature/Actions.js
Normal file
42
app/imports/model/creature/Actions.js
Normal file
@@ -0,0 +1,42 @@
|
||||
let Actions = new Mongo.Collection("actions");
|
||||
|
||||
/*
|
||||
* Actions are given to a character by items and features
|
||||
*/
|
||||
let actionSchema = new SimpleSchema({
|
||||
charId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
allowedValues: ["action, bonus, reaction, free"],
|
||||
defaultValue: "action",
|
||||
},
|
||||
//the immediate impact of doing this action (eg. -1 rages)
|
||||
adjustments: {
|
||||
type: [Schemas.Adjustment],
|
||||
defaultValue: [],
|
||||
},
|
||||
});
|
||||
|
||||
Actions.attachSchema(actionSchema);
|
||||
|
||||
Actions.attachBehaviour("softRemovable");
|
||||
makeChild(Actions);
|
||||
|
||||
Actions.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Actions.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
|
||||
export default Actions
|
||||
89
app/imports/model/creature/Attacks.js
Normal file
89
app/imports/model/creature/Attacks.js
Normal file
@@ -0,0 +1,89 @@
|
||||
let Attacks = new Mongo.Collection("attacks");
|
||||
|
||||
/*
|
||||
* Attacks are given to a character by items and features
|
||||
*/
|
||||
attackSchema = new SimpleSchema({
|
||||
charId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
defaultValue: "New Attack",
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
details: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
attackBonus: {
|
||||
type: String,
|
||||
defaultValue: "strengthMod + proficiencyBonus",
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
damage: {
|
||||
type: String,
|
||||
defaultValue: "1d8 + {strengthMod}",
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
damageType: {
|
||||
type: String,
|
||||
allowedValues: [
|
||||
"bludgeoning",
|
||||
"piercing",
|
||||
"slashing",
|
||||
"acid",
|
||||
"cold",
|
||||
"fire",
|
||||
"force",
|
||||
"lightning",
|
||||
"necrotic",
|
||||
"poison",
|
||||
"psychic",
|
||||
"radiant",
|
||||
"thunder",
|
||||
],
|
||||
defaultValue: "slashing",
|
||||
},
|
||||
//the id of the feature, buff or item that created this effect
|
||||
parent: {
|
||||
type: Schemas.Parent
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
allowedValues: _.pluck(colorOptions, "key"),
|
||||
defaultValue: "q",
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
},
|
||||
});
|
||||
|
||||
Attacks.attachSchema(attackSchema);
|
||||
|
||||
Attacks.attachBehaviour("softRemovable");
|
||||
makeChild(Attacks, ["name", "enabled"]); //children of lots of things
|
||||
|
||||
Attacks.after.insert(function (userId, attack) {
|
||||
//Check to see if this attack's parent is a spell, if so, mirror prepared state to enabled
|
||||
if (attack.parent.collection === "Spells") {
|
||||
var parentSpell = Spells.findOne(attack.parent.id);
|
||||
if (parentSpell.prepared === "unprepared") {
|
||||
Attacks.update(attack._id, {$set: {enabled: false}});
|
||||
} else if (parentSpell.prepared === "prepared" || "always") {
|
||||
Attacks.update(attack._id, {$set: {enabled: true}});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Attacks.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Attacks.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
|
||||
export default Attacks;
|
||||
87
app/imports/model/creature/Attributes.js
Normal file
87
app/imports/model/creature/Attributes.js
Normal file
@@ -0,0 +1,87 @@
|
||||
Attributes = new Mongo.Collection("attributes");
|
||||
|
||||
/*
|
||||
* Attributes are whole numbered stats of a character
|
||||
*/
|
||||
Schemas.Attribute = new SimpleSchema({
|
||||
charId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
// The nice-to-read name
|
||||
name: {
|
||||
type: String,
|
||||
index: 1,
|
||||
},
|
||||
// The technical, lowercase, single-word name used in formulae
|
||||
variableName: {
|
||||
type: String,
|
||||
},
|
||||
// Attributes need to store their order to keep the sheet consistent
|
||||
order: {
|
||||
type: Number,
|
||||
index: 1,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
allowedValues: [
|
||||
"ability", //Strength, Dex, Con, etc.
|
||||
"stat", // Speed, Armor Class
|
||||
"hitDice",
|
||||
"healthBar", // Hitpoints, Temporary Hitpoints
|
||||
"resource", // Rages, sorcery points
|
||||
"spellSlot", // Level 1, 2, 3... spell slots
|
||||
"utility", // Aren't displayed, Jump height, Carry capacity
|
||||
],
|
||||
index: 1,
|
||||
},
|
||||
baseValue: {
|
||||
type: Number,
|
||||
decimal: true,
|
||||
optional: true,
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
decimal: true,
|
||||
defaultValue: 0,
|
||||
},
|
||||
mod: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
adjustment: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
// Can the value be decimal?
|
||||
decimal: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
parent: {
|
||||
type: Schemas.Parent
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
},
|
||||
reset: {
|
||||
type: String,
|
||||
optional: true,
|
||||
allowedValues: ["shortRest", "longRest"],
|
||||
},
|
||||
// Some things are only reset by half on rest
|
||||
resetMultiplier: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
Attributes.attachSchema(Schemas.Attribute);
|
||||
|
||||
Attributes.attachBehaviour("softRemovable");
|
||||
makeChild(Attributes, ["enabled"]); //children of lots of things
|
||||
|
||||
Attributes.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Attributes.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
67
app/imports/model/creature/Buffs.js
Normal file
67
app/imports/model/creature/Buffs.js
Normal file
@@ -0,0 +1,67 @@
|
||||
Buffs = new Mongo.Collection("buffs");
|
||||
|
||||
Schemas.Buff = new SimpleSchema({
|
||||
charId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
allowedValues: [
|
||||
"inate", //this should be "innate", but changing it could be problematic
|
||||
"custom",
|
||||
],
|
||||
},
|
||||
"lifeTime.total": {
|
||||
type: Number,
|
||||
defaultValue: 0, //0 is infinite
|
||||
min: 0,
|
||||
},
|
||||
"lifeTime.spent": {
|
||||
type: Number,
|
||||
defaultValue: 0,
|
||||
min: 0,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
allowedValues: _.pluck(colorOptions, "key"),
|
||||
defaultValue: "q",
|
||||
},
|
||||
appliedBy: { //the charId of whoever applied the buff
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
appliedByDetails: {//the name and collection of the thing that applied the buff
|
||||
type: Object,
|
||||
optional: true,
|
||||
},
|
||||
"appliedByDetails.name": {
|
||||
type: String,
|
||||
},
|
||||
"appliedByDetails.collection": {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
Buffs.attachSchema(Schemas.Buff);
|
||||
|
||||
Buffs.attachBehaviour("softRemovable");
|
||||
makeParent(Buffs, ["name", "enabled"]); //parents of effects, attacks, proficiencies
|
||||
|
||||
Buffs.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Buffs.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
32
app/imports/model/creature/Classes.js
Normal file
32
app/imports/model/creature/Classes.js
Normal file
@@ -0,0 +1,32 @@
|
||||
Classes = new Mongo.Collection("classes");
|
||||
|
||||
Schemas.Class = new SimpleSchema({
|
||||
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
name: {type: String, optional: true, trim: false},
|
||||
level: {type: Number},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
autoValue: function() {
|
||||
if (this.isInsert) {
|
||||
return new Date();
|
||||
} else if (this.isUpsert) {
|
||||
return {$setOnInsert: new Date()};
|
||||
} else {
|
||||
this.unset();
|
||||
}
|
||||
},
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
allowedValues: _.pluck(colorOptions, "key"),
|
||||
defaultValue: "q",
|
||||
},
|
||||
});
|
||||
|
||||
Classes.attachSchema(Schemas.Class);
|
||||
|
||||
Classes.attachBehaviour("softRemovable");
|
||||
makeParent(Classes, "name"); //parents of effects and attacks
|
||||
|
||||
Classes.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Classes.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
42
app/imports/model/creature/Conditions.js
Normal file
42
app/imports/model/creature/Conditions.js
Normal file
@@ -0,0 +1,42 @@
|
||||
Conditions = new Mongo.Collection("conditions");
|
||||
|
||||
Schemas.Conditions = new SimpleSchema({
|
||||
charId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
"lifeTime.total": {
|
||||
type: Number,
|
||||
defaultValue: 0, //0 is infinite
|
||||
min: 0,
|
||||
},
|
||||
"lifeTime.spent": {
|
||||
type: Number,
|
||||
defaultValue: 0,
|
||||
min: 0,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
allowedValues: _.pluck(colorOptions, "key"),
|
||||
defaultValue: "q",
|
||||
},
|
||||
});
|
||||
|
||||
Conditions.attachSchema(Schemas.Conditions);
|
||||
|
||||
Conditions.attachBehaviour("softRemovable");
|
||||
makeParent(Conditions, ["name"]); //parents of effects, attacks, proficiencies
|
||||
|
||||
Conditions.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Conditions.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
168
app/imports/model/creature/Creatures.js
Normal file
168
app/imports/model/creature/Creatures.js
Normal file
@@ -0,0 +1,168 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
|
||||
//set up the collection for creatures
|
||||
Creatures = new Mongo.Collection("creatures");
|
||||
|
||||
Schemas.Creature = new SimpleSchema({
|
||||
//strings
|
||||
name: {type: String, defaultValue: "", trim: false, optional: true},
|
||||
urlName: {type: String, defaultValue: "-", trim: false, optional: true},
|
||||
alignment: {type: String, defaultValue: "", trim: false, optional: true},
|
||||
gender: {type: String, defaultValue: "", trim: false, optional: true},
|
||||
race: {type: String, defaultValue: "", trim: false, optional: true},
|
||||
picture: {type: String, defaultValue: "", trim: true, optional: true},
|
||||
description: {type: String, defaultValue: "", trim: false, optional: true},
|
||||
personality: {type: String, defaultValue: "", trim: false, optional: true},
|
||||
ideals: {type: String, defaultValue: "", trim: false, optional: true},
|
||||
bonds: {type: String, defaultValue: "", trim: false, optional: true},
|
||||
flaws: {type: String, defaultValue: "", trim: false, optional: true},
|
||||
backstory: {type: String, defaultValue: "", trim: false, optional: true},
|
||||
|
||||
//mechanics
|
||||
deathSave: {type: Schemas.DeathSave},
|
||||
xp: {type: Number, defaultValue: 0},
|
||||
weightCarried: {type: Number, defaultValue: 0},
|
||||
level: {type: Number, defaultValue: 0},
|
||||
type: {type: String, defaultValue: "pc", allowedValues: ["pc", "npc", "monster"]},
|
||||
|
||||
//permissions
|
||||
party: {type: String, regEx: SimpleSchema.RegEx.Id, optional: true},
|
||||
owner: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
readers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: [], index: 1},
|
||||
writers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: [], index: 1},
|
||||
color: {
|
||||
type: String,
|
||||
allowedValues: _.pluck(colorOptions, "key"),
|
||||
defaultValue: "q",
|
||||
},
|
||||
//TODO add per-creature settings
|
||||
//how many experiences to load at a time in XP table
|
||||
"settings.experiencesInc": {type: Number, defaultValue: 20},
|
||||
//slowed down by carrying too much?
|
||||
"settings.useVariantEncumbrance": {type: Boolean, defaultValue: false},
|
||||
"settings.useStandardEncumbrance": {type: Boolean, defaultValue: true},
|
||||
//hide spellcasting
|
||||
"settings.hideSpellcasting": {type: Boolean, defaultValue: false},
|
||||
//show to anyone with link
|
||||
"settings.viewPermission": {
|
||||
type: String,
|
||||
defaultValue: "whitelist",
|
||||
allowedValues: ["whitelist", "public"],
|
||||
index: 1,
|
||||
},
|
||||
"settings.swapStatAndModifier": {type: Boolean, defaultValue: false},
|
||||
"settings.exportFeatures": {type: Boolean, defaultValue: true},
|
||||
"settings.exportAttacks": {type: Boolean, defaultValue: true},
|
||||
"settings.exportDescription": {type: Boolean, defaultValue: true},
|
||||
"settings.newUserExperience": {type: Boolean, optional: true},
|
||||
});
|
||||
|
||||
Creatures.attachSchema(Schemas.Creature);
|
||||
|
||||
Creatures.calculate = {
|
||||
xpLevel: function(charId){
|
||||
var xp = Creatures.calculate.experience(charId);
|
||||
for (var i = 0; i < 19; i++){
|
||||
if (xp < XP_TABLE[i]){
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (xp > 355000) return 20;
|
||||
return 0;
|
||||
},
|
||||
};
|
||||
|
||||
const insertCharacter = new ValidatedMethod({
|
||||
|
||||
name: "Creatures.methods.insertCharacter", // DDP method name
|
||||
|
||||
validate: new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
}).validator(),
|
||||
|
||||
run({name}) {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error("Creatures.methods.insert.denied",
|
||||
"You need to be logged in to insert a creature");
|
||||
}
|
||||
|
||||
// Create the creature document
|
||||
Creatures.insert({name, owner: this.userId});
|
||||
this.unblock();
|
||||
//Add all the required attributes to it
|
||||
if (Meteor.isServer){
|
||||
addDefaultStats(charId);
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
const addDefaultStats = function(charId){
|
||||
const defaultDocs = getDefaultCreatureDocs(charId);
|
||||
Attributes.rawCollection().insert(defaultDocs.attributes, {ordered: false});
|
||||
Skills.rawCollection().insert(defaultDocs.skills, {ordered: false});
|
||||
DamageMultipliers.rawCollection().insert(defaultDocs.damageMultipliers, {ordered: false});
|
||||
};
|
||||
|
||||
//clean up all data related to that creature before removing it
|
||||
if (Meteor.isServer){
|
||||
Creatures.after.remove(function(userId, creature) {
|
||||
let charId = creature._id;
|
||||
Actions .remove({charId});
|
||||
Attacks .remove({charId});
|
||||
Attributes .remove({charId});
|
||||
Buffs .remove({charId});
|
||||
Classes .remove({charId});
|
||||
CustomBuffs .remove({charId});
|
||||
DamageMultipliers.remove({charId});
|
||||
Effects .remove({charId});
|
||||
Experiences .remove({charId});
|
||||
Features .remove({charId});
|
||||
Notes .remove({charId});
|
||||
Proficiencies .remove({charId});
|
||||
Skills .remove({charId});
|
||||
SpellLists .remove({charId});
|
||||
Items .remove({charId});
|
||||
Containers .remove({charId});
|
||||
});
|
||||
Creatures.after.update(function(userId, doc, fieldNames, modifier, options) {
|
||||
if (_.contains(fieldNames, "name")){
|
||||
var urlName = getSlug(doc.name, {maintainCase: true}) || "-";
|
||||
Creatures.update(doc._id, {$set: {urlName}});
|
||||
}
|
||||
});
|
||||
Creatures.before.insert(function(userId, doc) {
|
||||
doc.urlName = getSlug(doc.name, {maintainCase: true}) || "-";
|
||||
// The first creature a user creates should have the new user experience
|
||||
if (!Creatures.find({owner: userId}).count()){
|
||||
doc.settings.newUserExperience = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Creatures.allow({
|
||||
insert: function(userId, doc) {
|
||||
// the user must be logged in, and the document must be owned by the user
|
||||
return (userId && doc.owner === userId);
|
||||
},
|
||||
update: function(userId, doc, fields, modifier) {
|
||||
// can only change documents you have write access to
|
||||
return doc.owner === userId ||
|
||||
_.contains(doc.writers, userId);
|
||||
},
|
||||
remove: function(userId, doc) {
|
||||
// can only remove your own documents
|
||||
return doc.owner === userId;
|
||||
},
|
||||
fetch: ["owner", "writers"],
|
||||
});
|
||||
|
||||
Creatures.deny({
|
||||
update: function(userId, docs, fields, modifier) {
|
||||
// can't change owners
|
||||
return _.contains(fields, "owner");
|
||||
}
|
||||
});
|
||||
53
app/imports/model/creature/CustomBuffs.js
Normal file
53
app/imports/model/creature/CustomBuffs.js
Normal file
@@ -0,0 +1,53 @@
|
||||
CustomBuffs = new Mongo.Collection("customBuffs");
|
||||
|
||||
Schemas.CustomBuff = new SimpleSchema({
|
||||
charId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
allowedValues: [
|
||||
"self",
|
||||
"others",
|
||||
"both"
|
||||
],
|
||||
defaultValue: "self",
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
autoValue: function(){
|
||||
return false;
|
||||
//enabled is ALWAYS false on these, so that its children are also not enabled, so that the buff templates have no effects.
|
||||
},
|
||||
},
|
||||
"lifeTime.total": {
|
||||
type: Number,
|
||||
defaultValue: 0, //0 is infinite
|
||||
min: 0,
|
||||
},
|
||||
//the id of the feature, buff or item that creates this buff
|
||||
parent: {
|
||||
type: Schemas.Parent,
|
||||
},
|
||||
});
|
||||
|
||||
CustomBuffs.attachSchema(Schemas.CustomBuff);
|
||||
|
||||
CustomBuffs.attachBehaviour("softRemovable");
|
||||
makeParent(CustomBuffs, ["name", "enabled"]); //parents of effects, attacks, proficiencies. Since this represents a template, "enabled" is always false.
|
||||
makeChild(CustomBuffs); //children of lots of things
|
||||
|
||||
CustomBuffs.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
CustomBuffs.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
40
app/imports/model/creature/DamageMultipliers.js
Normal file
40
app/imports/model/creature/DamageMultipliers.js
Normal file
@@ -0,0 +1,40 @@
|
||||
DamageMultipliers = new Mongo.Collection("damageMultipliers");
|
||||
|
||||
/*
|
||||
* DamageMultipliers are whole numbered stats of a character
|
||||
*/
|
||||
Schemas.DamageMultiplier = new SimpleSchema({
|
||||
charId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
// The nice-to-read name
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
// The technical, lowercase, single-word name used in formulae
|
||||
variableName: {
|
||||
type: String,
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
decimal: true,
|
||||
defaultValue: 1,
|
||||
},
|
||||
parent: {
|
||||
type: Schemas.Parent
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
},
|
||||
});
|
||||
|
||||
DamageMultipliers.attachSchema(Schemas.DamageMultiplier);
|
||||
|
||||
DamageMultipliers.attachBehaviour("softRemovable");
|
||||
makeChild(DamageMultipliers, ["enabled"]); //children of lots of things
|
||||
|
||||
DamageMultipliers.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
DamageMultipliers.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
131
app/imports/model/creature/Effects.js
Normal file
131
app/imports/model/creature/Effects.js
Normal file
@@ -0,0 +1,131 @@
|
||||
Effects = new Mongo.Collection("effects");
|
||||
|
||||
/*
|
||||
* Effects are reason-value attached to skills and abilities
|
||||
* that modify their final value or presentation in some way
|
||||
*/
|
||||
Schemas.Effect = new SimpleSchema({
|
||||
charId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
optional: true, //TODO make necessary if there is no owner
|
||||
trim: false,
|
||||
},
|
||||
operation: {
|
||||
type: String,
|
||||
defaultValue: "add",
|
||||
allowedValues: [
|
||||
"base",
|
||||
"add",
|
||||
"mul",
|
||||
"min",
|
||||
"max",
|
||||
"advantage",
|
||||
"disadvantage",
|
||||
"passiveAdd",
|
||||
"fail",
|
||||
"conditional",
|
||||
],
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
decimal: true,
|
||||
optional: true,
|
||||
},
|
||||
calculation: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
//the thing that created this effect
|
||||
parent: {
|
||||
type: Schemas.Parent
|
||||
},
|
||||
//which stat the effect is applied to
|
||||
stat: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
},
|
||||
});
|
||||
|
||||
Effects.attachSchema(Schemas.Effect);
|
||||
|
||||
Effects.attachBehaviour("softRemovable");
|
||||
makeChild(Effects, ["enabled"]); //children of lots of things
|
||||
|
||||
Effects.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Effects.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
|
||||
//give characters default character effects
|
||||
Characters.after.insert(function(userId, char) {
|
||||
if (Meteor.isServer) {
|
||||
Effects.insert({
|
||||
charId: char._id,
|
||||
name: "Constitution modifier for each level",
|
||||
stat: "hitPoints",
|
||||
operation: "add",
|
||||
calculation: "level * constitutionMod",
|
||||
parent: {
|
||||
id: char._id,
|
||||
collection: "Characters",
|
||||
group: "Inate",
|
||||
},
|
||||
});
|
||||
Effects.insert({
|
||||
charId: char._id,
|
||||
name: "Proficiency bonus by level",
|
||||
stat: "proficiencyBonus",
|
||||
operation: "add",
|
||||
calculation: "floor(level / 4 + 1.75)",
|
||||
parent: {
|
||||
id: char._id,
|
||||
collection: "Characters",
|
||||
group: "Inate",
|
||||
},
|
||||
});
|
||||
Effects.insert({
|
||||
charId: char._id,
|
||||
name: "Dexterity Armor Bonus",
|
||||
stat: "armor",
|
||||
operation: "add",
|
||||
calculation: "dexterityArmor",
|
||||
parent: {
|
||||
id: char._id,
|
||||
collection: "Characters",
|
||||
group: "Inate",
|
||||
},
|
||||
});
|
||||
Effects.insert({
|
||||
charId: char._id,
|
||||
name: "Natural Armor",
|
||||
stat: "armor",
|
||||
operation: "base",
|
||||
value: 10,
|
||||
parent: {
|
||||
id: char._id,
|
||||
collection: "Characters",
|
||||
group: "Inate",
|
||||
},
|
||||
});
|
||||
Effects.insert({
|
||||
charId: char._id,
|
||||
name: "Natural Carrying Capacity",
|
||||
stat: "carryMultiplier",
|
||||
operation: "base",
|
||||
value: "1",
|
||||
parent: {
|
||||
id: char._id,
|
||||
collection: "Characters",
|
||||
group: "Inate",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
27
app/imports/model/creature/Experience.js
Normal file
27
app/imports/model/creature/Experience.js
Normal file
@@ -0,0 +1,27 @@
|
||||
Experiences = new Mongo.Collection("experience");
|
||||
|
||||
Schemas.Experience = new SimpleSchema({
|
||||
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
name: {type: String, optional: true, trim: false, defaultValue: "New Experience"},
|
||||
description: {type: String, optional: true, trim: false},
|
||||
value: {type: Number, defaultValue: 0},
|
||||
dateAdded: {
|
||||
type: Date,
|
||||
autoValue: function() {
|
||||
if (this.isInsert) {
|
||||
return new Date();
|
||||
} else if (this.isUpsert) {
|
||||
return {$setOnInsert: new Date()};
|
||||
} else {
|
||||
this.unset();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Experiences.attachSchema(Schemas.Experience);
|
||||
|
||||
Experiences.attachBehaviour("softRemovable");
|
||||
|
||||
Experiences.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Experiences.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
115
app/imports/model/creature/Features.js
Normal file
115
app/imports/model/creature/Features.js
Normal file
@@ -0,0 +1,115 @@
|
||||
Features = new Mongo.Collection("features");
|
||||
|
||||
Schemas.Feature = new SimpleSchema({
|
||||
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
name: {type: String, optional: true, trim: false},
|
||||
description: {type: String, optional: true, trim: false},
|
||||
uses: {type: String, optional: true, trim: false},
|
||||
used: {type: Number, defaultValue: 0},
|
||||
reset: {
|
||||
type: String,
|
||||
allowedValues: ["manual", "longRest", "shortRest"],
|
||||
defaultValue: "manual",
|
||||
},
|
||||
enabled: {type: Boolean, defaultValue: true},
|
||||
alwaysEnabled:{type: Boolean, defaultValue: true},
|
||||
color: {type: String,
|
||||
allowedValues: _.pluck(colorOptions, "key"),
|
||||
defaultValue: "q",
|
||||
},
|
||||
});
|
||||
|
||||
Features.attachSchema(Schemas.Feature);
|
||||
|
||||
Features.helpers({
|
||||
usesLeft: function(){
|
||||
return evaluate(this.charId, this.uses) - this.used;
|
||||
},
|
||||
usesValue: function(){
|
||||
return evaluate(this.charId, this.uses);
|
||||
},
|
||||
});
|
||||
|
||||
Features.attachBehaviour("softRemovable");
|
||||
makeParent(Features, ["name", "enabled"]); //parents of effects and attacks
|
||||
|
||||
Features.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Features.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
|
||||
//give characters default feature of base ability scores of 10
|
||||
Characters.after.insert(function(userId, char) {
|
||||
if (Meteor.isServer){
|
||||
var featureId = Features.insert({
|
||||
name: "Base Ability Scores",
|
||||
charId: char._id,
|
||||
enabled: true,
|
||||
alwaysEnabled: true,
|
||||
});
|
||||
Effects.insert({
|
||||
stat: "strength",
|
||||
charId: char._id,
|
||||
parent: {
|
||||
id: featureId,
|
||||
collection: "Features",
|
||||
},
|
||||
operation: "base",
|
||||
value: 10,
|
||||
enabled: true,
|
||||
});
|
||||
Effects.insert({
|
||||
stat: "dexterity",
|
||||
charId: char._id,
|
||||
parent: {
|
||||
id: featureId,
|
||||
collection: "Features",
|
||||
},
|
||||
operation: "base",
|
||||
value: 10,
|
||||
enabled: true,
|
||||
});
|
||||
Effects.insert({
|
||||
stat: "constitution",
|
||||
charId: char._id,
|
||||
parent: {
|
||||
id: featureId,
|
||||
collection: "Features",
|
||||
},
|
||||
operation: "base",
|
||||
value: 10,
|
||||
enabled: true,
|
||||
});
|
||||
Effects.insert({
|
||||
stat: "intelligence",
|
||||
charId: char._id,
|
||||
parent: {
|
||||
id: featureId,
|
||||
collection: "Features",
|
||||
},
|
||||
operation: "base",
|
||||
value: 10,
|
||||
enabled: true,
|
||||
});
|
||||
Effects.insert({
|
||||
stat: "wisdom",
|
||||
charId: char._id,
|
||||
parent: {
|
||||
id: featureId,
|
||||
collection: "Features",
|
||||
},
|
||||
operation: "base",
|
||||
value: 10,
|
||||
enabled: true,
|
||||
});
|
||||
Effects.insert({
|
||||
stat: "charisma",
|
||||
charId: char._id,
|
||||
parent: {
|
||||
id: featureId,
|
||||
collection: "Features",
|
||||
},
|
||||
operation: "base",
|
||||
value: 10,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
19
app/imports/model/creature/Notes.js
Normal file
19
app/imports/model/creature/Notes.js
Normal file
@@ -0,0 +1,19 @@
|
||||
Notes = new Mongo.Collection("notes");
|
||||
|
||||
Schemas.Note = new SimpleSchema({
|
||||
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
name: {type: String, optional: true, trim: false},
|
||||
description: {type: String, optional: true, trim: false},
|
||||
color: {
|
||||
type: String,
|
||||
allowedValues:_.pluck(colorOptions, "key"),
|
||||
defaultValue: "q",
|
||||
},
|
||||
});
|
||||
|
||||
Notes.attachSchema(Schemas.Note);
|
||||
|
||||
Notes.attachBehaviour("softRemovable");
|
||||
|
||||
Notes.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Notes.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
37
app/imports/model/creature/Proficiencies.js
Normal file
37
app/imports/model/creature/Proficiencies.js
Normal file
@@ -0,0 +1,37 @@
|
||||
Proficiencies = new Mongo.Collection("proficiencies");
|
||||
|
||||
Schemas.Proficiency = new SimpleSchema({
|
||||
charId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
trim: false,
|
||||
optional: true,
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
allowedValues: [0, 0.5, 1, 2],
|
||||
defaultValue: 1,
|
||||
decimal: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
allowedValues: ["skill", "save", "weapon", "armor", "tool", "language"],
|
||||
defaultValue: "skill",
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
},
|
||||
});
|
||||
|
||||
Proficiencies.attachSchema(Schemas.Proficiency);
|
||||
|
||||
Proficiencies.attachBehaviour("softRemovable");
|
||||
makeChild(Proficiencies, ["enabled"]);
|
||||
|
||||
Proficiencies.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Proficiencies.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
83
app/imports/model/creature/Skills.js
Normal file
83
app/imports/model/creature/Skills.js
Normal file
@@ -0,0 +1,83 @@
|
||||
Skills = new Mongo.Collection("skills");
|
||||
|
||||
/*
|
||||
* Skills are anything that results in a modifier to be added to a D20
|
||||
* Skills usually have an ability score modifier that they use as their basis
|
||||
*/
|
||||
Schemas.Skill = new SimpleSchema({
|
||||
charId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
// The nice-to-read name
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
// The technical, lowercase, single-word name used in formulae
|
||||
variableName: {
|
||||
type: String,
|
||||
},
|
||||
ability: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
allowedValues: [
|
||||
"skill",
|
||||
"save",
|
||||
"stat",
|
||||
"tool",
|
||||
"weapon",
|
||||
"language",
|
||||
"utility", //not displayed anywhere
|
||||
],
|
||||
},
|
||||
// Skills need to store their order to keep the sheet consistent
|
||||
order: {
|
||||
type: Number,
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
decimal: true,
|
||||
defaultValue: 0,
|
||||
},
|
||||
advantage: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
allowedValues: [-1, 0, 1],
|
||||
},
|
||||
passiveBonus: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
proficiency: {
|
||||
type: Number,
|
||||
allowedValues: [0, 0.5, 1, 2],
|
||||
defaultValue: 0,
|
||||
},
|
||||
conditionalBenefits: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
fail: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
parent: {
|
||||
type: Schemas.Parent
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
},
|
||||
});
|
||||
|
||||
Skills.attachSchema(Schemas.Skill);
|
||||
|
||||
Skills.attachBehaviour("softRemovable");
|
||||
makeChild(Skills, ["enabled"]); //children of lots of things
|
||||
|
||||
Skills.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Skills.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
37
app/imports/model/creature/SpellLists.js
Normal file
37
app/imports/model/creature/SpellLists.js
Normal file
@@ -0,0 +1,37 @@
|
||||
SpellLists = new Mongo.Collection("spellLists");
|
||||
|
||||
Schemas.SpellLists = new SimpleSchema({
|
||||
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
name: {type: String, optional: true, trim: false},
|
||||
description: {type: String, optional: true, trim: false},
|
||||
saveDC: {type: String, optional: true, trim: false},
|
||||
attackBonus: {type: String, optional: true, trim: false},
|
||||
maxPrepared: {type: String, optional: true, trim: false},
|
||||
color: {
|
||||
type: String,
|
||||
allowedValues: _.pluck(colorOptions, "key"),
|
||||
defaultValue: "q",
|
||||
},
|
||||
"settings.showUnprepared": {type: Boolean, defaultValue: true},
|
||||
});
|
||||
|
||||
SpellLists.attachSchema(Schemas.SpellLists);
|
||||
|
||||
SpellLists.helpers({
|
||||
numPrepared: function(){
|
||||
var num = 0;
|
||||
Spells.find(
|
||||
{charId: this.charId, listId: this._id, prepared: 1},
|
||||
{fields: {prepareCost: 1}}
|
||||
).forEach(function(spell){
|
||||
num += spell.prepareCost;
|
||||
});
|
||||
return num;
|
||||
}
|
||||
});
|
||||
|
||||
SpellLists.attachBehaviour("softRemovable");
|
||||
makeParent(SpellLists); //parents of spells
|
||||
|
||||
SpellLists.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
SpellLists.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
252
app/imports/model/creature/Spells.js
Normal file
252
app/imports/model/creature/Spells.js
Normal file
@@ -0,0 +1,252 @@
|
||||
Spells = new Mongo.Collection("spells");
|
||||
|
||||
Schemas.Spell = new SimpleSchema({
|
||||
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
prepared: {
|
||||
type: String,
|
||||
defaultValue: "prepared",
|
||||
allowedValues: ["prepared", "unprepared", "always"],
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
defaultValue: "New Spell",
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
castingTime: {
|
||||
type: String,
|
||||
optional: true,
|
||||
defaultValue: "action",
|
||||
trim: false,
|
||||
},
|
||||
range: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
duration: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
defaultValue: "Instantaneous",
|
||||
},
|
||||
"components.verbal": {type: Boolean, defaultValue: false},
|
||||
"components.somatic": {type: Boolean, defaultValue: false},
|
||||
"components.concentration": {type: Boolean, defaultValue: false},
|
||||
"components.material": {type: String, optional: true},
|
||||
ritual: {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
defaultValue: 1,
|
||||
},
|
||||
school: {
|
||||
type: String,
|
||||
defaultValue: "Abjuration",
|
||||
allowedValues: magicSchools,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
allowedValues: _.pluck(colorOptions, "key"),
|
||||
defaultValue: "q",
|
||||
},
|
||||
});
|
||||
|
||||
Spells.attachSchema(Schemas.Spell);
|
||||
|
||||
Spells.attachBehaviour("softRemovable");
|
||||
makeChild(Spells); //children of spell lists
|
||||
makeParent(Spells, ["name", "enabled"]); //parents of attacks
|
||||
|
||||
Spells.after.update(function (userId, spell, fieldNames) {
|
||||
//Update prepared state of spell and child attacks to be enabled or not
|
||||
if (_.contains(fieldNames, "prepared")) {
|
||||
var childAttacks = Attacks.find({"parent.id": spell._id}).fetch();
|
||||
if (spell.prepared === "unprepared") {
|
||||
_.each(childAttacks, function(attack){
|
||||
Attacks.update(attack._id, {$set: {enabled: false}});
|
||||
});
|
||||
} else if (spell.prepared === "prepared" || "always") {
|
||||
_.each(childAttacks, function(attack){
|
||||
Attacks.update(attack._id, {$set: {enabled: true}});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Spells.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Spells.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
|
||||
|
||||
|
||||
|
||||
var checkMovePermission = function(spellId, parent, destinationOnly) {
|
||||
var spell = Spells.findOne(spellId);
|
||||
if (!spell)
|
||||
throw new Meteor.Error("No such spell",
|
||||
"An spell could not be found to move");
|
||||
//handle permissions
|
||||
var permission;
|
||||
|
||||
if (!destinationOnly) { //if we're not modifying the origin, only the destination
|
||||
permission = Meteor.call("canWriteCharacter", spell.charId);
|
||||
if (!permission){
|
||||
throw new Meteor.Error("Access denied",
|
||||
"Not permitted to move spells from this character");
|
||||
}
|
||||
}
|
||||
if (parent.collection === "Characters"){
|
||||
permission = Meteor.call("canWriteCharacter", parent.id);
|
||||
if (!permission){
|
||||
throw new Meteor.Error("Access denied",
|
||||
"Not permitted to move spells to this character");
|
||||
}
|
||||
} else {
|
||||
var parentCollectionObject = global[parent.collection];
|
||||
var parentObject = null;
|
||||
if (parentCollectionObject)
|
||||
parentObject = parentCollectionObject.findOne(
|
||||
parent.id, {fields: {_id: 1, charId: 1}}
|
||||
);
|
||||
if (!parentObject) throw new Meteor.Error(
|
||||
"Invalid parent",
|
||||
"The destination parent " + parent.id +
|
||||
" does not exist in the collection " + parent.collection
|
||||
);
|
||||
if (parentObject.charId){
|
||||
permission = Meteor.call("canWriteCharacter", parentObject.charId);
|
||||
if (!permission){
|
||||
throw new Meteor.Error("Access denied",
|
||||
"Not permitted to move spells to this character");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var moveSpell = function(spellId, targetCollection, targetId) {
|
||||
var spell = Spells.findOne(spellId);
|
||||
if (!spell) return;
|
||||
targetCollection = targetCollection || spell.parent.collection;
|
||||
targetId = targetId || spell.parent.id;
|
||||
|
||||
if (Meteor.isServer) {
|
||||
checkMovePermission(spellId, {collection: targetCollection, id: targetId}, false);
|
||||
}
|
||||
|
||||
if (targetCollection == "Characters") { //then we are copying the spell to a different character.
|
||||
targetList = SpellLists.findOne({"charId": targetId});
|
||||
targetListId = targetList && targetList._id;
|
||||
if (!targetListId) {
|
||||
targetListId = SpellLists.insert({ //create a spell list if we don't already have one
|
||||
name: "New SpellList",
|
||||
charId: targetId,
|
||||
saveDC: "8 + intelligenceMod + proficiencyBonus",
|
||||
attackBonus: "intelligenceMod + proficiencyBonus",
|
||||
});
|
||||
}
|
||||
|
||||
Spells.update(
|
||||
spellId,
|
||||
{$set: {
|
||||
charId: targetId,
|
||||
"parent.collection": "SpellLists",
|
||||
"parent.id": targetListId,
|
||||
}}
|
||||
);
|
||||
}
|
||||
else { //we are moving the spell within the same character
|
||||
//update the spell provided the update will actually change something
|
||||
if (
|
||||
spell.parent.collection !== targetCollection ||
|
||||
spell.parent.id !== targetId
|
||||
){
|
||||
Spells.update(
|
||||
spellId,
|
||||
{$set: {
|
||||
"parent.collection": targetCollection,
|
||||
"parent.id": targetId,
|
||||
}}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var copySpell = function(spellId, targetCollection, targetId) {
|
||||
var spell = Spells.findOne(spellId);
|
||||
if (!spell) return;
|
||||
targetCollection = targetCollection || spell.parent.collection;
|
||||
targetId = targetId || spell.parent.id;
|
||||
|
||||
if (Meteor.isServer) {
|
||||
checkMovePermission(spellId, {collection: targetCollection, id: targetId}, true); //we're only reading from the source character
|
||||
}
|
||||
|
||||
|
||||
if (targetCollection == "Characters") { //then we are copying the spell to a different character.
|
||||
targetList = SpellLists.findOne({"charId": targetId});
|
||||
targetListId = targetList && targetList._id;
|
||||
if (!targetListId) {
|
||||
targetListId = SpellLists.insert({ //create a spell list if we don't already have one
|
||||
name: "New SpellList",
|
||||
charId: targetId,
|
||||
saveDC: "8 + intelligenceMod + proficiencyBonus",
|
||||
attackBonus: "intelligenceMod + proficiencyBonus",
|
||||
});
|
||||
}
|
||||
|
||||
newSpell = _.clone(spell);
|
||||
delete newSpell._id;
|
||||
newSpellId = Spells.insert(newSpell); //add a new copy of the spell
|
||||
Spells.update(
|
||||
newSpellId,
|
||||
{$set: {
|
||||
charId: targetId,
|
||||
"parent.collection": "SpellLists",
|
||||
"parent.id": targetListId,
|
||||
}}
|
||||
);
|
||||
}
|
||||
else { //else we are copying the spell within the same character
|
||||
newSpell = _.clone(spell);
|
||||
delete newSpell._id;
|
||||
newSpellId = Spells.insert(newSpell); //add a new copy of the spell
|
||||
Spells.update(
|
||||
newSpellId,
|
||||
{$set: {
|
||||
"parent.collection": targetCollection,
|
||||
"parent.id": targetId,
|
||||
}}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Meteor.methods({
|
||||
moveSpellToList: function(spellId, spellListId) {
|
||||
check(spellId, String);
|
||||
check(spellListId, String);
|
||||
moveSpell(spellId, "SpellLists", spellListId);
|
||||
},
|
||||
copySpellToList: function(spellId, spellListId) {
|
||||
check(spellId, String);
|
||||
check(spellListId, String);
|
||||
copySpell(spellId, "SpellLists", spellListId);
|
||||
},
|
||||
moveSpellToCharacter: function(spellId, charId) {
|
||||
check(spellId, String);
|
||||
check(charId, String);
|
||||
moveSpell(spellId, "Characters", charId);
|
||||
},
|
||||
copySpellToCharacter: function(spellId, charId) {
|
||||
check(spellId, String);
|
||||
check(charId, String);
|
||||
copySpell(spellId, "Characters", charId);
|
||||
},
|
||||
});
|
||||
41
app/imports/model/creature/TemporaryHitPoints.js
Normal file
41
app/imports/model/creature/TemporaryHitPoints.js
Normal file
@@ -0,0 +1,41 @@
|
||||
TemporaryHitPoints = new Mongo.Collection("temporaryHitPoints");
|
||||
|
||||
Schemas.TemporaryHitPoints = new SimpleSchema({
|
||||
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
name: {type: String, optional: true},
|
||||
maximum: {type: Number, defaultValue: 0, min: 0, max: 500},
|
||||
used: {type: Number, defaultValue: 0, min: 0, max: 500},
|
||||
deleteOnZero:{type: Boolean, defaultValue: false},
|
||||
dateAdded: {
|
||||
type: Date,
|
||||
autoValue: function() {
|
||||
if (this.isInsert) {
|
||||
return new Date();
|
||||
} else if (this.isUpsert) {
|
||||
return {$setOnInsert: new Date()};
|
||||
} else {
|
||||
this.unset();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
TemporaryHitPoints.attachSchema(Schemas.TemporaryHitPoints);
|
||||
|
||||
TemporaryHitPoints.helpers({
|
||||
left: function(){
|
||||
return this.maximum - this.used;
|
||||
}
|
||||
});
|
||||
|
||||
//remove the temporary hit points when they hit zero
|
||||
TemporaryHitPoints.after.update(
|
||||
function(userId, thp, fieldNames, modifier, options){
|
||||
if (thp.used >= thp.maximum && thp.deleteOnZero){
|
||||
TemporaryHitPoints.remove(thp._id);
|
||||
}
|
||||
}, {fetchPrevious: false}
|
||||
);
|
||||
|
||||
TemporaryHitPoints.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
TemporaryHitPoints.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
609
app/imports/model/creature/creatureComputation.js
Normal file
609
app/imports/model/creature/creatureComputation.js
Normal file
@@ -0,0 +1,609 @@
|
||||
// TODO allow abilities to get disadvantage, making all skills that are based
|
||||
// on them disadvantaged as well
|
||||
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
|
||||
const recomputeCharacter = new ValidatedMethod({
|
||||
|
||||
name: "Characters.methods.recomputeCharacter",
|
||||
|
||||
validate: new SimpleSchema({
|
||||
charId: { type: String }
|
||||
}).validator(),
|
||||
|
||||
run({charId}) {
|
||||
if (!canEditCharacter(charId, this.userId)) {
|
||||
throw new Meteor.Error('Characters.methods.recomputeCharacter.denied',
|
||||
'You do not have permission to recompute this character');
|
||||
}
|
||||
|
||||
computeCharacterById(charId);
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
/*
|
||||
* This function is the heart of DiceCloud. It recomputes a character's stats,
|
||||
* distilling down effects and proficiencies into the final stats that make up
|
||||
* a character.
|
||||
*
|
||||
* Essentially this is a backtracking algorithm that computes stats'
|
||||
* dependencies before computing stats themselves, while detecting
|
||||
* dependency loops.
|
||||
*
|
||||
* At the moment it makes no effort to limit recomputation to just what was
|
||||
* changed.
|
||||
*
|
||||
* Attempting to implement dependency management to limit recomputation to just
|
||||
* change affected stats should only happen as a last resort, when this function
|
||||
* can no longer be performed more efficiently, and server resources can not be
|
||||
* expanded to meet demand.
|
||||
*
|
||||
* A brief overview:
|
||||
* - Fetch the stats of the character and add them to
|
||||
* an object for quick lookup
|
||||
* - Fetch the effects and proficiencies which apply to each stat and store them with the stat
|
||||
* - Fetch the class levels and store them as well
|
||||
* - Mark each stat and effect as uncomputed
|
||||
* - Iterate over each stat in order and compute it
|
||||
* - If the stat is already computed, skip it
|
||||
* - If the stat is busy being computed, we are in a dependency loop, make it NaN and mark computed
|
||||
* - Mark the stat as busy computing
|
||||
* - Iterate over each effect which applies to the attribute
|
||||
* - If the effect is not computed compute it
|
||||
* - If the effect relies on another attribute, get its computed value
|
||||
* - Recurse if that attribute is uncomputed
|
||||
* - apply the effect to the attribute
|
||||
* - Conglomerate all the effects to compute the final stat values
|
||||
* - Mark the stat as computed
|
||||
* - Write the computed results back to the database
|
||||
*/
|
||||
const computeCharacterById = function (charId){
|
||||
let char = buildCharacter();
|
||||
char = computeCharacter(char);
|
||||
writeCharacter(char);
|
||||
return char;
|
||||
};
|
||||
|
||||
/*
|
||||
* Write the in-memory character to the database docs
|
||||
*/
|
||||
const writeCharacter = function (char) {
|
||||
writeAttributes(char);
|
||||
writeSkills(char);
|
||||
writeDamageMultipliers(char);
|
||||
Characters.update(char.id, {$set: {level: char.level}});
|
||||
};
|
||||
|
||||
/*
|
||||
* Write all the attributes from the in-memory char object to the Attirbute docs
|
||||
*/
|
||||
const writeAttributes = function (char) {
|
||||
let bulkWriteOps = _.map(char.atts, (att, variableName) => {
|
||||
let op = {
|
||||
updateMany: {
|
||||
filter: {charId: char.id, variableName},
|
||||
update: {$set: {
|
||||
value: att.result,
|
||||
}},
|
||||
}
|
||||
}
|
||||
if (att.mod){
|
||||
op.updateMany.update.mod = att.mod;
|
||||
}
|
||||
return op;
|
||||
});
|
||||
if (Meteor.isServer){
|
||||
Attributes.rawCollection().bulkWrite( bulkWriteOps, {ordered : false});
|
||||
} else {
|
||||
_.each(bulkWriteOps, op => {
|
||||
Attributes.update(op.updateMany.filter, op.updateMany.update, {multi: true});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Write all the skills from the in-memory char object to the Skills docs
|
||||
*/
|
||||
const writeSkills = function (char) {
|
||||
let bulkWriteOps = _.map(char.skills, (skill, variableName) => {
|
||||
let op = {
|
||||
updateMany: {
|
||||
filter: {charId: char.id, variableName},
|
||||
update: {$set: {
|
||||
value: skill.result,
|
||||
advantage: skill.advantage,
|
||||
passiveBonus: skill.passiveAdd,
|
||||
proficiency: skill.proficiency,
|
||||
conditionalBenefits: skill.conditional,
|
||||
fail: skill.fail,
|
||||
}},
|
||||
}
|
||||
}
|
||||
return op;
|
||||
});
|
||||
if (Meteor.isServer){
|
||||
Skills.rawCollection().bulkWrite( bulkWriteOps, {ordered : false});
|
||||
} else {
|
||||
_.each(bulkWriteOps, op => {
|
||||
Skills.update(op.updateMany.filter, op.updateMany.update, {multi: true});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Write all the damange multipliers from the in-memory char object to the docs
|
||||
*/
|
||||
const writeDamageMultipliers = function (char) {
|
||||
let bulkWriteOps = _.map(char.dms, (dm, variableName) => {
|
||||
let op = {
|
||||
updateMany: {
|
||||
filter: {charId: char.id, variableName},
|
||||
update: {$set: {
|
||||
value: dm.result,
|
||||
}},
|
||||
}
|
||||
}
|
||||
return op;
|
||||
});
|
||||
if (Meteor.isServer){
|
||||
DamageMultipliers.rawCollection().bulkWrite( bulkWriteOps, {ordered : false});
|
||||
} else {
|
||||
_.each(bulkWriteOps, op => {
|
||||
DamageMultipliers.update(op.updateMany.filter, op.updateMany.update, {multi: true});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the character's data from the database and build an in-memory model that
|
||||
* can be computed. Hits 6 database collections with indexed queries.
|
||||
*/
|
||||
const buildCharacter = function (charId){
|
||||
let char = {
|
||||
id: charId,
|
||||
atts: {},
|
||||
skills: {},
|
||||
dms: {},
|
||||
classes: {},
|
||||
level: 0,
|
||||
};
|
||||
// Fetch the attributes of the character and add them to an object for quick lookup
|
||||
Attributes.find({charId}).forEach(attribute => {
|
||||
if (!char.atts[attribute.variableName]){
|
||||
char.atts[attribute.variableName] = {
|
||||
computed: false,
|
||||
busyComputing: false,
|
||||
type: "attribute",
|
||||
attributeType: attribute.type,
|
||||
base: attribute.baseValue || 0,
|
||||
decimal: attribute.decimal,
|
||||
result: 0,
|
||||
mod: 0, // The resulting modifier if this is an ability
|
||||
add: 0,
|
||||
mul: 1,
|
||||
min: Number.NEGATIVE_INFINITY,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
effects: [],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch the skills of the character and store them
|
||||
Skills.find({charId}).forEach(skill => {
|
||||
if (!char.skills[skill.variableName]){
|
||||
char.skills[skill.variableName] = {
|
||||
computed: false,
|
||||
busyComputing: false,
|
||||
type: "skill",
|
||||
ability: skill.ability,
|
||||
result: 0, // For skills the result is the skillMod
|
||||
proficiency: 0,
|
||||
add: 0,
|
||||
mul: 1,
|
||||
min: Number.NEGATIVE_INFINITY,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
advantage: 0,
|
||||
disadvantage: 0,
|
||||
passiveAdd: 0,
|
||||
fail: 0,
|
||||
conditional: 0,
|
||||
effects: [],
|
||||
proficiencies: [],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch the damage multipliers of the character and store them
|
||||
DamageMultipliers.find({charId}).forEach(damageMultiplier =>{
|
||||
if (!char.dms[damageMultiplier.variableName]){
|
||||
char.dms[damageMultiplier.variableName] = {
|
||||
computed: false,
|
||||
busyComputing: false,
|
||||
type: "damageMultiplier",
|
||||
result: 0,
|
||||
immunityCount: 0,
|
||||
ressistanceCount: 0,
|
||||
vulnerabilityCount: 0,
|
||||
effects: [],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch the class levels and store them
|
||||
// don't use the word "class" it's reserved
|
||||
Classes.find({charId}).forEach(cls => {
|
||||
if (!char.classes[cls.name]){
|
||||
char.classes[cls.name] = {level: cls.level};
|
||||
char.level += cls.level;
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch the effects which apply to each stat and store them under the attribute
|
||||
Effects.find({
|
||||
charId: charId,
|
||||
enabled: true,
|
||||
}).forEach(effect => {
|
||||
let storedEffect = {
|
||||
computed: false,
|
||||
result: 0,
|
||||
operation: effect.operation,
|
||||
value: effect.value,
|
||||
calculation: effect.calculation,
|
||||
}
|
||||
if (char.atts[effect.stat]) {
|
||||
char.atts[effect.stat].effects.push(storedEffect);
|
||||
} else if (char.skills[effect.stat]) {
|
||||
char.skills[effect.stat].effects.push(storedEffect);
|
||||
} else if (char.dms[effect.stat]) {
|
||||
char.dms[effect.stat].effects.push(storedEffect);
|
||||
} else {
|
||||
// ignore effects that don't apply to an actual stat
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch the proficiencies and store them under each skill
|
||||
Proficiencies.find({
|
||||
charId: charId,
|
||||
enabled: true,
|
||||
type: {$in: ["skill", "save"]}
|
||||
}).forEach(proficiency => {
|
||||
if (char.skills[proficiency.name]) {
|
||||
char.skills[proficiency.name].proficiencies.push(effect);
|
||||
}
|
||||
});
|
||||
return char;
|
||||
}
|
||||
|
||||
/*
|
||||
* Compute the character's stats in-place, returns the same char object
|
||||
*/
|
||||
export const computeCharacter = function (char){
|
||||
// Iterate over each stat in order and compute it
|
||||
for (statName in char.atts){
|
||||
let stat = char.atts[statName]
|
||||
computeStat (stat, char);
|
||||
}
|
||||
for (statName in char.skills){
|
||||
let stat = char.skills[statName]
|
||||
computeStat (stat, char);
|
||||
}
|
||||
for (statName in char.dms){
|
||||
let stat = char.dms[statName]
|
||||
computeStat (stat, char);
|
||||
}
|
||||
return char;
|
||||
}
|
||||
|
||||
/*
|
||||
* Compute a single stat on a character
|
||||
*/
|
||||
const computeStat = function(stat, char){
|
||||
// If the stat is already computed, skip it
|
||||
if (stat.computed) return;
|
||||
|
||||
// If the stat is busy being computed, make it NaN and mark computed
|
||||
if (stat.busyComputing){
|
||||
// Trying to compute this stat again while it is already computing.
|
||||
// We must be in a dependency loop.
|
||||
stat.computed = true;
|
||||
stat.result = NaN;
|
||||
stat.busyComputing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Iterate over each effect which applies to the stat
|
||||
for (i in stat.effects){
|
||||
computeEffect(stat.effects[i], char);
|
||||
// apply the effect to the stat
|
||||
applyEffect(stat.effects[i], stat);
|
||||
}
|
||||
|
||||
// Conglomerate all the effects to compute the final stat values
|
||||
combineStat(stat, char);
|
||||
|
||||
// Mark the attribute as computed
|
||||
stat.computed = true;
|
||||
stat.busyComputing = false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Compute a single effect on a character
|
||||
*/
|
||||
const computeEffect = function(effect, char){
|
||||
if (effect.computed) return;
|
||||
if (_.isFinite(effect.value)){
|
||||
effect.result = effect.value;
|
||||
} else if(effect.operation === "conditional"){
|
||||
effect.result = effect.calculation;
|
||||
} else if(_.contains(["advantage", "disadvantage", "fail"], effect.operation)){
|
||||
effect.result = 1;
|
||||
} else if (_.isString(effect.calculation)){
|
||||
effect.result = evaluateCalculation(effect.calculation, char);
|
||||
}
|
||||
effect.computed = true;
|
||||
};
|
||||
|
||||
/*
|
||||
* Apply a computed effect to its stat
|
||||
*/
|
||||
const applyEffect = function(effect, stat){
|
||||
// Take the largest base value
|
||||
if (effect.operation === "base"){
|
||||
if (!_.has(stat, "base")) return;
|
||||
stat.base = effect.result > stat.base ? effect.result : stat.base;
|
||||
}
|
||||
// Add all adds together
|
||||
else if (effect.operation === "add"){
|
||||
if (!_.has(stat, "add")) return;
|
||||
stat.add += effect.result;
|
||||
}
|
||||
else if (effect.operation === "mul"){
|
||||
if (!_.has(stat, "mul")) return;
|
||||
if (stat.type === "damageMultiplier"){
|
||||
if (value === 0) stat.immunityCount++;
|
||||
else if (value === 0.5) stat.ressistanceCount++;
|
||||
else if (value === 2) stat.vulnerabilityCount++;
|
||||
} else {
|
||||
// Multiply all muls together
|
||||
stat.mul *= effect.result;
|
||||
}
|
||||
}
|
||||
// Take the largest min value
|
||||
if (effect.operation === "min"){
|
||||
if (!_.has(stat, "min")) return;
|
||||
stat.min = effect.result > stat.min ? effect.result : stat.min;
|
||||
}
|
||||
// Take the smallest max value
|
||||
if (effect.operation === "max"){
|
||||
if (!_.has(stat, "max")) return;
|
||||
stat.max = effect.result < stat.max ? effect.result : stat.max;
|
||||
}
|
||||
// Sum number of advantages
|
||||
else if (effect.operation === "advantage"){
|
||||
if (!_.has(stat, "advantage")) return;
|
||||
stat.advantage++;
|
||||
}
|
||||
// Sum number of disadvantages
|
||||
else if (effect.operation === "disadvantage"){
|
||||
if (!_.has(stat, "disadvantage")) return;
|
||||
stat.disadvantage++;
|
||||
}
|
||||
// Add all passive adds together
|
||||
else if (effect.operation === "passiveAdd"){
|
||||
if (!_.has(stat, "passiveAdd")) return;
|
||||
stat.passiveAdd += effect.result;
|
||||
}
|
||||
// Sum number of fails
|
||||
else if (effect.operation === "fail"){
|
||||
if (!_.has(stat, "fail")) return;
|
||||
stat.fail++;
|
||||
}
|
||||
// Sum number of conditionals
|
||||
else if (effect.operation === "conditional"){
|
||||
if (!_.has(stat, "conditional")) return;
|
||||
stat.conditional++;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Combine the results of multiple effects to get the result of the stat
|
||||
*/
|
||||
const combineStat = function(stat, char){
|
||||
if (stat.type === "attribute"){
|
||||
combineAttribute(stat, char)
|
||||
} else if (stat.type === "skill"){
|
||||
combineSkill(stat, char)
|
||||
} else if (stat.type === "damageMultiplier"){
|
||||
combineDamageMultiplier(stat, char);
|
||||
}
|
||||
}
|
||||
|
||||
const combineAttribute = function(stat, char){
|
||||
stat.result = (stat.base + stat.add) * stat.mul;
|
||||
if (stat.result < stat.min) stat.result = stat.min;
|
||||
if (stat.result > stat.max) stat.result = stat.max;
|
||||
// Round everything that isn't the carry multiplier
|
||||
if (!stat.decimal) stat.result = Math.floor(stat.result);
|
||||
if (stat.attributeType === "ability") {
|
||||
stat.mod = Math.floor((stat.result - 10) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
const combineSkill = function(stat, char){
|
||||
for (i in stat.proficiencies){
|
||||
let prof = stat.proficiencies[i];
|
||||
if (prof.value > stat.proficiency) stat.proficiency = prof.value;
|
||||
}
|
||||
let profBonus;
|
||||
if (char.skills.proificiencyBonus){
|
||||
if (!char.skills.proficiencyBonus.computed){
|
||||
computeStat(char.skills.proficiencyBonus, char);
|
||||
}
|
||||
profBonus = char.skills.proficiencyBonus.result;
|
||||
} else {
|
||||
profBonus = Math.floor(char.level / 4 + 1.75);
|
||||
}
|
||||
profBonus *= stat.proficiency;
|
||||
// Skills are based on some ability Modifier
|
||||
let abilityMod = 0;
|
||||
if (stat.ability && char.atts[stat.ability]){
|
||||
if (!char.atts[stat.ability].computed){
|
||||
computeStat(char.atts[stat.ability], char);
|
||||
}
|
||||
abilityMod = char.atts[stat.ability].mod;
|
||||
}
|
||||
stat.result = (abilityMod + profBonus + stat.add) * stat.mul;
|
||||
if (stat.result < stat.min) stat.result = stat.min;
|
||||
if (stat.result > stat.max) stat.result = stat.max;
|
||||
stat.result = Math.floor(stat.result);
|
||||
}
|
||||
|
||||
const combineDamageMultiplier = function(stat, char){
|
||||
if (stat.immunityCount) return 0;
|
||||
if (stat.ressistanceCount && !stat.vulnerabilityCount){
|
||||
stat.result = 0.5;
|
||||
} else if (!stat.ressistanceCount && stat.vulnerabilityCount){
|
||||
stat.result = 2;
|
||||
} else {
|
||||
stat.result = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate a string computation
|
||||
const evaluateCalculation = function(string, char){
|
||||
if (!string) return string;
|
||||
|
||||
// Replace all the string variables with numbers if possible
|
||||
string = string.replace(/\b[a-z,1-9]+\b/gi, function(sub){
|
||||
// Make case insensitive
|
||||
sub = sub.toLowerCase()
|
||||
// Attributes
|
||||
if (char.atts[sub]){
|
||||
if (!char.atts[sub].computed){
|
||||
computeStat(char.atts[sub], char);
|
||||
}
|
||||
return char.atts[sub].result;
|
||||
}
|
||||
// Modifiers
|
||||
if (/^\w+mod$/.test(sub)){
|
||||
var slice = sub.slice(0, -3);
|
||||
if (char.atts[slice]){
|
||||
if (!char.atts[slice].computed){
|
||||
computeStat(char.atts[sub], char);
|
||||
}
|
||||
return char.atts[slice].mod || NaN;
|
||||
}
|
||||
}
|
||||
// Skills
|
||||
if (char.skills[sub]){
|
||||
if (!char.skills[sub].computed){
|
||||
computeStat(char.skills[sub], char);
|
||||
}
|
||||
return char.skills[sub].result;
|
||||
}
|
||||
// Damage Multipliers
|
||||
if (char.dms[sub]){
|
||||
if (!char.dms[sub].computed){
|
||||
computeStat(char.dms[sub], char);
|
||||
}
|
||||
return char.dms[sub].result;
|
||||
}
|
||||
// Class levels
|
||||
if (/^\w+levels?$/.test(sub)){
|
||||
//strip out "level(s)"
|
||||
var className = sub.replace(/levels?$/, "");
|
||||
return char.classes[className] && char.classes[className].level || sub;
|
||||
}
|
||||
// Character level
|
||||
if (sub === "level"){
|
||||
return char.level;
|
||||
}
|
||||
// Give up
|
||||
return sub;
|
||||
});
|
||||
|
||||
// Evaluate the expression to a number or return it as is.
|
||||
try {
|
||||
var result = math.eval(string); // math.eval is safe
|
||||
return result;
|
||||
} catch (e){
|
||||
return string;
|
||||
}
|
||||
};
|
||||
|
||||
const recomputeCharacterXP = new ValidatedMethod({
|
||||
name: "Characters.methods.recomputeCharacterXP",
|
||||
|
||||
validate: new SimpleSchema({
|
||||
charId: { type: String }
|
||||
}).validator(),
|
||||
|
||||
run({charId}) {
|
||||
if (!canEditCharacter(charId, this.userId)) {
|
||||
// Throw errors with a specific error code
|
||||
throw new Meteor.Error("Characters.methods.recomputeCharacterXP.denied",
|
||||
"You do not have permission to recompute this character's XP");
|
||||
}
|
||||
var xp = 0;
|
||||
Experiences.find(
|
||||
{charId: charId},
|
||||
{fields: {value: 1}}
|
||||
).forEach(function(e){
|
||||
xp += e.value;
|
||||
});
|
||||
|
||||
Characters.update(charId, {$set: {xp}})
|
||||
return xp;
|
||||
},
|
||||
});
|
||||
|
||||
const recomputeCharacterWeightCarried = new ValidatedMethod({
|
||||
name: "Character.methods.recomputeCharacterWeightCarried",
|
||||
|
||||
validate: new SimpleSchema({
|
||||
charId: { type: String }
|
||||
}).validator(),
|
||||
|
||||
run({charId}){
|
||||
if (!canEditCharacter(charId, this.userId)) {
|
||||
// Throw errors with a specific error code
|
||||
throw new Meteor.Error("Characters.methods.recomputeCharacterWeightCarried.denied",
|
||||
"You do not have permission to recompute this character's carried weight");
|
||||
}
|
||||
var weightCarried = 0;
|
||||
// store a dictionary of carried containers
|
||||
var carriedContainers = {};
|
||||
Containers.find(
|
||||
{
|
||||
charId,
|
||||
isCarried: true,
|
||||
},
|
||||
{ fields: {
|
||||
isCarried: 1,
|
||||
weight: 1,
|
||||
}}
|
||||
).forEach(container => {
|
||||
carriedContainers[container._id] = true;
|
||||
weightCarried += container.weight;
|
||||
});
|
||||
Items.find(
|
||||
{
|
||||
charId,
|
||||
},
|
||||
{ fields: {
|
||||
weight: 1,
|
||||
parent: 1,
|
||||
}}
|
||||
).forEach(item => {
|
||||
// if the item is carried/equiped or in a carried container, add its weight
|
||||
if (parent.id === charId || carriedContainers[parent.id]){
|
||||
weightCarried += item.weight;
|
||||
}
|
||||
});
|
||||
|
||||
Characters.update(charId, {$set: {weightCarried}})
|
||||
return weightCarried;
|
||||
}
|
||||
});
|
||||
97
app/imports/model/creature/creatureComputation.test.js
Normal file
97
app/imports/model/creature/creatureComputation.test.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import {computeCharacter} from "./CharacterComputation.js";
|
||||
import assert from "assert";
|
||||
|
||||
const makeEffect = function(operation, value){
|
||||
let effect = {computed: false, result: 0, operation}
|
||||
if (_.isFinite(value)){
|
||||
effect.value = +value;
|
||||
} else {
|
||||
effect.calculation = value;
|
||||
}
|
||||
return effect;
|
||||
}
|
||||
|
||||
describe('computeCharacter', function () {
|
||||
it('computes an aritrary character', function () {
|
||||
let char = {
|
||||
atts: {
|
||||
attribute1: {
|
||||
computed: false,
|
||||
busyComputing: false,
|
||||
type: "attribute",
|
||||
attributeType: "ability",
|
||||
result: 0,
|
||||
mod: 0, // The resulting modifier if this is an ability
|
||||
base: 0,
|
||||
add: 0,
|
||||
mul: 1,
|
||||
min: Number.NEGATIVE_INFINITY,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
effects: [
|
||||
makeEffect("base", 10),
|
||||
makeEffect("add", 5),
|
||||
makeEffect("mul", 2),
|
||||
],
|
||||
},
|
||||
attribute2: {
|
||||
computed: false,
|
||||
busyComputing: false,
|
||||
type: "attribute",
|
||||
result: 0,
|
||||
mod: 0, // The resulting modifier if this is an ability
|
||||
base: 0,
|
||||
add: 0,
|
||||
mul: 1,
|
||||
min: Number.NEGATIVE_INFINITY,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
effects: [
|
||||
makeEffect("base", "attribute1"),
|
||||
makeEffect("max", 2),
|
||||
],
|
||||
},
|
||||
},
|
||||
skills: {
|
||||
skill1: {
|
||||
computed: false,
|
||||
busyComputing: false,
|
||||
type: "skill",
|
||||
ability: "attribute1",
|
||||
result: 0,
|
||||
proficiency: 0,
|
||||
add: 0,
|
||||
mul: 1,
|
||||
min: Number.NEGATIVE_INFINITY,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
advantage: 0,
|
||||
disadvantage: 0,
|
||||
passiveAdd: 0,
|
||||
fail: 0,
|
||||
conditional: 0,
|
||||
effects: [],
|
||||
proficiencies: [],
|
||||
},
|
||||
},
|
||||
dms: {
|
||||
dm1: {
|
||||
computed: false,
|
||||
busyComputing: false,
|
||||
type: "damageMultiplier",
|
||||
result: 0,
|
||||
immunityCount: 0,
|
||||
ressistanceCount: 0,
|
||||
vulnerabilityCount: 0,
|
||||
effects: [],
|
||||
}
|
||||
},
|
||||
classes: {
|
||||
Barbarian: {
|
||||
level: 5,
|
||||
},
|
||||
},
|
||||
level: 5,
|
||||
};
|
||||
char = computeCharacter(char);
|
||||
console.log(char);
|
||||
assert(true);
|
||||
});
|
||||
});
|
||||
21
app/imports/model/creature/subschemas/Adjustment.js
Normal file
21
app/imports/model/creature/subschemas/Adjustment.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Adjustments make instantaneous changes to the value of some attribute
|
||||
* Damage, healing and resource cost/recovery are all adjustments
|
||||
*/
|
||||
Schemas.Adjustment = new SimpleSchema({
|
||||
//which stat the adjustment is applied to
|
||||
stat: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
//the value added to the stat
|
||||
value: {
|
||||
type: Number,
|
||||
decimal: true,
|
||||
optional: true,
|
||||
},
|
||||
calculation: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
13
app/imports/model/creature/subschemas/Attribute.js
Normal file
13
app/imports/model/creature/subschemas/Attribute.js
Normal file
@@ -0,0 +1,13 @@
|
||||
Schemas.Attribute = new SimpleSchema({
|
||||
//the temporary shift of the attribute
|
||||
//should be zero after reset
|
||||
adjustment: {
|
||||
type: Number,
|
||||
defaultValue: 0,
|
||||
},
|
||||
reset: {
|
||||
type: String,
|
||||
defaultValue: "longRest",
|
||||
allowedValues: ["longRest", "shortRest"],
|
||||
},
|
||||
});
|
||||
22
app/imports/model/creature/subschemas/DeathSaves.js
Normal file
22
app/imports/model/creature/subschemas/DeathSaves.js
Normal file
@@ -0,0 +1,22 @@
|
||||
Schemas.DeathSave = new SimpleSchema({
|
||||
pass: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
max: 3,
|
||||
defaultValue: 0,
|
||||
},
|
||||
fail: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
max: 3,
|
||||
defaultValue: 0,
|
||||
},
|
||||
canDeathSave: {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
},
|
||||
stable: {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
});
|
||||
4
app/imports/model/creature/subschemas/Skill.js
Normal file
4
app/imports/model/creature/subschemas/Skill.js
Normal file
@@ -0,0 +1,4 @@
|
||||
Schemas.Skill = new SimpleSchema({
|
||||
//attribute name that this skill used as base mod for roll
|
||||
ability: {type: String, defaultValue: ""},
|
||||
});
|
||||
Reference in New Issue
Block a user