lowercased all model directories

This commit is contained in:
Stefan Zermatten
2018-10-12 09:21:03 +02:00
parent 189a1d0a16
commit 4b900d5664
38 changed files with 2504 additions and 0 deletions

View 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

View 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;

View 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);

View 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);

View 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);

View 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);

View 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");
}
});

View 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);

View 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);

View 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",
},
});
}
});

View 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);

View 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,
});
}
});

View 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);

View 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);

View 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);

View 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);

View 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);
},
});

View 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);

View 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;
}
});

View 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);
});
});

View 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,
},
});

View 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"],
},
});

View 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,
},
});

View File

@@ -0,0 +1,4 @@
Schemas.Skill = new SimpleSchema({
//attribute name that this skill used as base mod for roll
ability: {type: String, defaultValue: ""},
});