start fixing commong

This commit is contained in:
Matteo Pagliazzi
2019-10-09 16:51:17 +02:00
parent 9cd43db401
commit 0c27fb24a5
76 changed files with 442 additions and 275 deletions

View File

@@ -72,7 +72,7 @@
"npm": "^6"
},
"scripts": {
"lint": "eslint --ext .js .",
"lint": "eslint --ext .js --fix ./website/server",
"test": "npm run lint && gulp test && gulp apidoc",
"test:build": "gulp test:prepare:build",
"test:api-v3": "gulp test:api-v3",

View File

@@ -7,4 +7,4 @@ if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-e
pathToCommon = './script/index';
}
module.exports = require(pathToCommon);
module.exports = require(pathToCommon); // eslint-disable-line import/no-dynamic-require

View File

@@ -1,5 +0,0 @@
{
"extends": [
"habitrpg/lib/node",
]
}

View File

@@ -0,0 +1,5 @@
module.exports = {
extends: [
"habitrpg/lib/node",
]
}

View File

@@ -12,7 +12,8 @@ export const MIN_SHORTNAME_SIZE_FOR_CHALLENGES = 3;
export const CHAT_FLAG_LIMIT_FOR_HIDING = 2; // hide posts that have this many flags
export const CHAT_FLAG_FROM_MOD = 5; // a flag from a moderator counts as this many flags
export const CHAT_FLAG_FROM_SHADOW_MUTE = 10; // a shadow-muted user's post starts with this many flags
// a shadow-muted user's post starts with this many flags
export const CHAT_FLAG_FROM_SHADOW_MUTE = 10;
// @TODO use those constants to replace hard-coded numbers
export const SUPPORTED_SOCIAL_NETWORKS = [

View File

@@ -1,4 +1,4 @@
import prefill from './prefill.js';
import prefill from './prefill';
export default prefill({
none: {},

View File

@@ -1,4 +1,4 @@
import prefill from '../prefill.js';
import prefill from '../prefill';
export default prefill({
0: {},

View File

@@ -1,5 +1,5 @@
import prefill from '../prefill.js';
import sets from '../sets.js';
import prefill from '../prefill';
import sets from '../sets';
export default prefill({
0: {},

View File

@@ -1,5 +1,5 @@
import sets from '../sets.js';
import prefill from '../prefill.js';
import sets from '../sets';
import prefill from '../prefill';
export default prefill({
0: {},

View File

@@ -1,5 +1,5 @@
import sets from '../sets.js';
import prefill from '../prefill.js';
import sets from '../sets';
import prefill from '../prefill';
export default prefill({
white: {},

View File

@@ -1,4 +1,4 @@
import prefill from '../prefill.js';
import prefill from '../prefill';
export default prefill({
0: {},

View File

@@ -1,9 +1,9 @@
import bangs from './bangs.js';
import base from './base.js';
import beard from './beard.js';
import color from './color.js';
import flower from './flower.js';
import mustache from './mustache.js';
import bangs from './bangs';
import base from './base';
import beard from './beard';
import color from './color';
import flower from './flower';
import mustache from './mustache';
export default {
color,

View File

@@ -1,5 +1,5 @@
import sets from '../sets.js';
import prefill from '../prefill.js';
import sets from '../sets';
import prefill from '../prefill';
export default prefill({
0: {},

View File

@@ -1,5 +1,5 @@
import prefill from '../prefill.js';
import sets from '../sets.js';
import prefill from '../prefill';
import sets from '../sets';
export default prefill({
0: {},

View File

@@ -1,11 +1,11 @@
import forOwn from 'lodash/forOwn';
import clone from 'lodash/clone';
import hair from './hair';
import shirts from './shirt.js';
import skins from './skin.js';
import sizes from './size.js';
import backgrounds from './backgrounds.js';
import chairs from './chair.js';
import shirts from './shirt';
import skins from './skin';
import sizes from './size';
import backgrounds from './backgrounds';
import chairs from './chair';
const reorderedBgs = {};

View File

@@ -10,14 +10,28 @@ export default prefill({
winterHairColors: { setPrice: 5, availableUntil: '2016-01-01' },
pastelHairColors: { setPrice: 5, availableUntil: '2016-01-01' },
rainbowHairColors: { setPrice: 5, text: t('rainbowColors') },
shimmerHairColors: {setPrice: 5, availableFrom: '2019-04-09', availableUntil: '2019-05-02', text: t('shimmerColors')},
hauntedHairColors: {setPrice: 5, availableFrom: '2019-10-08', availableUntil: '2019-11-02', text: t('hauntedColors')},
winteryHairColors: {setPrice: 5, availableFrom: '2019-01-08', availableUntil: '2019-02-02', text: t('winteryColors')},
shimmerHairColors: {
setPrice: 5, availableFrom: '2019-04-09', availableUntil: '2019-05-02', text: t('shimmerColors'),
},
hauntedHairColors: {
setPrice: 5, availableFrom: '2019-10-08', availableUntil: '2019-11-02', text: t('hauntedColors'),
},
winteryHairColors: {
setPrice: 5, availableFrom: '2019-01-08', availableUntil: '2019-02-02', text: t('winteryColors'),
},
rainbowSkins: { setPrice: 5, text: t('rainbowSkins') },
animalSkins: { setPrice: 5, text: t('animalSkins') },
pastelSkins: {setPrice: 5, availableFrom: '2019-04-09', availableUntil: '2019-05-02', text: t('pastelSkins')},
pastelSkins: {
setPrice: 5, availableFrom: '2019-04-09', availableUntil: '2019-05-02', text: t('pastelSkins'),
},
spookySkins: { setPrice: 5, availableUntil: '2016-01-01', text: t('spookySkins') },
supernaturalSkins: {setPrice: 5, availableFrom: '2019-10-08', availableUntil: '2019-11-02', text: t('supernaturalSkins')},
splashySkins: {setPrice: 5, availableFrom: '2019-07-02', availableUntil: '2019-08-02', text: t('splashySkins')},
winterySkins: {setPrice: 5, availableFrom: '2019-01-08', availableUntil: '2019-02-02', text: t('winterySkins')},
supernaturalSkins: {
setPrice: 5, availableFrom: '2019-10-08', availableUntil: '2019-11-02', text: t('supernaturalSkins'),
},
splashySkins: {
setPrice: 5, availableFrom: '2019-07-02', availableUntil: '2019-08-02', text: t('splashySkins'),
},
winterySkins: {
setPrice: 5, availableFrom: '2019-01-08', availableUntil: '2019-02-02', text: t('winterySkins'),
},
});

View File

@@ -1,5 +1,5 @@
import sets from './sets.js';
import prefill from './prefill.js';
import sets from './sets';
import prefill from './prefill';
export default prefill({
black: {},

View File

@@ -1,5 +1,5 @@
import prefill from './prefill.js';
import sets from './sets.js';
import prefill from './prefill';
import sets from './sets';
export default prefill({
/* eslint-disable quote-props */

View File

@@ -10,7 +10,7 @@ const faq = {
},
};
for (let i = 0; i <= NUMBER_OF_QUESTIONS; i++) {
for (let i = 0; i <= NUMBER_OF_QUESTIONS; i += 1) {
const question = {
question: t(`faqQuestion${i}`),
ios: t(`iosFaqAnswer${i}`),

View File

@@ -29,7 +29,8 @@ const gear = {
};
/*
The gear is exported as a tree (defined above), and a flat list (eg, {weapon_healer_1: .., shield_special_0: ...}) since
The gear is exported as a tree (defined above), and a flat list
(eg, {weapon_healer_1: .., shield_special_0: ...}) since
they are needed in different forms at different points in the app
*/
const flat = {};
@@ -61,8 +62,11 @@ each(GEAR_TYPES, type => {
item.canOwn = user => {
const userHasOwnedItem = ownsItem(key)(user);
const eventIsCurrent = moment().isAfter(item.event.start) && moment().isBefore(item.event.end);
const compatibleWithUserClass = item.specialClass ? user.stats.class === item.specialClass : true;
const eventIsCurrent = moment()
.isAfter(item.event.start) && moment().isBefore(item.event.end);
const compatibleWithUserClass = item.specialClass
? user.stats.class === item.specialClass
: true;
return _canOwn(user) && (userHasOwnedItem || eventIsCurrent) && compatibleWithUserClass;
};

View File

@@ -261,7 +261,10 @@ export default function getLoginIncentives (api) {
},
110: {
rewardKey: ['Pet_Egg_Cactus', 'Pet_Egg_Dragon', 'Pet_Egg_Wolf'],
reward: [api.eggs.BearCub, api.eggs.Cactus, api.eggs.Dragon, api.eggs.FlyingPig, api.eggs.Fox, api.eggs.LionCub, api.eggs.PandaCub, api.eggs.TigerCub, api.eggs.Wolf],
reward: [
api.eggs.BearCub, api.eggs.Cactus, api.eggs.Dragon, api.eggs.FlyingPig,
api.eggs.Fox, api.eggs.LionCub, api.eggs.PandaCub, api.eggs.TigerCub, api.eggs.Wolf,
],
rewardName: 'oneOfAllPetEggs',
assignReward: function assignReward (user) {
if (!user.items.eggs.BearCub) user.items.eggs.BearCub = 0;
@@ -388,7 +391,10 @@ export default function getLoginIncentives (api) {
},
150: {
rewardKey: ['shop_head_special_clandestineCowl', 'shop_armor_special_sneakthiefRobes'],
reward: [api.gear.flat.head_special_clandestineCowl, api.gear.flat.armor_special_sneakthiefRobes],
reward: [
api.gear.flat.head_special_clandestineCowl,
api.gear.flat.armor_special_sneakthiefRobes,
],
assignReward: function assignReward (user) {
user.items.gear.owned.head_special_clandestineCowl = true; // eslint-disable-line camelcase
user.items.gear.owned.armor_special_sneakthiefRobes = true; // eslint-disable-line camelcase
@@ -406,10 +412,13 @@ export default function getLoginIncentives (api) {
},
170: {
rewardKey: ['shop_head_special_snowSovereignCrown', 'shop_armor_special_snowSovereignRobes'],
reward: [api.gear.flat.head_special_snowSovereignCrown, api.gear.flat.armor_special_snowSovereignRobes],
reward: [
api.gear.flat.head_special_snowSovereignCrown,
api.gear.flat.armor_special_snowSovereignRobes,
],
assignReward: function assignReward (user) {
user.items.gear.owned.head_special_snowSovereignCrown = true; // eslint-disable-line camelcase
user.items.gear.owned.armor_special_snowSovereignRobes = true; // eslint-disable-line camelcase
user.items.gear.owned.head_special_snowSovereignCrown = true; // eslint-disable-line camelcase, max-len
user.items.gear.owned.armor_special_snowSovereignRobes = true; // eslint-disable-line camelcase, max-len
if (user.markModified) user.markModified('items.gear.owned');
},
},
@@ -451,7 +460,10 @@ export default function getLoginIncentives (api) {
},
240: {
rewardKey: ['shop_weapon_special_nomadsScimitar', 'shop_armor_special_nomadsCuirass'],
reward: [api.gear.flat.weapon_special_nomadsScimitar, api.gear.flat.armor_special_nomadsCuirass],
reward: [
api.gear.flat.weapon_special_nomadsScimitar,
api.gear.flat.armor_special_nomadsCuirass,
],
assignReward: function assignReward (user) {
user.items.gear.owned.weapon_special_nomadsScimitar = true; // eslint-disable-line camelcase
user.items.gear.owned.armor_special_nomadsCuirass = true; // eslint-disable-line camelcase
@@ -468,7 +480,11 @@ export default function getLoginIncentives (api) {
},
280: {
rewardKey: ['Pet_Food_Meat', 'Pet_Food_Potatoe', 'Pet_Food_Milk'],
reward: [api.food.Meat, api.food.CottonCandyBlue, api.food.CottonCandyPink, api.food.Potatoe, api.food.Honey, api.food.Strawberry, api.food.Chocolate, api.food.Fish, api.food.Milk, api.food.RottenMeat],
reward: [
api.food.Meat, api.food.CottonCandyBlue, api.food.CottonCandyPink,
api.food.Potatoe, api.food.Honey, api.food.Strawberry, api.food.Chocolate,
api.food.Fish, api.food.Milk, api.food.RottenMeat,
],
rewardName: 'threeOfEachFood',
assignReward: function assignReward (user) {
if (!user.items.food.Meat) user.items.food.Meat = 0;
@@ -496,7 +512,10 @@ export default function getLoginIncentives (api) {
},
300: {
rewardKey: ['Pet_Egg_Cactus', 'Pet_Egg_Dragon', 'Pet_Egg_Wolf'],
reward: [api.eggs.BearCub, api.eggs.Cactus, api.eggs.Dragon, api.eggs.FlyingPig, api.eggs.Fox, api.eggs.LionCub, api.eggs.PandaCub, api.eggs.TigerCub, api.eggs.Wolf],
reward: [
api.eggs.BearCub, api.eggs.Cactus, api.eggs.Dragon, api.eggs.FlyingPig,
api.eggs.Fox, api.eggs.LionCub, api.eggs.PandaCub, api.eggs.TigerCub, api.eggs.Wolf,
],
rewardName: 'twoOfAllPetEggs',
assignReward: function assignReward (user) {
if (!user.items.eggs.BearCub) user.items.eggs.BearCub = 0;
@@ -549,7 +568,10 @@ export default function getLoginIncentives (api) {
},
380: {
rewardKey: ['Pet_Egg_Cactus', 'Pet_Egg_Dragon', 'Pet_Egg_Wolf'],
reward: [api.eggs.BearCub, api.eggs.Cactus, api.eggs.Dragon, api.eggs.FlyingPig, api.eggs.Fox, api.eggs.LionCub, api.eggs.PandaCub, api.eggs.TigerCub, api.eggs.Wolf],
reward: [
api.eggs.BearCub, api.eggs.Cactus, api.eggs.Dragon, api.eggs.FlyingPig,
api.eggs.Fox, api.eggs.LionCub, api.eggs.PandaCub, api.eggs.TigerCub, api.eggs.Wolf,
],
rewardName: 'threeOfAllPetEggs',
assignReward: function assignReward (user) {
if (!user.items.eggs.BearCub) user.items.eggs.BearCub = 0;
@@ -575,7 +597,11 @@ export default function getLoginIncentives (api) {
},
400: {
rewardKey: ['Pet_Food_Meat', 'Pet_Food_Potatoe', 'Pet_Food_Milk'],
reward: [api.food.Meat, api.food.CottonCandyBlue, api.food.CottonCandyPink, api.food.Potatoe, api.food.Honey, api.food.Strawberry, api.food.Chocolate, api.food.Fish, api.food.Milk, api.food.RottenMeat],
reward: [
api.food.Meat, api.food.CottonCandyBlue, api.food.CottonCandyPink,
api.food.Potatoe, api.food.Honey, api.food.Strawberry, api.food.Chocolate,
api.food.Fish, api.food.Milk, api.food.RottenMeat,
],
rewardName: 'fourOfEachFood',
assignReward: function assignReward (user) {
if (!user.items.food.Meat) user.items.food.Meat = 0;
@@ -638,10 +664,12 @@ export default function getLoginIncentives (api) {
},
},
};
// When the final check-in prize is added here, change checkinReceivedAllRewardsMessage in website/common/locales/en/loginIncentives.json
// When the final check-in prize is added here,
// change checkinReceivedAllRewardsMessage in website/common/locales/en/loginIncentives.json
// to say "You have received the final Check-In prize!". Confirm the message with Lemoness first.
// Add reference link to next reward and add filler days so we have a map to reference the next reward from any day
// Add reference link to next reward and add filler days
// so we have a map to reference the next reward from any day
// We could also, use a list, but then we would be cloning each of the rewards.
// Create a new array if we want the loginIncentives to be immutable in the future
let nextRewardKey;

View File

@@ -3505,14 +3505,15 @@ const quests = {
};
each(quests, (v, key) => {
let b;
defaults(v, {
key,
canBuy () {
return true;
},
});
b = v.boss;
const b = v.boss;
if (b) {
defaults(b, {
str: 1,

View File

@@ -11,23 +11,32 @@ import updateStats from '../fns/updateStats';
---------------------------------------------------------------
Text, notes, and mana are obvious. The rest:
* {target}: one of [task, self, party, user]. This is very important, because if the cast() function is expecting one
thing and receives another, it will cause errors. `self` is used for self buffs, multi-task debuffs, AOEs (eg, meteor-shower),
* {target}: one of [task, self, party, user].
* This is very important, because if the cast() function is expecting one
thing and receives another, it will cause errors.
`self` is used for self buffs, multi-task debuffs, AOEs (eg, meteor-shower),
etc. Basically, use self for anything that's not [task, party, user] and is an instant-cast
* {cast}: the function that's run to perform the ability's action. This is pretty slick - because this is exported to the
web, this function can be performed on the client and on the server. `user` param is self (needed for determining your
own stats for effectiveness of cast), and `target` param is one of [task, party, user]. In the case of `self` skills,
you act on `user` instead of `target`. You can trust these are the correct objects, as long as the `target` attr of the
spell is correct. Take a look at habitrpg/website/server/models/user.js and habitrpg/website/server/models/task.js for what attributes are
available on each model. Note `task.value` is its "redness". If party is passed in, it's an array of users,
* {cast}: the function that's run to perform the ability's action.
This is pretty slick - because this is exported to the
web, this function can be performed on the client and on the server.
`user` param is self (needed for determining your
own stats for effectiveness of cast), and `target` param is one of [task, party, user].
In the case of `self` skills,
you act on `user` instead of `target`. You can trust these are the correct objects,
as long as the `target` attr of the
spell is correct. Take a look at habitrpg/website/server/models/user.js and
habitrpg/website/server/models/task.js for what attributes are
available on each model. Note `task.value` is its "redness".
If party is passed in, it's an array of users,
so you'll want to iterate over them like: `_.each(target,function(member){...})`
Note, user.stats.mp is docked after automatically (it's appended to functions automatically down below in an _.each)
Note, user.stats.mp is docked after automatically
(it's appended to functions automatically down below in an _.each)
*/
function diminishingReturns (bonus, max, halfway) {
if (!halfway) halfway = max / 2;
if (!halfway) halfway = max / 2; // eslint-disable-line no-param-reassign
return max * (bonus / (bonus + halfway));
}
@@ -274,8 +283,8 @@ spells.special = {
target.stats.buffs.shinySeed = false;
target.stats.buffs.seafoam = false;
if (!target.achievements.snowball) target.achievements.snowball = 0;
target.achievements.snowball++;
user.items.special.snowball--;
target.achievements.snowball += 1;
user.items.special.snowball -= 1;
},
},
salt: {
@@ -305,8 +314,8 @@ spells.special = {
target.stats.buffs.shinySeed = false;
target.stats.buffs.seafoam = false;
if (!target.achievements.spookySparkles) target.achievements.spookySparkles = 0;
target.achievements.spookySparkles++;
user.items.special.spookySparkles--;
target.achievements.spookySparkles += 1;
user.items.special.spookySparkles -= 1;
},
},
opaquePotion: {
@@ -336,8 +345,8 @@ spells.special = {
target.stats.buffs.shinySeed = true;
target.stats.buffs.seafoam = false;
if (!target.achievements.shinySeed) target.achievements.shinySeed = 0;
target.achievements.shinySeed++;
user.items.special.shinySeed--;
target.achievements.shinySeed += 1;
user.items.special.shinySeed -= 1;
},
},
petalFreePotion: {
@@ -367,8 +376,8 @@ spells.special = {
target.stats.buffs.shinySeed = false;
target.stats.buffs.seafoam = true;
if (!target.achievements.seafoam) target.achievements.seafoam = 0;
target.achievements.seafoam++;
user.items.special.seafoam--;
target.achievements.seafoam += 1;
user.items.special.seafoam -= 1;
},
},
sand: {
@@ -395,11 +404,11 @@ spells.special = {
cast (user, target) {
if (user === target) {
if (!user.achievements.nye) user.achievements.nye = 0;
user.achievements.nye++;
user.achievements.nye += 1;
} else {
each([user, target], u => {
if (!u.achievements.nye) u.achievements.nye = 0;
u.achievements.nye++;
u.achievements.nye += 1;
});
}
@@ -432,11 +441,11 @@ spells.special = {
cast (user, target) {
if (user === target) {
if (!user.achievements.valentine) user.achievements.valentine = 0;
user.achievements.valentine++;
user.achievements.valentine += 1;
} else {
each([user, target], u => {
if (!u.achievements.valentine) u.achievements.valentine = 0;
u.achievements.valentine++;
u.achievements.valentine += 1;
});
}
@@ -469,11 +478,11 @@ spells.special = {
cast (user, target) {
if (user === target) {
if (!user.achievements.greeting) user.achievements.greeting = 0;
user.achievements.greeting++;
user.achievements.greeting += 1;
} else {
each([user, target], u => {
if (!u.achievements.greeting) u.achievements.greeting = 0;
u.achievements.greeting++;
u.achievements.greeting += 1;
});
}
@@ -506,11 +515,11 @@ spells.special = {
cast (user, target) {
if (user === target) {
if (!user.achievements.thankyou) user.achievements.thankyou = 0;
user.achievements.thankyou++;
user.achievements.thankyou += 1;
} else {
each([user, target], u => {
if (!u.achievements.thankyou) u.achievements.thankyou = 0;
u.achievements.thankyou++;
u.achievements.thankyou += 1;
});
}
@@ -543,11 +552,11 @@ spells.special = {
cast (user, target) {
if (user === target) {
if (!user.achievements.birthday) user.achievements.birthday = 0;
user.achievements.birthday++;
user.achievements.birthday += 1;
} else {
each([user, target], u => {
if (!u.achievements.birthday) u.achievements.birthday = 0;
u.achievements.birthday++;
u.achievements.birthday += 1;
});
}
@@ -580,11 +589,11 @@ spells.special = {
cast (user, target) {
if (user === target) {
if (!user.achievements.congrats) user.achievements.congrats = 0;
user.achievements.congrats++;
user.achievements.congrats += 1;
} else {
each([user, target], u => {
if (!u.achievements.congrats) u.achievements.congrats = 0;
u.achievements.congrats++;
u.achievements.congrats += 1;
});
}
@@ -617,11 +626,11 @@ spells.special = {
cast (user, target) {
if (user === target) {
if (!user.achievements.getwell) user.achievements.getwell = 0;
user.achievements.getwell++;
user.achievements.getwell += 1;
} else {
each([user, target], u => {
if (!u.achievements.getwell) u.achievements.getwell = 0;
u.achievements.getwell++;
u.achievements.getwell += 1;
});
}
@@ -654,11 +663,11 @@ spells.special = {
cast (user, target) {
if (user === target) {
if (!user.achievements.goodluck) user.achievements.goodluck = 0;
user.achievements.goodluck++;
user.achievements.goodluck += 1;
} else {
each([user, target], u => {
if (!u.achievements.goodluck) u.achievements.goodluck = 0;
u.achievements.goodluck++;
u.achievements.goodluck += 1;
});
}

View File

@@ -50,6 +50,6 @@ const subscriptionBlocks = {
},
};
each(subscriptionBlocks, (b, k) => b.key = k);
each(subscriptionBlocks, (b, k) => { b.key = k; });
export default subscriptionBlocks;

View File

@@ -1,6 +1,6 @@
import t from './translation';
export const tasksByCategory = {
export const tasksByCategory = { // eslint-disable-line import/prefer-default-export
work: [
{
type: 'habit',

View File

@@ -9,9 +9,11 @@ import gear from './gear';
const mystery = mysterySets;
each(mystery, (v, k) => v.items = filter(gear.flat, {
each(mystery, (v, k) => {
v.items = filter(gear.flat, {
mystery: k,
}));
});
});
const timeTravelerStore = user => {
let ownedKeys;

View File

@@ -10,7 +10,7 @@ export function beastMasterProgress (pets = {}) {
let count = 0;
each(DROP_ANIMALS, animal => {
if (pets[animal] > 0 || pets[animal] === -1) count++;
if (pets[animal] > 0 || pets[animal] === -1) count += 1;
});
return count;
@@ -20,7 +20,7 @@ export function beastCount (pets = {}) {
let count = 0;
each(DROP_ANIMALS, animal => {
if (pets[animal] > 0) count++;
if (pets[animal] > 0) count += 1;
});
return count;
@@ -30,7 +30,7 @@ export function dropPetsCurrentlyOwned (pets = {}) {
let count = 0;
each(DROP_ANIMALS, animal => {
if (pets[animal] > 0) count++;
if (pets[animal] > 0) count += 1;
});
return count;
@@ -40,7 +40,7 @@ export function mountMasterProgress (mounts = {}) {
let count = 0;
each(DROP_ANIMALS, animal => {
if (mounts[animal]) count++;
if (mounts[animal]) count += 1;
});
return count;

View File

@@ -23,8 +23,10 @@ export const DAY_MAPPING_STRING_TO_NUMBER = invert(DAY_MAPPING);
/*
Each time we perform date maths (cron, task-due-days, etc), we need to consider user preferences.
Specifically {dayStart} (custom day start) and {timezoneOffset}. This function sanitizes / defaults those values.
{now} is also passed in for various purposes, one example being the test scripts scripts testing different "now" times.
Specifically {dayStart} (custom day start) and {timezoneOffset}.
This function sanitizes / defaults those values.
{now} is also passed in for various purposes,
one example being the test scripts scripts testing different "now" times.
*/
function sanitizeOptions (o) {
@@ -34,7 +36,7 @@ function sanitizeOptions (o) {
let timezoneOffset;
const timezoneOffsetDefault = Number(moment().zone());
if (isFinite(o.timezoneOffsetOverride)) {
if (Number.isFinite(o.timezoneOffsetOverride)) {
timezoneOffset = Number(o.timezoneOffsetOverride);
} else if (Number.isFinite(o.timezoneOffset)) {
timezoneOffset = Number(o.timezoneOffset);
@@ -62,10 +64,17 @@ export function startOfWeek (options = {}) {
}
/*
This is designed for use with any date that has an important time portion (e.g., when comparing the current date-time with the previous cron's date-time for determing if cron should run now).
It changes the time portion of the date-time to be the Custom Day Start hour, so that the date-time is now the user's correct start of day.
It SUBTRACTS a day if the date-time's original hour is before CDS (e.g., if your CDS is 5am and it's currently 4am, it's still the previous day).
This is NOT suitable for manipulating any dates that are displayed to the user as a date with no time portion, such as a Daily's Start Dates (e.g., a Start Date of today shows only the date, so it should be considered to be today even if the hidden time portion is before CDS).
This is designed for use with any date that has an important time portion
(e.g., when comparing the current date-time with the previous cron's date-time
for determing if cron should run now).
It changes the time portion of the date-time to be the Custom Day Start hour,
so that the date-time is now the user's correct start of day.
It SUBTRACTS a day if the date-time's original hour is before CDS
(e.g., if your CDS is 5am and it's currently 4am, it's still the previous day).
This is NOT suitable for manipulating any dates that are displayed to the user
as a date with no time portion, such as a Daily's Start Dates
(e.g., a Start Date of today shows only the date,
so it should be considered to be today even if the hidden time portion is before CDS).
*/
export function startOfDay (options = {}) {
@@ -92,7 +101,8 @@ export function daysSince (yesterday, options = {}) {
}
/*
Should the user do this task on this date, given the task's repeat options and user.preferences.dayStart?
Should the user do this task on this date,
given the task's repeat options and user.preferences.dayStart?
*/
export function shouldDo (day, dailyTask, options = {}) {
@@ -102,9 +112,12 @@ export function shouldDo (day, dailyTask, options = {}) {
const o = sanitizeOptions(options);
const startOfDayWithCDSTime = startOfDay(defaults({ now: day }, o));
// The time portion of the Start Date is never visible to or modifiable by the user so we must ignore it.
// Therefore, we must also ignore the time portion of the user's day start (startOfDayWithCDSTime), otherwise the date comparison will be wrong for some times.
// NB: The user's day start date has already been converted to the PREVIOUS day's date if the time portion was before CDS.
// The time portion of the Start Date is never visible to
// or modifiable by the user so we must ignore it.
// Therefore, we must also ignore the time portion of the user's day start
// (startOfDayWithCDSTime), otherwise the date comparison will be wrong for some times.
// NB: The user's day start date has already been converted to the PREVIOUS
// day's date if the time portion was before CDS.
const startDate = moment(dailyTask.startDate).zone(o.timezoneOffset).startOf('day');
@@ -115,7 +128,7 @@ export function shouldDo (day, dailyTask, options = {}) {
const daysOfTheWeek = [];
if (dailyTask.repeat) {
for (const [repeatDay, active] of Object.entries(dailyTask.repeat)) {
if (!isFinite(DAY_MAPPING_STRING_TO_NUMBER[repeatDay])) continue; // eslint-disable-line no-continue
if (!Number.isFinite(DAY_MAPPING_STRING_TO_NUMBER[repeatDay])) continue; // eslint-disable-line no-continue
if (active) daysOfTheWeek.push(parseInt(DAY_MAPPING_STRING_TO_NUMBER[repeatDay], 10));
}
}
@@ -127,7 +140,7 @@ export function shouldDo (day, dailyTask, options = {}) {
if (options.nextDue) {
const filteredDates = [];
for (let i = 1; filteredDates.length < 6; i++) {
for (let i = 1; filteredDates.length < 6; i += 1) {
const calcDate = moment(startDate).add(dailyTask.everyX * i, 'days');
if (calcDate > startOfDayWithCDSTime) filteredDates.push(calcDate);
}
@@ -145,8 +158,8 @@ export function shouldDo (day, dailyTask, options = {}) {
schedule = schedule.every(daysOfTheWeek).daysOfWeek();
if (options.nextDue) {
const filteredDates = [];
for (let i = 0; filteredDates.length < 6; i++) {
for (let j = 0; j < daysOfTheWeek.length && filteredDates.length < 6; j++) {
for (let i = 0; filteredDates.length < 6; i += 1) {
for (let j = 0; j < daysOfTheWeek.length && filteredDates.length < 6; j += 1) {
const calcDate = moment(startDate).day(daysOfTheWeek[j]).add(dailyTask.everyX * i, 'weeks');
if (calcDate > startOfDayWithCDSTime) filteredDates.push(calcDate);
}
@@ -177,7 +190,7 @@ export function shouldDo (day, dailyTask, options = {}) {
if (options.nextDue) {
const filteredDates = [];
for (let i = 1; filteredDates.length < 6; i++) {
for (let i = 1; filteredDates.length < 6; i += 1) {
const recurDate = moment(startDate).add(dailyTask.everyX * i, 'months');
const calcDate = recurDate.clone();
calcDate.day(daysOfTheWeek[0]);
@@ -193,8 +206,11 @@ export function shouldDo (day, dailyTask, options = {}) {
calcDateWeek = Math.ceil(calcDate.date() / 7);
if (calcDate >= startOfDayWithCDSTime
&& calcDateWeek === startDateWeek && calcDate.month() === recurDate.month()) filteredDates.push(calcDate);
if (
calcDate >= startOfDayWithCDSTime
&& calcDateWeek === startDateWeek
&& calcDate.month() === recurDate.month()
) filteredDates.push(calcDate);
}
return filteredDates;
}
@@ -204,7 +220,7 @@ export function shouldDo (day, dailyTask, options = {}) {
schedule = schedule.every(dailyTask.daysOfMonth).daysOfMonth();
if (options.nextDue) {
const filteredDates = [];
for (let i = 1; filteredDates.length < 6; i++) {
for (let i = 1; filteredDates.length < 6; i += 1) {
const calcDate = moment(startDate).add(dailyTask.everyX * i, 'months');
if (calcDate >= startOfDayWithCDSTime) filteredDates.push(calcDate);
}
@@ -220,7 +236,7 @@ export function shouldDo (day, dailyTask, options = {}) {
if (options.nextDue) {
const filteredDates = [];
for (let i = 1; filteredDates.length < 6; i++) {
for (let i = 1; filteredDates.length < 6; i += 1) {
const calcDate = moment(startDate).add(dailyTask.everyX * i, 'years');
if (calcDate > startOfDayWithCDSTime) filteredDates.push(calcDate);
}

View File

@@ -10,7 +10,8 @@ import splitWhitespace from '../libs/splitWhitespace';
/*
Updates user stats with new stats. Handles death, leveling up, etc
{stats} new stats
{update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately
{update} if aggregated changes, pass in userObj as update.
otherwise commits will be made immediately
*/
function getStatToAllocate (user) {
@@ -53,9 +54,7 @@ function getStatToAllocate (user) {
statsObj[preference[3]] - ideal[3],
];
suggested = findIndex(diff, val => {
if (val === min(diff)) return true;
});
suggested = findIndex(diff, val => val === min(diff));
return suggested !== -1 ? preference[suggested] : 'str';
}
@@ -77,6 +76,6 @@ function getStatToAllocate (user) {
export default function autoAllocate (user) {
const statToIncrease = getStatToAllocate(user);
return user.stats[statToIncrease]++;
user.stats[statToIncrease] += 1;
return user.stats[statToIncrease];
}

View File

@@ -2,17 +2,19 @@ import omit from 'lodash/omit';
import reduce from 'lodash/reduce';
import isNumber from 'lodash/isNumber';
// Because the same op needs to be performed on the client and the server (critical hits, item drops, etc),
// Because the same op needs to be performed on the client and the server
// (critical hits, item drops, etc),
// we need things to be "random", but technically predictable so that they don't go out-of-sync
export default function predictableRandom (user, seed) {
if (!seed || seed === Math.PI) {
let stats = user.stats.toObject ? user.stats.toObject() : user.stats;
// These items are not part of the stat object but exists on the server (see controllers/user#getUser)
// These items are not part of the stat object but exists on the server
// (see controllers/user#getUser)
// we remove them in order to use the same user.stats both on server and on client
stats = omit(stats, ['toNextLevel', 'maxHealth', 'maxMP']);
seed = reduce(stats, (accumulator, val) => {
seed = reduce(stats, (accumulator, val) => { // eslint-disable-line no-param-reassign
if (isNumber(val)) {
return accumulator + val;
}
@@ -20,6 +22,7 @@ export default function predictableRandom (user, seed) {
}, 0);
}
const x = Math.sin(seed++) * 10000;
seed += 1; // eslint-disable-line no-param-reassign
const x = Math.sin(seed) * 10000;
return x - Math.floor(x);
}

View File

@@ -14,10 +14,10 @@ import statsComputed from '../libs/statsComputed';
// TODO This is only used on the server
// move to user model as an instance method?
// Clone a drop object maintaining its functions so that we can change it without affecting the original item
// Clone a drop object maintaining its functions
// so that we can change it without affecting the original item
function cloneDropItem (drop) {
return cloneDeepWith(drop, val => (isFunction(val) ? val : undefined), // undefined will be handled by lodash
);
return cloneDeepWith(drop, val => (isFunction(val) ? val : undefined));
}
function trueRandom () {
@@ -40,14 +40,17 @@ export default function randomDrop (user, options, req = {}, analytics) {
* (1 + (user.contributor.level / 40 || 0)) // Contrib levels: +2.5% per level
* (1 + (user.achievements.rebirths / 20 || 0)) // Rebirths: +5% per achievement
* (1 + (user.achievements.streak / 200 || 0)) // Streak achievements: +0.5% per achievement
* (user._tmp.crit || 1) * (1 + 0.5 * (reduce(task.checklist, (m, i) => // +50% per checklist item complete. TODO: make this into X individual drop chances instead
m + (i.completed ? 1 : 0), // eslint-disable-line indent
0) || 0)); // eslint-disable-line indent
// +50% per checklist item complete. TODO: make this into X individual drop chances instead
* (user._tmp.crit || 1)
* (1 + 0.5 * (reduce(
task.checklist, (m, i) => m + (i.completed ? 1 : 0), // eslint-disable-line indent
0,
) || 0)); // eslint-disable-line indent
chance = diminishingReturns(chance, 0.75);
if (predictableRandom() < chance) {
user.party.quest.progress.collectedItems = user.party.quest.progress.collectedItems || 0;
user.party.quest.progress.collectedItems++;
user.party.quest.progress.collectedItems += 1;
user._tmp.quest = user._tmp.quest || {};
user._tmp.quest.collection = 1;
if (user.markModified) user.markModified('party.quest.progress');
@@ -59,8 +62,10 @@ export default function randomDrop (user, options, req = {}, analytics) {
dropMultiplier = 1;
}
if (daysSince(user.items.lastDrop.date, user.preferences) === 0
&& user.items.lastDrop.count >= dropMultiplier * (5 + Math.floor(statsComputed(user).per / 25) + (user.contributor.level || 0))) {
if (
daysSince(user.items.lastDrop.date, user.preferences) === 0
&& user.items.lastDrop.count >= dropMultiplier * (5 + Math.floor(statsComputed(user).per / 25) + (user.contributor.level || 0))
) {
return;
}
@@ -85,7 +90,7 @@ export default function randomDrop (user, options, req = {}, analytics) {
drop = cloneDropItem(randomVal(content.dropEggs));
user.items.eggs[drop.key] = user.items.eggs[drop.key] || 0;
user.items.eggs[drop.key]++;
user.items.eggs[drop.key] += 1;
if (user.markModified) user.markModified('items.eggs');
drop.type = 'Egg';
@@ -103,10 +108,12 @@ export default function randomDrop (user, options, req = {}, analytics) {
} else { // common, 40% of 30%
acceptableDrops = ['Base', 'White', 'Desert'];
}
drop = cloneDropItem(randomVal(pickBy(content.hatchingPotions, (v, k) => acceptableDrops.indexOf(k) >= 0)));
drop = cloneDropItem(
randomVal(pickBy(content.hatchingPotions, (v, k) => acceptableDrops.indexOf(k) >= 0)),
);
user.items.hatchingPotions[drop.key] = user.items.hatchingPotions[drop.key] || 0;
user.items.hatchingPotions[drop.key]++;
user.items.hatchingPotions[drop.key] += 1;
if (user.markModified) user.markModified('items.hatchingPotions');
drop.type = 'HatchingPotion';
@@ -128,6 +135,6 @@ export default function randomDrop (user, options, req = {}, analytics) {
user._tmp.drop = drop;
user.items.lastDrop.date = Number(new Date());
user.items.lastDrop.count++;
user.items.lastDrop.count += 1;
}
}

View File

@@ -4,7 +4,9 @@ import includes from 'lodash/includes';
import content from '../content/index';
export default function ultimateGear (user) {
const owned = user.items.gear.owned.toObject ? user.items.gear.owned.toObject() : user.items.gear.owned;
const owned = user.items.gear.owned.toObject
? user.items.gear.owned.toObject()
: user.items.gear.owned;
content.classes.forEach(klass => {
if (user.achievements.ultimateGearSets[klass] !== true) {

View File

@@ -24,7 +24,7 @@ export default function updateStats (user, stats, req = {}, analytics) {
while (stats.exp >= experienceToNextLevel) {
stats.exp -= experienceToNextLevel;
user.stats.lvl++;
user.stats.lvl += 1;
experienceToNextLevel = toNextLevel(user.stats.lvl);
user.stats.hp = MAX_HEALTH;
@@ -71,7 +71,7 @@ export default function updateStats (user, stats, req = {}, analytics) {
if (user.addNotification) user.addNotification('DROPS_ENABLED');
if (user.items.eggs.Wolf > 0) {
user.items.eggs.Wolf++;
user.items.eggs.Wolf += 1;
} else {
user.items.eggs.Wolf = 1;
}
@@ -89,7 +89,7 @@ export default function updateStats (user, stats, req = {}, analytics) {
if (user.markModified) user.markModified('flags.levelDrops');
if (!user.items.quests[k]) user.items.quests[k] = 0;
user.items.quests[k]++;
user.items.quests[k] += 1;
if (user.markModified) user.markModified('items.quests');
if (analytics) {

View File

@@ -9,14 +9,15 @@ const i18n = {
};
function t (stringName) {
let vars = arguments[1];
const args = Array.from(arguments); // eslint-disable-line prefer-rest-params
let vars = args[1];
let locale;
if (isString(arguments[1])) {
if (isString(args[1])) {
vars = null;
locale = arguments[1];
} else if (arguments[2]) {
locale = arguments[2];
locale = args[1]; // eslint-disable-line prefer-destructuring
} else if (args[2]) {
locale = args[2]; // eslint-disable-line prefer-destructuring
}
const i18nNotSetup = !i18n.strings && !i18n.translations[locale];

View File

@@ -7,8 +7,9 @@ const achievsContent = content.achievements;
let index = 0;
function contribText (contrib, backer, language) {
if (!contrib && !backer) return;
if (!contrib && !backer) return null;
if (backer && backer.npc) return backer.npc;
const lvl = contrib && contrib.level;
if (lvl && lvl > 0) {
let contribTitle = '';
@@ -29,6 +30,8 @@ function contribText (contrib, backer, language) {
return `${contribTitle} ${contrib.text}`;
}
return null;
}
function _add (result, data) {
@@ -38,7 +41,7 @@ function _add (result, data) {
icon: data.icon,
earned: data.earned,
value: data.value,
index: index++,
index: index += 1,
optionalCount: data.optionalCount,
};
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-classes-per-file */
import extendableBuiltin from './extendableBuiltin';
// Base class for custom application errors

View File

@@ -1,8 +1,9 @@
// TODO use https://babeljs.io/docs/en/babel-plugin-transform-classes
// Babel 6 doesn't support extending native class (Error, Array, ...)
// This function makes it possible to extend native classes with the same results as Babel 5
export default function extendableBuiltin (klass) {
function ExtendableBuiltin () {
klass.apply(this, arguments);
klass.apply(this, arguments); // eslint-disable-line prefer-rest-params
}
ExtendableBuiltin.prototype = Object.create(klass.prototype);
Object.setPrototypeOf(ExtendableBuiltin, klass);

View File

@@ -20,7 +20,9 @@ function lockQuest (quest, user) {
}
function isItemSuggested (officialPinnedItems, itemInfo) {
return officialPinnedItems.findIndex(officialItem => officialItem.type === itemInfo.pinType && officialItem.path === itemInfo.path) > -1;
return officialPinnedItems.findIndex(officialItem => { // eslint-disable-line arrow-body-style
return officialItem.type === itemInfo.pinType && officialItem.path === itemInfo.path;
}) > -1;
}
function getDefaultGearProps (item, language) {
@@ -46,12 +48,12 @@ function getDefaultGearProps (item, language) {
export default function getItemInfo (user, type, item, officialPinnedItems, language = 'en') {
if (officialPinnedItems === undefined) {
officialPinnedItems = getOfficialPinnedItems(user);
officialPinnedItems = getOfficialPinnedItems(user); // eslint-disable-line no-param-reassign
}
let itemInfo;
switch (type) {
switch (type) { // eslint-disable-line default-case
case 'eggs':
itemInfo = {
key: item.key,
@@ -131,7 +133,9 @@ export default function getItemInfo (user, type, item, officialPinnedItems, lang
group: item.group,
value: item.goldValue ? item.goldValue : item.value,
locked,
previous: content.quests[item.previous] ? content.quests[item.previous].text(language) : null,
previous: content.quests[item.previous]
? content.quests[item.previous].text(language)
: null,
unlockCondition: item.unlockCondition,
drop: item.drop,
boss: item.boss,

View File

@@ -14,7 +14,9 @@ export default function getOfficialPinnedItems (user) {
// pinnedSets == current seasonal class set are always gold purchaseable
flatGearArray.filter(gear => user.items.gear.owned[gear.key] === undefined && gear.set === setToAdd).map(gear => {
flatGearArray
.filter(gear => user.items.gear.owned[gear.key] === undefined && gear.set === setToAdd)
.forEach(gear => {
officialItemsArray.push({
type: 'marketGear',
path: `gear.flat.${gear.key}`,

View File

@@ -1,9 +1,12 @@
export default function isPinned (user, item, checkOfficialPinnedItems /* getOfficialPinnedItems */) {
export default function isPinned (user, item, checkOfficialPinnedItems) {
if (user === null) return false;
const isPinnedOfficial = checkOfficialPinnedItems !== undefined && checkOfficialPinnedItems.findIndex(pinned => pinned.path === item.path) > -1;
const isItemUnpinned = user.unpinnedItems !== undefined && user.unpinnedItems.findIndex(unpinned => unpinned.path === item.path) > -1;
const isItemPinned = user.pinnedItems !== undefined && user.pinnedItems.findIndex(pinned => pinned.path === item.path) > -1;
const isPinnedOfficial = checkOfficialPinnedItems !== undefined
&& checkOfficialPinnedItems.findIndex(pinned => pinned.path === item.path) > -1;
const isItemUnpinned = user.unpinnedItems !== undefined
&& user.unpinnedItems.findIndex(unpinned => unpinned.path === item.path) > -1;
const isItemPinned = user.pinnedItems !== undefined
&& user.pinnedItems.findIndex(pinned => pinned.path === item.path) > -1;
if (isPinnedOfficial && !isItemUnpinned) return true;

View File

@@ -13,7 +13,7 @@ export default function percent (x, y, dir) {
roundFn = Math.round;
}
if (x === 0) {
x = 1;
x = 1; // eslint-disable-line no-param-reassign
}
return Math.max(0, roundFn(x / y * 100));
return Math.max(0, roundFn((x / y) * 100));
}

View File

@@ -4,8 +4,10 @@ import values from 'lodash/values';
import uuid from './uuid';
/*
Reflists are arrays, but stored as objects. Mongoose has a helluvatime working with arrays (the main problem for our
syncing issues) - so the goal is to move away from arrays to objects, since mongoose can reference elements by ID
Reflists are arrays, but stored as objects.
Mongoose has a helluvatime working with arrays (the main problem for our
syncing issues) - so the goal is to move away from arrays to objects,
since mongoose can reference elements by ID
no problem. To maintain sorting, we use these helper functions:
*/

View File

@@ -1,4 +1,3 @@
export default function splitWhitespace (s) {
return s.split(' ');
}

View File

@@ -2,7 +2,8 @@ import { v4 as uuid } from 'uuid';
import defaults from 'lodash/defaults';
import moment from 'moment';
// Even though Mongoose handles task defaults, we want to make sure defaults are set on the client-side before
// Even though Mongoose handles task defaults,
// we want to make sure defaults are set on the client-side before
// sending up to the server for performance
// TODO move to client code?

View File

@@ -17,7 +17,10 @@ export default function updateStore (user) {
let changes = [];
each(content.gearTypes, type => {
const found = lodashFind(content.gear.tree[type][user.stats.class], item => !user.items.gear.owned[item.key]);
const found = lodashFind(
content.gear.tree[type][user.stats.class],
item => !user.items.gear.owned[item.key],
);
if (found) changes.push(found);
});

View File

@@ -1,3 +1,4 @@
/* eslint-disable max-classes-per-file */
import _merge from 'lodash/merge';
import _get from 'lodash/get';
import i18n from '../../i18n';
@@ -61,7 +62,7 @@ export class AbstractBuyOperation {
*/
// eslint-disable-next-line no-unused-vars
i18n (key, params = {}) {
return i18n.t.apply(null, [...arguments, this.req.language]);
return i18n.t.apply(null, [...arguments, this.req.language]); // eslint-disable-line prefer-rest-params, max-len
}
/**

View File

@@ -20,7 +20,9 @@ import { BuyHourglassMountOperation } from './buyMount';
// @TODO: when we are sure buy is the only function used, let's move the buy files to a folder
export default function buy (user, req = {}, analytics, options = { quantity: 1, hourglass: false }) {
export default function buy (
user, req = {}, analytics, options = { quantity: 1, hourglass: false },
) {
const key = get(req, 'params.key');
const { hourglass } = options;
const { quantity } = options;

View File

@@ -17,7 +17,7 @@ import { AbstractGoldItemOperation } from './abstractBuyOperation';
const YIELD_EQUIPMENT_THRESHOLD = 0.6;
const YIELD_FOOD_THRESHOLD = 0.8;
export class BuyArmoireOperation extends AbstractGoldItemOperation {
export class BuyArmoireOperation extends AbstractGoldItemOperation { // eslint-disable-line import/prefer-default-export, max-len
constructor (user, req, analytics) {
super(user, req, analytics);
}
@@ -39,9 +39,15 @@ export class BuyArmoireOperation extends AbstractGoldItemOperation {
const eligibleEquipment = filter(content.gear.flat, eligible => eligible.klass === 'armoire' && !user.items.gear.owned[eligible.key]);
const armoireHasEquipment = !isEmpty(eligibleEquipment);
if (armoireHasEquipment && (armoireResult < YIELD_EQUIPMENT_THRESHOLD || !user.flags.armoireOpened)) {
if (
armoireHasEquipment
&& (armoireResult < YIELD_EQUIPMENT_THRESHOLD || !user.flags.armoireOpened)
) {
result = this._gearResult(user, eligibleEquipment);
} else if ((armoireHasEquipment && armoireResult < YIELD_FOOD_THRESHOLD) || armoireResult < 0.5) { // eslint-disable-line no-extra-parens
} else if (
(armoireHasEquipment && armoireResult < YIELD_FOOD_THRESHOLD)
|| armoireResult < 0.5
) {
result = this._foodResult(user);
} else {
result = this._experienceResult(user);
@@ -49,7 +55,8 @@ export class BuyArmoireOperation extends AbstractGoldItemOperation {
this.subtractCurrency(user, item);
let { message, armoireResp } = result;
let { message } = result;
const { armoireResp } = result;
if (!message) {
message = this.i18n('messageBought', {

View File

@@ -8,7 +8,7 @@ import {
import { AbstractGoldItemOperation } from './abstractBuyOperation';
import planGemLimits from '../../libs/planGemLimits';
export class BuyGemOperation extends AbstractGoldItemOperation {
export class BuyGemOperation extends AbstractGoldItemOperation { // eslint-disable-line import/prefer-default-export, max-len
constructor (user, req, analytics) {
super(user, req, analytics);
}
@@ -30,7 +30,8 @@ export class BuyGemOperation extends AbstractGoldItemOperation {
}
extractAndValidateParams (user, req) {
const key = this.key = get(req, 'params.key');
this.key = get(req, 'params.key');
const { key } = this.key;
if (!key) throw new BadRequest(this.i18n('missingKeyParam'));
let { convCap } = planGemLimits;

View File

@@ -5,7 +5,7 @@ import {
import { AbstractGoldItemOperation } from './abstractBuyOperation';
export class BuyHealthPotionOperation extends AbstractGoldItemOperation {
export class BuyHealthPotionOperation extends AbstractGoldItemOperation { // eslint-disable-line import/prefer-default-export, max-len
constructor (user, req, analytics) {
super(user, req, analytics);
}

View File

@@ -15,7 +15,7 @@ import { removePinnedGearAddPossibleNewOnes } from '../pinnedGearUtils';
import { AbstractGoldItemOperation } from './abstractBuyOperation';
import errorMessage from '../../libs/errorMessage';
export class BuyMarketGearOperation extends AbstractGoldItemOperation {
export class BuyMarketGearOperation extends AbstractGoldItemOperation { // eslint-disable-line import/prefer-default-export, max-len
constructor (user, req, analytics) {
super(user, req, analytics);
}
@@ -37,7 +37,8 @@ export class BuyMarketGearOperation extends AbstractGoldItemOperation {
}
extractAndValidateParams (user, req) {
const key = this.key = get(req, 'params.key');
this.key = get(req, 'params.key');
const { key } = this.key;
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
const item = content.gear.flat[key];

View File

@@ -9,7 +9,7 @@ import {
import { AbstractHourglassItemOperation } from './abstractBuyOperation';
export class BuyHourglassMountOperation extends AbstractHourglassItemOperation {
export class BuyHourglassMountOperation extends AbstractHourglassItemOperation { // eslint-disable-line import/prefer-default-export, max-len
constructor (user, req, analytics) {
super(user, req, analytics);
}
@@ -19,7 +19,8 @@ export class BuyHourglassMountOperation extends AbstractHourglassItemOperation {
}
extractAndValidateParams (user, req) {
const key = this.key = get(req, 'params.key');
this.key = get(req, 'params.key');
const { key } = this;
if (!key) throw new BadRequest(this.i18n('missingKeyParam'));

View File

@@ -40,7 +40,7 @@ export default function buyMysterySet (user, req = {}, analytics) {
if (user.markModified) user.markModified('items.gear.owned');
user.purchased.plan.consecutive.trinkets--;
user.purchased.plan.consecutive.trinkets -= 1;
return [
{ items: user.items, purchasedPlanConsecutive: user.purchased.plan.consecutive },

View File

@@ -9,7 +9,7 @@ import content from '../../content/index';
import errorMessage from '../../libs/errorMessage';
import { AbstractGemItemOperation } from './abstractBuyOperation';
export class BuyQuestWithGemOperation extends AbstractGemItemOperation {
export class BuyQuestWithGemOperation extends AbstractGemItemOperation { // eslint-disable-line import/prefer-default-export, max-len
constructor (user, req, analytics) {
super(user, req, analytics);
}
@@ -31,7 +31,8 @@ export class BuyQuestWithGemOperation extends AbstractGemItemOperation {
}
extractAndValidateParams (user, req) {
const key = this.key = get(req, 'params.key');
this.key = get(req, 'params.key');
const { key } = this.key;
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
const item = content.quests[key];
@@ -46,7 +47,10 @@ export class BuyQuestWithGemOperation extends AbstractGemItemOperation {
}
executeChanges (user, item, req) {
if (!user.items.quests[item.key] || user.items.quests[item.key] < 0) user.items.quests[item.key] = 0;
if (
!user.items.quests[item.key]
|| user.items.quests[item.key] < 0
) user.items.quests[item.key] = 0;
user.items.quests[item.key] += this.quantity;
if (user.markModified) user.markModified('items.quests');

View File

@@ -9,7 +9,7 @@ import content from '../../content/index';
import { AbstractGoldItemOperation } from './abstractBuyOperation';
import errorMessage from '../../libs/errorMessage';
export class BuyQuestWithGoldOperation extends AbstractGoldItemOperation {
export class BuyQuestWithGoldOperation extends AbstractGoldItemOperation { // eslint-disable-line import/prefer-default-export, max-len
constructor (user, req, analytics) {
super(user, req, analytics);
}
@@ -66,7 +66,10 @@ export class BuyQuestWithGoldOperation extends AbstractGoldItemOperation {
}
executeChanges (user, item, req) {
if (!user.items.quests[item.key] || user.items.quests[item.key] < 0) user.items.quests[item.key] = 0;
if (
!user.items.quests[item.key]
|| user.items.quests[item.key] < 0
) user.items.quests[item.key] = 0;
user.items.quests[item.key] += this.quantity;
if (user.markModified) user.markModified('items.quests');

View File

@@ -9,7 +9,7 @@ import {
import { AbstractGoldItemOperation } from './abstractBuyOperation';
import errorMessage from '../../libs/errorMessage';
export class BuySpellOperation extends AbstractGoldItemOperation {
export class BuySpellOperation extends AbstractGoldItemOperation { // eslint-disable-line import/prefer-default-export, max-len
constructor (user, req, analytics) {
super(user, req, analytics);
}
@@ -27,7 +27,8 @@ export class BuySpellOperation extends AbstractGoldItemOperation {
}
extractAndValidateParams (user, req) {
const key = this.key = get(req, 'params.key');
this.key = get(req, 'params.key');
const { key } = this;
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
const item = content.special[key];

View File

@@ -44,7 +44,7 @@ export default function purchaseHourglass (user, req = {}, analytics, quantity =
throw new NotAuthorized(i18n.t('notEnoughHourglasses', req.language));
}
user.purchased.plan.consecutive.trinkets--;
user.purchased.plan.consecutive.trinkets -= 1;
if (type === 'pets') {
user.items.pets[key] = 5;

View File

@@ -54,14 +54,14 @@ function purchaseItem (user, item, price, type, key) {
if (!user.items[subType][bundledKey] || user.items[subType][key] < 0) {
user.items[subType][bundledKey] = 0;
}
user.items[subType][bundledKey]++;
user.items[subType][bundledKey] += 1;
});
if (user.markModified) user.markModified(`items.${subType}`);
} else {
if (!user.items[type][key] || user.items[type][key] < 0) {
user.items[type][key] = 0;
}
user.items[type][key]++;
user.items[type][key] += 1;
if (user.markModified) user.markModified(`items.${type}`);
}
}

View File

@@ -40,7 +40,8 @@ export default function changeClass (user, req = {}, analytics) {
if (user.stats.lvl < 10) {
throw new NotAuthorized(i18n.t('lvl10ChangeClass', req.language));
} else if (!klass) {
// if no class is specified, reset points and set user.flags.classSelected to false. User will have paid 3 gems and will be prompted to select class.
// if no class is specified, reset points and set user.flags.classSelected to false.
// User will have paid 3 gems and will be prompted to select class.
balanceRemoved = resetClass(user, req);
} else if (klass === 'warrior' || klass === 'rogue' || klass === 'wizard' || klass === 'healer') {
if (user.flags.classSelected) {

View File

@@ -21,7 +21,7 @@ export default function equip (user, req = {}) {
let message;
switch (type) {
switch (type) { // eslint-disable-line default-case
case 'mount': {
if (!user.items.mounts[key]) {
throw new NotFound(i18n.t('mountNotOwned', req.language));
@@ -49,7 +49,11 @@ export default function equip (user, req = {}) {
if (user.items.gear[type][item.type] === key) {
user.items.gear[type] = {
...(user.items.gear[type].toObject ? user.items.gear[type].toObject() : user.items.gear[type]),
...(
user.items.gear[type].toObject
? user.items.gear[type].toObject()
: user.items.gear[type]
),
[item.type]: `${item.type}_base_0`,
};
if (user.markModified && type === 'owned') user.markModified('items.gear.owned');
@@ -60,7 +64,11 @@ export default function equip (user, req = {}) {
} else {
user.items.gear[type] = {
...(user.items.gear[type].toObject ? user.items.gear[type].toObject() : user.items.gear[type]),
...(
user.items.gear[type].toObject
? user.items.gear[type].toObject()
: user.items.gear[type]
),
[item.type]: item.key,
};
if (user.markModified && type === 'owned') user.markModified('items.gear.owned');

View File

@@ -90,7 +90,7 @@ export default function feed (user, req = {}) {
}
}
user.items.food[food.key]--;
user.items.food[food.key] -= 1;
if (user.markModified) user.markModified('items.food');
forEach(content.animalColorAchievements, achievement => {

View File

@@ -24,7 +24,13 @@ export default function hatch (user, req = {}) {
throw new NotFound(i18n.t('messageMissingEggPotion', req.language));
}
if ((content.hatchingPotions[hatchingPotion].premium || content.hatchingPotions[hatchingPotion].wacky) && !content.dropEggs[egg]) {
if (
(
content.hatchingPotions[hatchingPotion].premium
|| content.hatchingPotions[hatchingPotion].wacky
)
&& !content.dropEggs[egg]
) {
throw new BadRequest(i18n.t('messageInvalidEggPotionCombo', req.language));
}
@@ -35,8 +41,8 @@ export default function hatch (user, req = {}) {
}
user.items.pets[pet] = 5;
user.items.eggs[egg]--;
user.items.hatchingPotions[hatchingPotion]--;
user.items.eggs[egg] -= 1;
user.items.hatchingPotions[hatchingPotion] -= 1;
if (user.markModified) {
user.markModified('items.pets');
user.markModified('items.eggs');
@@ -45,7 +51,10 @@ export default function hatch (user, req = {}) {
forEach(content.animalColorAchievements, achievement => {
if (!user.achievements[achievement.petAchievement]) {
const petIndex = findIndex(keys(content.dropEggs), animal => isNaN(user.items.pets[`${animal}-${achievement.color}`]) || user.items.pets[`${animal}-${achievement.color}`] <= 0);
const petIndex = findIndex(
keys(content.dropEggs),
animal => Number.isNaN(user.items.pets[`${animal}-${achievement.color}`]) || user.items.pets[`${animal}-${achievement.color}`] <= 0,
);
if (petIndex === -1) {
user.achievements[achievement.petAchievement] = true;
if (user.addNotification) {

View File

@@ -37,7 +37,10 @@ export function selectGearToPin (user) {
const changes = [];
each(content.gearTypes, type => {
const found = lodashFind(content.gear.tree[type][user.stats.class], item => !user.items.gear.owned[item.key]);
const found = lodashFind(
content.gear.tree[type][user.stats.class],
item => !user.items.gear.owned[item.key],
);
if (found) changes.push(found);
});
@@ -59,11 +62,10 @@ export function addPinnedGear (user, type, path) {
export function addPinnedGearByClass (user) {
const newPinnedItems = selectGearToPin(user);
for (const item of newPinnedItems) {
newPinnedItems.forEach(item => {
const itemInfo = getItemInfo(user, 'marketGear', item);
addPinnedGear(user, itemInfo.pinType, itemInfo.path);
}
});
}
export function removeItemByPath (user, path) {
@@ -80,11 +82,10 @@ export function removeItemByPath (user, path) {
export function removePinnedGearByClass (user) {
const currentPinnedItems = selectGearToPin(user);
for (const item of currentPinnedItems) {
currentPinnedItems.forEach(item => {
const itemInfo = getItemInfo(user, 'marketGear', item);
removeItemByPath(user, itemInfo.path);
}
});
}
export function removePinnedGearAddPossibleNewOnes (user, itemPath, newItemKey) {
@@ -100,11 +101,12 @@ export function removePinnedGearAddPossibleNewOnes (user, itemPath, newItemKey)
addPinnedGearByClass(user);
// update the version, so that vue can refresh the seasonal shop
user._v++;
user._v += 1;
}
/**
* removes all pinned gear that the user already owns (like class starter gear which has been pinned before)
* removes all pinned gear that the user already owns
*(like class starter gear which has been pinned before)
* @param user
*/
export function removePinnedItemsByOwnedGear (user) {
@@ -126,9 +128,9 @@ export function togglePinnedItem (user, { item, type, path }, req = {}) {
if (!path) {
// If path isn't passed it means an item was passed
path = getItemInfo(user, type, item, officialPinnedItems, req.language).path;
path = getItemInfo(user, type, item, officialPinnedItems, req.language).path; // eslint-disable-line no-param-reassign, max-len
} else {
item = getItemByPathAndType(type, path);
item = getItemByPathAndType(type, path); // eslint-disable-line no-param-reassign
if (!item && PATHS_WITHOUT_ITEM.indexOf(path) === -1) {
// path not exists in our content structure

View File

@@ -16,6 +16,8 @@ function markNotificationAsRead (user, cardType) {
&& notification.data
&& notification.data.card === cardType
) return true;
return false;
});
if (indexToRemove !== -1) user.notifications.splice(indexToRemove, 1);

View File

@@ -97,7 +97,7 @@ export default function rebirth (user, tasks = [], req = {}, analytics) {
user.achievements.rebirths = 1;
user.achievements.rebirthLevel = lvl;
} else if (lvl > user.achievements.rebirthLevel || lvl === MAX_LEVEL) {
user.achievements.rebirths++;
user.achievements.rebirths += 1;
user.achievements.rebirthLevel = lvl;
}

View File

@@ -8,13 +8,14 @@ import {
import splitWhitespace from '../libs/splitWhitespace';
export default function releaseBoth (user, req = {}) {
let animal;
if (!user.achievements.triadBingo) {
throw new NotAuthorized(i18n.t('notEnoughPetsMounts', req.language));
}
if (beastMasterProgress(user.items.pets) !== 90 || mountMasterProgress(user.items.mounts) !== 90) {
if (
beastMasterProgress(user.items.pets) !== 90
|| mountMasterProgress(user.items.mounts) !== 90
) {
throw new NotAuthorized(i18n.t('notEnoughPetsMounts', req.language));
}
@@ -49,7 +50,7 @@ export default function releaseBoth (user, req = {}) {
user.items.currentPet = '';
}
for (animal in content.pets) {
Object.keys(content.pets).forEach(animal => {
if (user.items.pets[animal] === -1) {
giveTriadBingo = false;
} else if (!user.items.pets[animal]) {
@@ -61,7 +62,8 @@ export default function releaseBoth (user, req = {}) {
user.items.pets[animal] = 0;
user.items.mounts[animal] = null;
}
});
if (user.markModified) {
user.markModified('items.pets');
user.markModified('items.mounts');
@@ -71,21 +73,21 @@ export default function releaseBoth (user, req = {}) {
if (!user.achievements.beastMasterCount) {
user.achievements.beastMasterCount = 0;
}
user.achievements.beastMasterCount++;
user.achievements.beastMasterCount += 1;
}
if (giveMountMasterAchievement) {
if (!user.achievements.mountMasterCount) {
user.achievements.mountMasterCount = 0;
}
user.achievements.mountMasterCount++;
user.achievements.mountMasterCount += 1;
}
if (giveTriadBingo) {
if (!user.achievements.triadBingoCount) {
user.achievements.triadBingoCount = 0;
}
user.achievements.triadBingoCount++;
user.achievements.triadBingoCount += 1;
}
return [

View File

@@ -24,19 +24,20 @@ export default function releaseMounts (user, req = {}, analytics) {
user.items.currentMount = '';
}
for (const mount in content.pets) {
Object.keys(content.pets).forEach(mount => {
if (user.items.mounts[mount] === null || user.items.mounts[mount] === undefined) {
giveMountMasterAchievement = false;
}
user.items.mounts[mount] = null;
}
});
if (user.markModified) user.markModified('items.mounts');
if (giveMountMasterAchievement) {
if (!user.achievements.mountMasterCount) {
user.achievements.mountMasterCount = 0;
}
user.achievements.mountMasterCount++;
user.achievements.mountMasterCount += 1;
}
if (analytics) {

View File

@@ -24,19 +24,20 @@ export default function releasePets (user, req = {}, analytics) {
user.items.currentPet = '';
}
for (const pet in content.pets) {
Object.keys(content.pets).forEach(pet => {
if (!user.items.pets[pet]) {
giveBeastMasterAchievement = false;
}
user.items.pets[pet] = 0;
}
});
if (user.markModified) user.markModified('items.pets');
if (giveBeastMasterAchievement) {
if (!user.achievements.beastMasterCount) {
user.achievements.beastMasterCount = 0;
}
user.achievements.beastMasterCount++;
user.achievements.beastMasterCount += 1;
}
if (analytics) {

View File

@@ -9,7 +9,7 @@ export default function reroll (user, tasks = [], req = {}, analytics) {
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
user.balance--;
user.balance -= 1;
user.stats.hp = 50;
each(tasks, task => {

View File

@@ -24,7 +24,7 @@ export default function revive (user, req = {}, analytics) {
});
if (user.stats.lvl > 1) {
user.stats.lvl--;
user.stats.lvl -= 1;
}
const lostStat = randomVal(reduce(['str', 'con', 'per', 'int'], (m, k) => {
@@ -37,7 +37,7 @@ export default function revive (user, req = {}, analytics) {
});
if (lostStat) {
user.stats[lostStat]--;
user.stats[lostStat] -= 1;
}
const base = user.items.gear.owned;
@@ -68,9 +68,11 @@ export default function revive (user, req = {}, analytics) {
const itemIsArmoire = itm.klass === 'armoire';
if (itemHasValueOrWarrior0 && (itemClassEqualsUserClass || itemIsSpecial || itemIsArmoire)) {
if (
itemHasValueOrWarrior0
&& (itemClassEqualsUserClass || itemIsSpecial || itemIsArmoire)
) {
losableItems[key] = key;
return losableItems[key];
}
}
}

View File

@@ -28,7 +28,7 @@ function _getTaskValue (taskValue) {
function _calculateDelta (task, direction, cron) {
// Min/max on task redness
const currVal = _getTaskValue(task.value);
let nextDelta = Math.pow(0.9747, currVal) * (direction === 'down' ? -1 : 1);
let nextDelta = (0.9747 ** currVal) * (direction === 'down' ? -1 : 1);
// Checklists
if (task.checklist && task.checklist.length > 0) {
@@ -52,13 +52,13 @@ function _calculateDelta (task, direction, cron) {
// it will be a bit off
function _calculateReverseDelta (task, direction) {
const currVal = _getTaskValue(task.value);
let testVal = currVal + Math.pow(0.9747, currVal) * (direction === 'down' ? -1 : 1);
let testVal = currVal + (0.9747 ** currVal) * (direction === 'down' ? -1 : 1);
// Now keep moving closer to the original value until we get "close enough"
// Check how close we are to the original value by computing the delta off our guess
// and looking at the difference between that and our current value.
while (true) { // eslint-disable-line no-constant-condition
const calc = testVal + Math.pow(0.9747, testVal);
const calc = testVal + (0.9747 ** testVal);
const diff = currVal - calc;
if (Math.abs(diff) < CLOSE_ENOUGH) break;
@@ -84,7 +84,7 @@ function _calculateReverseDelta (task, direction) {
}
function _gainMP (user, val) {
val *= user._tmp.crit || 1;
val *= user._tmp.crit || 1; // eslint-disable-line no-param-reassign
user.stats.mp += val;
if (user.stats.mp >= statsComputed(user).maxMP) user.stats.mp = statsComputed(user).maxMP;
@@ -125,7 +125,8 @@ function _addPoints (user, task, stats, direction, delta) {
const streakBonus = currStreak / 100 + 1; // eg, 1-day streak is 1.01, 2-day is 1.02, etc
const afterStreak = gpMod * streakBonus;
if (currStreak > 0 && gpMod > 0) {
user._tmp.streakBonus = afterStreak - gpMod; // keep this on-hand for later, so we can notify streak-bonus
// keep this on-hand for later, so we can notify streak-bonus
user._tmp.streakBonus = afterStreak - gpMod;
}
stats.gp += afterStreak;
@@ -194,9 +195,13 @@ export default function scoreTask (options = {}, req = {}) {
exp: user.stats.exp,
};
if (task.group && task.group.approval && task.group.approval.required && !task.group.approval.approved) return 0;
if (
task.group && task.group.approval && task.group.approval.required
&& !task.group.approval.approved
) return 0;
// This is for setting one-time temporary flags, such as streakBonus or itemDropped. Useful for notifying
// This is for setting one-time temporary flags,
// such as streakBonus or itemDropped. Useful for notifying
// the API consumer, then cleared afterwards
user._tmp = {};
@@ -236,7 +241,8 @@ export default function scoreTask (options = {}, req = {}) {
lastHistoryEntry.value = task.value;
lastHistoryEntry.date = Number(new Date());
// @TODO remove this extra check after migration has run to set scoredUp and scoredDown in every task
// @TODO remove this extra check after migration
// has run to set scoredUp and scoredDown in every task
lastHistoryEntry.scoredUp = lastHistoryEntry.scoredUp || 0;
lastHistoryEntry.scoredDown = lastHistoryEntry.scoredDown || 0;
@@ -265,7 +271,8 @@ export default function scoreTask (options = {}, req = {}) {
} else {
delta += _changeTaskValue(user, task, direction, times, cron);
if (direction === 'down') delta = _calculateDelta(task, direction, cron); // recalculate delta for unchecking so the gp and exp come out correctly
_addPoints(user, task, stats, direction, delta); // obviously for delta>0, but also a trick to undo accidental checkboxes
// obviously for delta>0, but also a trick to undo accidental checkboxes
_addPoints(user, task, stats, direction, delta);
_gainMP(user, max([1, 0.01 * statsComputed(user).maxMP]) * (direction === 'down' ? -1 : 1));
if (direction === 'up') {
@@ -286,7 +293,9 @@ export default function scoreTask (options = {}, req = {}) {
task.history.push(historyEntry);
} else if (direction === 'down') {
// Remove a streak achievement if streak was a multiple of 21 and the daily was undone
if (task.streak !== 0 && task.streak % 21 === 0) user.achievements.streak = user.achievements.streak ? user.achievements.streak - 1 : 0;
if (task.streak !== 0 && task.streak % 21 === 0) {
user.achievements.streak = user.achievements.streak ? user.achievements.streak - 1 : 0;
}
task.streak -= 1;
task.completed = false;

View File

@@ -22,10 +22,10 @@ export default function allocate (user, req = {}) {
}
if (user.stats.points > 0) {
user.stats[stat]++;
user.stats.points--;
user.stats[stat] += 1;
user.stats.points -= 1;
if (stat === 'int') {
user.stats.mp++;
user.stats.mp += 1;
}
} else {
throw new NotAuthorized(i18n.t('notEnoughAttrPoints', req.language));

View File

@@ -34,11 +34,11 @@ export default function allocateBulk (user, req = {}) {
throw new NotAuthorized(i18n.t('notEnoughAttrPoints', req.language));
}
for (const [stat, value] of Object.entries(stats)) {
Object.entries(stats).forEach(([stat, value]) => {
user.stats[stat] += value;
user.stats.points -= value;
if (stat === 'int') user.stats.mp += value;
}
});
return [
user.stats,

View File

@@ -45,13 +45,14 @@ export default function unlock (user, req = {}, analytics) {
each(setPaths, singlePath => {
if (get(user, `purchased.${singlePath}`) === true) {
alreadyOwnedItems++;
alreadyOwnedItems += 1;
}
});
if (alreadyOwnedItems === setPaths.length) {
throw new NotAuthorized(i18n.t('alreadyUnlocked', req.language));
// TODO write math formula to check if buying the full set is cheaper than the items individually
// TODO write math formula to check if buying
// the full set is cheaper than the items individually
// (item cost * number of remaining items) < setCost`
} /* else if (alreadyOwnedItems > 0) {
throw new NotAuthorized(i18n.t('alreadyUnlockedPart', req.language));

View File

@@ -27,13 +27,15 @@ export function toNextLevel (lvl) {
} if (lvl === 5) {
return 150;
}
return Math.round((Math.pow(lvl, 2) * 0.25 + 10 * lvl + 139.75) / 10) * 10;
return Math.round(((lvl ** 2) * 0.25 + 10 * lvl + 139.75) / 10) * 10;
}
/*
A hyperbola function that creates diminishing returns, so you can't go to infinite (eg, with Exp gain).
A hyperbola function that creates diminishing returns,
so you can't go to infinite (eg, with Exp gain).
{max} The asymptote
{bonus} All the numbers combined for your point bonus (eg, task.value * user.stats.int * critChance, etc)
{bonus} All the numbers combined for your point bonus
(eg, task.value * user.stats.int * critChance, etc)
{halfway} (optional) the point at which the graph starts bending
*/