split user.fns

This commit is contained in:
Matteo Pagliazzi
2016-03-08 18:58:39 +01:00
parent 393a26e51a
commit bb6f0f4252
21 changed files with 808 additions and 704 deletions

View File

@@ -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";
}
})()]++;
};

15
common/script/fns/crit.js Normal file
View File

@@ -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;
}
};

279
common/script/fns/cron.js Normal file
View File

@@ -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;
};

View File

@@ -0,0 +1,5 @@
import dotGet from '../libs/dotGet';
module.exports = function(user, path) {
return dotGet(user, path);
};

View File

@@ -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);
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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,
};

View File

@@ -0,0 +1,5 @@
module.exports = function(user) {
user.ops = null;
user.fns = null;
return user = null;
};

View File

@@ -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);
};

View File

@@ -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);
}
};

View File

@@ -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++;
}
};

View File

@@ -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)];
};

View File

@@ -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;
}
};

View File

@@ -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;
}
};

View File

@@ -32,73 +32,12 @@ api.tnl = statHelpers.toNextLevel;
api.diminishingReturns = statHelpers.diminishingReturns; api.diminishingReturns = statHelpers.diminishingReturns;
$w = api.$w = importedLibs.splitWhitespace; $w = api.$w = importedLibs.splitWhitespace;
api.dotSet = importedLibs.dotSet;
api.dotSet = function(obj, path, val) { api.dotGet = importedLibs.dotGet;
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.refPush = importedLibs.refPush; api.refPush = importedLibs.refPush;
api.planGemLimits = importedLibs.planGemLimits; api.planGemLimits = importedLibs.planGemLimits;
/* preenHistory = importedLibs.preenHistory;
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;
};
/* /*
Preen 3-day past-completed To-Dos from Angular & mobile app Preen 3-day past-completed To-Dos from Angular & mobile app
@@ -414,6 +353,7 @@ TODO
*/ */
import importedOps from './ops'; import importedOps from './ops';
import importedFns from './fns';
api.wrap = function(user, main) { api.wrap = function(user, main) {
if (main == null) { if (main == null) {
@@ -431,7 +371,7 @@ api.wrap = function(user, main) {
reset: _.partial(importedOps.reset, user), reset: _.partial(importedOps.reset, user),
reroll: _.partial(importedOps.reroll, user), reroll: _.partial(importedOps.reroll, user),
rebirth: _.partial(importedOps.rebirth, user), rebirth: _.partial(importedOps.rebirth, user),
allocateNow: _.partial(importedOps.reroll, user), allocateNow: _.partial(importedOps.allocateNow, user),
clearCompleted: _.partial(importedOps.clearCompleted, user), clearCompleted: _.partial(importedOps.clearCompleted, user),
sortTask: _.partial(importedOps.sortTask, user), sortTask: _.partial(importedOps.sortTask, user),
updateTask: _.partial(importedOps.updateTask, user), updateTask: _.partial(importedOps.updateTask, user),
@@ -473,645 +413,20 @@ api.wrap = function(user, main) {
}; };
} }
user.fns = { user.fns = {
getItem: function(type) { getItem: _.partial(importedFns.getItem, user),
var item; handleTwoHanded: _.partial(importedFns.handleTwoHanded, user),
item = content.gear.flat[user.items.gear.equipped[type]]; predictableRandom: _.partial(importedFns.predictableRandom, user),
if (!item) { crit: _.partial(importedFns.crit, user),
return content.gear.flat[type + "_base_0"]; randomVal: _.partial(importedFns.randomVal, user),
} dotSet: _.partial(importedFns.dotSet, user),
return item; dotGet: _.partial(importedFns.dotGet, user),
}, randomDrop: _.partial(importedFns.randomDrop, user),
handleTwoHanded: function(item, type, req) { autoAllocate: _.partial(importedFns.autoAllocate, user),
var message, currentWeapon, currentShield; updateStats: _.partial(importedFns.updateStats, user),
if (type == null) { cron: _.partial(importedFns.cron, user),
type = 'equipped'; preenUserHistory: _.partial(importedFns.preenUserHistory, user),
} ultimateGear: _.partial(importedFns.ultimateGear, user),
currentShield = content.gear.flat[user.items.gear[type].shield]; nullify: _.partial(importedFns.nullify, user),
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;
}
}; };
Object.defineProperty(user, '_statsComputed', { Object.defineProperty(user, '_statsComputed', {
get: function() { get: function() {

View File

@@ -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);
};

View File

@@ -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);
};

View File

@@ -4,6 +4,9 @@ import refPush from './refPush';
import splitWhitespace from './splitWhitespace'; import splitWhitespace from './splitWhitespace';
import planGemLimits from './planGemLimits'; import planGemLimits from './planGemLimits';
import preenTodos from './preenTodos'; import preenTodos from './preenTodos';
import dotSet from './dotSet';
import dotGet from './dotGet';
import preenHistory from './preenHistory';
module.exports = { module.exports = {
uuid, uuid,
@@ -12,4 +15,7 @@ module.exports = {
splitWhitespace, splitWhitespace,
planGemLimits, planGemLimits,
preenTodos, preenTodos,
dotSet,
dotGet,
preenHistory,
}; };

View File

@@ -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;
};

View File

@@ -12,6 +12,9 @@ const COMMON_FILES = [
// @TODO remove these negations as the files are converted over. // @TODO remove these negations as the files are converted over.
'!./common/script/index.js', '!./common/script/index.js',
'!./common/script/content/index.js', '!./common/script/content/index.js',
'!./common/script/ops/**/*.js',
'!./common/script/fns/**/*.js',
'!./common/script/libs/**/*.js',
'!./common/script/public/**/*.js', '!./common/script/public/**/*.js',
]; ];
const TEST_FILES = [ const TEST_FILES = [