Files
habitica/common/script/index.js
2015-12-15 18:48:47 -06:00

2705 lines
93 KiB
JavaScript

import {
daysSince,
shouldDo,
} from '../../common/script/cron';
import * as statHelpers from './statHelpers';
var $w, _, api, content, i18n, moment, preenHistory, sortOrder,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
moment = require('moment');
_ = require('lodash');
content = require('./content/index');
i18n = require('./i18n');
api = module.exports = {};
api.i18n = i18n;
api.shouldDo = shouldDo;
api.maxLevel = statHelpers.MAX_LEVEL;
api.maxStatPoints = statHelpers.MAX_STAT_POINTS;
api.capByLevel = statHelpers.capByLevel;
api.maxHealth = statHelpers.MAX_HEALTH;
api.tnl = statHelpers.toNextLevel;
api.diminishingReturns = statHelpers.diminishingReturns;
$w = api.$w = function(s) {
return s.split(' ');
};
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);
};
/*
Reflists are arrays, but stored as objects. Mongoose has a helluvatime working with arrays (the main problem for our
syncing issues) - so the goal is to move away from arrays to objects, since mongoose can reference elements by ID
no problem. To maintain sorting, we use these helper functions:
*/
api.refPush = function(reflist, item, prune) {
if (prune == null) {
prune = 0;
}
item.sort = _.isEmpty(reflist) ? 0 : _.max(reflist, 'sort').sort + 1;
if (!(item.id && !reflist[item.id])) {
item.id = api.uuid();
}
return reflist[item.id] = item;
};
api.planGemLimits = {
convRate: 20,
convCap: 25
};
/*
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
*/
api.preenTodos = function(tasks) {
return _.where(tasks, function(t) {
return !t.completed || (t.challenge && t.challenge.id) || moment(t.dateCompleted).isAfter(moment().subtract({
days: 3
}));
});
};
/*
Update the in-browser store with new gear. FIXME this was in user.fns, but it was causing strange issues there
*/
sortOrder = _.reduce(content.gearTypes, (function(m, v, k) {
m[v] = k;
return m;
}), {});
api.updateStore = function(user) {
var changes;
if (!user) {
return;
}
changes = [];
_.each(content.gearTypes, function(type) {
var found;
found = _.find(content.gear.tree[type][user.stats["class"]], function(item) {
return !user.items.gear.owned[item.key];
});
if (found) {
changes.push(found);
}
return true;
});
changes = changes.concat(_.filter(content.gear.flat, function(v) {
var ref;
return ((ref = v.klass) === 'special' || ref === 'mystery' || ref === 'armoire') && !user.items.gear.owned[v.key] && (typeof v.canOwn === "function" ? v.canOwn(user) : void 0);
}));
return _.sortBy(changes, function(c) {
return sortOrder[c.type];
});
};
/*
------------------------------------------------------
Content
------------------------------------------------------
*/
api.content = content;
/*
------------------------------------------------------
Misc Helpers
------------------------------------------------------
*/
api.uuid = function() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
var r, v;
r = Math.random() * 16 | 0;
v = (c === "x" ? r : r & 0x3 | 0x8);
return v.toString(16);
});
};
api.countExists = function(items) {
return _.reduce(items, (function(m, v) {
return m + (v ? 1 : 0);
}), 0);
};
/*
Even though Mongoose handles task defaults, we want to make sure defaults are set on the client-side before
sending up to the server for performance
*/
api.taskDefaults = function(task) {
var defaults, ref, ref1, ref2;
if (task == null) {
task = {};
}
if (!(task.type && ((ref = task.type) === 'habit' || ref === 'daily' || ref === 'todo' || ref === 'reward'))) {
task.type = 'habit';
}
defaults = {
id: api.uuid(),
text: task.id != null ? task.id : '',
notes: '',
priority: 1,
challenge: {},
attribute: 'str',
dateCreated: new Date()
};
_.defaults(task, defaults);
if (task.type === 'habit') {
_.defaults(task, {
up: true,
down: true
});
}
if ((ref1 = task.type) === 'habit' || ref1 === 'daily') {
_.defaults(task, {
history: []
});
}
if ((ref2 = task.type) === 'daily' || ref2 === 'todo') {
_.defaults(task, {
completed: false
});
}
if (task.type === 'daily') {
_.defaults(task, {
streak: 0,
repeat: {
su: true,
m: true,
t: true,
w: true,
th: true,
f: true,
s: true
}
}, {
startDate: new Date(),
everyX: 1,
frequency: 'weekly'
});
}
task._id = task.id;
if (task.value == null) {
task.value = task.type === 'reward' ? 10 : 0;
}
if (!_.isNumber(task.priority)) {
task.priority = 1;
}
return task;
};
api.percent = function(x, y, dir) {
var roundFn;
switch (dir) {
case "up":
roundFn = Math.ceil;
break;
case "down":
roundFn = Math.floor;
break;
default:
roundFn = Math.round;
}
if (x === 0) {
x = 1;
}
return Math.max(0, roundFn(x / y * 100));
};
/*
Remove whitespace #FIXME are we using this anywwhere? Should we be?
*/
api.removeWhitespace = function(str) {
if (!str) {
return '';
}
return str.replace(/\s/g, '');
};
/*
Encode the download link for .ics iCal file
*/
api.encodeiCalLink = function(uid, apiToken) {
var loc, ref;
loc = (typeof window !== "undefined" && window !== null ? window.location.host : void 0) || (typeof process !== "undefined" && process !== null ? (ref = process.env) != null ? ref.BASE_URL : void 0 : void 0) || '';
return encodeURIComponent("http://" + loc + "/v1/users/" + uid + "/calendar.ics?apiToken=" + apiToken);
};
/*
Gold amount from their money
*/
api.gold = function(num) {
if (num) {
return Math.floor(num);
} else {
return "0";
}
};
/*
Silver amount from their money
*/
api.silver = function(num) {
if (num) {
return ("0" + Math.floor((num - Math.floor(num)) * 100)).slice(-2);
} else {
return "00";
}
};
/*
Task classes given everything about the class
*/
api.taskClasses = function(task, filters, dayStart, lastCron, showCompleted, main) {
var classes, completed, enabled, filter, priority, ref, repeat, type, value;
if (filters == null) {
filters = [];
}
if (dayStart == null) {
dayStart = 0;
}
if (lastCron == null) {
lastCron = +(new Date);
}
if (showCompleted == null) {
showCompleted = false;
}
if (main == null) {
main = false;
}
if (!task) {
return;
}
type = task.type, completed = task.completed, value = task.value, repeat = task.repeat, priority = task.priority;
if (main) {
if (!task._editing) {
for (filter in filters) {
enabled = filters[filter];
if (enabled && !((ref = task.tags) != null ? ref[filter] : void 0)) {
return 'hidden';
}
}
}
}
classes = type;
if (task._editing) {
classes += " beingEdited";
}
if (type === 'todo' || type === 'daily') {
if (completed || (type === 'daily' && !shouldDo(+(new Date), task, {
dayStart: dayStart
}))) {
classes += " completed";
} else {
classes += " uncompleted";
}
} else if (type === 'habit') {
if (task.down && task.up) {
classes += ' habit-wide';
}
if (!task.down && !task.up) {
classes += ' habit-narrow';
}
}
if (priority === 0.1) {
classes += ' difficulty-trivial';
} else if (priority === 1) {
classes += ' difficulty-easy';
} else if (priority === 1.5) {
classes += ' difficulty-medium';
} else if (priority === 2) {
classes += ' difficulty-hard';
}
if (value < -20) {
classes += ' color-worst';
} else if (value < -10) {
classes += ' color-worse';
} else if (value < -1) {
classes += ' color-bad';
} else if (value < 1) {
classes += ' color-neutral';
} else if (value < 5) {
classes += ' color-good';
} else if (value < 10) {
classes += ' color-better';
} else {
classes += ' color-best';
}
return classes;
};
/*
Friendly timestamp
*/
api.friendlyTimestamp = function(timestamp) {
return moment(timestamp).format('MM/DD h:mm:ss a');
};
/*
Does user have new chat messages?
*/
api.newChatMessages = function(messages, lastMessageSeen) {
if (!((messages != null ? messages.length : void 0) > 0)) {
return false;
}
return (messages != null ? messages[0] : void 0) && (messages[0].id !== lastMessageSeen);
};
/*
are any tags active?
*/
api.noTags = function(tags) {
return _.isEmpty(tags) || _.isEmpty(_.filter(tags, function(t) {
return t;
}));
};
/*
Are there tags applied?
*/
api.appliedTags = function(userTags, taskTags) {
var arr;
arr = [];
_.each(userTags, function(t) {
if (t == null) {
return;
}
if (taskTags != null ? taskTags[t.id] : void 0) {
return arr.push(t.name);
}
});
return arr.join(', ');
};
/*
Various counting functions
*/
api.count = require('./count');
/*
------------------------------------------------------
User (prototype wrapper to give it ops, helper funcs, and virtuals
------------------------------------------------------
*/
/*
User is now wrapped (both on client and server), adding a few new properties:
* getters (_statsComputed, tasks, etc)
* user.fns, which is a bunch of helper functions
These were originally up above, but they make more sense belonging to the user object so we don't have to pass
the user object all over the place. In fact, we should pull in more functions such as cron(), updateStats(), etc.
* user.ops, which is super important:
If a function is inside user.ops, it has magical properties. If you call it on the client it updates the user object in
the browser and when it's done it automatically POSTs to the server, calling src/controllers/user.js#OP_NAME (the exact same name
of the op function). The first argument req is {query, body, params}, it's what the express controller function
expects. This means we call our functions as if we were calling an Express route. Eg, instead of score(task, direction),
we call score({params:{id:task.id,direction:direction}}). This also forces us to think about our routes (whether to use
params, query, or body for variables). see http://stackoverflow.com/questions/4024271/rest-api-best-practices-where-to-put-parameters
If `src/controllers/user.js#OP_NAME` doesn't exist on the server, it's automatically added. It runs the code in user.ops.OP_NAME
to update the user model server-side, then performs `user.save()`. You can see this in action for `user.ops.buy`. That
function doesn't exist on the server - so the client calls it, it updates user in the browser, auto-POSTs to server, server
handles it by calling `user.ops.buy` again (to update user on the server), and then saves. We can do this for
everything that doesn't need any code difference from what's in user.ops.OP_NAME for special-handling server-side. If we
*do* need special handling, just add `src/controllers/user.js#OP_NAME` to override the user.ops.OP_NAME, and be
sure to call user.ops.OP_NAME at some point within the overridden function.
TODO
* Is this the best way to wrap the user object? I thought of using user.prototype, but user is an object not a Function.
user on the server is a Mongoose model, so we can use prototype - but to do it on the client, we'd probably have to
move to $resource for user
* Move to $resource!
*/
api.wrap = function(user, main) {
if (main == null) {
main = true;
}
if (user._wrapped) {
return;
}
user._wrapped = true;
if (main) {
user.ops = {
update: function(req, cb) {
_.each(req.body, function(v, k) {
user.fns.dotSet(k, v);
return true;
});
return typeof cb === "function" ? cb(null, user) : void 0;
},
sleep: function(req, cb) {
user.preferences.sleep = !user.preferences.sleep;
return typeof cb === "function" ? cb(null, {}) : void 0;
},
revive: function(req, cb, analytics) {
var analyticsData, base, cl, gearOwned, item, losableItems, lostItem, lostStat;
if (!(user.stats.hp <= 0)) {
return typeof cb === "function" ? cb({
code: 400,
message: "Cannot revive if not dead"
}) : void 0;
}
_.merge(user.stats, {
hp: 50,
exp: 0,
gp: 0
});
if (user.stats.lvl > 1) {
user.stats.lvl--;
}
lostStat = user.fns.randomVal(_.reduce(['str', 'con', 'per', 'int'], (function(m, k) {
if (user.stats[k]) {
m[k] = k;
}
return m;
}), {}));
if (lostStat) {
user.stats[lostStat]--;
}
cl = user.stats["class"];
gearOwned = (typeof (base = user.items.gear.owned).toObject === "function" ? base.toObject() : void 0) || user.items.gear.owned;
losableItems = {};
_.each(gearOwned, function(v, k) {
var itm;
if (v) {
itm = content.gear.flat['' + k];
if (itm) {
if ((itm.value > 0 || k === 'weapon_warrior_0') && (itm.klass === cl || (itm.klass === 'special' && (!itm.specialClass || itm.specialClass === cl)) || itm.klass === 'armoire')) {
return losableItems['' + k] = '' + k;
}
}
}
});
lostItem = user.fns.randomVal(losableItems);
if (item = content.gear.flat[lostItem]) {
user.items.gear.owned[lostItem] = false;
if (user.items.gear.equipped[item.type] === lostItem) {
user.items.gear.equipped[item.type] = item.type + "_base_0";
}
if (user.items.gear.costume[item.type] === lostItem) {
user.items.gear.costume[item.type] = item.type + "_base_0";
}
}
if (typeof user.markModified === "function") {
user.markModified('items.gear');
}
analyticsData = {
uuid: user._id,
lostItem: lostItem,
gaLabel: lostItem,
category: 'behavior'
};
if (analytics != null) {
analytics.track('Death', analyticsData);
}
return typeof cb === "function" ? cb((item ? {
code: 200,
message: i18n.t('messageLostItem', {
itemText: item.text(req.language)
}, req.language)
} : null), user) : void 0;
},
reset: function(req, cb) {
var gear;
user.habits = [];
user.dailys = [];
user.todos = [];
user.rewards = [];
user.stats.hp = 50;
user.stats.lvl = 1;
user.stats.gp = 0;
user.stats.exp = 0;
gear = user.items.gear;
_.each(['equipped', 'costume'], function(type) {
gear[type].armor = 'armor_base_0';
gear[type].weapon = 'weapon_base_0';
gear[type].head = 'head_base_0';
return gear[type].shield = 'shield_base_0';
});
if (typeof gear.owned === 'undefined') {
gear.owned = {};
}
_.each(gear.owned, function(v, k) {
if (gear.owned[k]) {
gear.owned[k] = false;
}
return true;
});
gear.owned.weapon_warrior_0 = true;
if (typeof user.markModified === "function") {
user.markModified('items.gear.owned');
}
user.preferences.costume = false;
return typeof cb === "function" ? cb(null, user) : void 0;
},
reroll: function(req, cb, analytics) {
var analyticsData;
if (user.balance < 1) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('notEnoughGems', req.language)
}) : void 0;
}
user.balance--;
_.each(user.tasks, function(task) {
if (task.type !== 'reward') {
return task.value = 0;
}
});
user.stats.hp = 50;
analyticsData = {
uuid: user._id,
acquireMethod: 'Gems',
gemCost: 4,
category: 'behavior'
};
if (analytics != null) {
analytics.track('Fortify Potion', analyticsData);
}
return typeof cb === "function" ? cb(null, user) : void 0;
},
rebirth: function(req, cb, analytics) {
var analyticsData, flags, gear, lvl, stats;
if (user.balance < 2 && user.stats.lvl < api.maxLevel) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('notEnoughGems', req.language)
}) : void 0;
}
analyticsData = {
uuid: user._id,
category: 'behavior'
};
if (user.stats.lvl < api.maxLevel) {
user.balance -= 2;
analyticsData.acquireMethod = 'Gems';
analyticsData.gemCost = 8;
} else {
analyticsData.gemCost = 0;
analyticsData.acquireMethod = '> 100';
}
if (analytics != null) {
analytics.track('Rebirth', analyticsData);
}
lvl = api.capByLevel(user.stats.lvl);
_.each(user.tasks, function(task) {
if (task.type !== 'reward') {
task.value = 0;
}
if (task.type === 'daily') {
return task.streak = 0;
}
});
stats = user.stats;
stats.buffs = {};
stats.hp = 50;
stats.lvl = 1;
stats["class"] = 'warrior';
_.each(['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp'], function(value) {
return stats[value] = 0;
});
// TODO during refactoring: move all gear code from rebirth() to its own function and then call it in reset() as well
gear = user.items.gear;
_.each(['equipped', 'costume'], function(type) {
gear[type] = {};
gear[type].armor = 'armor_base_0';
gear[type].weapon = 'weapon_warrior_0';
gear[type].head = 'head_base_0';
return gear[type].shield = 'shield_base_0';
});
if (user.items.currentPet) {
user.ops.equip({
params: {
type: 'pet',
key: user.items.currentPet
}
});
}
if (user.items.currentMount) {
user.ops.equip({
params: {
type: 'mount',
key: user.items.currentMount
}
});
}
_.each(gear.owned, function(v, k) {
if (gear.owned[k] && content.gear.flat[k].value) {
gear.owned[k] = false;
return true;
}
});
gear.owned.weapon_warrior_0 = true;
if (typeof user.markModified === "function") {
user.markModified('items.gear.owned');
}
user.preferences.costume = false;
flags = user.flags;
if (!user.achievements.beastMaster) {
flags.rebirthEnabled = false;
}
flags.itemsEnabled = false;
flags.dropsEnabled = false;
flags.classSelected = false;
flags.levelDrops = {};
if (!user.achievements.rebirths) {
user.achievements.rebirths = 1;
user.achievements.rebirthLevel = lvl;
} else if (lvl > user.achievements.rebirthLevel || lvl === 100) {
user.achievements.rebirths++;
user.achievements.rebirthLevel = lvl;
}
user.stats.buffs = {};
return typeof cb === "function" ? cb(null, user) : void 0;
},
allocateNow: function(req, cb) {
_.times(user.stats.points, user.fns.autoAllocate);
user.stats.points = 0;
if (typeof user.markModified === "function") {
user.markModified('stats');
}
return typeof cb === "function" ? cb(null, user.stats) : void 0;
},
clearCompleted: function(req, cb) {
_.remove(user.todos, function(t) {
var ref;
return t.completed && !((ref = t.challenge) != null ? ref.id : void 0);
});
if (typeof user.markModified === "function") {
user.markModified('todos');
}
return typeof cb === "function" ? cb(null, user.todos) : void 0;
},
sortTask: function(req, cb) {
var from, id, movedTask, preenedTasks, ref, task, tasks, to;
id = req.params.id;
ref = req.query, to = ref.to, from = ref.from;
task = user.tasks[id];
if (!task) {
return typeof cb === "function" ? cb({
code: 404,
message: i18n.t('messageTaskNotFound', req.language)
}) : void 0;
}
if (!((to != null) && (from != null))) {
return typeof cb === "function" ? cb('?to=__&from=__ are required') : void 0;
}
tasks = user[task.type + "s"];
if (task.type === 'todo' && tasks[from] !== task) {
preenedTasks = api.preenTodos(tasks);
if (to !== -1) {
to = tasks.indexOf(preenedTasks[to]);
}
from = tasks.indexOf(preenedTasks[from]);
}
if (tasks[from] !== task) {
return typeof cb === "function" ? cb({
code: 404,
message: i18n.t('messageTaskNotFound', req.language)
}) : void 0;
}
movedTask = tasks.splice(from, 1)[0];
if (to === -1) {
tasks.push(movedTask);
} else {
tasks.splice(to, 0, movedTask);
}
return typeof cb === "function" ? cb(null, tasks) : void 0;
},
updateTask: function(req, cb) {
var ref, task;
if (!(task = user.tasks[(ref = req.params) != null ? ref.id : void 0])) {
return typeof cb === "function" ? cb({
code: 404,
message: i18n.t('messageTaskNotFound', req.language)
}) : void 0;
}
_.merge(task, _.omit(req.body, ['checklist', 'id', 'type']));
if (req.body.checklist) {
task.checklist = req.body.checklist;
}
if (typeof task.markModified === "function") {
task.markModified('tags');
}
return typeof cb === "function" ? cb(null, task) : void 0;
},
deleteTask: function(req, cb) {
var i, ref, task;
task = user.tasks[(ref = req.params) != null ? ref.id : void 0];
if (!task) {
return typeof cb === "function" ? cb({
code: 404,
message: i18n.t('messageTaskNotFound', req.language)
}) : void 0;
}
i = user[task.type + "s"].indexOf(task);
if (~i) {
user[task.type + "s"].splice(i, 1);
}
return typeof cb === "function" ? cb(null, {}) : void 0;
},
addTask: function(req, cb) {
var task;
task = api.taskDefaults(req.body);
if (user.tasks[task.id] != null) {
return typeof cb === "function" ? cb({
code: 409,
message: i18n.t('messageDuplicateTaskID', req.language)
}) : void 0;
}
user[task.type + "s"].unshift(task);
if (user.preferences.newTaskEdit) {
task._editing = true;
}
if (user.preferences.tagsCollapsed) {
task._tags = true;
}
if (!user.preferences.advancedCollapsed) {
task._advanced = true;
}
if (typeof cb === "function") {
cb(null, task);
}
return task;
},
addTag: function(req, cb) {
if (user.tags == null) {
user.tags = [];
}
user.tags.push({
name: req.body.name,
id: req.body.id || api.uuid()
});
return typeof cb === "function" ? cb(null, user.tags) : void 0;
},
sortTag: function(req, cb) {
var from, ref, to;
ref = req.query, to = ref.to, from = ref.from;
if (!((to != null) && (from != null))) {
return typeof cb === "function" ? cb('?to=__&from=__ are required') : void 0;
}
user.tags.splice(to, 0, user.tags.splice(from, 1)[0]);
return typeof cb === "function" ? cb(null, user.tags) : void 0;
},
getTags: function(req, cb) {
return typeof cb === "function" ? cb(null, user.tags) : void 0;
},
getTag: function(req, cb) {
var i, tid;
tid = req.params.id;
i = _.findIndex(user.tags, {
id: tid
});
if (!~i) {
return typeof cb === "function" ? cb({
code: 404,
message: i18n.t('messageTagNotFound', req.language)
}) : void 0;
}
return typeof cb === "function" ? cb(null, user.tags[i]) : void 0;
},
updateTag: function(req, cb) {
var i, tid;
tid = req.params.id;
i = _.findIndex(user.tags, {
id: tid
});
if (!~i) {
return typeof cb === "function" ? cb({
code: 404,
message: i18n.t('messageTagNotFound', req.language)
}) : void 0;
}
user.tags[i].name = req.body.name;
return typeof cb === "function" ? cb(null, user.tags[i]) : void 0;
},
deleteTag: function(req, cb) {
var i, tag, tid;
tid = req.params.id;
i = _.findIndex(user.tags, {
id: tid
});
if (!~i) {
return typeof cb === "function" ? cb({
code: 404,
message: i18n.t('messageTagNotFound', req.language)
}) : void 0;
}
tag = user.tags[i];
delete user.filters[tag.id];
user.tags.splice(i, 1);
_.each(user.tasks, function(task) {
return delete task.tags[tag.id];
});
_.each(['habits', 'dailys', 'todos', 'rewards'], function(type) {
return typeof user.markModified === "function" ? user.markModified(type) : void 0;
});
return typeof cb === "function" ? cb(null, user.tags) : void 0;
},
addWebhook: function(req, cb) {
var wh;
wh = user.preferences.webhooks;
api.refPush(wh, {
url: req.body.url,
enabled: req.body.enabled || true,
id: req.body.id
});
if (typeof user.markModified === "function") {
user.markModified('preferences.webhooks');
}
return typeof cb === "function" ? cb(null, user.preferences.webhooks) : void 0;
},
updateWebhook: function(req, cb) {
_.merge(user.preferences.webhooks[req.params.id], req.body);
if (typeof user.markModified === "function") {
user.markModified('preferences.webhooks');
}
return typeof cb === "function" ? cb(null, user.preferences.webhooks) : void 0;
},
deleteWebhook: function(req, cb) {
delete user.preferences.webhooks[req.params.id];
if (typeof user.markModified === "function") {
user.markModified('preferences.webhooks');
}
return typeof cb === "function" ? cb(null, user.preferences.webhooks) : void 0;
},
addPushDevice: function(req, cb) {
var i, item, pd;
if (!user.pushDevices) {
user.pushDevices = [];
}
pd = user.pushDevices;
item = {
regId: req.body.regId,
type: req.body.type
};
i = _.findIndex(pd, {
regId: item.regId
});
if (i === -1) {
pd.push(item);
}
return typeof cb === "function" ? cb(null, user.pushDevices) : void 0;
},
clearPMs: function(req, cb) {
user.inbox.messages = {};
if (typeof user.markModified === "function") {
user.markModified('inbox.messages');
}
return typeof cb === "function" ? cb(null, user.inbox.messages) : void 0;
},
deletePM: function(req, cb) {
delete user.inbox.messages[req.params.id];
if (typeof user.markModified === "function") {
user.markModified('inbox.messages.' + req.params.id);
}
return typeof cb === "function" ? cb(null, user.inbox.messages) : void 0;
},
blockUser: function(req, cb) {
var i;
i = user.inbox.blocks.indexOf(req.params.uuid);
if (~i) {
user.inbox.blocks.splice(i, 1);
} else {
user.inbox.blocks.push(req.params.uuid);
}
if (typeof user.markModified === "function") {
user.markModified('inbox.blocks');
}
return typeof cb === "function" ? cb(null, user.inbox.blocks) : void 0;
},
feed: function(req, cb) {
var egg, eggText, evolve, food, message, pet, petDisplayName, potion, potionText, ref, ref1, ref2, userPets;
ref = req.params, pet = ref.pet, food = ref.food;
food = content.food[food];
ref1 = pet.split('-'), egg = ref1[0], potion = ref1[1];
userPets = user.items.pets;
potionText = content.hatchingPotions[potion] ? content.hatchingPotions[potion].text() : potion;
eggText = content.eggs[egg] ? content.eggs[egg].text() : egg;
petDisplayName = i18n.t('petName', {
potion: potionText,
egg: eggText
});
if (!userPets[pet]) {
return typeof cb === "function" ? cb({
code: 404,
message: i18n.t('messagePetNotFound', req.language)
}) : void 0;
}
if (!((ref2 = user.items.food) != null ? ref2[food.key] : void 0)) {
return typeof cb === "function" ? cb({
code: 404,
message: i18n.t('messageFoodNotFound', req.language)
}) : void 0;
}
if (content.specialPets[pet]) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('messageCannotFeedPet', req.language)
}) : void 0;
}
if (user.items.mounts[pet]) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('messageAlreadyMount', req.language)
}) : void 0;
}
message = '';
evolve = function() {
userPets[pet] = -1;
user.items.mounts[pet] = true;
if (pet === user.items.currentPet) {
user.items.currentPet = "";
}
return message = i18n.t('messageEvolve', {
egg: petDisplayName
}, req.language);
};
if (food.key === 'Saddle') {
evolve();
} else {
if (food.target === potion || content.hatchingPotions[potion].premium) {
userPets[pet] += 5;
message = i18n.t('messageLikesFood', {
egg: petDisplayName,
foodText: food.text(req.language)
}, req.language);
} else {
userPets[pet] += 2;
message = i18n.t('messageDontEnjoyFood', {
egg: petDisplayName,
foodText: food.text(req.language)
}, req.language);
}
if (userPets[pet] >= 50 && !user.items.mounts[pet]) {
evolve();
}
}
user.items.food[food.key]--;
return typeof cb === "function" ? cb({
code: 200,
message: message
}, {
value: userPets[pet]
}) : void 0;
},
buySpecialSpell: function(req, cb) {
var base, item, key, message;
key = req.params.key;
item = content.special[key];
if (user.stats.gp < item.value) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('messageNotEnoughGold', req.language)
}) : void 0;
}
user.stats.gp -= item.value;
if ((base = user.items.special)[key] == null) {
base[key] = 0;
}
user.items.special[key]++;
if (typeof user.markModified === "function") {
user.markModified('items.special');
}
message = i18n.t('messageBought', {
itemText: item.text(req.language)
}, req.language);
return typeof cb === "function" ? cb({
code: 200,
message: message
}, _.pick(user, $w('items stats'))) : void 0;
},
purchase: function(req, cb, analytics) {
var analyticsData, convCap, convRate, item, key, price, ref, ref1, ref2, ref3, type;
ref = req.params, type = ref.type, key = ref.key;
if (type === 'gems' && key === 'gem') {
ref1 = api.planGemLimits, convRate = ref1.convRate, convCap = ref1.convCap;
convCap += user.purchased.plan.consecutive.gemCapExtra;
if (!((ref2 = user.purchased) != null ? (ref3 = ref2.plan) != null ? ref3.customerId : void 0 : void 0)) {
return typeof cb === "function" ? cb({
code: 401,
message: "Must subscribe to purchase gems with GP"
}, req) : void 0;
}
if (!(user.stats.gp >= convRate)) {
return typeof cb === "function" ? cb({
code: 401,
message: "Not enough Gold"
}) : void 0;
}
if (user.purchased.plan.gemsBought >= convCap) {
return typeof cb === "function" ? cb({
code: 401,
message: "You've reached the Gold=>Gem conversion cap (" + convCap + ") for this month. We have this to prevent abuse / farming. The cap will reset within the first three days of next month."
}) : void 0;
}
user.balance += .25;
user.purchased.plan.gemsBought++;
user.stats.gp -= convRate;
analyticsData = {
uuid: user._id,
itemKey: key,
acquireMethod: 'Gold',
goldCost: convRate,
category: 'behavior'
};
if (analytics != null) {
analytics.track('purchase gems', analyticsData);
}
return typeof cb === "function" ? cb({
code: 200,
message: "+1 Gem"
}, _.pick(user, $w('stats balance'))) : void 0;
}
if (type !== 'eggs' && type !== 'hatchingPotions' && type !== 'food' && type !== 'quests' && type !== 'gear') {
return typeof cb === "function" ? cb({
code: 404,
message: ":type must be in [eggs,hatchingPotions,food,quests,gear]"
}, req) : void 0;
}
if (type === 'gear') {
item = content.gear.flat[key];
if (user.items.gear.owned[key]) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('alreadyHave', req.language)
}) : void 0;
}
price = (item.twoHanded || item.gearSet === 'animal' ? 2 : 1) / 4;
} else {
item = content[type][key];
price = item.value / 4;
}
if (!item) {
return typeof cb === "function" ? cb({
code: 404,
message: ":key not found for Content." + type
}, req) : void 0;
}
if (!item.canBuy(user)) {
return typeof cb === "function" ? cb({
code: 403,
message: i18n.t('messageNotAvailable', req.language)
}) : void 0;
}
if ((user.balance < price) || !user.balance) {
return typeof cb === "function" ? cb({
code: 403,
message: i18n.t('notEnoughGems', req.language)
}) : void 0;
}
user.balance -= price;
if (type === 'gear') {
user.items.gear.owned[key] = true;
} else {
if (!(user.items[type][key] > 0)) {
user.items[type][key] = 0;
}
user.items[type][key]++;
}
analyticsData = {
uuid: user._id,
itemKey: key,
itemType: 'Market',
acquireMethod: 'Gems',
gemCost: item.value,
category: 'behavior'
};
if (analytics != null) {
analytics.track('acquire item', analyticsData);
}
return typeof cb === "function" ? cb(null, _.pick(user, $w('items balance'))) : void 0;
},
releasePets: function(req, cb, analytics) {
var analyticsData, pet;
if (user.balance < 1) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('notEnoughGems', req.language)
}) : void 0;
} else {
user.balance -= 1;
for (pet in content.pets) {
user.items.pets[pet] = 0;
}
if (!user.achievements.beastMasterCount) {
user.achievements.beastMasterCount = 0;
}
user.achievements.beastMasterCount++;
user.items.currentPet = "";
}
analyticsData = {
uuid: user._id,
acquireMethod: 'Gems',
gemCost: 4,
category: 'behavior'
};
if (analytics != null) {
analytics.track('release pets', analyticsData);
}
return typeof cb === "function" ? cb(null, user) : void 0;
},
releaseMounts: function(req, cb, analytics) {
var analyticsData, mount;
if (user.balance < 1) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('notEnoughGems', req.language)
}) : void 0;
} else {
user.balance -= 1;
user.items.currentMount = "";
for (mount in content.pets) {
user.items.mounts[mount] = null;
}
if (!user.achievements.mountMasterCount) {
user.achievements.mountMasterCount = 0;
}
user.achievements.mountMasterCount++;
}
analyticsData = {
uuid: user._id,
acquireMethod: 'Gems',
gemCost: 4,
category: 'behavior'
};
if (analytics != null) {
analytics.track('release mounts', analyticsData);
}
return typeof cb === "function" ? cb(null, user) : void 0;
},
releaseBoth: function(req, cb) {
var analyticsData, animal, giveTriadBingo;
if (user.balance < 1.5 && !user.achievements.triadBingo) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('notEnoughGems', req.language)
}) : void 0;
} else {
giveTriadBingo = true;
if (!user.achievements.triadBingo) {
analyticsData = {
uuid: user._id,
acquireMethod: 'Gems',
gemCost: 6,
category: 'behavior'
};
if (typeof analytics !== "undefined" && analytics !== null) {
analytics.track('release pets & mounts', analyticsData);
}
user.balance -= 1.5;
}
user.items.currentMount = "";
user.items.currentPet = "";
for (animal in content.pets) {
if (user.items.pets[animal] === -1) {
giveTriadBingo = false;
}
user.items.pets[animal] = 0;
user.items.mounts[animal] = null;
}
if (!user.achievements.beastMasterCount) {
user.achievements.beastMasterCount = 0;
}
user.achievements.beastMasterCount++;
if (!user.achievements.mountMasterCount) {
user.achievements.mountMasterCount = 0;
}
user.achievements.mountMasterCount++;
if (giveTriadBingo) {
if (!user.achievements.triadBingoCount) {
user.achievements.triadBingoCount = 0;
}
user.achievements.triadBingoCount++;
}
}
return typeof cb === "function" ? cb(null, user) : void 0;
},
buy: function(req, cb, analytics) {
var analyticsData, armoireExp, armoireResp, armoireResult, base, buyResp, drop, eligibleEquipment, item, key, message, name;
key = req.params.key;
item = key === 'potion' ? content.potion : key === 'armoire' ? content.armoire : content.gear.flat[key];
if (!item) {
return typeof cb === "function" ? cb({
code: 404,
message: "Item '" + key + " not found (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.js)"
}) : void 0;
}
if (user.stats.gp < item.value) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('messageNotEnoughGold', req.language)
}) : void 0;
}
if ((item.canOwn != null) && !item.canOwn(user)) {
return typeof cb === "function" ? cb({
code: 401,
message: "You can't buy this item"
}) : void 0;
}
armoireResp = void 0;
if (item.key === 'potion') {
user.stats.hp += 15;
if (user.stats.hp > 50) {
user.stats.hp = 50;
}
} else if (item.key === 'armoire') {
armoireResult = user.fns.predictableRandom(user.stats.gp);
eligibleEquipment = _.filter(content.gear.flat, (function(i) {
return i.klass === 'armoire' && !user.items.gear.owned[i.key];
}));
if (!_.isEmpty(eligibleEquipment) && (armoireResult < .6 || !user.flags.armoireOpened)) {
eligibleEquipment.sort();
drop = user.fns.randomVal(eligibleEquipment);
user.items.gear.owned[drop.key] = true;
user.flags.armoireOpened = true;
message = i18n.t('armoireEquipment', {
image: '<span class="shop_' + drop.key + ' pull-left"></span>',
dropText: drop.text(req.language)
}, req.language);
if (api.count.remainingGearInSet(user.items.gear.owned, 'armoire') === 0) {
user.flags.armoireEmpty = true;
}
armoireResp = {
type: "gear",
dropKey: drop.key,
dropText: drop.text(req.language)
};
} else if ((!_.isEmpty(eligibleEquipment) && armoireResult < .8) || armoireResult < .5) {
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;
message = i18n.t('armoireFood', {
image: '<span class="Pet_Food_' + drop.key + ' pull-left"></span>',
dropArticle: drop.article,
dropText: drop.text(req.language)
}, req.language);
armoireResp = {
type: "food",
dropKey: drop.key,
dropArticle: drop.article,
dropText: drop.text(req.language)
};
} else {
armoireExp = Math.floor(user.fns.predictableRandom(user.stats.exp) * 40 + 10);
user.stats.exp += armoireExp;
message = i18n.t('armoireExp', req.language);
armoireResp = {
"type": "experience",
"value": armoireExp
};
}
} else {
if (user.preferences.autoEquip) {
user.items.gear.equipped[item.type] = item.key;
message = user.fns.handleTwoHanded(item, null, req);
}
user.items.gear.owned[item.key] = true;
if (message == null) {
message = i18n.t('messageBought', {
itemText: item.text(req.language)
}, req.language);
}
if (item.last) {
user.fns.ultimateGear();
}
}
user.stats.gp -= item.value;
analyticsData = {
uuid: user._id,
itemKey: key,
acquireMethod: 'Gold',
goldCost: item.value,
category: 'behavior'
};
if (analytics != null) {
analytics.track('acquire item', analyticsData);
}
buyResp = _.pick(user, $w('items achievements stats flags'));
if (armoireResp) {
buyResp["armoire"] = armoireResp;
}
return typeof cb === "function" ? cb({
code: 200,
message: message
}, buyResp) : void 0;
},
buyQuest: function(req, cb, analytics) {
var analyticsData, base, item, key, message, name;
key = req.params.key;
item = content.quests[key];
if (!item) {
return typeof cb === "function" ? cb({
code: 404,
message: "Quest '" + key + " not found (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.js)"
}) : void 0;
}
if (!(item.category === 'gold' && item.goldValue)) {
return typeof cb === "function" ? cb({
code: 404,
message: "Quest '" + key + " is not a Gold-purchasable quest (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.js)"
}) : void 0;
}
if (user.stats.gp < item.goldValue) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('messageNotEnoughGold', req.language)
}) : void 0;
}
message = i18n.t('messageBought', {
itemText: item.text(req.language)
}, req.language);
if ((base = user.items.quests)[name = item.key] == null) {
base[name] = 0;
}
user.items.quests[item.key] += 1;
user.stats.gp -= item.goldValue;
analyticsData = {
uuid: user._id,
itemKey: item.key,
itemType: 'Market',
goldCost: item.goldValue,
acquireMethod: 'Gold',
category: 'behavior'
};
if (analytics != null) {
analytics.track('acquire item', analyticsData);
}
return typeof cb === "function" ? cb({
code: 200,
message: message
}, user.items.quests) : void 0;
},
buyMysterySet: function(req, cb, analytics) {
var mysterySet, ref;
if (!(user.purchased.plan.consecutive.trinkets > 0)) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('notEnoughHourglasses', req.language)
}) : void 0;
}
mysterySet = (ref = content.timeTravelerStore(user.items.gear.owned)) != null ? ref[req.params.key] : void 0;
if ((typeof window !== "undefined" && window !== null ? window.confirm : void 0) != null) {
if (!window.confirm(i18n.t('hourglassBuyEquipSetConfirm'))) {
return;
}
}
if (!mysterySet) {
return typeof cb === "function" ? cb({
code: 404,
message: "Mystery set not found, or set already owned"
}) : void 0;
}
_.each(mysterySet.items, function(i) {
var analyticsData;
user.items.gear.owned[i.key] = true;
analyticsData = {
uuid: user._id,
itemKey: i.key,
itemType: 'Subscriber Gear',
acquireMethod: 'Hourglass',
category: 'behavior'
};
return analytics != null ? analytics.track('acquire item', analyticsData) : void 0;
});
user.purchased.plan.consecutive.trinkets--;
return typeof cb === "function" ? cb({
code: 200,
message: i18n.t('hourglassPurchaseSet', req.language)
}, _.pick(user, $w('items purchased.plan.consecutive'))) : void 0;
},
hourglassPurchase: function(req, cb, analytics) {
var analyticsData, key, ref, type;
ref = req.params, type = ref.type, key = ref.key;
if (!content.timeTravelStable[type]) {
return typeof cb === "function" ? cb({
code: 403,
message: i18n.t('typeNotAllowedHourglass', req.language) + JSON.stringify(_.keys(content.timeTravelStable))
}) : void 0;
}
if (!_.contains(_.keys(content.timeTravelStable[type]), key)) {
return typeof cb === "function" ? cb({
code: 403,
message: i18n.t(type + 'NotAllowedHourglass', req.language)
}) : void 0;
}
if (user.items[type][key]) {
return typeof cb === "function" ? cb({
code: 403,
message: i18n.t(type + 'AlreadyOwned', req.language)
}) : void 0;
}
if (!(user.purchased.plan.consecutive.trinkets > 0)) {
return typeof cb === "function" ? cb({
code: 403,
message: i18n.t('notEnoughHourglasses', req.language)
}) : void 0;
}
user.purchased.plan.consecutive.trinkets--;
if (type === 'pets') {
user.items.pets[key] = 5;
}
if (type === 'mounts') {
user.items.mounts[key] = true;
}
analyticsData = {
uuid: user._id,
itemKey: key,
itemType: type,
acquireMethod: 'Hourglass',
category: 'behavior'
};
if (analytics != null) {
analytics.track('acquire item', analyticsData);
}
return typeof cb === "function" ? cb({
code: 200,
message: i18n.t('hourglassPurchase', req.language)
}, _.pick(user, $w('items purchased.plan.consecutive'))) : void 0;
},
sell: function(req, cb) {
var key, ref, type;
ref = req.params, key = ref.key, type = ref.type;
if (type !== 'eggs' && type !== 'hatchingPotions' && type !== 'food') {
return typeof cb === "function" ? cb({
code: 404,
message: ":type not found. Must bes in [eggs, hatchingPotions, food]"
}) : void 0;
}
if (!user.items[type][key]) {
return typeof cb === "function" ? cb({
code: 404,
message: ":key not found for user.items." + type
}) : void 0;
}
user.items[type][key]--;
user.stats.gp += content[type][key].value;
return typeof cb === "function" ? cb(null, _.pick(user, $w('stats items'))) : void 0;
},
equip: function(req, cb) {
var item, key, message, ref, type;
ref = [req.params.type || 'equipped', req.params.key], type = ref[0], key = ref[1];
switch (type) {
case 'mount':
if (!user.items.mounts[key]) {
return typeof cb === "function" ? cb({
code: 404,
message: ":You do not own this mount."
}) : void 0;
}
user.items.currentMount = user.items.currentMount === key ? '' : key;
break;
case 'pet':
if (!user.items.pets[key]) {
return typeof cb === "function" ? cb({
code: 404,
message: ":You do not own this pet."
}) : void 0;
}
user.items.currentPet = user.items.currentPet === key ? '' : key;
break;
case 'costume':
case 'equipped':
item = content.gear.flat[key];
if (!user.items.gear.owned[key]) {
return typeof cb === "function" ? cb({
code: 404,
message: ":You do not own this gear."
}) : void 0;
}
if (user.items.gear[type][item.type] === key) {
user.items.gear[type][item.type] = item.type + "_base_0";
message = i18n.t('messageUnEquipped', {
itemText: item.text(req.language)
}, req.language);
} else {
user.items.gear[type][item.type] = item.key;
message = user.fns.handleTwoHanded(item, type, req);
}
if (typeof user.markModified === "function") {
user.markModified("items.gear." + type);
}
}
return typeof cb === "function" ? cb((message ? {
code: 200,
message: message
} : null), user.items) : void 0;
},
hatch: function(req, cb) {
var egg, hatchingPotion, pet, ref;
ref = req.params, egg = ref.egg, hatchingPotion = ref.hatchingPotion;
if (!(egg && hatchingPotion)) {
return typeof cb === "function" ? cb({
code: 400,
message: "Please specify query.egg & query.hatchingPotion"
}) : void 0;
}
if (!(user.items.eggs[egg] > 0 && user.items.hatchingPotions[hatchingPotion] > 0)) {
return typeof cb === "function" ? cb({
code: 403,
message: i18n.t('messageMissingEggPotion', req.language)
}) : void 0;
}
if (content.hatchingPotions[hatchingPotion].premium && !content.dropEggs[egg]) {
return typeof cb === "function" ? cb({
code: 403,
message: i18n.t('messageInvalidEggPotionCombo', req.language)
}) : void 0;
}
pet = egg + "-" + hatchingPotion;
if (user.items.pets[pet] && user.items.pets[pet] > 0) {
return typeof cb === "function" ? cb({
code: 403,
message: i18n.t('messageAlreadyPet', req.language)
}) : void 0;
}
user.items.pets[pet] = 5;
user.items.eggs[egg]--;
user.items.hatchingPotions[hatchingPotion]--;
return typeof cb === "function" ? cb({
code: 200,
message: i18n.t('messageHatched', req.language)
}, user.items) : void 0;
},
unlock: function(req, cb, analytics) {
var alreadyOwns, analyticsData, cost, fullSet, k, path, split, v;
path = req.query.path;
fullSet = ~path.indexOf(",");
cost = ~path.indexOf('background.') ? fullSet ? 3.75 : 1.75 : fullSet ? 1.25 : 0.5;
alreadyOwns = !fullSet && user.fns.dotGet("purchased." + path) === true;
if ((user.balance < cost || !user.balance) && !alreadyOwns) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('notEnoughGems', req.language)
}) : void 0;
}
if (fullSet) {
_.each(path.split(","), function(p) {
if (~path.indexOf('gear.')) {
user.fns.dotSet("" + p, true);
true;
} else {
}
user.fns.dotSet("purchased." + p, true);
return true;
});
} else {
if (alreadyOwns) {
split = path.split('.');
v = split.pop();
k = split.join('.');
if (k === 'background' && v === user.preferences.background) {
v = '';
}
user.fns.dotSet("preferences." + k, v);
return typeof cb === "function" ? cb(null, req) : void 0;
}
user.fns.dotSet("purchased." + path, true);
}
user.balance -= cost;
if (~path.indexOf('gear.')) {
if (typeof user.markModified === "function") {
user.markModified('gear.owned');
}
} else {
if (typeof user.markModified === "function") {
user.markModified('purchased');
}
}
analyticsData = {
uuid: user._id,
itemKey: path,
itemType: 'customization',
acquireMethod: 'Gems',
gemCost: cost / .25,
category: 'behavior'
};
if (analytics != null) {
analytics.track('acquire item', analyticsData);
}
return typeof cb === "function" ? cb(null, _.pick(user, $w('purchased preferences items'))) : void 0;
},
changeClass: function(req, cb, analytics) {
var analyticsData, klass, ref;
klass = (ref = req.query) != null ? ref["class"] : void 0;
if (klass === 'warrior' || klass === 'rogue' || klass === 'wizard' || klass === 'healer') {
analyticsData = {
uuid: user._id,
"class": klass,
acquireMethod: 'Gems',
gemCost: 3,
category: 'behavior'
};
if (analytics != null) {
analytics.track('change class', analyticsData);
}
user.stats["class"] = klass;
user.flags.classSelected = true;
_.each(["weapon", "armor", "shield", "head"], function(type) {
var foundKey;
foundKey = false;
_.findLast(user.items.gear.owned, function(v, k) {
if (~k.indexOf(type + "_" + klass) && v === true) {
return foundKey = k;
}
});
user.items.gear.equipped[type] = foundKey ? foundKey : type === "weapon" ? "weapon_" + klass + "_0" : type === "shield" && klass === "rogue" ? "shield_rogue_0" : type + "_base_0";
if (type === "weapon" || (type === "shield" && klass === "rogue")) {
user.items.gear.owned[type + "_" + klass + "_0"] = true;
}
return true;
});
} else {
if (user.preferences.disableClasses) {
user.preferences.disableClasses = false;
user.preferences.autoAllocate = false;
} else {
if (!(user.balance >= .75)) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('notEnoughGems', req.language)
}) : void 0;
}
user.balance -= .75;
}
_.merge(user.stats, {
str: 0,
con: 0,
per: 0,
int: 0,
points: api.capByLevel(user.stats.lvl)
});
user.flags.classSelected = false;
}
return typeof cb === "function" ? cb(null, _.pick(user, $w('stats flags items preferences'))) : void 0;
},
disableClasses: function(req, cb) {
user.stats["class"] = 'warrior';
user.flags.classSelected = true;
user.preferences.disableClasses = true;
user.preferences.autoAllocate = true;
user.stats.str = api.capByLevel(user.stats.lvl);
user.stats.points = 0;
return typeof cb === "function" ? cb(null, _.pick(user, $w('stats flags preferences'))) : void 0;
},
allocate: function(req, cb) {
var stat;
stat = req.query.stat || 'str';
if (user.stats.points > 0) {
user.stats[stat]++;
user.stats.points--;
if (stat === 'int') {
user.stats.mp++;
}
}
return typeof cb === "function" ? cb(null, _.pick(user, $w('stats'))) : void 0;
},
readCard: function(req, cb) {
var cardType;
cardType = req.params.cardType;
user.items.special[cardType + "Received"].shift();
if (typeof user.markModified === "function") {
user.markModified("items.special." + cardType + "Received");
}
user.flags.cardReceived = false;
return typeof cb === "function" ? cb(null, 'items.special flags.cardReceived') : void 0;
},
openMysteryItem: function(req, cb, analytics) {
var analyticsData, item, ref, ref1;
item = (ref = user.purchased.plan) != null ? (ref1 = ref.mysteryItems) != null ? ref1.shift() : void 0 : void 0;
if (!item) {
return typeof cb === "function" ? cb({
code: 400,
message: "Empty"
}) : void 0;
}
item = content.gear.flat[item];
user.items.gear.owned[item.key] = true;
if (typeof user.markModified === "function") {
user.markModified('purchased.plan.mysteryItems');
}
item.notificationType = 'Mystery';
analyticsData = {
uuid: user._id,
itemKey: item,
itemType: 'Subscriber Gear',
acquireMethod: 'Subscriber',
category: 'behavior'
};
if (analytics != null) {
analytics.track('open mystery item', analyticsData);
}
if (typeof window !== 'undefined') {
(user._tmp != null ? user._tmp : user._tmp = {}).drop = item;
}
return typeof cb === "function" ? cb(null, user.items.gear.owned) : void 0;
},
score: function(req, cb) {
var addPoints, calculateDelta, calculateReverseDelta, changeTaskValue, delta, direction, gainMP, id, multiplier, num, options, ref, stats, subtractPoints, task, th;
ref = req.params, id = ref.id, direction = ref.direction;
task = user.tasks[id];
options = req.query || {};
_.defaults(options, {
times: 1,
cron: false
});
user._tmp = {};
stats = {
gp: +user.stats.gp,
hp: +user.stats.hp,
exp: +user.stats.exp
};
task.value = +task.value;
task.streak = ~~task.streak;
if (task.priority == null) {
task.priority = 1;
}
if (task.value > stats.gp && task.type === 'reward') {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('messageNotEnoughGold', req.language)
}) : void 0;
}
delta = 0;
calculateDelta = function() {
var currVal, nextDelta, ref1;
currVal = task.value < -47.27 ? -47.27 : task.value > 21.27 ? 21.27 : task.value;
nextDelta = Math.pow(0.9747, currVal) * (direction === 'down' ? -1 : 1);
if (((ref1 = task.checklist) != null ? ref1.length : void 0) > 0) {
if (direction === 'down' && task.type === 'daily' && options.cron) {
nextDelta *= 1 - _.reduce(task.checklist, (function(m, i) {
return m + (i.completed ? 1 : 0);
}), 0) / task.checklist.length;
}
if (task.type === 'todo') {
nextDelta *= 1 + _.reduce(task.checklist, (function(m, i) {
return m + (i.completed ? 1 : 0);
}), 0);
}
}
return nextDelta;
};
calculateReverseDelta = function() {
var calc, closeEnough, currVal, diff, nextDelta, ref1, testVal;
currVal = task.value < -47.27 ? -47.27 : task.value > 21.27 ? 21.27 : task.value;
testVal = currVal + Math.pow(0.9747, currVal) * (direction === 'down' ? -1 : 1);
closeEnough = 0.00001;
while (true) {
calc = testVal + Math.pow(0.9747, testVal);
diff = currVal - calc;
if (Math.abs(diff) < closeEnough) {
break;
}
if (diff > 0) {
testVal -= diff;
} else {
testVal += diff;
}
}
nextDelta = testVal - currVal;
if (((ref1 = task.checklist) != null ? ref1.length : void 0) > 0) {
if (task.type === 'todo') {
nextDelta *= 1 + _.reduce(task.checklist, (function(m, i) {
return m + (i.completed ? 1 : 0);
}), 0);
}
}
return nextDelta;
};
changeTaskValue = function() {
return _.times(options.times, function() {
var nextDelta, ref1;
nextDelta = !options.cron && direction === 'down' ? calculateReverseDelta() : calculateDelta();
if (task.type !== 'reward') {
if (user.preferences.automaticAllocation === true && user.preferences.allocationMode === 'taskbased' && !(task.type === 'todo' && direction === 'down')) {
user.stats.training[task.attribute] += nextDelta;
}
if (direction === 'up') {
user.party.quest.progress.up = user.party.quest.progress.up || 0;
if ((ref1 = task.type) === 'daily' || ref1 === 'todo') {
user.party.quest.progress.up += nextDelta * (1 + (user._statsComputed.str / 200));
}
if (task.type === 'habit') {
user.party.quest.progress.up += nextDelta * (0.5 + (user._statsComputed.str / 400));
}
}
task.value += nextDelta;
}
return delta += nextDelta;
});
};
addPoints = function() {
var _crit, afterStreak, currStreak, gpMod, intBonus, perBonus, streakBonus;
_crit = (delta > 0 ? user.fns.crit() : 1);
if (_crit > 1) {
user._tmp.crit = _crit;
}
intBonus = 1 + (user._statsComputed.int * .025);
stats.exp += Math.round(delta * intBonus * task.priority * _crit * 6);
perBonus = 1 + user._statsComputed.per * .02;
gpMod = delta * task.priority * _crit * perBonus;
return stats.gp += task.streak ? (currStreak = direction === 'down' ? task.streak - 1 : task.streak, streakBonus = currStreak / 100 + 1, afterStreak = gpMod * streakBonus, currStreak > 0 ? gpMod > 0 ? user._tmp.streakBonus = afterStreak - gpMod : void 0 : void 0, afterStreak) : gpMod;
};
subtractPoints = function() {
var conBonus, hpMod;
conBonus = 1 - (user._statsComputed.con / 250);
if (conBonus < .1) {
conBonus = 0.1;
}
hpMod = delta * conBonus * task.priority * 2;
return stats.hp += Math.round(hpMod * 10) / 10;
};
gainMP = function(delta) {
delta *= user._tmp.crit || 1;
user.stats.mp += delta;
if (user.stats.mp >= user._statsComputed.maxMP) {
user.stats.mp = user._statsComputed.maxMP;
}
if (user.stats.mp < 0) {
return user.stats.mp = 0;
}
};
switch (task.type) {
case 'habit':
changeTaskValue();
if (delta > 0) {
addPoints();
} else {
subtractPoints();
}
gainMP(_.max([0.25, .0025 * user._statsComputed.maxMP]) * (direction === 'down' ? -1 : 1));
th = (task.history != null ? task.history : task.history = []);
if (th[th.length - 1] && moment(th[th.length - 1].date).isSame(new Date, 'day')) {
th[th.length - 1].value = task.value;
} else {
th.push({
date: +(new Date),
value: task.value
});
}
if (typeof user.markModified === "function") {
user.markModified("habits." + (_.findIndex(user.habits, {
id: task.id
})) + ".history");
}
break;
case 'daily':
if (options.cron) {
changeTaskValue();
subtractPoints();
if (!user.stats.buffs.streaks) {
task.streak = 0;
}
} else {
changeTaskValue();
if (direction === 'down') {
delta = calculateDelta();
}
addPoints();
gainMP(_.max([1, .01 * user._statsComputed.maxMP]) * (direction === 'down' ? -1 : 1));
if (direction === 'up') {
task.streak = task.streak ? task.streak + 1 : 1;
if ((task.streak % 21) === 0) {
user.achievements.streak = user.achievements.streak ? user.achievements.streak + 1 : 1;
}
} else {
if ((task.streak % 21) === 0) {
user.achievements.streak = user.achievements.streak ? user.achievements.streak - 1 : 0;
}
task.streak = task.streak ? task.streak - 1 : 0;
}
}
break;
case 'todo':
if (options.cron) {
changeTaskValue();
} else {
task.dateCompleted = direction === 'up' ? new Date : void 0;
changeTaskValue();
if (direction === 'down') {
delta = calculateDelta();
}
addPoints();
multiplier = _.max([
_.reduce(task.checklist, (function(m, i) {
return m + (i.completed ? 1 : 0);
}), 1), 1
]);
gainMP(_.max([multiplier, .01 * user._statsComputed.maxMP * multiplier]) * (direction === 'down' ? -1 : 1));
}
break;
case 'reward':
changeTaskValue();
stats.gp -= Math.abs(task.value);
num = parseFloat(task.value).toFixed(2);
if (stats.gp < 0) {
stats.hp += stats.gp;
stats.gp = 0;
}
}
user.fns.updateStats(stats, req);
if (typeof window === 'undefined') {
if (direction === 'up') {
user.fns.randomDrop({
task: task,
delta: delta
}, req);
}
}
if (typeof cb === "function") {
cb(null, user);
}
return delta;
}
};
}
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: function(stats, req, analytics) {
var tnl;
if (stats.hp <= 0) {
return user.stats.hp = 0;
}
user.stats.hp = stats.hp;
user.stats.gp = stats.gp >= 0 ? stats.gp : 0;
tnl = api.tnl(user.stats.lvl);
if (stats.exp >= tnl) {
user.stats.exp = stats.exp;
while (stats.exp >= tnl) {
stats.exp -= tnl;
user.stats.lvl++;
tnl = api.tnl(user.stats.lvl);
user.stats.hp = 50;
var userTotalStatPoints = user.stats.str + user.stats.int + user.stats.con + user.stats.per;
if (userTotalStatPoints >= api.maxStatPoints) {
continue;
}
if (user.preferences.automaticAllocation) {
user.fns.autoAllocate();
} else {
user.stats.points = user.stats.lvl - userTotalStatPoints;
if (user.stats.points < 0) {
user.stats.points = 0;
}
}
}
}
user.stats.exp = stats.exp;
if (user.flags == null) {
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 (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', {
get: function() {
var computed;
computed = _.reduce(['per', 'con', 'str', 'int'], (function(_this) {
return function(m, stat) {
m[stat] = _.reduce($w('stats stats.buffs items.gear.equipped.weapon items.gear.equipped.armor items.gear.equipped.head items.gear.equipped.shield'), function(m2, path) {
var item, val;
val = user.fns.dotGet(path);
return m2 + (~path.indexOf('items.gear') ? (item = content.gear.flat[val], (+(item != null ? item[stat] : void 0) || 0) * ((item != null ? item.klass : void 0) === user.stats["class"] || (item != null ? item.specialClass : void 0) === user.stats["class"] ? 1.5 : 1)) : +val[stat] || 0);
}, 0);
m[stat] += Math.floor(api.capByLevel(user.stats.lvl) / 2);
return m;
};
})(this), {});
computed.maxMP = computed.int * 2 + 30;
return computed;
}
});
return Object.defineProperty(user, 'tasks', {
get: function() {
var tasks;
tasks = user.habits.concat(user.dailys).concat(user.todos).concat(user.rewards);
return _.object(_.pluck(tasks, "id"), tasks);
}
});
};