From 3844bafae5cb545b8ee54454aeb7cdfd61ef7f52 Mon Sep 17 00:00:00 2001 From: Victor Piousbox Date: Sun, 3 Apr 2016 02:07:28 +0000 Subject: [PATCH] shared-code-random-drop --- common/script/fns/predictableRandomWrapper.js | 5 + common/script/fns/randomDrop.js | 114 ++++++++----- tasks/gulp-eslint.js | 1 - ...bleRandom.js => predictableRandom.test.js} | 0 test/common/fns/randomDrop.test.js | 160 ++++++++++++++++++ 5 files changed, 241 insertions(+), 39 deletions(-) create mode 100644 common/script/fns/predictableRandomWrapper.js rename test/common/fns/{predictableRandom.js => predictableRandom.test.js} (100%) create mode 100644 test/common/fns/randomDrop.test.js diff --git a/common/script/fns/predictableRandomWrapper.js b/common/script/fns/predictableRandomWrapper.js new file mode 100644 index 0000000000..49cb401e12 --- /dev/null +++ b/common/script/fns/predictableRandomWrapper.js @@ -0,0 +1,5 @@ +import predictableRandom from '../../../common/script/fns/predictableRandom'; + +module.exports = { + predictableRandom, +}; diff --git a/common/script/fns/randomDrop.js b/common/script/fns/randomDrop.js index 0ab6a5e69a..ab0f1b7927 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 './predictableRandomWrapper'; +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/tasks/gulp-eslint.js b/tasks/gulp-eslint.js index 829f47ba96..cbf8716aed 100644 --- a/tasks/gulp-eslint.js +++ b/tasks/gulp-eslint.js @@ -50,7 +50,6 @@ const COMMON_FILES = [ '!./common/script/fns/getItem.js', '!./common/script/fns/nullify.js', '!./common/script/fns/preenUserHistory.js', - '!./common/script/fns/randomDrop.js', '!./common/script/libs/appliedTags.js', '!./common/script/libs/countExists.js', '!./common/script/libs/dotGet.js', 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..e4f3141bd6 --- /dev/null +++ b/test/common/fns/randomDrop.test.js @@ -0,0 +1,160 @@ +import randomDrop from '../../../common/script/fns/randomDrop'; +import { + generateUser, + generateTodo, + generateHabit, + generateDaily, + generateReward, +} from '../../helpers/common.helper'; +import predictableRandomWrapper from '../../../common/script/fns/predictableRandomWrapper'; +import content from '../../../common/script/content/index'; + +describe('common.fns.randomDrop', () => { + let user; + let task; + + beforeEach(() => { + user = generateUser(); + user._tmp = user._tmp ? user._tmp : {}; + task = generateTodo({ userId: user._id }); + predictableRandomWrapper.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 + predictableRandomWrapper.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; + predictableRandomWrapper.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; + predictableRandomWrapper.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; + predictableRandomWrapper.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; + predictableRandomWrapper.predictableRandom = () => { + return 0.1; + }; + randomDrop(user, { task }); + expect(user._tmp).to.not.eql({}); + }); + + it('drops food', () => { + predictableRandomWrapper.predictableRandom = () => { + return 0.65; + }; + randomDrop(user, { task }); + expect(user._tmp.drop.type).to.eql('Food'); + }); + + it('drops eggs', () => { + predictableRandomWrapper.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', () => { + predictableRandomWrapper.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', () => { + predictableRandomWrapper.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', () => { + predictableRandomWrapper.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', () => { + predictableRandomWrapper.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 + }); + }); + }); +});