From 04c9c4cfc2fe05993aa4a10aa07597549e4dd47e Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Sat, 19 Nov 2022 22:39:35 +0200 Subject: [PATCH] Fixed bug where updates on sliders weren't debounced --- app/Model/Character/Characters.js | 448 +++++++++--------- .../character/stats/healthCard/healthCard.js | 115 +++-- .../lib/configuration/globalRateLimit.js | 16 + 3 files changed, 296 insertions(+), 283 deletions(-) create mode 100644 app/server/lib/configuration/globalRateLimit.js diff --git a/app/Model/Character/Characters.js b/app/Model/Character/Characters.js index b236a4a3..445e1c7b 100644 --- a/app/Model/Character/Characters.js +++ b/app/Model/Character/Characters.js @@ -3,183 +3,183 @@ 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}, + 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}, + 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}, + 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}, + 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}, + rageDamage: { type: Schemas.Attribute }, //hit dice - d6HitDice: {type: Schemas.Attribute}, - d8HitDice: {type: Schemas.Attribute}, - d10HitDice: {type: Schemas.Attribute}, - d12HitDice: {type: Schemas.Attribute}, + 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}, + 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"}, + strengthSave: { type: Schemas.Skill }, + "strengthSave.ability": { type: String, defaultValue: "strength" }, - dexteritySave: {type: Schemas.Skill}, - "dexteritySave.ability": {type: String, defaultValue: "dexterity"}, + dexteritySave: { type: Schemas.Skill }, + "dexteritySave.ability": { type: String, defaultValue: "dexterity" }, - constitutionSave:{type: Schemas.Skill}, - "constitutionSave.ability": {type: String, defaultValue: "constitution"}, + constitutionSave: { type: Schemas.Skill }, + "constitutionSave.ability": { type: String, defaultValue: "constitution" }, - intelligenceSave:{type: Schemas.Skill}, - "intelligenceSave.ability": {type: String, defaultValue: "intelligence"}, + intelligenceSave: { type: Schemas.Skill }, + "intelligenceSave.ability": { type: String, defaultValue: "intelligence" }, - wisdomSave: {type: Schemas.Skill}, - "wisdomSave.ability": {type: String, defaultValue: "wisdom"}, + wisdomSave: { type: Schemas.Skill }, + "wisdomSave.ability": { type: String, defaultValue: "wisdom" }, - charismaSave: {type: Schemas.Skill}, - "charismaSave.ability": {type: String, defaultValue: "charisma"}, + charismaSave: { type: Schemas.Skill }, + "charismaSave.ability": { type: String, defaultValue: "charisma" }, //skill skills - acrobatics: {type: Schemas.Skill}, - "acrobatics.ability": {type: String, defaultValue: "dexterity"}, + acrobatics: { type: Schemas.Skill }, + "acrobatics.ability": { type: String, defaultValue: "dexterity" }, - animalHandling: {type: Schemas.Skill}, - "animalHandling.ability": {type: String, defaultValue: "wisdom"}, + animalHandling: { type: Schemas.Skill }, + "animalHandling.ability": { type: String, defaultValue: "wisdom" }, - arcana: {type: Schemas.Skill}, - "arcana.ability": {type: String, defaultValue: "intelligence"}, + arcana: { type: Schemas.Skill }, + "arcana.ability": { type: String, defaultValue: "intelligence" }, - athletics: {type: Schemas.Skill}, - "athletics.ability": {type: String, defaultValue: "strength"}, + athletics: { type: Schemas.Skill }, + "athletics.ability": { type: String, defaultValue: "strength" }, - deception: {type: Schemas.Skill}, - "deception.ability": {type: String, defaultValue: "charisma"}, + deception: { type: Schemas.Skill }, + "deception.ability": { type: String, defaultValue: "charisma" }, - history: {type: Schemas.Skill}, - "history.ability": {type: String, defaultValue: "intelligence"}, + history: { type: Schemas.Skill }, + "history.ability": { type: String, defaultValue: "intelligence" }, - insight: {type: Schemas.Skill}, - "insight.ability": {type: String, defaultValue: "wisdom"}, + insight: { type: Schemas.Skill }, + "insight.ability": { type: String, defaultValue: "wisdom" }, - intimidation: {type: Schemas.Skill}, - "intimidation.ability": {type: String, defaultValue: "charisma"}, + intimidation: { type: Schemas.Skill }, + "intimidation.ability": { type: String, defaultValue: "charisma" }, - investigation: {type: Schemas.Skill}, - "investigation.ability": {type: String, defaultValue: "intelligence"}, + investigation: { type: Schemas.Skill }, + "investigation.ability": { type: String, defaultValue: "intelligence" }, - medicine: {type: Schemas.Skill}, - "medicine.ability": {type: String, defaultValue: "wisdom"}, + medicine: { type: Schemas.Skill }, + "medicine.ability": { type: String, defaultValue: "wisdom" }, - nature: {type: Schemas.Skill}, - "nature.ability": {type: String, defaultValue: "intelligence"}, + nature: { type: Schemas.Skill }, + "nature.ability": { type: String, defaultValue: "intelligence" }, - perception: {type: Schemas.Skill}, - "perception.ability": {type: String, defaultValue: "wisdom"}, + perception: { type: Schemas.Skill }, + "perception.ability": { type: String, defaultValue: "wisdom" }, - performance: {type: Schemas.Skill}, - "performance.ability": {type: String, defaultValue: "charisma"}, + performance: { type: Schemas.Skill }, + "performance.ability": { type: String, defaultValue: "charisma" }, - persuasion: {type: Schemas.Skill}, - "persuasion.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"}, + religion: { type: Schemas.Skill }, + "religion.ability": { type: String, defaultValue: "intelligence" }, - sleightOfHand: {type: Schemas.Skill}, - "sleightOfHand.ability": {type: String, defaultValue: "dexterity"}, + sleightOfHand: { type: Schemas.Skill }, + "sleightOfHand.ability": { type: String, defaultValue: "dexterity" }, - stealth: {type: Schemas.Skill}, - "stealth.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"}, + survival: { type: Schemas.Skill }, + "survival.ability": { type: String, defaultValue: "wisdom" }, //Mechanical Skills - initiative: {type: Schemas.Skill}, - "initiative.ability": {type: String, defaultValue: "dexterity"}, + initiative: { type: Schemas.Skill }, + "initiative.ability": { type: String, defaultValue: "dexterity" }, - dexterityArmor: {type: Schemas.Skill}, - "dexterityArmor.ability": {type: String, defaultValue: "dexterity"}, + dexterityArmor: { type: Schemas.Skill }, + "dexterityArmor.ability": { type: String, defaultValue: "dexterity" }, //mechanics - deathSave: {type: Schemas.DeathSave}, + 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: { + 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}, + "settings.experiencesInc": { type: Number, defaultValue: 20 }, //slowed down by carrying too much? - "settings.useVariantEncumbrance": {type: Boolean, defaultValue: false}, - "settings.useStandardEncumbrance": {type: Boolean, defaultValue: true}, + "settings.useVariantEncumbrance": { type: Boolean, defaultValue: false }, + "settings.useStandardEncumbrance": { type: Boolean, defaultValue: true }, //hide spellcasting - "settings.hideSpellcasting": {type: Boolean, defaultValue: false}, + "settings.hideSpellcasting": { type: Boolean, defaultValue: false }, //show to anyone with link "settings.viewPermission": { type: String, @@ -187,19 +187,19 @@ Schemas.Character = new SimpleSchema({ 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}, + "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){ +var attributeBase = preventLoop(function (charId, statName) { check(statName, String); //if it's a damage multiplier, we treat it specially - if (_.contains(DAMAGE_MULTIPLIERS, statName)){ + if (_.contains(DAMAGE_MULTIPLIERS, statName)) { var invulnerabilityCount = Effects.find({ charId: charId, stat: statName, @@ -222,11 +222,11 @@ var attributeBase = preventLoop(function(charId, statName){ operation: "mul", value: 2, }).count(); - if (!resistCount && !vulnCount){ + if (!resistCount && !vulnCount) { return 1; - } else if (resistCount && !vulnCount){ + } else if (resistCount && !vulnCount) { return 0.5; - } else if (!resistCount && vulnCount){ + } else if (!resistCount && vulnCount) { return 2; } else { return 1; @@ -243,18 +243,18 @@ var attributeBase = preventLoop(function(charId, statName){ charId: charId, stat: statName, enabled: true, - operation: {$in: ["base", "add", "mul", "min", "max"]}, - }).forEach(function(effect) { + operation: { $in: ["base", "add", "mul", "min", "max"] }, + }).forEach(function (effect) { value = evaluateEffect(charId, effect); - if (effect.operation === "base"){ + if (effect.operation === "base") { if (value > base) base = value; - } else if (effect.operation === "add"){ + } else if (effect.operation === "add") { add += value; - } else if (effect.operation === "mul"){ + } else if (effect.operation === "mul") { mul *= value; - } else if (effect.operation === "min"){ + } else if (effect.operation === "min") { if (value > min) min = value; - } else if (effect.operation === "max"){ + } else if (effect.operation === "max") { if (value < max) max = value; } }); @@ -263,18 +263,18 @@ var attributeBase = preventLoop(function(charId, statName){ if (result < min) result = min; if (result > max) result = max; // Don't round carry multiplier - if (statName === "carryMultiplier"){ + if (statName === "carryMultiplier") { return result; } return Math.floor(result); }); if (Meteor.isClient) { - Template.registerHelper("characterCalculate", function(func, charId, input) { + Template.registerHelper("characterCalculate", function (func, charId, input) { try { return Characters.calculate[func](charId, input); - } catch (e){ - if (!Characters.calculate[func]){ + } catch (e) { + if (!Characters.calculate[func]) { throw new Error(func + "is not a function name"); } else { throw e; @@ -284,10 +284,10 @@ if (Meteor.isClient) { } //create a local memoize with a argument concatenating hash function -var memoize = function(f) { +var memoize = function (f) { if (Meteor.isServer) return f; - return Tracker.memoize(f, function() { - return _.reduce(arguments, function(memo, arg) { + return Tracker.memoize(f, function () { + return _.reduce(arguments, function (memo, arg) { return memo + arg; }, ""); }); @@ -295,13 +295,13 @@ var memoize = function(f) { //memoize funcitons that have finds and slow loops Characters.calculate = { - getField: function(charId, fieldName) { + getField: function (charId, fieldName) { var fieldSelector = {}; fieldSelector[fieldName] = 1; - var char = Characters.findOne(charId, {fields: fieldSelector}); + var char = Characters.findOne(charId, { fields: fieldSelector }); if (!char) return; var field = char[fieldName]; - if (field === undefined){ + if (field === undefined) { throw new Meteor.Error( "getField failed", "getField could not find field " + @@ -312,8 +312,8 @@ Characters.calculate = { } return field; }, - fieldValue: function(charId, fieldName) { - if (!Schemas.Character.schema(fieldName)){ + 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 @@ -321,17 +321,17 @@ Characters.calculate = { } //duck typing to get the right value function //.ability implies skill - if (Schemas.Character.schema(fieldName + ".ability")){ + if (Schemas.Character.schema(fieldName + ".ability")) { return Characters.calculate.skillMod(charId, fieldName); } //adjustment implies attribute - if (Schemas.Character.schema(fieldName + ".adjustment")){ + 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){ + attributeValue: memoize(function (charId, attributeName) { var attribute = Characters.calculate.getField(charId, attributeName); if (!attribute) return; //base value @@ -340,10 +340,10 @@ Characters.calculate = { value += attribute.adjustment; return value; }), - attributeBase: memoize(function(charId, attributeName){ + attributeBase: memoize(function (charId, attributeName) { return attributeBase(charId, attributeName); }), - skillMod: memoize(preventLoop(function(charId, skillName){ + skillMod: memoize(preventLoop(function (charId, skillName) { var skill = Characters.calculate.getField(charId, skillName); if (!skill) return; //get the final value of the ability score @@ -369,16 +369,16 @@ Characters.calculate = { charId: charId, stat: skillName, enabled: true, - operation: {$in: ["base", "add", "mul", "min", "max"]}, - }).forEach(function(effect) { + operation: { $in: ["base", "add", "mul", "min", "max"] }, + }).forEach(function (effect) { value = evaluateEffect(charId, effect); - if (effect.operation === "add"){ + if (effect.operation === "add") { add += value; - } else if (effect.operation === "mul"){ + } else if (effect.operation === "mul") { mul *= value; - } else if (effect.operation === "min"){ + } else if (effect.operation === "min") { if (value > min) min = value; - } else if (effect.operation === "max"){ + } else if (effect.operation === "max") { if (value < max) max = value; } }); @@ -388,83 +388,83 @@ Characters.calculate = { return Math.floor(result); })), - proficiency: memoize(function(charId, skillName){ + proficiency: memoize(function (charId, skillName) { //return largest value in proficiency array var prof = Proficiencies.findOne( - {charId: charId, name: skillName, enabled: true}, - {sort: {value: -1}} + { charId: charId, name: skillName, enabled: true }, + { sort: { value: -1 } } ); return prof && prof.value || 0; }), - passiveSkill: memoize(function(charId, skillName){ + 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){ + { 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){ + advantage: memoize(function (charId, skillName) { var advantage = Effects.find( - {charId: charId, stat: skillName, enabled: true, operation: "advantage"} + { charId: charId, stat: skillName, enabled: true, operation: "advantage" } ).count(); var disadvantage = Effects.find( - {charId: charId, stat: skillName, enabled: true, operation: "disadvantage"} + { 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){ + abilityMod: function (charId, attribute) { return getMod( Characters.calculate.attributeValue(charId, attribute) ); }, - passiveAbility: function(charId, attribute){ + passiveAbility: function (charId, attribute) { var mod = +getMod(Characters.calculate.attributeValue(charId, attribute)); return 10 + mod; }, - xpLevel: function(charId){ + xpLevel: function (charId) { var xp = Characters.calculate.experience(charId); - for (var i = 0; i < 19; i++){ - if (xp < XP_TABLE[i]){ + 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){ + level: memoize(function (charId) { var level = 0; - Classes.find({charId: charId}).forEach(function(cls){ + Classes.find({ charId: charId }).forEach(function (cls) { level += cls.level; }); return level; }), - experience: memoize(function(charId){ + experience: memoize(function (charId) { var xp = 0; Experiences.find( - {charId: charId}, - {fields: {value: 1}} - ).forEach(function(e){ + { charId: charId }, + { fields: { value: 1 } } + ).forEach(function (e) { xp += e.value; }); return xp; }), }; -var deprecated = function() { +var deprecated = function () { //var err = new Error("this function has been deprecated"); var name = ""; - if (Template.instance()){ + if (Template.instance()) { name = Template.instance().view.name; } var logString = "this function has been deprecated \n"; - if (name){ + if (name) { logString += "View: " + name + "\n\n"; } //logString += err.stack + "\n\n---------------------\n\n"; @@ -477,104 +477,104 @@ var deprecated = function() { Characters.helpers({ //returns the value stored in the field requested //will set up dependencies on just that field - getField : function(fieldName){ + getField: function (fieldName) { deprecated(); return Characters.calculate.getField(this._id, fieldName); }, //returns the value of a field - fieldValue : function(fieldName){ + fieldValue: function (fieldName) { deprecated(); return Characters.calculate.fieldValue(this._id, fieldName); }, - attributeValue: function(attributeName){ + attributeValue: function (attributeName) { deprecated(); return Characters.calculate.attributeValue(this._id, attributeName); }, - attributeBase: function(attributeName){ + attributeBase: function (attributeName) { deprecated(); return Characters.calculate.attributeBase(this._id, attributeName); }, - skillMod: function(skillName){ + skillMod: function (skillName) { deprecated(); return Characters.calculate.skillMod(this._id, skillName); }, - proficiency: function(skillName){ + proficiency: function (skillName) { deprecated(); return Characters.calculate.proficiency(this._id, skillName); }, - passiveSkill: function(skillName){ + passiveSkill: function (skillName) { deprecated(); return Characters.calculate.passiveSkill(this._id, skillName); }, - advantage: function(skillName){ + advantage: function (skillName) { deprecated(); return Characters.calculate.advantage(this._id, skillName); }, - abilityMod: function(attribute){ + abilityMod: function (attribute) { deprecated(); return Characters.calculate.abilityMod(this._id, attribute); }, - passiveAbility: function(attribute){ + passiveAbility: function (attribute) { deprecated(); return Characters.calculate.passiveAbility(this._id, attribute); }, - xpLevel: function(){ + xpLevel: function () { deprecated(); return Characters.calculate.xpLevel(this._id); }, - level: function(){ + level: function () { deprecated(); return Characters.calculate.level(this._id); }, - experience: function(){ + 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}); +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.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}) || "-"; + 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()){ + if (!Characters.find({ owner: userId }).count()) { doc.settings.newUserExperience = true; } }); } Characters.allow({ - insert: function(userId, doc) { + 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) { + 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) { + remove: function (userId, doc) { // can only remove your own documents return doc.owner === userId; }, @@ -582,7 +582,7 @@ Characters.allow({ }); Characters.deny({ - update: function(userId, doc, fields, modifier) { + update: function (userId, doc, fields, modifier) { // can't change owners unless you are the current owner return _.contains(fields, "owner") && doc.owner !== userId; } diff --git a/app/client/views/character/stats/healthCard/healthCard.js b/app/client/views/character/stats/healthCard/healthCard.js index 1c1e7751..cd765a3d 100644 --- a/app/client/views/character/stats/healthCard/healthCard.js +++ b/app/client/views/character/stats/healthCard/healthCard.js @@ -3,18 +3,18 @@ const currentId = () => Template.currentData()._id; // Use binding to ensure max is always set before value to prevent value clamping poorly Template.healthCard.binding({ "#hitPointSlider": { - max: () => Characters.calculate.attributeBase(currentId() , "hitPoints"), - value: () => Characters.calculate.attributeValue(currentId() , "hitPoints"), + max: () => Characters.calculate.attributeBase(currentId(), "hitPoints"), + value: () => Characters.calculate.attributeValue(currentId(), "hitPoints"), }, }); // Reset the old value between characters so that we don't get red health lost // bar when changing character -Template.healthCard.onRendered(function(){ +Template.healthCard.onRendered(function () { let oldId = Template.currentData()._id; this.autorun(() => { const id = Template.currentData()._id; - if (oldId !== id){ + if (oldId !== id) { this.find("#hitPointSlider").resetOldValue(); var thpSlider = this.find("#temporaryHitPointSlider"); thpSlider && thpSlider.resetOldValue(); @@ -24,49 +24,49 @@ Template.healthCard.onRendered(function(){ }); Template.healthCard.helpers({ - extraHitPoints: function(){ - return TemporaryHitPoints.find({charId: this._id}); + extraHitPoints: function () { + return TemporaryHitPoints.find({ charId: this._id }); }, - showDeathSave: function(){ + showDeathSave: function () { return Characters.calculate.attributeValue(this._id, "hitPoints") <= 0; }, - deathSaveObject: function(){ - var char = Characters.findOne(this._id, {fields: {deathSave: 1}}); + deathSaveObject: function () { + var char = Characters.findOne(this._id, { fields: { deathSave: 1 } }); return char && char.deathSave; }, - failIcon: function(num){ + failIcon: function (num) { if (num <= this.fail) return "radio-button-checked"; else return "radio-button-unchecked"; }, - passIcon: function(num){ + passIcon: function (num) { if (num <= this.pass) return "radio-button-checked"; else return "radio-button-unchecked"; }, - failDisabled: function(num){ + failDisabled: function (num) { return !(num === this.fail || num - 1 === this.fail); }, - passDisabled: function(num){ + passDisabled: function (num) { return !(num === this.pass || num - 1 === this.pass); }, - dead: function(){ + dead: function () { return this.fail >= 3; }, - multipliers: function(){ + multipliers: function () { // jscs:disable maximumLineLength var multipliers = [ - {name: "Acid", value: Characters.calculate.attributeValue(this._id, "acidMultiplier")}, - {name: "Bludgeoning", value: Characters.calculate.attributeValue(this._id, "bludgeoningMultiplier")}, - {name: "Cold", value: Characters.calculate.attributeValue(this._id, "coldMultiplier")}, - {name: "Fire", value: Characters.calculate.attributeValue(this._id, "fireMultiplier")}, - {name: "Force", value: Characters.calculate.attributeValue(this._id, "forceMultiplier")}, - {name: "Lightning", value: Characters.calculate.attributeValue(this._id, "lightningMultiplier")}, - {name: "Necrotic", value: Characters.calculate.attributeValue(this._id, "necroticMultiplier")}, - {name: "Piercing", value: Characters.calculate.attributeValue(this._id, "piercingMultiplier")}, - {name: "Poison", value: Characters.calculate.attributeValue(this._id, "poisonMultiplier")}, - {name: "Psychic", value: Characters.calculate.attributeValue(this._id, "psychicMultiplier")}, - {name: "Radiant", value: Characters.calculate.attributeValue(this._id, "radiantMultiplier")}, - {name: "Slashing", value: Characters.calculate.attributeValue(this._id, "slashingMultiplier")}, - {name: "Thunder", value: Characters.calculate.attributeValue(this._id, "thunderMultiplier")}, + { name: "Acid", value: Characters.calculate.attributeValue(this._id, "acidMultiplier") }, + { name: "Bludgeoning", value: Characters.calculate.attributeValue(this._id, "bludgeoningMultiplier") }, + { name: "Cold", value: Characters.calculate.attributeValue(this._id, "coldMultiplier") }, + { name: "Fire", value: Characters.calculate.attributeValue(this._id, "fireMultiplier") }, + { name: "Force", value: Characters.calculate.attributeValue(this._id, "forceMultiplier") }, + { name: "Lightning", value: Characters.calculate.attributeValue(this._id, "lightningMultiplier") }, + { name: "Necrotic", value: Characters.calculate.attributeValue(this._id, "necroticMultiplier") }, + { name: "Piercing", value: Characters.calculate.attributeValue(this._id, "piercingMultiplier") }, + { name: "Poison", value: Characters.calculate.attributeValue(this._id, "poisonMultiplier") }, + { name: "Psychic", value: Characters.calculate.attributeValue(this._id, "psychicMultiplier") }, + { name: "Radiant", value: Characters.calculate.attributeValue(this._id, "radiantMultiplier") }, + { name: "Slashing", value: Characters.calculate.attributeValue(this._id, "slashingMultiplier") }, + { name: "Thunder", value: Characters.calculate.attributeValue(this._id, "thunderMultiplier") }, ]; // jscs:enable maximumLineLength multipliers = _.groupBy(multipliers, "value"); @@ -79,64 +79,61 @@ Template.healthCard.helpers({ }); Template.healthCard.events({ - "change #hitPointSlider": function(event){ + "change #hitPointSlider": _.debounce(function (event) { var value = event.currentTarget.value; var base = Characters.calculate.attributeBase(this._id, "hitPoints"); var adjustment = value - base; - Characters.update(this._id, {$set: {"hitPoints.adjustment": adjustment}}); + var modifier = { $set: { "hitPoints.adjustment": adjustment } }; //reset the death saves if we are gaining HP - if (value > 0) - Characters.update( - this._id, - {$set: { - "deathSave.pass": 0, - "deathSave.fail": 0, - "deathSave.stable": false, - }} - ); - }, - "change #temporaryHitPointSlider": function(event){ //this is the actual THP stat + if (value > 0) { + modifier.$set["deathSave.pass"] = 0; + modifier.$set["deathSave.fail"] = 0; + modifier.$set["deathSave.stable"] = false; + } + Characters.update(this._id, modifier); + }, 300), + "change #temporaryHitPointSlider": _.debounce(function (event) { //this is the actual THP stat var value = event.currentTarget.value; var base = Characters.calculate.attributeBase(this._id, "tempHP"); var adjustment = value - base; - Characters.update(this._id, {$set: {"tempHP.adjustment": adjustment}}); - }, - "change .extraHitPointSlider": function(event){ //this is the extra bars + Characters.update(this._id, { $set: { "tempHP.adjustment": adjustment } }); + }, 300), + "change .extraHitPointSlider": _.debounce(function (event) { //this is the extra bars var value = event.currentTarget.value; var used = this.maximum - value; - TemporaryHitPoints.update(this._id, {$set: {"used": used}}); - }, - "click .deleteEHP": function(event){ + TemporaryHitPoints.update(this._id, { $set: { "used": used } }); + }, 300), + "click .deleteEHP": function (event) { TemporaryHitPoints.remove(this._id); }, - "click #addExtraHP": function(event){ + "click #addExtraHP": function (event) { pushDialogStack({ template: "addEHPDialog", - data: {charId: this._id}, + data: { charId: this._id }, element: event.currentTarget.parentElement, }); }, - "click .failBubble": function(event){ + "click .failBubble": function (event) { if (event.currentTarget.disabled) return; var char = Template.parentData(); - if (event.currentTarget.icon === "radio-button-unchecked"){ - Characters.update(char._id, {$set: {"deathSave.fail": this.fail + 1}}); + if (event.currentTarget.icon === "radio-button-unchecked") { + Characters.update(char._id, { $set: { "deathSave.fail": this.fail + 1 } }); } else { - Characters.update(char._id, {$set: {"deathSave.fail": this.fail - 1}}); + Characters.update(char._id, { $set: { "deathSave.fail": this.fail - 1 } }); } }, - "click .passBubble": function(event){ + "click .passBubble": function (event) { if (event.currentTarget.disabled) return; var char = Template.parentData(); - if (event.currentTarget.icon === "radio-button-unchecked"){ - Characters.update(char._id, {$set: {"deathSave.pass": this.pass + 1}}); + if (event.currentTarget.icon === "radio-button-unchecked") { + Characters.update(char._id, { $set: { "deathSave.pass": this.pass + 1 } }); } else { - Characters.update(char._id, {$set: {"deathSave.pass": this.pass - 1}}); + Characters.update(char._id, { $set: { "deathSave.pass": this.pass - 1 } }); } }, - "click #stableButton": function(event){ + "click #stableButton": function (event) { var char = Characters.findOne(Template.parentData()._id, { - fields: {deathSave: 1} + fields: { deathSave: 1 } }); Characters.update(char._id, { $set: { diff --git a/app/server/lib/configuration/globalRateLimit.js b/app/server/lib/configuration/globalRateLimit.js new file mode 100644 index 00000000..ddd84634 --- /dev/null +++ b/app/server/lib/configuration/globalRateLimit.js @@ -0,0 +1,16 @@ +const logRateLimit = _.debounce(function (ruleInput) { + console.log(`Rate limit on ${ruleInput.name} by user ${ruleInput.userId} at IP ${ruleInput.clientAddress}`); +}, 500); + +DDPRateLimiter.addRule({ + type: 'method', + name(name) { + return true; + }, + // Rate limit per connection ID + connectionId() { return true; } +}, 10, 1000, (reply, ruleInput) => { + if (!reply.allowed) { + logRateLimit(ruleInput); + } +});