Files
DiceCloud/app/Model/Character/Characters.js
2022-11-19 22:39:35 +02:00

590 lines
19 KiB
JavaScript

//set up the collection for characters
Characters = new Mongo.Collection("characters");
Schemas.Character = new SimpleSchema({
//strings
name: { type: String, defaultValue: "", trim: false, optional: true, max: 128 },
urlName: { type: String, defaultValue: "-", trim: false, optional: true, max: 128 },
alignment: { type: String, defaultValue: "", trim: false, optional: true, max: 64 },
gender: { type: String, defaultValue: "", trim: false, optional: true, max: 64 },
race: { type: String, defaultValue: "", trim: false, optional: true, max: 64 },
picture: { type: String, defaultValue: "", trim: true, optional: true, max: 256 },
description: { type: String, defaultValue: "", trim: false, optional: true, max: 5000 },
personality: { type: String, defaultValue: "", trim: false, optional: true, max: 5000 },
ideals: { type: String, defaultValue: "", trim: false, optional: true, max: 5000 },
bonds: { type: String, defaultValue: "", trim: false, optional: true, max: 5000 },
flaws: { type: String, defaultValue: "", trim: false, optional: true, max: 5000 },
backstory: { type: String, defaultValue: "", trim: false, optional: true, max: 5000 },
//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, doc, fields, modifier) {
// can't change owners unless you are the current owner
return _.contains(fields, "owner") && doc.owner !== userId;
}
});