rename /rpg-docs to /app
This commit is contained in:
7
app/Model/Campaign/Instance.js
Normal file
7
app/Model/Campaign/Instance.js
Normal file
@@ -0,0 +1,7 @@
|
||||
Instances = new Mongo.Collection("instances");
|
||||
|
||||
Schemas.Instance = new SimpleSchema({
|
||||
//an instance is a single flow of time all parties in an instance are in-sync time wise
|
||||
});
|
||||
|
||||
Instances.attachSchema(Schemas.Instance);
|
||||
42
app/Model/Campaign/Party.js
Normal file
42
app/Model/Campaign/Party.js
Normal file
@@ -0,0 +1,42 @@
|
||||
Parties = new Mongo.Collection("parties");
|
||||
|
||||
Schemas.Party = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
defaultValue: "New Party",
|
||||
trim: false,
|
||||
optional: true,
|
||||
},
|
||||
characters: {
|
||||
type: [String],
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
defaultValue: [],
|
||||
},
|
||||
owner: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
});
|
||||
|
||||
Parties.attachSchema(Schemas.Party);
|
||||
|
||||
Parties.allow({
|
||||
insert: function(userId, doc) {
|
||||
return userId && doc.owner === userId;
|
||||
},
|
||||
update: function(userId, doc, fields, modifier) {
|
||||
return userId && doc.owner === userId;
|
||||
},
|
||||
remove: function(userId, doc) {
|
||||
return userId && doc.owner === userId;
|
||||
},
|
||||
fetch: ["owner"],
|
||||
});
|
||||
|
||||
Parties.deny({
|
||||
update: function(userId, docs, fields, modifier) {
|
||||
// can't change owners
|
||||
return _.contains(fields, "owner");
|
||||
}
|
||||
});
|
||||
40
app/Model/Character/Actions.js
Normal file
40
app/Model/Character/Actions.js
Normal file
@@ -0,0 +1,40 @@
|
||||
Actions = new Mongo.Collection("actions");
|
||||
|
||||
/*
|
||||
* Actions are given to a character by items and features
|
||||
*/
|
||||
Schemas.Action = 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(Schemas.Action);
|
||||
|
||||
Actions.attachBehaviour("softRemovable");
|
||||
makeChild(Actions);
|
||||
|
||||
Actions.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
Actions.deny(CHARACTER_SUBSCHEMA_DENY);
|
||||
87
app/Model/Character/Attacks.js
Normal file
87
app/Model/Character/Attacks.js
Normal file
@@ -0,0 +1,87 @@
|
||||
Attacks = new Mongo.Collection("attacks");
|
||||
|
||||
/*
|
||||
* Attacks are given to a character by items and features
|
||||
*/
|
||||
Schemas.Attack = 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(Schemas.Attack);
|
||||
|
||||
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);
|
||||
67
app/Model/Character/Buffs.js
Normal file
67
app/Model/Character/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);
|
||||
589
app/Model/Character/Characters.js
Normal file
589
app/Model/Character/Characters.js
Normal file
@@ -0,0 +1,589 @@
|
||||
//set up the collection for characters
|
||||
Characters = new Mongo.Collection("characters");
|
||||
|
||||
Schemas.Character = 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},
|
||||
|
||||
//attributes
|
||||
//ability scores
|
||||
strength: {type: Schemas.Attribute},
|
||||
dexterity: {type: Schemas.Attribute},
|
||||
constitution: {type: Schemas.Attribute},
|
||||
intelligence: {type: Schemas.Attribute},
|
||||
wisdom: {type: Schemas.Attribute},
|
||||
charisma: {type: Schemas.Attribute},
|
||||
|
||||
//stats
|
||||
hitPoints: {type: Schemas.Attribute},
|
||||
tempHP: {type: Schemas.Attribute},
|
||||
experience: {type: Schemas.Attribute},
|
||||
proficiencyBonus: {type: Schemas.Attribute},
|
||||
speed: {type: Schemas.Attribute},
|
||||
weight: {type: Schemas.Attribute},
|
||||
age: {type: Schemas.Attribute},
|
||||
ageRate: {type: Schemas.Attribute},
|
||||
armor: {type: Schemas.Attribute},
|
||||
carryMultiplier: {type: Schemas.Attribute},
|
||||
|
||||
//resources
|
||||
level1SpellSlots: {type: Schemas.Attribute},
|
||||
level2SpellSlots: {type: Schemas.Attribute},
|
||||
level3SpellSlots: {type: Schemas.Attribute},
|
||||
level4SpellSlots: {type: Schemas.Attribute},
|
||||
level5SpellSlots: {type: Schemas.Attribute},
|
||||
level6SpellSlots: {type: Schemas.Attribute},
|
||||
level7SpellSlots: {type: Schemas.Attribute},
|
||||
level8SpellSlots: {type: Schemas.Attribute},
|
||||
level9SpellSlots: {type: Schemas.Attribute},
|
||||
ki: {type: Schemas.Attribute},
|
||||
sorceryPoints: {type: Schemas.Attribute},
|
||||
rages: {type: Schemas.Attribute},
|
||||
superiorityDice: {type: Schemas.Attribute},
|
||||
expertiseDice: {type: Schemas.Attribute},
|
||||
|
||||
//specific features
|
||||
rageDamage: {type: Schemas.Attribute},
|
||||
|
||||
//hit dice
|
||||
d6HitDice: {type: Schemas.Attribute},
|
||||
d8HitDice: {type: Schemas.Attribute},
|
||||
d10HitDice: {type: Schemas.Attribute},
|
||||
d12HitDice: {type: Schemas.Attribute},
|
||||
|
||||
//vulnerabilities
|
||||
acidMultiplier: {type: Schemas.Attribute},
|
||||
bludgeoningMultiplier: {type: Schemas.Attribute},
|
||||
coldMultiplier: {type: Schemas.Attribute},
|
||||
fireMultiplier: {type: Schemas.Attribute},
|
||||
forceMultiplier: {type: Schemas.Attribute},
|
||||
lightningMultiplier: {type: Schemas.Attribute},
|
||||
necroticMultiplier: {type: Schemas.Attribute},
|
||||
piercingMultiplier: {type: Schemas.Attribute},
|
||||
poisonMultiplier: {type: Schemas.Attribute},
|
||||
psychicMultiplier: {type: Schemas.Attribute},
|
||||
radiantMultiplier: {type: Schemas.Attribute},
|
||||
slashingMultiplier: {type: Schemas.Attribute},
|
||||
thunderMultiplier: {type: Schemas.Attribute},
|
||||
|
||||
//skills
|
||||
//saves
|
||||
strengthSave: {type: Schemas.Skill},
|
||||
"strengthSave.ability": {type: String, defaultValue: "strength"},
|
||||
|
||||
dexteritySave: {type: Schemas.Skill},
|
||||
"dexteritySave.ability": {type: String, defaultValue: "dexterity"},
|
||||
|
||||
constitutionSave:{type: Schemas.Skill},
|
||||
"constitutionSave.ability": {type: String, defaultValue: "constitution"},
|
||||
|
||||
intelligenceSave:{type: Schemas.Skill},
|
||||
"intelligenceSave.ability": {type: String, defaultValue: "intelligence"},
|
||||
|
||||
wisdomSave: {type: Schemas.Skill},
|
||||
"wisdomSave.ability": {type: String, defaultValue: "wisdom"},
|
||||
|
||||
charismaSave: {type: Schemas.Skill},
|
||||
"charismaSave.ability": {type: String, defaultValue: "charisma"},
|
||||
|
||||
//skill skills
|
||||
acrobatics: {type: Schemas.Skill},
|
||||
"acrobatics.ability": {type: String, defaultValue: "dexterity"},
|
||||
|
||||
animalHandling: {type: Schemas.Skill},
|
||||
"animalHandling.ability": {type: String, defaultValue: "wisdom"},
|
||||
|
||||
arcana: {type: Schemas.Skill},
|
||||
"arcana.ability": {type: String, defaultValue: "intelligence"},
|
||||
|
||||
athletics: {type: Schemas.Skill},
|
||||
"athletics.ability": {type: String, defaultValue: "strength"},
|
||||
|
||||
deception: {type: Schemas.Skill},
|
||||
"deception.ability": {type: String, defaultValue: "charisma"},
|
||||
|
||||
history: {type: Schemas.Skill},
|
||||
"history.ability": {type: String, defaultValue: "intelligence"},
|
||||
|
||||
insight: {type: Schemas.Skill},
|
||||
"insight.ability": {type: String, defaultValue: "wisdom"},
|
||||
|
||||
intimidation: {type: Schemas.Skill},
|
||||
"intimidation.ability": {type: String, defaultValue: "charisma"},
|
||||
|
||||
investigation: {type: Schemas.Skill},
|
||||
"investigation.ability": {type: String, defaultValue: "intelligence"},
|
||||
|
||||
medicine: {type: Schemas.Skill},
|
||||
"medicine.ability": {type: String, defaultValue: "wisdom"},
|
||||
|
||||
nature: {type: Schemas.Skill},
|
||||
"nature.ability": {type: String, defaultValue: "intelligence"},
|
||||
|
||||
perception: {type: Schemas.Skill},
|
||||
"perception.ability": {type: String, defaultValue: "wisdom"},
|
||||
|
||||
performance: {type: Schemas.Skill},
|
||||
"performance.ability": {type: String, defaultValue: "charisma"},
|
||||
|
||||
persuasion: {type: Schemas.Skill},
|
||||
"persuasion.ability": {type: String, defaultValue: "charisma"},
|
||||
|
||||
religion: {type: Schemas.Skill},
|
||||
"religion.ability": {type: String, defaultValue: "intelligence"},
|
||||
|
||||
sleightOfHand: {type: Schemas.Skill},
|
||||
"sleightOfHand.ability": {type: String, defaultValue: "dexterity"},
|
||||
|
||||
stealth: {type: Schemas.Skill},
|
||||
"stealth.ability": {type: String, defaultValue: "dexterity"},
|
||||
|
||||
survival: {type: Schemas.Skill},
|
||||
"survival.ability": {type: String, defaultValue: "wisdom"},
|
||||
|
||||
//Mechanical Skills
|
||||
initiative: {type: Schemas.Skill},
|
||||
"initiative.ability": {type: String, defaultValue: "dexterity"},
|
||||
|
||||
dexterityArmor: {type: Schemas.Skill},
|
||||
"dexterityArmor.ability": {type: String, defaultValue: "dexterity"},
|
||||
|
||||
//mechanics
|
||||
deathSave: {type: Schemas.DeathSave},
|
||||
|
||||
//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-character 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},
|
||||
});
|
||||
|
||||
Characters.attachSchema(Schemas.Character);
|
||||
|
||||
var attributeBase = preventLoop(function(charId, statName){
|
||||
check(statName, String);
|
||||
//if it's a damage multiplier, we treat it specially
|
||||
if (_.contains(DAMAGE_MULTIPLIERS, statName)){
|
||||
var invulnerabilityCount = Effects.find({
|
||||
charId: charId,
|
||||
stat: statName,
|
||||
enabled: true,
|
||||
operation: "mul",
|
||||
value: 0,
|
||||
}).count();
|
||||
if (invulnerabilityCount) return 0;
|
||||
var resistCount = Effects.find({
|
||||
charId: charId,
|
||||
stat: statName,
|
||||
enabled: true,
|
||||
operation: "mul",
|
||||
value: 0.5,
|
||||
}).count();
|
||||
var vulnCount = Effects.find({
|
||||
charId: charId,
|
||||
stat: statName,
|
||||
enabled: true,
|
||||
operation: "mul",
|
||||
value: 2,
|
||||
}).count();
|
||||
if (!resistCount && !vulnCount){
|
||||
return 1;
|
||||
} else if (resistCount && !vulnCount){
|
||||
return 0.5;
|
||||
} else if (!resistCount && vulnCount){
|
||||
return 2;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
var value;
|
||||
var base = 0;
|
||||
var add = 0;
|
||||
var mul = 1;
|
||||
var min = Number.NEGATIVE_INFINITY;
|
||||
var max = Number.POSITIVE_INFINITY;
|
||||
|
||||
Effects.find({
|
||||
charId: charId,
|
||||
stat: statName,
|
||||
enabled: true,
|
||||
operation: {$in: ["base", "add", "mul", "min", "max"]},
|
||||
}).forEach(function(effect) {
|
||||
value = evaluateEffect(charId, effect);
|
||||
if (effect.operation === "base"){
|
||||
if (value > base) base = value;
|
||||
} else if (effect.operation === "add"){
|
||||
add += value;
|
||||
} else if (effect.operation === "mul"){
|
||||
mul *= value;
|
||||
} else if (effect.operation === "min"){
|
||||
if (value > min) min = value;
|
||||
} else if (effect.operation === "max"){
|
||||
if (value < max) max = value;
|
||||
}
|
||||
});
|
||||
|
||||
var result = (base + add) * mul;
|
||||
if (result < min) result = min;
|
||||
if (result > max) result = max;
|
||||
// Don't round carry multiplier
|
||||
if (statName === "carryMultiplier"){
|
||||
return result;
|
||||
}
|
||||
return Math.floor(result);
|
||||
});
|
||||
|
||||
if (Meteor.isClient) {
|
||||
Template.registerHelper("characterCalculate", function(func, charId, input) {
|
||||
try {
|
||||
return Characters.calculate[func](charId, input);
|
||||
} catch (e){
|
||||
if (!Characters.calculate[func]){
|
||||
throw new Error(func + "is not a function name");
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//create a local memoize with a argument concatenating hash function
|
||||
var memoize = function(f) {
|
||||
if (Meteor.isServer) return f;
|
||||
return Tracker.memoize(f, function() {
|
||||
return _.reduce(arguments, function(memo, arg) {
|
||||
return memo + arg;
|
||||
}, "");
|
||||
});
|
||||
};
|
||||
|
||||
//memoize funcitons that have finds and slow loops
|
||||
Characters.calculate = {
|
||||
getField: function(charId, fieldName) {
|
||||
var fieldSelector = {};
|
||||
fieldSelector[fieldName] = 1;
|
||||
var char = Characters.findOne(charId, {fields: fieldSelector});
|
||||
if (!char) return;
|
||||
var field = char[fieldName];
|
||||
if (field === undefined){
|
||||
throw new Meteor.Error(
|
||||
"getField failed",
|
||||
"getField could not find field " +
|
||||
fieldName +
|
||||
" in character " +
|
||||
char._id
|
||||
);
|
||||
}
|
||||
return field;
|
||||
},
|
||||
fieldValue: function(charId, fieldName) {
|
||||
if (!Schemas.Character.schema(fieldName)){
|
||||
throw new Meteor.Error(
|
||||
"Field not found",
|
||||
"Character's schema does not contain a field called: " + fieldName
|
||||
);
|
||||
}
|
||||
//duck typing to get the right value function
|
||||
//.ability implies skill
|
||||
if (Schemas.Character.schema(fieldName + ".ability")){
|
||||
return Characters.calculate.skillMod(charId, fieldName);
|
||||
}
|
||||
//adjustment implies attribute
|
||||
if (Schemas.Character.schema(fieldName + ".adjustment")){
|
||||
return Characters.calculate.attributeValue(charId, fieldName);
|
||||
}
|
||||
//fall back to just returning the field itself
|
||||
return Characters.calculate.getField(charId, fieldName);
|
||||
},
|
||||
attributeValue: memoize(function(charId, attributeName){
|
||||
var attribute = Characters.calculate.getField(charId, attributeName);
|
||||
if (!attribute) return;
|
||||
//base value
|
||||
var value = Characters.calculate.attributeBase(charId, attributeName);
|
||||
//plus adjustment
|
||||
value += attribute.adjustment;
|
||||
return value;
|
||||
}),
|
||||
attributeBase: memoize(function(charId, attributeName){
|
||||
return attributeBase(charId, attributeName);
|
||||
}),
|
||||
skillMod: memoize(preventLoop(function(charId, skillName){
|
||||
var skill = Characters.calculate.getField(charId, skillName);
|
||||
if (!skill) return;
|
||||
//get the final value of the ability score
|
||||
var ability = Characters.calculate.attributeValue(charId, skill.ability);
|
||||
|
||||
//base modifier
|
||||
var mod = +getMod(ability);
|
||||
|
||||
//multiply proficiency bonus by largest value in proficiency array
|
||||
var prof = Characters.calculate.proficiency(charId, skillName);
|
||||
|
||||
//add multiplied proficiency bonus to modifier
|
||||
mod += prof * Characters.calculate.attributeValue(charId, "proficiencyBonus");
|
||||
|
||||
//apply all effects
|
||||
var value;
|
||||
var add = 0;
|
||||
var mul = 1;
|
||||
var min = Number.NEGATIVE_INFINITY;
|
||||
var max = Number.POSITIVE_INFINITY;
|
||||
|
||||
Effects.find({
|
||||
charId: charId,
|
||||
stat: skillName,
|
||||
enabled: true,
|
||||
operation: {$in: ["base", "add", "mul", "min", "max"]},
|
||||
}).forEach(function(effect) {
|
||||
value = evaluateEffect(charId, effect);
|
||||
if (effect.operation === "add"){
|
||||
add += value;
|
||||
} else if (effect.operation === "mul"){
|
||||
mul *= value;
|
||||
} else if (effect.operation === "min"){
|
||||
if (value > min) min = value;
|
||||
} else if (effect.operation === "max"){
|
||||
if (value < max) max = value;
|
||||
}
|
||||
});
|
||||
var result = (mod + add) * mul;
|
||||
if (result < min) result = min;
|
||||
if (result > max) result = max;
|
||||
|
||||
return Math.floor(result);
|
||||
})),
|
||||
proficiency: memoize(function(charId, skillName){
|
||||
//return largest value in proficiency array
|
||||
var prof = Proficiencies.findOne(
|
||||
{charId: charId, name: skillName, enabled: true},
|
||||
{sort: {value: -1}}
|
||||
);
|
||||
return prof && prof.value || 0;
|
||||
}),
|
||||
passiveSkill: memoize(function(charId, skillName){
|
||||
var mod = +Characters.calculate.skillMod(charId, skillName);
|
||||
var value = 10 + mod;
|
||||
Effects.find(
|
||||
{charId: charId, stat: skillName, enabled: true, operation: "passiveAdd"}
|
||||
).forEach(function(effect){
|
||||
value += evaluateEffect(charId, effect);
|
||||
});
|
||||
var advantage = Characters.calculate.advantage(charId, skillName);
|
||||
value += 5 * advantage;
|
||||
return Math.floor(value);
|
||||
}),
|
||||
advantage: memoize(function(charId, skillName){
|
||||
var advantage = Effects.find(
|
||||
{charId: charId, stat: skillName, enabled: true, operation: "advantage"}
|
||||
).count();
|
||||
var disadvantage = Effects.find(
|
||||
{charId: charId, stat: skillName, enabled: true, operation: "disadvantage"}
|
||||
).count();
|
||||
if (advantage && !disadvantage) return 1;
|
||||
if (disadvantage && !advantage) return -1;
|
||||
return 0;
|
||||
}),
|
||||
abilityMod: function(charId, attribute){
|
||||
return getMod(
|
||||
Characters.calculate.attributeValue(charId, attribute)
|
||||
);
|
||||
},
|
||||
passiveAbility: function(charId, attribute){
|
||||
var mod = +getMod(Characters.calculate.attributeValue(charId, attribute));
|
||||
return 10 + mod;
|
||||
},
|
||||
xpLevel: function(charId){
|
||||
var xp = Characters.calculate.experience(charId);
|
||||
for (var i = 0; i < 19; i++){
|
||||
if (xp < XP_TABLE[i]){
|
||||
return i;
|
||||
}
|
||||
}
|
||||
if (xp > 355000) return 20;
|
||||
return 0;
|
||||
},
|
||||
level: memoize(function(charId){
|
||||
var level = 0;
|
||||
Classes.find({charId: charId}).forEach(function(cls){
|
||||
level += cls.level;
|
||||
});
|
||||
return level;
|
||||
}),
|
||||
experience: memoize(function(charId){
|
||||
var xp = 0;
|
||||
Experiences.find(
|
||||
{charId: charId},
|
||||
{fields: {value: 1}}
|
||||
).forEach(function(e){
|
||||
xp += e.value;
|
||||
});
|
||||
return xp;
|
||||
}),
|
||||
};
|
||||
|
||||
var deprecated = function() {
|
||||
//var err = new Error("this function has been deprecated");
|
||||
var name = "";
|
||||
if (Template.instance()){
|
||||
name = Template.instance().view.name;
|
||||
}
|
||||
var logString = "this function has been deprecated \n";
|
||||
if (name){
|
||||
logString += "View: " + name + "\n\n";
|
||||
}
|
||||
//logString += err.stack + "\n\n---------------------\n\n";
|
||||
console.log(logString);
|
||||
};
|
||||
|
||||
//functions and calculated values.
|
||||
//These functions can only rely on this._id since no other
|
||||
//field is likely to be attached to all returned characters
|
||||
Characters.helpers({
|
||||
//returns the value stored in the field requested
|
||||
//will set up dependencies on just that field
|
||||
getField : function(fieldName){
|
||||
deprecated();
|
||||
return Characters.calculate.getField(this._id, fieldName);
|
||||
},
|
||||
//returns the value of a field
|
||||
fieldValue : function(fieldName){
|
||||
deprecated();
|
||||
return Characters.calculate.fieldValue(this._id, fieldName);
|
||||
},
|
||||
attributeValue: function(attributeName){
|
||||
deprecated();
|
||||
return Characters.calculate.attributeValue(this._id, attributeName);
|
||||
},
|
||||
attributeBase: function(attributeName){
|
||||
deprecated();
|
||||
return Characters.calculate.attributeBase(this._id, attributeName);
|
||||
},
|
||||
skillMod: function(skillName){
|
||||
deprecated();
|
||||
return Characters.calculate.skillMod(this._id, skillName);
|
||||
},
|
||||
proficiency: function(skillName){
|
||||
deprecated();
|
||||
return Characters.calculate.proficiency(this._id, skillName);
|
||||
},
|
||||
passiveSkill: function(skillName){
|
||||
deprecated();
|
||||
return Characters.calculate.passiveSkill(this._id, skillName);
|
||||
},
|
||||
advantage: function(skillName){
|
||||
deprecated();
|
||||
return Characters.calculate.advantage(this._id, skillName);
|
||||
},
|
||||
abilityMod: function(attribute){
|
||||
deprecated();
|
||||
return Characters.calculate.abilityMod(this._id, attribute);
|
||||
},
|
||||
passiveAbility: function(attribute){
|
||||
deprecated();
|
||||
return Characters.calculate.passiveAbility(this._id, attribute);
|
||||
},
|
||||
xpLevel: function(){
|
||||
deprecated();
|
||||
return Characters.calculate.xpLevel(this._id);
|
||||
},
|
||||
level: function(){
|
||||
deprecated();
|
||||
return Characters.calculate.level(this._id);
|
||||
},
|
||||
experience: function(){
|
||||
deprecated();
|
||||
return Characters.calculate.experience(this._id);
|
||||
},
|
||||
});
|
||||
|
||||
//clean up all data related to that character before removing it
|
||||
if (Meteor.isServer){
|
||||
Characters.after.remove(function(userId, character) {
|
||||
Actions .remove({charId: character._id});
|
||||
Attacks .remove({charId: character._id});
|
||||
Buffs .remove({charId: character._id});
|
||||
Classes .remove({charId: character._id});
|
||||
CustomBuffs .remove({charId: character._id});
|
||||
Effects .remove({charId: character._id});
|
||||
Experiences .remove({charId: character._id});
|
||||
Features .remove({charId: character._id});
|
||||
Notes .remove({charId: character._id});
|
||||
Proficiencies .remove({charId: character._id});
|
||||
SpellLists .remove({charId: character._id});
|
||||
Items .remove({charId: character._id});
|
||||
Containers .remove({charId: character._id});
|
||||
});
|
||||
Characters.after.update(function(userId, doc, fieldNames, modifier, options) {
|
||||
if (_.contains(fieldNames, "name")){
|
||||
var urlName = getSlug(doc.name, {maintainCase: true}) || "-";
|
||||
Characters.update(doc._id, {$set: {urlName}});
|
||||
}
|
||||
});
|
||||
Characters.before.insert(function(userId, doc) {
|
||||
doc.urlName = getSlug(doc.name, {maintainCase: true}) || "-";
|
||||
// The first character a user creates should have the new user experience
|
||||
if (!Characters.find({owner: userId}).count()){
|
||||
doc.settings.newUserExperience = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Characters.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"],
|
||||
});
|
||||
|
||||
Characters.deny({
|
||||
update: function(userId, docs, fields, modifier) {
|
||||
// can't change owners
|
||||
return _.contains(fields, "owner");
|
||||
}
|
||||
});
|
||||
32
app/Model/Character/Classes.js
Normal file
32
app/Model/Character/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/Model/Character/Conditions.js
Normal file
42
app/Model/Character/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);
|
||||
53
app/Model/Character/CustomBuffs.js
Normal file
53
app/Model/Character/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);
|
||||
132
app/Model/Character/Effects.js
Normal file
132
app/Model/Character/Effects.js
Normal file
@@ -0,0 +1,132 @@
|
||||
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",
|
||||
"proficiency",
|
||||
"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/Model/Character/Experience.js
Normal file
27
app/Model/Character/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/Model/Character/Features.js
Normal file
115
app/Model/Character/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/Model/Character/Notes.js
Normal file
19
app/Model/Character/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/Model/Character/Proficiencies.js
Normal file
37
app/Model/Character/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);
|
||||
37
app/Model/Character/SpellLists.js
Normal file
37
app/Model/Character/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/Model/Character/Spells.js
Normal file
252
app/Model/Character/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);
|
||||
},
|
||||
});
|
||||
21
app/Model/Character/SubSchemas/Adjustment.js
Normal file
21
app/Model/Character/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/Model/Character/SubSchemas/Attribute.js
Normal file
13
app/Model/Character/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/Model/Character/SubSchemas/DeathSaves.js
Normal file
22
app/Model/Character/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/Model/Character/SubSchemas/Skill.js
Normal file
4
app/Model/Character/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: ""},
|
||||
});
|
||||
41
app/Model/Character/TemporaryHitPoints.js
Normal file
41
app/Model/Character/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);
|
||||
56
app/Model/Inventory/Containers.js
Normal file
56
app/Model/Inventory/Containers.js
Normal file
@@ -0,0 +1,56 @@
|
||||
//set up the collection for containers
|
||||
Containers = new Mongo.Collection("containers");
|
||||
|
||||
Schemas.Container = new SimpleSchema({
|
||||
name: {type: String, optional: true, trim: false},
|
||||
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
isCarried: {type: Boolean},
|
||||
weight: {type: Number, min: 0, defaultValue: 0, decimal: true},
|
||||
value: {type: Number, min: 0, defaultValue: 0, decimal: true},
|
||||
description:{type: String, optional: true, trim: false},
|
||||
color: {
|
||||
type: String,
|
||||
allowedValues: _.pluck(colorOptions, "key"),
|
||||
defaultValue: "q",
|
||||
},
|
||||
});
|
||||
|
||||
Containers.attachSchema(Schemas.Container);
|
||||
|
||||
Containers.helpers({
|
||||
contentsValue: function(){
|
||||
var value = 0;
|
||||
Items.find(
|
||||
{"parent.id": this._id},
|
||||
{fields: {quantity: 1, value: 1}}
|
||||
).forEach(function(item){
|
||||
value += item.totalValue();
|
||||
});
|
||||
return value;
|
||||
},
|
||||
totalValue: function(){
|
||||
return this.contentsValue() + this.value;
|
||||
},
|
||||
contentsWeight: function(){
|
||||
var weight = 0;
|
||||
Items.find(
|
||||
{"parent.id": this._id},
|
||||
{fields: {quantity: 1, weight: 1}}
|
||||
).forEach(function(item){
|
||||
weight += item.totalWeight();
|
||||
});
|
||||
return weight;
|
||||
},
|
||||
totalWeight: function(){
|
||||
return this.contentsWeight() + this.weight;
|
||||
},
|
||||
moveToCharacter: function(characterId){
|
||||
if (this.charId === characterId) return;
|
||||
Items.update(this._id, {$set: {charId: characterId}});
|
||||
},
|
||||
});
|
||||
|
||||
Containers.attachBehaviour("softRemovable");
|
||||
makeParent(Containers); //parents of items
|
||||
|
||||
Containers.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
268
app/Model/Inventory/Items.js
Normal file
268
app/Model/Inventory/Items.js
Normal file
@@ -0,0 +1,268 @@
|
||||
Items = new Mongo.Collection("items");
|
||||
|
||||
Schemas.Item = new SimpleSchema({
|
||||
name: {type: String, optional: true, trim: false, defaultValue: "New Item"},
|
||||
plural: {type: String, optional: true, trim: false},
|
||||
description:{type: String, optional: true, trim: false},
|
||||
charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, //id of owner
|
||||
quantity: {type: Number, min: 0, defaultValue: 1},
|
||||
weight: {type: Number, min: 0, defaultValue: 0, decimal: true},
|
||||
value: {type: Number, min: 0, defaultValue: 0, decimal: true},
|
||||
enabled: {type: Boolean, defaultValue: false},
|
||||
requiresAttunement: {type: Boolean, defaultValue: false},
|
||||
"settings.showIncrement": {type: Boolean, defaultValue: false},
|
||||
color: {
|
||||
type: String,
|
||||
allowedValues: _.pluck(colorOptions, "key"),
|
||||
defaultValue: "q",
|
||||
},
|
||||
});
|
||||
|
||||
Items.attachSchema(Schemas.Item);
|
||||
|
||||
var checkMovePermission = function(itemId, parent) {
|
||||
var item = Items.findOne(itemId);
|
||||
if (!item)
|
||||
throw new Meteor.Error("No such item",
|
||||
"An item could not be found to move");
|
||||
//handle permissions
|
||||
var permission = Meteor.call("canWriteCharacter", item.charId);
|
||||
if (!permission){
|
||||
throw new Meteor.Error("Access denied",
|
||||
"Not permitted to move items 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 items 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 items to this character");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var moveItem = function(itemId, enable, parentCollection, parentId) {
|
||||
var item = Items.findOne(itemId);
|
||||
if (!item) return;
|
||||
parentCollection = parentCollection || item.parent.collection;
|
||||
parentId = parentId || item.parent.id;
|
||||
|
||||
if (Meteor.isServer) {
|
||||
checkMovePermission(itemId, {collection: parentCollection, id: parentId});
|
||||
}
|
||||
|
||||
//update the item provided the update will actually change something
|
||||
if (
|
||||
item.parent.collection !== parentCollection ||
|
||||
item.parent.id !== parentId ||
|
||||
item.enabled !== enable
|
||||
){
|
||||
Items.update(
|
||||
itemId,
|
||||
{$set: {
|
||||
"parent.collection": parentCollection,
|
||||
"parent.id": parentId,
|
||||
enabled: enable,
|
||||
}}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Meteor.methods({
|
||||
moveItemToParent: function(itemId, parent) {
|
||||
check(itemId, String);
|
||||
check(parent, {collection: String, id: String});
|
||||
moveItem(itemId, false, parent.collection, parent.id);
|
||||
},
|
||||
moveItemToCharacter: function(itemId, charId) {
|
||||
check(itemId, String);
|
||||
check(charId, String);
|
||||
moveItem(itemId, false, "Characters", charId);
|
||||
},
|
||||
moveItemToContainer: function(itemId, containerId) {
|
||||
check(itemId, String);
|
||||
check(containerId, String);
|
||||
moveItem(itemId, false, "Containers", containerId);
|
||||
},
|
||||
equipItem: function(itemId, charId){
|
||||
check(itemId, String);
|
||||
check(charId, String);
|
||||
moveItem(itemId, true, "Characters", charId);
|
||||
},
|
||||
unequipItem: function(itemId, charId){
|
||||
check(itemId, String);
|
||||
check(charId, String);
|
||||
moveItem(itemId, false, "Characters", charId);
|
||||
},
|
||||
splitItemToParent: function(itemId, moveQuantity, parent){
|
||||
check(itemId, String);
|
||||
check(moveQuantity, Number);
|
||||
check(parent, {id: String, collection: String});
|
||||
|
||||
//get the item
|
||||
var item = Items.findOne(itemId);
|
||||
if (!item) return;
|
||||
|
||||
//don't bother moving nothing
|
||||
if (moveQuantity <= 0 || item.quantity <= 0){
|
||||
return;
|
||||
}
|
||||
//ensure we are only moving up to the current stack size
|
||||
if (item.quantity < moveQuantity){
|
||||
moveQuantity = this.quantity;
|
||||
}
|
||||
|
||||
if (Meteor.isServer) {
|
||||
checkMovePermission(itemId, parent);
|
||||
}
|
||||
|
||||
//create a new item stack
|
||||
var newStack = _.omit(EJSON.clone(item), "_id");
|
||||
newStack.parent = parent;
|
||||
newStack.quantity = moveQuantity;
|
||||
|
||||
//find out if we have an exact replica in the destination
|
||||
var query = _.omit(newStack, ["parent", "quantity"]);
|
||||
query["parent.collection"] = newStack.parent.collection;
|
||||
query["parent.id"] = newStack.parent.id;
|
||||
query._id = {$ne: itemId}; //make sure we don't join it to itself
|
||||
var existingStack = Items.findOne(query);
|
||||
if (existingStack){
|
||||
//increase the existing stack's size
|
||||
Items.update(
|
||||
existingStack._id,
|
||||
{$inc: {quantity: moveQuantity}}
|
||||
);
|
||||
} else {
|
||||
//insert the new stack
|
||||
Items.insert(newStack, function(err, id){
|
||||
if (err) throw err;
|
||||
//copy the children also
|
||||
Meteor.call("cloneChildren", item._id, {collection: "Items", id: id});
|
||||
});
|
||||
}
|
||||
|
||||
//reduce the old stack's size
|
||||
var oldQuantity = item.quantity - moveQuantity;
|
||||
if (oldQuantity === 0){
|
||||
Items.remove(itemId);
|
||||
} else {
|
||||
Items.update(itemId, {$set: {quantity: oldQuantity}});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Items.helpers({
|
||||
totalValue: function(){
|
||||
return this.value * this.quantity;
|
||||
},
|
||||
totalWeight: function(){
|
||||
return this.weight * this.quantity;
|
||||
},
|
||||
pluralName: function(){
|
||||
if (this.plural && this.quantity !== 1){
|
||||
return this.plural;
|
||||
} else {
|
||||
return this.name;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Items.before.update(function(userId, doc, fieldNames, modifier, options){
|
||||
if (
|
||||
modifier && modifier.$set && modifier.$set.enabled && //we are equipping this item
|
||||
!(
|
||||
modifier.$set["parent.collection"] === "Characters" &&
|
||||
modifier.$set["parent.id"]
|
||||
) //and we haven"t specified a character to equip to
|
||||
){
|
||||
//equip it to the current character
|
||||
modifier.$set["parent.collection"] = "Characters";
|
||||
modifier.$set["parent.id"] = doc.charId;
|
||||
}
|
||||
});
|
||||
|
||||
Items.attachBehaviour("softRemovable");
|
||||
makeChild(Items); //children of containers
|
||||
makeParent(Items, ["name", "enabled"]); //parents of effects and attacks
|
||||
|
||||
Items.allow(CHARACTER_SUBSCHEMA_ALLOW);
|
||||
|
||||
//give characters default items
|
||||
Characters.after.insert(function(userId, char) {
|
||||
if (Meteor.isServer){
|
||||
var containerId = Containers.insert({
|
||||
name: "Coin Pouch",
|
||||
charId: char._id,
|
||||
isCarried: true,
|
||||
description: "A sturdy pouch for coins",
|
||||
color: "d",
|
||||
});
|
||||
Items.insert({
|
||||
name: "Gold piece",
|
||||
plural: "Gold pieces",
|
||||
charId: char._id,
|
||||
quantity: 0,
|
||||
weight: 0.02,
|
||||
value: 1,
|
||||
color: "n",
|
||||
parent: {
|
||||
id: containerId,
|
||||
collection: "Containers",
|
||||
},
|
||||
settings: {
|
||||
showIncrement: true,
|
||||
},
|
||||
});
|
||||
Items.insert({
|
||||
name: "Silver piece",
|
||||
plural: "Silver pieces",
|
||||
charId: char._id,
|
||||
quantity: 0,
|
||||
weight: 0.02,
|
||||
value: 0.1,
|
||||
color: "q",
|
||||
parent: {
|
||||
id: containerId,
|
||||
collection: "Containers",
|
||||
},
|
||||
settings: {
|
||||
showIncrement: true,
|
||||
},
|
||||
});
|
||||
Items.insert({
|
||||
name: "Copper piece",
|
||||
plural: "Copper pieces",
|
||||
charId: char._id,
|
||||
quantity: 0,
|
||||
weight: 0.02,
|
||||
value: 0.01,
|
||||
color: "s",
|
||||
parent: {
|
||||
id: containerId,
|
||||
collection: "Containers",
|
||||
},
|
||||
settings: {
|
||||
showIncrement: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
47
app/Model/Library/Library.js
Normal file
47
app/Model/Library/Library.js
Normal file
@@ -0,0 +1,47 @@
|
||||
Libraries = new Mongo.Collection("library");
|
||||
|
||||
Schemas.Library = new SimpleSchema({
|
||||
name: {type: String},
|
||||
owner: {type: String, regEx: SimpleSchema.RegEx.Id},
|
||||
readers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: []},
|
||||
writers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: []},
|
||||
public: {type: Boolean, defaultValue: false},
|
||||
});
|
||||
|
||||
Libraries.attachSchema(Schemas.Library);
|
||||
|
||||
Libraries.allow({
|
||||
insert(userId, doc) {
|
||||
return userId && doc.owner === userId;
|
||||
},
|
||||
update(userId, doc, fields, modifier) {
|
||||
return canEdit(userId, doc);
|
||||
},
|
||||
remove(userId, doc) {
|
||||
return canEdit(userId, doc);
|
||||
},
|
||||
fetch: ["owner", "writers"],
|
||||
});
|
||||
|
||||
Libraries.deny({
|
||||
// For now, only admins can manage libraries
|
||||
insert(userId, doc){
|
||||
var user = Meteor.users.findOne(userId);
|
||||
return !user || !_.contains(user.roles, "admin");
|
||||
},
|
||||
update(userId, doc, fields, modifier) {
|
||||
// Can't change owners
|
||||
return _.contains(fields, "owner")
|
||||
},
|
||||
fetch: [],
|
||||
});
|
||||
|
||||
const canEdit = function(userId, library){
|
||||
if (!userId || !library) return;
|
||||
return library.owner === userId || _.contains(library.writers, userId);
|
||||
};
|
||||
|
||||
Libraries.canEdit = function(userId, libraryId){
|
||||
const library = Libraries.findOne(libraryId);
|
||||
return canEdit(userId, library);
|
||||
};
|
||||
41
app/Model/Library/LibraryAttacks.js
Normal file
41
app/Model/Library/LibraryAttacks.js
Normal file
@@ -0,0 +1,41 @@
|
||||
Schemas.LibraryAttacks = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
defaultValue: "New Attack",
|
||||
trim: false,
|
||||
},
|
||||
details: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
attackBonus: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
damage: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
damageType: {
|
||||
type: String,
|
||||
allowedValues: [
|
||||
"bludgeoning",
|
||||
"piercing",
|
||||
"slashing",
|
||||
"acid",
|
||||
"cold",
|
||||
"fire",
|
||||
"force",
|
||||
"lightning",
|
||||
"necrotic",
|
||||
"poison",
|
||||
"psychic",
|
||||
"radiant",
|
||||
"thunder",
|
||||
],
|
||||
defaultValue: "slashing",
|
||||
},
|
||||
});
|
||||
40
app/Model/Library/LibraryEffects.js
Normal file
40
app/Model/Library/LibraryEffects.js
Normal file
@@ -0,0 +1,40 @@
|
||||
Schemas.LibraryEffects = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true, //TODO make necessary if there is no owner
|
||||
trim: false,
|
||||
},
|
||||
operation: {
|
||||
type: String,
|
||||
defaultValue: "add",
|
||||
allowedValues: [
|
||||
"base",
|
||||
"proficiency",
|
||||
"add",
|
||||
"mul",
|
||||
"min",
|
||||
"max",
|
||||
"advantage",
|
||||
"disadvantage",
|
||||
"passiveAdd",
|
||||
"fail",
|
||||
"conditional",
|
||||
],
|
||||
},
|
||||
// Effects either have a value OR a calculation
|
||||
value: {
|
||||
type: Number,
|
||||
decimal: true,
|
||||
optional: true,
|
||||
},
|
||||
calculation: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
},
|
||||
//which stat the effect is applied to
|
||||
stat: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
44
app/Model/Library/LibraryItems.js
Normal file
44
app/Model/Library/LibraryItems.js
Normal file
@@ -0,0 +1,44 @@
|
||||
LibraryItems = new Mongo.Collection("libraryItems");
|
||||
|
||||
Schemas.LibraryItems = new SimpleSchema({
|
||||
libraryName:{type: String, optional: true, trim: false},
|
||||
name: {type: String, defaultValue: "New Item", trim: false},
|
||||
plural: {type: String, optional: true, trim: false},
|
||||
description:{type: String, optional: true, trim: false},
|
||||
quantity: {type: Number, min: 0, defaultValue: 1},
|
||||
weight: {type: Number, min: 0, defaultValue: 0, decimal: true},
|
||||
value: {type: Number, min: 0, defaultValue: 0, decimal: true},
|
||||
requiresAttunement: {type: Boolean, defaultValue: false},
|
||||
|
||||
library: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
|
||||
"settings.category": {
|
||||
type: String,
|
||||
optional: true,
|
||||
allowedValues: [
|
||||
"adventuringGear", "armor", "weapons", "tools",
|
||||
],
|
||||
},
|
||||
"settings.showIncrement": {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
|
||||
effects: {type: [Schemas.LibraryEffects], defaultValue: []},
|
||||
attacks: {type: [Schemas.LibraryAttacks], defaultValue: []},
|
||||
});
|
||||
|
||||
LibraryItems.attachSchema(Schemas.LibraryItems);
|
||||
|
||||
LibraryItems.allow({
|
||||
insert(userId, doc) {
|
||||
return Libraries.canEdit(userId, doc.library);
|
||||
},
|
||||
update(userId, doc, fields, modifier) {
|
||||
return Libraries.canEdit(userId, doc.library);
|
||||
},
|
||||
remove(userId, doc) {
|
||||
return Libraries.canEdit(userId, doc.library);
|
||||
},
|
||||
fetch: ["library"],
|
||||
});
|
||||
66
app/Model/Library/LibrarySpells.js
Normal file
66
app/Model/Library/LibrarySpells.js
Normal file
@@ -0,0 +1,66 @@
|
||||
LibrarySpells = new Mongo.Collection("librarySpells");
|
||||
|
||||
Schemas.LibrarySpells = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
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,
|
||||
},
|
||||
library: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
effects: {type: [Schemas.LibraryEffects], defaultValue: []},
|
||||
attacks: {type: [Schemas.LibraryAttacks], defaultValue: []},
|
||||
});
|
||||
|
||||
LibrarySpells.attachSchema(Schemas.LibrarySpells);
|
||||
|
||||
LibrarySpells.allow({
|
||||
insert(userId, doc) {
|
||||
return Libraries.canEdit(userId, doc.library);
|
||||
},
|
||||
update(userId, doc, fields, modifier) {
|
||||
return Libraries.canEdit(userId, doc.library);
|
||||
},
|
||||
remove(userId, doc) {
|
||||
return Libraries.canEdit(userId, doc.library);
|
||||
},
|
||||
fetch: ["library"],
|
||||
});
|
||||
9
app/Model/Meta/Blacklist.js
Normal file
9
app/Model/Meta/Blacklist.js
Normal file
@@ -0,0 +1,9 @@
|
||||
Blacklist = new Mongo.Collection("blacklist");
|
||||
|
||||
Schemas.Blacklist = new SimpleSchema({
|
||||
userId: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
Blacklist.attachSchema(Schemas.Blacklist);
|
||||
27
app/Model/Meta/ChangeLogs.js
Normal file
27
app/Model/Meta/ChangeLogs.js
Normal file
@@ -0,0 +1,27 @@
|
||||
ChangeLogs = new Mongo.Collection("changeLogs");
|
||||
|
||||
Schemas.ChangeLog = new SimpleSchema({
|
||||
version: {
|
||||
type: String,
|
||||
},
|
||||
changes: {
|
||||
type: [String],
|
||||
},
|
||||
});
|
||||
|
||||
ChangeLogs.attachSchema(Schemas.ChangeLog);
|
||||
|
||||
ChangeLogs.allow({
|
||||
insert: function(userId, doc) {
|
||||
var user = Meteor.users.findOne(userId);
|
||||
if (user) return _.contains(user.roles, "admin");
|
||||
},
|
||||
update: function(userId, doc, fields, modifier) {
|
||||
var user = Meteor.users.findOne(userId);
|
||||
if (user) return _.contains(user.roles, "admin");
|
||||
},
|
||||
remove: function(userId, doc) {
|
||||
var user = Meteor.users.findOne(userId);
|
||||
if (user) return _.contains(user.roles, "admin");
|
||||
},
|
||||
});
|
||||
79
app/Model/Meta/Reports.js
Normal file
79
app/Model/Meta/Reports.js
Normal file
@@ -0,0 +1,79 @@
|
||||
Reports = new Mongo.Collection("reports");
|
||||
|
||||
Schemas.Report = new SimpleSchema({
|
||||
owner: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
trim: false,
|
||||
optional: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: false,
|
||||
optional: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
allowedValues: ["General Feedback", "Bug", "Suggested Change", "Feature Request"],
|
||||
defaultValue: "General Feedback",
|
||||
},
|
||||
//the immediate impact of doing this action (eg. -1 rages)
|
||||
severity: {
|
||||
type: Number,
|
||||
defaultValue: 5,
|
||||
min: 1,
|
||||
max: 10,
|
||||
},
|
||||
metaData: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
},
|
||||
});
|
||||
|
||||
Reports.attachSchema(Schemas.Report);
|
||||
|
||||
Meteor.methods({
|
||||
insertReport: function(report) {
|
||||
check(report, {
|
||||
title: String,
|
||||
description: String,
|
||||
type: String,
|
||||
severity: Number,
|
||||
metaData: Object,
|
||||
});
|
||||
report.owner = this.userId;
|
||||
var id = Reports.insert(report);
|
||||
var user = Meteor.users.findOne(this.userId);
|
||||
var sender = user &&
|
||||
user.emails &&
|
||||
user.emails[0] &&
|
||||
user.emails[0].address ||
|
||||
user.services &&
|
||||
user.services.google &&
|
||||
user.services.google.email ||
|
||||
"reports@dicecloud.com";
|
||||
var bodyText = "Report ID: " + id +
|
||||
"\nSeverity: " + report.severity +
|
||||
"\nType: " + report.type +
|
||||
"\n\n" + report.description;
|
||||
Email.send({
|
||||
from: sender,
|
||||
to: "stefan.zermatten@gmail.com",
|
||||
subject: "DiceCloud feedback - " + report.title,
|
||||
text: bodyText,
|
||||
});
|
||||
},
|
||||
deleteReport: function(id) {
|
||||
var user = Meteor.users.findOne(this.userId);
|
||||
if (!_.contains(user.roles, "admin")){
|
||||
throw new Meteor.Error(
|
||||
"not admin",
|
||||
"The user must be an administrator to delete feedback"
|
||||
);
|
||||
}
|
||||
Reports.remove(id);
|
||||
},
|
||||
});
|
||||
105
app/Model/Users/Users.js
Normal file
105
app/Model/Users/Users.js
Normal file
@@ -0,0 +1,105 @@
|
||||
Schemas.UserProfile = new SimpleSchema({
|
||||
username: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
Schemas.User = new SimpleSchema({
|
||||
username: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
profile: {
|
||||
type: Schemas.UserProfile,
|
||||
optional: true,
|
||||
},
|
||||
emails: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
"emails.$": {
|
||||
type: Object,
|
||||
},
|
||||
"emails.$.address": {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Email,
|
||||
},
|
||||
"emails.$.verified": {
|
||||
type: Boolean,
|
||||
},
|
||||
registered_emails: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
"registered_emails.$": {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: Date
|
||||
},
|
||||
services: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
blackbox: true,
|
||||
},
|
||||
roles: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
blackbox: true,
|
||||
},
|
||||
roles: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
"roles.$": {
|
||||
type: String
|
||||
},
|
||||
// In order to avoid an 'Exception in setInterval callback' from Meteor
|
||||
heartbeat: {
|
||||
type: Date,
|
||||
optional: true,
|
||||
},
|
||||
apiKey: {
|
||||
type: String,
|
||||
index: 1,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
Meteor.users.attachSchema(Schemas.User);
|
||||
|
||||
Meteor.users.allow({
|
||||
update: function(userId, doc, fields, modifier) {
|
||||
if (
|
||||
doc._id === userId &&
|
||||
_.contains(fields, "username") &&
|
||||
_.contains(fields, "profile") &&
|
||||
fields.length === 2 &&
|
||||
_.keys(modifier).length === 1 &&
|
||||
modifier.$set &&
|
||||
modifier.$set["profile.username"] &&
|
||||
modifier.$set.username &&
|
||||
_.keys(modifier.$set).length === 2
|
||||
){
|
||||
var expectedUsername = modifier.$set["profile.username"];
|
||||
expectedUsername = expectedUsername.toLowerCase().replace(/\s+/gm, "");
|
||||
if (modifier.$set.username !== expectedUsername){
|
||||
return false;
|
||||
}
|
||||
var foundUser = Meteor.call("getUserId", expectedUsername);
|
||||
return !foundUser || foundUser === userId;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Meteor.isServer) Meteor.methods({
|
||||
generateMyApiKey() {
|
||||
var user = Meteor.users.findOne(this.userId);
|
||||
if (!user) return;
|
||||
if (user && user.apiKey) return;
|
||||
var apiKey = Random.id(30);
|
||||
Meteor.users.update(this.userId, {$set: {apiKey}});
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user