diff --git a/common/script/fns/autoAllocate.js b/common/script/fns/autoAllocate.js new file mode 100644 index 0000000000..ab037e628b --- /dev/null +++ b/common/script/fns/autoAllocate.js @@ -0,0 +1,56 @@ +import _ from 'lodash'; +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 + */ + +module.exports = function(user) { + return user.stats[(function() { + var diff, ideal, lvlDiv7, preference, stats, suggested; + switch (user.preferences.allocationMode) { + case "flat": + stats = _.pick(user.stats, splitWhitespace('con str per int')); + return _.invert(stats)[_.min(stats)]; + case "classbased": + lvlDiv7 = user.stats.lvl / 7; + ideal = [lvlDiv7 * 3, lvlDiv7 * 2, lvlDiv7, lvlDiv7]; + preference = (function() { + switch (user.stats["class"]) { + case "wizard": + return ["int", "per", "con", "str"]; + case "rogue": + return ["per", "str", "int", "con"]; + case "healer": + return ["con", "int", "str", "per"]; + default: + return ["str", "con", "per", "int"]; + } + })(); + diff = [user.stats[preference[0]] - ideal[0], user.stats[preference[1]] - ideal[1], user.stats[preference[2]] - ideal[2], user.stats[preference[3]] - ideal[3]]; + suggested = _.findIndex(diff, (function(val) { + if (val === _.min(diff)) { + return true; + } + })); + if (~suggested) { + return preference[suggested]; + } else { + return "str"; + } + case "taskbased": + suggested = _.invert(user.stats.training)[_.max(user.stats.training)]; + _.merge(user.stats.training, { + str: 0, + int: 0, + con: 0, + per: 0 + }); + return suggested || "str"; + default: + return "str"; + } + })()]++; +}; diff --git a/common/script/fns/crit.js b/common/script/fns/crit.js new file mode 100644 index 0000000000..69ac9e5b93 --- /dev/null +++ b/common/script/fns/crit.js @@ -0,0 +1,15 @@ +module.exports = function(user, stat, chance) { + var s; + if (stat == null) { + stat = 'str'; + } + if (chance == null) { + chance = .03; + } + s = user._statsComputed[stat]; + if (user.fns.predictableRandom() <= chance * (1 + s / 100)) { + return 1.5 + 4 * s / (s + 200); + } else { + return 1; + } +}; diff --git a/common/script/fns/cron.js b/common/script/fns/cron.js new file mode 100644 index 0000000000..b43da4e0cd --- /dev/null +++ b/common/script/fns/cron.js @@ -0,0 +1,279 @@ +import moment from 'moment'; +import _ from 'lodash'; +import { + daysSince, + shouldDo, +} from '../cron'; +import { + capByLevel, + toNextLevel, +} from '../statHelpers'; +/* + ------------------------------------------------------ + Cron + ------------------------------------------------------ + */ + +/* + At end of day, add value to all incomplete Daily & Todo tasks (further incentive) + For incomplete Dailys, deduct experience + Make sure to run this function once in a while as server will not take care of overnight calculations. + And you have to run it every time client connects. + {user} + */ + +module.exports = function(user, options) { + var _progress, analyticsData, base, base1, base2, base3, base4, clearBuffs, dailyChecked, dailyDueUnchecked, daysMissed, expTally, lvl, lvlDiv2, multiDaysCountAsOneDay, now, perfect, plan, progress, ref, ref1, ref2, ref3, todoTally; + if (options == null) { + options = {}; + } + now = +options.now || +(new Date); + daysMissed = daysSince(user.lastCron, _.defaults({ + now: now + }, user.preferences)); + if (!(daysMissed > 0)) { + return; + } + user.auth.timestamps.loggedin = new Date(); + user.lastCron = now; + if (user.items.lastDrop.count > 0) { + user.items.lastDrop.count = 0; + } + perfect = true; + clearBuffs = { + str: 0, + int: 0, + per: 0, + con: 0, + stealth: 0, + streaks: false + }; + plan = (ref = user.purchased) != null ? ref.plan : void 0; + if (plan != null ? plan.customerId : void 0) { + if (typeof plan.dateUpdated === "undefined") { + // partial compensation for bug in subscription creation - https://github.com/HabitRPG/habitrpg/issues/6682 + plan.dateUpdated = new Date(); + } + if (moment(plan.dateUpdated).format('MMYYYY') !== moment().format('MMYYYY')) { + plan.gemsBought = 0; + plan.dateUpdated = new Date(); + _.defaults(plan.consecutive, { + count: 0, + offset: 0, + trinkets: 0, + gemCapExtra: 0 + }); + plan.consecutive.count++; + if (plan.consecutive.offset > 0) { + plan.consecutive.offset--; + } else if (plan.consecutive.count % 3 === 0) { + plan.consecutive.trinkets++; + plan.consecutive.gemCapExtra += 5; + if (plan.consecutive.gemCapExtra > 25) { + plan.consecutive.gemCapExtra = 25; + } + } + } + if (plan.dateTerminated && moment(plan.dateTerminated).isBefore(+(new Date))) { + _.merge(plan, { + planId: null, + customerId: null, + paymentMethod: null + }); + _.merge(plan.consecutive, { + count: 0, + offset: 0, + gemCapExtra: 0 + }); + if (typeof user.markModified === "function") { + user.markModified('purchased.plan'); + } + } + } + if (user.preferences.sleep === true) { + user.stats.buffs = clearBuffs; + user.dailys.forEach(function(daily) { + var completed, repeat, thatDay; + completed = daily.completed, repeat = daily.repeat; + thatDay = moment(now).subtract({ + days: 1 + }); + if (shouldDo(thatDay.toDate(), daily, user.preferences) || completed) { + _.each(daily.checklist, (function(box) { + box.completed = false; + return true; + })); + } + return daily.completed = false; + }); + return; + } + multiDaysCountAsOneDay = true; + todoTally = 0; + user.todos.forEach(function(task) { + var absVal, completed, delta, id; + if (!task) { + return; + } + id = task.id, completed = task.completed; + delta = user.ops.score({ + params: { + id: task.id, + direction: 'down' + }, + query: { + times: multiDaysCountAsOneDay != null ? multiDaysCountAsOneDay : { + 1: daysMissed + }, + cron: true + } + }); + absVal = completed ? Math.abs(task.value) : task.value; + return todoTally += absVal; + }); + dailyChecked = 0; + dailyDueUnchecked = 0; + if ((base = user.party.quest.progress).down == null) { + base.down = 0; + } + user.dailys.forEach(function(task) { + var EvadeTask, completed, delta, fractionChecked, id, j, n, ref1, ref2, scheduleMisses, thatDay; + if (!task) { + return; + } + id = task.id, completed = task.completed; + EvadeTask = 0; + scheduleMisses = daysMissed; + if (completed) { + dailyChecked += 1; + } else { + scheduleMisses = 0; + for (n = j = 0, ref1 = daysMissed; 0 <= ref1 ? j < ref1 : j > ref1; n = 0 <= ref1 ? ++j : --j) { + thatDay = moment(now).subtract({ + days: n + 1 + }); + if (shouldDo(thatDay.toDate(), task, user.preferences)) { + scheduleMisses++; + if (user.stats.buffs.stealth) { + user.stats.buffs.stealth--; + EvadeTask++; + } + if (multiDaysCountAsOneDay) { + break; + } + } + } + if (scheduleMisses > EvadeTask) { + perfect = false; + if (((ref2 = task.checklist) != null ? ref2.length : void 0) > 0) { + fractionChecked = _.reduce(task.checklist, (function(m, i) { + return m + (i.completed ? 1 : 0); + }), 0) / task.checklist.length; + dailyDueUnchecked += 1 - fractionChecked; + dailyChecked += fractionChecked; + } else { + dailyDueUnchecked += 1; + } + delta = user.ops.score({ + params: { + id: task.id, + direction: 'down' + }, + query: { + times: multiDaysCountAsOneDay != null ? multiDaysCountAsOneDay : { + 1: scheduleMisses - EvadeTask + }, + cron: true + } + }); + user.party.quest.progress.down += delta * (task.priority < 1 ? task.priority : 1); + } + } + (task.history != null ? task.history : task.history = []).push({ + date: +(new Date), + value: task.value + }); + task.completed = false; + if (completed || (scheduleMisses > 0)) { + return _.each(task.checklist, (function(i) { + i.completed = false; + return true; + })); + } + }); + user.habits.forEach(function(task) { + if (task.up === false || task.down === false) { + if (Math.abs(task.value) < 0.1) { + return task.value = 0; + } else { + return task.value = task.value / 2; + } + } + }); + ((base1 = (user.history != null ? user.history : user.history = {})).todos != null ? base1.todos : base1.todos = []).push({ + date: now, + value: todoTally + }); + expTally = user.stats.exp; + lvl = 0; + while (lvl < (user.stats.lvl - 1)) { + lvl++; + expTally += toNextLevel(lvl); + } + ((base2 = user.history).exp != null ? base2.exp : base2.exp = []).push({ + date: now, + value: expTally + }); + if (!((ref1 = user.purchased) != null ? (ref2 = ref1.plan) != null ? ref2.customerId : void 0 : void 0)) { + user.fns.preenUserHistory(); + if (typeof user.markModified === "function") { + user.markModified('history'); + } + if (typeof user.markModified === "function") { + user.markModified('dailys'); + } + } + user.stats.buffs = perfect ? ((base3 = user.achievements).perfect != null ? base3.perfect : base3.perfect = 0, user.achievements.perfect++, lvlDiv2 = Math.ceil(capByLevel(user.stats.lvl) / 2), { + str: lvlDiv2, + int: lvlDiv2, + per: lvlDiv2, + con: lvlDiv2, + stealth: 0, + streaks: false + }) : clearBuffs; + if (dailyDueUnchecked === 0 && dailyChecked === 0) { + dailyChecked = 1; + } + user.stats.mp += _.max([10, .1 * user._statsComputed.maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked); + if (user.stats.mp > user._statsComputed.maxMP) { + user.stats.mp = user._statsComputed.maxMP; + } + progress = user.party.quest.progress; + _progress = _.cloneDeep(progress); + _.merge(progress, { + down: 0, + up: 0 + }); + progress.collect = _.transform(progress.collect, (function(m, v, k) { + return m[k] = 0; + })); + if ((base4 = user.flags).cronCount == null) { + base4.cronCount = 0; + } + user.flags.cronCount++; + analyticsData = { + category: 'behavior', + gaLabel: 'Cron Count', + gaValue: user.flags.cronCount, + uuid: user._id, + user: user, + resting: user.preferences.sleep, + cronCount: user.flags.cronCount, + progressUp: _.min([_progress.up, 900]), + progressDown: _progress.down + }; + if ((ref3 = options.analytics) != null) { + ref3.track('Cron', analyticsData); + } + return _progress; +}; diff --git a/common/script/fns/dotGet.js b/common/script/fns/dotGet.js new file mode 100644 index 0000000000..c95ba55656 --- /dev/null +++ b/common/script/fns/dotGet.js @@ -0,0 +1,5 @@ +import dotGet from '../libs/dotGet'; + +module.exports = function(user, path) { + return dotGet(user, path); +}; diff --git a/common/script/fns/dotSet.js b/common/script/fns/dotSet.js new file mode 100644 index 0000000000..283e7a50d0 --- /dev/null +++ b/common/script/fns/dotSet.js @@ -0,0 +1,12 @@ +import dotSet from '../libs/dotSet'; + +/* +This allows you to set object properties by dot-path. Eg, you can run pathSet('stats.hp',50,user) which is the same as +user.stats.hp = 50. This is useful because in our habitrpg-shared functions we're returning changesets as {path:value}, +so that different consumers can implement setters their own way. Derby needs model.set(path, value) for example, where +Angular sets object properties directly - in which case, this function will be used. + */ + +module.exports = function(user, path, val) { + return dotSet(user, path, val); +}; diff --git a/common/script/fns/getItem.js b/common/script/fns/getItem.js new file mode 100644 index 0000000000..b73ecf9073 --- /dev/null +++ b/common/script/fns/getItem.js @@ -0,0 +1,11 @@ +import content from '../content/index'; +import i18n from '../i18n'; + +module.exports = function(user, type) { + var item; + item = content.gear.flat[user.items.gear.equipped[type]]; + if (!item) { + return content.gear.flat[type + "_base_0"]; + } + return item; +}; diff --git a/common/script/fns/handleTwoHanded.js b/common/script/fns/handleTwoHanded.js new file mode 100644 index 0000000000..c700346988 --- /dev/null +++ b/common/script/fns/handleTwoHanded.js @@ -0,0 +1,24 @@ +import content from '../content/index'; +import i18n from '../i18n'; + +module.exports = function(user, item, type, req) { + var message, currentWeapon, currentShield; + if (type == null) { + type = 'equipped'; + } + currentShield = content.gear.flat[user.items.gear[type].shield]; + currentWeapon = content.gear.flat[user.items.gear[type].weapon]; + + if (item.type === "shield" && (currentWeapon ? currentWeapon.twoHanded : false)) { + user.items.gear[type].weapon = 'weapon_base_0'; + message = i18n.t('messageTwoHandedUnequip', { + twoHandedText: currentWeapon.text(req.language), offHandedText: item.text(req.language), + }, req.language); + } else if (item.twoHanded && (currentShield && user.items.gear[type].shield != "shield_base_0")) { + user.items.gear[type].shield = "shield_base_0"; + message = i18n.t('messageTwoHandedEquip', { + twoHandedText: item.text(req.language), offHandedText: currentShield.text(req.language), + }, req.language); + } + return message; +}; diff --git a/common/script/fns/index.js b/common/script/fns/index.js new file mode 100644 index 0000000000..24f3ab604b --- /dev/null +++ b/common/script/fns/index.js @@ -0,0 +1,31 @@ +import getItem from './getItem'; +import handleTwoHanded from './handleTwoHanded'; +import predictableRandom from './predictableRandom'; +import crit from './crit'; +import randomVal from './randomVal'; +import dotSet from './dotSet'; +import dotGet from './dotGet'; +import randomDrop from './randomDrop'; +import autoAllocate from './autoAllocate'; +import updateStats from './updateStats'; +import cron from './cron'; +import preenUserHistory from './preenUserHistory'; +import ultimateGear from './ultimateGear'; +import nullify from './nullify'; + +module.exports = { + getItem, + handleTwoHanded, + predictableRandom, + crit, + randomVal, + dotSet, + dotGet, + randomDrop, + autoAllocate, + updateStats, + cron, + preenUserHistory, + ultimateGear, + nullify, +}; diff --git a/common/script/fns/nullify.js b/common/script/fns/nullify.js new file mode 100644 index 0000000000..b6e30aa3b9 --- /dev/null +++ b/common/script/fns/nullify.js @@ -0,0 +1,5 @@ +module.exports = function(user) { + user.ops = null; + user.fns = null; + return user = null; +}; diff --git a/common/script/fns/predictableRandom.js b/common/script/fns/predictableRandom.js new file mode 100644 index 0000000000..64c8153746 --- /dev/null +++ b/common/script/fns/predictableRandom.js @@ -0,0 +1,20 @@ +import _ from 'lodash'; +/* +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 + */ + +module.exports = function(user, seed) { + var x; + if (!seed || seed === Math.PI) { + seed = _.reduce(user.stats, (function(m, v) { + if (_.isNumber(v)) { + return m + v; + } else { + return m; + } + }), 0); + } + x = Math.sin(seed++) * 10000; + return x - Math.floor(x); +}; diff --git a/common/script/fns/preenUserHistory.js b/common/script/fns/preenUserHistory.js new file mode 100644 index 0000000000..a45dd82719 --- /dev/null +++ b/common/script/fns/preenUserHistory.js @@ -0,0 +1,25 @@ +import _ from 'lodash'; +import preenHistory from '../libs/preenHistory'; + +module.exports = function(user, minHistLen) { + if (minHistLen == null) { + minHistLen = 7; + } + _.each(user.habits.concat(user.dailys), function(task) { + var ref; + if (((ref = task.history) != null ? ref.length : void 0) > minHistLen) { + task.history = preenHistory(task.history); + } + return true; + }); + _.defaults(user.history, { + todos: [], + exp: [] + }); + if (user.history.exp.length > minHistLen) { + user.history.exp = preenHistory(user.history.exp); + } + if (user.history.todos.length > minHistLen) { + return user.history.todos = preenHistory(user.history.todos); + } +}; diff --git a/common/script/fns/randomDrop.js b/common/script/fns/randomDrop.js new file mode 100644 index 0000000000..e9c284afe0 --- /dev/null +++ b/common/script/fns/randomDrop.js @@ -0,0 +1,75 @@ +import _ from 'lodash'; +import content from '../content/index'; +import i18n from '../i18n'; +import { daysSince } from '../cron'; +import { diminishingReturns } from '../statHelpers'; + +module.exports = function(user, modifiers, req) { + var acceptableDrops, base, base1, base2, chance, drop, dropK, dropMultiplier, name, name1, name2, quest, rarity, ref, ref1, ref2, ref3, 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 = 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 + }); + user.party.quest.progress.collect[dropK]++; + if (typeof user.markModified === "function") { + 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)))) { + 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 = user.fns.randomVal(_.where(content.food, { + canDrop: true + })); + if ((base = user.items.food)[name = drop.key] == null) { + base[name] = 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) + }, req.language); + } else if (rarity > .3) { + drop = user.fns.randomVal(content.dropEggs); + if ((base1 = user.items.eggs)[name1 = drop.key] == null) { + base1[name1] = 0; + } + user.items.eggs[drop.key]++; + drop.type = 'Egg'; + drop.dialog = i18n.t('messageDropEgg', { + dropText: drop.text(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 = user.fns.randomVal(_.pick(content.hatchingPotions, (function(v, k) { + return acceptableDrops.indexOf(k) >= 0; + }))); + if ((base2 = user.items.hatchingPotions)[name2 = drop.key] == null) { + base2[name2] = 0; + } + user.items.hatchingPotions[drop.key]++; + drop.type = 'HatchingPotion'; + drop.dialog = i18n.t('messageDropPotion', { + dropText: drop.text(req.language), + dropNotes: drop.notes(req.language) + }, req.language); + } + user._tmp.drop = drop; + user.items.lastDrop.date = +(new Date); + return user.items.lastDrop.count++; + } +}; diff --git a/common/script/fns/randomVal.js b/common/script/fns/randomVal.js new file mode 100644 index 0000000000..3c2b5b82e8 --- /dev/null +++ b/common/script/fns/randomVal.js @@ -0,0 +1,14 @@ +import _ from 'lodash'; + +/* + Get a random property from an object + returns random property (the value) + */ + +module.exports = function(user, obj, options) { + var array, rand; + array = (options != null ? options.key : void 0) ? _.keys(obj) : _.values(obj); + rand = user.fns.predictableRandom(options != null ? options.seed : void 0); + array.sort(); + return array[Math.floor(rand * array.length)]; +}; diff --git a/common/script/fns/ultimateGear.js b/common/script/fns/ultimateGear.js new file mode 100644 index 0000000000..1333e8cc38 --- /dev/null +++ b/common/script/fns/ultimateGear.js @@ -0,0 +1,33 @@ +import content from '../content/index'; +import _ from 'lodash'; + +module.exports = function(user) { + var base, owned; + owned = typeof window !== "undefined" && window !== null ? user.items.gear.owned : user.items.gear.owned.toObject(); + if ((base = user.achievements).ultimateGearSets == null) { + base.ultimateGearSets = { + healer: false, + wizard: false, + rogue: false, + warrior: false + }; + } + content.classes.forEach(function(klass) { + if (user.achievements.ultimateGearSets[klass] !== true) { + return user.achievements.ultimateGearSets[klass] = _.reduce(['armor', 'shield', 'head', 'weapon'], function(soFarGood, type) { + var found; + found = _.find(content.gear.tree[type][klass], { + last: true + }); + return soFarGood && (!found || owned[found.key] === true); + }, true); + } + }); + if (typeof user.markModified === "function") { + user.markModified('achievements.ultimateGearSets'); + } + if (_.contains(user.achievements.ultimateGearSets, true) && user.flags.armoireEnabled !== true) { + user.flags.armoireEnabled = true; + return typeof user.markModified === "function" ? user.markModified('flags') : void 0; + } +}; diff --git a/common/script/fns/updateStats.js b/common/script/fns/updateStats.js new file mode 100644 index 0000000000..3ce83ed664 --- /dev/null +++ b/common/script/fns/updateStats.js @@ -0,0 +1,109 @@ +import _ from 'lodash'; +import { + MAX_HEALTH, + MAX_STAT_POINTS +} from '../constants'; +import { toNextLevel } from '../statHelpers'; +module.exports = function (user, stats, req, analytics) { + let allocatedStatPoints; + let totalStatPoints; + let experienceToNextLevel; + + if (stats.hp <= 0) { + user.stats.hp = 0; + return user.stats.hp; + } + + user.stats.hp = stats.hp; + user.stats.gp = stats.gp >= 0 ? stats.gp : 0; + + experienceToNextLevel = toNextLevel(user.stats.lvl); + + if (stats.exp >= experienceToNextLevel) { + user.stats.exp = stats.exp; + + while (stats.exp >= experienceToNextLevel) { + stats.exp -= experienceToNextLevel; + user.stats.lvl++; + + experienceToNextLevel = toNextLevel(user.stats.lvl); + user.stats.hp = MAX_HEALTH; + allocatedStatPoints = user.stats.str + user.stats.int + user.stats.con + user.stats.per; + totalStatPoints = allocatedStatPoints + user.stats.points; + + if (totalStatPoints >= MAX_STAT_POINTS) { + continue; // eslint-disable-line no-continue + } + if (user.preferences.automaticAllocation) { + user.fns.autoAllocate(); + } else { + user.stats.points = user.stats.lvl - allocatedStatPoints; + totalStatPoints = user.stats.points + allocatedStatPoints; + + if (totalStatPoints > MAX_STAT_POINTS) { + user.stats.points = MAX_STAT_POINTS - allocatedStatPoints; + } + + if (user.stats.points < 0) { + user.stats.points = 0; + } + } + } + } + + user.stats.exp = stats.exp; + user.flags = user.flags || {}; + + if (!user.flags.customizationsNotification && (user.stats.exp > 5 || user.stats.lvl > 1)) { + user.flags.customizationsNotification = true; + } + if (!user.flags.itemsEnabled && (user.stats.exp > 10 || user.stats.lvl > 1)) { + user.flags.itemsEnabled = true; + } + if (!user.flags.dropsEnabled && user.stats.lvl >= 3) { + user.flags.dropsEnabled = true; + if (user.items.eggs["Wolf"] > 0) { + user.items.eggs["Wolf"]++; + } else { + user.items.eggs["Wolf"] = 1; + } + } + if (!user.flags.classSelected && user.stats.lvl >= 10) { + user.flags.classSelected; + } + _.each({ + vice1: 30, + atom1: 15, + moonstone1: 60, + goldenknight1: 40 + }, function(lvl, k) { + var analyticsData, base, base1, ref; + if (!((ref = user.flags.levelDrops) != null ? ref[k] : void 0) && user.stats.lvl >= lvl) { + if ((base = user.items.quests)[k] == null) { + base[k] = 0; + } + user.items.quests[k]++; + ((base1 = user.flags).levelDrops != null ? base1.levelDrops : base1.levelDrops = {})[k] = true; + if (typeof user.markModified === "function") { + user.markModified('flags.levelDrops'); + } + analyticsData = { + uuid: user._id, + itemKey: k, + acquireMethod: 'Level Drop', + category: 'behavior' + }; + if (analytics != null) { + analytics.track('acquire item', analyticsData); + } + if (!user._tmp) user._tmp = {} + return user._tmp.drop = { + type: 'Quest', + key: k + }; + } + }); + if (!user.flags.rebirthEnabled && (user.stats.lvl >= 50 || user.achievements.beastMaster)) { + return user.flags.rebirthEnabled = true; + } +}; diff --git a/common/script/index.js b/common/script/index.js index 97484f7c2b..aad11a061d 100644 --- a/common/script/index.js +++ b/common/script/index.js @@ -32,73 +32,12 @@ api.tnl = statHelpers.toNextLevel; api.diminishingReturns = statHelpers.diminishingReturns; $w = api.$w = importedLibs.splitWhitespace; - -api.dotSet = function(obj, path, val) { - var arr; - arr = path.split('.'); - return _.reduce(arr, (function(_this) { - return function(curr, next, index) { - if ((arr.length - 1) === index) { - curr[next] = val; - } - return curr[next] != null ? curr[next] : curr[next] = {}; - }; - })(this), obj); -}; - -api.dotGet = function(obj, path) { - return _.reduce(path.split('.'), ((function(_this) { - return function(curr, next) { - return curr != null ? curr[next] : void 0; - }; - })(this)), obj); -}; - - +api.dotSet = importedLibs.dotSet; +api.dotGet = importedLibs.dotGet; api.refPush = importedLibs.refPush; - api.planGemLimits = importedLibs.planGemLimits; -/* -Preen history for users with > 7 history entries -This takes an infinite array of single day entries [day day day day day...], and turns it into a condensed array -of averages, condensing more the further back in time we go. Eg, 7 entries each for last 7 days; 1 entry each week -of this month; 1 entry for each month of this year; 1 entry per previous year: [day*7 week*4 month*12 year*infinite] - */ - -preenHistory = function(history) { - var newHistory, preen, thisMonth; - history = _.filter(history, function(h) { - return !!h; - }); - newHistory = []; - preen = function(amount, groupBy) { - var groups; - groups = _.chain(history).groupBy(function(h) { - return moment(h.date).format(groupBy); - }).sortBy(function(h, k) { - return k; - }).value(); - groups = groups.slice(-amount); - groups.pop(); - return _.each(groups, function(group) { - newHistory.push({ - date: moment(group[0].date).toDate(), - value: _.reduce(group, (function(m, obj) { - return m + obj.value; - }), 0) / group.length - }); - return true; - }); - }; - preen(50, "YYYY"); - preen(moment().format('MM'), "YYYYMM"); - thisMonth = moment().format('YYYYMM'); - newHistory = newHistory.concat(_.filter(history, function(h) { - return moment(h.date).format('YYYYMM') === thisMonth; - })); - return newHistory; -}; +preenHistory = importedLibs.preenHistory; /* Preen 3-day past-completed To-Dos from Angular & mobile app @@ -414,6 +353,7 @@ TODO */ import importedOps from './ops'; +import importedFns from './fns'; api.wrap = function(user, main) { if (main == null) { @@ -431,7 +371,7 @@ api.wrap = function(user, main) { reset: _.partial(importedOps.reset, user), reroll: _.partial(importedOps.reroll, user), rebirth: _.partial(importedOps.rebirth, user), - allocateNow: _.partial(importedOps.reroll, user), + allocateNow: _.partial(importedOps.allocateNow, user), clearCompleted: _.partial(importedOps.clearCompleted, user), sortTask: _.partial(importedOps.sortTask, user), updateTask: _.partial(importedOps.updateTask, user), @@ -473,645 +413,20 @@ api.wrap = function(user, main) { }; } user.fns = { - getItem: function(type) { - var item; - item = content.gear.flat[user.items.gear.equipped[type]]; - if (!item) { - return content.gear.flat[type + "_base_0"]; - } - return item; - }, - handleTwoHanded: function(item, type, req) { - var message, currentWeapon, currentShield; - if (type == null) { - type = 'equipped'; - } - currentShield = content.gear.flat[user.items.gear[type].shield]; - currentWeapon = content.gear.flat[user.items.gear[type].weapon]; - - if (item.type === "shield" && (currentWeapon ? currentWeapon.twoHanded : false)) { - user.items.gear[type].weapon = 'weapon_base_0'; - message = i18n.t('messageTwoHandedUnequip', { - twoHandedText: currentWeapon.text(req.language), offHandedText: item.text(req.language), - }, req.language); - } else if (item.twoHanded && (currentShield && user.items.gear[type].shield != "shield_base_0")) { - user.items.gear[type].shield = "shield_base_0"; - message = i18n.t('messageTwoHandedEquip', { - twoHandedText: item.text(req.language), offHandedText: currentShield.text(req.language), - }, req.language); - } - return message; - }, - - /* - 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 - */ - predictableRandom: function(seed) { - var x; - if (!seed || seed === Math.PI) { - seed = _.reduce(user.stats, (function(m, v) { - if (_.isNumber(v)) { - return m + v; - } else { - return m; - } - }), 0); - } - x = Math.sin(seed++) * 10000; - return x - Math.floor(x); - }, - crit: function(stat, chance) { - var s; - if (stat == null) { - stat = 'str'; - } - if (chance == null) { - chance = .03; - } - s = user._statsComputed[stat]; - if (user.fns.predictableRandom() <= chance * (1 + s / 100)) { - return 1.5 + 4 * s / (s + 200); - } else { - return 1; - } - }, - - /* - Get a random property from an object - returns random property (the value) - */ - randomVal: function(obj, options) { - var array, rand; - array = (options != null ? options.key : void 0) ? _.keys(obj) : _.values(obj); - rand = user.fns.predictableRandom(options != null ? options.seed : void 0); - array.sort(); - return array[Math.floor(rand * array.length)]; - }, - - /* - This allows you to set object properties by dot-path. Eg, you can run pathSet('stats.hp',50,user) which is the same as - user.stats.hp = 50. This is useful because in our habitrpg-shared functions we're returning changesets as {path:value}, - so that different consumers can implement setters their own way. Derby needs model.set(path, value) for example, where - Angular sets object properties directly - in which case, this function will be used. - */ - dotSet: function(path, val) { - return api.dotSet(user, path, val); - }, - dotGet: function(path) { - return api.dotGet(user, path); - }, - randomDrop: function(modifiers, req) { - var acceptableDrops, base, base1, base2, chance, drop, dropK, dropMultiplier, name, name1, name2, quest, rarity, ref, ref1, ref2, ref3, 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 = api.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 - }); - user.party.quest.progress.collect[dropK]++; - if (typeof user.markModified === "function") { - 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)))) { - 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 = user.fns.randomVal(_.where(content.food, { - canDrop: true - })); - if ((base = user.items.food)[name = drop.key] == null) { - base[name] = 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) - }, req.language); - } else if (rarity > .3) { - drop = user.fns.randomVal(content.dropEggs); - if ((base1 = user.items.eggs)[name1 = drop.key] == null) { - base1[name1] = 0; - } - user.items.eggs[drop.key]++; - drop.type = 'Egg'; - drop.dialog = i18n.t('messageDropEgg', { - dropText: drop.text(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 = user.fns.randomVal(_.pick(content.hatchingPotions, (function(v, k) { - return indexOf.call(acceptableDrops, k) >= 0; - }))); - if ((base2 = user.items.hatchingPotions)[name2 = drop.key] == null) { - base2[name2] = 0; - } - user.items.hatchingPotions[drop.key]++; - drop.type = 'HatchingPotion'; - drop.dialog = i18n.t('messageDropPotion', { - dropText: drop.text(req.language), - dropNotes: drop.notes(req.language) - }, req.language); - } - user._tmp.drop = drop; - user.items.lastDrop.date = +(new Date); - return user.items.lastDrop.count++; - } - }, - - /* - 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 - */ - autoAllocate: function() { - return user.stats[(function() { - var diff, ideal, lvlDiv7, preference, stats, suggested; - switch (user.preferences.allocationMode) { - case "flat": - stats = _.pick(user.stats, $w('con str per int')); - return _.invert(stats)[_.min(stats)]; - case "classbased": - lvlDiv7 = user.stats.lvl / 7; - ideal = [lvlDiv7 * 3, lvlDiv7 * 2, lvlDiv7, lvlDiv7]; - preference = (function() { - switch (user.stats["class"]) { - case "wizard": - return ["int", "per", "con", "str"]; - case "rogue": - return ["per", "str", "int", "con"]; - case "healer": - return ["con", "int", "str", "per"]; - default: - return ["str", "con", "per", "int"]; - } - })(); - diff = [user.stats[preference[0]] - ideal[0], user.stats[preference[1]] - ideal[1], user.stats[preference[2]] - ideal[2], user.stats[preference[3]] - ideal[3]]; - suggested = _.findIndex(diff, (function(val) { - if (val === _.min(diff)) { - return true; - } - })); - if (~suggested) { - return preference[suggested]; - } else { - return "str"; - } - case "taskbased": - suggested = _.invert(user.stats.training)[_.max(user.stats.training)]; - _.merge(user.stats.training, { - str: 0, - int: 0, - con: 0, - per: 0 - }); - return suggested || "str"; - default: - return "str"; - } - })()]++; - }, - updateStats (stats, req, analytics) { - let allocatedStatPoints; - let totalStatPoints; - let experienceToNextLevel; - - if (stats.hp <= 0) { - user.stats.hp = 0; - return user.stats.hp; - } - - user.stats.hp = stats.hp; - user.stats.gp = stats.gp >= 0 ? stats.gp : 0; - - experienceToNextLevel = api.tnl(user.stats.lvl); - - if (stats.exp >= experienceToNextLevel) { - user.stats.exp = stats.exp; - - while (stats.exp >= experienceToNextLevel) { - stats.exp -= experienceToNextLevel; - user.stats.lvl++; - - experienceToNextLevel = api.tnl(user.stats.lvl); - user.stats.hp = MAX_HEALTH; - allocatedStatPoints = user.stats.str + user.stats.int + user.stats.con + user.stats.per; - totalStatPoints = allocatedStatPoints + user.stats.points; - - if (totalStatPoints >= MAX_STAT_POINTS) { - continue; // eslint-disable-line no-continue - } - if (user.preferences.automaticAllocation) { - user.fns.autoAllocate(); - } else { - user.stats.points = user.stats.lvl - allocatedStatPoints; - totalStatPoints = user.stats.points + allocatedStatPoints; - - if (totalStatPoints > MAX_STAT_POINTS) { - user.stats.points = MAX_STAT_POINTS - allocatedStatPoints; - } - - if (user.stats.points < 0) { - user.stats.points = 0; - } - } - } - } - - user.stats.exp = stats.exp; - user.flags = user.flags || {}; - - if (!user.flags.customizationsNotification && (user.stats.exp > 5 || user.stats.lvl > 1)) { - user.flags.customizationsNotification = true; - } - if (!user.flags.itemsEnabled && (user.stats.exp > 10 || user.stats.lvl > 1)) { - user.flags.itemsEnabled = true; - } - if (!user.flags.dropsEnabled && user.stats.lvl >= 3) { - user.flags.dropsEnabled = true; - if (user.items.eggs["Wolf"] > 0) { - user.items.eggs["Wolf"]++; - } else { - user.items.eggs["Wolf"] = 1; - } - } - if (!user.flags.classSelected && user.stats.lvl >= 10) { - user.flags.classSelected; - } - _.each({ - vice1: 30, - atom1: 15, - moonstone1: 60, - goldenknight1: 40 - }, function(lvl, k) { - var analyticsData, base, base1, ref; - if (!((ref = user.flags.levelDrops) != null ? ref[k] : void 0) && user.stats.lvl >= lvl) { - if ((base = user.items.quests)[k] == null) { - base[k] = 0; - } - user.items.quests[k]++; - ((base1 = user.flags).levelDrops != null ? base1.levelDrops : base1.levelDrops = {})[k] = true; - if (typeof user.markModified === "function") { - user.markModified('flags.levelDrops'); - } - analyticsData = { - uuid: user._id, - itemKey: k, - acquireMethod: 'Level Drop', - category: 'behavior' - }; - if (analytics != null) { - analytics.track('acquire item', analyticsData); - } - if (!user._tmp) user._tmp = {} - return user._tmp.drop = { - type: 'Quest', - key: k - }; - } - }); - if (!user.flags.rebirthEnabled && (user.stats.lvl >= 50 || user.achievements.beastMaster)) { - return user.flags.rebirthEnabled = true; - } - }, - - /* - ------------------------------------------------------ - Cron - ------------------------------------------------------ - */ - - /* - At end of day, add value to all incomplete Daily & Todo tasks (further incentive) - For incomplete Dailys, deduct experience - Make sure to run this function once in a while as server will not take care of overnight calculations. - And you have to run it every time client connects. - {user} - */ - cron: function(options) { - var _progress, analyticsData, base, base1, base2, base3, base4, clearBuffs, dailyChecked, dailyDueUnchecked, daysMissed, expTally, lvl, lvlDiv2, multiDaysCountAsOneDay, now, perfect, plan, progress, ref, ref1, ref2, ref3, todoTally; - if (options == null) { - options = {}; - } - now = +options.now || +(new Date); - daysMissed = daysSince(user.lastCron, _.defaults({ - now: now - }, user.preferences)); - if (!(daysMissed > 0)) { - return; - } - user.auth.timestamps.loggedin = new Date(); - user.lastCron = now; - if (user.items.lastDrop.count > 0) { - user.items.lastDrop.count = 0; - } - perfect = true; - clearBuffs = { - str: 0, - int: 0, - per: 0, - con: 0, - stealth: 0, - streaks: false - }; - plan = (ref = user.purchased) != null ? ref.plan : void 0; - if (plan != null ? plan.customerId : void 0) { - if (typeof plan.dateUpdated === "undefined") { - // partial compensation for bug in subscription creation - https://github.com/HabitRPG/habitrpg/issues/6682 - plan.dateUpdated = new Date(); - } - if (moment(plan.dateUpdated).format('MMYYYY') !== moment().format('MMYYYY')) { - plan.gemsBought = 0; - plan.dateUpdated = new Date(); - _.defaults(plan.consecutive, { - count: 0, - offset: 0, - trinkets: 0, - gemCapExtra: 0 - }); - plan.consecutive.count++; - if (plan.consecutive.offset > 0) { - plan.consecutive.offset--; - } else if (plan.consecutive.count % 3 === 0) { - plan.consecutive.trinkets++; - plan.consecutive.gemCapExtra += 5; - if (plan.consecutive.gemCapExtra > 25) { - plan.consecutive.gemCapExtra = 25; - } - } - } - if (plan.dateTerminated && moment(plan.dateTerminated).isBefore(+(new Date))) { - _.merge(plan, { - planId: null, - customerId: null, - paymentMethod: null - }); - _.merge(plan.consecutive, { - count: 0, - offset: 0, - gemCapExtra: 0 - }); - if (typeof user.markModified === "function") { - user.markModified('purchased.plan'); - } - } - } - if (user.preferences.sleep === true) { - user.stats.buffs = clearBuffs; - user.dailys.forEach(function(daily) { - var completed, repeat, thatDay; - completed = daily.completed, repeat = daily.repeat; - thatDay = moment(now).subtract({ - days: 1 - }); - if (shouldDo(thatDay.toDate(), daily, user.preferences) || completed) { - _.each(daily.checklist, (function(box) { - box.completed = false; - return true; - })); - } - return daily.completed = false; - }); - return; - } - multiDaysCountAsOneDay = true; - todoTally = 0; - user.todos.forEach(function(task) { - var absVal, completed, delta, id; - if (!task) { - return; - } - id = task.id, completed = task.completed; - delta = user.ops.score({ - params: { - id: task.id, - direction: 'down' - }, - query: { - times: multiDaysCountAsOneDay != null ? multiDaysCountAsOneDay : { - 1: daysMissed - }, - cron: true - } - }); - absVal = completed ? Math.abs(task.value) : task.value; - return todoTally += absVal; - }); - dailyChecked = 0; - dailyDueUnchecked = 0; - if ((base = user.party.quest.progress).down == null) { - base.down = 0; - } - user.dailys.forEach(function(task) { - var EvadeTask, completed, delta, fractionChecked, id, j, n, ref1, ref2, scheduleMisses, thatDay; - if (!task) { - return; - } - id = task.id, completed = task.completed; - EvadeTask = 0; - scheduleMisses = daysMissed; - if (completed) { - dailyChecked += 1; - } else { - scheduleMisses = 0; - for (n = j = 0, ref1 = daysMissed; 0 <= ref1 ? j < ref1 : j > ref1; n = 0 <= ref1 ? ++j : --j) { - thatDay = moment(now).subtract({ - days: n + 1 - }); - if (shouldDo(thatDay.toDate(), task, user.preferences)) { - scheduleMisses++; - if (user.stats.buffs.stealth) { - user.stats.buffs.stealth--; - EvadeTask++; - } - if (multiDaysCountAsOneDay) { - break; - } - } - } - if (scheduleMisses > EvadeTask) { - perfect = false; - if (((ref2 = task.checklist) != null ? ref2.length : void 0) > 0) { - fractionChecked = _.reduce(task.checklist, (function(m, i) { - return m + (i.completed ? 1 : 0); - }), 0) / task.checklist.length; - dailyDueUnchecked += 1 - fractionChecked; - dailyChecked += fractionChecked; - } else { - dailyDueUnchecked += 1; - } - delta = user.ops.score({ - params: { - id: task.id, - direction: 'down' - }, - query: { - times: multiDaysCountAsOneDay != null ? multiDaysCountAsOneDay : { - 1: scheduleMisses - EvadeTask - }, - cron: true - } - }); - user.party.quest.progress.down += delta * (task.priority < 1 ? task.priority : 1); - } - } - (task.history != null ? task.history : task.history = []).push({ - date: +(new Date), - value: task.value - }); - task.completed = false; - if (completed || (scheduleMisses > 0)) { - return _.each(task.checklist, (function(i) { - i.completed = false; - return true; - })); - } - }); - user.habits.forEach(function(task) { - if (task.up === false || task.down === false) { - if (Math.abs(task.value) < 0.1) { - return task.value = 0; - } else { - return task.value = task.value / 2; - } - } - }); - ((base1 = (user.history != null ? user.history : user.history = {})).todos != null ? base1.todos : base1.todos = []).push({ - date: now, - value: todoTally - }); - expTally = user.stats.exp; - lvl = 0; - while (lvl < (user.stats.lvl - 1)) { - lvl++; - expTally += api.tnl(lvl); - } - ((base2 = user.history).exp != null ? base2.exp : base2.exp = []).push({ - date: now, - value: expTally - }); - if (!((ref1 = user.purchased) != null ? (ref2 = ref1.plan) != null ? ref2.customerId : void 0 : void 0)) { - user.fns.preenUserHistory(); - if (typeof user.markModified === "function") { - user.markModified('history'); - } - if (typeof user.markModified === "function") { - user.markModified('dailys'); - } - } - user.stats.buffs = perfect ? ((base3 = user.achievements).perfect != null ? base3.perfect : base3.perfect = 0, user.achievements.perfect++, lvlDiv2 = Math.ceil(api.capByLevel(user.stats.lvl) / 2), { - str: lvlDiv2, - int: lvlDiv2, - per: lvlDiv2, - con: lvlDiv2, - stealth: 0, - streaks: false - }) : clearBuffs; - if (dailyDueUnchecked === 0 && dailyChecked === 0) { - dailyChecked = 1; - } - user.stats.mp += _.max([10, .1 * user._statsComputed.maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked); - if (user.stats.mp > user._statsComputed.maxMP) { - user.stats.mp = user._statsComputed.maxMP; - } - progress = user.party.quest.progress; - _progress = _.cloneDeep(progress); - _.merge(progress, { - down: 0, - up: 0 - }); - progress.collect = _.transform(progress.collect, (function(m, v, k) { - return m[k] = 0; - })); - if ((base4 = user.flags).cronCount == null) { - base4.cronCount = 0; - } - user.flags.cronCount++; - analyticsData = { - category: 'behavior', - gaLabel: 'Cron Count', - gaValue: user.flags.cronCount, - uuid: user._id, - user: user, - resting: user.preferences.sleep, - cronCount: user.flags.cronCount, - progressUp: _.min([_progress.up, 900]), - progressDown: _progress.down - }; - if ((ref3 = options.analytics) != null) { - ref3.track('Cron', analyticsData); - } - return _progress; - }, - preenUserHistory: function(minHistLen) { - if (minHistLen == null) { - minHistLen = 7; - } - _.each(user.habits.concat(user.dailys), function(task) { - var ref; - if (((ref = task.history) != null ? ref.length : void 0) > minHistLen) { - task.history = preenHistory(task.history); - } - return true; - }); - _.defaults(user.history, { - todos: [], - exp: [] - }); - if (user.history.exp.length > minHistLen) { - user.history.exp = preenHistory(user.history.exp); - } - if (user.history.todos.length > minHistLen) { - return user.history.todos = preenHistory(user.history.todos); - } - }, - ultimateGear: function() { - var base, owned; - owned = typeof window !== "undefined" && window !== null ? user.items.gear.owned : user.items.gear.owned.toObject(); - if ((base = user.achievements).ultimateGearSets == null) { - base.ultimateGearSets = { - healer: false, - wizard: false, - rogue: false, - warrior: false - }; - } - content.classes.forEach(function(klass) { - if (user.achievements.ultimateGearSets[klass] !== true) { - return user.achievements.ultimateGearSets[klass] = _.reduce(['armor', 'shield', 'head', 'weapon'], function(soFarGood, type) { - var found; - found = _.find(content.gear.tree[type][klass], { - last: true - }); - return soFarGood && (!found || owned[found.key] === true); - }, true); - } - }); - if (typeof user.markModified === "function") { - user.markModified('achievements.ultimateGearSets'); - } - if (_.contains(user.achievements.ultimateGearSets, true) && user.flags.armoireEnabled !== true) { - user.flags.armoireEnabled = true; - return typeof user.markModified === "function" ? user.markModified('flags') : void 0; - } - }, - nullify: function() { - user.ops = null; - user.fns = null; - return user = null; - } + getItem: _.partial(importedFns.getItem, user), + handleTwoHanded: _.partial(importedFns.handleTwoHanded, user), + predictableRandom: _.partial(importedFns.predictableRandom, user), + crit: _.partial(importedFns.crit, user), + randomVal: _.partial(importedFns.randomVal, user), + dotSet: _.partial(importedFns.dotSet, user), + dotGet: _.partial(importedFns.dotGet, user), + randomDrop: _.partial(importedFns.randomDrop, user), + autoAllocate: _.partial(importedFns.autoAllocate, user), + updateStats: _.partial(importedFns.updateStats, user), + cron: _.partial(importedFns.cron, user), + preenUserHistory: _.partial(importedFns.preenUserHistory, user), + ultimateGear: _.partial(importedFns.ultimateGear, user), + nullify: _.partial(importedFns.nullify, user), }; Object.defineProperty(user, '_statsComputed', { get: function() { diff --git a/common/script/libs/dotGet.js b/common/script/libs/dotGet.js new file mode 100644 index 0000000000..4585d8fd53 --- /dev/null +++ b/common/script/libs/dotGet.js @@ -0,0 +1,9 @@ +import _ from 'lodash'; + +module.exports = function(obj, path) { + return _.reduce(path.split('.'), ((function(_this) { + return function(curr, next) { + return curr != null ? curr[next] : void 0; + }; + })(this)), obj); +}; diff --git a/common/script/libs/dotSet.js b/common/script/libs/dotSet.js new file mode 100644 index 0000000000..40165078aa --- /dev/null +++ b/common/script/libs/dotSet.js @@ -0,0 +1,14 @@ +import _ from 'lodash'; + +module.exports = function(obj, path, val) { + var arr; + arr = path.split('.'); + return _.reduce(arr, (function(_this) { + return function(curr, next, index) { + if ((arr.length - 1) === index) { + curr[next] = val; + } + return curr[next] != null ? curr[next] : curr[next] = {}; + }; + })(this), obj); +}; diff --git a/common/script/libs/index.js b/common/script/libs/index.js index 2842fa9ec9..6a293aa494 100644 --- a/common/script/libs/index.js +++ b/common/script/libs/index.js @@ -4,6 +4,9 @@ import refPush from './refPush'; import splitWhitespace from './splitWhitespace'; import planGemLimits from './planGemLimits'; import preenTodos from './preenTodos'; +import dotSet from './dotSet'; +import dotGet from './dotGet'; +import preenHistory from './preenHistory'; module.exports = { uuid, @@ -12,4 +15,7 @@ module.exports = { splitWhitespace, planGemLimits, preenTodos, + dotSet, + dotGet, + preenHistory, }; diff --git a/common/script/libs/preenHistory.js b/common/script/libs/preenHistory.js new file mode 100644 index 0000000000..0d354c238c --- /dev/null +++ b/common/script/libs/preenHistory.js @@ -0,0 +1,43 @@ +import moment from 'moment'; +import _ from 'lodash'; + +/* +Preen history for users with > 7 history entries +This takes an infinite array of single day entries [day day day day day...], and turns it into a condensed array +of averages, condensing more the further back in time we go. Eg, 7 entries each for last 7 days; 1 entry each week +of this month; 1 entry for each month of this year; 1 entry per previous year: [day*7 week*4 month*12 year*infinite] + */ + +module.exports = function(history) { + var newHistory, preen, thisMonth; + history = _.filter(history, function(h) { + return !!h; + }); + newHistory = []; + preen = function(amount, groupBy) { + var groups; + groups = _.chain(history).groupBy(function(h) { + return moment(h.date).format(groupBy); + }).sortBy(function(h, k) { + return k; + }).value(); + groups = groups.slice(-amount); + groups.pop(); + return _.each(groups, function(group) { + newHistory.push({ + date: moment(group[0].date).toDate(), + value: _.reduce(group, (function(m, obj) { + return m + obj.value; + }), 0) / group.length + }); + return true; + }); + }; + preen(50, "YYYY"); + preen(moment().format('MM'), "YYYYMM"); + thisMonth = moment().format('YYYYMM'); + newHistory = newHistory.concat(_.filter(history, function(h) { + return moment(h.date).format('YYYYMM') === thisMonth; + })); + return newHistory; +}; diff --git a/tasks/gulp-eslint.js b/tasks/gulp-eslint.js index e093e869ea..735845beb7 100644 --- a/tasks/gulp-eslint.js +++ b/tasks/gulp-eslint.js @@ -12,6 +12,9 @@ const COMMON_FILES = [ // @TODO remove these negations as the files are converted over. '!./common/script/index.js', '!./common/script/content/index.js', + '!./common/script/ops/**/*.js', + '!./common/script/fns/**/*.js', + '!./common/script/libs/**/*.js', '!./common/script/public/**/*.js', ]; const TEST_FILES = [