Changed rpg-docs folder to app
This commit is contained in:
258
app/lib/functions/characterExport.js
Normal file
258
app/lib/functions/characterExport.js
Normal file
@@ -0,0 +1,258 @@
|
||||
characterExport = function(charId){
|
||||
var {
|
||||
character, classes, effects, proficiencies,
|
||||
} = getCharacterForComputation(charId);
|
||||
var char = character;
|
||||
computedCharacter = computeCharacter({
|
||||
character, classes, effects, proficiencies,
|
||||
});
|
||||
if (!char) {
|
||||
return {
|
||||
error: charId + " character not found"
|
||||
};
|
||||
}
|
||||
if (char.settings.viewPermission !== "public" && Meteor.isServer){
|
||||
return {
|
||||
error: charId + " character is not viewable to anyone with link"
|
||||
};
|
||||
}
|
||||
var baseValue = function(attributeName){
|
||||
var attribute = computedCharacter[attributeName];
|
||||
return attribute && attribute.value;
|
||||
};
|
||||
var attributeValue = function(attributeName){
|
||||
var base = baseValue(attributeName);
|
||||
var adjustment = char[attributeName] && char[attributeName].adjustment;
|
||||
return base + adjustment;
|
||||
};
|
||||
var abilityMod = function(attributeName){
|
||||
return signedString(getMod(attributeValue(attributeName)));
|
||||
};
|
||||
var skillMod = function(skillName){
|
||||
return signedString(baseValue(skillName));
|
||||
};
|
||||
var proficiency = function(skillName){
|
||||
var skill = computedCharacter[skillName];
|
||||
return skill && skill.proficiency;
|
||||
};
|
||||
var passiveSkill = function(skillName){
|
||||
var attribute = computedCharacter[skillName];
|
||||
if (!attribute) return;
|
||||
return 10 + baseValue(skillName) + attribute.passiveAdd;
|
||||
};
|
||||
var experience = function(){
|
||||
var xp = 0;
|
||||
Experiences.find(
|
||||
{charId: charId},
|
||||
{fields: {value: 1}}
|
||||
).forEach(function(e){
|
||||
xp += e.value;
|
||||
});
|
||||
return xp;
|
||||
};
|
||||
var getClasses = function(){
|
||||
return _.map(classes, c => `${c.name} ${c.level}`).join(", ");
|
||||
};
|
||||
var getHitDiceString = function(){
|
||||
var d6 = baseValue("d6HitDice");
|
||||
var d8 = baseValue("d8HitDice");
|
||||
var d10 = baseValue("d10HitDice");
|
||||
var d12 = baseValue("d12HitDice");
|
||||
var con = abilityMod("constitution");
|
||||
var string = "" +
|
||||
(d6 ? `${d6}d6 + ` : "") +
|
||||
(d8 ? `${d8}d8 + ` : "") +
|
||||
(d10 ? `${d10}d10 + ` : "") +
|
||||
(d12 ? `${d12}d12 + ` : "") +
|
||||
con;
|
||||
return string;
|
||||
}
|
||||
var getSkills = function(charId){
|
||||
var allSkills = [
|
||||
{name: "acrobatics", attribute: "dexterity"},
|
||||
{name: "animalHandling", attribute: "wisdom"},
|
||||
{name: "arcana", attribute: "intelligence"},
|
||||
{name: "athletics", attribute: "strength"},
|
||||
{name: "deception", attribute: "charisma"},
|
||||
{name: "history", attribute: "intelligence"},
|
||||
{name: "insight", attribute: "wisdom"},
|
||||
{name: "intimidation", attribute: "charisma"},
|
||||
{name: "investigation", attribute: "intelligence"},
|
||||
{name: "medicine", attribute: "wisdom"},
|
||||
{name: "nature", attribute: "intelligence"},
|
||||
{name: "perception", attribute: "wisdom"},
|
||||
{name: "performance", attribute: "charisma"},
|
||||
{name: "persuasion", attribute: "charisma"},
|
||||
{name: "religion", attribute: "intelligence"},
|
||||
{name: "sleightOfHand", attribute: "dexterity"},
|
||||
{name: "stealth", attribute: "dexterity"},
|
||||
{name: "survival", attribute: "wisdom"},
|
||||
];
|
||||
var skills = {};
|
||||
_.each(allSkills, skill => {
|
||||
var value = skillMod(skill.name);
|
||||
var prof = proficiency(skill.name);
|
||||
var name = skill.name.charAt(0).toUpperCase() + skill.name.slice(1);
|
||||
skills[name] = value;
|
||||
skills[name + "Proficiency"] = prof;
|
||||
});
|
||||
return skills;
|
||||
};
|
||||
var character = {
|
||||
"Id": char._id,
|
||||
"Name": char.name,
|
||||
"Source": "DiceCloud",
|
||||
"Alignment": char.alignment || "",
|
||||
"Gender": char.gender || "",
|
||||
"Race": char.race || "",
|
||||
"Level": _.reduce(classes, (memo, cls) => memo + cls.level, 0),
|
||||
"Experience": experience(),
|
||||
"Class": getClasses(charId),
|
||||
"HPBase": baseValue("hitPoints"),
|
||||
"HPValue": attributeValue("hitPoints"),
|
||||
"HitDice": getHitDiceString(charId) || "",
|
||||
"AC": attributeValue("armor"),
|
||||
"Initiative": skillMod("initiative"),
|
||||
"Speed": attributeValue("speed"),
|
||||
"ProficiencyBonus": attributeValue("proficiencyBonus"),
|
||||
"passivePerception": passiveSkill("perception"),
|
||||
|
||||
"Languages": getLanguages(charId),
|
||||
"Description": char.description || "",
|
||||
"Backstory": char.backstory || "",
|
||||
"Personality": char.personality || "" ,
|
||||
"Bonds": char.bonds || "",
|
||||
"Ideals": char.ideals || "",
|
||||
"Flaws": char.flaws || "",
|
||||
"PictureURL": char.picture || "",
|
||||
|
||||
"Strength": attributeValue("strength"),
|
||||
"Dexterity": attributeValue("dexterity"),
|
||||
"Constitution": attributeValue("constitution"),
|
||||
"intelligence": attributeValue("intelligence"),
|
||||
"Wisdom": attributeValue("wisdom"),
|
||||
"Charisma": attributeValue("charisma"),
|
||||
|
||||
"StrengthMod": abilityMod("strength"),
|
||||
"DexterityMod": abilityMod("dexterity"),
|
||||
"ConstitutionMod": abilityMod("constitution"),
|
||||
"intelligenceMod": abilityMod("intelligence"),
|
||||
"WisdomMod": abilityMod("wisdom"),
|
||||
"CharismaMod": abilityMod("charisma"),
|
||||
|
||||
//"DamageVulnerabilities": damageMods.vulnerabilities,
|
||||
//"DamageResistances": damageMods.resistances,
|
||||
//"DamageImmunities": damageMods.immunities,
|
||||
|
||||
"StrengthSave": skillMod("strengthSave"),
|
||||
"StrengthSaveProficiency": proficiency("strengthSave"),
|
||||
"DexteritySave": skillMod("dexteritySave"),
|
||||
"DexteritySaveProficiency": proficiency("dexteritySave"),
|
||||
"ConstitutionSave": skillMod("constitutionSave"),
|
||||
"ConstitutionSaveProficiency": proficiency("constitutionSave"),
|
||||
"intelligenceSave": skillMod("intelligenceSave"),
|
||||
"intelligenceSaveProficiency": proficiency("intelligenceSave"),
|
||||
"WisdomSave": skillMod("wisdomSave"),
|
||||
"WisdomSaveProficiency": proficiency("wisdomSave"),
|
||||
"CharismaSave": skillMod("charismaSave"),
|
||||
"CharismaSaveProficiency": proficiency("charismaSave"),
|
||||
|
||||
"Level1SpellSlots": attributeValue("level1SpellSlots"),
|
||||
"Level2SpellSlots": attributeValue("level2SpellSlots"),
|
||||
"Level3SpellSlots": attributeValue("level3SpellSlots"),
|
||||
"Level4SpellSlots": attributeValue("level4SpellSlots"),
|
||||
"Level5SpellSlots": attributeValue("level5SpellSlots"),
|
||||
"Level6SpellSlots": attributeValue("level6SpellSlots"),
|
||||
"Level7SpellSlots": attributeValue("level7SpellSlots"),
|
||||
"Level8SpellSlots": attributeValue("level8SpellSlots"),
|
||||
"Level9SpellSlots": attributeValue("level9SpellSlots"),
|
||||
"Ki": attributeValue("ki"),
|
||||
"Rages": attributeValue("rages"),
|
||||
"RageDamage": attributeValue("rageDamage"),
|
||||
"SorceryPoints": attributeValue("sorceryPoints"),
|
||||
|
||||
"DeathSavePasses": char.deathSave.pass,
|
||||
"DeathSaveFails": char.deathSave.fail,
|
||||
"DeathSaveStable": char.deathSave.stable,
|
||||
};
|
||||
_.extend(character, getSkills(charId));
|
||||
_.extend(character, getAttacks(charId));
|
||||
return character;
|
||||
}
|
||||
|
||||
var getArmorString = function(charId){
|
||||
var bases = Effects.find({
|
||||
charId: charId,
|
||||
stat: "armor",
|
||||
operation: "base",
|
||||
enabled: true,
|
||||
}).map(e => ({
|
||||
ame: e.name,
|
||||
value: evaluateEffect(charId, e),
|
||||
}));
|
||||
var base = bases.length && _.max(bases, b => b.value).name || "";
|
||||
var effects = Effects.find({
|
||||
charId: charId,
|
||||
stat: "armor",
|
||||
operation: {$ne: "base"},
|
||||
enabled: true,
|
||||
}).map(e => e.name);
|
||||
var strings = base ? [base] : [];
|
||||
strings = strings.concat(effects);
|
||||
return strings.join(", ");
|
||||
}
|
||||
/*
|
||||
var getDamageMods = function(charId){
|
||||
// jscs:disable maximumLineLength
|
||||
var multipliers = [
|
||||
{name: "Acid", value: Characters.calculate.attributeValue(charId, "acidMultiplier")},
|
||||
{name: "Bludgeoning", value: Characters.calculate.attributeValue(charId, "bludgeoningMultiplier")},
|
||||
{name: "Cold", value: Characters.calculate.attributeValue(charId, "coldMultiplier")},
|
||||
{name: "Fire", value: Characters.calculate.attributeValue(charId, "fireMultiplier")},
|
||||
{name: "Force", value: Characters.calculate.attributeValue(charId, "forceMultiplier")},
|
||||
{name: "Lightning", value: Characters.calculate.attributeValue(charId, "lightningMultiplier")},
|
||||
{name: "Necrotic", value: Characters.calculate.attributeValue(charId, "necroticMultiplier")},
|
||||
{name: "Piercing", value: Characters.calculate.attributeValue(charId, "piercingMultiplier")},
|
||||
{name: "Poison", value: Characters.calculate.attributeValue(charId, "poisonMultiplier")},
|
||||
{name: "Psychic", value: Characters.calculate.attributeValue(charId, "psychicMultiplier")},
|
||||
{name: "Radiant", value: Characters.calculate.attributeValue(charId, "radiantMultiplier")},
|
||||
{name: "Slashing", value: Characters.calculate.attributeValue(charId, "slashingMultiplier")},
|
||||
{name: "Thunder", value: Characters.calculate.attributeValue(charId, "thunderMultiplier")},
|
||||
];
|
||||
// jscs:enable maximumLineLength
|
||||
multipliers = _.groupBy(multipliers, "value");
|
||||
var names = o => o.name;
|
||||
return {
|
||||
"immunities": _.map(multipliers["0"], names).join(", "),
|
||||
"resistances": _.map(multipliers["0.5"], names).join(", "),
|
||||
"vulnerabilities": _.map(multipliers["2"], names).join(", "),
|
||||
};
|
||||
}
|
||||
*/
|
||||
|
||||
var getLanguages = function(charId){
|
||||
return Proficiencies.find({
|
||||
charId,
|
||||
enabled: true,
|
||||
type: "language",
|
||||
}).map(l => l.name).join(", ");
|
||||
};
|
||||
|
||||
var getAttacks = function(charId){
|
||||
var attacks = {};
|
||||
var i = 1;
|
||||
Attacks.find(
|
||||
{charId, enabled: true},
|
||||
{sort: {color: 1, name: 1}}
|
||||
).forEach(a => {
|
||||
attacks[`Attack${i++}`] = a.name +
|
||||
` +${evaluate(charId, a.attackBonus)} to hit, ` +
|
||||
`${evaluateString(charId, a.damage)} ${a.damageType} damage, ` +
|
||||
`${a.details}`;
|
||||
});
|
||||
return attacks;
|
||||
};
|
||||
|
||||
var signedString = function(number) {
|
||||
return number >= 0 ? "+" + number : "" + number;
|
||||
};
|
||||
7
app/lib/functions/characterUtility.js
Normal file
7
app/lib/functions/characterUtility.js
Normal file
@@ -0,0 +1,7 @@
|
||||
getMod = function(score){
|
||||
return Math.floor((score - 10) / 2);
|
||||
};
|
||||
|
||||
signedString = function(number){
|
||||
return number >= 0 ? "+" + number : "" + number;
|
||||
};
|
||||
224
app/lib/functions/computeCharacter.js
Normal file
224
app/lib/functions/computeCharacter.js
Normal file
@@ -0,0 +1,224 @@
|
||||
getCharacterForComputation = function(charId){
|
||||
const character = Characters.findOne(charId);
|
||||
const classes = Classes.find({charId}).fetch();
|
||||
const effects = Effects.find({charId, enabled: true}).fetch();
|
||||
const proficiencies = Proficiencies.find({
|
||||
charId,
|
||||
enabled: true,
|
||||
type: {$in: ["skill", "save"]},
|
||||
}).fetch();
|
||||
return {character, classes, effects, proficiencies};
|
||||
}
|
||||
|
||||
computeCharacter = function({character, classes, effects, proficiencies}){
|
||||
var charId = character._id;
|
||||
let computedClasses = computeCharacterClasses(charId, classes);
|
||||
let changed = false;
|
||||
computedCharacter = {};
|
||||
let i;
|
||||
for (i = 0; i < 15; i++){
|
||||
[computedCharacter, changed] = compute({
|
||||
classes: computedClasses,
|
||||
oldChar: computedCharacter,
|
||||
charId,
|
||||
character,
|
||||
effects,
|
||||
proficiencies,
|
||||
});
|
||||
if (!changed) break;
|
||||
}
|
||||
return computedCharacter;
|
||||
};
|
||||
|
||||
var ensureCharacterExists = (character) => {
|
||||
if (!character) {
|
||||
throw new Meteor.Error("Character doesn't exist",
|
||||
"You can't recompute a character that doesn't exist");
|
||||
}
|
||||
};
|
||||
|
||||
var ensureWritePermissions = (character, userId) => {
|
||||
if (
|
||||
userId &&
|
||||
userId !== character.owner &&
|
||||
!_.contains(character.writers, userId)
|
||||
){
|
||||
throw new Meteor.Error("Character write denied",
|
||||
"You don't have permission to recompute this character");
|
||||
}
|
||||
};
|
||||
|
||||
var computeCharacterClasses = function(charId, classes){
|
||||
let computedClasses = {};
|
||||
_.each(classes, (cls) => {
|
||||
if (computedClasses[cls.name]){
|
||||
computedClasses[cls.name].level += cls.level;
|
||||
} else {
|
||||
computedClasses[cls.name] = cls;
|
||||
}
|
||||
});
|
||||
return computedClasses;
|
||||
}
|
||||
|
||||
var compute = function({
|
||||
charId, oldChar, character, classes, effects, proficiencies,
|
||||
}){
|
||||
let newChar = {};
|
||||
_.each(effects, (effect, index) => {
|
||||
if (!effect.stat || effect.operation === "conditional") return;
|
||||
if (!newChar[effect.stat]) newChar[effect.stat] = defaultStat();
|
||||
let value = effect.calculation ?
|
||||
computeEffect(effect.calculation, oldChar, classes) :
|
||||
effect.value || 0;
|
||||
let stat = newChar[effect.stat];
|
||||
if (!_.isNumber(value)) return;
|
||||
switch (effect.operation) {
|
||||
case "base":
|
||||
if (value > stat.base) stat.base = value;
|
||||
break;
|
||||
case "proficiency":
|
||||
if (value > stat.proficiency) stat.proficiency = value;
|
||||
break;
|
||||
case "add":
|
||||
stat.add += value;
|
||||
break;
|
||||
case "mul":
|
||||
stat.mul *= value;
|
||||
break;
|
||||
case "min":
|
||||
if (value > stat.min) stat.min = value;
|
||||
break;
|
||||
case "max":
|
||||
if (value < stat.max) stat.max = value;
|
||||
break;
|
||||
case "advantage":
|
||||
stat.advantage++;
|
||||
break;
|
||||
case "disadvantage":
|
||||
stat.disadvantage++;
|
||||
break;
|
||||
case "passiveAdd":
|
||||
stat.passiveAdd += value;
|
||||
break;
|
||||
case "fail":
|
||||
stat.fail = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
_.each(proficiencies, proficiency => {
|
||||
if (!proficiency.name) return;
|
||||
if (!newChar[proficiency.name]) newChar[proficiency.name] = defaultStat();
|
||||
let stat = newChar[proficiency.name];
|
||||
let value = proficiency.value;
|
||||
if (value > stat.proficiency) stat.proficiency = value;
|
||||
});
|
||||
let changed = false;
|
||||
_.each(ATTRIBUTES, function(statName) {
|
||||
if (!newChar[statName]) newChar[statName] = defaultStat();
|
||||
let stat = newChar[statName];
|
||||
stat.value = (stat.base + stat.add) * stat.mul;
|
||||
if (stat.value < stat.min) stat.value = stat.min;
|
||||
if (stat.value > stat.max) stat.value = stat.max;
|
||||
if (!_.isEqual(stat.value, oldChar[statName] && oldChar[statName].value)){
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
_.each(ALL_SKILLS, function(statName) {
|
||||
if (!newChar[statName]) newChar[statName] = defaultStat();
|
||||
let stat = newChar[statName];
|
||||
stat.value = characterAbilityMod(
|
||||
oldChar, character[statName] && character[statName].ability
|
||||
);
|
||||
stat.value += stat.base + stat.add;
|
||||
stat.value += stat.proficiency *
|
||||
characterFieldValue(oldChar, "proficiencyBonus");
|
||||
stat.value *= stat.mul;
|
||||
if (stat.value < stat.min) stat.value = stat.min;
|
||||
if (stat.value > stat.max) stat.value = stat.max;
|
||||
if (!_.isEqual(stat.value, oldChar[statName] && oldChar[statName].value)){
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
return [newChar, changed];
|
||||
};
|
||||
|
||||
var defaultStat = function(){
|
||||
return {
|
||||
base: 0,
|
||||
proficiency: 0,
|
||||
add: 0,
|
||||
mul: 1,
|
||||
min: Number.NEGATIVE_INFINITY,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
advantage: 0,
|
||||
disadvantage: 0,
|
||||
passiveAdd: 0,
|
||||
fail: false,
|
||||
}
|
||||
}
|
||||
|
||||
var computeEffect = function(string, character, classes){
|
||||
if (!string) return string;
|
||||
string = string.replace(/\b[a-z][\w]+/gi, function(sub){
|
||||
//fields
|
||||
if (character[sub]){
|
||||
return characterFieldValue(character, sub);
|
||||
}
|
||||
//ability modifiers
|
||||
if (_.contains(ABILITY_MODS, sub)){
|
||||
var slice = sub.slice(0, -3);
|
||||
return getMod(
|
||||
character[slice] ? characterFieldValue(character, slice) : 0
|
||||
);
|
||||
}
|
||||
//class levels
|
||||
if (/\w+levels?\b/gi.test(sub)){
|
||||
//strip out "level"
|
||||
var className = sub.replace(/levels?\b/gi, "");
|
||||
return characterClassLevel(classes, className)
|
||||
}
|
||||
//character level
|
||||
if (sub.toUpperCase() === "LEVEL"){
|
||||
return characterTotalLevel(classes);
|
||||
}
|
||||
// exclude math functions
|
||||
if (math[sub]){
|
||||
return sub;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
try {
|
||||
var result = math.eval(string);
|
||||
return result;
|
||||
} catch (e){
|
||||
return string;
|
||||
}
|
||||
};
|
||||
|
||||
var characterFieldValue = function(character, field){
|
||||
if (_.isNumber(character[field] && character[field].value)){
|
||||
return character[field].value;
|
||||
} else {
|
||||
return field;
|
||||
}
|
||||
};
|
||||
|
||||
var characterClassLevel = function(classes, className){
|
||||
if (_.isNumber(classes[className] && classes[className].level)){
|
||||
return classes[className].level;
|
||||
} else {
|
||||
return className;
|
||||
}
|
||||
};
|
||||
|
||||
var characterTotalLevel = function(classes){
|
||||
return _.reduce(classes, (memo, cls) => memo + cls.level, 0);
|
||||
};
|
||||
|
||||
var characterAbilityMod = function(character, abilityName){
|
||||
if (_.isNumber(character[abilityName] && character[abilityName].value)){
|
||||
return getMod(character[abilityName].value);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
104
app/lib/functions/evaluate.js
Normal file
104
app/lib/functions/evaluate.js
Normal file
@@ -0,0 +1,104 @@
|
||||
// if we want to add more functions, consider pulling out into its own file
|
||||
(function() {
|
||||
math.import({
|
||||
"if": function(pred, a, b) {
|
||||
return (!!(pred)) ? a : b;
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
//evaluates a calculation string
|
||||
evaluate = function(charId, string, opts){
|
||||
var spellListId = opts && opts.spellListId;
|
||||
if (!string) return string;
|
||||
string = string.replace(/\b[a-z,1-9]+\b/gi, function(sub){
|
||||
//fields
|
||||
if (Schemas.Character.schema(sub)){
|
||||
return Characters.calculate.fieldValue(charId, sub);
|
||||
}
|
||||
//ability modifiers
|
||||
var abilityMods = [
|
||||
"strengthMod",
|
||||
"dexterityMod",
|
||||
"constitutionMod",
|
||||
"intelligenceMod",
|
||||
"wisdomMod",
|
||||
"charismaMod",
|
||||
];
|
||||
if (_.contains(abilityMods, sub)){
|
||||
var slice = sub.slice(0, -3);
|
||||
try {
|
||||
return Characters.calculate.abilityMod(charId, slice);
|
||||
} catch (e){
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
//class levels
|
||||
if (/\w+levels?\b/gi.test(sub)){
|
||||
//strip out "level"
|
||||
var className = sub.replace(/levels?\b/gi, "");
|
||||
var cls = Classes.findOne({charId: charId, name: className});
|
||||
return cls && cls.level || sub;
|
||||
}
|
||||
//character level
|
||||
if (sub.toUpperCase() === "LEVEL"){
|
||||
return Characters.calculate.level(charId);
|
||||
}
|
||||
if (spellListId && sub.toUpperCase() === "DC") {
|
||||
var list = SpellLists.findOne(spellListId);
|
||||
if (list && list.saveDC){
|
||||
return evaluate(charId, list.saveDC);
|
||||
}
|
||||
}
|
||||
if (spellListId && sub.toUpperCase() === "ATTACKBONUS") {
|
||||
var list = SpellLists.findOne(spellListId);
|
||||
if (list && list.attackBonus){
|
||||
return evaluate(charId, list.attackBonus);
|
||||
}
|
||||
}
|
||||
return sub;
|
||||
});
|
||||
try {
|
||||
var result = math.eval(string);
|
||||
return result;
|
||||
} catch (e){
|
||||
return string;
|
||||
}
|
||||
};
|
||||
|
||||
//takes a string with {calculations} and returns it with the results
|
||||
//of the calculations returned in place
|
||||
evaluateString = function(charId, string){
|
||||
//define brackets as curly brackets around anything that isn't a curly bracket
|
||||
if (!string) return string;
|
||||
var brackets = /\{[^\{\}]*\}/g;
|
||||
var result = string.replace(brackets, function(exp){
|
||||
exp = exp.replace(/(\{|\})/g, ""); //remove curly brackets
|
||||
return evaluate(charId, exp);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
evaluateSpellString = function (charId, spellListId, string) {
|
||||
//define brackets as curly brackets around anything that isn't a curly bracket
|
||||
if (!string) return string;
|
||||
var brackets = /\{[^\{\}]*\}/g;
|
||||
var result = string.replace(brackets, function(exp){
|
||||
exp = exp.replace(/(\{|\})/g, ""); //remove curly brackets
|
||||
return evaluate(charId, exp, {spellListId});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
//returns the value of the effect if it exists,
|
||||
//otherwise returns the result of the calculation if it exists,
|
||||
//otherwise returns 0
|
||||
evaluateEffect = function(charId, effect){
|
||||
if (_.isFinite(effect.value)){
|
||||
return effect.value;
|
||||
} else if (_.isString(effect.calculation)){
|
||||
return +evaluate(charId, effect.calculation);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
219
app/lib/functions/parenting.js
Normal file
219
app/lib/functions/parenting.js
Normal file
@@ -0,0 +1,219 @@
|
||||
var childSchema = new SimpleSchema({
|
||||
parent: {type: Object},
|
||||
"parent.collection": {type: String},
|
||||
"parent.id": {type: String, regEx: SimpleSchema.RegEx.Id, index: 1},
|
||||
"parent.group": {type: String, optional: true},
|
||||
"removedWith": {
|
||||
optional: true,
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
});
|
||||
|
||||
var joinWithDefaultKeys = function(keys){
|
||||
var defaultKeys = [
|
||||
"charId",
|
||||
];
|
||||
return _.union(keys, defaultKeys);
|
||||
};
|
||||
|
||||
var limitModifierToKeys = function(modifier, keys){
|
||||
if (!modifier) return;
|
||||
modifier = _.pick(modifier, ["$set", "$unset"]);
|
||||
if (modifier.$set) modifier.$set = _.pick(modifier.$set, keys);
|
||||
if (modifier.$unset) modifier.$unset = _.pick(modifier.$unset, keys);
|
||||
if (_.isEmpty(modifier.$set)) delete modifier.$set;
|
||||
if (_.isEmpty(modifier.$unset)) delete modifier.$unset;
|
||||
return modifier;
|
||||
};
|
||||
|
||||
var getParent = function(doc){
|
||||
if (!doc || !doc.parent) return;
|
||||
var parentCol = Meteor.isClient ?
|
||||
window[doc.parent.collection] : global[doc.parent.collection];
|
||||
if (parentCol)
|
||||
return parentCol.findOne(doc.parent.id, {removed: true});
|
||||
};
|
||||
|
||||
var inheritParentProperties = function(doc, collection){
|
||||
var parent = getParent(doc);
|
||||
if (!parent) throw new Meteor.Error(
|
||||
"Parenting Error",
|
||||
"Document's parent does not exist"
|
||||
);
|
||||
var handMeDowns = _.pick(parent, collection.inheritedKeys);
|
||||
if (
|
||||
_.contains(collection.inheritedKeys, "charId") &&
|
||||
doc.parent.collection === "Characters"
|
||||
){
|
||||
handMeDowns.charId = doc.parent.id;
|
||||
}
|
||||
if (_.isEmpty(handMeDowns)) return;
|
||||
collection.update(doc._id, {$set: handMeDowns});
|
||||
};
|
||||
|
||||
var childCollections = [];
|
||||
|
||||
makeChild = function(collection, inheritedKeys){
|
||||
inheritedKeys = inheritedKeys || [];
|
||||
if (inheritedKeys) {
|
||||
collection.inheritedKeys = joinWithDefaultKeys(inheritedKeys);
|
||||
}
|
||||
collection.helpers({
|
||||
//returns the parent even if it's removed
|
||||
getParent: function(){
|
||||
return getParent(this);
|
||||
},
|
||||
getParentCollection: function(){
|
||||
return Meteor.isClient ?
|
||||
window[this.parent.collection] : global[this.parent.collection];
|
||||
},
|
||||
});
|
||||
|
||||
//when created, inherit parent properties
|
||||
collection.after.insert(function(userId, doc){
|
||||
inheritParentProperties(doc, collection);
|
||||
});
|
||||
|
||||
collection.before.update(function(userId, doc, fieldNames, modifier, options){
|
||||
//if we are restoring this asset, unmark that it was removed with its parent, we no longer care
|
||||
if (modifier && modifier.$unset && modifier.$unset.removed) {
|
||||
modifier.$unset.removedWith = "";
|
||||
}
|
||||
});
|
||||
|
||||
collection.after.update(function(userId, doc, fieldNames, modifier, options) {
|
||||
if (modifier && modifier.$set && modifier.$set["parent.id"]){
|
||||
//when we change parents, inherit its properties
|
||||
inheritParentProperties(doc, collection);
|
||||
}
|
||||
});
|
||||
|
||||
collection.softRemoveNode = collection.softRemoveNode || function(id){
|
||||
collection.softRemove(id);
|
||||
};
|
||||
|
||||
collection.restoreNode = collection.restoreNode || function(id){
|
||||
collection.restore(id);
|
||||
};
|
||||
|
||||
collection.attachSchema(childSchema);
|
||||
|
||||
childCollections.push(collection);
|
||||
};
|
||||
|
||||
makeParent = function(collection, donatedKeys){
|
||||
donatedKeys = joinWithDefaultKeys(donatedKeys);
|
||||
var collectionName = collection._collection.name;
|
||||
//after changing, push the changes to all children
|
||||
collection.after.update(function(userId, doc, fieldNames, modifier, options) {
|
||||
modifier = limitModifierToKeys(modifier, donatedKeys);
|
||||
doc = _.pick(doc, ["_id", "charId"]);
|
||||
if (!modifier) return;
|
||||
Meteor.call("updateChildren", doc, modifier, true);
|
||||
});
|
||||
collection.softRemoveNode = function(id){
|
||||
Meteor.call("softRemoveNode", collectionName, id);
|
||||
};
|
||||
|
||||
collection.restoreNode = function(id){
|
||||
Meteor.call("restoreNode", collectionName, id);
|
||||
};
|
||||
|
||||
if (Meteor.isServer) collection.after.remove(function(userId, doc) {
|
||||
_.each(childCollections, function(collection){
|
||||
collection.remove(
|
||||
{"parent.id": doc._id}
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var checkPermission = function(userId, charId){
|
||||
var char = Characters.findOne(charId, {fields: {owner: 1, writers: 1}});
|
||||
if (!char)
|
||||
throw new Meteor.Error("Access Denied, no charId",
|
||||
"Character " + charId + " does not exist");
|
||||
if (!userId)
|
||||
throw new Meteor.Error("Access Denied, no userId",
|
||||
"No UserId set when trying to update character asset.");
|
||||
if (char.owner !== userId && !_.contains(char.writers, userId))
|
||||
throw new Meteor.Error("Access Denied, not permitted",
|
||||
"Not permitted to update assets of this character.");
|
||||
return true;
|
||||
};
|
||||
|
||||
var cascadeSoftRemove = function(id, removedWithId){
|
||||
_.each(childCollections, function(treeCollection){
|
||||
treeCollection.update(
|
||||
{"parent.id": id},
|
||||
{$set: {
|
||||
removed: true,
|
||||
removedWith: removedWithId,
|
||||
}},
|
||||
{multi: true}
|
||||
);
|
||||
treeCollection.find({"parent.id": id}).forEach(function(doc){
|
||||
cascadeSoftRemove(doc._id, removedWithId);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var checkRemovePermission = function(collectionName, id, self){
|
||||
check(collectionName, String);
|
||||
check(id, String);
|
||||
var collection = Mongo.Collection.get(collectionName);
|
||||
var node = collection.findOne(id);
|
||||
var charId = node && node.charId;
|
||||
checkPermission(self.userId, charId);
|
||||
};
|
||||
|
||||
Meteor.methods({
|
||||
softRemoveNode: function(collectionName, id){
|
||||
checkRemovePermission(collectionName, id, this);
|
||||
var collection = Mongo.Collection.get(collectionName);
|
||||
collection.softRemove(id);
|
||||
cascadeSoftRemove(id, id);
|
||||
},
|
||||
restoreNode: function(collectionName, id){
|
||||
checkRemovePermission(collectionName, id, this);
|
||||
var collection = Mongo.Collection.get(collectionName);
|
||||
collection.restore(id);
|
||||
_.each(childCollections, function(treeCollection){
|
||||
treeCollection.update(
|
||||
{removedWith: id, removed: true},
|
||||
{$unset: {removed: true, removedWith: ""}},
|
||||
{multi: true}
|
||||
);
|
||||
});
|
||||
},
|
||||
updateChildren: function(parent, modifier, limitToInheritance) {
|
||||
check(parent, {_id: String, charId: String});
|
||||
check(modifier, Object);
|
||||
checkPermission(this.userId, parent.charId);
|
||||
var selector = {"parent.id": parent._id};
|
||||
_.each(childCollections, function(collection){
|
||||
var thisModifier;
|
||||
if (limitToInheritance){
|
||||
thisModifier = limitModifierToKeys(modifier, collection.inheritedKeys);
|
||||
} else {
|
||||
thisModifier = _.clone(modifier);
|
||||
}
|
||||
if (_.isEmpty(thisModifier)) return;
|
||||
collection.update(selector, thisModifier, {multi: true, removed: true});
|
||||
});
|
||||
},
|
||||
cloneChildren: function(objectId, newParent){
|
||||
check(objectId, String);
|
||||
check(newParent, {id: String, collection: String});
|
||||
|
||||
_.each(childCollections, function(collection){
|
||||
var keys = collection.simpleSchema().objectKeys();
|
||||
collection.find({"parent.id": objectId}).forEach(function(doc){
|
||||
var newDoc = _.pick(doc, keys);
|
||||
newDoc.parent = newParent;
|
||||
collection.insert(newDoc);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
18
app/lib/functions/permissions.js
Normal file
18
app/lib/functions/permissions.js
Normal file
@@ -0,0 +1,18 @@
|
||||
canEditCharacter = function(charId, userId){
|
||||
userId = userId || Meteor.userId();
|
||||
var char = Characters.findOne(charId, {fields: {owner: 1, writers: 1}});
|
||||
if (!char) return true;
|
||||
return (userId === char.owner || _.contains(char.writers, userId));
|
||||
};
|
||||
|
||||
canViewCharacter = function(charId, userId){
|
||||
userId = userId || Meteor.userId();
|
||||
var char = Characters.findOne(
|
||||
charId,
|
||||
{fields: {owner: 1, writers: 1, readers: 1}}
|
||||
);
|
||||
if (!char) return true;
|
||||
return userId === char.owner ||
|
||||
_.contains(char.writers, userId) ||
|
||||
_.contains(char.readers, userId);
|
||||
};
|
||||
66
app/lib/functions/pointBuy.js
Normal file
66
app/lib/functions/pointBuy.js
Normal file
@@ -0,0 +1,66 @@
|
||||
pointBuyCost = {
|
||||
"8": 0,
|
||||
"9": 1,
|
||||
"10": 2,
|
||||
"11": 3,
|
||||
"12": 4,
|
||||
"13": 5,
|
||||
"14": 7,
|
||||
"15": 9,
|
||||
};
|
||||
|
||||
var getPointBuyEffect = function(stat, value, pointsUsed, charId){
|
||||
return {
|
||||
modifier:{
|
||||
charId: charId,
|
||||
stat: stat,
|
||||
name: pointsUsed + " Point Buy",
|
||||
operation: "base",
|
||||
value: value,
|
||||
parent: {collection: "Characters", id: charId},
|
||||
enabled: true,
|
||||
},
|
||||
selector:{
|
||||
charId: charId,
|
||||
stat: stat,
|
||||
operation: "base",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
var checkPermission = function(userId, charId){
|
||||
var char = Characters.findOne(charId, {fields: {owner: 1, writers: 1}});
|
||||
if (!char)
|
||||
throw new Meteor.Error("Access Denied",
|
||||
"Character " + charId + " does not exist");
|
||||
if (!userId)
|
||||
throw new Meteor.Error("Access Denied",
|
||||
"No UserId set when trying to update character asset.");
|
||||
if (char.owner !== userId && !_.contains(char.writers, userId))
|
||||
throw new Meteor.Error("Access Denied",
|
||||
"Not permitted to update assets of this character.");
|
||||
return true;
|
||||
};
|
||||
|
||||
Meteor.methods({
|
||||
pointBuyAbilityScores: function(charId, points){
|
||||
check(points, {
|
||||
strength: Number,
|
||||
dexterity: Number,
|
||||
constitution: Number,
|
||||
intelligence: Number,
|
||||
wisdom: Number,
|
||||
charisma: Number,
|
||||
});
|
||||
check(charId, String);
|
||||
checkPermission(this.userId, charId);
|
||||
var pointsUsed = 0;
|
||||
_.each(points, function(value, key){
|
||||
pointsUsed += pointBuyCost[value];
|
||||
});
|
||||
_.each(points, function(value, ability){
|
||||
var pbEffect = getPointBuyEffect(ability, value, pointsUsed, charId);
|
||||
Effects.upsert(pbEffect.selector, pbEffect.modifier);
|
||||
});
|
||||
}
|
||||
});
|
||||
35
app/lib/functions/preventLoop.js
Normal file
35
app/lib/functions/preventLoop.js
Normal file
@@ -0,0 +1,35 @@
|
||||
preventLoop = function(inputFunction){
|
||||
var self = this;
|
||||
if (!_.isFunction(inputFunction)){
|
||||
throw new Meteor.Error(
|
||||
"Not a function",
|
||||
"preventLoop can only take a function as an argument"
|
||||
);
|
||||
}
|
||||
//store a private array of arguments we have been given without returning
|
||||
//if we try to visit the same argument twice before resolving its value
|
||||
//we are in a dependency loop and need to GTFO
|
||||
var visitedArgs = [];
|
||||
return function(){
|
||||
var result;
|
||||
var hash = _.reduce(arguments, function(memo, arg) {
|
||||
return memo + arg;
|
||||
}, "");
|
||||
//we're still evaluating this attribute, must be in a loop
|
||||
if (_.contains(visitedArgs, hash)) {
|
||||
console.warn("dependency loop detected");
|
||||
return NaN;
|
||||
} else {
|
||||
//push this hash to the list of visited hashes
|
||||
//we can't visit it again unless it returns first
|
||||
visitedArgs.push(hash);
|
||||
}
|
||||
try {
|
||||
result = inputFunction.apply(this, arguments);
|
||||
} finally{
|
||||
//this hash returns or fails, pull it from the array
|
||||
visitedArgs = _.without(visitedArgs, hash);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
};
|
||||
14
app/lib/functions/shareCharacter.js
Normal file
14
app/lib/functions/shareCharacter.js
Normal file
@@ -0,0 +1,14 @@
|
||||
Meteor.methods({
|
||||
"getUserId": function(username){
|
||||
if (!username) return;
|
||||
regex = new RegExp("^" + username + "$", "i")
|
||||
var user = Meteor.users.findOne(
|
||||
{$or: [
|
||||
{username: username},
|
||||
{"emails.address": regex},
|
||||
{"services.google.email": regex},
|
||||
]}
|
||||
);
|
||||
return user && user._id;
|
||||
}
|
||||
});
|
||||
14
app/lib/functions/updatePolymerInputs.js
Normal file
14
app/lib/functions/updatePolymerInputs.js
Normal file
@@ -0,0 +1,14 @@
|
||||
updatePolymerInputs = function(self){
|
||||
_.defer(function(){
|
||||
//update all autogrows after they've been filled
|
||||
var pata = self.$("paper-autogrow-textarea");
|
||||
pata.each(function(index, el){
|
||||
el.update($(el).children().get(0));
|
||||
});
|
||||
//update all input fields as well
|
||||
var input = self.$("paper-input");
|
||||
input.each(function(index, el){
|
||||
el.valueChanged();
|
||||
});
|
||||
});
|
||||
};
|
||||
9
app/lib/functions/vMixExport.js
Normal file
9
app/lib/functions/vMixExport.js
Normal file
@@ -0,0 +1,9 @@
|
||||
vMixCharacter = function(charId){
|
||||
return JSON.stringify([characterExport(charId)], null, 2);
|
||||
};
|
||||
|
||||
vMixParty = function(partyId){
|
||||
var party = Parties.findOne(partyId);
|
||||
var chars = _.map(party.characters, charId => characterExport(charId));
|
||||
return JSON.stringify(chars, null, 2);
|
||||
};
|
||||
Reference in New Issue
Block a user