diff --git a/.eslintignore b/.eslintignore index 17ddc90a01..2528e22535 100644 --- a/.eslintignore +++ b/.eslintignore @@ -12,7 +12,6 @@ website/public/ # Temporarilly disabled. These should be removed when the linting errors are fixed common/script/content/index.js -common/script/fns/randomDrop.js common/script/public/**/*.js website/src/**/api-v2/**/*.js diff --git a/common/script/fns/crit.js b/common/script/fns/crit.js index 07b144a805..65a0cbb062 100644 --- a/common/script/fns/crit.js +++ b/common/script/fns/crit.js @@ -1,7 +1,8 @@ +import predictableRandom from './predictableRandom'; module.exports = function crit (user, stat = 'str', chance = 0.03) { let s = user._statsComputed[stat]; - if (user.fns.predictableRandom() <= chance * (1 + s / 100)) { + if (predictableRandom(user) <= chance * (1 + s / 100)) { return 1.5 + 4 * s / (s + 200); } else { return 1; diff --git a/common/script/fns/randomDrop.js b/common/script/fns/randomDrop.js index 0ab6a5e69a..3d1114676a 100644 --- a/common/script/fns/randomDrop.js +++ b/common/script/fns/randomDrop.js @@ -3,80 +3,118 @@ import content from '../content/index'; import i18n from '../i18n'; import { daysSince } from '../cron'; import { diminishingReturns } from '../statHelpers'; +import { predictableRandom } from './predictableRandom'; +import randomVal from './randomVal'; // Clone a drop object maintaining its functions so that we can change it without affecting the original item function cloneDropItem (drop) { - return _.cloneDeep(drop, function (val) { + return _.cloneDeep(drop, (val) => { return _.isFunction(val) ? val : undefined; // undefined will be handled by lodash }); } -module.exports = function randomDrop (user, modifiers, req) { - var acceptableDrops, base, base1, base2, chance, drop, dropK, dropMultiplier, name, name1, name2, quest, rarity, ref, ref1, ref2, ref3, task; +module.exports = function randomDrop (user, modifiers, req = {}) { + let acceptableDrops; + let chance; + let drop; + let dropK; + let dropMultiplier; + let quest; + let rarity; + let task; + task = modifiers.task; - chance = _.min([Math.abs(task.value - 21.27), 37.5]) / 150 + .02; - chance *= task.priority * (1 + (task.streak / 100 || 0)) * (1 + (user._statsComputed.per / 100)) * (1 + (user.contributor.level / 40 || 0)) * (1 + (user.achievements.rebirths / 20 || 0)) * (1 + (user.achievements.streak / 200 || 0)) * (user._tmp.crit || 1) * (1 + .5 * (_.reduce(task.checklist, (function(m, i) { - return m + (i.completed ? 1 : 0); - }), 0) || 0)); + + chance = _.min([Math.abs(task.value - 21.27), 37.5]) / 150 + 0.02; + chance *= task.priority * // Task priority: +50% for Medium, +100% for Hard + (1 + (task.streak / 100 || 0)) * // Streak bonus: +1% per streak + (1 + user._statsComputed.per / 100) * // PERception: +1% per point + (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) => { + return m + (i.completed ? 1 : 0); // +50% per checklist item complete. TODO: make this into X individual drop chances instead + }, 0) || 0)); chance = diminishingReturns(chance, 0.75); - quest = content.quests[(ref = user.party.quest) != null ? ref.key : void 0]; - if ((quest != null ? quest.collect : void 0) && user.fns.predictableRandom(user.stats.gp) < chance) { - dropK = user.fns.randomVal(quest.collect, { - key: true + + if (user.party.quest.key) + quest = content.quests[user.party.quest.key]; + if (quest && quest.collect && predictableRandom(user, user.stats.gp) < chance) { + dropK = randomVal(user, quest.collect, { + key: true, }); + if (!user.party.quest.progress.collect[dropK]) + user.party.quest.progress.collect[dropK] = 0; user.party.quest.progress.collect[dropK]++; - if (typeof user.markModified === "function") { - user.markModified('party.quest.progress'); - } + user.markModified('party.quest.progress'); } - dropMultiplier = ((ref1 = user.purchased) != null ? (ref2 = ref1.plan) != null ? ref2.customerId : void 0 : void 0) ? 2 : 1; - if ((daysSince(user.items.lastDrop.date, user.preferences) === 0) && (user.items.lastDrop.count >= dropMultiplier * (5 + Math.floor(user._statsComputed.per / 25) + (user.contributor.level || 0)))) { + + if (user.purchased && user.purchased.plan && user.purchased.plan.custsomerId) { + dropMultiplier = 2; + } else { + dropMultiplier = 1; + } + + if (daysSince(user.items.lastDrop.date, user.preferences) === 0 && + user.items.lastDrop.count >= dropMultiplier * (5 + Math.floor(user._statsComputed.per / 25) + (user.contributor.level || 0))) { return; } - if (((ref3 = user.flags) != null ? ref3.dropsEnabled : void 0) && user.fns.predictableRandom(user.stats.exp) < chance) { - rarity = user.fns.predictableRandom(user.stats.gp); - if (rarity > .6) { - drop = cloneDropItem(user.fns.randomVal(_.where(content.food, { - canDrop: true + + if (user.flags && user.flags.dropsEnabled && predictableRandom(user, user.stats.exp) < chance) { + rarity = predictableRandom(user, user.stats.gp); + + if (rarity > 0.6) { // food 40% chance + drop = cloneDropItem(randomVal(user, _.where(content.food, { + canDrop: true, }))); - if ((base = user.items.food)[name = drop.key] == null) { - base[name] = 0; + + if (!user.items.food[drop.key]) { + user.items.food[drop.key] = 0; } user.items.food[drop.key] += 1; drop.type = 'Food'; drop.dialog = i18n.t('messageDropFood', { dropArticle: drop.article, dropText: drop.text(req.language), - dropNotes: drop.notes(req.language) + dropNotes: drop.notes(req.language), }, req.language); - } else if (rarity > .3) { - drop = cloneDropItem(user.fns.randomVal(content.dropEggs)); - if ((base1 = user.items.eggs)[name1 = drop.key] == null) { - base1[name1] = 0; + } else if (rarity > 0.3) { // eggs 30% chance + drop = cloneDropItem(randomVal(user, content.dropEggs)); + if (!user.items.eggs[drop.key]) { + user.items.eggs[drop.key] = 0; } user.items.eggs[drop.key]++; drop.type = 'Egg'; drop.dialog = i18n.t('messageDropEgg', { dropText: drop.text(req.language), - dropNotes: drop.notes(req.language) + dropNotes: drop.notes(req.language), }, req.language); - } else { - acceptableDrops = rarity < .02 ? ['Golden'] : rarity < .09 ? ['Zombie', 'CottonCandyPink', 'CottonCandyBlue'] : rarity < .18 ? ['Red', 'Shade', 'Skeleton'] : ['Base', 'White', 'Desert']; - drop = cloneDropItem(user.fns.randomVal(_.pick(content.hatchingPotions, (function(v, k) { + } else { // Hatching Potion, 30% chance - break down by rarity. + if (rarity < 0.02) { // Very Rare: 10% (of 30%) + acceptableDrops = ['Golden']; + } else if (rarity < 0.09) { // Rare: 20% of 30% + acceptableDrops = ['Zombie', 'CottonCandyPink', 'CottonCandyBlue']; + } else if (rarity < 0.18) { // uncommon: 30% of 30% + acceptableDrops = ['Red', 'Shade', 'Skeleton']; + } else { // common, 40% of 30% + acceptableDrops = ['Base', 'White', 'Desert']; + } + drop = cloneDropItem(randomVal(user, _.pick(content.hatchingPotions, (v, k) => { return acceptableDrops.indexOf(k) >= 0; - })))); - if ((base2 = user.items.hatchingPotions)[name2 = drop.key] == null) { - base2[name2] = 0; + }))); + if (!user.items.hatchingPotions[drop.key]) { + user.items.hatchingPotions[drop.key] = 0; } user.items.hatchingPotions[drop.key]++; drop.type = 'HatchingPotion'; drop.dialog = i18n.t('messageDropPotion', { dropText: drop.text(req.language), - dropNotes: drop.notes(req.language) + dropNotes: drop.notes(req.language), }, req.language); } + user._tmp.drop = drop; - user.items.lastDrop.date = +(new Date); - return user.items.lastDrop.count++; + user.items.lastDrop.date = Number(new Date()); + user.items.lastDrop.count++; } }; diff --git a/common/script/index.js b/common/script/index.js index 0a6cfae2c2..2ae99076c3 100644 --- a/common/script/index.js +++ b/common/script/index.js @@ -86,7 +86,6 @@ api.count = count; import statsComputed from './libs/statsComputed'; -// TODO As ops and fns are ported, exported them through the api object import scoreTask from './ops/scoreTask'; import sleep from './ops/sleep'; import allocate from './ops/allocate'; @@ -123,6 +122,31 @@ import reroll from './ops/reroll'; import addPushDevice from './ops/addPushDevice'; import reset from './ops/reset'; +import autoAllocate from './fns/autoAllocate'; +import crit from './fns/crit'; +import dotGetFn from './fns/dotGet'; +import dotSetFn from './fns/dotSet'; +import handleTwoHanded from './fns/handleTwoHanded'; +import nullify from './fns/nullify'; +import predictableRandom from './fns/predictableRandom'; +import randomDrop from './fns/randomDrop'; +import randomVal from './fns/randomVal'; +import resetGear from './fns/resetGear'; +import ultimateGear from './fns/ultimateGear'; +import updateStats from './fns/updateStats'; + +api.fns = { + autoAllocate, + crit, + handleTwoHanded, + predictableRandom, + randomDrop, + randomVal, + resetGear, + ultimateGear, + updateStats, +}; + api.ops = { scoreTask, sleep, @@ -161,28 +185,12 @@ api.ops = { reset, }; -import handleTwoHanded from './fns/handleTwoHanded'; -import predictableRandom from './fns/predictableRandom'; -import randomVal from './fns/randomVal'; -import ultimateGear from './fns/ultimateGear'; -import autoAllocate from './fns/autoAllocate'; - -api.fns = { - handleTwoHanded, - predictableRandom, - randomVal, - ultimateGear, - autoAllocate, -}; - - /* ------------------------------------------------------ User (prototype wrapper to give it ops, helper funcs, and virtuals ------------------------------------------------------ */ - /* User is now wrapped (both on client and server), adding a few new properties: * getters (_statsComputed, tasks, etc) diff --git a/common/script/ops/addPushDevice.js b/common/script/ops/addPushDevice.js index 431a74177b..fb3946b658 100644 --- a/common/script/ops/addPushDevice.js +++ b/common/script/ops/addPushDevice.js @@ -6,6 +6,7 @@ import { NotAuthorized, } from '../libs/errors'; +// TODO move to server code module.exports = function addPushDevice (user, req = {}) { let regId = _.get(req, 'body.regId'); if (!regId) throw new BadRequest(i18n.t('regIdRequired', req.language)); diff --git a/common/script/ops/scoreTask.js b/common/script/ops/scoreTask.js index ddaf0a155c..ba0ca42c47 100644 --- a/common/script/ops/scoreTask.js +++ b/common/script/ops/scoreTask.js @@ -4,6 +4,7 @@ import { } from '../libs/errors'; import i18n from '../i18n'; import updateStats from '../fns/updateStats'; +import crit from '../fns/crit'; const MAX_TASK_VALUE = 21.27; const MIN_TASK_VALUE = -47.27; @@ -105,7 +106,7 @@ function _subtractPoints (user, task, stats, delta) { function _addPoints (user, task, stats, direction, delta) { // ===== CRITICAL HITS ===== // allow critical hit only when checking off a task, not when unchecking it: - let _crit = delta > 0 ? user.fns.crit() : 1; + let _crit = delta > 0 ? crit(user) : 1; // if there was a crit, alert the user via notification if (_crit > 1) user._tmp.crit = _crit; diff --git a/test/common/fns/predictableRandom.js b/test/common/fns/predictableRandom.test.js similarity index 100% rename from test/common/fns/predictableRandom.js rename to test/common/fns/predictableRandom.test.js diff --git a/test/common/fns/randomDrop.test.js b/test/common/fns/randomDrop.test.js new file mode 100644 index 0000000000..9569067ffc --- /dev/null +++ b/test/common/fns/randomDrop.test.js @@ -0,0 +1,165 @@ +// TODO disable until we can find a way to stub predictableRandom + +/* eslint-disable */ + +import randomDrop from '../../../common/script/fns/randomDrop'; +import { + generateUser, + generateTodo, + generateHabit, + generateDaily, + generateReward, +} from '../../helpers/common.helper'; +// import predictableRandom from '../../../common/script/fns/predictableRandom'; // eslint-disable +import content from '../../../common/script/content/index'; + +xdescribe('common.fns.randomDrop', () => { + let user; + let task; + let predictableRandom; + + beforeEach(() => { + user = generateUser(); + user._tmp = user._tmp ? user._tmp : {}; + task = generateTodo({ userId: user._id }); + predictableRandom = () => { + return 0.5; + }; + }); + + /** + * function signature as follows: + * randomDrop(user, modifiers) {} + * modifiers = { task, delta = null } + **/ + + it('drops an item for the user.party.quest.progress', () => { + expect(user.party.quest.progress.collect).to.eql({}); + user.party.quest.key = 'vice2'; + let collectWhat = Object.keys(content.quests[user.party.quest.key].collect)[0]; // lightCrystal + predictableRandom = () => { + return 0.0001; + }; + randomDrop(user, { task }); + expect(user.party.quest.progress.collect[collectWhat]).to.eql(1); + randomDrop(user, { task }); + expect(user.party.quest.progress.collect[collectWhat]).to.eql(2); + }); + + context('drops enabled', () => { + beforeEach(() => { + user.flags.dropsEnabled = true; + task.priority = 100000; + }); + + it('does nothing if user.items.lastDrop.count is exceeded', () => { + user.items.lastDrop.count = 100; + randomDrop(user, { task }); + expect(user._tmp).to.eql({}); + }); + + it('drops something when the task is a todo', () => { + expect(user._tmp).to.eql({}); + user.flags.dropsEnabled = true; + predictableRandom = () => { + return 0.1; + }; + randomDrop(user, { task }); + expect(user._tmp).to.not.eql({}); + }); + + it('drops something when the task is a habit', () => { + task = generateHabit({ userId: user._id }); + expect(user._tmp).to.eql({}); + user.flags.dropsEnabled = true; + predictableRandom = () => { + return 0.1; + }; + randomDrop(user, { task }); + expect(user._tmp).to.not.eql({}); + }); + + it('drops something when the task is a daily', () => { + task = generateDaily({ userId: user._id }); + expect(user._tmp).to.eql({}); + user.flags.dropsEnabled = true; + predictableRandom = () => { + return 0.1; + }; + randomDrop(user, { task }); + expect(user._tmp).to.not.eql({}); + }); + + it('drops something when the task is a reward', () => { + task = generateReward({ userId: user._id }); + expect(user._tmp).to.eql({}); + user.flags.dropsEnabled = true; + predictableRandom = () => { + return 0.1; + }; + randomDrop(user, { task }); + expect(user._tmp).to.not.eql({}); + }); + + it('drops food', () => { + predictableRandom = () => { + return 0.65; + }; + randomDrop(user, { task }); + expect(user._tmp.drop.type).to.eql('Food'); + }); + + it('drops eggs', () => { + predictableRandom = () => { + return 0.35; + }; + randomDrop(user, { task }); + expect(user._tmp.drop.type).to.eql('Egg'); + }); + + context('drops hatching potion', () => { + it('drops a very rare potion', () => { + predictableRandom = () => { + return 0.01; + }; + randomDrop(user, { task }); + expect(user._tmp.drop.type).to.eql('HatchingPotion'); + expect(user._tmp.drop.value).to.eql(5); + expect(user._tmp.drop.key).to.eql('Golden'); + }); + + it('drops a rare potion', () => { + predictableRandom = () => { + return 0.08; + }; + randomDrop(user, { task }); + expect(user._tmp.drop.type).to.eql('HatchingPotion'); + expect(user._tmp.drop.value).to.eql(4); + let acceptableDrops = ['Zombie', 'CottonCandyPink', 'CottonCandyBlue']; + expect(acceptableDrops).to.contain(user._tmp.drop.key); // deterministically 'CottonCandyBlue' + }); + + it('drops an uncommon potion', () => { + predictableRandom = () => { + return 0.17; + }; + randomDrop(user, { task }); + expect(user._tmp.drop.type).to.eql('HatchingPotion'); + expect(user._tmp.drop.value).to.eql(3); + let acceptableDrops = ['Red', 'Shade', 'Skeleton']; + expect(acceptableDrops).to.contain(user._tmp.drop.key); // always skeleton + }); + + it('drops a common potion', () => { + predictableRandom = () => { + return 0.20; + }; + randomDrop(user, { task }); + expect(user._tmp.drop.type).to.eql('HatchingPotion'); + expect(user._tmp.drop.value).to.eql(2); + let acceptableDrops = ['Base', 'White', 'Desert']; + expect(acceptableDrops).to.contain(user._tmp.drop.key); // always Desert + }); + }); + }); +});