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

View File

@@ -1,4 +1,9 @@
{
"presets": ["es2015"],
"plugins": ["syntax-async-functions","transform-regenerator"]
"plugins": [
["transform-async-to-module-method", {
"module": "bluebird",
"method": "coroutine"
}]
]
}

View File

@@ -1,3 +1,3 @@
{
"directory": "website/public/bower_components"
"directory": "website/client/bower_components"
}

View File

@@ -3,24 +3,26 @@ common/dist/
common/transpiled-babel/
coverage/
database_reports/
migrations/
website/build/
website/transpiled-babel/
# The files in website/public/js should be moved out and browserified
website/public/
migrations/*
# The files in website/client/js should be moved out and browserified
website/client/
# Temporarilly disabled. These should be removed when the linting errors are fixed
common/script/index.js
common/script/content/index.js
common/script/ops/**/*.js
common/script/fns/**/*.js
common/script/libs/**/*.js
common/script/public/**/*.js
website/src/**/*.js
website/server/**/api-v2/**/*.js
website/server/routes/payments.js
website/server/routes/pages.js
website/server/middlewares/apiThrottle.js
website/server/middlewares/forceRefresh.js
debug-scripts/*
scripts/*
tasks/*.js
gulpfile.js
Gruntfile.js

View File

@@ -2,5 +2,8 @@
"extends": [
"habitrpg/server",
"habitrpg/babel"
]
],
"globals": {
"Promise": true
}
}

11
.gitignore vendored
View File

@@ -1,6 +1,7 @@
.DS_Store
website/public/gen
website/public/common
website/client/gen
website/client/common
website/client/apidoc
website/transpiled-babel/
common/transpiled-babel/
node_modules
@@ -9,8 +10,8 @@ node_modules
config.json
npm-debug.log*
lib
website/public/bower_components
website/public/new-stuff.html
website/client/bower_components
website/client/new-stuff.html
website/build
newrelic_agent.log
.bower-tmp
@@ -24,7 +25,7 @@ src/*/*.map
src/*/*/*.map
test/*.js
test/*.map
website/public/docs
website/client/docs
*.sublime-workspace
coverage
coverage.html

View File

@@ -2,7 +2,7 @@ node_modules/**
.bower-cache/**
.bower-tmp/**
.bower-registry/**
website/public/**
website/client/**
website/views/**
website/build/**
.git/**

View File

@@ -2,7 +2,7 @@ language: node_js
node_js:
- '4.3.1'
before_install:
- "npm install -g npm@2"
- "npm install -g npm@3"
- "npm install -g gulp"
- "sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10"
- "echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list"

View File

@@ -43,11 +43,11 @@ module.exports = function(grunt) {
options: {
compress: false, // AFTER
'include css': true,
paths: ['website/public']
paths: ['website/client']
},
files: {
'website/build/app.css': ['website/public/css/index.styl'],
'website/build/static.css': ['website/public/css/static.styl']
'website/build/app.css': ['website/client/css/index.styl'],
'website/build/static.css': ['website/client/css/static.styl']
}
}
},
@@ -55,13 +55,13 @@ module.exports = function(grunt) {
copy: {
build: {
files: [
{expand: true, cwd: 'website/public/', src: 'favicon.ico', dest: 'website/build/'},
{expand: true, cwd: 'website/public/', src: 'favicon_192x192.png', dest: 'website/build/'},
{expand: true, cwd: 'website/client/', src: 'favicon.ico', dest: 'website/build/'},
{expand: true, cwd: 'website/client/', src: 'favicon_192x192.png', dest: 'website/build/'},
{expand: true, cwd: '', src: 'common/dist/sprites/spritesmith*.png', dest: 'website/build/'},
{expand: true, cwd: '', src: 'common/img/sprites/backer-only/*.gif', dest: 'website/build/'},
{expand: true, cwd: '', src: 'common/img/sprites/npc_ian.gif', dest: 'website/build/'},
{expand: true, cwd: '', src: 'common/img/sprites/quest_*.gif', dest: 'website/build/'},
{expand: true, cwd: 'website/public/', src: 'bower_components/bootstrap/dist/fonts/*', dest: 'website/build/'}
{expand: true, cwd: 'website/client/', src: 'bower_components/bootstrap/dist/fonts/*', dest: 'website/build/'}
]
}
},
@@ -88,9 +88,9 @@ module.exports = function(grunt) {
}
});
//Load build files from public/manifest.json
grunt.registerTask('loadManifestFiles', 'Load all build files from public/manifest.json', function(){
var files = grunt.file.readJSON('./website/public/manifest.json');
//Load build files from client/manifest.json
grunt.registerTask('loadManifestFiles', 'Load all build files from client/manifest.json', function(){
var files = grunt.file.readJSON('./website/client/manifest.json');
var uglify = {};
var cssmin = {};
@@ -101,7 +101,7 @@ module.exports = function(grunt) {
_.each(files[key].js, function(val){
var path = "./";
if( val.indexOf('common/') == -1)
path = './website/public/';
path = './website/client/';
js.push(path + val);
});
@@ -110,7 +110,7 @@ module.exports = function(grunt) {
_.each(files[key].css, function(val){
var path = "./";
if( val.indexOf('common/') == -1) {
path = (val == 'app.css' || val == 'static.css') ? './website/build/' : './website/public/';
path = (val == 'app.css' || val == 'static.css') ? './website/build/' : './website/client/';
}
css.push(path + val)
});
@@ -122,7 +122,7 @@ module.exports = function(grunt) {
grunt.config.set('cssmin.build.files', cssmin);
// Rewrite urls to relative path
grunt.config.set('cssmin.build.options', {'target': 'website/public/css/whatever-css.css'});
grunt.config.set('cssmin.build.options', {'target': 'website/client/css/whatever-css.css'});
});
// Register tasks.
@@ -131,7 +131,7 @@ module.exports = function(grunt) {
grunt.registerTask('build:test', ['test:prepare:translations', 'build:dev']);
grunt.registerTask('test:prepare:translations', function() {
var i18n = require('./website/src/libs/i18n'),
var i18n = require('./website/server/libs/api-v3/i18n'),
fs = require('fs');
fs.writeFileSync('test/spec/mocks/translations.js',
"if(!window.env) window.env = {};\n" +

View File

@@ -1 +1 @@
web: node ./website/transpiled-babel/server.js
web: node ./website/transpiled-babel/index.js

View File

@@ -9,7 +9,7 @@
"ignore": [
"**/.*",
"node_modules",
"public/bower_components",
"website/client/bower_components",
"test",
"tests"
],

View File

@@ -1,3 +1,5 @@
require('babel-polyfill');
var shared = require('./script/index');
var _ = require('lodash');
var moment = require('moment');

View File

@@ -346,13 +346,13 @@
width: 48px;
height: 52px;
}
.achievement-spookDust {
.achievement-spookySparkles {
background-image: url(spritesmith-main-0.png);
background-position: -25px -1601px;
width: 24px;
height: 26px;
}
.achievement-spookDust2x {
.achievement-spookySparkles2x {
background-image: url(spritesmith-main-0.png);
background-position: -980px -1548px;
width: 48px;

View File

@@ -1960,31 +1960,31 @@
width: 81px;
height: 99px;
}
.Pet-LionCub-Base {
.Pet-Lion-Veteran {
background-image: url(spritesmith-main-11.png);
background-position: -1640px -700px;
width: 81px;
height: 99px;
}
.Pet-LionCub-CottonCandyBlue {
.Pet-LionCub-Base {
background-image: url(spritesmith-main-11.png);
background-position: -1640px -800px;
width: 81px;
height: 99px;
}
.Pet-LionCub-CottonCandyPink {
.Pet-LionCub-CottonCandyBlue {
background-image: url(spritesmith-main-11.png);
background-position: -1640px -900px;
width: 81px;
height: 99px;
}
.Pet-LionCub-Desert {
.Pet-LionCub-CottonCandyPink {
background-image: url(spritesmith-main-11.png);
background-position: -1640px -1000px;
width: 81px;
height: 99px;
}
.Pet-LionCub-Floral {
.Pet-LionCub-Desert {
background-image: url(spritesmith-main-11.png);
background-position: -1640px -1100px;
width: 81px;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 153 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -270,7 +270,7 @@
}
.shop_armor_special_candycane {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -410px;
background-position: -1674px -451px;
width: 40px;
height: 40px;
}
@@ -282,19 +282,19 @@
}
.shop_armor_special_snowflake {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -615px;
background-position: -1674px -656px;
width: 40px;
height: 40px;
}
.shop_armor_special_winter2015Healer {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -1189px;
background-position: -1674px -1230px;
width: 40px;
height: 40px;
}
.shop_armor_special_winter2015Mage {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -1230px;
background-position: -1674px -1271px;
width: 40px;
height: 40px;
}
@@ -456,13 +456,13 @@
}
.shop_shield_special_winter2015Warrior {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -1558px;
background-position: -1674px -1517px;
width: 40px;
height: 40px;
}
.shop_shield_special_winter2016Healer {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -1476px;
background-position: -1599px -1599px;
width: 40px;
height: 40px;
}
@@ -1098,103 +1098,103 @@
}
.shop_head_rogue_3 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -984px;
background-position: -1674px -1025px;
width: 40px;
height: 40px;
}
.shop_head_rogue_4 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -943px;
background-position: -1674px -984px;
width: 40px;
height: 40px;
}
.shop_head_rogue_5 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -902px;
background-position: -1674px -943px;
width: 40px;
height: 40px;
}
.shop_head_special_0 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -861px;
background-position: -1674px -902px;
width: 40px;
height: 40px;
}
.shop_head_special_1 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -820px;
background-position: -1674px -861px;
width: 40px;
height: 40px;
}
.shop_head_special_2 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -738px;
background-position: -1674px -779px;
width: 40px;
height: 40px;
}
.shop_head_special_fireCoralCirclet {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -697px;
background-position: -1674px -738px;
width: 40px;
height: 40px;
}
.shop_head_warrior_1 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -656px;
background-position: -1674px -697px;
width: 40px;
height: 40px;
}
.shop_head_warrior_2 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -574px;
background-position: -1674px -615px;
width: 40px;
height: 40px;
}
.shop_head_warrior_3 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -533px;
background-position: -1674px -574px;
width: 40px;
height: 40px;
}
.shop_head_warrior_4 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -492px;
background-position: -1674px -533px;
width: 40px;
height: 40px;
}
.shop_head_warrior_5 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -451px;
background-position: -1674px -492px;
width: 40px;
height: 40px;
}
.shop_head_wizard_1 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -369px;
background-position: -1674px -410px;
width: 40px;
height: 40px;
}
.shop_head_wizard_2 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -328px;
background-position: -1674px -369px;
width: 40px;
height: 40px;
}
.shop_head_wizard_3 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -287px;
background-position: -1674px -328px;
width: 40px;
height: 40px;
}
.shop_head_wizard_4 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -246px;
background-position: -1674px -287px;
width: 40px;
height: 40px;
}
.shop_head_wizard_5 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -205px;
background-position: -1674px -246px;
width: 40px;
height: 40px;
}
@@ -1296,7 +1296,7 @@
}
.shop_headAccessory_special_bearEars {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -164px;
background-position: -1674px -205px;
width: 40px;
height: 40px;
}
@@ -1314,31 +1314,31 @@
}
.shop_headAccessory_special_lionEars {
background-image: url(spritesmith-main-5.png);
background-position: -1599px -1599px;
background-position: -1674px 0px;
width: 40px;
height: 40px;
}
.shop_headAccessory_special_pandaEars {
background-image: url(spritesmith-main-5.png);
background-position: -1674px 0px;
background-position: -1674px -41px;
width: 40px;
height: 40px;
}
.shop_headAccessory_special_pigEars {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -41px;
background-position: -1674px -82px;
width: 40px;
height: 40px;
}
.shop_headAccessory_special_tigerEars {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -82px;
background-position: -1674px -123px;
width: 40px;
height: 40px;
}
.shop_headAccessory_special_wolfEars {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -123px;
background-position: -1674px -164px;
width: 40px;
height: 40px;
}
@@ -1464,55 +1464,55 @@
}
.shop_shield_healer_1 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -1025px;
background-position: -1674px -1066px;
width: 40px;
height: 40px;
}
.shop_shield_healer_2 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -1066px;
background-position: -1674px -1107px;
width: 40px;
height: 40px;
}
.shop_shield_healer_3 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -1107px;
background-position: -1674px -1148px;
width: 40px;
height: 40px;
}
.shop_shield_healer_4 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -1148px;
background-position: -1674px -1189px;
width: 40px;
height: 40px;
}
.shop_shield_healer_5 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -1271px;
background-position: -1674px -1312px;
width: 40px;
height: 40px;
}
.shop_shield_rogue_0 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -1312px;
background-position: -1674px -1353px;
width: 40px;
height: 40px;
}
.shop_shield_rogue_1 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -1353px;
background-position: -1674px -1394px;
width: 40px;
height: 40px;
}
.shop_shield_rogue_2 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -1394px;
background-position: -1674px -1435px;
width: 40px;
height: 40px;
}
.shop_shield_rogue_3 {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -1435px;
background-position: -1674px -1476px;
width: 40px;
height: 40px;
}
@@ -2056,6 +2056,12 @@
width: 64px;
height: 54px;
}
.ghost {
background-image: url(spritesmith-main-5.png);
background-position: -455px -1144px;
width: 90px;
height: 90px;
}
.inventory_present {
background-image: url(spritesmith-main-5.png);
background-position: -1616px -1049px;
@@ -2172,7 +2178,7 @@
}
.inventory_special_opaquePotion {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -779px;
background-position: -1674px -820px;
width: 40px;
height: 40px;
}
@@ -2194,7 +2200,7 @@
width: 57px;
height: 54px;
}
.inventory_special_spookDust {
.inventory_special_spookySparkles {
background-image: url(spritesmith-main-5.png);
background-position: -1299px -1508px;
width: 57px;
@@ -2238,23 +2244,17 @@
}
.seafoam_star {
background-image: url(spritesmith-main-5.png);
background-position: -455px -1144px;
background-position: -364px -1144px;
width: 90px;
height: 90px;
}
.shop_armoire {
background-image: url(spritesmith-main-5.png);
background-position: -1674px -1517px;
background-position: -1674px -1558px;
width: 40px;
height: 40px;
}
.snowman {
background-image: url(spritesmith-main-5.png);
background-position: -364px -1144px;
width: 90px;
height: 90px;
}
.spookman {
background-image: url(spritesmith-main-5.png);
background-position: -273px -1144px;
width: 90px;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 255 KiB

View File

@@ -682,7 +682,7 @@
width: 32px;
height: 32px;
}
.shop_spookDust {
.shop_spookySparkles {
background-image: url(spritesmith-main-6.png);
background-position: -790px -1176px;
width: 32px;
@@ -730,7 +730,7 @@
width: 40px;
height: 40px;
}
.shop_heallAll {
.shop_healAll {
background-image: url(spritesmith-main-6.png);
background-position: -791px -1643px;
width: 40px;

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1,4 +1,6 @@
var pathToCommon;
'use strict';
let pathToCommon;
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
pathToCommon = './transpiled-babel/index';

View File

@@ -74,9 +74,10 @@
"onlyLeaderDeleteChal": "Only the challenge leader can delete it.",
"onlyLeaderUpdateChal": "Only the challenge leader can update it.",
"winnerNotFound": "Winner with id \"<%= userId %>\" not found or not part of the challenge.",
"noCompletedTodosChallenge": "\"includeComepletedTodos\" is not supported when fetching a challenge tasks.",
"noCompletedTodosChallenge": "\"includeComepletedTodos\" is not supported when fetching challenge tasks.",
"userTasksNoChallengeId": "When \"tasksOwner\" is \"user\" \"challengeId\" can't be passed.",
"onlyChalLeaderEditTasks": "Tasks belonging to a challenge can only be edited by the leader.",
"userAlreadyInChallenge": "User is already participating in this challenge.",
"cantOnlyUnlinkChalTask": "Only broken challenges tasks can be unlinked."
"cantOnlyUnlinkChalTask": "Only broken challenges tasks can be unlinked.",
"shortNameTooShort": "Tag Name must have at least 3 characters."
}

View File

@@ -182,8 +182,8 @@
"userAlreadyInAParty": "User already in a party.",
"userWithIDNotFound": "User with id \"<%= userId %>\" not found.",
"userHasNoLocalRegistration": "User does not have a local registration (username, email, password).",
"uuidsMustBeAnArray": "UUIDs invites must be a an Array.",
"emailsMustBeAnArray": "Email invites must be a an Array.",
"uuidsMustBeAnArray": "User ID invites must be an array.",
"emailsMustBeAnArray": "Email address invites must be an array.",
"canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time",
"onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!"
}

View File

@@ -5,7 +5,7 @@
"annoyingFriends": "Annoying Friends",
"annoyingFriendsText": "Got snowballed <%= snowballs %> times by party members.",
"alarmingFriends": "Alarming Friends",
"alarmingFriendsText": "Got spooked <%= spookDust %> times by party members.",
"alarmingFriendsText": "Got spooked <%= spookySparkles %> times by party members.",
"agriculturalFriends": "Agricultural Friends",
"agriculturalFriendsText": "Got transformed into a flower <%= seeds %> times by party members.",
"aquaticFriends": "Aquatic Friends",

View File

@@ -1,6 +1,6 @@
{
"habiticaBackSoon": "Don't worry, Habitica will be back soon!",
"importantMaintenance": "We are doing important maintenance that we estimate will last until <%= localDate %> in your timezone.",
"importantMaintenance": "We are doing important maintenance that we estimate will last until 10pm Pacific Time (5am UTC).",
"maintenance": "Maintenance",
"maintenanceMoreInfo": "Want more information about the maintenance? <%= linkStart %>Check out our info page<%= linkEnd %>.",
"noDamageKeepStreaks": "You will NOT take damage or lose streaks!",

View File

@@ -12,6 +12,7 @@
"etherealLion": "Ethereal Lion",
"veteranWolf": "Veteran Wolf",
"veteranTiger": "Veteran Tiger",
"veteranLion": "Veteran Lion",
"cerberusPup": "Cerberus Pup",
"hydra": "Hydra",
"mantisShrimp": "Mantis Shrimp",

View File

@@ -47,6 +47,7 @@
"customDayStart": "Custom Day Start",
"changeCustomDayStart": "Change Custom Day Start?",
"sureChangeCustomDayStart": "Are you sure you want to change your custom day start?",
"customDayStartHasChanged": "Your custom day start has changed.",
"nextCron": "Your Dailies will next reset the first time you use Habitica after <%= time %>. Make sure you have completed your Dailies before this time!",
"customDayStartInfo1": "Habitica defaults to check and reset your Dailies at midnight in your own time zone each day. You can customize that time here.",
"misc": "Misc",

View File

@@ -52,8 +52,8 @@
"spellSpecialSaltText": "Salt",
"spellSpecialSaltNotes": "Someone has snowballed you. Ha ha, very funny. Now get this snow off me!",
"spellSpecialSpookDustText": "Spooky Sparkles",
"spellSpecialSpookDustNotes": "Turn a friend into a floating blanket with eyes!",
"spellSpecialSpookySparklesText": "Spooky Sparkles",
"spellSpecialSpookySparklesNotes": "Turn a friend into a floating blanket with eyes!",
"spellSpecialOpaquePotionText": "Opaque Potion",
"spellSpecialOpaquePotionNotes": "Cancel the effects of Spooky Sparkles.",
@@ -70,7 +70,7 @@
"spellNotFound": "Skill \"<%= spellId %>\" not found.",
"partyNotFound": "Party not found",
"targetIdUUID": "\"targetId\" must be a valid User ID.",
"challengeTasksNoCast": "Casting a skill on challenge tasks is not supported.",
"challengeTasksNoCast": "Casting a skill on challenge tasks is not allowed.",
"spellNotOwned": "You don't own this skill.",
"spellLevelTooHigh": "You must be level <%= level %> to use this skill."
}

View File

@@ -134,7 +134,7 @@
"notAllowedHourglass": "Pet/Mount not available for purchase with Mystic Hourglass.",
"readCard": "<%= cardType %> has been read",
"cardTypeRequired": "Card type required",
"cardTypeNotAllowed": "Unkown card type.",
"cardTypeNotAllowed": "Unknown card type.",
"invalidCoupon": "Invalid coupon code.",
"couponUsed": "Coupon code already used.",
"noSudoAccess": "You don't have sudo access.",

View File

@@ -73,6 +73,7 @@
"clearTags": "Clear",
"hideTags": "Hide",
"showTags": "Show",
"toRequired": "You must supply a to value",
"startDate": "Start Date",
"startDateHelpTitle": "When should this task start?",
"startDateHelp": "Set the date for which this task takes effect. Will not be due on earlier days.",

View File

@@ -1,3 +1,5 @@
export const MAX_HEALTH = 50;
export const MAX_LEVEL = 100;
export const MAX_STAT_POINTS = MAX_LEVEL;
export const ATTRIBUTES = ['str', 'int', 'per', 'con'];
export const TAVERN_ID = '00000000-0000-4000-A000-000000000000';

View File

@@ -426,6 +426,7 @@ api.specialPets = {
'Phoenix-Base': 'phoenix',
'Turkey-Gilded': 'gildedTurkey',
'MagicalBee-Base': 'magicalBee',
'Lion-Veteran': 'veteranLion',
};
api.specialMounts = {
@@ -601,6 +602,14 @@ api.questMounts = _.transform(api.questEggs, function(m, egg) {
}));
});
api.premiumMounts = _.transform(api.dropEggs, function(m, egg) {
return _.defaults(m, _.transform(api.hatchingPotions, function(m2, pot) {
if (pot.premium) {
return m2[egg.key + "-" + pot.key] = true;
}
}));
});
api.food = {
Meat: {
text: t('foodMeat'),

View File

@@ -1,6 +1,6 @@
import t from './translation';
import _ from 'lodash';
import { NotAuthorized } from '../libs/errors';
/*
---------------------------------------------------------------
Spells
@@ -15,7 +15,7 @@ import _ from 'lodash';
web, this function can be performed on the client and on the server. `user` param is self (needed for determining your
own stats for effectiveness of cast), and `target` param is one of [task, party, user]. In the case of `self` spells,
you act on `user` instead of `target`. You can trust these are the correct objects, as long as the `target` attr of the
spell is correct. Take a look at habitrpg/src/models/user.js and habitrpg/src/models/task.js for what attributes are
spell is correct. Take a look at habitrpg/website/server/models/user.js and habitrpg/website/server/models/task.js for what attributes are
available on each model. Note `task.value` is its "redness". If party is passed in, it's an array of users,
so you'll want to iterate over them like: `_.each(target,function(member){...})`
@@ -40,14 +40,12 @@ spells.wizard = {
lvl: 11,
target: 'task',
notes: t('spellWizardFireballNotes'),
cast (user, target) {
cast (user, target, req) {
let bonus = user._statsComputed.int * user.fns.crit('per');
bonus *= Math.ceil((target.value < 0 ? 1 : target.value + 1) * 0.075);
user.stats.exp += diminishingReturns(bonus, 75);
if (!user.party.quest.progress) user.party.quest.progress = 0;
user.party.quest.progress.up += Math.ceil(user._statsComputed.int * 0.1);
// TODO change, pass req to spell?
let req = {language: user.preferences.language};
user.fns.updateStats(user.stats, req);
},
},
@@ -166,12 +164,11 @@ spells.rogue = {
lvl: 12,
target: 'task',
notes: t('spellRogueBackStabNotes'),
cast (user, target) {
cast (user, target, req) {
let _crit = user.fns.crit('str', 0.3);
let bonus = calculateBonus(target.value, user._statsComputed.str, _crit);
user.stats.exp += diminishingReturns(bonus, 75, 50);
user.stats.gp += diminishingReturns(bonus, 18, 75);
let req = {language: user.preferences.language};
user.fns.updateStats(user.stats, req);
},
},
@@ -197,7 +194,7 @@ spells.rogue = {
notes: t('spellRogueStealthNotes'),
cast (user) {
if (!user.stats.buffs.stealth) user.stats.buffs.stealth = 0;
user.stats.buffs.stealth += Math.ceil(diminishingReturns(user._statsComputed.per, user.dailys.length * 0.64, 55));
user.stats.buffs.stealth += Math.ceil(diminishingReturns(user._statsComputed.per, user.tasksOrder.dailys.length * 0.64, 55));
},
},
};
@@ -218,10 +215,10 @@ spells.healer = {
text: t('spellHealerBrightnessText'),
mana: 15,
lvl: 12,
target: 'self',
target: 'tasks',
notes: t('spellHealerBrightnessNotes'),
cast (user) {
_.each(user.tasks, (task) => {
cast (user, tasks) {
_.each(tasks, (task) => {
if (task.type !== 'reward') {
task.value += 4 * (user._statsComputed.int / (user._statsComputed.int + 40));
}
@@ -242,7 +239,7 @@ spells.healer = {
});
},
},
heallAll: { // Blessing
healAll: { // Blessing
text: t('spellHealerHealAllText'),
mana: 25,
lvl: 14,
@@ -262,11 +259,13 @@ spells.special = {
text: t('spellSpecialSnowballAuraText'),
mana: 0,
value: 15,
previousPurchase: true,
target: 'user',
notes: t('spellSpecialSnowballAuraNotes'),
cast (user, target) {
cast (user, target, req) {
if (!user.items.special.snowball) throw new NotAuthorized(t('spellNotOwned')(req.language));
target.stats.buffs.snowball = true;
target.stats.buffs.spookDust = false;
target.stats.buffs.spookySparkles = false;
target.stats.buffs.shinySeed = false;
target.stats.buffs.seafoam = false;
if (!target.achievements.snowball) target.achievements.snowball = 0;
@@ -286,20 +285,22 @@ spells.special = {
user.stats.gp -= 5;
},
},
spookDust: {
text: t('spellSpecialSpookDustText'),
spookySparkles: {
text: t('spellSpecialSpookySparklesText'),
mana: 0,
value: 15,
previousPurchase: true,
target: 'user',
notes: t('spellSpecialSpookDustNotes'),
cast (user, target) {
notes: t('spellSpecialSpookySparklesNotes'),
cast (user, target, req) {
if (!user.items.special.spookySparkles) throw new NotAuthorized(t('spellNotOwned')(req.language));
target.stats.buffs.snowball = false;
target.stats.buffs.spookDust = true;
target.stats.buffs.spookySparkles = true;
target.stats.buffs.shinySeed = false;
target.stats.buffs.seafoam = false;
if (!target.achievements.spookDust) target.achievements.spookDust = 0;
target.achievements.spookDust++;
user.items.special.spookDust--;
if (!target.achievements.spookySparkles) target.achievements.spookySparkles = 0;
target.achievements.spookySparkles++;
user.items.special.spookySparkles--;
},
},
opaquePotion: {
@@ -310,7 +311,7 @@ spells.special = {
target: 'self',
notes: t('spellSpecialOpaquePotionNotes'),
cast (user) {
user.stats.buffs.spookDust = false;
user.stats.buffs.spookySparkles = false;
user.stats.gp -= 5;
},
},
@@ -318,11 +319,13 @@ spells.special = {
text: t('spellSpecialShinySeedText'),
mana: 0,
value: 15,
previousPurchase: true,
target: 'user',
notes: t('spellSpecialShinySeedNotes'),
cast (user, target) {
cast (user, target, req) {
if (!user.items.special.shinySeed) throw new NotAuthorized(t('spellNotOwned')(req.language));
target.stats.buffs.snowball = false;
target.stats.buffs.spookDust = false;
target.stats.buffs.spookySparkles = false;
target.stats.buffs.shinySeed = true;
target.stats.buffs.seafoam = false;
if (!target.achievements.shinySeed) target.achievements.shinySeed = 0;
@@ -346,11 +349,13 @@ spells.special = {
text: t('spellSpecialSeafoamText'),
mana: 0,
value: 15,
previousPurchase: true,
target: 'user',
notes: t('spellSpecialSeafoamNotes'),
cast (user, target) {
cast (user, target, req) {
if (!user.items.special.seafoam) throw new NotAuthorized(t('spellNotOwned')(req.language));
target.stats.buffs.snowball = false;
target.stats.buffs.spookDust = false;
target.stats.buffs.spookySparkles = false;
target.stats.buffs.shinySeed = false;
target.stats.buffs.seafoam = true;
if (!target.achievements.seafoam) target.achievements.seafoam = 0;
@@ -391,7 +396,10 @@ spells.special = {
if (!target.items.special.nyeReceived) target.items.special.nyeReceived = [];
target.items.special.nyeReceived.push(user.profile.name);
if (!target.flags) target.flags = {};
target.flags.cardReceived = true;
user.stats.gp -= 10;
},
},
@@ -416,7 +424,10 @@ spells.special = {
if (!target.items.special.valentineReceived) target.items.special.valentineReceived = [];
target.items.special.valentineReceived.push(user.profile.name);
if (!target.flags) target.flags = {};
target.flags.cardReceived = true;
user.stats.gp -= 10;
},
},
@@ -440,7 +451,10 @@ spells.special = {
if (!target.items.special.greetingReceived) target.items.special.greetingReceived = [];
target.items.special.greetingReceived.push(user.profile.name);
if (!target.flags) target.flags = {};
target.flags.cardReceived = true;
user.stats.gp -= 10;
},
},
@@ -465,7 +479,10 @@ spells.special = {
if (!target.items.special.thankyouReceived) target.items.special.thankyouReceived = [];
target.items.special.thankyouReceived.push(user.profile.name);
if (!target.flags) target.flags = {};
target.flags.cardReceived = true;
user.stats.gp -= 10;
},
},
@@ -487,9 +504,13 @@ spells.special = {
u.achievements.birthday++;
});
}
if (!target.items.special.birthdayReceived) target.items.special.birthdayReceived = [];
target.items.special.birthdayReceived.push(user.profile.name);
if (!target.flags) target.flags = {};
target.flags.cardReceived = true;
user.stats.gp -= 10;
},
},
@@ -499,8 +520,8 @@ _.each(spells, (spellClass) => {
_.each(spellClass, (spell, key) => {
spell.key = key;
let _cast = spell.cast;
spell.cast = function castSpell (user, target) {
_cast(user, target);
spell.cast = function castSpell (user, target, req) {
_cast(user, target, req);
user.stats.mp -= spell.mana;
};
});

View File

@@ -1,3 +1,4 @@
// TODO what can be moved to /website/server?
/*
------------------------------------------------------
Cron and time / day functions

View File

@@ -7,50 +7,69 @@ import splitWhitespace from '../libs/splitWhitespace';
{update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately
*/
module.exports = function(user) {
return user.stats[(function() {
var diff, ideal, lvlDiv7, preference, stats, suggested;
switch (user.preferences.allocationMode) {
case "flat":
stats = _.pick(user.stats, splitWhitespace('con str per int'));
return _.invert(stats)[_.min(stats)];
case "classbased":
lvlDiv7 = user.stats.lvl / 7;
ideal = [lvlDiv7 * 3, lvlDiv7 * 2, lvlDiv7, lvlDiv7];
preference = (function() {
switch (user.stats["class"]) {
case "wizard":
return ["int", "per", "con", "str"];
case "rogue":
return ["per", "str", "int", "con"];
case "healer":
return ["con", "int", "str", "per"];
default:
return ["str", "con", "per", "int"];
}
})();
diff = [user.stats[preference[0]] - ideal[0], user.stats[preference[1]] - ideal[1], user.stats[preference[2]] - ideal[2], user.stats[preference[3]] - ideal[3]];
suggested = _.findIndex(diff, (function(val) {
if (val === _.min(diff)) {
return true;
}
}));
if (~suggested) {
return preference[suggested];
} else {
return "str";
}
case "taskbased":
suggested = _.invert(user.stats.training)[_.max(user.stats.training)];
_.merge(user.stats.training, {
str: 0,
int: 0,
con: 0,
per: 0
});
return suggested || "str";
default:
return "str";
function getStatToAllocate (user) {
let suggested;
let statsObj = user.stats.toObject ? user.stats.toObject() : user.stats;
switch (user.preferences.allocationMode) {
case 'flat': {
let stats = _.pick(statsObj, splitWhitespace('con str per int'));
return _.invert(stats)[_.min(stats)];
}
})()]++;
case 'classbased': {
let lvlDiv7 = statsObj.lvl / 7;
let ideal = [lvlDiv7 * 3, lvlDiv7 * 2, lvlDiv7, lvlDiv7];
let preference;
switch (statsObj.class) {
case 'wizard': {
preference = ['int', 'per', 'con', 'str'];
break;
}
case 'rogue': {
preference = ['per', 'str', 'int', 'con'];
break;
}
case 'healer': {
preference = ['con', 'int', 'str', 'per'];
break;
}
default: {
preference = ['str', 'con', 'per', 'int'];
}
}
let diff = [
statsObj[preference[0]] - ideal[0],
statsObj[preference[1]] - ideal[1],
statsObj[preference[2]] - ideal[2],
statsObj[preference[3]] - ideal[3],
];
suggested = _.findIndex(diff, (val) => {
if (val === _.min(diff)) return true;
});
return suggested !== -1 ? preference[suggested] : 'str';
}
case 'taskbased': {
suggested = _.invert(statsObj.training)[_.max(statsObj.training)];
let training = statsObj.training;
training.str = 0;
training.int = 0;
training.con = 0;
training.per = 0;
return suggested || 'str';
}
default: {
return 'str';
}
}
}
module.exports = function autoAllocate (user) {
return user.stats[getStatToAllocate(user)]++;
};

View File

@@ -1,13 +1,8 @@
module.exports = function(user, stat, chance) {
var s;
if (stat == null) {
stat = 'str';
}
if (chance == null) {
chance = .03;
}
s = user._statsComputed[stat];
if (user.fns.predictableRandom() <= chance * (1 + s / 100)) {
import predictableRandom from './predictableRandom';
module.exports = function crit (user, stat = 'str', chance = 0.03) {
let s = user._statsComputed[stat];
if (predictableRandom(user) <= chance * (1 + s / 100)) {
return 1.5 + 4 * s / (s + 200);
} else {
return 1;

View File

@@ -1,358 +0,0 @@
import moment from 'moment';
import _ from 'lodash';
import {
daysSince,
shouldDo,
} from '../cron';
import {
capByLevel,
toNextLevel,
} from '../statHelpers';
/*
------------------------------------------------------
Cron
------------------------------------------------------
*/
/*
At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
For incomplete Dailys, deduct experience
Make sure to run this function once in a while as server will not take care of overnight calculations.
And you have to run it every time client connects.
{user}
*/
module.exports = function(user, options) {
var _progress, analyticsData, base, base1, base2, base3, base4, clearBuffs, dailyChecked, dailyDueUnchecked, daysMissed, expTally, lvl, lvlDiv2, multiDaysCountAsOneDay, now, perfect, plan, progress, ref, ref1, ref2, ref3, todoTally, timezoneOffsetFromUserPrefs, timezoneOffsetFromBrowser, timezoneOffsetAtLastCron;
if (options == null) {
options = {};
}
now = +options.now || +(new Date);
// If the user's timezone has changed (due to travel or daylight savings),
// cron can be triggered twice in one day, so we check for that and use
// both timezones to work out if cron should run.
// CDS = Custom Day Start time.
timezoneOffsetFromUserPrefs = user.preferences.timezoneOffset || 0;
timezoneOffsetAtLastCron = (_.isFinite(user.preferences.timezoneOffsetAtLastCron)) ? user.preferences.timezoneOffsetAtLastCron : timezoneOffsetFromUserPrefs;
timezoneOffsetFromBrowser = (_.isFinite(+options.timezoneOffset)) ? +options.timezoneOffset : timezoneOffsetFromUserPrefs;
// NB: All timezone offsets can be 0, so can't use `... || ...` to apply non-zero defaults
if (timezoneOffsetFromBrowser !== timezoneOffsetFromUserPrefs) {
// The user's browser has just told Habitica that the user's timezone has
// changed so store and use the new zone.
user.preferences.timezoneOffset = timezoneOffsetFromBrowser;
timezoneOffsetFromUserPrefs = timezoneOffsetFromBrowser;
}
// How many days have we missed using the user's current timezone:
daysMissed = daysSince(user.lastCron, _.defaults({
now: now
}, user.preferences));
if (timezoneOffsetAtLastCron != timezoneOffsetFromUserPrefs) {
// Since cron last ran, the user's timezone has changed.
// How many days have we missed using the old timezone:
let daysMissedNewZone = daysMissed;
let daysMissedOldZone = daysSince(user.lastCron, _.defaults({
now: now,
timezoneOffsetOverride: timezoneOffsetAtLastCron,
}, user.preferences));
if (timezoneOffsetAtLastCron < timezoneOffsetFromUserPrefs) {
// The timezone change was in the unsafe direction.
// E.g., timezone changes from UTC+1 (offset -60) to UTC+0 (offset 0).
// or timezone changes from UTC-4 (offset 240) to UTC-5 (offset 300).
// Local time changed from, for example, 03:00 to 02:00.
if (daysMissedOldZone > 0 && daysMissedNewZone > 0) {
// Both old and new timezones indicate that we SHOULD run cron, so
// it is safe to do so immediately.
daysMissed = Math.min(daysMissedOldZone, daysMissedNewZone);
// use minimum value to be nice to user
}
else if (daysMissedOldZone > 0) {
// The old timezone says that cron should run; the new timezone does not.
// This should be impossible for this direction of timezone change, but
// just in case I'm wrong...
console.log("zone has changed - old zone says run cron, NEW zone says no - stop cron now only -- SHOULD NOT HAVE GOT TO HERE", timezoneOffsetAtLastCron, timezoneOffsetFromUserPrefs, now); // used in production for confirming this never happens
}
else if (daysMissedNewZone > 0) {
// The old timezone says that cron should NOT run -- i.e., cron has
// already run today, from the old timezone's point of view.
// The new timezone says that cron SHOULD run, but this is almost
// certainly incorrect.
// This happens when cron occurred at a time soon after the CDS. When
// you reinterpret that time in the new timezone, it looks like it
// was before the CDS, because local time has stepped backwards.
// To fix this, rewrite the cron time to a time that the new
// timezone interprets as being in today.
daysMissed = 0; // prevent cron running now
let timezoneOffsetDiff = timezoneOffsetAtLastCron - timezoneOffsetFromUserPrefs;
// e.g., for dangerous zone change: 240 - 300 = -60 or -660 - -600 = -60
user.lastCron = moment(user.lastCron).subtract(timezoneOffsetDiff, 'minutes');
// NB: We don't change user.auth.timestamps.loggedin so that will still record the time that the previous cron actually ran.
// From now on we can ignore the old timezone:
user.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs;
}
else {
// Both old and new timezones indicate that cron should
// NOT run.
daysMissed = 0; // prevent cron running now
}
}
else if (timezoneOffsetAtLastCron > timezoneOffsetFromUserPrefs) {
daysMissed = daysMissedNewZone;
// TODO: Either confirm that there is nothing that could possibly go wrong here and remove the need for this else branch, or fix stuff. There are probably situations where the Dailies do not reset early enough for a user who was expecting the zone change and wants to use all their Dailies immediately in the new zone; if so, we should provide an option for easy reset of Dailies (can't be automatic because there will be other situations where the user was not prepared).
}
}
if (!(daysMissed > 0)) {
return;
}
user.auth.timestamps.loggedin = new Date();
user.lastCron = now;
user.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs;
if (user.items.lastDrop.count > 0) {
user.items.lastDrop.count = 0;
}
perfect = true;
clearBuffs = {
str: 0,
int: 0,
per: 0,
con: 0,
stealth: 0,
streaks: false
};
plan = (ref = user.purchased) != null ? ref.plan : void 0;
if (plan != null ? plan.customerId : void 0) {
if (typeof plan.dateUpdated === "undefined") {
// partial compensation for bug in subscription creation - https://github.com/HabitRPG/habitrpg/issues/6682
plan.dateUpdated = new Date();
}
if (moment(plan.dateUpdated).format('MMYYYY') !== moment().format('MMYYYY')) {
plan.gemsBought = 0;
plan.dateUpdated = new Date();
_.defaults(plan.consecutive, {
count: 0,
offset: 0,
trinkets: 0,
gemCapExtra: 0
});
plan.consecutive.count++;
if (plan.consecutive.offset > 0) {
plan.consecutive.offset--;
} else if (plan.consecutive.count % 3 === 0) {
plan.consecutive.trinkets++;
plan.consecutive.gemCapExtra += 5;
if (plan.consecutive.gemCapExtra > 25) {
plan.consecutive.gemCapExtra = 25;
}
}
}
if (plan.dateTerminated && moment(plan.dateTerminated).isBefore(+(new Date))) {
_.merge(plan, {
planId: null,
customerId: null,
paymentMethod: null
});
_.merge(plan.consecutive, {
count: 0,
offset: 0,
gemCapExtra: 0
});
if (typeof user.markModified === "function") {
user.markModified('purchased.plan');
}
}
}
if (user.preferences.sleep === true) {
user.stats.buffs = clearBuffs;
user.dailys.forEach(function(daily) {
var completed, repeat, thatDay;
completed = daily.completed, repeat = daily.repeat;
thatDay = moment(now).subtract({
days: 1
});
if (shouldDo(thatDay.toDate(), daily, user.preferences) || completed) {
_.each(daily.checklist, (function(box) {
box.completed = false;
return true;
}));
}
return daily.completed = false;
});
return;
}
multiDaysCountAsOneDay = true;
todoTally = 0;
user.todos.forEach(function(task) {
var absVal, completed, delta, id;
if (!task) {
return;
}
id = task.id, completed = task.completed;
delta = user.ops.score({
params: {
id: task.id,
direction: 'down'
},
query: {
times: multiDaysCountAsOneDay != null ? multiDaysCountAsOneDay : {
1: daysMissed
},
cron: true
}
});
absVal = completed ? Math.abs(task.value) : task.value;
return todoTally += absVal;
});
dailyChecked = 0;
dailyDueUnchecked = 0;
if ((base = user.party.quest.progress).down == null) {
base.down = 0;
}
user.dailys.forEach(function(task) {
var EvadeTask, completed, delta, fractionChecked, id, j, n, ref1, ref2, scheduleMisses, thatDay;
if (!task) {
return;
}
id = task.id, completed = task.completed;
EvadeTask = 0;
scheduleMisses = daysMissed;
if (completed) {
dailyChecked += 1;
} else {
scheduleMisses = 0;
for (n = j = 0, ref1 = daysMissed; 0 <= ref1 ? j < ref1 : j > ref1; n = 0 <= ref1 ? ++j : --j) {
thatDay = moment(now).subtract({
days: n + 1
});
if (shouldDo(thatDay.toDate(), task, user.preferences)) {
scheduleMisses++;
if (user.stats.buffs.stealth) {
user.stats.buffs.stealth--;
EvadeTask++;
}
if (multiDaysCountAsOneDay) {
break;
}
}
}
if (scheduleMisses > EvadeTask) {
perfect = false;
if (((ref2 = task.checklist) != null ? ref2.length : void 0) > 0) {
fractionChecked = _.reduce(task.checklist, (function(m, i) {
return m + (i.completed ? 1 : 0);
}), 0) / task.checklist.length;
dailyDueUnchecked += 1 - fractionChecked;
dailyChecked += fractionChecked;
} else {
dailyDueUnchecked += 1;
}
delta = user.ops.score({
params: {
id: task.id,
direction: 'down'
},
query: {
times: multiDaysCountAsOneDay != null ? multiDaysCountAsOneDay : {
1: scheduleMisses - EvadeTask
},
cron: true
}
});
user.party.quest.progress.down += delta * (task.priority < 1 ? task.priority : 1);
}
}
(task.history != null ? task.history : task.history = []).push({
date: +(new Date),
value: task.value
});
task.completed = false;
if (completed || (scheduleMisses > 0)) {
return _.each(task.checklist, (function(i) {
i.completed = false;
return true;
}));
}
});
user.habits.forEach(function(task) {
if (task.up === false || task.down === false) {
if (Math.abs(task.value) < 0.1) {
return task.value = 0;
} else {
return task.value = task.value / 2;
}
}
});
((base1 = (user.history != null ? user.history : user.history = {})).todos != null ? base1.todos : base1.todos = []).push({
date: now,
value: todoTally
});
expTally = user.stats.exp;
lvl = 0;
while (lvl < (user.stats.lvl - 1)) {
lvl++;
expTally += toNextLevel(lvl);
}
((base2 = user.history).exp != null ? base2.exp : base2.exp = []).push({
date: now,
value: expTally
});
if (!((ref1 = user.purchased) != null ? (ref2 = ref1.plan) != null ? ref2.customerId : void 0 : void 0)) {
user.fns.preenUserHistory();
if (typeof user.markModified === "function") {
user.markModified('history');
}
if (typeof user.markModified === "function") {
user.markModified('dailys');
}
}
user.stats.buffs = perfect ? ((base3 = user.achievements).perfect != null ? base3.perfect : base3.perfect = 0, user.achievements.perfect++, lvlDiv2 = Math.ceil(capByLevel(user.stats.lvl) / 2), {
str: lvlDiv2,
int: lvlDiv2,
per: lvlDiv2,
con: lvlDiv2,
stealth: 0,
streaks: false
}) : clearBuffs;
if (dailyDueUnchecked === 0 && dailyChecked === 0) {
dailyChecked = 1;
}
user.stats.mp += _.max([10, .1 * user._statsComputed.maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked);
if (user.stats.mp > user._statsComputed.maxMP) {
user.stats.mp = user._statsComputed.maxMP;
}
progress = user.party.quest.progress;
_progress = _.cloneDeep(progress);
_.merge(progress, {
down: 0,
up: 0
});
progress.collect = _.transform(progress.collect, (function(m, v, k) {
return m[k] = 0;
}));
if ((base4 = user.flags).cronCount == null) {
base4.cronCount = 0;
}
user.flags.cronCount++;
analyticsData = {
category: 'behavior',
gaLabel: 'Cron Count',
gaValue: user.flags.cronCount,
uuid: user._id,
user: user,
resting: user.preferences.sleep,
cronCount: user.flags.cronCount,
progressUp: _.min([_progress.up, 900]),
progressDown: _progress.down
};
if ((ref3 = options.analytics) != null) {
ref3.track('Cron', analyticsData);
}
return _progress;
};

View File

@@ -1,5 +1,7 @@
import dotGet from '../libs/dotGet';
import _ from 'lodash';
module.exports = function(user, path) {
return dotGet(user, path);
// TODO remove completely, use _.get, only used in client
module.exports = function dotGet (user, path) {
return _.get(user, path);
};

View File

@@ -1,12 +1,14 @@
import dotSet from '../libs/dotSet';
import _ from 'lodash';
/*
This allows you to set object properties by dot-path. Eg, you can run pathSet('stats.hp',50,user) which is the same as
user.stats.hp = 50. This is useful because in our habitrpg-shared functions we're returning changesets as {path:value},
so that different consumers can implement setters their own way. Derby needs model.set(path, value) for example, where
Angular sets object properties directly - in which case, this function will be used.
*/
This allows you to set object properties by dot-path. Eg, you can run pathSet('stats.hp',50,user) which is the same as
user.stats.hp = 50. This is useful because in our habitrpg-shared functions we're returning changesets as {path:value},
so that different consumers can implement setters their own way. Derby needs model.set(path, value) for example, where
Angular sets object properties directly - in which case, this function will be used.
*/
module.exports = function(user, path, val) {
return dotSet(user, path, val);
// TODO use directly _.set and remove this fn, only used in client
module.exports = function dotSet (user, path, val) {
return _.set(user, path, val);
};

View File

@@ -1,11 +0,0 @@
import content from '../content/index';
import i18n from '../i18n';
module.exports = function(user, type) {
var item;
item = content.gear.flat[user.items.gear.equipped[type]];
if (!item) {
return content.gear.flat[type + "_base_0"];
}
return item;
};

View File

@@ -1,24 +1,23 @@
import content from '../content/index';
import i18n from '../i18n';
module.exports = function(user, item, type, req) {
var message, currentWeapon, currentShield;
if (type == null) {
type = 'equipped';
}
currentShield = content.gear.flat[user.items.gear[type].shield];
currentWeapon = content.gear.flat[user.items.gear[type].weapon];
module.exports = function handleTwoHanded (user, item, type = 'equipped', req = {}) {
let currentShield = content.gear.flat[user.items.gear[type].shield];
let currentWeapon = content.gear.flat[user.items.gear[type].weapon];
if (item.type === "shield" && (currentWeapon ? currentWeapon.twoHanded : false)) {
let message;
if (item.type === 'shield' && (currentWeapon ? currentWeapon.twoHanded : false)) {
user.items.gear[type].weapon = 'weapon_base_0';
message = i18n.t('messageTwoHandedUnequip', {
twoHandedText: currentWeapon.text(req.language), offHandedText: item.text(req.language),
}, req.language);
} else if (item.twoHanded && (currentShield && user.items.gear[type].shield != "shield_base_0")) {
user.items.gear[type].shield = "shield_base_0";
} else if (item.twoHanded && (currentShield && user.items.gear[type].shield !== 'shield_base_0')) {
user.items.gear[type].shield = 'shield_base_0';
message = i18n.t('messageTwoHandedEquip', {
twoHandedText: item.text(req.language), offHandedText: currentShield.text(req.language),
}, req.language);
}
return message;
};

View File

@@ -1,4 +1,3 @@
import getItem from './getItem';
import handleTwoHanded from './handleTwoHanded';
import predictableRandom from './predictableRandom';
import crit from './crit';
@@ -8,13 +7,10 @@ import dotGet from './dotGet';
import randomDrop from './randomDrop';
import autoAllocate from './autoAllocate';
import updateStats from './updateStats';
import cron from './cron';
import preenUserHistory from './preenUserHistory';
import ultimateGear from './ultimateGear';
import nullify from './nullify';
module.exports = {
getItem,
handleTwoHanded,
predictableRandom,
crit,
@@ -24,8 +20,6 @@ module.exports = {
randomDrop,
autoAllocate,
updateStats,
cron,
preenUserHistory,
ultimateGear,
nullify,
};

View File

@@ -1,5 +1,7 @@
module.exports = function(user) {
// TODO remove once v2 is retired
module.exports = function nullify (user) {
user.ops = null;
user.fns = null;
return user = null;
user = null;
};

View File

@@ -1,20 +1,24 @@
import _ from 'lodash';
/*
Because the same op needs to be performed on the client and the server (critical hits, item drops, etc),
we need things to be "random", but technically predictable so that they don't go out-of-sync
*/
module.exports = function(user, seed) {
var x;
// Because the same op needs to be performed on the client and the server (critical hits, item drops, etc),
// we need things to be "random", but technically predictable so that they don't go out-of-sync
module.exports = function predictableRandom (user, seed) {
if (!seed || seed === Math.PI) {
seed = _.reduce(user.stats, (function(m, v) {
if (_.isNumber(v)) {
return m + v;
let stats = user.stats.toObject ? user.stats.toObject() : user.stats;
// These items are not part of the stat object but exists on the server (see controllers/user#getUser)
// we remove them in order to use the same user.stats both on server and on client
stats = _.omit(stats, 'toNextLevel', 'maxHealth', 'maxMP');
seed = _.reduce(stats, (accumulator, val) => {
if (_.isNumber(val)) {
return accumulator + val;
} else {
return m;
return accumulator;
}
}), 0);
}, 0);
}
x = Math.sin(seed++) * 10000;
let x = Math.sin(seed++) * 10000;
return x - Math.floor(x);
};

View File

@@ -1,25 +0,0 @@
import _ from 'lodash';
import preenHistory from '../libs/preenHistory';
module.exports = function(user, minHistLen) {
if (minHistLen == null) {
minHistLen = 7;
}
_.each(user.habits.concat(user.dailys), function(task) {
var ref;
if (((ref = task.history) != null ? ref.length : void 0) > minHistLen) {
task.history = preenHistory(task.history);
}
return true;
});
_.defaults(user.history, {
todos: [],
exp: []
});
if (user.history.exp.length > minHistLen) {
user.history.exp = preenHistory(user.history.exp);
}
if (user.history.todos.length > minHistLen) {
return user.history.todos = preenHistory(user.history.todos);
}
};

View File

@@ -3,80 +3,118 @@ import content from '../content/index';
import i18n from '../i18n';
import { daysSince } from '../cron';
import { diminishingReturns } from '../statHelpers';
import predictableRandom from './predictableRandom';
import randomVal from './randomVal';
// Clone a drop object maintaining its functions so that we can change it without affecting the original item
function cloneDropItem (drop) {
return _.cloneDeep(drop, function (val) {
return _.cloneDeep(drop, (val) => {
return _.isFunction(val) ? val : undefined; // undefined will be handled by lodash
});
}
module.exports = function(user, modifiers, req) {
var acceptableDrops, base, base1, base2, chance, drop, dropK, dropMultiplier, name, name1, name2, quest, rarity, ref, ref1, ref2, ref3, task;
module.exports = function randomDrop (user, modifiers, req = {}) {
let acceptableDrops;
let chance;
let drop;
let dropK;
let dropMultiplier;
let quest;
let rarity;
let task;
task = modifiers.task;
chance = _.min([Math.abs(task.value - 21.27), 37.5]) / 150 + .02;
chance *= task.priority * (1 + (task.streak / 100 || 0)) * (1 + (user._statsComputed.per / 100)) * (1 + (user.contributor.level / 40 || 0)) * (1 + (user.achievements.rebirths / 20 || 0)) * (1 + (user.achievements.streak / 200 || 0)) * (user._tmp.crit || 1) * (1 + .5 * (_.reduce(task.checklist, (function(m, i) {
return m + (i.completed ? 1 : 0);
}), 0) || 0));
chance = _.min([Math.abs(task.value - 21.27), 37.5]) / 150 + 0.02;
chance *= task.priority * // Task priority: +50% for Medium, +100% for Hard
(1 + (task.streak / 100 || 0)) * // Streak bonus: +1% per streak
(1 + user._statsComputed.per / 100) * // PERception: +1% per point
(1 + (user.contributor.level / 40 || 0)) * // Contrib levels: +2.5% per level
(1 + (user.achievements.rebirths / 20 || 0)) * // Rebirths: +5% per achievement
(1 + (user.achievements.streak / 200 || 0)) * // Streak achievements: +0.5% per achievement
(user._tmp.crit || 1) * (1 + 0.5 * (_.reduce(task.checklist, (m, i) => {
return m + (i.completed ? 1 : 0); // +50% per checklist item complete. TODO: make this into X individual drop chances instead
}, 0) || 0));
chance = diminishingReturns(chance, 0.75);
quest = content.quests[(ref = user.party.quest) != null ? ref.key : void 0];
if ((quest != null ? quest.collect : void 0) && user.fns.predictableRandom(user.stats.gp) < chance) {
dropK = user.fns.randomVal(quest.collect, {
key: true
if (user.party.quest.key)
quest = content.quests[user.party.quest.key];
if (quest && quest.collect && predictableRandom(user, user.stats.gp) < chance) {
dropK = randomVal(user, quest.collect, {
key: true,
});
if (!user.party.quest.progress.collect[dropK])
user.party.quest.progress.collect[dropK] = 0;
user.party.quest.progress.collect[dropK]++;
if (typeof user.markModified === "function") {
user.markModified('party.quest.progress');
}
user.markModified('party.quest.progress');
}
dropMultiplier = ((ref1 = user.purchased) != null ? (ref2 = ref1.plan) != null ? ref2.customerId : void 0 : void 0) ? 2 : 1;
if ((daysSince(user.items.lastDrop.date, user.preferences) === 0) && (user.items.lastDrop.count >= dropMultiplier * (5 + Math.floor(user._statsComputed.per / 25) + (user.contributor.level || 0)))) {
if (user.purchased && user.purchased.plan && user.purchased.plan.customerId) {
dropMultiplier = 2;
} else {
dropMultiplier = 1;
}
if (daysSince(user.items.lastDrop.date, user.preferences) === 0 &&
user.items.lastDrop.count >= dropMultiplier * (5 + Math.floor(user._statsComputed.per / 25) + (user.contributor.level || 0))) {
return;
}
if (((ref3 = user.flags) != null ? ref3.dropsEnabled : void 0) && user.fns.predictableRandom(user.stats.exp) < chance) {
rarity = user.fns.predictableRandom(user.stats.gp);
if (rarity > .6) {
drop = cloneDropItem(user.fns.randomVal(_.where(content.food, {
canDrop: true
if (user.flags && user.flags.dropsEnabled && predictableRandom(user, user.stats.exp) < chance) {
rarity = predictableRandom(user, user.stats.gp);
if (rarity > 0.6) { // food 40% chance
drop = cloneDropItem(randomVal(user, _.where(content.food, {
canDrop: true,
})));
if ((base = user.items.food)[name = drop.key] == null) {
base[name] = 0;
if (!user.items.food[drop.key]) {
user.items.food[drop.key] = 0;
}
user.items.food[drop.key] += 1;
drop.type = 'Food';
drop.dialog = i18n.t('messageDropFood', {
dropArticle: drop.article,
dropText: drop.text(req.language),
dropNotes: drop.notes(req.language)
dropNotes: drop.notes(req.language),
}, req.language);
} else if (rarity > .3) {
drop = cloneDropItem(user.fns.randomVal(content.dropEggs));
if ((base1 = user.items.eggs)[name1 = drop.key] == null) {
base1[name1] = 0;
} else if (rarity > 0.3) { // eggs 30% chance
drop = cloneDropItem(randomVal(user, content.dropEggs));
if (!user.items.eggs[drop.key]) {
user.items.eggs[drop.key] = 0;
}
user.items.eggs[drop.key]++;
drop.type = 'Egg';
drop.dialog = i18n.t('messageDropEgg', {
dropText: drop.text(req.language),
dropNotes: drop.notes(req.language)
dropNotes: drop.notes(req.language),
}, req.language);
} else {
acceptableDrops = rarity < .02 ? ['Golden'] : rarity < .09 ? ['Zombie', 'CottonCandyPink', 'CottonCandyBlue'] : rarity < .18 ? ['Red', 'Shade', 'Skeleton'] : ['Base', 'White', 'Desert'];
drop = cloneDropItem(user.fns.randomVal(_.pick(content.hatchingPotions, (function(v, k) {
} else { // Hatching Potion, 30% chance - break down by rarity.
if (rarity < 0.02) { // Very Rare: 10% (of 30%)
acceptableDrops = ['Golden'];
} else if (rarity < 0.09) { // Rare: 20% of 30%
acceptableDrops = ['Zombie', 'CottonCandyPink', 'CottonCandyBlue'];
} else if (rarity < 0.18) { // uncommon: 30% of 30%
acceptableDrops = ['Red', 'Shade', 'Skeleton'];
} else { // common, 40% of 30%
acceptableDrops = ['Base', 'White', 'Desert'];
}
drop = cloneDropItem(randomVal(user, _.pick(content.hatchingPotions, (v, k) => {
return acceptableDrops.indexOf(k) >= 0;
}))));
if ((base2 = user.items.hatchingPotions)[name2 = drop.key] == null) {
base2[name2] = 0;
})));
if (!user.items.hatchingPotions[drop.key]) {
user.items.hatchingPotions[drop.key] = 0;
}
user.items.hatchingPotions[drop.key]++;
drop.type = 'HatchingPotion';
drop.dialog = i18n.t('messageDropPotion', {
dropText: drop.text(req.language),
dropNotes: drop.notes(req.language)
dropNotes: drop.notes(req.language),
}, req.language);
}
user._tmp.drop = drop;
user.items.lastDrop.date = +(new Date);
return user.items.lastDrop.count++;
user.items.lastDrop.date = Number(new Date());
user.items.lastDrop.count++;
}
};

View File

@@ -1,14 +1,12 @@
import _ from 'lodash';
import predictableRandom from './predictableRandom';
/*
Get a random property from an object
returns random property (the value)
*/
// Get a random property from an object
// returns random property (the value)
module.exports = function(user, obj, options) {
var array, rand;
array = (options != null ? options.key : void 0) ? _.keys(obj) : _.values(obj);
rand = user.fns.predictableRandom(options != null ? options.seed : void 0);
module.exports = function randomVal (user, obj, options = {}) {
let array = options.key ? _.keys(obj) : _.values(obj);
let rand = predictableRandom(user, options.seed);
array.sort();
return array[Math.floor(rand * array.length)];
};

View File

@@ -0,0 +1,25 @@
import _ from 'lodash';
import content from '../content/index';
module.exports = function resetGear (user) {
let gear = user.items.gear;
_.each(['equipped', 'costume'], function resetUserGear (type) {
gear[type] = {};
gear[type].armor = 'armor_base_0';
gear[type].weapon = 'weapon_warrior_0';
gear[type].head = 'head_base_0';
gear[type].shield = 'shield_base_0';
});
// Gear.owned is a Mongo object so the _.each function iterates over hidden properties.
// The content.gear.flat[k] check should prevent this causing an error
_.each(gear.owned, function resetOwnedGear (v, k) {
if (gear.owned[k] && content.gear.flat[k] && content.gear.flat[k].value) {
gear.owned[k] = false;
}
});
gear.owned.weapon_warrior_0 = true; // eslint-disable-line camelcase
user.preferences.costume = false;
};

View File

@@ -1,33 +1,23 @@
import content from '../content/index';
import _ from 'lodash';
module.exports = function(user) {
var base, owned;
owned = typeof window !== "undefined" && window !== null ? user.items.gear.owned : user.items.gear.owned.toObject();
if ((base = user.achievements).ultimateGearSets == null) {
base.ultimateGearSets = {
healer: false,
wizard: false,
rogue: false,
warrior: false
};
}
content.classes.forEach(function(klass) {
module.exports = function ultimateGear (user) {
let owned = typeof window !== 'undefined' ? user.items.gear.owned : user.items.gear.owned.toObject();
content.classes.forEach((klass) => {
if (user.achievements.ultimateGearSets[klass] !== true) {
return user.achievements.ultimateGearSets[klass] = _.reduce(['armor', 'shield', 'head', 'weapon'], function(soFarGood, type) {
var found;
found = _.find(content.gear.tree[type][klass], {
last: true
user.achievements.ultimateGearSets[klass] = _.reduce(['armor', 'shield', 'head', 'weapon'], (soFarGood, type) => {
let found = _.find(content.gear.tree[type][klass], {
last: true,
});
return soFarGood && (!found || owned[found.key] === true);
}, true);
}
});
if (typeof user.markModified === "function") {
user.markModified('achievements.ultimateGearSets');
}
if (_.contains(user.achievements.ultimateGearSets, true) && user.flags.armoireEnabled !== true) {
user.flags.armoireEnabled = true;
return typeof user.markModified === "function" ? user.markModified('flags') : void 0;
}
return;
};

View File

@@ -1,21 +1,19 @@
import _ from 'lodash';
import {
MAX_HEALTH,
MAX_STAT_POINTS
MAX_STAT_POINTS,
} from '../constants';
import { toNextLevel } from '../statHelpers';
module.exports = function (user, stats, req, analytics) {
import autoAllocate from './autoAllocate';
module.exports = function updateStats (user, stats, req = {}, analytics) {
let allocatedStatPoints;
let totalStatPoints;
let experienceToNextLevel;
if (stats.hp <= 0) {
user.stats.hp = 0;
return user.stats.hp;
}
user.stats.hp = stats.hp;
user.stats.gp = stats.gp >= 0 ? stats.gp : 0;
user.stats.hp = stats.hp > 0 ? stats.hp : 0;
user.stats.gp = stats.gp > 0 ? stats.gp : 0;
if (!user._tmp) user._tmp = {};
experienceToNextLevel = toNextLevel(user.stats.lvl);
@@ -35,7 +33,7 @@ module.exports = function (user, stats, req, analytics) {
continue; // eslint-disable-line no-continue
}
if (user.preferences.automaticAllocation) {
user.fns.autoAllocate();
autoAllocate(user);
} else {
user.stats.points = user.stats.lvl - allocatedStatPoints;
totalStatPoints = user.stats.points + allocatedStatPoints;
@@ -52,7 +50,6 @@ module.exports = function (user, stats, req, analytics) {
}
user.stats.exp = stats.exp;
user.flags = user.flags || {};
if (!user.flags.customizationsNotification && (user.stats.exp > 5 || user.stats.lvl > 1)) {
user.flags.customizationsNotification = true;
@@ -62,48 +59,39 @@ module.exports = function (user, stats, req, analytics) {
}
if (!user.flags.dropsEnabled && user.stats.lvl >= 3) {
user.flags.dropsEnabled = true;
if (user.items.eggs["Wolf"] > 0) {
user.items.eggs["Wolf"]++;
if (user.items.eggs.Wolf > 0) {
user.items.eggs.Wolf++;
} else {
user.items.eggs["Wolf"] = 1;
user.items.eggs.Wolf = 1;
}
}
if (!user.flags.classSelected && user.stats.lvl >= 10) {
user.flags.classSelected;
}
_.each({
vice1: 30,
atom1: 15,
moonstone1: 60,
goldenknight1: 40
}, function(lvl, k) {
var analyticsData, base, base1, ref;
if (!((ref = user.flags.levelDrops) != null ? ref[k] : void 0) && user.stats.lvl >= lvl) {
if ((base = user.items.quests)[k] == null) {
base[k] = 0;
}
goldenknight1: 40,
}, (lvl, k) => {
if (user.stats.lvl >= lvl && !user.flags.levelDrops[k]) {
user.flags.levelDrops[k] = true;
if (!user.items.quests[k])
user.items.quests[k] = 0;
user.items.quests[k]++;
((base1 = user.flags).levelDrops != null ? base1.levelDrops : base1.levelDrops = {})[k] = true;
if (typeof user.markModified === "function") {
user.markModified('flags.levelDrops');
user.markModified('flags.levelDrops');
if (analytics) {
analytics.track('acquire item', {
uuid: user._id,
itemKey: k,
acquireMethod: 'Level Drop',
category: 'behavior',
});
}
analyticsData = {
uuid: user._id,
itemKey: k,
acquireMethod: 'Level Drop',
category: 'behavior'
};
if (analytics != null) {
analytics.track('acquire item', analyticsData);
}
if (!user._tmp) user._tmp = {}
return user._tmp.drop = {
user._tmp.drop = {
type: 'Quest',
key: k
key: k,
};
}
});
if (!user.flags.rebirthEnabled && (user.stats.lvl >= 50 || user.achievements.beastMaster)) {
return user.flags.rebirthEnabled = true;
user.flags.rebirthEnabled = true;
}
};

View File

@@ -1,85 +1,197 @@
import moment from 'moment';
import _ from 'lodash';
import {
daysSince,
shouldDo,
} from './cron';
// When using a common module from the website or the server NEVER import the module directly
// but access it through `api` (the main common) module, otherwise you would require the non transpiled version of the file in production.
let api = module.exports = {};
import content from './content/index';
api.content = content;
import * as errors from './libs/errors';
api.errors = errors;
import i18n from './i18n';
api.i18n = i18n;
// TODO under api.libs.cron?
import { shouldDo, daysSince } from './cron';
api.shouldDo = shouldDo;
api.daysSince = daysSince;
// TODO under api.constants? and capitalize exported names too
import {
MAX_HEALTH,
MAX_LEVEL,
MAX_STAT_POINTS,
TAVERN_ID,
} from './constants';
import * as statHelpers from './statHelpers';
import importedLibs from './libs';
var $w, preenHistory, sortOrder,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
import content from './content/index';
import i18n from './i18n';
let api = module.exports = {};
api.i18n = i18n;
api.shouldDo = shouldDo;
api.maxLevel = MAX_LEVEL;
api.capByLevel = statHelpers.capByLevel;
api.maxHealth = MAX_HEALTH;
api.maxStatPoints = MAX_STAT_POINTS;
api.TAVERN_ID = TAVERN_ID;
// TODO under api.libs.statHelpers?
import * as statHelpers from './statHelpers';
api.capByLevel = statHelpers.capByLevel;
api.tnl = statHelpers.toNextLevel;
api.diminishingReturns = statHelpers.diminishingReturns;
$w = api.$w = importedLibs.splitWhitespace;
api.dotSet = importedLibs.dotSet;
api.dotGet = importedLibs.dotGet;
api.refPush = importedLibs.refPush;
api.planGemLimits = importedLibs.planGemLimits;
import splitWhitespace from './libs/splitWhitespace';
api.$w = splitWhitespace;
preenHistory = importedLibs.preenHistory;
import dotSet from './libs/dotSet';
api.dotSet = dotSet;
api.preenTodos = importedLibs.preenTodos;
api.updateStore = importedLibs.updateStore;
import dotGet from './libs/dotGet';
api.dotGet = dotGet;
import refPush from './libs/refPush';
api.refPush = refPush;
/*
------------------------------------------------------
Content
------------------------------------------------------
*/
import planGemLimits from './libs/planGemLimits';
api.planGemLimits = planGemLimits;
api.content = content;
import preenTodos from './libs/preenTodos';
api.preenTodos = preenTodos;
import updateStore from './libs/updateStore';
api.updateStore = updateStore;
/*
------------------------------------------------------
Misc Helpers
------------------------------------------------------
*/
import uuid from './libs/uuid';
api.uuid = uuid;
api.uuid = importedLibs.uuid;
api.countExists = importedLibs.countExists;
api.taskDefaults = importedLibs.taskDefaults;
api.percent = importedLibs.percent;
api.removeWhitespace = importedLibs.removeWhitespace;
api.encodeiCalLink = importedLibs.encodeiCalLink;
api.gold = importedLibs.gold;
api.silver = importedLibs.silver;
api.taskClasses = importedLibs.taskClasses;
api.friendlyTimestamp = importedLibs.friendlyTimestamp;
api.newChatMessages = importedLibs.newChatMessages;
api.noTags = importedLibs.noTags;
api.appliedTags = importedLibs.appliedTags;
import taskDefaults from './libs/taskDefaults';
api.taskDefaults = taskDefaults;
import percent from './libs/percent';
api.percent = percent;
/*
Various counting functions
*/
import gold from './libs/gold';
api.gold = gold;
import silver from './libs/silver';
api.silver = silver;
import taskClasses from './libs/taskClasses';
api.taskClasses = taskClasses;
import noTags from './libs/noTags';
api.noTags = noTags;
import appliedTags from './libs/appliedTags';
api.appliedTags = appliedTags;
import pickDeep from './libs/pickDeep';
api.pickDeep = pickDeep;
import count from './count';
api.count = count;
import statsComputed from './libs/statsComputed';
api.statsComputed = statsComputed;
import autoAllocate from './fns/autoAllocate';
import crit from './fns/crit';
import handleTwoHanded from './fns/handleTwoHanded';
import predictableRandom from './fns/predictableRandom';
import randomDrop from './fns/randomDrop';
import randomVal from './fns/randomVal';
import resetGear from './fns/resetGear';
import ultimateGear from './fns/ultimateGear';
import updateStats from './fns/updateStats';
api.fns = {
autoAllocate,
crit,
handleTwoHanded,
predictableRandom,
randomDrop,
randomVal,
resetGear,
ultimateGear,
updateStats,
};
import scoreTask from './ops/scoreTask';
import sleep from './ops/sleep';
import allocate from './ops/allocate';
import buy from './ops/buy';
import buyGear from './ops/buyGear';
import buyHealthPotion from './ops/buyHealthPotion';
import buyArmoire from './ops/buyArmoire';
import buyMysterySet from './ops/buyMysterySet';
import buyQuest from './ops/buyQuest';
import buySpecialSpell from './ops/buySpecialSpell';
import allocateNow from './ops/allocateNow';
import hatch from './ops/hatch';
import feed from './ops/feed';
import equip from './ops/equip';
import changeClass from './ops/changeClass';
import disableClasses from './ops/disableClasses';
import purchase from './ops/purchase';
import purchaseHourglass from './ops/hourglassPurchase';
import readCard from './ops/readCard';
import openMysteryItem from './ops/openMysteryItem';
import addWebhook from './ops/addWebhook';
import updateWebhook from './ops/updateWebhook';
import deleteWebhook from './ops/deleteWebhook';
import releasePets from './ops/releasePets';
import releaseBoth from './ops/releaseBoth';
import releaseMounts from './ops/releaseMounts';
import updateTask from './ops/updateTask';
import clearCompleted from './ops/clearCompleted';
import sell from './ops/sell';
import unlock from './ops/unlock';
import revive from './ops/revive';
import rebirth from './ops/rebirth';
import blockUser from './ops/blockUser';
import clearPMs from './ops/clearPMs';
import deletePM from './ops/deletePM';
import reroll from './ops/reroll';
import addPushDevice from './ops/addPushDevice';
import reset from './ops/reset';
import markPmsRead from './ops/markPMSRead';
api.ops = {
scoreTask,
sleep,
allocate,
buy,
buyGear,
buyHealthPotion,
buyArmoire,
buyMysterySet,
buySpecialSpell,
buyQuest,
allocateNow,
hatch,
feed,
equip,
changeClass,
disableClasses,
purchase,
purchaseHourglass,
readCard,
openMysteryItem,
addWebhook,
updateWebhook,
deleteWebhook,
releasePets,
releaseBoth,
releaseMounts,
updateTask,
clearCompleted,
sell,
unlock,
revive,
rebirth,
blockUser,
clearPMs,
deletePM,
reroll,
addPushDevice,
reset,
markPmsRead,
};
/*
------------------------------------------------------
@@ -87,10 +199,9 @@ User (prototype wrapper to give it ops, helper funcs, and virtuals
------------------------------------------------------
*/
/*
User is now wrapped (both on client and server), adding a few new properties:
* getters (_statsComputed, tasks, etc)
* getters (_statsComputed)
* user.fns, which is a bunch of helper functions
These were originally up above, but they make more sense belonging to the user object so we don't have to pass
the user object all over the place. In fact, we should pull in more functions such as cron(), updateStats(), etc.
@@ -121,14 +232,16 @@ TODO
import importedOps from './ops';
import importedFns from './fns';
api.wrap = function(user, main) {
if (main == null) {
main = true;
}
if (user._wrapped) {
return;
}
// TODO Kept for the client side
api.wrap = function wrapUser (user, main = true) {
if (user._wrapped) return;
user._wrapped = true;
// Make markModified available on the client side as a noop function
if (!user.markModified) {
user.markModified = function noopMarkModified () {};
}
if (main) {
user.ops = {
update: _.partial(importedOps.update, user),
@@ -163,6 +276,9 @@ api.wrap = function(user, main) {
releaseMounts: _.partial(importedOps.releaseMounts, user),
releaseBoth: _.partial(importedOps.releaseBoth, user),
buy: _.partial(importedOps.buy, user),
buyHealthPotion: _.partial(importedOps.buyHealthPotion, user),
buyArmoire: _.partial(importedOps.buyArmoire, user),
buyGear: _.partial(importedOps.buyGear, user),
buyQuest: _.partial(importedOps.buyQuest, user),
buyMysterySet: _.partial(importedOps.buyMysterySet, user),
hourglassPurchase: _.partial(importedOps.hourglassPurchase, user),
@@ -175,11 +291,12 @@ api.wrap = function(user, main) {
allocate: _.partial(importedOps.allocate, user),
readCard: _.partial(importedOps.readCard, user),
openMysteryItem: _.partial(importedOps.openMysteryItem, user),
score: _.partial(importedOps.score, user),
score: _.partial(importedOps.scoreTask, user),
markPmsRead: _.partial(importedOps.markPmsRead, user),
};
}
user.fns = {
getItem: _.partial(importedFns.getItem, user),
handleTwoHanded: _.partial(importedFns.handleTwoHanded, user),
predictableRandom: _.partial(importedFns.predictableRandom, user),
crit: _.partial(importedFns.crit, user),
@@ -189,34 +306,14 @@ api.wrap = function(user, main) {
randomDrop: _.partial(importedFns.randomDrop, user),
autoAllocate: _.partial(importedFns.autoAllocate, user),
updateStats: _.partial(importedFns.updateStats, user),
cron: _.partial(importedFns.cron, user),
preenUserHistory: _.partial(importedFns.preenUserHistory, user),
statsComputed: _.partial(statsComputed, user),
ultimateGear: _.partial(importedFns.ultimateGear, user),
nullify: _.partial(importedFns.nullify, user),
};
Object.defineProperty(user, '_statsComputed', {
get: function() {
var computed;
computed = _.reduce(['per', 'con', 'str', 'int'], (function(_this) {
return function(m, stat) {
m[stat] = _.reduce($w('stats stats.buffs items.gear.equipped.weapon items.gear.equipped.armor items.gear.equipped.head items.gear.equipped.shield'), function(m2, path) {
var item, val;
val = user.fns.dotGet(path);
return m2 + (~path.indexOf('items.gear') ? (item = content.gear.flat[val], (+(item != null ? item[stat] : void 0) || 0) * ((item != null ? item.klass : void 0) === user.stats["class"] || (item != null ? item.specialClass : void 0) === user.stats["class"] ? 1.5 : 1)) : +val[stat] || 0);
}, 0);
m[stat] += Math.floor(api.capByLevel(user.stats.lvl) / 2);
return m;
};
})(this), {});
computed.maxMP = computed.int * 2 + 30;
return computed;
}
});
return Object.defineProperty(user, 'tasks', {
get: function() {
var tasks;
tasks = user.habits.concat(user.dailys).concat(user.todos).concat(user.rewards);
return _.object(_.pluck(tasks, "id"), tasks);
}
get () {
return statsComputed(user);
},
});
};

View File

@@ -1,19 +1,14 @@
import _ from 'lodash';
/*
Are there tags applied?
*/
module.exports = function(userTags, taskTags) {
var arr;
arr = [];
_.each(userTags, function(t) {
if (t == null) {
return;
}
if (taskTags != null ? taskTags[t.id] : void 0) {
return arr.push(t.name);
}
// TODO move to client
module.exports = function appliedTags (userTags, taskTags = []) {
let arr = userTags.filter(tag => {
return taskTags.indexOf(tag.id) !== -1;
}).map(tag => {
return tag.name;
});
return arr.join(', ');
};

View File

@@ -1,7 +0,0 @@
import _ from 'lodash';
module.exports = function(items) {
return _.reduce(items, (function(m, v) {
return m + (v ? 1 : 0);
}), 0);
};

View File

@@ -1,9 +1,5 @@
import _ from 'lodash';
module.exports = function(obj, path) {
return _.reduce(path.split('.'), ((function(_this) {
return function(curr, next) {
return curr != null ? curr[next] : void 0;
};
})(this)), obj);
};
// TODO remove completely, only used in client
module.exports = _.get;

View File

@@ -1,14 +1,5 @@
import _ from 'lodash';
module.exports = function(obj, path, val) {
var arr;
arr = path.split('.');
return _.reduce(arr, (function(_this) {
return function(curr, next, index) {
if ((arr.length - 1) === index) {
curr[next] = val;
}
return curr[next] != null ? curr[next] : curr[next] = {};
};
})(this), obj);
};
// TODO remove completely, only used in client
module.exports = _.set;

View File

@@ -1,9 +0,0 @@
/*
Encode the download link for .ics iCal file
*/
module.exports = function(uid, apiToken) {
var loc, ref;
loc = (typeof window !== "undefined" && window !== null ? window.location.host : void 0) || (typeof process !== "undefined" && process !== null ? (ref = process.env) != null ? ref.BASE_URL : void 0 : void 0) || '';
return encodeURIComponent("http://" + loc + "/v1/users/" + uid + "/calendar.ics?apiToken=" + apiToken);
};

View File

@@ -0,0 +1,42 @@
import extendableBuiltin from './extendableBuiltin';
// Base class for custom application errors
// It extends Error and capture the stack trace
export class CustomError extends extendableBuiltin(Error) {
constructor () {
super();
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
// We specify an httpCode for all errors so that they can be used in the API too
export class NotAuthorized extends CustomError {
constructor (customMessage) {
super();
this.name = this.constructor.name;
this.httpCode = 401;
this.message = customMessage || 'Not authorized.';
}
}
export class BadRequest extends CustomError {
constructor (customMessage) {
super();
this.name = this.constructor.name;
this.httpCode = 400;
this.message = customMessage || 'Bad request.';
}
}
export class NotFound extends CustomError {
constructor (customMessage) {
super();
this.name = this.constructor.name;
this.httpCode = 404;
this.message = customMessage || 'Not found.';
}
}

View File

@@ -0,0 +1,11 @@
// Babel 6 doesn't support extending native class (Error, Array, ...)
// This function makes it possible to extend native classes with the same results as Babel 5
module.exports = function extendableBuiltin (klass) {
function ExtendableBuiltin () {
klass.apply(this, arguments);
}
ExtendableBuiltin.prototype = Object.create(klass.prototype);
Object.setPrototypeOf(ExtendableBuiltin, klass);
return ExtendableBuiltin;
};

View File

@@ -1,9 +0,0 @@
import moment from 'moment';
/*
Friendly timestamp
*/
module.exports = function(timestamp) {
return moment(timestamp).format('MM/DD h:mm:ss a');
};

View File

@@ -1,7 +1,9 @@
module.exports = function(num) {
// TODO move to client
module.exports = function gold (num) {
if (num) {
return Math.floor(num);
} else {
return "0";
return '0';
}
};

View File

@@ -1,46 +0,0 @@
import uuid from './uuid';
import taskDefaults from './taskDefaults';
import refPush from './refPush';
import splitWhitespace from './splitWhitespace';
import planGemLimits from './planGemLimits';
import preenTodos from './preenTodos';
import dotSet from './dotSet';
import dotGet from './dotGet';
import preenHistory from './preenHistory';
import countExists from './countExists';
import updateStore from './updateStore';
import appliedTags from './appliedTags';
import encodeiCalLink from './encodeiCalLink';
import friendlyTimestamp from './friendlyTimestamp';
import gold from './gold';
import newChatMessages from './newChatMessages';
import noTags from './noTags';
import percent from './percent';
import removeWhitespace from './removeWhitespace';
import silver from './silver';
import taskClasses from './taskClasses';
module.exports = {
uuid,
taskDefaults,
refPush,
splitWhitespace,
planGemLimits,
preenTodos,
dotSet,
dotGet,
preenHistory,
countExists,
updateStore,
appliedTags,
encodeiCalLink,
friendlyTimestamp,
gold,
newChatMessages,
noTags,
percent,
removeWhitespace,
silver,
taskClasses,
};

View File

@@ -1,10 +0,0 @@
/*
Does user have new chat messages?
*/
module.exports = function(messages, lastMessageSeen) {
if (!((messages != null ? messages.length : void 0) > 0)) {
return false;
}
return (messages != null ? messages[0] : void 0) && (messages[0].id !== lastMessageSeen);
};

View File

@@ -4,8 +4,10 @@ import _ from 'lodash';
are any tags active?
*/
module.exports = function(tags) {
return _.isEmpty(tags) || _.isEmpty(_.filter(tags, function(t) {
// TODO move to client
module.exports = function noTags (tags) {
return _.isEmpty(tags) || _.isEmpty(_.filter(tags, (t) => {
return t;
}));
};

View File

@@ -1,10 +1,12 @@
module.exports = function(x, y, dir) {
var roundFn;
// TODO move to client
module.exports = function percent (x, y, dir) {
let roundFn;
switch (dir) {
case "up":
case 'up':
roundFn = Math.ceil;
break;
case "down":
case 'down':
roundFn = Math.floor;
break;
default:

View File

@@ -0,0 +1,13 @@
// An utility to pick deep properties from an object.
// Works like _.pick but supports nested props (ie pickDeep(obj, ['deep.property']))
import _ from 'lodash';
module.exports = function pickDeep (obj, properties) {
if (!_.isArray(properties)) throw new Error('"properties" must be an array');
let result = {};
_.each(properties, (prop) => _.set(result, prop, _.get(obj, prop)));
return result;
};

View File

@@ -1,43 +0,0 @@
import moment from 'moment';
import _ from 'lodash';
/*
Preen history for users with > 7 history entries
This takes an infinite array of single day entries [day day day day day...], and turns it into a condensed array
of averages, condensing more the further back in time we go. Eg, 7 entries each for last 7 days; 1 entry each week
of this month; 1 entry for each month of this year; 1 entry per previous year: [day*7 week*4 month*12 year*infinite]
*/
module.exports = function(history) {
var newHistory, preen, thisMonth;
history = _.filter(history, function(h) {
return !!h;
});
newHistory = [];
preen = function(amount, groupBy) {
var groups;
groups = _.chain(history).groupBy(function(h) {
return moment(h.date).format(groupBy);
}).sortBy(function(h, k) {
return k;
}).value();
groups = groups.slice(-amount);
groups.pop();
return _.each(groups, function(group) {
newHistory.push({
date: moment(group[0].date).toDate(),
value: _.reduce(group, (function(m, obj) {
return m + obj.value;
}), 0) / group.length
});
return true;
});
};
preen(50, "YYYY");
preen(moment().format('MM'), "YYYYMM");
thisMonth = moment().format('YYYYMM');
newHistory = newHistory.concat(_.filter(history, function(h) {
return moment(h.date).format('YYYYMM') === thisMonth;
}));
return newHistory;
};

View File

@@ -1,14 +1,12 @@
import moment from 'moment';
import _ from 'lodash';
/*
Preen 3-day past-completed To-Dos from Angular & mobile app
*/
// TODO used only in v2
module.exports = function(tasks) {
return _.filter(tasks, function(t) {
return !t.completed || (t.challenge && t.challenge.id) || moment(t.dateCompleted).isAfter(moment().subtract({
days: 3
module.exports = function preenTodos (tasks) {
return _.filter(tasks, (t) => {
return !t.completed || t.challenge && t.challenge.id || moment(t.dateCompleted).isAfter(moment().subtract({
days: 3,
}));
});
};

View File

@@ -7,13 +7,14 @@ import uuid from './uuid';
no problem. To maintain sorting, we use these helper functions:
*/
module.exports = function(reflist, item, prune) {
if (prune == null) {
prune = 0;
}
module.exports = function refPush (reflist, item) {
item.sort = _.isEmpty(reflist) ? 0 : _.max(reflist, 'sort').sort + 1;
if (!(item.id && !reflist[item.id])) {
item.id = uuid();
}
return reflist[item.id] = item;
reflist[item.id] = item;
return reflist[item.id];
};

View File

@@ -1,10 +0,0 @@
/*
Remove whitespace #FIXME are we using this anywwhere? Should we be?
*/
module.exports = function(str) {
if (!str) {
return '';
}
return str.replace(/\s/g, '');
};

View File

@@ -2,10 +2,13 @@
Silver amount from their money
*/
module.exports = function(num) {
// TODO move to client
module.exports = function silver (num) {
if (num) {
return ("0" + Math.floor((num - Math.floor(num)) * 100)).slice(-2);
let centCount = Math.floor((num - Math.floor(num)) * 100);
return `0${centCount}`.slice(-2);
} else {
return "00";
return '00';
}
};

View File

@@ -1,3 +1,4 @@
module.exports = function(s) {
module.exports = function splitWhitespace (s) {
return s.split(' ');
};

View File

@@ -0,0 +1,28 @@
import _ from 'lodash';
import content from '../content/index';
import * as statHelpers from '../statHelpers';
module.exports = function statsComputed (user) {
let paths = ['stats', 'stats.buffs', 'items.gear.equipped.weapon', 'items.gear.equipped.armor',
'items.gear.equipped.head', 'items.gear.equipped.shield'];
let computed = _.reduce(['per', 'con', 'str', 'int'], (m, stat) => {
m[stat] = _.reduce(paths, (m2, path) => {
let val = _.get(user, path);
let item = content.gear.flat[val];
if (!item) item = {};
if (!item[stat]) {
item[stat] = 0;
} else {
item[stat] = Number(item[stat]);
}
let thisMultiplier = item.klass === user.stats.class || item.specialClass === user.stats.class ? 1.5 : 1;
let thisReturn = path.indexOf('items.gear') !== -1 ? item[stat] * thisMultiplier : Number(val[stat]);
return m2 + thisReturn || 0;
}, 0);
m[stat] += Math.floor(statHelpers.capByLevel(user.stats.lvl) / 2);
return m;
}, {});
computed.maxMP = computed.int * 2 + 30;
return computed;
};

View File

@@ -1,51 +1,45 @@
import {
shouldDo
shouldDo,
} from '../cron';
/*
Task classes given everything about the class
*/
module.exports = function(task, filters, dayStart, lastCron, showCompleted, main) {
var classes, completed, enabled, filter, priority, ref, repeat, type, value;
if (filters == null) {
filters = [];
}
if (dayStart == null) {
dayStart = 0;
}
if (lastCron == null) {
lastCron = +(new Date);
}
if (showCompleted == null) {
showCompleted = false;
}
if (main == null) {
main = false;
}
// TODO move to the client
module.exports = function taskClasses (task, filters = [], dayStart = 0, lastCron = Number(new Date()), showCompleted = false, main = false) {
if (!task) {
return;
return '';
}
type = task.type, completed = task.completed, value = task.value, repeat = task.repeat, priority = task.priority;
if (main) {
if (!task._editing) {
for (filter in filters) {
enabled = filters[filter];
if (enabled && !((ref = task.tags) != null ? ref[filter] : void 0)) {
return 'hidden';
}
let type = task.type;
let classes = task.type;
let completed = task.completed;
let value = task.value;
let priority = task.priority;
if (main && !task._editing) {
for (let filter in filters) {
let enabled = filters[filter];
if (!task.tags) task.tags = [];
if (enabled && task.tags.indexOf(filter) === -1) {
return 'hidden';
}
}
}
classes = type;
classes = task.type;
if (task._editing) {
classes += " beingEdited";
classes += ' beingEdited';
}
if (type === 'todo' || type === 'daily') {
if (completed || (type === 'daily' && !shouldDo(+(new Date), task, {
dayStart: dayStart
if (completed || (type === 'daily' && !shouldDo(Number(new Date()), task, { // eslint-disable-line no-extra-parens
dayStart,
}))) {
classes += " completed";
classes += ' completed';
} else {
classes += " uncompleted";
classes += ' uncompleted';
}
} else if (type === 'habit') {
if (task.down && task.up) {
@@ -55,6 +49,7 @@ module.exports = function(task, filters, dayStart, lastCron, showCompleted, main
classes += ' habit-narrow';
}
}
if (priority === 0.1) {
classes += ' difficulty-trivial';
} else if (priority === 1) {
@@ -64,6 +59,7 @@ module.exports = function(task, filters, dayStart, lastCron, showCompleted, main
} else if (priority === 2) {
classes += ' difficulty-hard';
}
if (value < -20) {
classes += ' color-worst';
} else if (value < -10) {
@@ -79,5 +75,6 @@ module.exports = function(task, filters, dayStart, lastCron, showCompleted, main
} else {
classes += ' color-best';
}
return classes;
};

View File

@@ -1,71 +1,74 @@
import uuid from './uuid';
import { v4 as uuid } from 'uuid';
import _ from 'lodash';
import moment from 'moment';
/*
Even though Mongoose handles task defaults, we want to make sure defaults are set on the client-side before
sending up to the server for performance
*/
// Even though Mongoose handles task defaults, we want to make sure defaults are set on the client-side before
// sending up to the server for performance
// TODO revisit
// TODO move to client code?
module.exports = function(task) {
var defaults, ref, ref1, ref2;
if (task == null) {
task = {};
}
if (!(task.type && ((ref = task.type) === 'habit' || ref === 'daily' || ref === 'todo' || ref === 'reward'))) {
const tasksTypes = ['habit', 'daily', 'todo', 'reward'];
module.exports = function taskDefaults (task = {}) {
if (!task.type || tasksTypes.indexOf(task.type) === -1) {
task.type = 'habit';
}
defaults = {
id: uuid(),
text: task.id != null ? task.id : '',
let defaultId = uuid();
let defaults = {
_id: defaultId,
text: task._id || defaultId,
notes: '',
tags: [],
value: task.type === 'reward' ? 10 : 0,
priority: 1,
challenge: {},
reminders: [],
attribute: 'str',
dateCreated: new Date()
createdAt: new Date(), // TODO these are going to be overwritten by the server...
updatedAt: new Date(),
};
_.defaults(task, defaults);
if (task.type === 'habit' || task.type === 'daily') {
_.defaults(task, {
history: [],
});
}
if (task.type === 'todo' || task.type === 'daily') {
_.defaults(task, {
completed: false,
collapseChecklist: false,
checklist: [],
});
}
if (task.type === 'habit') {
_.defaults(task, {
up: true,
down: true
});
}
if ((ref1 = task.type) === 'habit' || ref1 === 'daily') {
_.defaults(task, {
history: []
});
}
if ((ref2 = task.type) === 'daily' || ref2 === 'todo') {
_.defaults(task, {
completed: false
down: true,
});
}
if (task.type === 'daily') {
_.defaults(task, {
streak: 0,
repeat: {
su: true,
m: true,
t: true,
w: true,
th: true,
f: true,
s: true
}
}, {
startDate: new Date(),
s: true,
su: true,
},
startDate: moment().startOf('day').toDate(),
everyX: 1,
frequency: 'weekly'
frequency: 'weekly',
});
}
task._id = task.id;
if (task.value == null) {
task.value = task.type === 'reward' ? 10 : 0;
}
if (!_.isNumber(task.priority)) {
task.priority = 1;
}
return task;
};

View File

@@ -1,36 +1,31 @@
import _ from 'lodash';
import content from '../content/index';
/*
Update the in-browser store with new gear. FIXME this was in user.fns, but it was causing strange issues there
*/
// Return the list of gear items available for purchase
var sortOrder = _.reduce(content.gearTypes, (function(m, v, k) {
m[v] = k;
return m;
}), {});
let sortOrder = _.reduce(content.gearTypes, (accumulator, val, key) => {
accumulator[val] = key;
return accumulator;
}, {});
module.exports = function(user) {
var changes;
if (!user) {
return;
}
changes = [];
_.each(content.gearTypes, function(type) {
var found;
found = _.find(content.gear.tree[type][user.stats["class"]], function(item) {
module.exports = function updateStore (user) {
let changes = [];
_.each(content.gearTypes, (type) => {
let found = _.find(content.gear.tree[type][user.stats.class], (item) => {
return !user.items.gear.owned[item.key];
});
if (found) {
changes.push(found);
if (found) changes.push(found);
});
changes = changes.concat(_.filter(content.gear.flat, (val) => {
if (['special', 'mystery', 'armoire'].indexOf(val.klass) !== -1 && !user.items.gear.owned[val.key] && (val.canOwn ? val.canOwn(user) : false)) {
return true;
} else {
return false;
}
return true;
});
changes = changes.concat(_.filter(content.gear.flat, function(v) {
var ref;
return ((ref = v.klass) === 'special' || ref === 'mystery' || ref === 'armoire') && !user.items.gear.owned[v.key] && (typeof v.canOwn === "function" ? v.canOwn(user) : void 0);
}));
return _.sortBy(changes, function(c) {
return sortOrder[c.type];
});
return _.sortBy(changes, (change) => sortOrder[change.type]);
};

View File

@@ -1 +1,4 @@
module.exports = require('uuid').v4;
import uuid from 'uuid';
// TODO remove this file completely
module.exports = uuid.v4;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Some files were not shown because too many files have changed in this diff Show More