API v3 [WIP] (#6144)

* Fixed more tests

* Added tags into user service

* Added api-v3 auth urls

* v3: fix package.json

* v3: fix package.json

* Fixed auth tests. Updated Authctrl response

* v3: remove newrelic config file in favour of env variables

* v3: upgrade some deps

* switch from Q to Bluebird

* v3 fix tests with deferred

* Removed extra consoles.log. Changed data.data to res.data

* v3 fix tests and use coroutines instead of regenerator

* v3: fix tests

* v3: do not await a non promise

* v3: q -> bluebird

* Changed id param for registration response

* Updated party query and create

* Ensured login callback happens after user sync

* Add challenges to groups. Fixed isMemberOfGuild check

* Updated party and group tests

* Fixed cron test

* return user.id and send analytics event before changing page

* fix trailing spaces

* disable redirects

* Api v3 party tavern fixes (#7191)

* Added check if user is in party before query

* Cached party query. Prevented party request when user is not in party. Updated Party create with no invites

* Update tavern ctrl to use new promise

* v3: misc fixes

* Api v3 task fixes (#7193)

* Update task view to use _id

* Added try catch to user service ops calls

* v3 client: saving after syncing is complete

* Fixed test broken by part sync change (#7195)

* v3: fix todo scoring and try to fix production testing problem

* revert changes to mongoose config

* mongoose: increase keepAlive

* test mongoose fix

* fix: Only apply captureStackTrace if it exists on the error object

* v3: fix reminders with no startDate

* mongoose: use options

* chore(): rename website/src -> website/server and website/public -> website/client (#7199)

* v3 fix GET /groups: return an error only if an invalid type is supplied not when there are 0 results (#7203)

* [API v3] Fix calls to user.ops and deleting tags (#7204)

* v3: fixes calls to user.ops from views and deleting tags

* v3: fix tests that use user._statsComputed

* Api v3 fixes continued (#7205)

* Added timzeone offset back

* Added APIToken back to settings page

* Fixed fetch recent messages for party

* Fixed returning group description

* Fixed check if user is member of challenge

* Fixed party members appearing in header

* Updated get myGroups param to include public groups. Fixed isMemberOf group

* Fixed hourglass purchase

* Fixed challenge addding tasks on first creating

* Updated tests to accomidate new changes

* fix: Correct checklist on client

Closes #7207

* fix: Pin eslint to 2.9

* minor improvements to cron code for clarity; fix inaccurate comments; add TODOs for rest-in-inn actions

* fix: Add missing type param to equip call

closes #7212

* rename and reword pubChalsMinPrize to reflect that it's only for Tavern challenges

* allows players to send gems to each other; other minor related changes - fixes https://github.com/HabitRPG/habitrpg/issues/7227

* fix tests for /members/transfer-gems

* fix: Set gems sent notification as translatable string

* chore: Remove unusued variable

* fix: Remove requirement on message paramter in transfer-gems

* add a missing variable declaration

* chore: clarify comments on cron code

* fix: Correct client request from habitrpg -> tavern

* update apidoc URL in package.json

Closes #7222

* Fixed start party by invites

* Updated spell casting to v3

* Fixed adding and removing tags on tasks

* Fixed page reload on settings change

* Fixed battle monsters with friends button

* Loaded completed todos when done is clicked

* chore: Reinstate floating version number for eslint

babel-eslint regression fixed

* Fixed reload tests

* change "an user" to "a user" in comments and text (no code changes) (#7257)

* fix: Alert user that drops were recieved

* remove userServices.js from karma.conf - it's been moved to website/client/js/services

* feat: Create debug update user route

* fix: Correct set cron debug function

* feat: Add make admin button to debug menu

* lint: Add missing semicolons in test

* fix: Temporarilly comment out udpate user debug route

* v3: fix _tmp for crit and streakBonus

* v3: execute all actions when leaving a solo party

* v3 client: fix group not found when leaving party

* v3 migration: fix challenge prize

* v3 cron: only save modified tasks

* v3: add CHALLENGE_TASK_NOT_FOUND to valid broken reasons

* v3: fix tasks chart

* v3 client: fix ability to leave challenge

* v3 client: fix filtering by tag and correctly show tag tooltip

* v3 common: fix tags tests

* v3 client: support unlinking not found challenges tasks

* v3: disable Bluebird warning for missing return, fixes #7269

* feat: Separate out update-user into set-cron and make-admin debug routes

* chore: Disable make admin debug route for v3 prod testing

* v3: misc fixes

* v3: misc fixes

* v3: fix adding multiple tasks

* Fixed join/leave button updates

* Queried only user groups to be available when creating challenges

* Fixed bulk add tasks to challenge

* Synced challenge tasks after leave and join.

* Fixed default selected group

* Fixed challenge member info. Fixed challenge winner selection

* Fixed deleting challenge tasks

* Fixed particiapting filter

* v3 client: fix casting spells

* v3: do not log sensitive data

* v3: always save user when casting spell

* v3: always save user when casting spell

* v3: more fixes for spells

* fix typos and missing information in apidocs - fixes https://github.com/HabitRPG/habitrpg/issues/7277 (#7282)

* v3: add TODO for client side spells

* feat: Add modify inventory debug menu

* Fixed viewing user progress on challenge

* Updated tests

* fix: Fix quest progress button

* fix incorrect Armoire test; remove unneeded param details from apidocs; disambiguate health potion

* v3: fix stealth casting

* v3: fix tasks saving and selection for rebirth reroll and reset (server-only)

* v3: fix auto allocation

* v3 client: misc fixes

* rename buyPotion and buy-potion to buyHealthPotion and buy-health-potion; fix apidoc param error

* Added delete for saved challenge task

* Fixed member modal on front page

* adjust text in apidocs for errors / clarity / consistency / standard terminology (no code changes) (#7298)

* fix bug in Rebirth test, add new tests, adjust apidocs (#7293)

* Updated task model to allow setting streak (#7306)

* fix: Correct missing * in apidoc comments

* Api v3 challenge fixes (#7287)

* Fixed join/leave button updates

* Queried only user groups to be available when creating challenges

* Fixed bulk add tasks to challenge

* Synced challenge tasks after leave and join.

* Fixed default selected group

* Fixed challenge member info. Fixed challenge winner selection

* Fixed deleting challenge tasks

* Fixed particiapting filter

* Fixed viewing user progress on challenge

* Updated tests

* Added delete for saved challenge task

* v3: fix sorting

* [API v3] add CRON_SAFE_MODE (#7286)

* add CRON_SAFE_MODE to example config file, fix some bugs, add an unrelated low-priority TODO

* create CRON_SAFE_MODE to disable parts of cron for use after extended outage - fixes https://github.com/HabitRPG/habitrpg/issues/7161

* fix a bug with CRON_SAFE_MODE, remove duplicated code, remove completed TODO comment

* fix check for CRON_SAFE_MODE

* v3 client: fix typo

* adjust debug menu Modify Inventory: hungrier pets, fewer Special items, "Hide" buttons

* completed To-Dos: return the 30 most recent instead of 30 oldest (#7318)

* v3 migration: fix createdAt date

* adjust locales text, key names, and files for Rebirth, Reset, and Fortify / ReRoll for consistency with existing strings (#7321)

* v3: fix unlinking multiple tasks

* v3 fix releasing pets

* v3: fix authenticating with apiUrl

* v3: fix typo

* v3 fix client tests for unlinking

* v3 client: do not show start quest button when quest is active

* v3 client: fix ability to send cards

* v3 client: fix misc challenge issues

* v3: fix notifications

* v3 client: more user friendly errors

* v3 client: only load completed todos once

* v3 client: fix tests

* v3: move TAVERN_ID to common code

* fix: Provide default type and text for new task creation in score route

* fix: Provide default history [] for habit in score route

* fix: Add _legacyId prop to tasks to support non-uuid identifiers

* chore: Change v3 migration to use _legacyId instead of legacyId

* fix: check for _legacyId in tasks if id does not exist

* refactor: Extract out finding task by id or _legacyId into a function

* Api v3 party quest fixes (#7341)

* Fix display of add challenge message when group challenges are empty

* Fixed forced quest start to update quest without reload

* Fixed needing to reload when accepting party invite

* Fix group leave and join reload

* Fixed leave current party and join another

* Updated party tests

* v3 client: remove console.log statement

* v3: misc fixes

* v3 client: fix predicatbale random

* v3: info about API v3

* v3: update footer with links to developer resources

* v3: support party invitation from email

* v3 client: fix chat flagging

* fix: Correct get tasks route to properly get todos (#7349)

* move locales strings from api-v3.json to other locales files (#7347)

* move locales strings from api-v3.json: authentication strings -> front.json

* move locales strings from api-v3.json: authentication strings -> tasks.json

* move locales strings from api-v3.json: authentication strings -> groups.json

* move locales strings from api-v3.json: authentication strings -> challenge.json

* move locales strings from api-v3.json: authentication strings -> groups.json (again)

* move locales strings from api-v3.json: authentication strings -> quests.json

* move locales strings from api-v3.json: authentication strings -> subscriber.json

* move locales strings from api-v3.json: authentication strings -> spells.json

* move locales strings from api-v3.json: authentication strings -> character.json

* move locales strings from api-v3.json: authentication strings -> groups.json (PMs)

* move locales strings from api-v3.json: authentication strings -> npc.json

* move locales strings from api-v3.json: authentication strings -> pets.json

* move locales strings from api-v3.json: authentication strings -> miscellaneous

* move locales strings from api-v3.json: authentication strings -> contrib.json and settings.json

* move locales strings from api-v3.json: delete unused string (invalidTasksOwner), delete api-v3.json, whitespace cleanup

* v3 client: fix sticky header

* v3: remove unused code

* v3 client: correctly redirect after inviting

* Removed v2 calls from views (#7351)

* v3: fix tests for challenge export

* v3: fallbackto authWithHeaders if wuthWithSession or authWithUrl fails

* Added force cache update when fetching new messages (#7360)

* v3: fetch whole user when booting from group tto avoid issues with pre save hook expecting all data

* v3: misc fixes for payments

* v3: limit fields of challenge tasks that can be updated

* fix(tests): never connect to NODE_DB_URI for tests

* Added new route for setting last cron and updated front end

* v3: fix iap url

* v3: fix build and ios IAP

* Changed route to user set custom day start

* v3: iap accessible under /api/v3, fixes to spells and groups invitations

* v3: correctly use v3 routes in client

* remove XP, GP when unticking a Daily with a completed checklist - fixes https://github.com/HabitRPG/habitrpg/issues/7246

* use natural language for error message about skills on challenge tasks (#7336), fix other gramatical error

* Updated ui when user rejects a guild invite (#7368)

* feat: complete custom day start route

Closes #7363

* fix: Correct spelling of healAll skill

fix: Correct sprite name of healAll skill

* fix: Change all instances of spookDust -> spookySparkles

* add dateCreated to all tasks; add empty challenge object to tasks that don't have one (#7386)

* add plumilla to artists for Tangle Tree in Bailey message

* Fixed quest drop modal (#7377)

* Fixed quest drop modal

* Fixed broken party test

* [API v3] Maintenance Mode (#7367)

* WIP(maintenance): maintenance

* WIP(maintenance): working locale features

* fix(maintenance): don't translate info page target

* WIP(maintenance): start adding info page

* fix(maintenance): linting

* feat: Add container to maintenance info page

* fix(maintenance): add config.json edits
Also DRY variables for main vs info pages

* fix(maintenance): linting

* refactor(maintenance): further slim down variables

* refactor: Remove unnecessary variables

* fix: Correct string interpolation in maintenace view

* feat: Dynamically add time to maintenance pages

* maintenance mode: do not connect to mongodb

* fix(maintenance): clean up timezones etc.

* fix(maintenance): remove unneeded sprite

* Tavern party challenges invites fix (#7394)

* Added challenges and invitations to party

* Loaded tavern challenges

* Updated group and quest services tests

* v3: implement automatic syncing if user is not up to date

* Removed unnecessary fields when updating groups and challenges (#7395)

* v3: do not saved populated user

* v3: correctly return user subset

* Chained party promises together (#7396)

* v3: $w -> splitWhitespace

* use bluebird

* use babel polyfill

* migration: fix items

* update links for v3

* Updated shortname validation to support multiple browsers

* Docs changes (#7401)

* chore: Clarify transfer-gems documentation

* chore: Clarify api status route documentation

* chore: Mark webhooks as BETA

* Added tags update route. Added sort to user service (#7381)

* Added tags update route. Added sort to user service

* Change update tasks route to reorder tasks

* Fixed linting issue

* Changed params for reorder tags route

* Fixed not found tag and added test

* Added password confirmation when deleteing account (#7402)

* fix production logging

* feat(commit): push

* empty commit

* feat(maintenance): post-downtime news & awards (#7406)

* fix exporting avatar

* second attempt at fixing exporting avatar

* fix production logging

* s3: convert moment to date instance

* fix avatar sharing and caching (30 minutes)

* fix: Correct missing parameter

Closes #7433

* fix: Validate challenge shortname on server

* adjust text strings - fixes https://github.com/HabitRPG/habitrpg/issues/5631 and also Short Name -> Tag Name
This commit is contained in:
Matteo Pagliazzi
2016-05-23 13:58:31 +02:00
parent ef3a2fc286
commit 28f2e9c356
993 changed files with 44888 additions and 12883 deletions
+32 -11
View File
@@ -1,20 +1,41 @@
import _ from 'lodash';
import i18n from '../i18n';
import {
BadRequest,
NotAuthorized,
} from '../libs/errors';
// TODO move to server code
module.exports = function addPushDevice (user, req = {}) {
let regId = _.get(req, 'body.regId');
if (!regId) throw new BadRequest(i18n.t('regIdRequired', req.language));
let type = _.get(req, 'body.type');
if (!type) throw new BadRequest(i18n.t('typeRequired', req.language));
module.exports = function(user, req, cb) {
var i, item, pd;
if (!user.pushDevices) {
user.pushDevices = [];
}
pd = user.pushDevices;
item = {
regId: req.body.regId,
type: req.body.type
let pushDevices = user.pushDevices;
let item = {
regId,
type,
};
i = _.findIndex(pd, {
regId: item.regId
let indexOfPushDevice = _.findIndex(pushDevices, {
regId: item.regId,
});
if (i === -1) {
pd.push(item);
if (indexOfPushDevice !== -1) {
throw new NotAuthorized(i18n.t('pushDeviceAlreadyAdded', req.language));
}
return typeof cb === "function" ? cb(null, user.pushDevices) : void 0;
pushDevices.push(item);
return [
user.pushDevices,
i18n.t('pushDeviceAdded', req.language),
];
};
+9 -4
View File
@@ -1,12 +1,17 @@
import uuid from '../libs/uuid';
import _ from 'lodash';
module.exports = function(user, req, cb) {
if (user.tags == null) {
// TODO used only in client, move there?
module.exports = function addTag (user, req = {}) {
if (!user.tags) {
user.tags = [];
}
user.tags.push({
name: req.body.name,
id: req.body.id || uuid()
id: _.get(req, 'body.id') || uuid(),
});
return typeof cb === "function" ? cb(null, user.tags) : void 0;
return user.tags;
};
+10 -14
View File
@@ -1,27 +1,23 @@
import taskDefaults from '../libs/taskDefaults';
import i18n from '../i18n';
module.exports = function(user, req, cb) {
var task;
task = 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);
// TODO move to client since it's only used there?
module.exports = function addTask (user, req = {body: {}}) {
let task = taskDefaults(req.body);
user.tasksOrder[`${task.type}s`].unshift(task._id);
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;
};
+23 -11
View File
@@ -1,15 +1,27 @@
import refPush from '../libs/refPush';
import validator from 'validator';
import i18n from '../i18n';
import {
BadRequest,
} from '../libs/errors';
import _ from 'lodash';
module.exports = function(user, req, cb) {
var wh;
wh = user.preferences.webhooks;
refPush(wh, {
url: req.body.url,
enabled: req.body.enabled || true,
id: req.body.id
});
if (typeof user.markModified === "function") {
user.markModified('preferences.webhooks');
module.exports = function addWebhook (user, req = {}) {
let wh = user.preferences.webhooks;
if (!validator.isURL(_.get(req, 'body.url'))) throw new BadRequest(i18n.t('invalidUrl', req.language));
if (!validator.isBoolean(_.get(req, 'body.enabled'))) throw new BadRequest(i18n.t('invalidEnabled', req.language));
user.markModified('preferences.webhooks');
if (req.v2 === true) {
return user.preferences.webhooks;
} else {
return [
refPush(wh, {
url: req.body.url,
enabled: req.body.enabled,
}),
];
}
return typeof cb === "function" ? cb(null, user.preferences.webhooks) : void 0;
};
+21 -5
View File
@@ -1,15 +1,31 @@
import _ from 'lodash';
import splitWhitespace from '../libs/splitWhitespace';
import {
ATTRIBUTES,
} from '../constants';
import {
BadRequest,
NotAuthorized,
} from '../libs/errors';
import i18n from '../i18n';
module.exports = function allocate (user, req = {}) {
let stat = _.get(req, 'query.stat', 'str');
if (ATTRIBUTES.indexOf(stat) === -1) {
throw new BadRequest(i18n.t('invalidAttribute', {attr: stat}, req.language));
}
module.exports = function(user, 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++;
}
} else {
throw new NotAuthorized(i18n.t('notEnoughAttrPoints', req.language));
}
return typeof cb === "function" ? cb(null, _.pick(user, splitWhitespace('stats'))) : void 0;
return [
user.stats,
];
};
+10 -5
View File
@@ -1,10 +1,15 @@
import _ from 'lodash';
import autoAllocate from '../fns/autoAllocate';
module.exports = function(user, req, cb) {
_.times(user.stats.points, user.fns.autoAllocate);
module.exports = function allocateNow (user, req = {}) {
_.times(user.stats.points, () => autoAllocate(user));
user.stats.points = 0;
if (typeof user.markModified === "function") {
user.markModified('stats');
if (req.v2 === true) {
return _.pick(user, 'stats');
} else {
return [
user.stats,
];
}
return typeof cb === "function" ? cb(null, user.stats) : void 0;
};
+18 -10
View File
@@ -1,13 +1,21 @@
module.exports = function(user, req, cb) {
var i;
i = user.inbox.blocks.indexOf(req.params.uuid);
if (~i) {
user.inbox.blocks.splice(i, 1);
} else {
import validator from 'validator';
import i18n from '../i18n';
import {
BadRequest,
} from '../libs/errors';
module.exports = function blockUser (user, req = {}) {
if (!validator.isUUID(req.params.uuid)) throw new BadRequest(i18n.t('invalidUUID', req.language));
let i = user.inbox.blocks.indexOf(req.params.uuid);
if (i === -1) {
user.inbox.blocks.push(req.params.uuid);
} else {
user.inbox.blocks.splice(i, 1);
}
if (typeof user.markModified === "function") {
user.markModified('inbox.blocks');
}
return typeof cb === "function" ? cb(null, user.inbox.blocks) : void 0;
user.markModified('inbox.blocks');
return [
user.inbox.blocks,
];
};
+18 -113
View File
@@ -1,119 +1,24 @@
import content from '../content/index';
import i18n from '../i18n';
import _ from 'lodash';
import count from '../count';
import splitWhitespace from '../libs/splitWhitespace';
import {
BadRequest,
} from '../libs/errors';
import buyHealthPotion from './buyHealthPotion';
import buyArmoire from './buyArmoire';
import buyGear from './buyGear';
module.exports = function(user, 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 (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
};
}
module.exports = function buy (user, req = {}, analytics) {
let key = _.get(req, 'params.key');
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
let buyRes;
if (key === 'potion') {
buyRes = buyHealthPotion(user, req, analytics);
} else if (key === 'armoire') {
buyRes = buyArmoire(user, req, analytics);
} 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();
}
buyRes = buyGear(user, req, analytics);
}
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, splitWhitespace('items achievements stats flags'));
if (armoireResp) {
buyResp["armoire"] = armoireResp;
}
return typeof cb === "function" ? cb({
code: 200,
message: message
}, buyResp) : void 0;
return buyRes;
};
+115
View File
@@ -0,0 +1,115 @@
import content from '../content/index';
import i18n from '../i18n';
import _ from 'lodash';
import count from '../count';
import splitWhitespace from '../libs/splitWhitespace';
import {
NotAuthorized,
} from '../libs/errors';
import predictableRandom from '../fns/predictableRandom';
import randomVal from '../fns/randomVal';
module.exports = function buyArmoire (user, req = {}, analytics) {
let item = content.armoire;
if (user.stats.gp < item.value) {
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
if (item.canOwn && !item.canOwn(user)) {
throw new NotAuthorized(i18n.t('cannotBuyItem', req.language));
}
let armoireResp;
let armoireResult;
let eligibleEquipment;
let drop;
let message;
armoireResult = predictableRandom(user, user.stats.gp);
eligibleEquipment = _.filter(content.gear.flat, (eligible) => {
return eligible.klass === 'armoire' && !user.items.gear.owned[eligible.key];
});
if (!_.isEmpty(eligibleEquipment) && (armoireResult < 0.6 || !user.flags.armoireOpened)) {
eligibleEquipment.sort();
drop = randomVal(user, eligibleEquipment);
if (user.items.gear.owned[drop.key]) {
throw new NotAuthorized(i18n.t('equipmentAlradyOwned', req.language));
}
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 (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 < 0.8) || armoireResult < 0.5) { // eslint-disable-line no-extra-parens
drop = randomVal(user, _.where(content.food, {
canDrop: true,
}));
user.items.food[drop.key] = user.items.food[drop.key] || 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 {
let armoireExp = Math.floor(predictableRandom(user, user.stats.exp) * 40 + 10);
user.stats.exp += armoireExp;
message = i18n.t('armoireExp', req.language);
armoireResp = {
type: 'experience',
value: armoireExp,
};
}
user.stats.gp -= item.value;
if (!message) {
message = i18n.t('messageBought', {
itemText: item.text(req.language),
}, req.language);
}
if (analytics) {
analytics.track('acquire item', {
uuid: user._id,
itemKey: 'Armoire',
acquireMethod: 'Gold',
goldCost: item.value,
category: 'behavior',
});
}
let resData = _.pick(user, splitWhitespace('items flags'));
if (armoireResp) resData.armoire = armoireResp;
if (req.v2 === true) {
return resData;
} else {
return [
resData,
message,
];
}
};
+70
View File
@@ -0,0 +1,70 @@
import content from '../content/index';
import i18n from '../i18n';
import _ from 'lodash';
import splitWhitespace from '../libs/splitWhitespace';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../libs/errors';
import handleTwoHanded from '../fns/handleTwoHanded';
import ultimateGear from '../fns/ultimateGear';
module.exports = function buyGear (user, req = {}, analytics) {
let key = _.get(req, 'params.key');
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
let item = content.gear.flat[key];
if (!item) throw new NotFound(i18n.t('itemNotFound', {key}, req.language));
if (user.stats.gp < item.value) {
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
if (item.canOwn && !item.canOwn(user)) {
throw new NotAuthorized(i18n.t('cannotBuyItem', req.language));
}
let message;
if (user.items.gear.owned[item.key]) {
throw new NotAuthorized(i18n.t('equipmentAlreadyOwned', req.language));
}
if (user.preferences.autoEquip) {
user.items.gear.equipped[item.type] = item.key;
message = handleTwoHanded(user, item, undefined, req);
}
user.items.gear.owned[item.key] = true;
if (item.last) ultimateGear(user);
user.stats.gp -= item.value;
if (!message) {
message = i18n.t('messageBought', {
itemText: item.text(req.language),
}, req.language);
}
if (analytics) {
analytics.track('acquire item', {
uuid: user._id,
itemKey: key,
acquireMethod: 'Gold',
goldCost: item.value,
category: 'behavior',
});
}
if (req.v2 === true) {
return _.pick(user, splitWhitespace('items achievements stats flags'));
} else {
return [
_.pick(user, splitWhitespace('items achievements stats flags')),
message,
];
}
};
+48
View File
@@ -0,0 +1,48 @@
import content from '../content/index';
import i18n from '../i18n';
import {
NotAuthorized,
} from '../libs/errors';
module.exports = function buyHealthPotion (user, req = {}, analytics) {
let item = content.potion;
if (user.stats.gp < item.value) {
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
if (item.canOwn && !item.canOwn(user)) {
throw new NotAuthorized(i18n.t('cannotBuyItem', req.language));
}
user.stats.hp += 15;
if (user.stats.hp > 50) {
user.stats.hp = 50;
}
user.stats.gp -= item.value;
let message = i18n.t('messageBought', {
itemText: item.text(req.language),
}, req.language);
if (analytics) {
analytics.track('acquire item', {
uuid: user._id,
itemKey: 'Potion',
acquireMethod: 'Gold',
goldCost: item.value,
category: 'behavior',
});
}
if (req.v2 === true) {
return user.stats;
} else {
return [
user.stats,
message,
];
}
};
+43 -31
View File
@@ -2,42 +2,54 @@ import i18n from '../i18n';
import content from '../content/index';
import _ from 'lodash';
import splitWhitespace from '../libs/splitWhitespace';
import pickDeep from '../libs/pickDeep';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../libs/errors';
module.exports = function buyMysterySet (user, req = {}, analytics) {
let key = _.get(req, 'params.key');
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
module.exports = function(user, 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;
}
throw new NotAuthorized(i18n.t('notEnoughHourglasses', req.language));
}
let ref = content.timeTravelerStore(user.items.gear.owned);
let mysterySet = ref ? ref[key] : undefined;
if (!mysterySet) {
return typeof cb === "function" ? cb({
code: 404,
message: "Mystery set not found, or set already owned"
}) : void 0;
throw new NotFound(i18n.t('mysterySetNotFound', req.language));
}
_.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;
if (typeof window !== 'undefined' && window.confirm) { // TODO move to client
if (!window.confirm(i18n.t('hourglassBuyEquipSetConfirm'))) return;
}
_.each(mysterySet.items, item => {
user.items.gear.owned[item.key] = true;
if (analytics) {
analytics.track('acquire item', {
uuid: user._id,
itemKey: item.key,
itemType: 'Subscriber Gear',
acquireMethod: 'Hourglass',
category: 'behavior',
});
}
});
user.purchased.plan.consecutive.trinkets--;
return typeof cb === "function" ? cb({
code: 200,
message: i18n.t('hourglassPurchaseSet', req.language)
}, _.pick(user, splitWhitespace('items purchased.plan.consecutive'))) : void 0;
if (req.v2 === true) {
return pickDeep(user, splitWhitespace('items purchased.plan.consecutive'));
} else {
return [
{ items: user.items, purchasedPlanConsecutive: user.purchased.plan.consecutive },
i18n.t('hourglassPurchaseSet', req.language),
];
}
};
+40 -39
View File
@@ -1,49 +1,50 @@
import i18n from '../i18n';
import content from '../content/index';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../libs/errors';
import _ from 'lodash';
// buy a quest with gold
module.exports = function buyQuest (user, req = {}, analytics) {
let key = _.get(req, 'params.key');
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
let item = content.quests[key];
if (!item) throw new NotFound(i18n.t('questNotFound', {key}, req.language));
module.exports = function(user, 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;
throw new NotAuthorized(i18n.t('questNotGoldPurchasable', {key}, req.language));
}
if (user.stats.gp < item.goldValue) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('messageNotEnoughGold', req.language)
}) : void 0;
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
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.items.quests[item.key] = user.items.quests[item.key] || 0;
user.items.quests[item.key]++;
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);
if (analytics) {
analytics.track('acquire item', {
uuid: user._id,
itemKey: item.key,
itemType: 'Market',
goldCost: item.goldValue,
acquireMethod: 'Gold',
category: 'behavior',
});
}
if (req.v2 === true) {
return user.items.quests;
} else {
return [
user.items.quests,
i18n.t('messageBought', {
itemText: item.text(req.language),
}, req.language),
];
}
return typeof cb === "function" ? cb({
code: 200,
message: message
}, user.items.quests) : void 0;
};
+24 -20
View File
@@ -2,30 +2,34 @@ import i18n from '../i18n';
import content from '../content/index';
import _ from 'lodash';
import splitWhitespace from '../libs/splitWhitespace';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../libs/errors';
module.exports = function buySpecialSpell (user, req = {}) {
let key = _.get(req, 'params.key');
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
let item = content.special[key];
if (!item) throw new NotFound(i18n.t('spellNotFound', {spellId: key}, req.language));
module.exports = function(user, 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;
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
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');
if (req.v2 === true) {
return _.pick(user, splitWhitespace('items stats'));
} else {
return [
_.pick(user, splitWhitespace('items stats')),
i18n.t('messageBought', {
itemText: item.text(req.language),
}, req.language),
];
}
message = i18n.t('messageBought', {
itemText: item.text(req.language)
}, req.language);
return typeof cb === "function" ? cb({
code: 200,
message: message
}, _.pick(user, splitWhitespace('items stats'))) : void 0;
};
+59 -40
View File
@@ -2,58 +2,77 @@ import i18n from '../i18n';
import _ from 'lodash';
import splitWhitespace from '../libs/splitWhitespace';
import { capByLevel } from '../statHelpers';
import {
NotAuthorized,
} from '../libs/errors';
module.exports = function(user, 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;
module.exports = function changeClass (user, req = {}, analytics) {
let klass = _.get(req, 'query.class');
// user.flags.classSelected is set to false after the user paid the 3 gems
if (user.stats.lvl < 10) {
throw new NotAuthorized(i18n.t('lvl10ChangeClass', req.language));
} else if (!user.flags.classSelected && (klass === 'warrior' || klass === 'rogue' || klass === 'wizard' || klass === 'healer')) {
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;
_.each(['weapon', 'armor', 'shield', 'head'], (type) => {
let foundKey = false;
_.findLast(user.items.gear.owned, (val, key) => {
if (key.indexOf(`${type}_${klass}`) !== -1 && val === true) {
foundKey = key;
return true;
}
});
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;
if (!foundKey) {
if (type === 'weapon') {
foundKey = `weapon_${klass}_0`;
} else if (type === 'shield' && klass === 'rogue') {
foundKey = 'shield_rogue_0';
} else {
foundKey = `${type}_base_0`;
}
}
user.items.gear.equipped[type] = foundKey;
if (type === 'weapon' || (type === 'shield' && klass === 'rogue')) { // eslint-disable-line no-extra-parens
user.items.gear.owned[`${type}_${klass}_0`] = true;
}
return true;
});
if (analytics) {
analytics.track('change class', {
uuid: user._id,
class: klass,
acquireMethod: 'Gems',
gemCost: 3,
category: 'behavior',
});
}
} 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;
if (user.balance < 0.75) throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
user.balance -= 0.75;
}
_.merge(user.stats, {
str: 0,
con: 0,
per: 0,
int: 0,
points: capByLevel(user.stats.lvl)
});
user.stats.str = 0;
user.stats.con = 0;
user.stats.per = 0;
user.stats.int = 0;
user.stats.points = capByLevel(user.stats.lvl);
user.flags.classSelected = false;
}
return typeof cb === "function" ? cb(null, _.pick(user, splitWhitespace('stats flags items preferences'))) : void 0;
if (req.v2 === true) {
return _.pick(user, splitWhitespace('stats flags items preferences'));
} else {
return [
_.pick(user, splitWhitespace('stats flags items preferences')),
];
}
};
+6 -8
View File
@@ -1,12 +1,10 @@
import _ from 'lodash';
module.exports = function(user, req, cb) {
_.remove(user.todos, function(t) {
var ref;
return t.completed && !((ref = t.challenge) != null ? ref.id : void 0);
// TODO move to client since it's only used there?
// TODO rename file to clearCompletedTodos
module.exports = function clearCompletedTodos (todos) {
_.remove(todos, todo => {
return todo.completed && (!todo.challenge || !todo.challenge.id || todo.challenge.broken);
});
if (typeof user.markModified === "function") {
user.markModified('todos');
}
return typeof cb === "function" ? cb(null, user.todos) : void 0;
};
+5 -5
View File
@@ -1,7 +1,7 @@
module.exports = function(user, req, cb) {
module.exports = function clearPMs (user) {
user.inbox.messages = {};
if (typeof user.markModified === "function") {
user.markModified('inbox.messages');
}
return typeof cb === "function" ? cb(null, user.inbox.messages) : void 0;
user.markModified('inbox.messages');
return [
user.inbox.messages,
];
};
+8 -6
View File
@@ -1,7 +1,9 @@
module.exports = function(user, 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;
import _ from 'lodash';
module.exports = function deletePM (user, req = {}) {
delete user.inbox.messages[_.get(req, 'params.id')];
user.markModified(`inbox.messages.${req.params.id}`);
return [
user.inbox.messages,
];
};
+22 -16
View File
@@ -1,26 +1,32 @@
import i18n from '../i18n';
import _ from 'lodash';
import { NotFound } from '../libs/errors';
module.exports = function(user, req, cb) {
var i, tag, tid;
tid = req.params.id;
i = _.findIndex(user.tags, {
id: tid
// TODO used only in client, move there?
module.exports = function deleteTag (user, req = {}) {
let tid = _.get(req, 'params.id');
let index = _.findIndex(user.tags, {
id: tid,
});
if (!~i) {
return typeof cb === "function" ? cb({
code: 404,
message: i18n.t('messageTagNotFound', req.language)
}) : void 0;
if (index === -1) {
throw new NotFound(i18n.t('messageTagNotFound', req.language));
}
tag = user.tags[i];
let tag = user.tags[index];
delete user.filters[tag.id];
user.tags.splice(i, 1);
_.each(user.tasks, function(task) {
user.tags.splice(index, 1);
_.each(user.tasks, (task) => {
return delete task.tags[tag.id];
});
_.each(['habits', 'dailys', 'todos', 'rewards'], function(type) {
return typeof user.markModified === "function" ? user.markModified(type) : void 0;
_.each(['habits', 'dailys', 'todos', 'rewards'], (type) => {
user.markModified(type);
});
return typeof cb === "function" ? cb(null, user.tags) : void 0;
return user.tags;
};
+18 -13
View File
@@ -1,17 +1,22 @@
import i18n from '../i18n';
import { NotFound } from '../libs/errors';
import _ from 'lodash';
module.exports = function(user, 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;
// TODO used only in client, move there?
module.exports = function deleteTask (user, req = {}) {
let tid = _.get(req, 'params.id');
let taskType = _.get(req, 'params.taskType');
let index = _.findIndex(user[`${taskType}s`], function findById (task) {
return task._id === tid;
});
if (index === -1) {
throw new NotFound(i18n.t('messageTaskNotFound', req.language));
}
i = user[task.type + "s"].indexOf(task);
if (~i) {
user[task.type + "s"].splice(i, 1);
}
return typeof cb === "function" ? cb(null, {}) : void 0;
user[`${taskType}s`].splice(index, 1);
return {};
};
+9 -6
View File
@@ -1,7 +1,10 @@
module.exports = function(user, 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;
import _ from 'lodash';
module.exports = function deleteWebhook (user, req) {
delete user.preferences.webhooks[_.get(req, 'params.id')];
user.markModified('preferences.webhooks');
return [
user.preferences.webhooks,
];
};
+10 -3
View File
@@ -2,12 +2,19 @@ import splitWhitespace from '../libs/splitWhitespace';
import { capByLevel } from '../statHelpers';
import _ from 'lodash';
module.exports = function(user, req, cb) {
user.stats["class"] = 'warrior';
module.exports = function disableClasses (user, req = {}) {
user.stats.class = 'warrior';
user.flags.classSelected = true;
user.preferences.disableClasses = true;
user.preferences.autoAllocate = true;
user.stats.str = capByLevel(user.stats.lvl);
user.stats.points = 0;
return typeof cb === "function" ? cb(null, _.pick(user, splitWhitespace('stats flags preferences'))) : void 0;
if (req.v2 === true) {
return _.pick(user, splitWhitespace('stats flags preferences'));
} else {
return [
_.pick(user, splitWhitespace('stats flags preferences')),
];
}
};
+45 -29
View File
@@ -1,52 +1,68 @@
import content from '../content/index';
import i18n from '../i18n';
import handleTwoHanded from '../fns/handleTwoHanded';
import {
NotFound,
BadRequest,
} from '../libs/errors';
import _ from 'lodash';
module.exports = function equip (user, req = {}) {
// Being type a parameter followed by another parameter
// when using the API it must be passes specifically in the URL, it's won't default to equipped
let type = _.get(req, 'params.type', 'equipped');
let key = _.get(req, 'params.key');
if (!key || !type) throw new BadRequest(i18n.t('missingTypeKeyEquip', req.language));
if (['mount', 'pet', 'costume', 'equipped'].indexOf(type) === -1) {
throw new BadRequest(i18n.t('invalidTypeEquip', req.language));
}
let message;
module.exports = function(user, 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':
case 'mount': {
if (!user.items.mounts[key]) {
return typeof cb === "function" ? cb({
code: 404,
message: ":You do not own this mount."
}) : void 0;
throw new NotFound(i18n.t('mountNotOwned', req.language));
}
user.items.currentMount = user.items.currentMount === key ? '' : key;
break;
case 'pet':
}
case 'pet': {
if (!user.items.pets[key]) {
return typeof cb === "function" ? cb({
code: 404,
message: ":You do not own this pet."
}) : void 0;
throw new NotFound(i18n.t('petNotOwned', req.language));
}
user.items.currentPet = user.items.currentPet === key ? '' : key;
break;
}
case 'costume':
case 'equipped':
item = content.gear.flat[key];
case 'equipped': {
if (!user.items.gear.owned[key]) {
return typeof cb === "function" ? cb({
code: 404,
message: ":You do not own this gear."
}) : void 0;
throw new NotFound(i18n.t('gearNotOwned', req.language));
}
let item = content.gear.flat[key];
if (user.items.gear[type][item.type] === key) {
user.items.gear[type][item.type] = item.type + "_base_0";
user.items.gear[type][item.type] = `${item.type}_base_0`;
message = i18n.t('messageUnEquipped', {
itemText: item.text(req.language)
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);
message = handleTwoHanded(user, item, type, req);
}
break;
}
}
if (req.v2 === true) {
return user.items;
} else {
let res = [user.items];
if (message) res.push(message);
return res;
}
return typeof cb === "function" ? cb((message ? {
code: 200,
message: message
} : null), user.items) : void 0;
};
+74 -50
View File
@@ -1,78 +1,102 @@
import content from '../content/index';
import i18n from '../i18n';
import _ from 'lodash';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../libs/errors';
function evolve (user, pet, petDisplayName, req) {
user.items.pets[pet] = -1;
user.items.mounts[pet] = true;
if (pet === user.items.currentPet) {
user.items.currentPet = '';
}
return i18n.t('messageEvolve', {
egg: petDisplayName,
}, req.language);
}
module.exports = function feed (user, req = {}) {
let pet = _.get(req, 'params.pet');
let foodK = _.get(req, 'params.food');
if (!pet || !foodK) throw new BadRequest(i18n.t('missingPetFoodFeed', req.language));
if (pet.indexOf('-') === -1) {
throw new BadRequest(i18n.t('invalidPetName', req.language));
}
let food = content.food[foodK];
if (!food) {
throw new NotFound(i18n.t('messageFoodNotFound', req.language));
}
let userPets = user.items.pets;
module.exports = function(user, 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;
throw new NotFound(i18n.t('messagePetNotFound', req.language));
}
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;
let [egg, potion] = pet.split('-');
let potionText = content.hatchingPotions[potion] ? content.hatchingPotions[potion].text(req.language) : potion;
let eggText = content.eggs[egg] ? content.eggs[egg].text(req.language) : egg;
let petDisplayName = i18n.t('petName', {
potion: potionText,
egg: eggText,
}, req.language);
if (!user.items.food[food.key]) {
throw new NotFound(i18n.t('messageFoodNotFound', req.language));
}
if (content.specialPets[pet]) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('messageCannotFeedPet', req.language)
}) : void 0;
throw new NotAuthorized(i18n.t('messageCannotFeedPet', req.language));
}
if (user.items.mounts[pet]) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('messageAlreadyMount', req.language)
}) : void 0;
throw new NotAuthorized(i18n.t('messageAlreadyMount', req.language));
}
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);
};
let message;
if (food.key === 'Saddle') {
evolve();
message = evolve(user, pet, petDisplayName, req);
} else {
if (food.target === potion || content.hatchingPotions[potion].premium) {
userPets[pet] += 5;
message = i18n.t('messageLikesFood', {
egg: petDisplayName,
foodText: food.text(req.language)
foodText: food.text(req.language),
}, req.language);
} else {
userPets[pet] += 2;
message = i18n.t('messageDontEnjoyFood', {
egg: petDisplayName,
foodText: food.text(req.language)
foodText: food.text(req.language),
}, req.language);
}
if (userPets[pet] >= 50 && !user.items.mounts[pet]) {
evolve();
message = evolve(user, pet, petDisplayName, req);
}
}
user.items.food[food.key]--;
return typeof cb === "function" ? cb({
code: 200,
message: message
}, {
value: userPets[pet]
}) : void 0;
if (req.v2 === true) {
return {
value: userPets[pet],
};
} else {
return [
userPets[pet],
message,
];
}
};
+12 -11
View File
@@ -1,17 +1,18 @@
import _ from 'lodash';
import i18n from '../i18n';
import { NotFound } from '../libs/errors';
module.exports = function(user, req, cb) {
var i, tid;
tid = req.params.id;
i = _.findIndex(user.tags, {
id: tid
// TODO used only in client, move there?
module.exports = function getTag (user, req = {}) {
let tid = _.get(req, 'params.id');
let index = _.findIndex(user.tags, {
id: tid,
});
if (!~i) {
return typeof cb === "function" ? cb({
code: 404,
message: i18n.t('messageTagNotFound', req.language)
}) : void 0;
if (index === -1) {
throw new NotFound(i18n.t('messageTagNotFound', req.language));
}
return typeof cb === "function" ? cb(null, user.tags[i]) : void 0;
return user.tags[index];
};
+4 -2
View File
@@ -1,3 +1,5 @@
module.exports = function(user, req, cb) {
return typeof cb === "function" ? cb(null, user.tags) : void 0;
// TODO used only in client, move there?
module.exports = function getTags (user) {
return user.tags;
};
+29 -24
View File
@@ -1,39 +1,44 @@
import content from '../content/index';
import i18n from '../i18n';
import _ from 'lodash';
import {
BadRequest,
NotAuthorized,
NotFound,
} from '../libs/errors';
module.exports = function hatch (user, req = {}) {
let egg = _.get(req, 'params.egg');
let hatchingPotion = _.get(req, 'params.hatchingPotion');
module.exports = function(user, 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;
throw new BadRequest(i18n.t('missingEggHatchingPotionHatch', req.language));
}
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;
throw new NotFound(i18n.t('messageMissingEggPotion', req.language));
}
if (content.hatchingPotions[hatchingPotion].premium && !content.dropEggs[egg]) {
return typeof cb === "function" ? cb({
code: 403,
message: i18n.t('messageInvalidEggPotionCombo', req.language)
}) : void 0;
throw new BadRequest(i18n.t('messageInvalidEggPotionCombo', req.language));
}
pet = egg + "-" + hatchingPotion;
let 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;
throw new NotAuthorized(i18n.t('messageAlreadyPet', req.language));
}
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;
if (req.v2 === true) {
return user.items;
} else {
return [
user.items,
i18n.t('messageHatched', req.language),
];
}
};
+40 -33
View File
@@ -1,54 +1,61 @@
import content from '../content/index';
import i18n from '../i18n';
import _ from 'lodash';
import {
BadRequest,
NotAuthorized,
} from '../libs/errors';
import splitWhitespace from '../libs/splitWhitespace';
module.exports = function(user, req, cb, analytics) {
var analyticsData, key, ref, type;
ref = req.params, type = ref.type, key = ref.key;
module.exports = function purchaseHourglass (user, req = {}, analytics) {
let key = _.get(req, 'params.key');
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
let type = _.get(req, 'params.type');
if (!type) throw new BadRequest(i18n.t('missingTypeParam', req.language));
if (!content.timeTravelStable[type]) {
return typeof cb === "function" ? cb({
code: 403,
message: i18n.t('typeNotAllowedHourglass', {allowedTypes: _.keys(content.timeTravelStable).toString()}, req.language)
}) : void 0;
throw new NotAuthorized(i18n.t('typeNotAllowedHourglass', {allowedTypes: _.keys(content.timeTravelStable).toString()}, req.language));
}
if (!_.contains(_.keys(content.timeTravelStable[type]), key)) {
return typeof cb === "function" ? cb({
code: 403,
message: i18n.t(type + 'NotAllowedHourglass', req.language)
}) : void 0;
throw new NotAuthorized(i18n.t('notAllowedHourglass', req.language));
}
if (user.items[type][key]) {
return typeof cb === "function" ? cb({
code: 403,
message: i18n.t(type + 'AlreadyOwned', req.language)
}) : void 0;
throw new NotAuthorized(i18n.t(`${type}AlreadyOwned`, req.language));
}
if (!(user.purchased.plan.consecutive.trinkets > 0)) {
return typeof cb === "function" ? cb({
code: 403,
message: i18n.t('notEnoughHourglasses', req.language)
}) : void 0;
if (user.purchased.plan.consecutive.trinkets <= 0) {
throw new NotAuthorized(i18n.t('notEnoughHourglasses', req.language));
}
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);
if (analytics) {
analytics.track('acquire item', {
uuid: user._id,
itemKey: key,
itemType: type,
acquireMethod: 'Hourglass',
category: 'behavior',
});
}
if (req.v2 === true) {
return _.pick(user, splitWhitespace('items purchased.plan.consecutive'));
} else {
return [
{ items: user.items, purchasedPlanConsecutive: user.purchased.plan.consecutive },
i18n.t('hourglassPurchase', req.language),
];
}
return typeof cb === "function" ? cb({
code: 200,
message: i18n.t('hourglassPurchase', req.language)
}, _.pick(user, splitWhitespace('items purchased.plan.consecutive'))) : void 0;
};
+11 -2
View File
@@ -30,6 +30,9 @@ import releasePets from './releasePets';
import releaseMounts from './releaseMounts';
import releaseBoth from './releaseBoth';
import buy from './buy';
import buyGear from './buyGear';
import buyHealthPotion from './buyHealthPotion';
import buyArmoire from './buyArmoire';
import buyQuest from './buyQuest';
import buyMysterySet from './buyMysterySet';
import hourglassPurchase from './hourglassPurchase';
@@ -42,7 +45,9 @@ import disableClasses from './disableClasses';
import allocate from './allocate';
import readCard from './readCard';
import openMysteryItem from './openMysteryItem';
import score from './score';
import scoreTask from './scoreTask';
import markPmsRead from './markPMSRead';
module.exports = {
update,
@@ -77,6 +82,9 @@ module.exports = {
releaseMounts,
releaseBoth,
buy,
buyGear,
buyHealthPotion,
buyArmoire,
buyQuest,
buyMysterySet,
hourglassPurchase,
@@ -89,5 +97,6 @@ module.exports = {
allocate,
readCard,
openMysteryItem,
score,
scoreTask,
markPmsRead,
};
+14
View File
@@ -0,0 +1,14 @@
import i18n from '../i18n';
module.exports = function markPmsRead (user, req = {}) {
user.inbox.newMessages = 0;
if (req.v2 === true) {
return user;
} else {
return [
user.inbox.newMessages,
i18n.t('pmsMarkedRead'),
];
}
};
+35 -23
View File
@@ -1,32 +1,44 @@
import content from '../content/index';
import i18n from '../i18n';
import {
BadRequest,
} from '../libs/errors';
import _ from 'lodash';
module.exports = function openMysteryItem (user, req = {}, analytics) {
let item = user.purchased.plan.mysteryItems.shift();
module.exports = function(user, 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');
throw new BadRequest(i18n.t('mysteryItemIsEmpty', req.language));
}
item = _.cloneDeep(content.gear.flat[item]);
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);
user.items.gear.owned[item.key] = true;
user.markModified('purchased.plan.mysteryItems');
if (analytics) {
analytics.track('open mystery item', {
uuid: user._id,
itemKey: item,
itemType: 'Subscriber Gear',
acquireMethod: 'Subscriber',
category: 'behavior',
});
}
if (typeof window !== 'undefined') {
(user._tmp != null ? user._tmp : user._tmp = {}).drop = item;
if (!user._tmp) user._tmp = {};
user._tmp.drop = item;
}
if (req.v2 === true) {
return user.items.gear.owned;
} else {
return [
user.items.gear.owned,
i18n.t('mysteryItemOpened', req.language),
];
}
return typeof cb === "function" ? cb(null, user.items.gear.owned) : void 0;
};
+89 -69
View File
@@ -3,105 +3,125 @@ import i18n from '../i18n';
import _ from 'lodash';
import splitWhitespace from '../libs/splitWhitespace';
import planGemLimits from '../libs/planGemLimits';
import {
NotFound,
NotAuthorized,
BadRequest,
} from '../libs/errors';
module.exports = function purchase (user, req = {}, analytics) {
let type = _.get(req.params, 'type');
let key = _.get(req.params, 'key');
let item;
let price;
if (!type) {
throw new BadRequest(i18n.t('typeRequired', req.language));
}
if (!key) {
throw new BadRequest(i18n.t('keyRequired', req.language));
}
module.exports = function(user, 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 = planGemLimits, convRate = ref1.convRate, convCap = ref1.convCap;
let convRate = planGemLimits.convRate;
let convCap = planGemLimits.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.purchased || !user.purchased.plan || !user.purchased.plan.customerId) {
throw new NotAuthorized(i18n.t('mustSubscribeToPurchaseGems', req.language));
}
if (!(user.stats.gp >= convRate)) {
return typeof cb === "function" ? cb({
code: 401,
message: "Not enough Gold"
}) : void 0;
if (user.stats.gp < convRate) {
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
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;
throw new NotAuthorized(i18n.t('reachedGoldToGemCap', {convCap}, req.language));
}
user.balance += .25;
user.balance += 0.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);
if (analytics) {
analytics.track('purchase gems', {
uuid: user._id,
itemKey: key,
acquireMethod: 'Gold',
goldCost: convRate,
category: 'behavior',
});
}
return typeof cb === "function" ? cb({
code: 200,
message: "+1 Gem"
}, _.pick(user, splitWhitespace('stats balance'))) : void 0;
return [
_.pick(user, splitWhitespace('stats balance')),
i18n.t('plusOneGem'),
];
}
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;
let acceptedTypes = ['eggs', 'hatchingPotions', 'food', 'quests', 'gear'];
if (acceptedTypes.indexOf(type) === -1) {
throw new NotFound(i18n.t('notAccteptedType', req.language));
}
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;
if (!item) {
throw new NotFound(i18n.t('contentKeyNotFound', {type}, req.language));
}
if (user.items.gear.owned[key]) {
throw new NotAuthorized(i18n.t('alreadyHave', req.language));
}
price = (item.twoHanded || item.gearSet === 'animal' ? 2 : 1) / 4;
} else {
item = content[type][key];
if (!item) {
throw new NotFound(i18n.t('contentKeyNotFound', {type}, req.language));
}
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;
throw new NotAuthorized(i18n.t('messageNotAvailable', req.language));
}
if ((user.balance < price) || !user.balance) {
return typeof cb === "function" ? cb({
code: 403,
message: i18n.t('notEnoughGems', req.language)
}) : void 0;
if (!user.balance || user.balance < price) {
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
user.balance -= price;
if (type === 'gear') {
user.items.gear.owned[key] = true;
} else {
if (!(user.items[type][key] > 0)) {
if (!user.items[type][key] || 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);
if (analytics) {
analytics.track('acquire item', {
uuid: user._id,
itemKey: key,
itemType: 'Market',
acquireMethod: 'Gems',
gemCost: item.value,
category: 'behavior',
});
}
if (req.v2 === true) {
return _.pick(user, splitWhitespace('items balance'));
} else {
return [
_.pick(user, splitWhitespace('items balance')),
];
}
return typeof cb === "function" ? cb(null, _.pick(user, splitWhitespace('items balance'))) : void 0;
};
+29 -7
View File
@@ -1,10 +1,32 @@
module.exports = function(user, 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");
import splitWhitespace from '../libs/splitWhitespace';
import _ from 'lodash';
import i18n from '../i18n';
import {
BadRequest,
NotAuthorized,
} from '../libs/errors';
import content from '../content/index';
module.exports = function readCard (user, req = {}) {
let cardType = _.get(req.params, 'cardType');
if (!cardType) {
throw new BadRequest(i18n.t('cardTypeRequired', req.language));
}
if (_.keys(content.cardTypes).indexOf(cardType) === -1) {
throw new NotAuthorized(i18n.t('cardTypeNotAllowed', req.language));
}
user.items.special[`${cardType}Received`].shift();
user.flags.cardReceived = false;
return typeof cb === "function" ? cb(null, 'items.special flags.cardReceived') : void 0;
if (req.v2 === true) {
return _.pick(user, splitWhitespace('items.special flags.cardReceived'));
} else {
return [
{ specialItems: user.items.special, cardReceived: user.flags.cardReceived },
i18n.t('readCard', {cardType}, req.language),
];
}
};
+56 -50
View File
@@ -1,21 +1,25 @@
import content from '../content/index';
import i18n from '../i18n';
import _ from 'lodash';
import { capByLevel } from '../statHelpers';
import { MAX_LEVEL } from '../constants';
import {
NotAuthorized,
} from '../libs/errors';
import resetGear from '../fns/resetGear';
import equip from './equip';
module.exports = function(user, req, cb, analytics) {
var analyticsData, flags, gear, lvl, stats;
const USERSTATSLIST = ['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp'];
module.exports = function rebirth (user, tasks = [], req = {}, analytics) {
if (user.balance < 2 && user.stats.lvl < MAX_LEVEL) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('notEnoughGems', req.language)
}) : void 0;
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
analyticsData = {
let analyticsData = {
uuid: user._id,
category: 'behavior'
category: 'behavior',
};
if (user.stats.lvl < MAX_LEVEL) {
user.balance -= 2;
analyticsData.acquireMethod = 'Gems';
@@ -24,63 +28,55 @@ module.exports = function(user, req, cb, analytics) {
analyticsData.gemCost = 0;
analyticsData.acquireMethod = '> 100';
}
if (analytics != null) {
if (analytics) {
analytics.track('Rebirth', analyticsData);
}
lvl = capByLevel(user.stats.lvl);
_.each(user.tasks, function(task) {
if (task.type !== 'reward') {
task.value = 0;
}
if (task.type === 'daily') {
return task.streak = 0;
let lvl = capByLevel(user.stats.lvl);
_.each(tasks, function resetTasks (task) {
if (!task.challenge || !task.challenge.id || task.challenge.broken) {
if (task.type !== 'reward') {
task.value = 0;
}
if (task.type === 'daily') {
task.streak = 0;
}
}
});
stats = user.stats;
let 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';
stats.class = 'warrior';
_.each(USERSTATSLIST, function resetStats (value) {
stats[value] = 0;
});
resetGear(user);
if (user.items.currentPet) {
user.ops.equip({
equip(user, {
params: {
type: 'pet',
key: user.items.currentPet
}
key: user.items.currentPet,
},
});
}
if (user.items.currentMount) {
user.ops.equip({
equip(user, {
params: {
type: 'mount',
key: user.items.currentMount
}
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;
let flags = user.flags;
if (!user.achievements.beastMaster) {
flags.rebirthEnabled = false;
}
@@ -88,13 +84,23 @@ module.exports = function(user, req, cb, analytics) {
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) {
} else if (lvl > user.achievements.rebirthLevel || lvl === MAX_LEVEL) {
user.achievements.rebirths++;
user.achievements.rebirthLevel = lvl;
}
user.stats.buffs = {};
return typeof cb === "function" ? cb(null, user) : void 0;
if (req.v2 === true) {
return user;
} else {
return [
{user, tasks},
i18n.t('rebirthComplete'),
];
}
};
+58 -40
View File
@@ -1,50 +1,68 @@
import content from '../content/index';
import i18n from '../i18n';
import {
NotAuthorized,
} from '../libs/errors';
import splitWhitespace from '../libs/splitWhitespace';
import _ from 'lodash';
module.exports = function releaseBoth (user, req = {}, analytics) {
let animal;
module.exports = function(user, req, cb, analytics) {
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 = {
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
let giveTriadBingo = true;
if (!user.achievements.triadBingo) {
if (analytics) {
analytics.track('release pets & mounts', {
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++;
category: 'behavior',
});
}
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++;
}
if (req.v2 === true) {
return user;
} else {
return [
_.pick(user, splitWhitespace('achievements items balance')),
i18n.t('mountsAndPetsReleased'),
];
}
return typeof cb === "function" ? cb(null, user) : void 0;
};
+36 -25
View File
@@ -1,32 +1,43 @@
import content from '../content/index';
import i18n from '../i18n';
import {
NotAuthorized,
} from '../libs/errors';
module.exports = function releaseMounts (user, req = {}, analytics) {
let mount;
module.exports = function(user, 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;
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
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++;
if (analytics) {
analytics.track('release mounts', {
uuid: user._id,
acquireMethod: 'Gems',
gemCost: 4,
category: 'behavior',
});
}
if (req.v2 === true) {
return user;
} 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++;
return [
user.items.mounts,
i18n.t('mountsReleased'),
];
}
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;
};
+34 -25
View File
@@ -1,32 +1,41 @@
import content from '../content/index';
import i18n from '../i18n';
import {
NotAuthorized,
} from '../libs/errors';
module.exports = function(user, req, cb, analytics) {
var analyticsData, pet;
module.exports = function releasePets (user, req = {}, analytics) {
if (user.balance < 1) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('notEnoughGems', req.language)
}) : void 0;
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
user.balance -= 1;
user.items.currentPet = '';
for (let pet in content.pets) {
user.items.pets[pet] = 0;
}
if (!user.achievements.beastMasterCount) {
user.achievements.beastMasterCount = 0;
}
user.achievements.beastMasterCount++;
if (analytics) {
analytics.track('release pets', {
uuid: user._id,
acquireMethod: 'Gems',
gemCost: 4,
category: 'behavior',
});
}
if (req.v2 === true) {
return user;
} 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 = "";
return [
user.items.pets,
i18n.t('petsReleased'),
];
}
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;
};
+30 -19
View File
@@ -1,29 +1,40 @@
import i18n from '../i18n';
import _ from 'lodash';
import {
NotAuthorized,
} from '../libs/errors';
module.exports = function(user, req, cb, analytics) {
var analyticsData;
module.exports = function reroll (user, tasks = [], req = {}, analytics) {
if (user.balance < 1) {
return typeof cb === "function" ? cb({
code: 401,
message: i18n.t('notEnoughGems', req.language)
}) : void 0;
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
user.balance--;
_.each(user.tasks, function(task) {
if (task.type !== 'reward') {
return task.value = 0;
user.stats.hp = 50;
_.each(tasks, function resetTaskValues (task) {
if (!task.challenge || !task.challenge.id || task.challenge.broken) {
if (task.type !== 'reward') {
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);
if (analytics) {
analytics.track('Fortify Potion', {
uuid: user._id,
acquireMethod: 'Gems',
gemCost: 4,
category: 'behavior',
});
}
if (req.v2 === true) {
return user;
} else {
return [
{user, tasks},
i18n.t('fortifyComplete'),
];
}
return typeof cb === "function" ? cb(null, user) : void 0;
};
+20 -26
View File
@@ -1,35 +1,29 @@
import _ from 'lodash';
import resetGear from '../fns/resetGear';
import i18n from '../i18n';
module.exports = function(user, req, cb) {
var gear;
user.habits = [];
user.dailys = [];
user.todos = [];
user.rewards = [];
module.exports = function reset (user, tasks = [], req = {}) {
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;
let tasksToRemove = [];
tasks.forEach(task => {
if (!task.challenge || !task.challenge.id || task.challenge.broken) {
tasksToRemove.push(task._id);
let i = user.tasksOrder[`${task.type}s`].indexOf(task._id);
if (i !== -1) user.tasksOrder[`${task.type}s`].splice(i, 1);
}
return true;
});
gear.owned.weapon_warrior_0 = true;
if (typeof user.markModified === "function") {
user.markModified('items.gear.owned');
resetGear(user);
if (req.v2 === true) {
return user;
} else {
return [
{user, tasksToRemove},
i18n.t('resetComplete'),
];
}
user.preferences.costume = false;
return typeof cb === "function" ? cb(null, user) : void 0;
};
+74 -39
View File
@@ -1,72 +1,107 @@
import content from '../content/index';
import i18n from '../i18n';
import _ from 'lodash';
import {
NotAuthorized,
} from '../libs/errors';
import randomVal from '../fns/randomVal';
module.exports = function(user, 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;
module.exports = function revive (user, req = {}, analytics) {
if (user.stats.hp > 0) {
throw new NotAuthorized(i18n.t('cannotRevive', req.language));
}
_.merge(user.stats, {
hp: 50,
exp: 0,
gp: 0
gp: 0,
});
if (user.stats.lvl > 1) {
user.stats.lvl--;
}
lostStat = user.fns.randomVal(_.reduce(['str', 'con', 'per', 'int'], (function(m, k) {
let lostStat = randomVal(user, _.reduce(['str', 'con', 'per', 'int'], function findRandomStat (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];
let base = user.items.gear.owned;
let gearOwned;
if (typeof base.toObject === 'function') {
gearOwned = base.toObject();
} else {
gearOwned = user.items.gear.owned;
}
let losableItems = {};
let userClass = user.stats.class;
_.each(gearOwned, function findLosableItems (value, key) {
let itm;
if (value) {
itm = content.gear.flat[key];
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;
let itemHasValueOrWarrior0 = itm.value > 0 || key === 'weapon_warrior_0';
let itemClassEqualsUserClass = itm.klass === userClass;
let itemClassSpecial = itm.klass === 'special';
let itemNotSpecialOrUserClassIsSpecial = !itm.specialClass || itm.specialClass === userClass;
let itemIsSpecial = itemNotSpecialOrUserClassIsSpecial && itemClassSpecial;
let itemIsArmoire = itm.klass === 'armoire';
if (itemHasValueOrWarrior0 && (itemClassEqualsUserClass || itemIsSpecial || itemIsArmoire)) {
losableItems[key] = key;
return losableItems[key];
}
}
}
});
lostItem = user.fns.randomVal(losableItems);
if (item = content.gear.flat[lostItem]) {
let lostItem = randomVal(user, losableItems);
let message = '';
let item = content.gear.flat[lostItem];
if (item) {
user.items.gear.owned[lostItem] = false;
if (user.items.gear.equipped[item.type] === lostItem) {
user.items.gear.equipped[item.type] = item.type + "_base_0";
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";
user.items.gear.costume[item.type] = `${item.type}_base_0`;
}
message = i18n.t('messageLostItem', { itemText: item.text(req.language)}, req.language);
}
if (typeof user.markModified === "function") {
user.markModified('items.gear');
if (analytics) {
analytics.track('Death', {
uuid: user._id,
lostItem,
gaLabel: lostItem,
category: 'behavior',
});
}
analyticsData = {
uuid: user._id,
lostItem: lostItem,
gaLabel: lostItem,
category: 'behavior'
};
if (analytics != null) {
analytics.track('Death', analyticsData);
if (req.v2 === true) {
return user;
} else {
return [
user.items,
message,
];
}
return typeof cb === "function" ? cb((item ? {
code: 200,
message: i18n.t('messageLostItem', {
itemText: item.text(req.language)
}, req.language)
} : null), user) : void 0;
};
-221
View File
@@ -1,221 +0,0 @@
import moment from 'moment';
import _ from 'lodash';
import i18n from '../i18n';
module.exports = function(user, 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;
};
+260
View File
@@ -0,0 +1,260 @@
import _ from 'lodash';
import {
NotAuthorized,
} from '../libs/errors';
import i18n from '../i18n';
import updateStats from '../fns/updateStats';
import crit from '../fns/crit';
const MAX_TASK_VALUE = 21.27;
const MIN_TASK_VALUE = -47.27;
const CLOSE_ENOUGH = 0.00001;
function _getTaskValue (taskValue) {
if (taskValue < MIN_TASK_VALUE) {
return MIN_TASK_VALUE;
} else if (taskValue > MAX_TASK_VALUE) {
return MAX_TASK_VALUE;
} else {
return taskValue;
}
}
// Calculates the next task.value based on direction
// Uses a capped inverse log y=.95^x, y>= -5
function _calculateDelta (task, direction, cron) {
// Min/max on task redness
let currVal = _getTaskValue(task.value);
let nextDelta = Math.pow(0.9747, currVal) * (direction === 'down' ? -1 : 1);
// Checklists
if (task.checklist && task.checklist.length > 0) {
// If the Daily, only dock them a portion based on their checklist completion
if (direction === 'down' && task.type === 'daily' && cron) {
nextDelta *= 1 - _.reduce(task.checklist, (m, i) => m + (i.completed ? 1 : 0), 0) / task.checklist.length;
}
// If To-Do, point-match the TD per checklist item completed
if (task.type === 'todo') {
nextDelta *= 1 + _.reduce(task.checklist, (m, i) => m + (i.completed ? 1 : 0), 0);
}
}
return nextDelta;
}
// Approximates the reverse delta for the task value
// This is meant to return the task value to its original value when unchecking a task.
// First, calculate the value using the normal way for our first guess although
// it will be a bit off
function _calculateReverseDelta (task, direction) {
let currVal = _getTaskValue(task.value);
let testVal = currVal + Math.pow(0.9747, currVal) * (direction === 'down' ? -1 : 1);
// Now keep moving closer to the original value until we get "close enough"
// Check how close we are to the original value by computing the delta off our guess
// and looking at the difference between that and our current value.
while (true) { // eslint-disable-line no-constant-condition
let calc = testVal + Math.pow(0.9747, testVal);
let diff = currVal - calc;
if (Math.abs(diff) < CLOSE_ENOUGH) break;
if (diff > 0) {
testVal -= diff;
} else {
testVal += diff;
}
}
// When we get close enough, return the difference between our approximated value
// and the current value. This will be the delta calculated from the original value
// before the task was checked.
let nextDelta = testVal - currVal;
// Checklists - If To-Do, point-match the TD per checklist item completed
if (task.checklist && task.checklist.length > 0 && task.type === 'todo') {
nextDelta *= 1 + _.reduce(task.checklist, (m, i) => m + (i.completed ? 1 : 0), 0);
}
return nextDelta;
}
function _gainMP (user, val) {
val *= user._tmp.crit || 1;
user.stats.mp += val;
if (user.stats.mp >= user._statsComputed.maxMP) user.stats.mp = user._statsComputed.maxMP;
if (user.stats.mp < 0) {
user.stats.mp = 0;
}
}
// HP modifier
// ===== CONSTITUTION =====
// TODO Decreases HP loss from bad habits / missed dailies by 0.5% per point.
function _subtractPoints (user, task, stats, delta) {
let conBonus = 1 - user._statsComputed.con / 250;
if (conBonus < 0.1) conBonus = 0.1;
let hpMod = delta * conBonus * task.priority * 2; // constant 2 multiplier for better results
stats.hp += Math.round(hpMod * 10) / 10; // round to 1dp
return stats.hp;
}
function _addPoints (user, task, stats, direction, delta) {
// ===== CRITICAL HITS =====
// allow critical hit only when checking off a task, not when unchecking it:
let _crit = delta > 0 ? crit(user) : 1;
// if there was a crit, alert the user via notification
if (_crit > 1) user._tmp.crit = _crit;
// Exp Modifier
// ===== Intelligence =====
// TODO Increases Experience gain by .2% per point.
let intBonus = 1 + user._statsComputed.int * 0.025;
stats.exp += Math.round(delta * intBonus * task.priority * _crit * 6);
// GP modifier
// ===== PERCEPTION =====
// TODO Increases Gold gained from tasks by .3% per point.
let perBonus = 1 + user._statsComputed.per * 0.02;
let gpMod = delta * task.priority * _crit * perBonus;
if (task.streak) {
let currStreak = direction === 'down' ? task.streak - 1 : task.streak;
let streakBonus = currStreak / 100 + 1; // eg, 1-day streak is 1.01, 2-day is 1.02, etc
let afterStreak = gpMod * streakBonus;
if (currStreak > 0 && gpMod > 0) {
user._tmp.streakBonus = afterStreak - gpMod; // keep this on-hand for later, so we can notify streak-bonus
}
stats.gp += afterStreak;
} else {
stats.gp += gpMod;
}
}
function _changeTaskValue (user, task, direction, times, cron) {
let addToDelta = 0;
// If multiple days have passed, multiply times days missed
_.times(times, () => {
// Each iteration calculate the nextDelta, which is then accumulated in the total delta.
let nextDelta = !cron && direction === 'down' ? _calculateReverseDelta(task, direction) : _calculateDelta(task, direction, cron);
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') { // Make progress on quest based on STR
user.party.quest.progress.up = user.party.quest.progress.up || 0;
if (task.type === 'todo' || task.type === 'daily') {
user.party.quest.progress.up += nextDelta * (1 + user._statsComputed.str / 200);
} else if (task.type === 'habit') {
user.party.quest.progress.up += nextDelta * (0.5 + user._statsComputed.str / 400);
}
}
task.value += nextDelta;
}
addToDelta += nextDelta;
});
return addToDelta;
}
module.exports = function scoreTask (options = {}, req = {}) {
let {user, task, direction, times = 1, cron = false} = options;
let delta = 0;
let stats = {
gp: user.stats.gp,
hp: user.stats.hp,
exp: user.stats.exp,
};
// This is for setting one-time temporary flags, such as streakBonus or itemDropped. Useful for notifying
// the API consumer, then cleared afterwards
user._tmp = {};
// If they're trying to purchase a too-expensive reward, don't allow them to do that.
if (task.value > user.stats.gp && task.type === 'reward') throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
if (task.type === 'habit') {
delta += _changeTaskValue(user, task, direction, times, cron);
// Add habit value to habit-history (if different)
if (delta > 0) {
_addPoints(user, task, stats, direction, delta);
} else {
_subtractPoints(user, task, stats, delta);
}
_gainMP(user, _.max([0.25, 0.0025 * user._statsComputed.maxMP]) * (direction === 'down' ? -1 : 1));
task.history = task.history || [];
// Add history entry, even more than 1 per day
task.history.push({
date: Number(new Date()),
value: task.value,
});
} else if (task.type === 'daily') {
if (cron) {
delta += _changeTaskValue(user, task, direction, times, cron);
_subtractPoints(user, task, stats, delta);
if (!user.stats.buffs.streaks) task.streak = 0;
} else {
delta += _changeTaskValue(user, task, direction, times, cron);
if (direction === 'down') delta = _calculateDelta(task, direction, cron); // recalculate delta for unchecking so the gp and exp come out correctly
_addPoints(user, task, stats, direction, delta); // obviously for delta>0, but also a trick to undo accidental checkboxes
_gainMP(user, _.max([1, 0.01 * user._statsComputed.maxMP]) * (direction === 'down' ? -1 : 1));
if (direction === 'up') {
task.streak += 1;
// Give a streak achievement when the streak is a multiple of 21
if (task.streak % 21 === 0) user.achievements.streak = user.achievements.streak ? user.achievements.streak + 1 : 1;
task.completed = true;
} else if (direction === 'down') {
// Remove a streak achievement if streak was a multiple of 21 and the daily was undone
if (task.streak % 21 === 0) user.achievements.streak = user.achievements.streak ? user.achievements.streak - 1 : 0;
task.streak -= 1;
task.completed = false;
}
}
} else if (task.type === 'todo') {
if (cron) { // don't touch stats on cron
delta += _changeTaskValue(user, task, direction, times, cron);
} else {
if (direction === 'up') {
task.dateCompleted = new Date();
task.completed = true;
} else if (direction === 'down') {
task.completed = false;
task.dateCompleted = undefined;
}
delta += _changeTaskValue(user, task, direction, times, cron);
if (direction === 'down') delta = _calculateDelta(task, direction, cron); // recalculate delta for unchecking so the gp and exp come out correctly
_addPoints(user, task, stats, direction, delta);
// MP++ per checklist item in ToDo, bonus per CLI
let multiplier = _.max([_.reduce(task.checklist, (m, i) => m + (i.completed ? 1 : 0), 1), 1]);
_gainMP(user, _.max([multiplier, 0.01 * user._statsComputed.maxMP * multiplier]) * (direction === 'down' ? -1 : 1));
}
} else if (task.type === 'reward') {
// Don't adjust values for rewards
delta += _changeTaskValue(user, task, direction, times, cron);
// purchase item
stats.gp -= Math.abs(task.value);
// hp - gp difference
if (stats.gp < 0) {
stats.hp += stats.gp;
stats.gp = 0;
}
}
updateStats(user, stats, req);
return [delta];
};
+33 -13
View File
@@ -1,23 +1,43 @@
import content from '../content/index';
import i18n from '../i18n';
import _ from 'lodash';
import splitWhitespace from '../libs/splitWhitespace';
import {
NotFound,
NotAuthorized,
BadRequest,
} from '../libs/errors';
module.exports = function(user, 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;
const ACCEPTEDTYPES = ['eggs', 'hatchingPotions', 'food'];
module.exports = function sell (user, req = {}) {
let key = _.get(req.params, 'key');
let type = _.get(req.params, 'type');
if (!type) {
throw new BadRequest(i18n.t('typeRequired', req.language));
}
if (!key) {
throw new BadRequest(i18n.t('keyRequired', req.language));
}
if (ACCEPTEDTYPES.indexOf(type) === -1) {
throw new NotAuthorized(i18n.t('typeNotSellable', {acceptedTypes: ACCEPTEDTYPES.join(', ')}, req.language));
}
if (!user.items[type][key]) {
return typeof cb === "function" ? cb({
code: 404,
message: ":key not found for user.items." + type
}) : void 0;
throw new NotFound(i18n.t('userItemsKeyNotFound', {type}, req.language));
}
user.items[type][key]--;
user.stats.gp += content[type][key].value;
return typeof cb === "function" ? cb(null, _.pick(user, splitWhitespace('stats items'))) : void 0;
if (req.v2 === true) {
return _.pick(user, splitWhitespace('stats items'));
} else {
return [
_.pick(user, splitWhitespace('stats items')),
];
}
};
+7 -2
View File
@@ -1,4 +1,9 @@
module.exports = function(user, req, cb) {
module.exports = function sleep (user, req = {}) {
user.preferences.sleep = !user.preferences.sleep;
return typeof cb === "function" ? cb(null, {}) : void 0;
if (req.v2 === true) {
return {};
} else {
return [user.preferences.sleep];
}
};
+17 -7
View File
@@ -1,9 +1,19 @@
module.exports = function(user, 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;
import { BadRequest } from '../libs/errors';
import _ from 'lodash';
// TODO used only in client, move there?
module.exports = function sortTag (user, req = {}) {
let to = _.get(req, 'query.to');
let fromParam = _.get(req, 'query.from');
let invalidTo = !to && to !== 0;
let invalidFrom = !fromParam && fromParam !== 0;
if (invalidTo || invalidFrom) {
throw new BadRequest('?to=__&from=__ are required');
}
user.tags.splice(to, 0, user.tags.splice(from, 1)[0]);
return typeof cb === "function" ? cb(null, user.tags) : void 0;
user.tags.splice(to, 0, user.tags.splice(fromParam, 1)[0]);
return user.tags;
};
+34 -24
View File
@@ -1,39 +1,49 @@
import i18n from '../i18n';
import preenTodos from '../libs/preenTodos';
import {
NotFound,
BadRequest,
} from '../libs/errors';
import _ from 'lodash';
module.exports = function(user, 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;
// TODO used only in client, move there?
module.exports = function sortTask (user, req = {}) {
let id = _.get(req, 'params.id');
let to = _.get(req, 'query.to');
let fromParam = _.get(req, 'query.from');
let taskType = _.get(req, 'params.taskType');
let index = _.findIndex(user[`${taskType}s`], function findById (task) {
return task._id === id;
});
if (index === -1) {
throw new NotFound(i18n.t('messageTaskNotFound', req.language));
}
if (!((to != null) && (from != null))) {
return typeof cb === "function" ? cb('?to=__&from=__ are required') : void 0;
if (!to && !fromParam) {
throw new BadRequest('?to=__&from=__ are required');
}
tasks = user[task.type + "s"];
if (task.type === 'todo' && tasks[from] !== task) {
preenedTasks = preenTodos(tasks);
let tasks = user[`${taskType}s`];
if (taskType === 'todo') {
let preenedTasks = preenTodos(tasks);
if (to !== -1) {
to = tasks.indexOf(preenedTasks[to]);
}
from = tasks.indexOf(preenedTasks[from]);
fromParam = tasks.indexOf(preenedTasks[fromParam]);
}
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];
let movedTask = tasks.splice(fromParam, 1)[0];
if (to === -1) {
tasks.push(movedTask);
} else {
tasks.splice(to, 0, movedTask);
}
return typeof cb === "function" ? cb(null, tasks) : void 0;
return tasks;
};
+96 -46
View File
@@ -1,63 +1,113 @@
import i18n from '../i18n';
import _ from 'lodash';
import splitWhitespace from '../libs/splitWhitespace';
import {
NotAuthorized,
BadRequest,
} from '../libs/errors';
module.exports = function(user, 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 item is already purchased -> equip it
// Otherwise unlock it
module.exports = function unlock (user, req = {}, analytics) {
let path = _.get(req.query, 'path');
if (!path) {
throw new BadRequest(i18n.t('pathRequired', req.language));
}
if (fullSet) {
_.each(path.split(","), function(p) {
if (~path.indexOf('gear.')) {
user.fns.dotSet("" + p, true);
true;
} else {
let isFullSet = path.indexOf(',') !== -1;
let isBackground = path.indexOf('background.') !== -1;
let cost;
if (isBackground && isFullSet) {
cost = 3.75;
} else if (isBackground) {
cost = 1.75;
} else if (isFullSet) {
cost = 1.25;
} else {
cost = 0.5;
}
let setPaths;
let alreadyOwns;
if (isFullSet) {
setPaths = path.split(',');
let alreadyOwnedItems = 0;
_.each(setPaths, singlePath => {
if (_.get(user, `purchased.${singlePath}`) === true) {
alreadyOwnedItems++;
}
user.fns.dotSet("purchased." + p, true);
return true;
});
if (alreadyOwnedItems === setPaths.length) {
throw new NotAuthorized(i18n.t('alreadyUnlocked', req.language));
// TODO write math formula to check if buying the full set is cheaper than the items individually
// (item cost * number of remaining items) < setCost`
} /* else if (alreadyOwnedItems > 0) {
throw new NotAuthorized(i18n.t('alreadyUnlockedPart', req.language));
} */
} else {
alreadyOwns = _.get(user, `purchased.${path}`) === true;
}
if ((!user.balance || user.balance < cost) && !alreadyOwns) {
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
if (isFullSet) {
_.each(setPaths, function markItemsAsPurchased (pathPart) {
if (path.indexOf('gear.') !== -1) {
_.set(user, pathPart, true);
}
_.set(user, `purchased.${pathPart}`, true);
});
} else {
if (alreadyOwns) {
split = path.split('.');
v = split.pop();
k = split.join('.');
if (k === 'background' && v === user.preferences.background) {
v = '';
if (alreadyOwns) { // eslint-disable-line no-lonely-if
let split = path.split('.');
let value = split.pop();
let key = split.join('.');
if (key === 'background' && value === user.preferences.background) {
value = '';
}
user.fns.dotSet("preferences." + k, v);
return typeof cb === "function" ? cb(null, req) : void 0;
_.set(user, `preferences.${key}`, value);
} else {
_.set(user, `purchased.${path}`, true);
}
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") {
if (!alreadyOwns) {
if (path.indexOf('gear.') === -1) {
user.markModified('purchased');
}
user.balance -= cost;
if (analytics) {
analytics.track('acquire item', {
uuid: user._id,
itemKey: path,
itemType: 'customization',
acquireMethod: 'Gems',
gemCost: cost / 0.25,
category: 'behavior',
});
}
}
analyticsData = {
uuid: user._id,
itemKey: path,
itemType: 'customization',
acquireMethod: 'Gems',
gemCost: cost / .25,
category: 'behavior'
};
if (analytics != null) {
analytics.track('acquire item', analyticsData);
let response = [
_.pick(user, splitWhitespace('purchased preferences items')),
];
if (!alreadyOwns) response.push(i18n.t('unlocked', req.language));
if (req.v2 === true) {
return response[0];
} else {
return response;
}
return typeof cb === "function" ? cb(null, _.pick(user, splitWhitespace('purchased preferences items'))) : void 0;
};
+7 -5
View File
@@ -1,9 +1,11 @@
import _ from 'lodash';
module.exports = function(user, req, cb) {
_.each(req.body, function(v, k) {
user.fns.dotSet(k, v);
return true;
// TODO used only in client, move there?
module.exports = function updateUser (user, req = {}) {
_.each(req.body, (val, key) => {
_.set(user, key, val);
});
return typeof cb === "function" ? cb(null, user) : void 0;
return user;
};
+14 -12
View File
@@ -1,18 +1,20 @@
import i18n from '../i18n';
import _ from 'lodash';
import { NotFound } from '../libs/errors';
module.exports = function(user, req, cb) {
var i, tid;
tid = req.params.id;
i = _.findIndex(user.tags, {
id: tid
// TODO used only in client, move there?
module.exports = function updateTag (user, req = {}) {
let tid = _.get(req, 'params.id');
let index = _.findIndex(user.tags, {
id: tid,
});
if (!~i) {
return typeof cb === "function" ? cb({
code: 404,
message: i18n.t('messageTagNotFound', req.language)
}) : void 0;
if (index === -1) {
throw new NotFound(i18n.t('messageTagNotFound', req.language));
}
user.tags[i].name = req.body.name;
return typeof cb === "function" ? cb(null, user.tags[i]) : void 0;
user.tags[index].name = _.get(req, 'body.name');
return user.tags[index];
};
+19 -17
View File
@@ -1,23 +1,25 @@
import i18n from '../i18n';
import _ from 'lodash';
module.exports = function(user, 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;
// From server pass task.toObject() not the task document directly
module.exports = function updateTask (task, req = {}) {
let body = req.body || {};
// If reminders are updated -> replace the original ones
if (body.reminders) {
task.reminders = body.reminders;
}
_.merge(task, _.omit(req.body, ['checklist', 'reminders', 'id', 'type']));
if (req.body.checklist) {
task.checklist = req.body.checklist;
// If checklist is updated -> replace the original one
if (body.checklist) {
task.checklist = body.checklist;
}
if (req.body.reminders) {
task.reminders = req.body.reminders;
// If tags are updated -> replace the original ones
if (body.tags) {
task.tags = body.tags;
}
if (typeof task.markModified === "function") {
task.markModified('tags');
}
return typeof cb === "function" ? cb(null, task) : void 0;
_.merge(task, _.omit(body, ['_id', 'id', 'type', 'reminders', 'checklist', 'tags']));
return [task];
};
+17 -6
View File
@@ -1,9 +1,20 @@
import _ from 'lodash';
import validator from 'validator';
import i18n from '../i18n';
import {
BadRequest,
} from '../libs/errors';
module.exports = function(user, req, cb) {
_.merge(user.preferences.webhooks[req.params.id], req.body);
if (typeof user.markModified === "function") {
user.markModified('preferences.webhooks');
module.exports = function updateWebhook (user, req) {
if (!validator.isURL(req.body.url)) throw new BadRequest(i18n.t('invalidUrl', req.language));
if (!validator.isBoolean(req.body.enabled)) throw new BadRequest(i18n.t('invalidEnabled', req.language));
user.markModified('preferences.webhooks');
user.preferences.webhooks[req.params.id].url = req.body.url;
user.preferences.webhooks[req.params.id].enabled = req.body.enabled;
if (req.v2 === true) {
return user.preferences.webhooks;
} else {
return [user.preferences.webhooks[req.params.id]];
}
return typeof cb === "function" ? cb(null, user.preferences.webhooks) : void 0;
};