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
7
.babelrc
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": ["syntax-async-functions","transform-regenerator"]
|
||||
"plugins": [
|
||||
["transform-async-to-module-method", {
|
||||
"module": "bluebird",
|
||||
"method": "coroutine"
|
||||
}]
|
||||
]
|
||||
}
|
||||
|
||||
2
.bowerrc
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"directory": "website/public/bower_components"
|
||||
"directory": "website/client/bower_components"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,5 +2,8 @@
|
||||
"extends": [
|
||||
"habitrpg/server",
|
||||
"habitrpg/babel"
|
||||
]
|
||||
],
|
||||
"globals": {
|
||||
"Promise": true
|
||||
}
|
||||
}
|
||||
|
||||
11
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@ node_modules/**
|
||||
.bower-cache/**
|
||||
.bower-tmp/**
|
||||
.bower-registry/**
|
||||
website/public/**
|
||||
website/client/**
|
||||
website/views/**
|
||||
website/build/**
|
||||
.git/**
|
||||
|
||||
@@ -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"
|
||||
|
||||
26
Gruntfile.js
@@ -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" +
|
||||
|
||||
2
Procfile
@@ -1 +1 @@
|
||||
web: node ./website/transpiled-babel/server.js
|
||||
web: node ./website/transpiled-babel/index.js
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"public/bower_components",
|
||||
"website/client/bower_components",
|
||||
"test",
|
||||
"tests"
|
||||
],
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require('babel-polyfill');
|
||||
|
||||
var shared = require('./script/index');
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
|
||||
4
common/dist/sprites/spritesmith-main-0.css
vendored
@@ -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;
|
||||
|
||||
10
common/dist/sprites/spritesmith-main-11.css
vendored
@@ -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;
|
||||
|
||||
BIN
common/dist/sprites/spritesmith-main-11.png
vendored
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
780
common/dist/sprites/spritesmith-main-12.css
vendored
BIN
common/dist/sprites/spritesmith-main-12.png
vendored
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 124 KiB |
96
common/dist/sprites/spritesmith-main-5.css
vendored
@@ -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;
|
||||
|
||||
BIN
common/dist/sprites/spritesmith-main-5.png
vendored
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 255 KiB |
4
common/dist/sprites/spritesmith-main-6.css
vendored
@@ -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;
|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
BIN
common/img/sprites/spritesmith/stable/pets/Pet-Lion-Veteran.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
@@ -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';
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"etherealLion": "Ethereal Lion",
|
||||
"veteranWolf": "Veteran Wolf",
|
||||
"veteranTiger": "Veteran Tiger",
|
||||
"veteranLion": "Veteran Lion",
|
||||
"cerberusPup": "Cerberus Pup",
|
||||
"hydra": "Hydra",
|
||||
"mantisShrimp": "Mantis Shrimp",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// TODO what can be moved to /website/server?
|
||||
/*
|
||||
------------------------------------------------------
|
||||
Cron and time / day functions
|
||||
|
||||
@@ -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)]++;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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++;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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)];
|
||||
};
|
||||
|
||||
25
common/script/fns/resetGear.js
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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(', ');
|
||||
};
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
module.exports = function(items) {
|
||||
return _.reduce(items, (function(m, v) {
|
||||
return m + (v ? 1 : 0);
|
||||
}), 0);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
42
common/script/libs/errors.js
Normal 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.';
|
||||
}
|
||||
}
|
||||
11
common/script/libs/extendableBuiltin.js
Normal 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;
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import moment from 'moment';
|
||||
|
||||
/*
|
||||
Friendly timestamp
|
||||
*/
|
||||
|
||||
module.exports = function(timestamp) {
|
||||
return moment(timestamp).format('MM/DD h:mm:ss a');
|
||||
};
|
||||
@@ -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';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
13
common/script/libs/pickDeep.js
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
@@ -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, '');
|
||||
};
|
||||
@@ -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';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
module.exports = function(s) {
|
||||
|
||||
module.exports = function splitWhitespace (s) {
|
||||
return s.split(' ');
|
||||
};
|
||||
|
||||
28
common/script/libs/statsComputed.js
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
module.exports = require('uuid').v4;
|
||||
import uuid from 'uuid';
|
||||
|
||||
// TODO remove this file completely
|
||||
module.exports = uuid.v4;
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,119 +1,24 @@
|
||||
import content from '../content/index';
|
||||
import i18n from '../i18n';
|
||||
import _ from 'lodash';
|
||||
import count from '../count';
|
||||
import splitWhitespace from '../libs/splitWhitespace';
|
||||
import {
|
||||
BadRequest,
|
||||
} from '../libs/errors';
|
||||
import buyHealthPotion from './buyHealthPotion';
|
||||
import buyArmoire from './buyArmoire';
|
||||
import buyGear from './buyGear';
|
||||
|
||||
module.exports = function(user, req, cb, analytics) {
|
||||
var analyticsData, armoireExp, armoireResp, armoireResult, base, buyResp, drop, eligibleEquipment, item, key, message, name;
|
||||
key = req.params.key;
|
||||
item = key === 'potion' ? content.potion : key === 'armoire' ? content.armoire : content.gear.flat[key];
|
||||
if (!item) {
|
||||
return typeof cb === "function" ? cb({
|
||||
code: 404,
|
||||
message: "Item '" + key + " not found (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.js)"
|
||||
}) : void 0;
|
||||
}
|
||||
if (user.stats.gp < item.value) {
|
||||
return typeof cb === "function" ? cb({
|
||||
code: 401,
|
||||
message: i18n.t('messageNotEnoughGold', req.language)
|
||||
}) : void 0;
|
||||
}
|
||||
if ((item.canOwn != null) && !item.canOwn(user)) {
|
||||
return typeof cb === "function" ? cb({
|
||||
code: 401,
|
||||
message: "You can't buy this item"
|
||||
}) : void 0;
|
||||
}
|
||||
armoireResp = void 0;
|
||||
if (item.key === 'potion') {
|
||||
user.stats.hp += 15;
|
||||
if (user.stats.hp > 50) {
|
||||
user.stats.hp = 50;
|
||||
}
|
||||
} else if (item.key === 'armoire') {
|
||||
armoireResult = user.fns.predictableRandom(user.stats.gp);
|
||||
eligibleEquipment = _.filter(content.gear.flat, (function(i) {
|
||||
return i.klass === 'armoire' && !user.items.gear.owned[i.key];
|
||||
}));
|
||||
if (!_.isEmpty(eligibleEquipment) && (armoireResult < .6 || !user.flags.armoireOpened)) {
|
||||
eligibleEquipment.sort();
|
||||
drop = user.fns.randomVal(eligibleEquipment);
|
||||
user.items.gear.owned[drop.key] = true;
|
||||
user.flags.armoireOpened = true;
|
||||
message = i18n.t('armoireEquipment', {
|
||||
image: '<span class="shop_' + drop.key + ' pull-left"></span>',
|
||||
dropText: drop.text(req.language)
|
||||
}, req.language);
|
||||
if (count.remainingGearInSet(user.items.gear.owned, 'armoire') === 0) {
|
||||
user.flags.armoireEmpty = true;
|
||||
}
|
||||
armoireResp = {
|
||||
type: "gear",
|
||||
dropKey: drop.key,
|
||||
dropText: drop.text(req.language)
|
||||
};
|
||||
} else if ((!_.isEmpty(eligibleEquipment) && armoireResult < .8) || armoireResult < .5) {
|
||||
drop = user.fns.randomVal(_.where(content.food, {
|
||||
canDrop: true
|
||||
}));
|
||||
if ((base = user.items.food)[name = drop.key] == null) {
|
||||
base[name] = 0;
|
||||
}
|
||||
user.items.food[drop.key] += 1;
|
||||
message = i18n.t('armoireFood', {
|
||||
image: '<span class="Pet_Food_' + drop.key + ' pull-left"></span>',
|
||||
dropArticle: drop.article,
|
||||
dropText: drop.text(req.language)
|
||||
}, req.language);
|
||||
armoireResp = {
|
||||
type: "food",
|
||||
dropKey: drop.key,
|
||||
dropArticle: drop.article,
|
||||
dropText: drop.text(req.language)
|
||||
};
|
||||
} else {
|
||||
armoireExp = Math.floor(user.fns.predictableRandom(user.stats.exp) * 40 + 10);
|
||||
user.stats.exp += armoireExp;
|
||||
message = i18n.t('armoireExp', req.language);
|
||||
armoireResp = {
|
||||
"type": "experience",
|
||||
"value": armoireExp
|
||||
};
|
||||
}
|
||||
module.exports = function buy (user, req = {}, analytics) {
|
||||
let key = _.get(req, 'params.key');
|
||||
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
|
||||
|
||||
let buyRes;
|
||||
if (key === 'potion') {
|
||||
buyRes = buyHealthPotion(user, req, analytics);
|
||||
} else if (key === 'armoire') {
|
||||
buyRes = buyArmoire(user, req, analytics);
|
||||
} else {
|
||||
if (user.preferences.autoEquip) {
|
||||
user.items.gear.equipped[item.type] = item.key;
|
||||
message = user.fns.handleTwoHanded(item, null, req);
|
||||
}
|
||||
user.items.gear.owned[item.key] = true;
|
||||
if (message == null) {
|
||||
message = i18n.t('messageBought', {
|
||||
itemText: item.text(req.language)
|
||||
}, req.language);
|
||||
}
|
||||
if (item.last) {
|
||||
user.fns.ultimateGear();
|
||||
}
|
||||
buyRes = buyGear(user, req, analytics);
|
||||
}
|
||||
user.stats.gp -= item.value;
|
||||
analyticsData = {
|
||||
uuid: user._id,
|
||||
itemKey: key,
|
||||
acquireMethod: 'Gold',
|
||||
goldCost: item.value,
|
||||
category: 'behavior'
|
||||
};
|
||||
if (analytics != null) {
|
||||
analytics.track('acquire item', analyticsData);
|
||||
}
|
||||
buyResp = _.pick(user, splitWhitespace('items achievements stats flags'));
|
||||
if (armoireResp) {
|
||||
buyResp["armoire"] = armoireResp;
|
||||
}
|
||||
return typeof cb === "function" ? cb({
|
||||
code: 200,
|
||||
message: message
|
||||
}, buyResp) : void 0;
|
||||
|
||||
return buyRes;
|
||||
};
|
||||
|
||||
115
common/script/ops/buyArmoire.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import content from '../content/index';
|
||||
import i18n from '../i18n';
|
||||
import _ from 'lodash';
|
||||
import count from '../count';
|
||||
import splitWhitespace from '../libs/splitWhitespace';
|
||||
import {
|
||||
NotAuthorized,
|
||||
} from '../libs/errors';
|
||||
import predictableRandom from '../fns/predictableRandom';
|
||||
import randomVal from '../fns/randomVal';
|
||||
|
||||
module.exports = function buyArmoire (user, req = {}, analytics) {
|
||||
let item = content.armoire;
|
||||
|
||||
if (user.stats.gp < item.value) {
|
||||
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
|
||||
}
|
||||
|
||||
if (item.canOwn && !item.canOwn(user)) {
|
||||
throw new NotAuthorized(i18n.t('cannotBuyItem', req.language));
|
||||
}
|
||||
|
||||
let armoireResp;
|
||||
let armoireResult;
|
||||
let eligibleEquipment;
|
||||
let drop;
|
||||
let message;
|
||||
|
||||
armoireResult = predictableRandom(user, user.stats.gp);
|
||||
eligibleEquipment = _.filter(content.gear.flat, (eligible) => {
|
||||
return eligible.klass === 'armoire' && !user.items.gear.owned[eligible.key];
|
||||
});
|
||||
|
||||
if (!_.isEmpty(eligibleEquipment) && (armoireResult < 0.6 || !user.flags.armoireOpened)) {
|
||||
eligibleEquipment.sort();
|
||||
drop = randomVal(user, eligibleEquipment);
|
||||
|
||||
if (user.items.gear.owned[drop.key]) {
|
||||
throw new NotAuthorized(i18n.t('equipmentAlradyOwned', req.language));
|
||||
}
|
||||
|
||||
user.items.gear.owned[drop.key] = true;
|
||||
user.flags.armoireOpened = true;
|
||||
message = i18n.t('armoireEquipment', {
|
||||
image: `<span class="shop_${drop.key} pull-left"></span>`,
|
||||
dropText: drop.text(req.language),
|
||||
}, req.language);
|
||||
|
||||
if (count.remainingGearInSet(user.items.gear.owned, 'armoire') === 0) {
|
||||
user.flags.armoireEmpty = true;
|
||||
}
|
||||
|
||||
armoireResp = {
|
||||
type: 'gear',
|
||||
dropKey: drop.key,
|
||||
dropText: drop.text(req.language),
|
||||
};
|
||||
} else if ((!_.isEmpty(eligibleEquipment) && armoireResult < 0.8) || armoireResult < 0.5) { // eslint-disable-line no-extra-parens
|
||||
drop = randomVal(user, _.where(content.food, {
|
||||
canDrop: true,
|
||||
}));
|
||||
user.items.food[drop.key] = user.items.food[drop.key] || 0;
|
||||
user.items.food[drop.key] += 1;
|
||||
|
||||
message = i18n.t('armoireFood', {
|
||||
image: `<span class="Pet_Food_${drop.key} pull-left"></span>`,
|
||||
dropArticle: drop.article,
|
||||
dropText: drop.text(req.language),
|
||||
}, req.language);
|
||||
armoireResp = {
|
||||
type: 'food',
|
||||
dropKey: drop.key,
|
||||
dropArticle: drop.article,
|
||||
dropText: drop.text(req.language),
|
||||
};
|
||||
} else {
|
||||
let armoireExp = Math.floor(predictableRandom(user, user.stats.exp) * 40 + 10);
|
||||
user.stats.exp += armoireExp;
|
||||
message = i18n.t('armoireExp', req.language);
|
||||
armoireResp = {
|
||||
type: 'experience',
|
||||
value: armoireExp,
|
||||
};
|
||||
}
|
||||
|
||||
user.stats.gp -= item.value;
|
||||
|
||||
if (!message) {
|
||||
message = i18n.t('messageBought', {
|
||||
itemText: item.text(req.language),
|
||||
}, req.language);
|
||||
}
|
||||
|
||||
if (analytics) {
|
||||
analytics.track('acquire item', {
|
||||
uuid: user._id,
|
||||
itemKey: 'Armoire',
|
||||
acquireMethod: 'Gold',
|
||||
goldCost: item.value,
|
||||
category: 'behavior',
|
||||
});
|
||||
}
|
||||
|
||||
let resData = _.pick(user, splitWhitespace('items flags'));
|
||||
if (armoireResp) resData.armoire = armoireResp;
|
||||
|
||||
if (req.v2 === true) {
|
||||
return resData;
|
||||
} else {
|
||||
return [
|
||||
resData,
|
||||
message,
|
||||
];
|
||||
}
|
||||
};
|
||||
70
common/script/ops/buyGear.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import content from '../content/index';
|
||||
import i18n from '../i18n';
|
||||
import _ from 'lodash';
|
||||
import splitWhitespace from '../libs/splitWhitespace';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
} from '../libs/errors';
|
||||
import handleTwoHanded from '../fns/handleTwoHanded';
|
||||
import ultimateGear from '../fns/ultimateGear';
|
||||
|
||||
module.exports = function buyGear (user, req = {}, analytics) {
|
||||
let key = _.get(req, 'params.key');
|
||||
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
|
||||
|
||||
let item = content.gear.flat[key];
|
||||
|
||||
if (!item) throw new NotFound(i18n.t('itemNotFound', {key}, req.language));
|
||||
|
||||
if (user.stats.gp < item.value) {
|
||||
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
|
||||
}
|
||||
|
||||
if (item.canOwn && !item.canOwn(user)) {
|
||||
throw new NotAuthorized(i18n.t('cannotBuyItem', req.language));
|
||||
}
|
||||
|
||||
let message;
|
||||
|
||||
if (user.items.gear.owned[item.key]) {
|
||||
throw new NotAuthorized(i18n.t('equipmentAlreadyOwned', req.language));
|
||||
}
|
||||
|
||||
if (user.preferences.autoEquip) {
|
||||
user.items.gear.equipped[item.type] = item.key;
|
||||
message = handleTwoHanded(user, item, undefined, req);
|
||||
}
|
||||
|
||||
user.items.gear.owned[item.key] = true;
|
||||
|
||||
if (item.last) ultimateGear(user);
|
||||
|
||||
user.stats.gp -= item.value;
|
||||
|
||||
if (!message) {
|
||||
message = i18n.t('messageBought', {
|
||||
itemText: item.text(req.language),
|
||||
}, req.language);
|
||||
}
|
||||
|
||||
if (analytics) {
|
||||
analytics.track('acquire item', {
|
||||
uuid: user._id,
|
||||
itemKey: key,
|
||||
acquireMethod: 'Gold',
|
||||
goldCost: item.value,
|
||||
category: 'behavior',
|
||||
});
|
||||
}
|
||||
|
||||
if (req.v2 === true) {
|
||||
return _.pick(user, splitWhitespace('items achievements stats flags'));
|
||||
} else {
|
||||
return [
|
||||
_.pick(user, splitWhitespace('items achievements stats flags')),
|
||||
message,
|
||||
];
|
||||
}
|
||||
};
|
||||
48
common/script/ops/buyHealthPotion.js
Normal 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,
|
||||
];
|
||||
}
|
||||
};
|
||||
@@ -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),
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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')),
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
};
|
||||
|
||||