diff --git a/Gruntfile.js b/Gruntfile.js
index be40b40816..f669f21901 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -51,17 +51,6 @@ module.exports = function(grunt) {
}
},
- browserify: {
- dist: {
- src: ["common/index.js"],
- dest: "common/dist/scripts/habitrpg-shared.js"
- },
- options: {
- transform: ['coffeeify']
- //debug: true Huge data uri source map (400kb!)
- }
- },
-
copy: {
build: {
files: [
@@ -134,12 +123,11 @@ module.exports = function(grunt) {
});
// Register tasks.
- grunt.registerTask('build:prod', ['loadManifestFiles', 'clean:build', 'browserify', 'uglify', 'stylus', 'cssmin', 'copy:build', 'hashres']);
- grunt.registerTask('build:dev', ['browserify', 'cssmin', 'stylus']);
+ grunt.registerTask('build:prod', ['loadManifestFiles', 'clean:build', 'uglify', 'stylus', 'cssmin', 'copy:build', 'hashres']);
+ grunt.registerTask('build:dev', ['cssmin', 'stylus']);
grunt.registerTask('build:test', ['test:prepare:translations', 'build:dev']);
grunt.registerTask('test:prepare:translations', function() {
- require('coffee-script');
var i18n = require('./website/src/libs/i18n'),
fs = require('fs');
fs.writeFileSync('test/spec/mocks/translations.js',
@@ -148,7 +136,6 @@ module.exports = function(grunt) {
});
// Load tasks
- grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-stylus');
diff --git a/common/index.js b/common/index.js
index 6594ecc720..e6a9c9fb98 100644
--- a/common/index.js
+++ b/common/index.js
@@ -1,4 +1,4 @@
-module.exports = require('./script/index.coffee');
+module.exports = require('./script/index');
var _ = require('lodash');
var moment = require('moment');
diff --git a/common/script/content/faq.js b/common/script/content/faq.js
index 7d8468ee3e..e65f8969e6 100644
--- a/common/script/content/faq.js
+++ b/common/script/content/faq.js
@@ -1,6 +1,5 @@
'use strict';
-require('coffee-script');
var t = require('./translation.js');
var NUMBER_OF_QUESTIONS = 12;
diff --git a/common/script/content/index.js b/common/script/content/index.js
new file mode 100644
index 0000000000..0005d09460
--- /dev/null
+++ b/common/script/content/index.js
@@ -0,0 +1,7005 @@
+var _, api, calculateBonus, classes, diminishingReturns, events, gear, gearTypes, moment, t;
+
+api = module.exports;
+
+_ = require('lodash');
+
+moment = require('moment');
+
+t = require('./translation.js');
+
+
+/*
+ ---------------------------------------------------------------
+ Gear (Weapons, Armor, Head, Shield)
+ Item definitions: {index, text, notes, value, str, def, int, per, classes, type}
+ ---------------------------------------------------------------
+ */
+
+classes = ['warrior', 'rogue', 'healer', 'wizard'];
+
+gearTypes = ['weapon', 'armor', 'head', 'shield', 'body', 'back', 'headAccessory', 'eyewear'];
+
+events = {
+ winter: {
+ start: '2013-12-31',
+ end: '2014-02-01'
+ },
+ birthday: {
+ start: '2013-01-30',
+ end: '2014-02-01'
+ },
+ spring: {
+ start: '2014-03-21',
+ end: '2014-05-01'
+ },
+ summer: {
+ start: '2014-06-20',
+ end: '2014-08-01'
+ },
+ gaymerx: {
+ start: '2014-07-02',
+ end: '2014-08-01'
+ },
+ fall: {
+ start: '2014-09-21',
+ end: '2014-11-01'
+ },
+ winter2015: {
+ start: '2014-12-21',
+ end: '2015-02-02'
+ },
+ spring2015: {
+ start: '2015-03-20',
+ end: '2015-05-02'
+ },
+ summer2015: {
+ start: '2015-06-20',
+ end: '2015-08-02'
+ },
+ fall2015: {
+ start: '2015-09-21',
+ end: '2015-11-02'
+ }
+};
+
+api.mystery = {
+ 201402: {
+ start: '2014-02-22',
+ end: '2014-02-28',
+ text: 'Winged Messenger Set'
+ },
+ 201403: {
+ start: '2014-03-24',
+ end: '2014-04-02',
+ text: 'Forest Walker Set'
+ },
+ 201404: {
+ start: '2014-04-24',
+ end: '2014-05-02',
+ text: 'Twilight Butterfly Set'
+ },
+ 201405: {
+ start: '2014-05-21',
+ end: '2014-06-02',
+ text: 'Flame Wielder Set'
+ },
+ 201406: {
+ start: '2014-06-23',
+ end: '2014-07-02',
+ text: 'Octomage Set'
+ },
+ 201407: {
+ start: '2014-07-23',
+ end: '2014-08-02',
+ text: 'Undersea Explorer Set'
+ },
+ 201408: {
+ start: '2014-08-23',
+ end: '2014-09-02',
+ text: 'Sun Sorcerer Set'
+ },
+ 201409: {
+ start: '2014-09-24',
+ end: '2014-10-02',
+ text: 'Autumn Strider Set'
+ },
+ 201410: {
+ start: '2014-10-24',
+ end: '2014-11-02',
+ text: 'Winged Goblin Set'
+ },
+ 201411: {
+ start: '2014-11-24',
+ end: '2014-12-02',
+ text: 'Feast and Fun Set'
+ },
+ 201412: {
+ start: '2014-12-25',
+ end: '2015-01-02',
+ text: 'Penguin Set'
+ },
+ 201501: {
+ start: '2015-01-26',
+ end: '2015-02-02',
+ text: 'Starry Knight Set'
+ },
+ 201502: {
+ start: '2015-02-24',
+ end: '2015-03-02',
+ text: 'Winged Enchanter Set'
+ },
+ 201503: {
+ start: '2015-03-25',
+ end: '2015-04-02',
+ text: 'Aquamarine Set'
+ },
+ 201504: {
+ start: '2015-04-24',
+ end: '2015-05-02',
+ text: 'Busy Bee Set'
+ },
+ 201505: {
+ start: '2015-05-25',
+ end: '2015-06-02',
+ text: 'Green Knight Set'
+ },
+ 201506: {
+ start: '2015-06-25',
+ end: '2015-07-02',
+ text: 'Neon Snorkeler Set'
+ },
+ 201507: {
+ start: '2015-07-24',
+ end: '2015-08-02',
+ text: 'Rad Surfer Set'
+ },
+ 201508: {
+ start: '2015-08-23',
+ end: '2015-09-02',
+ text: 'Cheetah Costume Set'
+ },
+ 201509: {
+ start: '2015-09-24',
+ end: '2015-10-02',
+ text: 'Werewolf Set'
+ },
+ 201510: {
+ start: '2015-10-26',
+ end: '2015-11-02',
+ text: 'Horned Goblin Set'
+ },
+ 301404: {
+ start: '3014-03-24',
+ end: '3014-04-02',
+ text: 'Steampunk Standard Set'
+ },
+ 301405: {
+ start: '3014-04-24',
+ end: '3014-05-02',
+ text: 'Steampunk Accessories Set'
+ },
+ wondercon: {
+ start: '2014-03-24',
+ end: '2014-04-01'
+ }
+};
+
+_.each(api.mystery, function(v, k) {
+ return v.key = k;
+});
+
+api.itemList = {
+ 'weapon': {
+ localeKey: 'weapon',
+ isEquipment: true
+ },
+ 'armor': {
+ localeKey: 'armor',
+ isEquipment: true
+ },
+ 'head': {
+ localeKey: 'headgear',
+ isEquipment: true
+ },
+ 'shield': {
+ localeKey: 'offhand',
+ isEquipment: true
+ },
+ 'back': {
+ localeKey: 'back',
+ isEquipment: true
+ },
+ 'body': {
+ localeKey: 'body',
+ isEquipment: true
+ },
+ 'headAccessory': {
+ localeKey: 'headAccessory',
+ isEquipment: true
+ },
+ 'eyewear': {
+ localeKey: 'eyewear',
+ isEquipment: true
+ },
+ 'hatchingPotions': {
+ localeKey: 'hatchingPotion',
+ isEquipment: false
+ },
+ 'eggs': {
+ localeKey: 'eggSingular',
+ isEquipment: false
+ },
+ 'quests': {
+ localeKey: 'quest',
+ isEquipment: false
+ },
+ 'food': {
+ localeKey: 'foodText',
+ isEquipment: false
+ },
+ 'Saddle': {
+ localeKey: 'foodSaddleText',
+ isEquipment: false
+ }
+};
+
+gear = {
+ weapon: {
+ base: {
+ 0: {
+ text: t('weaponBase0Text'),
+ notes: t('weaponBase0Notes'),
+ value: 0
+ }
+ },
+ warrior: {
+ 0: {
+ text: t('weaponWarrior0Text'),
+ notes: t('weaponWarrior0Notes'),
+ value: 1
+ },
+ 1: {
+ text: t('weaponWarrior1Text'),
+ notes: t('weaponWarrior1Notes', {
+ str: 3
+ }),
+ str: 3,
+ value: 20
+ },
+ 2: {
+ text: t('weaponWarrior2Text'),
+ notes: t('weaponWarrior2Notes', {
+ str: 6
+ }),
+ str: 6,
+ value: 30
+ },
+ 3: {
+ text: t('weaponWarrior3Text'),
+ notes: t('weaponWarrior3Notes', {
+ str: 9
+ }),
+ str: 9,
+ value: 45
+ },
+ 4: {
+ text: t('weaponWarrior4Text'),
+ notes: t('weaponWarrior4Notes', {
+ str: 12
+ }),
+ str: 12,
+ value: 65
+ },
+ 5: {
+ text: t('weaponWarrior5Text'),
+ notes: t('weaponWarrior5Notes', {
+ str: 15
+ }),
+ str: 15,
+ value: 90
+ },
+ 6: {
+ text: t('weaponWarrior6Text'),
+ notes: t('weaponWarrior6Notes', {
+ str: 18
+ }),
+ str: 18,
+ value: 120,
+ last: true
+ }
+ },
+ rogue: {
+ 0: {
+ text: t('weaponRogue0Text'),
+ notes: t('weaponRogue0Notes'),
+ str: 0,
+ value: 0
+ },
+ 1: {
+ text: t('weaponRogue1Text'),
+ notes: t('weaponRogue1Notes', {
+ str: 2
+ }),
+ str: 2,
+ value: 20
+ },
+ 2: {
+ text: t('weaponRogue2Text'),
+ notes: t('weaponRogue2Notes', {
+ str: 3
+ }),
+ str: 3,
+ value: 35
+ },
+ 3: {
+ text: t('weaponRogue3Text'),
+ notes: t('weaponRogue3Notes', {
+ str: 4
+ }),
+ str: 4,
+ value: 50
+ },
+ 4: {
+ text: t('weaponRogue4Text'),
+ notes: t('weaponRogue4Notes', {
+ str: 6
+ }),
+ str: 6,
+ value: 70
+ },
+ 5: {
+ text: t('weaponRogue5Text'),
+ notes: t('weaponRogue5Notes', {
+ str: 8
+ }),
+ str: 8,
+ value: 90
+ },
+ 6: {
+ text: t('weaponRogue6Text'),
+ notes: t('weaponRogue6Notes', {
+ str: 10
+ }),
+ str: 10,
+ value: 120,
+ last: true
+ }
+ },
+ wizard: {
+ 0: {
+ twoHanded: true,
+ text: t('weaponWizard0Text'),
+ notes: t('weaponWizard0Notes'),
+ value: 0
+ },
+ 1: {
+ twoHanded: true,
+ text: t('weaponWizard1Text'),
+ notes: t('weaponWizard1Notes', {
+ int: 3,
+ per: 1
+ }),
+ int: 3,
+ per: 1,
+ value: 30
+ },
+ 2: {
+ twoHanded: true,
+ text: t('weaponWizard2Text'),
+ notes: t('weaponWizard2Notes', {
+ int: 6,
+ per: 2
+ }),
+ int: 6,
+ per: 2,
+ value: 50
+ },
+ 3: {
+ twoHanded: true,
+ text: t('weaponWizard3Text'),
+ notes: t('weaponWizard3Notes', {
+ int: 9,
+ per: 3
+ }),
+ int: 9,
+ per: 3,
+ value: 80
+ },
+ 4: {
+ twoHanded: true,
+ text: t('weaponWizard4Text'),
+ notes: t('weaponWizard4Notes', {
+ int: 12,
+ per: 5
+ }),
+ int: 12,
+ per: 5,
+ value: 120
+ },
+ 5: {
+ twoHanded: true,
+ text: t('weaponWizard5Text'),
+ notes: t('weaponWizard5Notes', {
+ int: 15,
+ per: 7
+ }),
+ int: 15,
+ per: 7,
+ value: 160
+ },
+ 6: {
+ twoHanded: true,
+ text: t('weaponWizard6Text'),
+ notes: t('weaponWizard6Notes', {
+ int: 18,
+ per: 10
+ }),
+ int: 18,
+ per: 10,
+ value: 200,
+ last: true
+ }
+ },
+ healer: {
+ 0: {
+ text: t('weaponHealer0Text'),
+ notes: t('weaponHealer0Notes'),
+ value: 0
+ },
+ 1: {
+ text: t('weaponHealer1Text'),
+ notes: t('weaponHealer1Notes', {
+ int: 2
+ }),
+ int: 2,
+ value: 20
+ },
+ 2: {
+ text: t('weaponHealer2Text'),
+ notes: t('weaponHealer2Notes', {
+ int: 3
+ }),
+ int: 3,
+ value: 30
+ },
+ 3: {
+ text: t('weaponHealer3Text'),
+ notes: t('weaponHealer3Notes', {
+ int: 5
+ }),
+ int: 5,
+ value: 45
+ },
+ 4: {
+ text: t('weaponHealer4Text'),
+ notes: t('weaponHealer4Notes', {
+ int: 7
+ }),
+ int: 7,
+ value: 65
+ },
+ 5: {
+ text: t('weaponHealer5Text'),
+ notes: t('weaponHealer5Notes', {
+ int: 9
+ }),
+ int: 9,
+ value: 90
+ },
+ 6: {
+ text: t('weaponHealer6Text'),
+ notes: t('weaponHealer6Notes', {
+ int: 11
+ }),
+ int: 11,
+ value: 120,
+ last: true
+ }
+ },
+ special: {
+ 0: {
+ text: t('weaponSpecial0Text'),
+ notes: t('weaponSpecial0Notes', {
+ str: 20
+ }),
+ str: 20,
+ value: 150,
+ canOwn: (function(u) {
+ var ref;
+ return +((ref = u.backer) != null ? ref.tier : void 0) >= 70;
+ })
+ },
+ 1: {
+ text: t('weaponSpecial1Text'),
+ notes: t('weaponSpecial1Notes', {
+ attrs: 6
+ }),
+ str: 6,
+ per: 6,
+ con: 6,
+ int: 6,
+ value: 170,
+ canOwn: (function(u) {
+ var ref;
+ return +((ref = u.contributor) != null ? ref.level : void 0) >= 4;
+ })
+ },
+ 2: {
+ text: t('weaponSpecial2Text'),
+ notes: t('weaponSpecial2Notes', {
+ attrs: 25
+ }),
+ str: 25,
+ per: 25,
+ value: 200,
+ canOwn: (function(u) {
+ var ref;
+ return (+((ref = u.backer) != null ? ref.tier : void 0) >= 300) || (u.items.gear.owned.weapon_special_2 != null);
+ })
+ },
+ 3: {
+ text: t('weaponSpecial3Text'),
+ notes: t('weaponSpecial3Notes', {
+ attrs: 17
+ }),
+ str: 17,
+ int: 17,
+ con: 17,
+ value: 200,
+ canOwn: (function(u) {
+ var ref;
+ return (+((ref = u.backer) != null ? ref.tier : void 0) >= 300) || (u.items.gear.owned.weapon_special_3 != null);
+ })
+ },
+ critical: {
+ text: t('weaponSpecialCriticalText'),
+ notes: t('weaponSpecialCriticalNotes', {
+ attrs: 40
+ }),
+ str: 40,
+ per: 40,
+ value: 200,
+ canOwn: (function(u) {
+ var ref;
+ return !!((ref = u.contributor) != null ? ref.critical : void 0);
+ })
+ },
+ tridentOfCrashingTides: {
+ text: t('weaponSpecialTridentOfCrashingTidesText'),
+ notes: t('weaponSpecialTridentOfCrashingTidesNotes', {
+ int: 15
+ }),
+ int: 15,
+ value: 130,
+ canOwn: (function(u) {
+ return u.items.gear.owned.weapon_special_tridentOfCrashingTides != null;
+ })
+ },
+ yeti: {
+ event: events.winter,
+ specialClass: 'warrior',
+ text: t('weaponSpecialYetiText'),
+ notes: t('weaponSpecialYetiNotes', {
+ str: 15
+ }),
+ str: 15,
+ value: 90
+ },
+ ski: {
+ event: events.winter,
+ specialClass: 'rogue',
+ text: t('weaponSpecialSkiText'),
+ notes: t('weaponSpecialSkiNotes', {
+ str: 8
+ }),
+ str: 8,
+ value: 90
+ },
+ candycane: {
+ event: events.winter,
+ specialClass: 'wizard',
+ twoHanded: true,
+ text: t('weaponSpecialCandycaneText'),
+ notes: t('weaponSpecialCandycaneNotes', {
+ int: 15,
+ per: 7
+ }),
+ int: 15,
+ per: 7,
+ value: 160
+ },
+ snowflake: {
+ event: events.winter,
+ specialClass: 'healer',
+ text: t('weaponSpecialSnowflakeText'),
+ notes: t('weaponSpecialSnowflakeNotes', {
+ int: 9
+ }),
+ int: 9,
+ value: 90
+ },
+ springRogue: {
+ event: events.spring,
+ specialClass: 'rogue',
+ text: t('weaponSpecialSpringRogueText'),
+ notes: t('weaponSpecialSpringRogueNotes', {
+ str: 8
+ }),
+ value: 80,
+ str: 8
+ },
+ springWarrior: {
+ event: events.spring,
+ specialClass: 'warrior',
+ text: t('weaponSpecialSpringWarriorText'),
+ notes: t('weaponSpecialSpringWarriorNotes', {
+ str: 15
+ }),
+ value: 90,
+ str: 15
+ },
+ springMage: {
+ event: events.spring,
+ specialClass: 'wizard',
+ twoHanded: true,
+ text: t('weaponSpecialSpringMageText'),
+ notes: t('weaponSpecialSpringMageNotes', {
+ int: 15,
+ per: 7
+ }),
+ value: 160,
+ int: 15,
+ per: 7
+ },
+ springHealer: {
+ event: events.spring,
+ specialClass: 'healer',
+ text: t('weaponSpecialSpringHealerText'),
+ notes: t('weaponSpecialSpringHealerNotes', {
+ int: 9
+ }),
+ value: 90,
+ int: 9
+ },
+ summerRogue: {
+ event: events.summer,
+ specialClass: 'rogue',
+ text: t('weaponSpecialSummerRogueText'),
+ notes: t('weaponSpecialSummerRogueNotes', {
+ str: 8
+ }),
+ value: 80,
+ str: 8
+ },
+ summerWarrior: {
+ event: events.summer,
+ specialClass: 'warrior',
+ text: t('weaponSpecialSummerWarriorText'),
+ notes: t('weaponSpecialSummerWarriorNotes', {
+ str: 15
+ }),
+ value: 90,
+ str: 15
+ },
+ summerMage: {
+ event: events.summer,
+ specialClass: 'wizard',
+ twoHanded: true,
+ text: t('weaponSpecialSummerMageText'),
+ notes: t('weaponSpecialSummerMageNotes', {
+ int: 15,
+ per: 7
+ }),
+ value: 160,
+ int: 15,
+ per: 7
+ },
+ summerHealer: {
+ event: events.summer,
+ specialClass: 'healer',
+ text: t('weaponSpecialSummerHealerText'),
+ notes: t('weaponSpecialSummerHealerNotes', {
+ int: 9
+ }),
+ value: 90,
+ int: 9
+ },
+ fallRogue: {
+ event: events.fall,
+ specialClass: 'rogue',
+ text: t('weaponSpecialFallRogueText'),
+ notes: t('weaponSpecialFallRogueNotes', {
+ str: 8
+ }),
+ value: 80,
+ str: 8,
+ canBuy: (function() {
+ return true;
+ })
+ },
+ fallWarrior: {
+ event: events.fall,
+ specialClass: 'warrior',
+ text: t('weaponSpecialFallWarriorText'),
+ notes: t('weaponSpecialFallWarriorNotes', {
+ str: 15
+ }),
+ value: 90,
+ str: 15,
+ canBuy: (function() {
+ return true;
+ })
+ },
+ fallMage: {
+ event: events.fall,
+ specialClass: 'wizard',
+ twoHanded: true,
+ text: t('weaponSpecialFallMageText'),
+ notes: t('weaponSpecialFallMageNotes', {
+ int: 15,
+ per: 7
+ }),
+ value: 160,
+ int: 15,
+ per: 7,
+ canBuy: (function() {
+ return true;
+ })
+ },
+ fallHealer: {
+ event: events.fall,
+ specialClass: 'healer',
+ text: t('weaponSpecialFallHealerText'),
+ notes: t('weaponSpecialFallHealerNotes', {
+ int: 9
+ }),
+ value: 90,
+ int: 9,
+ canBuy: (function() {
+ return true;
+ })
+ },
+ winter2015Rogue: {
+ event: events.winter2015,
+ specialClass: 'rogue',
+ text: t('weaponSpecialWinter2015RogueText'),
+ notes: t('weaponSpecialWinter2015RogueNotes', {
+ str: 8
+ }),
+ value: 80,
+ str: 8
+ },
+ winter2015Warrior: {
+ event: events.winter2015,
+ specialClass: 'warrior',
+ text: t('weaponSpecialWinter2015WarriorText'),
+ notes: t('weaponSpecialWinter2015WarriorNotes', {
+ str: 15
+ }),
+ value: 90,
+ str: 15
+ },
+ winter2015Mage: {
+ event: events.winter2015,
+ specialClass: 'wizard',
+ twoHanded: true,
+ text: t('weaponSpecialWinter2015MageText'),
+ notes: t('weaponSpecialWinter2015MageNotes', {
+ int: 15,
+ per: 7
+ }),
+ value: 160,
+ int: 15,
+ per: 7
+ },
+ winter2015Healer: {
+ event: events.winter2015,
+ specialClass: 'healer',
+ text: t('weaponSpecialWinter2015HealerText'),
+ notes: t('weaponSpecialWinter2015HealerNotes', {
+ int: 9
+ }),
+ value: 90,
+ int: 9
+ },
+ spring2015Rogue: {
+ event: events.spring2015,
+ specialClass: 'rogue',
+ text: t('weaponSpecialSpring2015RogueText'),
+ notes: t('weaponSpecialSpring2015RogueNotes', {
+ str: 8
+ }),
+ value: 80,
+ str: 8
+ },
+ spring2015Warrior: {
+ event: events.spring2015,
+ specialClass: 'warrior',
+ text: t('weaponSpecialSpring2015WarriorText'),
+ notes: t('weaponSpecialSpring2015WarriorNotes', {
+ str: 15
+ }),
+ value: 90,
+ str: 15
+ },
+ spring2015Mage: {
+ event: events.spring2015,
+ specialClass: 'wizard',
+ twoHanded: true,
+ text: t('weaponSpecialSpring2015MageText'),
+ notes: t('weaponSpecialSpring2015MageNotes', {
+ int: 15,
+ per: 7
+ }),
+ value: 160,
+ int: 15,
+ per: 7
+ },
+ spring2015Healer: {
+ event: events.spring2015,
+ specialClass: 'healer',
+ text: t('weaponSpecialSpring2015HealerText'),
+ notes: t('weaponSpecialSpring2015HealerNotes', {
+ int: 9
+ }),
+ value: 90,
+ int: 9
+ },
+ summer2015Rogue: {
+ event: events.summer2015,
+ specialClass: 'rogue',
+ text: t('weaponSpecialSummer2015RogueText'),
+ notes: t('weaponSpecialSummer2015RogueNotes', {
+ str: 8
+ }),
+ value: 80,
+ str: 8
+ },
+ summer2015Warrior: {
+ event: events.summer2015,
+ specialClass: 'warrior',
+ text: t('weaponSpecialSummer2015WarriorText'),
+ notes: t('weaponSpecialSummer2015WarriorNotes', {
+ str: 15
+ }),
+ value: 90,
+ str: 15
+ },
+ summer2015Mage: {
+ event: events.summer2015,
+ specialClass: 'wizard',
+ twoHanded: true,
+ text: t('weaponSpecialSummer2015MageText'),
+ notes: t('weaponSpecialSummer2015MageNotes', {
+ int: 15,
+ per: 7
+ }),
+ value: 160,
+ int: 15,
+ per: 7
+ },
+ summer2015Healer: {
+ event: events.summer2015,
+ specialClass: 'healer',
+ text: t('weaponSpecialSummer2015HealerText'),
+ notes: t('weaponSpecialSummer2015HealerNotes', {
+ int: 9
+ }),
+ value: 90,
+ int: 9
+ },
+ fall2015Rogue: {
+ event: events.fall2015,
+ specialClass: 'rogue',
+ text: t('weaponSpecialFall2015RogueText'),
+ notes: t('weaponSpecialFall2015RogueNotes', {
+ str: 8
+ }),
+ value: 80,
+ str: 8
+ },
+ fall2015Warrior: {
+ event: events.fall2015,
+ specialClass: 'warrior',
+ text: t('weaponSpecialFall2015WarriorText'),
+ notes: t('weaponSpecialFall2015WarriorNotes', {
+ str: 15
+ }),
+ value: 90,
+ str: 15
+ },
+ fall2015Mage: {
+ event: events.fall2015,
+ specialClass: 'wizard',
+ twoHanded: true,
+ text: t('weaponSpecialFall2015MageText'),
+ notes: t('weaponSpecialFall2015MageNotes', {
+ int: 15,
+ per: 7
+ }),
+ value: 160,
+ int: 15,
+ per: 7
+ },
+ fall2015Healer: {
+ event: events.fall2015,
+ specialClass: 'healer',
+ text: t('weaponSpecialFall2015HealerText'),
+ notes: t('weaponSpecialFall2015HealerNotes', {
+ int: 9
+ }),
+ value: 90,
+ int: 9
+ }
+ },
+ mystery: {
+ 201411: {
+ text: t('weaponMystery201411Text'),
+ notes: t('weaponMystery201411Notes'),
+ mystery: '201411',
+ value: 0
+ },
+ 201502: {
+ text: t('weaponMystery201502Text'),
+ notes: t('weaponMystery201502Notes'),
+ mystery: '201502',
+ value: 0
+ },
+ 201505: {
+ text: t('weaponMystery201505Text'),
+ notes: t('weaponMystery201505Notes'),
+ mystery: '201505',
+ value: 0
+ },
+ 301404: {
+ text: t('weaponMystery301404Text'),
+ notes: t('weaponMystery301404Notes'),
+ mystery: '301404',
+ value: 0
+ }
+ },
+ armoire: {
+ basicCrossbow: {
+ text: t('weaponArmoireBasicCrossbowText'),
+ notes: t('weaponArmoireBasicCrossbowNotes', {
+ str: 5,
+ per: 5,
+ con: 5
+ }),
+ value: 100,
+ str: 5,
+ per: 5,
+ con: 5,
+ canOwn: (function(u) {
+ return u.items.gear.owned.weapon_armoire_basicCrossbow != null;
+ })
+ },
+ lunarSceptre: {
+ text: t('weaponArmoireLunarSceptreText'),
+ notes: t('weaponArmoireLunarSceptreNotes', {
+ con: 7,
+ int: 7
+ }),
+ value: 100,
+ con: 7,
+ int: 7,
+ set: 'soothing',
+ canOwn: (function(u) {
+ return u.items.gear.owned.weapon_armoire_lunarSceptre != null;
+ })
+ },
+ rancherLasso: {
+ twoHanded: true,
+ text: t('weaponArmoireRancherLassoText'),
+ notes: t('weaponArmoireRancherLassoNotes', {
+ str: 5,
+ per: 5,
+ int: 5
+ }),
+ value: 100,
+ str: 5,
+ per: 5,
+ int: 5,
+ set: 'rancher',
+ canOwn: (function(u) {
+ return u.items.gear.owned.weapon_armoire_rancherLasso != null;
+ })
+ },
+ mythmakerSword: {
+ text: t('weaponArmoireMythmakerSwordText'),
+ notes: t('weaponArmoireMythmakerSwordNotes', {
+ attrs: 6
+ }),
+ value: 100,
+ str: 6,
+ per: 6,
+ set: 'goldenToga',
+ canOwn: (function(u) {
+ return u.items.gear.owned.weapon_armoire_mythmakerSword != null;
+ })
+ },
+ ironCrook: {
+ text: t('weaponArmoireIronCrookText'),
+ notes: t('weaponArmoireIronCrookNotes', {
+ attrs: 7
+ }),
+ value: 100,
+ str: 7,
+ per: 7,
+ set: 'hornedIron',
+ canOwn: (function(u) {
+ return u.items.gear.owned.weapon_armoire_ironCrook != null;
+ })
+ },
+ goldWingStaff: {
+ text: t('weaponArmoireGoldWingStaffText'),
+ notes: t('weaponArmoireGoldWingStaffNotes', {
+ attrs: 4
+ }),
+ value: 100,
+ con: 4,
+ int: 4,
+ per: 4,
+ str: 4,
+ canOwn: (function(u) {
+ return u.items.gear.owned.weapon_armoire_goldWingStaff != null;
+ })
+ },
+ batWand: {
+ text: t('weaponArmoireBatWandText'),
+ notes: t('weaponArmoireBatWandNotes', {
+ int: 10,
+ per: 2
+ }),
+ value: 100,
+ int: 10,
+ per: 2,
+ canOwn: (function(u) {
+ return u.items.gear.owned.weapon_armoire_batWand != null;
+ })
+ },
+ shepherdsCrook: {
+ text: t('weaponArmoireShepherdsCrookText'),
+ notes: t('weaponArmoireShepherdsCrookNotes', {
+ con: 9
+ }),
+ value: 100,
+ con: 9,
+ set: 'shepherd',
+ canOwn: (function(u) {
+ return u.items.gear.owned.weapon_armoire_shepherdsCrook != null;
+ })
+ }
+ }
+ },
+ armor: {
+ base: {
+ 0: {
+ text: t('armorBase0Text'),
+ notes: t('armorBase0Notes'),
+ value: 0
+ }
+ },
+ warrior: {
+ 1: {
+ text: t('armorWarrior1Text'),
+ notes: t('armorWarrior1Notes', {
+ con: 3
+ }),
+ con: 3,
+ value: 30
+ },
+ 2: {
+ text: t('armorWarrior2Text'),
+ notes: t('armorWarrior2Notes', {
+ con: 5
+ }),
+ con: 5,
+ value: 45
+ },
+ 3: {
+ text: t('armorWarrior3Text'),
+ notes: t('armorWarrior3Notes', {
+ con: 7
+ }),
+ con: 7,
+ value: 65
+ },
+ 4: {
+ text: t('armorWarrior4Text'),
+ notes: t('armorWarrior4Notes', {
+ con: 9
+ }),
+ con: 9,
+ value: 90
+ },
+ 5: {
+ text: t('armorWarrior5Text'),
+ notes: t('armorWarrior5Notes', {
+ con: 11
+ }),
+ con: 11,
+ value: 120,
+ last: true
+ }
+ },
+ rogue: {
+ 1: {
+ text: t('armorRogue1Text'),
+ notes: t('armorRogue1Notes', {
+ per: 6
+ }),
+ per: 6,
+ value: 30
+ },
+ 2: {
+ text: t('armorRogue2Text'),
+ notes: t('armorRogue2Notes', {
+ per: 9
+ }),
+ per: 9,
+ value: 45
+ },
+ 3: {
+ text: t('armorRogue3Text'),
+ notes: t('armorRogue3Notes', {
+ per: 12
+ }),
+ per: 12,
+ value: 65
+ },
+ 4: {
+ text: t('armorRogue4Text'),
+ notes: t('armorRogue4Notes', {
+ per: 15
+ }),
+ per: 15,
+ value: 90
+ },
+ 5: {
+ text: t('armorRogue5Text'),
+ notes: t('armorRogue5Notes', {
+ per: 18
+ }),
+ per: 18,
+ value: 120,
+ last: true
+ }
+ },
+ wizard: {
+ 1: {
+ text: t('armorWizard1Text'),
+ notes: t('armorWizard1Notes', {
+ int: 2
+ }),
+ int: 2,
+ value: 30
+ },
+ 2: {
+ text: t('armorWizard2Text'),
+ notes: t('armorWizard2Notes', {
+ int: 4
+ }),
+ int: 4,
+ value: 45
+ },
+ 3: {
+ text: t('armorWizard3Text'),
+ notes: t('armorWizard3Notes', {
+ int: 6
+ }),
+ int: 6,
+ value: 65
+ },
+ 4: {
+ text: t('armorWizard4Text'),
+ notes: t('armorWizard4Notes', {
+ int: 9
+ }),
+ int: 9,
+ value: 90
+ },
+ 5: {
+ text: t('armorWizard5Text'),
+ notes: t('armorWizard5Notes', {
+ int: 12
+ }),
+ int: 12,
+ value: 120,
+ last: true
+ }
+ },
+ healer: {
+ 1: {
+ text: t('armorHealer1Text'),
+ notes: t('armorHealer1Notes', {
+ con: 6
+ }),
+ con: 6,
+ value: 30
+ },
+ 2: {
+ text: t('armorHealer2Text'),
+ notes: t('armorHealer2Notes', {
+ con: 9
+ }),
+ con: 9,
+ value: 45
+ },
+ 3: {
+ text: t('armorHealer3Text'),
+ notes: t('armorHealer3Notes', {
+ con: 12
+ }),
+ con: 12,
+ value: 65
+ },
+ 4: {
+ text: t('armorHealer4Text'),
+ notes: t('armorHealer4Notes', {
+ con: 15
+ }),
+ con: 15,
+ value: 90
+ },
+ 5: {
+ text: t('armorHealer5Text'),
+ notes: t('armorHealer5Notes', {
+ con: 18
+ }),
+ con: 18,
+ value: 120,
+ last: true
+ }
+ },
+ special: {
+ 0: {
+ text: t('armorSpecial0Text'),
+ notes: t('armorSpecial0Notes', {
+ con: 20
+ }),
+ con: 20,
+ value: 150,
+ canOwn: (function(u) {
+ var ref;
+ return +((ref = u.backer) != null ? ref.tier : void 0) >= 45;
+ })
+ },
+ 1: {
+ text: t('armorSpecial1Text'),
+ notes: t('armorSpecial1Notes', {
+ attrs: 6
+ }),
+ con: 6,
+ str: 6,
+ per: 6,
+ int: 6,
+ value: 170,
+ canOwn: (function(u) {
+ var ref;
+ return +((ref = u.contributor) != null ? ref.level : void 0) >= 2;
+ })
+ },
+ 2: {
+ text: t('armorSpecial2Text'),
+ notes: t('armorSpecial2Notes', {
+ attrs: 25
+ }),
+ int: 25,
+ con: 25,
+ value: 200,
+ canOwn: (function(u) {
+ var ref;
+ return +((ref = u.backer) != null ? ref.tier : void 0) >= 300 || (u.items.gear.owned.armor_special_2 != null);
+ })
+ },
+ finnedOceanicArmor: {
+ text: t('armorSpecialFinnedOceanicArmorText'),
+ notes: t('armorSpecialFinnedOceanicArmorNotes', {
+ str: 15
+ }),
+ str: 15,
+ value: 130,
+ canOwn: (function(u) {
+ return u.items.gear.owned.armor_special_finnedOceanicArmor != null;
+ })
+ },
+ yeti: {
+ event: events.winter,
+ specialClass: 'warrior',
+ text: t('armorSpecialYetiText'),
+ notes: t('armorSpecialYetiNotes', {
+ con: 9
+ }),
+ con: 9,
+ value: 90
+ },
+ ski: {
+ event: events.winter,
+ specialClass: 'rogue',
+ text: t('armorSpecialSkiText'),
+ notes: t('armorSpecialSkiNotes', {
+ per: 15
+ }),
+ per: 15,
+ value: 90
+ },
+ candycane: {
+ event: events.winter,
+ specialClass: 'wizard',
+ text: t('armorSpecialCandycaneText'),
+ notes: t('armorSpecialCandycaneNotes', {
+ int: 9
+ }),
+ int: 9,
+ value: 90
+ },
+ snowflake: {
+ event: events.winter,
+ specialClass: 'healer',
+ text: t('armorSpecialSnowflakeText'),
+ notes: t('armorSpecialSnowflakeNotes', {
+ con: 15
+ }),
+ con: 15,
+ value: 90
+ },
+ birthday: {
+ event: events.birthday,
+ text: t('armorSpecialBirthdayText'),
+ notes: t('armorSpecialBirthdayNotes'),
+ value: 0
+ },
+ springRogue: {
+ event: events.spring,
+ specialClass: 'rogue',
+ text: t('armorSpecialSpringRogueText'),
+ notes: t('armorSpecialSpringRogueNotes', {
+ per: 15
+ }),
+ value: 90,
+ per: 15
+ },
+ springWarrior: {
+ event: events.spring,
+ specialClass: 'warrior',
+ text: t('armorSpecialSpringWarriorText'),
+ notes: t('armorSpecialSpringWarriorNotes', {
+ con: 9
+ }),
+ value: 90,
+ con: 9
+ },
+ springMage: {
+ event: events.spring,
+ specialClass: 'wizard',
+ text: t('armorSpecialSpringMageText'),
+ notes: t('armorSpecialSpringMageNotes', {
+ int: 9
+ }),
+ value: 90,
+ int: 9
+ },
+ springHealer: {
+ event: events.spring,
+ specialClass: 'healer',
+ text: t('armorSpecialSpringHealerText'),
+ notes: t('armorSpecialSpringHealerNotes', {
+ con: 15
+ }),
+ value: 90,
+ con: 15
+ },
+ summerRogue: {
+ event: events.summer,
+ specialClass: 'rogue',
+ text: t('armorSpecialSummerRogueText'),
+ notes: t('armorSpecialSummerRogueNotes', {
+ per: 15
+ }),
+ value: 90,
+ per: 15
+ },
+ summerWarrior: {
+ event: events.summer,
+ specialClass: 'warrior',
+ text: t('armorSpecialSummerWarriorText'),
+ notes: t('armorSpecialSummerWarriorNotes', {
+ con: 9
+ }),
+ value: 90,
+ con: 9
+ },
+ summerMage: {
+ event: events.summer,
+ specialClass: 'wizard',
+ text: t('armorSpecialSummerMageText'),
+ notes: t('armorSpecialSummerMageNotes', {
+ int: 9
+ }),
+ value: 90,
+ int: 9
+ },
+ summerHealer: {
+ event: events.summer,
+ specialClass: 'healer',
+ text: t('armorSpecialSummerHealerText'),
+ notes: t('armorSpecialSummerHealerNotes', {
+ con: 15
+ }),
+ value: 90,
+ con: 15
+ },
+ fallRogue: {
+ event: events.fall,
+ specialClass: 'rogue',
+ text: t('armorSpecialFallRogueText'),
+ notes: t('armorSpecialFallRogueNotes', {
+ per: 15
+ }),
+ value: 90,
+ per: 15,
+ canBuy: (function() {
+ return true;
+ })
+ },
+ fallWarrior: {
+ event: events.fall,
+ specialClass: 'warrior',
+ text: t('armorSpecialFallWarriorText'),
+ notes: t('armorSpecialFallWarriorNotes', {
+ con: 9
+ }),
+ value: 90,
+ con: 9,
+ canBuy: (function() {
+ return true;
+ })
+ },
+ fallMage: {
+ event: events.fall,
+ specialClass: 'wizard',
+ text: t('armorSpecialFallMageText'),
+ notes: t('armorSpecialFallMageNotes', {
+ int: 9
+ }),
+ value: 90,
+ int: 9,
+ canBuy: (function() {
+ return true;
+ })
+ },
+ fallHealer: {
+ event: events.fall,
+ specialClass: 'healer',
+ text: t('armorSpecialFallHealerText'),
+ notes: t('armorSpecialFallHealerNotes', {
+ con: 15
+ }),
+ value: 90,
+ con: 15,
+ canBuy: (function() {
+ return true;
+ })
+ },
+ winter2015Rogue: {
+ event: events.winter2015,
+ specialClass: 'rogue',
+ text: t('armorSpecialWinter2015RogueText'),
+ notes: t('armorSpecialWinter2015RogueNotes', {
+ per: 15
+ }),
+ value: 90,
+ per: 15
+ },
+ winter2015Warrior: {
+ event: events.winter2015,
+ specialClass: 'warrior',
+ text: t('armorSpecialWinter2015WarriorText'),
+ notes: t('armorSpecialWinter2015WarriorNotes', {
+ con: 9
+ }),
+ value: 90,
+ con: 9
+ },
+ winter2015Mage: {
+ event: events.winter2015,
+ specialClass: 'wizard',
+ text: t('armorSpecialWinter2015MageText'),
+ notes: t('armorSpecialWinter2015MageNotes', {
+ int: 9
+ }),
+ value: 90,
+ int: 9
+ },
+ winter2015Healer: {
+ event: events.winter2015,
+ specialClass: 'healer',
+ text: t('armorSpecialWinter2015HealerText'),
+ notes: t('armorSpecialWinter2015HealerNotes', {
+ con: 15
+ }),
+ value: 90,
+ con: 15
+ },
+ birthday2015: {
+ text: t('armorSpecialBirthday2015Text'),
+ notes: t('armorSpecialBirthday2015Notes'),
+ value: 0,
+ canOwn: (function(u) {
+ return u.items.gear.owned.armor_special_birthday2015 != null;
+ })
+ },
+ spring2015Rogue: {
+ event: events.spring2015,
+ specialClass: 'rogue',
+ text: t('armorSpecialSpring2015RogueText'),
+ notes: t('armorSpecialSpring2015RogueNotes', {
+ per: 15
+ }),
+ value: 90,
+ per: 15
+ },
+ spring2015Warrior: {
+ event: events.spring2015,
+ specialClass: 'warrior',
+ text: t('armorSpecialSpring2015WarriorText'),
+ notes: t('armorSpecialSpring2015WarriorNotes', {
+ con: 9
+ }),
+ value: 90,
+ con: 9
+ },
+ spring2015Mage: {
+ event: events.spring2015,
+ specialClass: 'wizard',
+ text: t('armorSpecialSpring2015MageText'),
+ notes: t('armorSpecialSpring2015MageNotes', {
+ int: 9
+ }),
+ value: 90,
+ int: 9
+ },
+ spring2015Healer: {
+ event: events.spring2015,
+ specialClass: 'healer',
+ text: t('armorSpecialSpring2015HealerText'),
+ notes: t('armorSpecialSpring2015HealerNotes', {
+ con: 15
+ }),
+ value: 90,
+ con: 15
+ },
+ summer2015Rogue: {
+ event: events.summer2015,
+ specialClass: 'rogue',
+ text: t('armorSpecialSummer2015RogueText'),
+ notes: t('armorSpecialSummer2015RogueNotes', {
+ per: 15
+ }),
+ value: 90,
+ per: 15
+ },
+ summer2015Warrior: {
+ event: events.summer2015,
+ specialClass: 'warrior',
+ text: t('armorSpecialSummer2015WarriorText'),
+ notes: t('armorSpecialSummer2015WarriorNotes', {
+ con: 9
+ }),
+ value: 90,
+ con: 9
+ },
+ summer2015Mage: {
+ event: events.summer2015,
+ specialClass: 'wizard',
+ text: t('armorSpecialSummer2015MageText'),
+ notes: t('armorSpecialSummer2015MageNotes', {
+ int: 9
+ }),
+ value: 90,
+ int: 9
+ },
+ summer2015Healer: {
+ event: events.summer2015,
+ specialClass: 'healer',
+ text: t('armorSpecialSummer2015HealerText'),
+ notes: t('armorSpecialSummer2015HealerNotes', {
+ con: 15
+ }),
+ value: 90,
+ con: 15
+ },
+ fall2015Rogue: {
+ event: events.fall2015,
+ specialClass: 'rogue',
+ text: t('armorSpecialFall2015RogueText'),
+ notes: t('armorSpecialFall2015RogueNotes', {
+ per: 15
+ }),
+ value: 90,
+ per: 15
+ },
+ fall2015Warrior: {
+ event: events.fall2015,
+ specialClass: 'warrior',
+ text: t('armorSpecialFall2015WarriorText'),
+ notes: t('armorSpecialFall2015WarriorNotes', {
+ con: 9
+ }),
+ value: 90,
+ con: 9
+ },
+ fall2015Mage: {
+ event: events.fall2015,
+ specialClass: 'wizard',
+ text: t('armorSpecialFall2015MageText'),
+ notes: t('armorSpecialFall2015MageNotes', {
+ int: 9
+ }),
+ value: 90,
+ int: 9
+ },
+ fall2015Healer: {
+ event: events.fall2015,
+ specialClass: 'healer',
+ text: t('armorSpecialFall2015HealerText'),
+ notes: t('armorSpecialFall2015HealerNotes', {
+ con: 15
+ }),
+ value: 90,
+ con: 15
+ },
+ gaymerx: {
+ event: events.gaymerx,
+ text: t('armorSpecialGaymerxText'),
+ notes: t('armorSpecialGaymerxNotes'),
+ value: 0
+ }
+ },
+ mystery: {
+ 201402: {
+ text: t('armorMystery201402Text'),
+ notes: t('armorMystery201402Notes'),
+ mystery: '201402',
+ value: 0
+ },
+ 201403: {
+ text: t('armorMystery201403Text'),
+ notes: t('armorMystery201403Notes'),
+ mystery: '201403',
+ value: 0
+ },
+ 201405: {
+ text: t('armorMystery201405Text'),
+ notes: t('armorMystery201405Notes'),
+ mystery: '201405',
+ value: 0
+ },
+ 201406: {
+ text: t('armorMystery201406Text'),
+ notes: t('armorMystery201406Notes'),
+ mystery: '201406',
+ value: 0
+ },
+ 201407: {
+ text: t('armorMystery201407Text'),
+ notes: t('armorMystery201407Notes'),
+ mystery: '201407',
+ value: 0
+ },
+ 201408: {
+ text: t('armorMystery201408Text'),
+ notes: t('armorMystery201408Notes'),
+ mystery: '201408',
+ value: 0
+ },
+ 201409: {
+ text: t('armorMystery201409Text'),
+ notes: t('armorMystery201409Notes'),
+ mystery: '201409',
+ value: 0
+ },
+ 201410: {
+ text: t('armorMystery201410Text'),
+ notes: t('armorMystery201410Notes'),
+ mystery: '201410',
+ value: 0
+ },
+ 201412: {
+ text: t('armorMystery201412Text'),
+ notes: t('armorMystery201412Notes'),
+ mystery: '201412',
+ value: 0
+ },
+ 201501: {
+ text: t('armorMystery201501Text'),
+ notes: t('armorMystery201501Notes'),
+ mystery: '201501',
+ value: 0
+ },
+ 201503: {
+ text: t('armorMystery201503Text'),
+ notes: t('armorMystery201503Notes'),
+ mystery: '201503',
+ value: 0
+ },
+ 201504: {
+ text: t('armorMystery201504Text'),
+ notes: t('armorMystery201504Notes'),
+ mystery: '201504',
+ value: 0
+ },
+ 201506: {
+ text: t('armorMystery201506Text'),
+ notes: t('armorMystery201506Notes'),
+ mystery: '201506',
+ value: 0
+ },
+ 201508: {
+ text: t('armorMystery201508Text'),
+ notes: t('armorMystery201508Notes'),
+ mystery: '201508',
+ value: 0
+ },
+ 201509: {
+ text: t('armorMystery201509Text'),
+ notes: t('armorMystery201509Notes'),
+ mystery: '201509',
+ value: 0
+ },
+ 301404: {
+ text: t('armorMystery301404Text'),
+ notes: t('armorMystery301404Notes'),
+ mystery: '301404',
+ value: 0
+ }
+ },
+ armoire: {
+ lunarArmor: {
+ text: t('armorArmoireLunarArmorText'),
+ notes: t('armorArmoireLunarArmorNotes', {
+ str: 7,
+ int: 7
+ }),
+ value: 100,
+ str: 7,
+ int: 7,
+ set: 'soothing',
+ canOwn: (function(u) {
+ return u.items.gear.owned.armor_armoire_lunarArmor != null;
+ })
+ },
+ gladiatorArmor: {
+ text: t('armorArmoireGladiatorArmorText'),
+ notes: t('armorArmoireGladiatorArmorNotes', {
+ str: 7,
+ per: 7
+ }),
+ value: 100,
+ str: 7,
+ per: 7,
+ set: 'gladiator',
+ canOwn: (function(u) {
+ return u.items.gear.owned.armor_armoire_gladiatorArmor != null;
+ })
+ },
+ rancherRobes: {
+ text: t('armorArmoireRancherRobesText'),
+ notes: t('armorArmoireRancherRobesNotes', {
+ str: 5,
+ per: 5,
+ int: 5
+ }),
+ value: 100,
+ str: 5,
+ per: 5,
+ int: 5,
+ set: 'rancher',
+ canOwn: (function(u) {
+ return u.items.gear.owned.armor_armoire_rancherRobes != null;
+ })
+ },
+ goldenToga: {
+ text: t('armorArmoireGoldenTogaText'),
+ notes: t('armorArmoireGoldenTogaNotes', {
+ attrs: 8
+ }),
+ value: 100,
+ str: 8,
+ con: 8,
+ set: 'goldenToga',
+ canOwn: (function(u) {
+ return u.items.gear.owned.armor_armoire_goldenToga != null;
+ })
+ },
+ hornedIronArmor: {
+ text: t('armorArmoireHornedIronArmorText'),
+ notes: t('armorArmoireHornedIronArmorNotes', {
+ con: 9,
+ per: 7
+ }),
+ value: 100,
+ con: 9,
+ per: 7,
+ set: 'hornedIron',
+ canOwn: (function(u) {
+ return u.items.gear.owned.armor_armoire_hornedIronArmor != null;
+ })
+ },
+ plagueDoctorOvercoat: {
+ text: t('armorArmoirePlagueDoctorOvercoatText'),
+ notes: t('armorArmoirePlagueDoctorOvercoatNotes', {
+ int: 6,
+ str: 5,
+ con: 6
+ }),
+ value: 100,
+ int: 6,
+ str: 5,
+ con: 6,
+ set: 'plagueDoctor',
+ canOwn: (function(u) {
+ return u.items.gear.owned.armor_armoire_plagueDoctorOvercoat != null;
+ })
+ },
+ shepherdRobes: {
+ text: t('armorArmoireShepherdRobesText'),
+ notes: t('armorArmoireShepherdRobesNotes', {
+ attrs: 9
+ }),
+ value: 100,
+ str: 9,
+ per: 9,
+ set: 'shepherd',
+ canOwn: (function(u) {
+ return u.items.gear.owned.armor_armoire_shepherdRobes != null;
+ })
+ },
+ royalRobes: {
+ text: t('armorArmoireRoyalRobesText'),
+ notes: t('armorArmoireRoyalRobesNotes', {
+ attrs: 5
+ }),
+ value: 100,
+ con: 5,
+ per: 5,
+ int: 5,
+ set: 'royal',
+ canOwn: (function(u) {
+ return u.items.gear.owned.armor_armoire_royalRobes != null;
+ })
+ }
+ }
+ },
+ head: {
+ base: {
+ 0: {
+ text: t('headBase0Text'),
+ notes: t('headBase0Notes'),
+ value: 0
+ }
+ },
+ warrior: {
+ 1: {
+ text: t('headWarrior1Text'),
+ notes: t('headWarrior1Notes', {
+ str: 2
+ }),
+ str: 2,
+ value: 15
+ },
+ 2: {
+ text: t('headWarrior2Text'),
+ notes: t('headWarrior2Notes', {
+ str: 4
+ }),
+ str: 4,
+ value: 25
+ },
+ 3: {
+ text: t('headWarrior3Text'),
+ notes: t('headWarrior3Notes', {
+ str: 6
+ }),
+ str: 6,
+ value: 40
+ },
+ 4: {
+ text: t('headWarrior4Text'),
+ notes: t('headWarrior4Notes', {
+ str: 9
+ }),
+ str: 9,
+ value: 60
+ },
+ 5: {
+ text: t('headWarrior5Text'),
+ notes: t('headWarrior5Notes', {
+ str: 12
+ }),
+ str: 12,
+ value: 80,
+ last: true
+ }
+ },
+ rogue: {
+ 1: {
+ text: t('headRogue1Text'),
+ notes: t('headRogue1Notes', {
+ per: 2
+ }),
+ per: 2,
+ value: 15
+ },
+ 2: {
+ text: t('headRogue2Text'),
+ notes: t('headRogue2Notes', {
+ per: 4
+ }),
+ per: 4,
+ value: 25
+ },
+ 3: {
+ text: t('headRogue3Text'),
+ notes: t('headRogue3Notes', {
+ per: 6
+ }),
+ per: 6,
+ value: 40
+ },
+ 4: {
+ text: t('headRogue4Text'),
+ notes: t('headRogue4Notes', {
+ per: 9
+ }),
+ per: 9,
+ value: 60
+ },
+ 5: {
+ text: t('headRogue5Text'),
+ notes: t('headRogue5Notes', {
+ per: 12
+ }),
+ per: 12,
+ value: 80,
+ last: true
+ }
+ },
+ wizard: {
+ 1: {
+ text: t('headWizard1Text'),
+ notes: t('headWizard1Notes', {
+ per: 2
+ }),
+ per: 2,
+ value: 15
+ },
+ 2: {
+ text: t('headWizard2Text'),
+ notes: t('headWizard2Notes', {
+ per: 3
+ }),
+ per: 3,
+ value: 25
+ },
+ 3: {
+ text: t('headWizard3Text'),
+ notes: t('headWizard3Notes', {
+ per: 5
+ }),
+ per: 5,
+ value: 40
+ },
+ 4: {
+ text: t('headWizard4Text'),
+ notes: t('headWizard4Notes', {
+ per: 7
+ }),
+ per: 7,
+ value: 60
+ },
+ 5: {
+ text: t('headWizard5Text'),
+ notes: t('headWizard5Notes', {
+ per: 10
+ }),
+ per: 10,
+ value: 80,
+ last: true
+ }
+ },
+ healer: {
+ 1: {
+ text: t('headHealer1Text'),
+ notes: t('headHealer1Notes', {
+ int: 2
+ }),
+ int: 2,
+ value: 15
+ },
+ 2: {
+ text: t('headHealer2Text'),
+ notes: t('headHealer2Notes', {
+ int: 3
+ }),
+ int: 3,
+ value: 25
+ },
+ 3: {
+ text: t('headHealer3Text'),
+ notes: t('headHealer3Notes', {
+ int: 5
+ }),
+ int: 5,
+ value: 40
+ },
+ 4: {
+ text: t('headHealer4Text'),
+ notes: t('headHealer4Notes', {
+ int: 7
+ }),
+ int: 7,
+ value: 60
+ },
+ 5: {
+ text: t('headHealer5Text'),
+ notes: t('headHealer5Notes', {
+ int: 9
+ }),
+ int: 9,
+ value: 80,
+ last: true
+ }
+ },
+ special: {
+ 0: {
+ text: t('headSpecial0Text'),
+ notes: t('headSpecial0Notes', {
+ int: 20
+ }),
+ int: 20,
+ value: 150,
+ canOwn: (function(u) {
+ var ref;
+ return +((ref = u.backer) != null ? ref.tier : void 0) >= 45;
+ })
+ },
+ 1: {
+ text: t('headSpecial1Text'),
+ notes: t('headSpecial1Notes', {
+ attrs: 6
+ }),
+ con: 6,
+ str: 6,
+ per: 6,
+ int: 6,
+ value: 170,
+ canOwn: (function(u) {
+ var ref;
+ return +((ref = u.contributor) != null ? ref.level : void 0) >= 3;
+ })
+ },
+ 2: {
+ text: t('headSpecial2Text'),
+ notes: t('headSpecial2Notes', {
+ attrs: 25
+ }),
+ int: 25,
+ str: 25,
+ value: 200,
+ canOwn: (function(u) {
+ var ref;
+ return (+((ref = u.backer) != null ? ref.tier : void 0) >= 300) || (u.items.gear.owned.head_special_2 != null);
+ })
+ },
+ fireCoralCirclet: {
+ text: t('headSpecialFireCoralCircletText'),
+ notes: t('headSpecialFireCoralCircletNotes', {
+ per: 15
+ }),
+ per: 15,
+ value: 130,
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_special_fireCoralCirclet != null;
+ })
+ },
+ nye: {
+ event: events.winter,
+ text: t('headSpecialNyeText'),
+ notes: t('headSpecialNyeNotes'),
+ value: 0
+ },
+ yeti: {
+ event: events.winter,
+ specialClass: 'warrior',
+ text: t('headSpecialYetiText'),
+ notes: t('headSpecialYetiNotes', {
+ str: 9
+ }),
+ str: 9,
+ value: 60
+ },
+ ski: {
+ event: events.winter,
+ specialClass: 'rogue',
+ text: t('headSpecialSkiText'),
+ notes: t('headSpecialSkiNotes', {
+ per: 9
+ }),
+ per: 9,
+ value: 60
+ },
+ candycane: {
+ event: events.winter,
+ specialClass: 'wizard',
+ text: t('headSpecialCandycaneText'),
+ notes: t('headSpecialCandycaneNotes', {
+ per: 7
+ }),
+ per: 7,
+ value: 60
+ },
+ snowflake: {
+ event: events.winter,
+ specialClass: 'healer',
+ text: t('headSpecialSnowflakeText'),
+ notes: t('headSpecialSnowflakeNotes', {
+ int: 7
+ }),
+ int: 7,
+ value: 60
+ },
+ springRogue: {
+ event: events.spring,
+ specialClass: 'rogue',
+ text: t('headSpecialSpringRogueText'),
+ notes: t('headSpecialSpringRogueNotes', {
+ per: 9
+ }),
+ value: 60,
+ per: 9
+ },
+ springWarrior: {
+ event: events.spring,
+ specialClass: 'warrior',
+ text: t('headSpecialSpringWarriorText'),
+ notes: t('headSpecialSpringWarriorNotes', {
+ str: 9
+ }),
+ value: 60,
+ str: 9
+ },
+ springMage: {
+ event: events.spring,
+ specialClass: 'wizard',
+ text: t('headSpecialSpringMageText'),
+ notes: t('headSpecialSpringMageNotes', {
+ per: 7
+ }),
+ value: 60,
+ per: 7
+ },
+ springHealer: {
+ event: events.spring,
+ specialClass: 'healer',
+ text: t('headSpecialSpringHealerText'),
+ notes: t('headSpecialSpringHealerNotes', {
+ int: 7
+ }),
+ value: 60,
+ int: 7
+ },
+ summerRogue: {
+ event: events.summer,
+ specialClass: 'rogue',
+ text: t('headSpecialSummerRogueText'),
+ notes: t('headSpecialSummerRogueNotes', {
+ per: 9
+ }),
+ value: 60,
+ per: 9
+ },
+ summerWarrior: {
+ event: events.summer,
+ specialClass: 'warrior',
+ text: t('headSpecialSummerWarriorText'),
+ notes: t('headSpecialSummerWarriorNotes', {
+ str: 9
+ }),
+ value: 60,
+ str: 9
+ },
+ summerMage: {
+ event: events.summer,
+ specialClass: 'wizard',
+ text: t('headSpecialSummerMageText'),
+ notes: t('headSpecialSummerMageNotes', {
+ per: 7
+ }),
+ value: 60,
+ per: 7
+ },
+ summerHealer: {
+ event: events.summer,
+ specialClass: 'healer',
+ text: t('headSpecialSummerHealerText'),
+ notes: t('headSpecialSummerHealerNotes', {
+ int: 7
+ }),
+ value: 60,
+ int: 7
+ },
+ fallRogue: {
+ event: events.fall,
+ specialClass: 'rogue',
+ text: t('headSpecialFallRogueText'),
+ notes: t('headSpecialFallRogueNotes', {
+ per: 9
+ }),
+ value: 60,
+ per: 9,
+ canBuy: (function() {
+ return true;
+ })
+ },
+ fallWarrior: {
+ event: events.fall,
+ specialClass: 'warrior',
+ text: t('headSpecialFallWarriorText'),
+ notes: t('headSpecialFallWarriorNotes', {
+ str: 9
+ }),
+ value: 60,
+ str: 9,
+ canBuy: (function() {
+ return true;
+ })
+ },
+ fallMage: {
+ event: events.fall,
+ specialClass: 'wizard',
+ text: t('headSpecialFallMageText'),
+ notes: t('headSpecialFallMageNotes', {
+ per: 7
+ }),
+ value: 60,
+ per: 7,
+ canBuy: (function() {
+ return true;
+ })
+ },
+ fallHealer: {
+ event: events.fall,
+ specialClass: 'healer',
+ text: t('headSpecialFallHealerText'),
+ notes: t('headSpecialFallHealerNotes', {
+ int: 7
+ }),
+ value: 60,
+ int: 7,
+ canBuy: (function() {
+ return true;
+ })
+ },
+ winter2015Rogue: {
+ event: events.winter2015,
+ specialClass: 'rogue',
+ text: t('headSpecialWinter2015RogueText'),
+ notes: t('headSpecialWinter2015RogueNotes', {
+ per: 9
+ }),
+ value: 60,
+ per: 9
+ },
+ winter2015Warrior: {
+ event: events.winter2015,
+ specialClass: 'warrior',
+ text: t('headSpecialWinter2015WarriorText'),
+ notes: t('headSpecialWinter2015WarriorNotes', {
+ str: 9
+ }),
+ value: 60,
+ str: 9
+ },
+ winter2015Mage: {
+ event: events.winter2015,
+ specialClass: 'wizard',
+ text: t('headSpecialWinter2015MageText'),
+ notes: t('headSpecialWinter2015MageNotes', {
+ per: 7
+ }),
+ value: 60,
+ per: 7
+ },
+ winter2015Healer: {
+ event: events.winter2015,
+ specialClass: 'healer',
+ text: t('headSpecialWinter2015HealerText'),
+ notes: t('headSpecialWinter2015HealerNotes', {
+ int: 7
+ }),
+ value: 60,
+ int: 7
+ },
+ nye2014: {
+ text: t('headSpecialNye2014Text'),
+ notes: t('headSpecialNye2014Notes'),
+ value: 0,
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_special_nye2014 != null;
+ })
+ },
+ spring2015Rogue: {
+ event: events.spring2015,
+ specialClass: 'rogue',
+ text: t('headSpecialSpring2015RogueText'),
+ notes: t('headSpecialSpring2015RogueNotes', {
+ per: 9
+ }),
+ value: 60,
+ per: 9
+ },
+ spring2015Warrior: {
+ event: events.spring2015,
+ specialClass: 'warrior',
+ text: t('headSpecialSpring2015WarriorText'),
+ notes: t('headSpecialSpring2015WarriorNotes', {
+ str: 9
+ }),
+ value: 60,
+ str: 9
+ },
+ spring2015Mage: {
+ event: events.spring2015,
+ specialClass: 'wizard',
+ text: t('headSpecialSpring2015MageText'),
+ notes: t('headSpecialSpring2015MageNotes', {
+ per: 7
+ }),
+ value: 60,
+ per: 7
+ },
+ spring2015Healer: {
+ event: events.spring2015,
+ specialClass: 'healer',
+ text: t('headSpecialSpring2015HealerText'),
+ notes: t('headSpecialSpring2015HealerNotes', {
+ int: 7
+ }),
+ value: 60,
+ int: 7
+ },
+ summer2015Rogue: {
+ event: events.summer2015,
+ specialClass: 'rogue',
+ text: t('headSpecialSummer2015RogueText'),
+ notes: t('headSpecialSummer2015RogueNotes', {
+ per: 9
+ }),
+ value: 60,
+ per: 9
+ },
+ summer2015Warrior: {
+ event: events.summer2015,
+ specialClass: 'warrior',
+ text: t('headSpecialSummer2015WarriorText'),
+ notes: t('headSpecialSummer2015WarriorNotes', {
+ str: 9
+ }),
+ value: 60,
+ str: 9
+ },
+ summer2015Mage: {
+ event: events.summer2015,
+ specialClass: 'wizard',
+ text: t('headSpecialSummer2015MageText'),
+ notes: t('headSpecialSummer2015MageNotes', {
+ per: 7
+ }),
+ value: 60,
+ per: 7
+ },
+ summer2015Healer: {
+ event: events.summer2015,
+ specialClass: 'healer',
+ text: t('headSpecialSummer2015HealerText'),
+ notes: t('headSpecialSummer2015HealerNotes', {
+ int: 7
+ }),
+ value: 60,
+ int: 7
+ },
+ fall2015Rogue: {
+ event: events.fall2015,
+ specialClass: 'rogue',
+ text: t('headSpecialFall2015RogueText'),
+ notes: t('headSpecialFall2015RogueNotes', {
+ per: 9
+ }),
+ value: 60,
+ per: 9
+ },
+ fall2015Warrior: {
+ event: events.fall2015,
+ specialClass: 'warrior',
+ text: t('headSpecialFall2015WarriorText'),
+ notes: t('headSpecialFall2015WarriorNotes', {
+ str: 9
+ }),
+ value: 60,
+ str: 9
+ },
+ fall2015Mage: {
+ event: events.fall2015,
+ specialClass: 'wizard',
+ text: t('headSpecialFall2015MageText'),
+ notes: t('headSpecialFall2015MageNotes', {
+ per: 7
+ }),
+ value: 60,
+ per: 7
+ },
+ fall2015Healer: {
+ event: events.fall2015,
+ specialClass: 'healer',
+ text: t('headSpecialFall2015HealerText'),
+ notes: t('headSpecialFall2015HealerNotes', {
+ int: 7
+ }),
+ value: 60,
+ int: 7
+ },
+ gaymerx: {
+ event: events.gaymerx,
+ text: t('headSpecialGaymerxText'),
+ notes: t('headSpecialGaymerxNotes'),
+ value: 0
+ }
+ },
+ mystery: {
+ 201402: {
+ text: t('headMystery201402Text'),
+ notes: t('headMystery201402Notes'),
+ mystery: '201402',
+ value: 0
+ },
+ 201405: {
+ text: t('headMystery201405Text'),
+ notes: t('headMystery201405Notes'),
+ mystery: '201405',
+ value: 0
+ },
+ 201406: {
+ text: t('headMystery201406Text'),
+ notes: t('headMystery201406Notes'),
+ mystery: '201406',
+ value: 0
+ },
+ 201407: {
+ text: t('headMystery201407Text'),
+ notes: t('headMystery201407Notes'),
+ mystery: '201407',
+ value: 0
+ },
+ 201408: {
+ text: t('headMystery201408Text'),
+ notes: t('headMystery201408Notes'),
+ mystery: '201408',
+ value: 0
+ },
+ 201411: {
+ text: t('headMystery201411Text'),
+ notes: t('headMystery201411Notes'),
+ mystery: '201411',
+ value: 0
+ },
+ 201412: {
+ text: t('headMystery201412Text'),
+ notes: t('headMystery201412Notes'),
+ mystery: '201412',
+ value: 0
+ },
+ 201501: {
+ text: t('headMystery201501Text'),
+ notes: t('headMystery201501Notes'),
+ mystery: '201501',
+ value: 0
+ },
+ 201505: {
+ text: t('headMystery201505Text'),
+ notes: t('headMystery201505Notes'),
+ mystery: '201505',
+ value: 0
+ },
+ 201508: {
+ text: t('headMystery201508Text'),
+ notes: t('headMystery201508Notes'),
+ mystery: '201508',
+ value: 0
+ },
+ 201509: {
+ text: t('headMystery201509Text'),
+ notes: t('headMystery201509Notes'),
+ mystery: '201509',
+ value: 0
+ },
+ 301404: {
+ text: t('headMystery301404Text'),
+ notes: t('headMystery301404Notes'),
+ mystery: '301404',
+ value: 0
+ },
+ 301405: {
+ text: t('headMystery301405Text'),
+ notes: t('headMystery301405Notes'),
+ mystery: '301405',
+ value: 0
+ }
+ },
+ armoire: {
+ lunarCrown: {
+ text: t('headArmoireLunarCrownText'),
+ notes: t('headArmoireLunarCrownNotes', {
+ con: 7,
+ per: 7
+ }),
+ value: 100,
+ con: 7,
+ per: 7,
+ set: 'soothing',
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_lunarCrown != null;
+ })
+ },
+ redHairbow: {
+ text: t('headArmoireRedHairbowText'),
+ notes: t('headArmoireRedHairbowNotes', {
+ str: 5,
+ int: 5,
+ con: 5
+ }),
+ value: 100,
+ str: 5,
+ int: 5,
+ con: 5,
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_redHairbow != null;
+ })
+ },
+ violetFloppyHat: {
+ text: t('headArmoireVioletFloppyHatText'),
+ notes: t('headArmoireVioletFloppyHatNotes', {
+ per: 5,
+ int: 5,
+ con: 5
+ }),
+ value: 100,
+ per: 5,
+ int: 5,
+ con: 5,
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_violetFloppyHat != null;
+ })
+ },
+ gladiatorHelm: {
+ text: t('headArmoireGladiatorHelmText'),
+ notes: t('headArmoireGladiatorHelmNotes', {
+ per: 7,
+ int: 7
+ }),
+ value: 100,
+ per: 7,
+ int: 7,
+ set: 'gladiator',
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_gladiatorHelm != null;
+ })
+ },
+ rancherHat: {
+ text: t('headArmoireRancherHatText'),
+ notes: t('headArmoireRancherHatNotes', {
+ str: 5,
+ per: 5,
+ int: 5
+ }),
+ value: 100,
+ str: 5,
+ per: 5,
+ int: 5,
+ set: 'rancher',
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_rancherHat != null;
+ })
+ },
+ royalCrown: {
+ text: t('headArmoireRoyalCrownText'),
+ notes: t('headArmoireRoyalCrownNotes', {
+ str: 10
+ }),
+ value: 100,
+ str: 10,
+ set: 'royal',
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_royalCrown != null;
+ })
+ },
+ blueHairbow: {
+ text: t('headArmoireBlueHairbowText'),
+ notes: t('headArmoireBlueHairbowNotes', {
+ per: 5,
+ int: 5,
+ con: 5
+ }),
+ value: 100,
+ per: 5,
+ int: 5,
+ con: 5,
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_blueHairbow != null;
+ })
+ },
+ goldenLaurels: {
+ text: t('headArmoireGoldenLaurelsText'),
+ notes: t('headArmoireGoldenLaurelsNotes', {
+ attrs: 8
+ }),
+ value: 100,
+ per: 8,
+ con: 8,
+ set: 'goldenToga',
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_goldenLaurels != null;
+ })
+ },
+ hornedIronHelm: {
+ text: t('headArmoireHornedIronHelmText'),
+ notes: t('headArmoireHornedIronHelmNotes', {
+ con: 9,
+ str: 7
+ }),
+ value: 100,
+ con: 9,
+ str: 7,
+ set: 'hornedIron',
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_hornedIronHelm != null;
+ })
+ },
+ yellowHairbow: {
+ text: t('headArmoireYellowHairbowText'),
+ notes: t('headArmoireYellowHairbowNotes', {
+ attrs: 5
+ }),
+ value: 100,
+ int: 5,
+ per: 5,
+ str: 5,
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_yellowHairbow != null;
+ })
+ },
+ redFloppyHat: {
+ text: t('headArmoireRedFloppyHatText'),
+ notes: t('headArmoireRedFloppyHatNotes', {
+ attrs: 6
+ }),
+ value: 100,
+ con: 6,
+ int: 6,
+ per: 6,
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_redFloppyHat != null;
+ })
+ },
+ plagueDoctorHat: {
+ text: t('headArmoirePlagueDoctorHatText'),
+ notes: t('headArmoirePlagueDoctorHatNotes', {
+ int: 5,
+ str: 6,
+ con: 5
+ }),
+ value: 100,
+ int: 5,
+ str: 6,
+ con: 5,
+ set: 'plagueDoctor',
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_plagueDoctorHat != null;
+ })
+ },
+ blackCat: {
+ text: t('headArmoireBlackCatText'),
+ notes: t('headArmoireBlackCatNotes', {
+ attrs: 9
+ }),
+ value: 100,
+ int: 9,
+ per: 9,
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_blackCat != null;
+ })
+ },
+ orangeCat: {
+ text: t('headArmoireOrangeCatText'),
+ notes: t('headArmoireOrangeCatNotes', {
+ attrs: 9
+ }),
+ value: 100,
+ con: 9,
+ str: 9,
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_orangeCat != null;
+ })
+ },
+ blueFloppyHat: {
+ text: t('headArmoireBlueFloppyHatText'),
+ notes: t('headArmoireBlueFloppyHatNotes', {
+ attrs: 7
+ }),
+ value: 100,
+ per: 7,
+ int: 7,
+ con: 7,
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_blueFloppyHat != null;
+ })
+ },
+ shepherdHeaddress: {
+ text: t('headArmoireShepherdHeaddressText'),
+ notes: t('headArmoireShepherdHeaddressNotes', {
+ int: 9
+ }),
+ value: 100,
+ int: 9,
+ set: 'shepherd',
+ canOwn: (function(u) {
+ return u.items.gear.owned.head_armoire_shepherdHeaddress != null;
+ })
+ }
+ }
+ },
+ shield: {
+ base: {
+ 0: {
+ text: t('shieldBase0Text'),
+ notes: t('shieldBase0Notes'),
+ value: 0
+ }
+ },
+ warrior: {
+ 1: {
+ text: t('shieldWarrior1Text'),
+ notes: t('shieldWarrior1Notes', {
+ con: 2
+ }),
+ con: 2,
+ value: 20
+ },
+ 2: {
+ text: t('shieldWarrior2Text'),
+ notes: t('shieldWarrior2Notes', {
+ con: 3
+ }),
+ con: 3,
+ value: 35
+ },
+ 3: {
+ text: t('shieldWarrior3Text'),
+ notes: t('shieldWarrior3Notes', {
+ con: 5
+ }),
+ con: 5,
+ value: 50
+ },
+ 4: {
+ text: t('shieldWarrior4Text'),
+ notes: t('shieldWarrior4Notes', {
+ con: 7
+ }),
+ con: 7,
+ value: 70
+ },
+ 5: {
+ text: t('shieldWarrior5Text'),
+ notes: t('shieldWarrior5Notes', {
+ con: 9
+ }),
+ con: 9,
+ value: 90,
+ last: true
+ }
+ },
+ rogue: {
+ 0: {
+ text: t('weaponRogue0Text'),
+ notes: t('weaponRogue0Notes'),
+ str: 0,
+ value: 0
+ },
+ 1: {
+ text: t('weaponRogue1Text'),
+ notes: t('weaponRogue1Notes', {
+ str: 2
+ }),
+ str: 2,
+ value: 20
+ },
+ 2: {
+ text: t('weaponRogue2Text'),
+ notes: t('weaponRogue2Notes', {
+ str: 3
+ }),
+ str: 3,
+ value: 35
+ },
+ 3: {
+ text: t('weaponRogue3Text'),
+ notes: t('weaponRogue3Notes', {
+ str: 4
+ }),
+ str: 4,
+ value: 50
+ },
+ 4: {
+ text: t('weaponRogue4Text'),
+ notes: t('weaponRogue4Notes', {
+ str: 6
+ }),
+ str: 6,
+ value: 70
+ },
+ 5: {
+ text: t('weaponRogue5Text'),
+ notes: t('weaponRogue5Notes', {
+ str: 8
+ }),
+ str: 8,
+ value: 90
+ },
+ 6: {
+ text: t('weaponRogue6Text'),
+ notes: t('weaponRogue6Notes', {
+ str: 10
+ }),
+ str: 10,
+ value: 120,
+ last: true
+ }
+ },
+ wizard: {},
+ healer: {
+ 1: {
+ text: t('shieldHealer1Text'),
+ notes: t('shieldHealer1Notes', {
+ con: 2
+ }),
+ con: 2,
+ value: 20
+ },
+ 2: {
+ text: t('shieldHealer2Text'),
+ notes: t('shieldHealer2Notes', {
+ con: 4
+ }),
+ con: 4,
+ value: 35
+ },
+ 3: {
+ text: t('shieldHealer3Text'),
+ notes: t('shieldHealer3Notes', {
+ con: 6
+ }),
+ con: 6,
+ value: 50
+ },
+ 4: {
+ text: t('shieldHealer4Text'),
+ notes: t('shieldHealer4Notes', {
+ con: 9
+ }),
+ con: 9,
+ value: 70
+ },
+ 5: {
+ text: t('shieldHealer5Text'),
+ notes: t('shieldHealer5Notes', {
+ con: 12
+ }),
+ con: 12,
+ value: 90,
+ last: true
+ }
+ },
+ special: {
+ 0: {
+ text: t('shieldSpecial0Text'),
+ notes: t('shieldSpecial0Notes', {
+ per: 20
+ }),
+ per: 20,
+ value: 150,
+ canOwn: (function(u) {
+ var ref;
+ return +((ref = u.backer) != null ? ref.tier : void 0) >= 45;
+ })
+ },
+ 1: {
+ text: t('shieldSpecial1Text'),
+ notes: t('shieldSpecial1Notes', {
+ attrs: 6
+ }),
+ con: 6,
+ str: 6,
+ per: 6,
+ int: 6,
+ value: 170,
+ canOwn: (function(u) {
+ var ref;
+ return +((ref = u.contributor) != null ? ref.level : void 0) >= 5;
+ })
+ },
+ goldenknight: {
+ text: t('shieldSpecialGoldenknightText'),
+ notes: t('shieldSpecialGoldenknightNotes', {
+ attrs: 25
+ }),
+ con: 25,
+ per: 25,
+ value: 200,
+ canOwn: (function(u) {
+ return u.items.gear.owned.shield_special_goldenknight != null;
+ })
+ },
+ moonpearlShield: {
+ text: t('shieldSpecialMoonpearlShieldText'),
+ notes: t('shieldSpecialMoonpearlShieldNotes', {
+ con: 15
+ }),
+ con: 15,
+ value: 130,
+ canOwn: (function(u) {
+ return u.items.gear.owned.shield_special_moonpearlShield != null;
+ })
+ },
+ yeti: {
+ event: events.winter,
+ specialClass: 'warrior',
+ text: t('shieldSpecialYetiText'),
+ notes: t('shieldSpecialYetiNotes', {
+ con: 7
+ }),
+ con: 7,
+ value: 70
+ },
+ ski: {
+ event: events.winter,
+ specialClass: 'rogue',
+ text: t('weaponSpecialSkiText'),
+ notes: t('weaponSpecialSkiNotes', {
+ str: 8
+ }),
+ str: 8,
+ value: 90
+ },
+ snowflake: {
+ event: events.winter,
+ specialClass: 'healer',
+ text: t('shieldSpecialSnowflakeText'),
+ notes: t('shieldSpecialSnowflakeNotes', {
+ con: 9
+ }),
+ con: 9,
+ value: 70
+ },
+ springRogue: {
+ event: events.spring,
+ specialClass: 'rogue',
+ text: t('shieldSpecialSpringRogueText'),
+ notes: t('shieldSpecialSpringRogueNotes', {
+ str: 8
+ }),
+ value: 80,
+ str: 8
+ },
+ springWarrior: {
+ event: events.spring,
+ specialClass: 'warrior',
+ text: t('shieldSpecialSpringWarriorText'),
+ notes: t('shieldSpecialSpringWarriorNotes', {
+ con: 7
+ }),
+ value: 70,
+ con: 7
+ },
+ springHealer: {
+ event: events.spring,
+ specialClass: 'healer',
+ text: t('shieldSpecialSpringHealerText'),
+ notes: t('shieldSpecialSpringHealerNotes', {
+ con: 9
+ }),
+ value: 70,
+ con: 9
+ },
+ summerRogue: {
+ event: events.summer,
+ specialClass: 'rogue',
+ text: t('shieldSpecialSummerRogueText'),
+ notes: t('shieldSpecialSummerRogueNotes', {
+ str: 8
+ }),
+ value: 80,
+ str: 8
+ },
+ summerWarrior: {
+ event: events.summer,
+ specialClass: 'warrior',
+ text: t('shieldSpecialSummerWarriorText'),
+ notes: t('shieldSpecialSummerWarriorNotes', {
+ con: 7
+ }),
+ value: 70,
+ con: 7
+ },
+ summerHealer: {
+ event: events.summer,
+ specialClass: 'healer',
+ text: t('shieldSpecialSummerHealerText'),
+ notes: t('shieldSpecialSummerHealerNotes', {
+ con: 9
+ }),
+ value: 70,
+ con: 9
+ },
+ fallRogue: {
+ event: events.fall,
+ specialClass: 'rogue',
+ text: t('shieldSpecialFallRogueText'),
+ notes: t('shieldSpecialFallRogueNotes', {
+ str: 8
+ }),
+ value: 80,
+ str: 8,
+ canBuy: (function() {
+ return true;
+ })
+ },
+ fallWarrior: {
+ event: events.fall,
+ specialClass: 'warrior',
+ text: t('shieldSpecialFallWarriorText'),
+ notes: t('shieldSpecialFallWarriorNotes', {
+ con: 7
+ }),
+ value: 70,
+ con: 7,
+ canBuy: (function() {
+ return true;
+ })
+ },
+ fallHealer: {
+ event: events.fall,
+ specialClass: 'healer',
+ text: t('shieldSpecialFallHealerText'),
+ notes: t('shieldSpecialFallHealerNotes', {
+ con: 9
+ }),
+ value: 70,
+ con: 9,
+ canBuy: (function() {
+ return true;
+ })
+ },
+ winter2015Rogue: {
+ event: events.winter2015,
+ specialClass: 'rogue',
+ text: t('shieldSpecialWinter2015RogueText'),
+ notes: t('shieldSpecialWinter2015RogueNotes', {
+ str: 8
+ }),
+ value: 80,
+ str: 8
+ },
+ winter2015Warrior: {
+ event: events.winter2015,
+ specialClass: 'warrior',
+ text: t('shieldSpecialWinter2015WarriorText'),
+ notes: t('shieldSpecialWinter2015WarriorNotes', {
+ con: 7
+ }),
+ value: 70,
+ con: 7
+ },
+ winter2015Healer: {
+ event: events.winter2015,
+ specialClass: 'healer',
+ text: t('shieldSpecialWinter2015HealerText'),
+ notes: t('shieldSpecialWinter2015HealerNotes', {
+ con: 9
+ }),
+ value: 70,
+ con: 9
+ },
+ spring2015Rogue: {
+ event: events.spring2015,
+ specialClass: 'rogue',
+ text: t('shieldSpecialSpring2015RogueText'),
+ notes: t('shieldSpecialSpring2015RogueNotes', {
+ str: 8
+ }),
+ value: 80,
+ str: 8
+ },
+ spring2015Warrior: {
+ event: events.spring2015,
+ specialClass: 'warrior',
+ text: t('shieldSpecialSpring2015WarriorText'),
+ notes: t('shieldSpecialSpring2015WarriorNotes', {
+ con: 7
+ }),
+ value: 70,
+ con: 7
+ },
+ spring2015Healer: {
+ event: events.spring2015,
+ specialClass: 'healer',
+ text: t('shieldSpecialSpring2015HealerText'),
+ notes: t('shieldSpecialSpring2015HealerNotes', {
+ con: 9
+ }),
+ value: 70,
+ con: 9
+ },
+ summer2015Rogue: {
+ event: events.summer2015,
+ specialClass: 'rogue',
+ text: t('shieldSpecialSummer2015RogueText'),
+ notes: t('shieldSpecialSummer2015RogueNotes', {
+ str: 8
+ }),
+ value: 80,
+ str: 8
+ },
+ summer2015Warrior: {
+ event: events.summer2015,
+ specialClass: 'warrior',
+ text: t('shieldSpecialSummer2015WarriorText'),
+ notes: t('shieldSpecialSummer2015WarriorNotes', {
+ con: 7
+ }),
+ value: 70,
+ con: 7
+ },
+ summer2015Healer: {
+ event: events.summer2015,
+ specialClass: 'healer',
+ text: t('shieldSpecialSummer2015HealerText'),
+ notes: t('shieldSpecialSummer2015HealerNotes', {
+ con: 9
+ }),
+ value: 70,
+ con: 9
+ },
+ fall2015Rogue: {
+ event: events.fall2015,
+ specialClass: 'rogue',
+ text: t('shieldSpecialFall2015RogueText'),
+ notes: t('shieldSpecialFall2015RogueNotes', {
+ str: 8
+ }),
+ value: 80,
+ str: 8
+ },
+ fall2015Warrior: {
+ event: events.fall2015,
+ specialClass: 'warrior',
+ text: t('shieldSpecialFall2015WarriorText'),
+ notes: t('shieldSpecialFall2015WarriorNotes', {
+ con: 7
+ }),
+ value: 70,
+ con: 7
+ },
+ fall2015Healer: {
+ event: events.fall2015,
+ specialClass: 'healer',
+ text: t('shieldSpecialFall2015HealerText'),
+ notes: t('shieldSpecialFall2015HealerNotes', {
+ con: 9
+ }),
+ value: 70,
+ con: 9
+ }
+ },
+ mystery: {
+ 301405: {
+ text: t('shieldMystery301405Text'),
+ notes: t('shieldMystery301405Notes'),
+ mystery: '301405',
+ value: 0
+ }
+ },
+ armoire: {
+ gladiatorShield: {
+ text: t('shieldArmoireGladiatorShieldText'),
+ notes: t('shieldArmoireGladiatorShieldNotes', {
+ con: 5,
+ str: 5
+ }),
+ value: 100,
+ con: 5,
+ str: 5,
+ set: 'gladiator',
+ canOwn: (function(u) {
+ return u.items.gear.owned.shield_armoire_gladiatorShield != null;
+ })
+ },
+ midnightShield: {
+ text: t('shieldArmoireMidnightShieldText'),
+ notes: t('shieldArmoireMidnightShieldNotes', {
+ con: 10,
+ str: 2
+ }),
+ value: 100,
+ con: 10,
+ str: 2,
+ canOwn: (function(u) {
+ return u.items.gear.owned.shield_armoire_midnightShield != null;
+ })
+ },
+ royalCane: {
+ text: t('shieldArmoireRoyalCaneText'),
+ notes: t('shieldArmoireRoyalCaneNotes', {
+ attrs: 5
+ }),
+ value: 100,
+ con: 5,
+ int: 5,
+ per: 5,
+ set: 'royal',
+ canOwn: (function(u) {
+ return u.items.gear.owned.shield_armoire_royalCane != null;
+ })
+ }
+ }
+ },
+ back: {
+ base: {
+ 0: {
+ text: t('backBase0Text'),
+ notes: t('backBase0Notes'),
+ value: 0
+ }
+ },
+ mystery: {
+ 201402: {
+ text: t('backMystery201402Text'),
+ notes: t('backMystery201402Notes'),
+ mystery: '201402',
+ value: 0
+ },
+ 201404: {
+ text: t('backMystery201404Text'),
+ notes: t('backMystery201404Notes'),
+ mystery: '201404',
+ value: 0
+ },
+ 201410: {
+ text: t('backMystery201410Text'),
+ notes: t('backMystery201410Notes'),
+ mystery: '201410',
+ value: 0
+ },
+ 201504: {
+ text: t('backMystery201504Text'),
+ notes: t('backMystery201504Notes'),
+ mystery: '201504',
+ value: 0
+ },
+ 201507: {
+ text: t('backMystery201507Text'),
+ notes: t('backMystery201507Notes'),
+ mystery: '201507',
+ value: 0
+ },
+ 201510: {
+ text: t('backMystery201510Text'),
+ notes: t('backMystery201510Notes'),
+ mystery: '201510',
+ value: 0
+ }
+ },
+ special: {
+ wondercon_red: {
+ text: t('backSpecialWonderconRedText'),
+ notes: t('backSpecialWonderconRedNotes'),
+ value: 0,
+ mystery: 'wondercon'
+ },
+ wondercon_black: {
+ text: t('backSpecialWonderconBlackText'),
+ notes: t('backSpecialWonderconBlackNotes'),
+ value: 0,
+ mystery: 'wondercon'
+ }
+ }
+ },
+ body: {
+ base: {
+ 0: {
+ text: t('bodyBase0Text'),
+ notes: t('bodyBase0Notes'),
+ value: 0
+ }
+ },
+ special: {
+ wondercon_red: {
+ text: t('bodySpecialWonderconRedText'),
+ notes: t('bodySpecialWonderconRedNotes'),
+ value: 0,
+ mystery: 'wondercon'
+ },
+ wondercon_gold: {
+ text: t('bodySpecialWonderconGoldText'),
+ notes: t('bodySpecialWonderconGoldNotes'),
+ value: 0,
+ mystery: 'wondercon'
+ },
+ wondercon_black: {
+ text: t('bodySpecialWonderconBlackText'),
+ notes: t('bodySpecialWonderconBlackNotes'),
+ value: 0,
+ mystery: 'wondercon'
+ },
+ summerHealer: {
+ event: events.summer,
+ specialClass: 'healer',
+ text: t('bodySpecialSummerHealerText'),
+ notes: t('bodySpecialSummerHealerNotes'),
+ value: 20
+ },
+ summerMage: {
+ event: events.summer,
+ specialClass: 'wizard',
+ text: t('bodySpecialSummerMageText'),
+ notes: t('bodySpecialSummerMageNotes'),
+ value: 20
+ },
+ summer2015Healer: {
+ event: events.summer2015,
+ specialClass: 'healer',
+ text: t('bodySpecialSummer2015HealerText'),
+ notes: t('bodySpecialSummer2015HealerNotes'),
+ value: 20
+ },
+ summer2015Mage: {
+ event: events.summer2015,
+ specialClass: 'wizard',
+ text: t('bodySpecialSummer2015MageText'),
+ notes: t('bodySpecialSummer2015MageNotes'),
+ value: 20
+ },
+ summer2015Rogue: {
+ event: events.summer2015,
+ specialClass: 'rogue',
+ text: t('bodySpecialSummer2015RogueText'),
+ notes: t('bodySpecialSummer2015RogueNotes'),
+ value: 20
+ },
+ summer2015Warrior: {
+ event: events.summer2015,
+ specialClass: 'warrior',
+ text: t('bodySpecialSummer2015WarriorText'),
+ notes: t('bodySpecialSummer2015WarriorNotes'),
+ value: 20
+ }
+ }
+ },
+ headAccessory: {
+ base: {
+ 0: {
+ text: t('headAccessoryBase0Text'),
+ notes: t('headAccessoryBase0Notes'),
+ value: 0,
+ last: true
+ }
+ },
+ special: {
+ springRogue: {
+ event: events.spring,
+ specialClass: 'rogue',
+ text: t('headAccessorySpecialSpringRogueText'),
+ notes: t('headAccessorySpecialSpringRogueNotes'),
+ value: 20
+ },
+ springWarrior: {
+ event: events.spring,
+ specialClass: 'warrior',
+ text: t('headAccessorySpecialSpringWarriorText'),
+ notes: t('headAccessorySpecialSpringWarriorNotes'),
+ value: 20
+ },
+ springMage: {
+ event: events.spring,
+ specialClass: 'wizard',
+ text: t('headAccessorySpecialSpringMageText'),
+ notes: t('headAccessorySpecialSpringMageNotes'),
+ value: 20
+ },
+ springHealer: {
+ event: events.spring,
+ specialClass: 'healer',
+ text: t('headAccessorySpecialSpringHealerText'),
+ notes: t('headAccessorySpecialSpringHealerNotes'),
+ value: 20
+ },
+ spring2015Rogue: {
+ event: events.spring2015,
+ specialClass: 'rogue',
+ text: t('headAccessorySpecialSpring2015RogueText'),
+ notes: t('headAccessorySpecialSpring2015RogueNotes'),
+ value: 20
+ },
+ spring2015Warrior: {
+ event: events.spring2015,
+ specialClass: 'warrior',
+ text: t('headAccessorySpecialSpring2015WarriorText'),
+ notes: t('headAccessorySpecialSpring2015WarriorNotes'),
+ value: 20
+ },
+ spring2015Mage: {
+ event: events.spring2015,
+ specialClass: 'wizard',
+ text: t('headAccessorySpecialSpring2015MageText'),
+ notes: t('headAccessorySpecialSpring2015MageNotes'),
+ value: 20
+ },
+ spring2015Healer: {
+ event: events.spring2015,
+ specialClass: 'healer',
+ text: t('headAccessorySpecialSpring2015HealerText'),
+ notes: t('headAccessorySpecialSpring2015HealerNotes'),
+ value: 20
+ },
+ bearEars: {
+ gearSet: 'animal',
+ text: t('headAccessoryBearEarsText'),
+ notes: t('headAccessoryBearEarsNotes'),
+ value: 20,
+ canOwn: (function(u) {
+ return u.items.gear.owned.headAccessory_special_bearEars != null;
+ }),
+ canBuy: (function() {
+ return true;
+ })
+ },
+ cactusEars: {
+ gearSet: 'animal',
+ text: t('headAccessoryCactusEarsText'),
+ notes: t('headAccessoryCactusEarsNotes'),
+ value: 20,
+ canOwn: (function(u) {
+ return u.items.gear.owned.headAccessory_special_cactusEars != null;
+ }),
+ canBuy: (function() {
+ return true;
+ })
+ },
+ foxEars: {
+ gearSet: 'animal',
+ text: t('headAccessoryFoxEarsText'),
+ notes: t('headAccessoryFoxEarsNotes'),
+ value: 20,
+ canOwn: (function(u) {
+ return u.items.gear.owned.headAccessory_special_foxEars != null;
+ }),
+ canBuy: (function() {
+ return true;
+ })
+ },
+ lionEars: {
+ gearSet: 'animal',
+ text: t('headAccessoryLionEarsText'),
+ notes: t('headAccessoryLionEarsNotes'),
+ value: 20,
+ canOwn: (function(u) {
+ return u.items.gear.owned.headAccessory_special_lionEars != null;
+ }),
+ canBuy: (function() {
+ return true;
+ })
+ },
+ pandaEars: {
+ gearSet: 'animal',
+ text: t('headAccessoryPandaEarsText'),
+ notes: t('headAccessoryPandaEarsNotes'),
+ value: 20,
+ canOwn: (function(u) {
+ return u.items.gear.owned.headAccessory_special_pandaEars != null;
+ }),
+ canBuy: (function() {
+ return true;
+ })
+ },
+ pigEars: {
+ gearSet: 'animal',
+ text: t('headAccessoryPigEarsText'),
+ notes: t('headAccessoryPigEarsNotes'),
+ value: 20,
+ canOwn: (function(u) {
+ return u.items.gear.owned.headAccessory_special_pigEars != null;
+ }),
+ canBuy: (function() {
+ return true;
+ })
+ },
+ tigerEars: {
+ gearSet: 'animal',
+ text: t('headAccessoryTigerEarsText'),
+ notes: t('headAccessoryTigerEarsNotes'),
+ value: 20,
+ canOwn: (function(u) {
+ return u.items.gear.owned.headAccessory_special_tigerEars != null;
+ }),
+ canBuy: (function() {
+ return true;
+ })
+ },
+ wolfEars: {
+ gearSet: 'animal',
+ text: t('headAccessoryWolfEarsText'),
+ notes: t('headAccessoryWolfEarsNotes'),
+ value: 20,
+ canOwn: (function(u) {
+ return u.items.gear.owned.headAccessory_special_wolfEars != null;
+ }),
+ canBuy: (function() {
+ return true;
+ })
+ }
+ },
+ mystery: {
+ 201403: {
+ text: t('headAccessoryMystery201403Text'),
+ notes: t('headAccessoryMystery201403Notes'),
+ mystery: '201403',
+ value: 0
+ },
+ 201404: {
+ text: t('headAccessoryMystery201404Text'),
+ notes: t('headAccessoryMystery201404Notes'),
+ mystery: '201404',
+ value: 0
+ },
+ 201409: {
+ text: t('headAccessoryMystery201409Text'),
+ notes: t('headAccessoryMystery201409Notes'),
+ mystery: '201409',
+ value: 0
+ },
+ 201502: {
+ text: t('headAccessoryMystery201502Text'),
+ notes: t('headAccessoryMystery201502Notes'),
+ mystery: '201502',
+ value: 0
+ },
+ 201510: {
+ text: t('headAccessoryMystery201510Text'),
+ notes: t('headAccessoryMystery201510Notes'),
+ mystery: '201510',
+ value: 0
+ },
+ 301405: {
+ text: t('headAccessoryMystery301405Text'),
+ notes: t('headAccessoryMystery301405Notes'),
+ mystery: '301405',
+ value: 0
+ }
+ }
+ },
+ eyewear: {
+ base: {
+ 0: {
+ text: t('eyewearBase0Text'),
+ notes: t('eyewearBase0Notes'),
+ value: 0,
+ last: true
+ }
+ },
+ special: {
+ wondercon_red: {
+ text: t('eyewearSpecialWonderconRedText'),
+ notes: t('eyewearSpecialWonderconRedNotes'),
+ value: 0,
+ mystery: 'wondercon'
+ },
+ wondercon_black: {
+ text: t('eyewearSpecialWonderconBlackText'),
+ notes: t('eyewearSpecialWonderconBlackNotes'),
+ value: 0,
+ mystery: 'wondercon'
+ },
+ summerRogue: {
+ event: events.summer,
+ specialClass: 'rogue',
+ text: t('eyewearSpecialSummerRogueText'),
+ notes: t('eyewearSpecialSummerRogueNotes'),
+ value: 20
+ },
+ summerWarrior: {
+ event: events.summer,
+ specialClass: 'warrior',
+ text: t('eyewearSpecialSummerWarriorText'),
+ notes: t('eyewearSpecialSummerWarriorNotes'),
+ value: 20
+ }
+ },
+ mystery: {
+ 201503: {
+ text: t('eyewearMystery201503Text'),
+ notes: t('eyewearMystery201503Notes'),
+ mystery: '201503',
+ value: 0
+ },
+ 201506: {
+ text: t('eyewearMystery201506Text'),
+ notes: t('eyewearMystery201506Notes'),
+ mystery: '201506',
+ value: 0
+ },
+ 201507: {
+ text: t('eyewearMystery201507Text'),
+ notes: t('eyewearMystery201507Notes'),
+ mystery: '201507',
+ value: 0
+ },
+ 301404: {
+ text: t('eyewearMystery301404Text'),
+ notes: t('eyewearMystery301404Notes'),
+ mystery: '301404',
+ value: 0
+ },
+ 301405: {
+ text: t('eyewearMystery301405Text'),
+ notes: t('eyewearMystery301405Notes'),
+ mystery: '301405',
+ value: 0
+ }
+ },
+ armoire: {
+ plagueDoctorMask: {
+ text: t('eyewearArmoirePlagueDoctorMaskText'),
+ notes: t('eyewearArmoirePlagueDoctorMaskNotes'),
+ value: 100,
+ set: 'plagueDoctor',
+ canOwn: (function(u) {
+ return u.items.gear.owned.eyewear_armoire_plagueDoctorMask != null;
+ })
+ }
+ }
+ }
+};
+
+
+/*
+ The gear is exported as a tree (defined above), and a flat list (eg, {weapon_healer_1: .., shield_special_0: ...}) since
+ they are needed in different forms at different points in the app
+ */
+
+api.gear = {
+ tree: gear,
+ flat: {}
+};
+
+_.each(gearTypes, function(type) {
+ return _.each(classes.concat(['base', 'special', 'mystery', 'armoire']), function(klass) {
+ return _.each(gear[type][klass], function(item, i) {
+ var _canOwn, key;
+ key = type + "_" + klass + "_" + i;
+ _.defaults(item, {
+ type: type,
+ key: key,
+ klass: klass,
+ index: i,
+ str: 0,
+ int: 0,
+ per: 0,
+ con: 0,
+ canBuy: (function() {
+ return false;
+ })
+ });
+ if (item.event) {
+ _canOwn = item.canOwn || (function() {
+ return true;
+ });
+ item.canOwn = function(u) {
+ return _canOwn(u) && ((u.items.gear.owned[key] != null) || (moment().isAfter(item.event.start) && moment().isBefore(item.event.end))) && (item.specialClass ? u.stats["class"] === item.specialClass : true);
+ };
+ }
+ if (item.mystery) {
+ item.canOwn = function(u) {
+ return u.items.gear.owned[key] != null;
+ };
+ }
+ return api.gear.flat[key] = item;
+ });
+ });
+});
+
+
+/*
+ Time Traveler Store, mystery sets need their items mapped in
+ */
+
+_.each(api.mystery, function(v, k) {
+ return v.items = _.where(api.gear.flat, {
+ mystery: k
+ });
+});
+
+api.timeTravelerStore = function(owned) {
+ var ownedKeys;
+ ownedKeys = _.keys((typeof owned.toObject === "function" ? owned.toObject() : void 0) || owned);
+ return _.reduce(api.mystery, function(m, v, k) {
+ if (k === 'wondercon' || ~ownedKeys.indexOf(v.items[0].key)) {
+ return m;
+ }
+ m[k] = v;
+ return m;
+ }, {});
+};
+
+
+/*
+ ---------------------------------------------------------------
+ Unique Rewards: Potion and Armoire
+ ---------------------------------------------------------------
+ */
+
+api.potion = {
+ type: 'potion',
+ text: t('potionText'),
+ notes: t('potionNotes'),
+ value: 25,
+ key: 'potion'
+};
+
+api.armoire = {
+ type: 'armoire',
+ text: t('armoireText'),
+ notes: (function(user, count) {
+ if (user.flags.armoireEmpty) {
+ return t('armoireNotesEmpty')();
+ }
+ return t('armoireNotesFull')() + count;
+ }),
+ value: 100,
+ key: 'armoire',
+ canOwn: (function(u) {
+ return _.contains(u.achievements.ultimateGearSets, true);
+ })
+};
+
+
+/*
+ ---------------------------------------------------------------
+ Classes
+ ---------------------------------------------------------------
+ */
+
+api.classes = classes;
+
+
+/*
+ ---------------------------------------------------------------
+ Gear Types
+ ---------------------------------------------------------------
+ */
+
+api.gearTypes = gearTypes;
+
+
+/*
+ ---------------------------------------------------------------
+ Spells
+ ---------------------------------------------------------------
+ Text, notes, and mana are obvious. The rest:
+
+ * {target}: one of [task, self, party, user]. This is very important, because if the cast() function is expecting one
+ thing and receives another, it will cause errors. `self` is used for self buffs, multi-task debuffs, AOEs (eg, meteor-shower),
+ etc. Basically, use self for anything that's not [task, party, user] and is an instant-cast
+
+ * {cast}: the function that's run to perform the ability's action. This is pretty slick - because this is exported to the
+ 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
+ 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){...})`
+
+ Note, user.stats.mp is docked after automatically (it's appended to functions automatically down below in an _.each)
+ */
+
+diminishingReturns = function(bonus, max, halfway) {
+ if (halfway == null) {
+ halfway = max / 2;
+ }
+ return max * (bonus / (bonus + halfway));
+};
+
+calculateBonus = function(value, stat, crit, stat_scale) {
+ if (crit == null) {
+ crit = 1;
+ }
+ if (stat_scale == null) {
+ stat_scale = 0.5;
+ }
+ return (value < 0 ? 1 : value + 1) + (stat * stat_scale * crit);
+};
+
+api.spells = {
+ wizard: {
+ fireball: {
+ text: t('spellWizardFireballText'),
+ mana: 10,
+ lvl: 11,
+ target: 'task',
+ notes: t('spellWizardFireballNotes'),
+ cast: function(user, target) {
+ var base, bonus, req;
+ bonus = user._statsComputed.int * user.fns.crit('per');
+ bonus *= Math.ceil((target.value < 0 ? 1 : target.value + 1) * .075);
+ user.stats.exp += diminishingReturns(bonus, 75);
+ if ((base = user.party.quest.progress).up == null) {
+ base.up = 0;
+ }
+ user.party.quest.progress.up += Math.ceil(user._statsComputed.int * .1);
+ req = {
+ language: user.preferences.language
+ };
+ return user.fns.updateStats(user.stats, req);
+ }
+ },
+ mpheal: {
+ text: t('spellWizardMPHealText'),
+ mana: 30,
+ lvl: 12,
+ target: 'party',
+ notes: t('spellWizardMPHealNotes'),
+ cast: function(user, target) {
+ return _.each(target, function(member) {
+ var bonus;
+ bonus = user._statsComputed.int;
+ if (user._id !== member._id) {
+ return member.stats.mp += Math.ceil(diminishingReturns(bonus, 25, 125));
+ }
+ });
+ }
+ },
+ earth: {
+ text: t('spellWizardEarthText'),
+ mana: 35,
+ lvl: 13,
+ target: 'party',
+ notes: t('spellWizardEarthNotes'),
+ cast: function(user, target) {
+ return _.each(target, function(member) {
+ var base, bonus;
+ bonus = user._statsComputed.int - user.stats.buffs.int;
+ if ((base = member.stats.buffs).int == null) {
+ base.int = 0;
+ }
+ return member.stats.buffs.int += Math.ceil(diminishingReturns(bonus, 30, 200));
+ });
+ }
+ },
+ frost: {
+ text: t('spellWizardFrostText'),
+ mana: 40,
+ lvl: 14,
+ target: 'self',
+ notes: t('spellWizardFrostNotes'),
+ cast: function(user, target) {
+ return user.stats.buffs.streaks = true;
+ }
+ }
+ },
+ warrior: {
+ smash: {
+ text: t('spellWarriorSmashText'),
+ mana: 10,
+ lvl: 11,
+ target: 'task',
+ notes: t('spellWarriorSmashNotes'),
+ cast: function(user, target) {
+ var base, bonus;
+ bonus = user._statsComputed.str * user.fns.crit('con');
+ target.value += diminishingReturns(bonus, 2.5, 35);
+ if ((base = user.party.quest.progress).up == null) {
+ base.up = 0;
+ }
+ return user.party.quest.progress.up += diminishingReturns(bonus, 55, 70);
+ }
+ },
+ defensiveStance: {
+ text: t('spellWarriorDefensiveStanceText'),
+ mana: 25,
+ lvl: 12,
+ target: 'self',
+ notes: t('spellWarriorDefensiveStanceNotes'),
+ cast: function(user, target) {
+ var base, bonus;
+ bonus = user._statsComputed.con - user.stats.buffs.con;
+ if ((base = user.stats.buffs).con == null) {
+ base.con = 0;
+ }
+ return user.stats.buffs.con += Math.ceil(diminishingReturns(bonus, 40, 200));
+ }
+ },
+ valorousPresence: {
+ text: t('spellWarriorValorousPresenceText'),
+ mana: 20,
+ lvl: 13,
+ target: 'party',
+ notes: t('spellWarriorValorousPresenceNotes'),
+ cast: function(user, target) {
+ return _.each(target, function(member) {
+ var base, bonus;
+ bonus = user._statsComputed.str - user.stats.buffs.str;
+ if ((base = member.stats.buffs).str == null) {
+ base.str = 0;
+ }
+ return member.stats.buffs.str += Math.ceil(diminishingReturns(bonus, 20, 200));
+ });
+ }
+ },
+ intimidate: {
+ text: t('spellWarriorIntimidateText'),
+ mana: 15,
+ lvl: 14,
+ target: 'party',
+ notes: t('spellWarriorIntimidateNotes'),
+ cast: function(user, target) {
+ return _.each(target, function(member) {
+ var base, bonus;
+ bonus = user._statsComputed.con - user.stats.buffs.con;
+ if ((base = member.stats.buffs).con == null) {
+ base.con = 0;
+ }
+ return member.stats.buffs.con += Math.ceil(diminishingReturns(bonus, 24, 200));
+ });
+ }
+ }
+ },
+ rogue: {
+ pickPocket: {
+ text: t('spellRoguePickPocketText'),
+ mana: 10,
+ lvl: 11,
+ target: 'task',
+ notes: t('spellRoguePickPocketNotes'),
+ cast: function(user, target) {
+ var bonus;
+ bonus = calculateBonus(target.value, user._statsComputed.per);
+ return user.stats.gp += diminishingReturns(bonus, 25, 75);
+ }
+ },
+ backStab: {
+ text: t('spellRogueBackStabText'),
+ mana: 15,
+ lvl: 12,
+ target: 'task',
+ notes: t('spellRogueBackStabNotes'),
+ cast: function(user, target) {
+ var _crit, bonus, req;
+ _crit = user.fns.crit('str', .3);
+ bonus = calculateBonus(target.value, user._statsComputed.str, _crit);
+ user.stats.exp += diminishingReturns(bonus, 75, 50);
+ user.stats.gp += diminishingReturns(bonus, 18, 75);
+ req = {
+ language: user.preferences.language
+ };
+ return user.fns.updateStats(user.stats, req);
+ }
+ },
+ toolsOfTrade: {
+ text: t('spellRogueToolsOfTradeText'),
+ mana: 25,
+ lvl: 13,
+ target: 'party',
+ notes: t('spellRogueToolsOfTradeNotes'),
+ cast: function(user, target) {
+ return _.each(target, function(member) {
+ var base, bonus;
+ bonus = user._statsComputed.per - user.stats.buffs.per;
+ if ((base = member.stats.buffs).per == null) {
+ base.per = 0;
+ }
+ return member.stats.buffs.per += Math.ceil(diminishingReturns(bonus, 100, 50));
+ });
+ }
+ },
+ stealth: {
+ text: t('spellRogueStealthText'),
+ mana: 45,
+ lvl: 14,
+ target: 'self',
+ notes: t('spellRogueStealthNotes'),
+ cast: function(user, target) {
+ var base;
+ if ((base = user.stats.buffs).stealth == null) {
+ base.stealth = 0;
+ }
+ return user.stats.buffs.stealth += Math.ceil(diminishingReturns(user._statsComputed.per, user.dailys.length * 0.64, 55));
+ }
+ }
+ },
+ healer: {
+ heal: {
+ text: t('spellHealerHealText'),
+ mana: 15,
+ lvl: 11,
+ target: 'self',
+ notes: t('spellHealerHealNotes'),
+ cast: function(user, target) {
+ user.stats.hp += (user._statsComputed.con + user._statsComputed.int + 5) * .075;
+ if (user.stats.hp > 50) {
+ return user.stats.hp = 50;
+ }
+ }
+ },
+ brightness: {
+ text: t('spellHealerBrightnessText'),
+ mana: 15,
+ lvl: 12,
+ target: 'self',
+ notes: t('spellHealerBrightnessNotes'),
+ cast: function(user, target) {
+ return _.each(user.tasks, function(target) {
+ if (target.type === 'reward') {
+ return;
+ }
+ return target.value += 4 * (user._statsComputed.int / (user._statsComputed.int + 40));
+ });
+ }
+ },
+ protectAura: {
+ text: t('spellHealerProtectAuraText'),
+ mana: 30,
+ lvl: 13,
+ target: 'party',
+ notes: t('spellHealerProtectAuraNotes'),
+ cast: function(user, target) {
+ return _.each(target, function(member) {
+ var base, bonus;
+ bonus = user._statsComputed.con - user.stats.buffs.con;
+ if ((base = member.stats.buffs).con == null) {
+ base.con = 0;
+ }
+ return member.stats.buffs.con += Math.ceil(diminishingReturns(bonus, 200, 200));
+ });
+ }
+ },
+ heallAll: {
+ text: t('spellHealerHealAllText'),
+ mana: 25,
+ lvl: 14,
+ target: 'party',
+ notes: t('spellHealerHealAllNotes'),
+ cast: function(user, target) {
+ return _.each(target, function(member) {
+ member.stats.hp += (user._statsComputed.con + user._statsComputed.int + 5) * .04;
+ if (member.stats.hp > 50) {
+ return member.stats.hp = 50;
+ }
+ });
+ }
+ }
+ },
+ special: {
+ snowball: {
+ text: t('spellSpecialSnowballAuraText'),
+ mana: 0,
+ value: 15,
+ target: 'user',
+ notes: t('spellSpecialSnowballAuraNotes'),
+ cast: function(user, target) {
+ var base;
+ target.stats.buffs.snowball = true;
+ target.stats.buffs.spookDust = false;
+ target.stats.buffs.shinySeed = false;
+ target.stats.buffs.seafoam = false;
+ if ((base = target.achievements).snowball == null) {
+ base.snowball = 0;
+ }
+ target.achievements.snowball++;
+ return user.items.special.snowball--;
+ }
+ },
+ salt: {
+ text: t('spellSpecialSaltText'),
+ mana: 0,
+ value: 5,
+ immediateUse: true,
+ target: 'self',
+ notes: t('spellSpecialSaltNotes'),
+ cast: function(user, target) {
+ user.stats.buffs.snowball = false;
+ return user.stats.gp -= 5;
+ }
+ },
+ spookDust: {
+ text: t('spellSpecialSpookDustText'),
+ mana: 0,
+ value: 15,
+ target: 'user',
+ notes: t('spellSpecialSpookDustNotes'),
+ cast: function(user, target) {
+ var base;
+ target.stats.buffs.snowball = false;
+ target.stats.buffs.spookDust = true;
+ target.stats.buffs.shinySeed = false;
+ target.stats.buffs.seafoam = false;
+ if ((base = target.achievements).spookDust == null) {
+ base.spookDust = 0;
+ }
+ target.achievements.spookDust++;
+ return user.items.special.spookDust--;
+ }
+ },
+ opaquePotion: {
+ text: t('spellSpecialOpaquePotionText'),
+ mana: 0,
+ value: 5,
+ immediateUse: true,
+ target: 'self',
+ notes: t('spellSpecialOpaquePotionNotes'),
+ cast: function(user, target) {
+ user.stats.buffs.spookDust = false;
+ return user.stats.gp -= 5;
+ }
+ },
+ shinySeed: {
+ text: t('spellSpecialShinySeedText'),
+ mana: 0,
+ value: 15,
+ target: 'user',
+ notes: t('spellSpecialShinySeedNotes'),
+ cast: function(user, target) {
+ var base;
+ target.stats.buffs.snowball = false;
+ target.stats.buffs.spookDust = false;
+ target.stats.buffs.shinySeed = true;
+ target.stats.buffs.seafoam = false;
+ if ((base = target.achievements).shinySeed == null) {
+ base.shinySeed = 0;
+ }
+ target.achievements.shinySeed++;
+ return user.items.special.shinySeed--;
+ }
+ },
+ petalFreePotion: {
+ text: t('spellSpecialPetalFreePotionText'),
+ mana: 0,
+ value: 5,
+ immediateUse: true,
+ target: 'self',
+ notes: t('spellSpecialPetalFreePotionNotes'),
+ cast: function(user, target) {
+ user.stats.buffs.shinySeed = false;
+ return user.stats.gp -= 5;
+ }
+ },
+ seafoam: {
+ text: t('spellSpecialSeafoamText'),
+ mana: 0,
+ value: 15,
+ target: 'user',
+ notes: t('spellSpecialSeafoamNotes'),
+ cast: function(user, target) {
+ var base;
+ target.stats.buffs.snowball = false;
+ target.stats.buffs.spookDust = false;
+ target.stats.buffs.shinySeed = false;
+ target.stats.buffs.seafoam = true;
+ if ((base = target.achievements).seafoam == null) {
+ base.seafoam = 0;
+ }
+ target.achievements.seafoam++;
+ return user.items.special.seafoam--;
+ }
+ },
+ sand: {
+ text: t('spellSpecialSandText'),
+ mana: 0,
+ value: 5,
+ immediateUse: true,
+ target: 'self',
+ notes: t('spellSpecialSandNotes'),
+ cast: function(user, target) {
+ user.stats.buffs.seafoam = false;
+ return user.stats.gp -= 5;
+ }
+ },
+ nye: {
+ text: t('nyeCard'),
+ mana: 0,
+ value: 10,
+ immediateUse: true,
+ silent: true,
+ target: 'user',
+ notes: t('nyeCardNotes'),
+ cast: function(user, target) {
+ var base;
+ if (user === target) {
+ if ((base = user.achievements).nye == null) {
+ base.nye = 0;
+ }
+ user.achievements.nye++;
+ } else {
+ _.each([user, target], function(t) {
+ var base1;
+ if ((base1 = t.achievements).nye == null) {
+ base1.nye = 0;
+ }
+ return t.achievements.nye++;
+ });
+ }
+ if (!target.items.special.nyeReceived) {
+ target.items.special.nyeReceived = [];
+ }
+ target.items.special.nyeReceived.push(user.profile.name);
+ target.flags.cardReceived = true;
+ if (typeof target.markModified === "function") {
+ target.markModified('items.special.nyeReceived');
+ }
+ return user.stats.gp -= 10;
+ }
+ },
+ valentine: {
+ text: t('valentineCard'),
+ mana: 0,
+ value: 10,
+ immediateUse: true,
+ silent: true,
+ target: 'user',
+ notes: t('valentineCardNotes'),
+ cast: function(user, target) {
+ var base;
+ if (user === target) {
+ if ((base = user.achievements).valentine == null) {
+ base.valentine = 0;
+ }
+ user.achievements.valentine++;
+ } else {
+ _.each([user, target], function(t) {
+ var base1;
+ if ((base1 = t.achievements).valentine == null) {
+ base1.valentine = 0;
+ }
+ return t.achievements.valentine++;
+ });
+ }
+ if (!target.items.special.valentineReceived) {
+ target.items.special.valentineReceived = [];
+ }
+ target.items.special.valentineReceived.push(user.profile.name);
+ target.flags.cardReceived = true;
+ if (typeof target.markModified === "function") {
+ target.markModified('items.special.valentineReceived');
+ }
+ return user.stats.gp -= 10;
+ }
+ },
+ greeting: {
+ text: t('greetingCard'),
+ mana: 0,
+ value: 10,
+ immediateUse: true,
+ silent: true,
+ target: 'user',
+ notes: t('greetingCardNotes'),
+ cast: function(user, target) {
+ var base;
+ if (user === target) {
+ if ((base = user.achievements).greeting == null) {
+ base.greeting = 0;
+ }
+ user.achievements.greeting++;
+ } else {
+ _.each([user, target], function(t) {
+ var base1;
+ if ((base1 = t.achievements).greeting == null) {
+ base1.greeting = 0;
+ }
+ return t.achievements.greeting++;
+ });
+ }
+ if (!target.items.special.greetingReceived) {
+ target.items.special.greetingReceived = [];
+ }
+ target.items.special.greetingReceived.push(user.profile.name);
+ target.flags.cardReceived = true;
+ if (typeof target.markModified === "function") {
+ target.markModified('items.special.greetingReceived');
+ }
+ return user.stats.gp -= 10;
+ }
+ },
+ thankyou: {
+ text: t('thankyouCard'),
+ mana: 0,
+ value: 10,
+ immediateUse: true,
+ silent: true,
+ target: 'user',
+ notes: t('thankyouCardNotes'),
+ cast: function(user, target) {
+ var base;
+ if (user === target) {
+ if ((base = user.achievements).thankyou == null) {
+ base.thankyou = 0;
+ }
+ user.achievements.thankyou++;
+ } else {
+ _.each([user, target], function(t) {
+ var base1;
+ if ((base1 = t.achievements).thankyou == null) {
+ base1.thankyou = 0;
+ }
+ return t.achievements.thankyou++;
+ });
+ }
+ if (!target.items.special.thankyouReceived) {
+ target.items.special.thankyouReceived = [];
+ }
+ target.items.special.thankyouReceived.push(user.profile.name);
+ target.flags.cardReceived = true;
+ if (typeof target.markModified === "function") {
+ target.markModified('items.special.thankyouReceived');
+ }
+ return user.stats.gp -= 10;
+ }
+ }
+ }
+};
+
+api.cardTypes = {
+ greeting: {
+ key: 'greeting',
+ messageOptions: 4,
+ yearRound: true
+ },
+ nye: {
+ key: 'nye',
+ messageOptions: 5
+ },
+ thankyou: {
+ key: 'thankyou',
+ messageOptions: 4,
+ yearRound: true
+ },
+ valentine: {
+ key: 'valentine',
+ messageOptions: 4
+ }
+};
+
+_.each(api.spells, function(spellClass) {
+ return _.each(spellClass, function(spell, key) {
+ var _cast;
+ spell.key = key;
+ _cast = spell.cast;
+ return spell.cast = function(user, target) {
+ _cast(user, target);
+ return user.stats.mp -= spell.mana;
+ };
+ });
+});
+
+api.special = api.spells.special;
+
+
+/*
+ ---------------------------------------------------------------
+ Drops
+ ---------------------------------------------------------------
+ */
+
+api.dropEggs = {
+ Wolf: {
+ text: t('dropEggWolfText'),
+ adjective: t('dropEggWolfAdjective')
+ },
+ TigerCub: {
+ text: t('dropEggTigerCubText'),
+ mountText: t('dropEggTigerCubMountText'),
+ adjective: t('dropEggTigerCubAdjective')
+ },
+ PandaCub: {
+ text: t('dropEggPandaCubText'),
+ mountText: t('dropEggPandaCubMountText'),
+ adjective: t('dropEggPandaCubAdjective')
+ },
+ LionCub: {
+ text: t('dropEggLionCubText'),
+ mountText: t('dropEggLionCubMountText'),
+ adjective: t('dropEggLionCubAdjective')
+ },
+ Fox: {
+ text: t('dropEggFoxText'),
+ adjective: t('dropEggFoxAdjective')
+ },
+ FlyingPig: {
+ text: t('dropEggFlyingPigText'),
+ adjective: t('dropEggFlyingPigAdjective')
+ },
+ Dragon: {
+ text: t('dropEggDragonText'),
+ adjective: t('dropEggDragonAdjective')
+ },
+ Cactus: {
+ text: t('dropEggCactusText'),
+ adjective: t('dropEggCactusAdjective')
+ },
+ BearCub: {
+ text: t('dropEggBearCubText'),
+ mountText: t('dropEggBearCubMountText'),
+ adjective: t('dropEggBearCubAdjective')
+ }
+};
+
+_.each(api.dropEggs, function(egg, key) {
+ return _.defaults(egg, {
+ canBuy: (function() {
+ return true;
+ }),
+ value: 3,
+ key: key,
+ notes: t('eggNotes', {
+ eggText: egg.text,
+ eggAdjective: egg.adjective
+ }),
+ mountText: egg.text
+ });
+});
+
+api.questEggs = {
+ Gryphon: {
+ text: t('questEggGryphonText'),
+ adjective: t('questEggGryphonAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.gryphon != null) > 0;
+ })
+ },
+ Hedgehog: {
+ text: t('questEggHedgehogText'),
+ adjective: t('questEggHedgehogAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.hedgehog != null) > 0;
+ })
+ },
+ Deer: {
+ text: t('questEggDeerText'),
+ adjective: t('questEggDeerAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.ghost_stag != null) > 0;
+ })
+ },
+ Egg: {
+ text: t('questEggEggText'),
+ adjective: t('questEggEggAdjective'),
+ mountText: t('questEggEggMountText')
+ },
+ Rat: {
+ text: t('questEggRatText'),
+ adjective: t('questEggRatAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.rat != null) > 0;
+ })
+ },
+ Octopus: {
+ text: t('questEggOctopusText'),
+ adjective: t('questEggOctopusAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.octopus != null) > 0;
+ })
+ },
+ Seahorse: {
+ text: t('questEggSeahorseText'),
+ adjective: t('questEggSeahorseAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.dilatory_derby != null) > 0;
+ })
+ },
+ Parrot: {
+ text: t('questEggParrotText'),
+ adjective: t('questEggParrotAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.harpy != null) > 0;
+ })
+ },
+ Rooster: {
+ text: t('questEggRoosterText'),
+ adjective: t('questEggRoosterAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.rooster != null) > 0;
+ })
+ },
+ Spider: {
+ text: t('questEggSpiderText'),
+ adjective: t('questEggSpiderAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.spider != null) > 0;
+ })
+ },
+ Owl: {
+ text: t('questEggOwlText'),
+ adjective: t('questEggOwlAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.owl != null) > 0;
+ })
+ },
+ Penguin: {
+ text: t('questEggPenguinText'),
+ adjective: t('questEggPenguinAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.penguin != null) > 0;
+ })
+ },
+ TRex: {
+ text: t('questEggTRexText'),
+ adjective: t('questEggTRexAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && ((u.achievements.quests.trex != null) > 0 || (u.achievements.quests.trex_undead != null) > 0);
+ })
+ },
+ Rock: {
+ text: t('questEggRockText'),
+ adjective: t('questEggRockAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.rock != null) > 0;
+ })
+ },
+ Bunny: {
+ text: t('questEggBunnyText'),
+ adjective: t('questEggBunnyAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.bunny != null) > 0;
+ })
+ },
+ Slime: {
+ text: t('questEggSlimeText'),
+ adjective: t('questEggSlimeAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.slime != null) > 0;
+ })
+ },
+ Sheep: {
+ text: t('questEggSheepText'),
+ adjective: t('questEggSheepAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.sheep != null) > 0;
+ })
+ },
+ Cuttlefish: {
+ text: t('questEggCuttlefishText'),
+ adjective: t('questEggCuttlefishAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.kraken != null) > 0;
+ })
+ },
+ Whale: {
+ text: t('questEggWhaleText'),
+ adjective: t('questEggWhaleAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.whale != null) > 0;
+ })
+ },
+ Cheetah: {
+ text: t('questEggCheetahText'),
+ adjective: t('questEggCheetahAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.cheetah != null) > 0;
+ })
+ },
+ Horse: {
+ text: t('questEggHorseText'),
+ adjective: t('questEggHorseAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.horse != null) > 0;
+ })
+ },
+ Frog: {
+ text: t('questEggFrogText'),
+ adjective: t('questEggFrogAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.frog != null) > 0;
+ })
+ },
+ Snake: {
+ text: t('questEggSnakeText'),
+ adjective: t('questEggSnakeAdjective'),
+ canBuy: (function(u) {
+ return u.achievements.quests && (u.achievements.quests.snake != null) > 0;
+ })
+ }
+};
+
+_.each(api.questEggs, function(egg, key) {
+ return _.defaults(egg, {
+ canBuy: (function() {
+ return false;
+ }),
+ value: 3,
+ key: key,
+ notes: t('eggNotes', {
+ eggText: egg.text,
+ eggAdjective: egg.adjective
+ }),
+ mountText: egg.text
+ });
+});
+
+api.eggs = _.assign(_.cloneDeep(api.dropEggs), api.questEggs);
+
+api.specialPets = {
+ 'Wolf-Veteran': 'veteranWolf',
+ 'Wolf-Cerberus': 'cerberusPup',
+ 'Dragon-Hydra': 'hydra',
+ 'Turkey-Base': 'turkey',
+ 'BearCub-Polar': 'polarBearPup',
+ 'MantisShrimp-Base': 'mantisShrimp',
+ 'JackOLantern-Base': 'jackolantern',
+ 'Mammoth-Base': 'mammoth',
+ 'Tiger-Veteran': 'veteranTiger',
+ 'Phoenix-Base': 'phoenix'
+};
+
+api.specialMounts = {
+ 'BearCub-Polar': 'polarBear',
+ 'LionCub-Ethereal': 'etherealLion',
+ 'MantisShrimp-Base': 'mantisShrimp',
+ 'Turkey-Base': 'turkey',
+ 'Mammoth-Base': 'mammoth',
+ 'Orca-Base': 'orca',
+ 'Gryphon-RoyalPurple': 'royalPurpleGryphon',
+ 'Phoenix-Base': 'phoenix',
+ 'JackOLantern-Base': 'jackolantern'
+};
+
+api.timeTravelStable = {
+ pets: {
+ 'Mammoth-Base': t('mammoth'),
+ 'MantisShrimp-Base': t('mantisShrimp')
+ },
+ mounts: {
+ 'Mammoth-Base': t('mammoth'),
+ 'MantisShrimp-Base': t('mantisShrimp')
+ }
+};
+
+api.dropHatchingPotions = {
+ Base: {
+ value: 2,
+ text: t('hatchingPotionBase')
+ },
+ White: {
+ value: 2,
+ text: t('hatchingPotionWhite')
+ },
+ Desert: {
+ value: 2,
+ text: t('hatchingPotionDesert')
+ },
+ Red: {
+ value: 3,
+ text: t('hatchingPotionRed')
+ },
+ Shade: {
+ value: 3,
+ text: t('hatchingPotionShade')
+ },
+ Skeleton: {
+ value: 3,
+ text: t('hatchingPotionSkeleton')
+ },
+ Zombie: {
+ value: 4,
+ text: t('hatchingPotionZombie')
+ },
+ CottonCandyPink: {
+ value: 4,
+ text: t('hatchingPotionCottonCandyPink')
+ },
+ CottonCandyBlue: {
+ value: 4,
+ text: t('hatchingPotionCottonCandyBlue')
+ },
+ Golden: {
+ value: 5,
+ text: t('hatchingPotionGolden')
+ }
+};
+
+api.premiumHatchingPotions = {
+ Spooky: {
+ value: 2,
+ text: t('hatchingPotionSpooky'),
+ limited: true,
+ canBuy: (function() {
+ return false;
+ })
+ }
+};
+
+_.each(api.dropHatchingPotions, function(pot, key) {
+ return _.defaults(pot, {
+ key: key,
+ value: 2,
+ notes: t('hatchingPotionNotes', {
+ potText: pot.text
+ }),
+ premium: false,
+ limited: false,
+ canBuy: (function() {
+ return true;
+ })
+ });
+});
+
+_.each(api.premiumHatchingPotions, function(pot, key) {
+ return _.defaults(pot, {
+ key: key,
+ value: 2,
+ notes: t('hatchingPotionNotes', {
+ potText: pot.text
+ }),
+ addlNotes: t('premiumPotionAddlNotes'),
+ premium: true,
+ limited: false,
+ canBuy: (function() {
+ return true;
+ })
+ });
+});
+
+api.hatchingPotions = {};
+
+_.merge(api.hatchingPotions, api.dropHatchingPotions);
+
+_.merge(api.hatchingPotions, api.premiumHatchingPotions);
+
+api.pets = _.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.premiumPets = _.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.questPets = _.transform(api.questEggs, function(m, egg) {
+ return _.defaults(m, _.transform(api.hatchingPotions, function(m2, pot) {
+ if (!pot.premium) {
+ return m2[egg.key + "-" + pot.key] = true;
+ }
+ }));
+});
+
+api.mounts = _.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.questMounts = _.transform(api.questEggs, 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'),
+ canBuy: (function() {
+ return true;
+ }),
+ canDrop: true,
+ target: 'Base',
+ article: ''
+ },
+ Milk: {
+ text: t('foodMilk'),
+ canBuy: (function() {
+ return true;
+ }),
+ canDrop: true,
+ target: 'White',
+ article: ''
+ },
+ Potatoe: {
+ text: t('foodPotatoe'),
+ canBuy: (function() {
+ return true;
+ }),
+ canDrop: true,
+ target: 'Desert',
+ article: 'a '
+ },
+ Strawberry: {
+ text: t('foodStrawberry'),
+ canBuy: (function() {
+ return true;
+ }),
+ canDrop: true,
+ target: 'Red',
+ article: 'a '
+ },
+ Chocolate: {
+ text: t('foodChocolate'),
+ canBuy: (function() {
+ return true;
+ }),
+ canDrop: true,
+ target: 'Shade',
+ article: ''
+ },
+ Fish: {
+ text: t('foodFish'),
+ canBuy: (function() {
+ return true;
+ }),
+ canDrop: true,
+ target: 'Skeleton',
+ article: 'a '
+ },
+ RottenMeat: {
+ text: t('foodRottenMeat'),
+ canBuy: (function() {
+ return true;
+ }),
+ canDrop: true,
+ target: 'Zombie',
+ article: ''
+ },
+ CottonCandyPink: {
+ text: t('foodCottonCandyPink'),
+ canBuy: (function() {
+ return true;
+ }),
+ canDrop: true,
+ target: 'CottonCandyPink',
+ article: ''
+ },
+ CottonCandyBlue: {
+ text: t('foodCottonCandyBlue'),
+ canBuy: (function() {
+ return true;
+ }),
+ canDrop: true,
+ target: 'CottonCandyBlue',
+ article: ''
+ },
+ Honey: {
+ text: t('foodHoney'),
+ canBuy: (function() {
+ return true;
+ }),
+ canDrop: true,
+ target: 'Golden',
+ article: ''
+ },
+ Saddle: {
+ canBuy: (function() {
+ return true;
+ }),
+ text: t('foodSaddleText'),
+ value: 5,
+ notes: t('foodSaddleNotes')
+ },
+ Cake_Skeleton: {
+ text: t('foodCakeSkeleton'),
+ target: 'Skeleton',
+ article: ''
+ },
+ Cake_Base: {
+ text: t('foodCakeBase'),
+ target: 'Base',
+ article: ''
+ },
+ Cake_CottonCandyBlue: {
+ text: t('foodCakeCottonCandyBlue'),
+ target: 'CottonCandyBlue',
+ article: ''
+ },
+ Cake_CottonCandyPink: {
+ text: t('foodCakeCottonCandyPink'),
+ target: 'CottonCandyPink',
+ article: ''
+ },
+ Cake_Shade: {
+ text: t('foodCakeShade'),
+ target: 'Shade',
+ article: ''
+ },
+ Cake_White: {
+ text: t('foodCakeWhite'),
+ target: 'White',
+ article: ''
+ },
+ Cake_Golden: {
+ text: t('foodCakeGolden'),
+ target: 'Golden',
+ article: ''
+ },
+ Cake_Zombie: {
+ text: t('foodCakeZombie'),
+ target: 'Zombie',
+ article: ''
+ },
+ Cake_Desert: {
+ text: t('foodCakeDesert'),
+ target: 'Desert',
+ article: ''
+ },
+ Cake_Red: {
+ text: t('foodCakeRed'),
+ target: 'Red',
+ article: ''
+ },
+ Candy_Skeleton: {
+ text: t('foodCandySkeleton'),
+ target: 'Skeleton',
+ article: ''
+ },
+ Candy_Base: {
+ text: t('foodCandyBase'),
+ target: 'Base',
+ article: ''
+ },
+ Candy_CottonCandyBlue: {
+ text: t('foodCandyCottonCandyBlue'),
+ target: 'CottonCandyBlue',
+ article: ''
+ },
+ Candy_CottonCandyPink: {
+ text: t('foodCandyCottonCandyPink'),
+ target: 'CottonCandyPink',
+ article: ''
+ },
+ Candy_Shade: {
+ text: t('foodCandyShade'),
+ target: 'Shade',
+ article: ''
+ },
+ Candy_White: {
+ text: t('foodCandyWhite'),
+ target: 'White',
+ article: ''
+ },
+ Candy_Golden: {
+ text: t('foodCandyGolden'),
+ target: 'Golden',
+ article: ''
+ },
+ Candy_Zombie: {
+ text: t('foodCandyZombie'),
+ target: 'Zombie',
+ article: ''
+ },
+ Candy_Desert: {
+ text: t('foodCandyDesert'),
+ target: 'Desert',
+ article: ''
+ },
+ Candy_Red: {
+ text: t('foodCandyRed'),
+ target: 'Red',
+ article: ''
+ }
+};
+
+_.each(api.food, function(food, key) {
+ return _.defaults(food, {
+ value: 1,
+ key: key,
+ notes: t('foodNotes'),
+ canBuy: (function() {
+ return false;
+ }),
+ canDrop: false
+ });
+});
+
+api.userCanOwnQuestCategories = ['unlockable', 'gold', 'pet'];
+
+api.quests = {
+ dilatory: {
+ text: t("questDilatoryText"),
+ notes: t("questDilatoryNotes"),
+ completion: t("questDilatoryCompletion"),
+ value: 0,
+ canBuy: (function() {
+ return false;
+ }),
+ category: 'world',
+ boss: {
+ name: t("questDilatoryBoss"),
+ hp: 5000000,
+ str: 1,
+ def: 1,
+ rage: {
+ title: t("questDilatoryBossRageTitle"),
+ description: t("questDilatoryBossRageDescription"),
+ value: 4000000,
+ tavern: t('questDilatoryBossRageTavern'),
+ stables: t('questDilatoryBossRageStables'),
+ market: t('questDilatoryBossRageMarket')
+ }
+ },
+ drop: {
+ items: [
+ {
+ type: 'pets',
+ key: 'MantisShrimp-Base',
+ text: t('questDilatoryDropMantisShrimpPet')
+ }, {
+ type: 'mounts',
+ key: 'MantisShrimp-Base',
+ text: t('questDilatoryDropMantisShrimpMount')
+ }, {
+ type: 'food',
+ key: 'Meat',
+ text: t('foodMeat')
+ }, {
+ type: 'food',
+ key: 'Milk',
+ text: t('foodMilk')
+ }, {
+ type: 'food',
+ key: 'Potatoe',
+ text: t('foodPotatoe')
+ }, {
+ type: 'food',
+ key: 'Strawberry',
+ text: t('foodStrawberry')
+ }, {
+ type: 'food',
+ key: 'Chocolate',
+ text: t('foodChocolate')
+ }, {
+ type: 'food',
+ key: 'Fish',
+ text: t('foodFish')
+ }, {
+ type: 'food',
+ key: 'RottenMeat',
+ text: t('foodRottenMeat')
+ }, {
+ type: 'food',
+ key: 'CottonCandyPink',
+ text: t('foodCottonCandyPink')
+ }, {
+ type: 'food',
+ key: 'CottonCandyBlue',
+ text: t('foodCottonCandyBlue')
+ }, {
+ type: 'food',
+ key: 'Honey',
+ text: t('foodHoney')
+ }
+ ],
+ gp: 0,
+ exp: 0
+ }
+ },
+ stressbeast: {
+ text: t("questStressbeastText"),
+ notes: t("questStressbeastNotes"),
+ completion: t("questStressbeastCompletion"),
+ completionChat: t("questStressbeastCompletionChat"),
+ value: 0,
+ canBuy: (function() {
+ return false;
+ }),
+ category: 'world',
+ boss: {
+ name: t("questStressbeastBoss"),
+ hp: 2750000,
+ str: 1,
+ def: 1,
+ rage: {
+ title: t("questStressbeastBossRageTitle"),
+ description: t("questStressbeastBossRageDescription"),
+ value: 1450000,
+ healing: .3,
+ stables: t('questStressbeastBossRageStables'),
+ bailey: t('questStressbeastBossRageBailey'),
+ guide: t('questStressbeastBossRageGuide')
+ },
+ desperation: {
+ threshold: 500000,
+ str: 3.5,
+ def: 2,
+ text: t('questStressbeastDesperation')
+ }
+ },
+ drop: {
+ items: [
+ {
+ type: 'pets',
+ key: 'Mammoth-Base',
+ text: t('questStressbeastDropMammothPet')
+ }, {
+ type: 'mounts',
+ key: 'Mammoth-Base',
+ text: t('questStressbeastDropMammothMount')
+ }, {
+ type: 'food',
+ key: 'Meat',
+ text: t('foodMeat')
+ }, {
+ type: 'food',
+ key: 'Milk',
+ text: t('foodMilk')
+ }, {
+ type: 'food',
+ key: 'Potatoe',
+ text: t('foodPotatoe')
+ }, {
+ type: 'food',
+ key: 'Strawberry',
+ text: t('foodStrawberry')
+ }, {
+ type: 'food',
+ key: 'Chocolate',
+ text: t('foodChocolate')
+ }, {
+ type: 'food',
+ key: 'Fish',
+ text: t('foodFish')
+ }, {
+ type: 'food',
+ key: 'RottenMeat',
+ text: t('foodRottenMeat')
+ }, {
+ type: 'food',
+ key: 'CottonCandyPink',
+ text: t('foodCottonCandyPink')
+ }, {
+ type: 'food',
+ key: 'CottonCandyBlue',
+ text: t('foodCottonCandyBlue')
+ }, {
+ type: 'food',
+ key: 'Honey',
+ text: t('foodHoney')
+ }
+ ],
+ gp: 0,
+ exp: 0
+ }
+ },
+ burnout: {
+ text: t('questBurnoutText'),
+ notes: t('questBurnoutNotes'),
+ completion: t('questBurnoutCompletion'),
+ completionChat: t('questBurnoutCompletionChat'),
+ value: 0,
+ canBuy: (function() {
+ return false;
+ }),
+ category: 'world',
+ boss: {
+ name: t('questBurnoutBoss'),
+ hp: 11000000,
+ str: 2.5,
+ def: 1,
+ rage: {
+ title: t('questBurnoutBossRageTitle'),
+ description: t('questBurnoutBossRageDescription'),
+ value: 1000000,
+ quests: t('questBurnoutBossRageQuests'),
+ seasonalShop: t('questBurnoutBossRageSeasonalShop'),
+ tavern: t('questBurnoutBossRageTavern')
+ }
+ },
+ drop: {
+ items: [
+ {
+ type: 'pets',
+ key: 'Phoenix-Base',
+ text: t('questBurnoutDropPhoenixPet')
+ }, {
+ type: 'mounts',
+ key: 'Phoenix-Base',
+ text: t('questBurnoutDropPhoenixMount')
+ }, {
+ type: 'food',
+ key: 'Candy_Base',
+ text: t('foodCandyBase')
+ }, {
+ type: 'food',
+ key: 'Candy_White',
+ text: t('foodCandyWhite')
+ }, {
+ type: 'food',
+ key: 'Candy_Desert',
+ text: t('foodCandyDesert')
+ }, {
+ type: 'food',
+ key: 'Candy_Red',
+ text: t('foodCandyRed')
+ }, {
+ type: 'food',
+ key: 'Candy_Shade',
+ text: t('foodCandyShade')
+ }, {
+ type: 'food',
+ key: 'Candy_Skeleton',
+ text: t('foodCandySkeleton')
+ }, {
+ type: 'food',
+ key: 'Candy_Zombie',
+ text: t('foodCandyZombie')
+ }, {
+ type: 'food',
+ key: 'Candy_CottonCandyPink',
+ text: t('foodCandyCottonCandyPink')
+ }, {
+ type: 'food',
+ key: 'Candy_CottonCandyBlue',
+ text: t('foodCandyCottonCandyBlue')
+ }, {
+ type: 'food',
+ key: 'Candy_Golden',
+ text: t('foodCandyGolden')
+ }
+ ],
+ gp: 0,
+ exp: 0
+ }
+ },
+ evilsanta: {
+ canBuy: (function() {
+ return false;
+ }),
+ text: t('questEvilSantaText'),
+ notes: t('questEvilSantaNotes'),
+ completion: t('questEvilSantaCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questEvilSantaBoss'),
+ hp: 300,
+ str: 1
+ },
+ drop: {
+ items: [
+ {
+ type: 'mounts',
+ key: 'BearCub-Polar',
+ text: t('questEvilSantaDropBearCubPolarMount')
+ }
+ ],
+ gp: 20,
+ exp: 100
+ }
+ },
+ evilsanta2: {
+ canBuy: (function() {
+ return false;
+ }),
+ text: t('questEvilSanta2Text'),
+ notes: t('questEvilSanta2Notes'),
+ completion: t('questEvilSanta2Completion'),
+ value: 4,
+ previous: 'evilsanta',
+ category: 'pet',
+ collect: {
+ tracks: {
+ text: t('questEvilSanta2CollectTracks'),
+ count: 20
+ },
+ branches: {
+ text: t('questEvilSanta2CollectBranches'),
+ count: 10
+ }
+ },
+ drop: {
+ items: [
+ {
+ type: 'pets',
+ key: 'BearCub-Polar',
+ text: t('questEvilSanta2DropBearCubPolarPet')
+ }
+ ],
+ gp: 20,
+ exp: 100
+ }
+ },
+ gryphon: {
+ text: t('questGryphonText'),
+ notes: t('questGryphonNotes'),
+ completion: t('questGryphonCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questGryphonBoss'),
+ hp: 300,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Gryphon',
+ text: t('questGryphonDropGryphonEgg')
+ }, {
+ type: 'eggs',
+ key: 'Gryphon',
+ text: t('questGryphonDropGryphonEgg')
+ }, {
+ type: 'eggs',
+ key: 'Gryphon',
+ text: t('questGryphonDropGryphonEgg')
+ }
+ ],
+ gp: 25,
+ exp: 125,
+ unlock: t('questGryphonUnlockText')
+ }
+ },
+ hedgehog: {
+ text: t('questHedgehogText'),
+ notes: t('questHedgehogNotes'),
+ completion: t('questHedgehogCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questHedgehogBoss'),
+ hp: 400,
+ str: 1.25
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Hedgehog',
+ text: t('questHedgehogDropHedgehogEgg')
+ }, {
+ type: 'eggs',
+ key: 'Hedgehog',
+ text: t('questHedgehogDropHedgehogEgg')
+ }, {
+ type: 'eggs',
+ key: 'Hedgehog',
+ text: t('questHedgehogDropHedgehogEgg')
+ }
+ ],
+ gp: 30,
+ exp: 125,
+ unlock: t('questHedgehogUnlockText')
+ }
+ },
+ ghost_stag: {
+ text: t('questGhostStagText'),
+ notes: t('questGhostStagNotes'),
+ completion: t('questGhostStagCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questGhostStagBoss'),
+ hp: 1200,
+ str: 2.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Deer',
+ text: t('questGhostStagDropDeerEgg')
+ }, {
+ type: 'eggs',
+ key: 'Deer',
+ text: t('questGhostStagDropDeerEgg')
+ }, {
+ type: 'eggs',
+ key: 'Deer',
+ text: t('questGhostStagDropDeerEgg')
+ }
+ ],
+ gp: 80,
+ exp: 800,
+ unlock: t('questGhostStagUnlockText')
+ }
+ },
+ vice1: {
+ text: t('questVice1Text'),
+ notes: t('questVice1Notes'),
+ value: 4,
+ lvl: 30,
+ category: 'unlockable',
+ boss: {
+ name: t('questVice1Boss'),
+ hp: 750,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'quests',
+ key: "vice2",
+ text: t('questVice1DropVice2Quest')
+ }
+ ],
+ gp: 20,
+ exp: 100
+ }
+ },
+ vice2: {
+ text: t('questVice2Text'),
+ notes: t('questVice2Notes'),
+ value: 4,
+ lvl: 30,
+ category: 'unlockable',
+ previous: 'vice1',
+ collect: {
+ lightCrystal: {
+ text: t('questVice2CollectLightCrystal'),
+ count: 45
+ }
+ },
+ drop: {
+ items: [
+ {
+ type: 'quests',
+ key: 'vice3',
+ text: t('questVice2DropVice3Quest')
+ }
+ ],
+ gp: 20,
+ exp: 75
+ }
+ },
+ vice3: {
+ text: t('questVice3Text'),
+ notes: t('questVice3Notes'),
+ completion: t('questVice3Completion'),
+ previous: 'vice2',
+ value: 4,
+ lvl: 30,
+ category: 'unlockable',
+ boss: {
+ name: t('questVice3Boss'),
+ hp: 1500,
+ str: 3
+ },
+ drop: {
+ items: [
+ {
+ type: 'gear',
+ key: "weapon_special_2",
+ text: t('questVice3DropWeaponSpecial2')
+ }, {
+ type: 'eggs',
+ key: 'Dragon',
+ text: t('questVice3DropDragonEgg')
+ }, {
+ type: 'eggs',
+ key: 'Dragon',
+ text: t('questVice3DropDragonEgg')
+ }, {
+ type: 'hatchingPotions',
+ key: 'Shade',
+ text: t('questVice3DropShadeHatchingPotion')
+ }, {
+ type: 'hatchingPotions',
+ key: 'Shade',
+ text: t('questVice3DropShadeHatchingPotion')
+ }
+ ],
+ gp: 100,
+ exp: 1000
+ }
+ },
+ egg: {
+ text: t('questEggHuntText'),
+ notes: t('questEggHuntNotes'),
+ completion: t('questEggHuntCompletion'),
+ value: 1,
+ canBuy: (function() {
+ return false;
+ }),
+ category: 'pet',
+ collect: {
+ plainEgg: {
+ text: t('questEggHuntCollectPlainEgg'),
+ count: 100
+ }
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Egg',
+ text: t('questEggHuntDropPlainEgg')
+ }, {
+ type: 'eggs',
+ key: 'Egg',
+ text: t('questEggHuntDropPlainEgg')
+ }, {
+ type: 'eggs',
+ key: 'Egg',
+ text: t('questEggHuntDropPlainEgg')
+ }, {
+ type: 'eggs',
+ key: 'Egg',
+ text: t('questEggHuntDropPlainEgg')
+ }, {
+ type: 'eggs',
+ key: 'Egg',
+ text: t('questEggHuntDropPlainEgg')
+ }, {
+ type: 'eggs',
+ key: 'Egg',
+ text: t('questEggHuntDropPlainEgg')
+ }, {
+ type: 'eggs',
+ key: 'Egg',
+ text: t('questEggHuntDropPlainEgg')
+ }, {
+ type: 'eggs',
+ key: 'Egg',
+ text: t('questEggHuntDropPlainEgg')
+ }, {
+ type: 'eggs',
+ key: 'Egg',
+ text: t('questEggHuntDropPlainEgg')
+ }, {
+ type: 'eggs',
+ key: 'Egg',
+ text: t('questEggHuntDropPlainEgg')
+ }
+ ],
+ gp: 0,
+ exp: 0
+ }
+ },
+ rat: {
+ text: t('questRatText'),
+ notes: t('questRatNotes'),
+ completion: t('questRatCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questRatBoss'),
+ hp: 1200,
+ str: 2.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Rat',
+ text: t('questRatDropRatEgg')
+ }, {
+ type: 'eggs',
+ key: 'Rat',
+ text: t('questRatDropRatEgg')
+ }, {
+ type: 'eggs',
+ key: 'Rat',
+ text: t('questRatDropRatEgg')
+ }
+ ],
+ gp: 80,
+ exp: 800,
+ unlock: t('questRatUnlockText')
+ }
+ },
+ octopus: {
+ text: t('questOctopusText'),
+ notes: t('questOctopusNotes'),
+ completion: t('questOctopusCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questOctopusBoss'),
+ hp: 1200,
+ str: 2.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Octopus',
+ text: t('questOctopusDropOctopusEgg')
+ }, {
+ type: 'eggs',
+ key: 'Octopus',
+ text: t('questOctopusDropOctopusEgg')
+ }, {
+ type: 'eggs',
+ key: 'Octopus',
+ text: t('questOctopusDropOctopusEgg')
+ }
+ ],
+ gp: 80,
+ exp: 800,
+ unlock: t('questOctopusUnlockText')
+ }
+ },
+ dilatory_derby: {
+ text: t('questSeahorseText'),
+ notes: t('questSeahorseNotes'),
+ completion: t('questSeahorseCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questSeahorseBoss'),
+ hp: 300,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Seahorse',
+ text: t('questSeahorseDropSeahorseEgg')
+ }, {
+ type: 'eggs',
+ key: 'Seahorse',
+ text: t('questSeahorseDropSeahorseEgg')
+ }, {
+ type: 'eggs',
+ key: 'Seahorse',
+ text: t('questSeahorseDropSeahorseEgg')
+ }
+ ],
+ gp: 25,
+ exp: 125,
+ unlock: t('questSeahorseUnlockText')
+ }
+ },
+ atom1: {
+ text: t('questAtom1Text'),
+ notes: t('questAtom1Notes'),
+ value: 4,
+ lvl: 15,
+ category: 'unlockable',
+ collect: {
+ soapBars: {
+ text: t('questAtom1CollectSoapBars'),
+ count: 20
+ }
+ },
+ drop: {
+ items: [
+ {
+ type: 'quests',
+ key: "atom2",
+ text: t('questAtom1Drop')
+ }
+ ],
+ gp: 7,
+ exp: 50
+ }
+ },
+ atom2: {
+ text: t('questAtom2Text'),
+ notes: t('questAtom2Notes'),
+ previous: 'atom1',
+ value: 4,
+ lvl: 15,
+ category: 'unlockable',
+ boss: {
+ name: t('questAtom2Boss'),
+ hp: 300,
+ str: 1
+ },
+ drop: {
+ items: [
+ {
+ type: 'quests',
+ key: "atom3",
+ text: t('questAtom2Drop')
+ }
+ ],
+ gp: 20,
+ exp: 100
+ }
+ },
+ atom3: {
+ text: t('questAtom3Text'),
+ notes: t('questAtom3Notes'),
+ previous: 'atom2',
+ completion: t('questAtom3Completion'),
+ value: 4,
+ lvl: 15,
+ category: 'unlockable',
+ boss: {
+ name: t('questAtom3Boss'),
+ hp: 800,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'gear',
+ key: "head_special_2",
+ text: t('headSpecial2Text')
+ }, {
+ type: 'hatchingPotions',
+ key: "Base",
+ text: t('questAtom3DropPotion')
+ }, {
+ type: 'hatchingPotions',
+ key: "Base",
+ text: t('questAtom3DropPotion')
+ }
+ ],
+ gp: 25,
+ exp: 125
+ }
+ },
+ harpy: {
+ text: t('questHarpyText'),
+ notes: t('questHarpyNotes'),
+ completion: t('questHarpyCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questHarpyBoss'),
+ hp: 600,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Parrot',
+ text: t('questHarpyDropParrotEgg')
+ }, {
+ type: 'eggs',
+ key: 'Parrot',
+ text: t('questHarpyDropParrotEgg')
+ }, {
+ type: 'eggs',
+ key: 'Parrot',
+ text: t('questHarpyDropParrotEgg')
+ }
+ ],
+ gp: 43,
+ exp: 350,
+ unlock: t('questHarpyUnlockText')
+ }
+ },
+ rooster: {
+ text: t('questRoosterText'),
+ notes: t('questRoosterNotes'),
+ completion: t('questRoosterCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questRoosterBoss'),
+ hp: 300,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Rooster',
+ text: t('questRoosterDropRoosterEgg')
+ }, {
+ type: 'eggs',
+ key: 'Rooster',
+ text: t('questRoosterDropRoosterEgg')
+ }, {
+ type: 'eggs',
+ key: 'Rooster',
+ text: t('questRoosterDropRoosterEgg')
+ }
+ ],
+ gp: 25,
+ exp: 125,
+ unlock: t('questRoosterUnlockText')
+ }
+ },
+ spider: {
+ text: t('questSpiderText'),
+ notes: t('questSpiderNotes'),
+ completion: t('questSpiderCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questSpiderBoss'),
+ hp: 400,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Spider',
+ text: t('questSpiderDropSpiderEgg')
+ }, {
+ type: 'eggs',
+ key: 'Spider',
+ text: t('questSpiderDropSpiderEgg')
+ }, {
+ type: 'eggs',
+ key: 'Spider',
+ text: t('questSpiderDropSpiderEgg')
+ }
+ ],
+ gp: 31,
+ exp: 200,
+ unlock: t('questSpiderUnlockText')
+ }
+ },
+ moonstone1: {
+ text: t('questMoonstone1Text'),
+ notes: t('questMoonstone1Notes'),
+ value: 4,
+ lvl: 60,
+ category: 'unlockable',
+ collect: {
+ moonstone: {
+ text: t('questMoonstone1CollectMoonstone'),
+ count: 500
+ }
+ },
+ drop: {
+ items: [
+ {
+ type: 'quests',
+ key: "moonstone2",
+ text: t('questMoonstone1DropMoonstone2Quest')
+ }
+ ],
+ gp: 50,
+ exp: 100
+ }
+ },
+ moonstone2: {
+ text: t('questMoonstone2Text'),
+ notes: t('questMoonstone2Notes'),
+ value: 4,
+ lvl: 60,
+ previous: 'moonstone1',
+ category: 'unlockable',
+ boss: {
+ name: t('questMoonstone2Boss'),
+ hp: 1500,
+ str: 3
+ },
+ drop: {
+ items: [
+ {
+ type: 'quests',
+ key: 'moonstone3',
+ text: t('questMoonstone2DropMoonstone3Quest')
+ }
+ ],
+ gp: 500,
+ exp: 1000
+ }
+ },
+ moonstone3: {
+ text: t('questMoonstone3Text'),
+ notes: t('questMoonstone3Notes'),
+ completion: t('questMoonstone3Completion'),
+ previous: 'moonstone2',
+ value: 4,
+ lvl: 60,
+ category: 'unlockable',
+ boss: {
+ name: t('questMoonstone3Boss'),
+ hp: 2000,
+ str: 3.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'gear',
+ key: "armor_special_2",
+ text: t('armorSpecial2Text')
+ }, {
+ type: 'food',
+ key: 'RottenMeat',
+ text: t('questMoonstone3DropRottenMeat')
+ }, {
+ type: 'food',
+ key: 'RottenMeat',
+ text: t('questMoonstone3DropRottenMeat')
+ }, {
+ type: 'food',
+ key: 'RottenMeat',
+ text: t('questMoonstone3DropRottenMeat')
+ }, {
+ type: 'food',
+ key: 'RottenMeat',
+ text: t('questMoonstone3DropRottenMeat')
+ }, {
+ type: 'food',
+ key: 'RottenMeat',
+ text: t('questMoonstone3DropRottenMeat')
+ }, {
+ type: 'hatchingPotions',
+ key: 'Zombie',
+ text: t('questMoonstone3DropZombiePotion')
+ }, {
+ type: 'hatchingPotions',
+ key: 'Zombie',
+ text: t('questMoonstone3DropZombiePotion')
+ }, {
+ type: 'hatchingPotions',
+ key: 'Zombie',
+ text: t('questMoonstone3DropZombiePotion')
+ }
+ ],
+ gp: 900,
+ exp: 1500
+ }
+ },
+ goldenknight1: {
+ text: t('questGoldenknight1Text'),
+ notes: t('questGoldenknight1Notes'),
+ value: 4,
+ lvl: 40,
+ category: 'unlockable',
+ collect: {
+ testimony: {
+ text: t('questGoldenknight1CollectTestimony'),
+ count: 300
+ }
+ },
+ drop: {
+ items: [
+ {
+ type: 'quests',
+ key: "goldenknight2",
+ text: t('questGoldenknight1DropGoldenknight2Quest')
+ }
+ ],
+ gp: 15,
+ exp: 120
+ }
+ },
+ goldenknight2: {
+ text: t('questGoldenknight2Text'),
+ notes: t('questGoldenknight2Notes'),
+ value: 4,
+ previous: 'goldenknight1',
+ lvl: 40,
+ category: 'unlockable',
+ boss: {
+ name: t('questGoldenknight2Boss'),
+ hp: 1000,
+ str: 3
+ },
+ drop: {
+ items: [
+ {
+ type: 'quests',
+ key: 'goldenknight3',
+ text: t('questGoldenknight2DropGoldenknight3Quest')
+ }
+ ],
+ gp: 75,
+ exp: 750
+ }
+ },
+ goldenknight3: {
+ text: t('questGoldenknight3Text'),
+ notes: t('questGoldenknight3Notes'),
+ completion: t('questGoldenknight3Completion'),
+ previous: 'goldenknight2',
+ value: 4,
+ lvl: 40,
+ category: 'unlockable',
+ boss: {
+ name: t('questGoldenknight3Boss'),
+ hp: 1700,
+ str: 3.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'food',
+ key: 'Honey',
+ text: t('questGoldenknight3DropHoney')
+ }, {
+ type: 'food',
+ key: 'Honey',
+ text: t('questGoldenknight3DropHoney')
+ }, {
+ type: 'food',
+ key: 'Honey',
+ text: t('questGoldenknight3DropHoney')
+ }, {
+ type: 'hatchingPotions',
+ key: 'Golden',
+ text: t('questGoldenknight3DropGoldenPotion')
+ }, {
+ type: 'hatchingPotions',
+ key: 'Golden',
+ text: t('questGoldenknight3DropGoldenPotion')
+ }, {
+ type: 'gear',
+ key: 'shield_special_goldenknight',
+ text: t('questGoldenknight3DropWeapon')
+ }
+ ],
+ gp: 900,
+ exp: 1500
+ }
+ },
+ basilist: {
+ text: t('questBasilistText'),
+ notes: t('questBasilistNotes'),
+ completion: t('questBasilistCompletion'),
+ value: 4,
+ category: 'unlockable',
+ unlockCondition: {
+ condition: 'party invite',
+ text: t('inviteFriends')
+ },
+ boss: {
+ name: t('questBasilistBoss'),
+ hp: 100,
+ str: 0.5
+ },
+ drop: {
+ gp: 8,
+ exp: 42
+ }
+ },
+ owl: {
+ text: t('questOwlText'),
+ notes: t('questOwlNotes'),
+ completion: t('questOwlCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questOwlBoss'),
+ hp: 500,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Owl',
+ text: t('questOwlDropOwlEgg')
+ }, {
+ type: 'eggs',
+ key: 'Owl',
+ text: t('questOwlDropOwlEgg')
+ }, {
+ type: 'eggs',
+ key: 'Owl',
+ text: t('questOwlDropOwlEgg')
+ }
+ ],
+ gp: 37,
+ exp: 275,
+ unlock: t('questOwlUnlockText')
+ }
+ },
+ penguin: {
+ text: t('questPenguinText'),
+ notes: t('questPenguinNotes'),
+ completion: t('questPenguinCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questPenguinBoss'),
+ hp: 400,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Penguin',
+ text: t('questPenguinDropPenguinEgg')
+ }, {
+ type: 'eggs',
+ key: 'Penguin',
+ text: t('questPenguinDropPenguinEgg')
+ }, {
+ type: 'eggs',
+ key: 'Penguin',
+ text: t('questPenguinDropPenguinEgg')
+ }
+ ],
+ gp: 31,
+ exp: 200,
+ unlock: t('questPenguinUnlockText')
+ }
+ },
+ trex: {
+ text: t('questTRexText'),
+ notes: t('questTRexNotes'),
+ completion: t('questTRexCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questTRexBoss'),
+ hp: 800,
+ str: 2
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'TRex',
+ text: t('questTRexDropTRexEgg')
+ }, {
+ type: 'eggs',
+ key: 'TRex',
+ text: t('questTRexDropTRexEgg')
+ }, {
+ type: 'eggs',
+ key: 'TRex',
+ text: t('questTRexDropTRexEgg')
+ }
+ ],
+ gp: 55,
+ exp: 500,
+ unlock: t('questTRexUnlockText')
+ }
+ },
+ trex_undead: {
+ text: t('questTRexUndeadText'),
+ notes: t('questTRexUndeadNotes'),
+ completion: t('questTRexUndeadCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questTRexUndeadBoss'),
+ hp: 500,
+ str: 2,
+ rage: {
+ title: t("questTRexUndeadRageTitle"),
+ description: t("questTRexUndeadRageDescription"),
+ value: 50,
+ healing: .3,
+ effect: t('questTRexUndeadRageEffect')
+ }
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'TRex',
+ text: t('questTRexDropTRexEgg')
+ }, {
+ type: 'eggs',
+ key: 'TRex',
+ text: t('questTRexDropTRexEgg')
+ }, {
+ type: 'eggs',
+ key: 'TRex',
+ text: t('questTRexDropTRexEgg')
+ }
+ ],
+ gp: 55,
+ exp: 500,
+ unlock: t('questTRexUnlockText')
+ }
+ },
+ rock: {
+ text: t('questRockText'),
+ notes: t('questRockNotes'),
+ completion: t('questRockCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questRockBoss'),
+ hp: 400,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Rock',
+ text: t('questRockDropRockEgg')
+ }, {
+ type: 'eggs',
+ key: 'Rock',
+ text: t('questRockDropRockEgg')
+ }, {
+ type: 'eggs',
+ key: 'Rock',
+ text: t('questRockDropRockEgg')
+ }
+ ],
+ gp: 31,
+ exp: 200,
+ unlock: t('questRockUnlockText')
+ }
+ },
+ bunny: {
+ text: t('questBunnyText'),
+ notes: t('questBunnyNotes'),
+ completion: t('questBunnyCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questBunnyBoss'),
+ hp: 300,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Bunny',
+ text: t('questBunnyDropBunnyEgg')
+ }, {
+ type: 'eggs',
+ key: 'Bunny',
+ text: t('questBunnyDropBunnyEgg')
+ }, {
+ type: 'eggs',
+ key: 'Bunny',
+ text: t('questBunnyDropBunnyEgg')
+ }
+ ],
+ gp: 25,
+ exp: 125,
+ unlock: t('questBunnyUnlockText')
+ }
+ },
+ slime: {
+ text: t('questSlimeText'),
+ notes: t('questSlimeNotes'),
+ completion: t('questSlimeCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questSlimeBoss'),
+ hp: 400,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Slime',
+ text: t('questSlimeDropSlimeEgg')
+ }, {
+ type: 'eggs',
+ key: 'Slime',
+ text: t('questSlimeDropSlimeEgg')
+ }, {
+ type: 'eggs',
+ key: 'Slime',
+ text: t('questSlimeDropSlimeEgg')
+ }
+ ],
+ gp: 31,
+ exp: 200,
+ unlock: t('questSlimeUnlockText')
+ }
+ },
+ sheep: {
+ text: t('questSheepText'),
+ notes: t('questSheepNotes'),
+ completion: t('questSheepCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questSheepBoss'),
+ hp: 300,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Sheep',
+ text: t('questSheepDropSheepEgg')
+ }, {
+ type: 'eggs',
+ key: 'Sheep',
+ text: t('questSheepDropSheepEgg')
+ }, {
+ type: 'eggs',
+ key: 'Sheep',
+ text: t('questSheepDropSheepEgg')
+ }
+ ],
+ gp: 25,
+ exp: 125,
+ unlock: t('questSheepUnlockText')
+ }
+ },
+ kraken: {
+ text: t('questKrakenText'),
+ notes: t('questKrakenNotes'),
+ completion: t('questKrakenCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questKrakenBoss'),
+ hp: 800,
+ str: 2
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Cuttlefish',
+ text: t('questKrakenDropCuttlefishEgg')
+ }, {
+ type: 'eggs',
+ key: 'Cuttlefish',
+ text: t('questKrakenDropCuttlefishEgg')
+ }, {
+ type: 'eggs',
+ key: 'Cuttlefish',
+ text: t('questKrakenDropCuttlefishEgg')
+ }
+ ],
+ gp: 55,
+ exp: 500,
+ unlock: t('questKrakenUnlockText')
+ }
+ },
+ whale: {
+ text: t('questWhaleText'),
+ notes: t('questWhaleNotes'),
+ completion: t('questWhaleCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questWhaleBoss'),
+ hp: 500,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Whale',
+ text: t('questWhaleDropWhaleEgg')
+ }, {
+ type: 'eggs',
+ key: 'Whale',
+ text: t('questWhaleDropWhaleEgg')
+ }, {
+ type: 'eggs',
+ key: 'Whale',
+ text: t('questWhaleDropWhaleEgg')
+ }
+ ],
+ gp: 37,
+ exp: 275,
+ unlock: t('questWhaleUnlockText')
+ }
+ },
+ dilatoryDistress1: {
+ text: t('questDilatoryDistress1Text'),
+ notes: t('questDilatoryDistress1Notes'),
+ completion: t('questDilatoryDistress1Completion'),
+ value: 4,
+ goldValue: 200,
+ category: 'gold',
+ collect: {
+ fireCoral: {
+ text: t('questDilatoryDistress1CollectFireCoral'),
+ count: 25
+ },
+ blueFins: {
+ text: t('questDilatoryDistress1CollectBlueFins'),
+ count: 25
+ }
+ },
+ drop: {
+ items: [
+ {
+ type: 'gear',
+ key: "armor_special_finnedOceanicArmor",
+ text: t('questDilatoryDistress1DropArmor')
+ }
+ ],
+ gp: 0,
+ exp: 75
+ }
+ },
+ dilatoryDistress2: {
+ text: t('questDilatoryDistress2Text'),
+ notes: t('questDilatoryDistress2Notes'),
+ completion: t('questDilatoryDistress2Completion'),
+ previous: 'dilatoryDistress1',
+ value: 4,
+ goldValue: 300,
+ category: 'gold',
+ boss: {
+ name: t('questDilatoryDistress2Boss'),
+ hp: 500,
+ rage: {
+ title: t("questDilatoryDistress2RageTitle"),
+ description: t("questDilatoryDistress2RageDescription"),
+ value: 50,
+ healing: .3,
+ effect: t('questDilatoryDistress2RageEffect')
+ }
+ },
+ drop: {
+ items: [
+ {
+ type: 'hatchingPotions',
+ key: 'Skeleton',
+ text: t('questDilatoryDistress2DropSkeletonPotion')
+ }, {
+ type: 'hatchingPotions',
+ key: 'CottonCandyBlue',
+ text: t('questDilatoryDistress2DropCottonCandyBluePotion')
+ }, {
+ type: 'gear',
+ key: "head_special_fireCoralCirclet",
+ text: t('questDilatoryDistress2DropHeadgear')
+ }
+ ],
+ gp: 0,
+ exp: 500
+ }
+ },
+ dilatoryDistress3: {
+ text: t('questDilatoryDistress3Text'),
+ notes: t('questDilatoryDistress3Notes'),
+ completion: t('questDilatoryDistress3Completion'),
+ previous: 'dilatoryDistress2',
+ value: 4,
+ goldValue: 400,
+ category: 'gold',
+ boss: {
+ name: t('questDilatoryDistress3Boss'),
+ hp: 1000,
+ str: 2
+ },
+ drop: {
+ items: [
+ {
+ type: 'food',
+ key: 'Fish',
+ text: t('questDilatoryDistress3DropFish')
+ }, {
+ type: 'food',
+ key: 'Fish',
+ text: t('questDilatoryDistress3DropFish')
+ }, {
+ type: 'food',
+ key: 'Fish',
+ text: t('questDilatoryDistress3DropFish')
+ }, {
+ type: 'gear',
+ key: "weapon_special_tridentOfCrashingTides",
+ text: t('questDilatoryDistress3DropWeapon')
+ }, {
+ type: 'gear',
+ key: "shield_special_moonpearlShield",
+ text: t('questDilatoryDistress3DropShield')
+ }
+ ],
+ gp: 0,
+ exp: 650
+ }
+ },
+ cheetah: {
+ text: t('questCheetahText'),
+ notes: t('questCheetahNotes'),
+ completion: t('questCheetahCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questCheetahBoss'),
+ hp: 600,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Cheetah',
+ text: t('questCheetahDropCheetahEgg')
+ }, {
+ type: 'eggs',
+ key: 'Cheetah',
+ text: t('questCheetahDropCheetahEgg')
+ }, {
+ type: 'eggs',
+ key: 'Cheetah',
+ text: t('questCheetahDropCheetahEgg')
+ }
+ ],
+ gp: 43,
+ exp: 350,
+ unlock: t('questCheetahUnlockText')
+ }
+ },
+ horse: {
+ text: t('questHorseText'),
+ notes: t('questHorseNotes'),
+ completion: t('questHorseCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questHorseBoss'),
+ hp: 500,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Horse',
+ text: t('questHorseDropHorseEgg')
+ }, {
+ type: 'eggs',
+ key: 'Horse',
+ text: t('questHorseDropHorseEgg')
+ }, {
+ type: 'eggs',
+ key: 'Horse',
+ text: t('questHorseDropHorseEgg')
+ }
+ ],
+ gp: 37,
+ exp: 275,
+ unlock: t('questHorseUnlockText')
+ }
+ },
+ frog: {
+ text: t('questFrogText'),
+ notes: t('questFrogNotes'),
+ completion: t('questFrogCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questFrogBoss'),
+ hp: 300,
+ str: 1.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Frog',
+ text: t('questFrogDropFrogEgg')
+ }, {
+ type: 'eggs',
+ key: 'Frog',
+ text: t('questFrogDropFrogEgg')
+ }, {
+ type: 'eggs',
+ key: 'Frog',
+ text: t('questFrogDropFrogEgg')
+ }
+ ],
+ gp: 25,
+ exp: 125,
+ unlock: t('questFrogUnlockText')
+ }
+ },
+ snake: {
+ text: t('questSnakeText'),
+ notes: t('questSnakeNotes'),
+ completion: t('questSnakeCompletion'),
+ value: 4,
+ category: 'pet',
+ boss: {
+ name: t('questSnakeBoss'),
+ hp: 1100,
+ str: 2.5
+ },
+ drop: {
+ items: [
+ {
+ type: 'eggs',
+ key: 'Snake',
+ text: t('questSnakeDropSnakeEgg')
+ }, {
+ type: 'eggs',
+ key: 'Snake',
+ text: t('questSnakeDropSnakeEgg')
+ }, {
+ type: 'eggs',
+ key: 'Snake',
+ text: t('questSnakeDropSnakeEgg')
+ }
+ ],
+ gp: 73,
+ exp: 725,
+ unlock: t('questSnakeUnlockText')
+ }
+ }
+};
+
+_.each(api.quests, function(v, key) {
+ var b;
+ _.defaults(v, {
+ key: key,
+ canBuy: (function() {
+ return true;
+ })
+ });
+ b = v.boss;
+ if (b) {
+ _.defaults(b, {
+ str: 1,
+ def: 1
+ });
+ if (b.rage) {
+ return _.defaults(b.rage, {
+ title: t('bossRageTitle'),
+ description: t('bossRageDescription')
+ });
+ }
+ }
+});
+
+api.questsByLevel = _.sortBy(api.quests, function(quest) {
+ return quest.lvl || 0;
+});
+
+api.backgrounds = {
+ backgrounds062014: {
+ beach: {
+ text: t('backgroundBeachText'),
+ notes: t('backgroundBeachNotes')
+ },
+ fairy_ring: {
+ text: t('backgroundFairyRingText'),
+ notes: t('backgroundFairyRingNotes')
+ },
+ forest: {
+ text: t('backgroundForestText'),
+ notes: t('backgroundForestNotes')
+ }
+ },
+ backgrounds072014: {
+ open_waters: {
+ text: t('backgroundOpenWatersText'),
+ notes: t('backgroundOpenWatersNotes')
+ },
+ coral_reef: {
+ text: t('backgroundCoralReefText'),
+ notes: t('backgroundCoralReefNotes')
+ },
+ seafarer_ship: {
+ text: t('backgroundSeafarerShipText'),
+ notes: t('backgroundSeafarerShipNotes')
+ }
+ },
+ backgrounds082014: {
+ volcano: {
+ text: t('backgroundVolcanoText'),
+ notes: t('backgroundVolcanoNotes')
+ },
+ clouds: {
+ text: t('backgroundCloudsText'),
+ notes: t('backgroundCloudsNotes')
+ },
+ dusty_canyons: {
+ text: t('backgroundDustyCanyonsText'),
+ notes: t('backgroundDustyCanyonsNotes')
+ }
+ },
+ backgrounds092014: {
+ thunderstorm: {
+ text: t('backgroundThunderstormText'),
+ notes: t('backgroundThunderstormNotes')
+ },
+ autumn_forest: {
+ text: t('backgroundAutumnForestText'),
+ notes: t('backgroundAutumnForestNotes')
+ },
+ harvest_fields: {
+ text: t('backgroundHarvestFieldsText'),
+ notes: t('backgroundHarvestFieldsNotes')
+ }
+ },
+ backgrounds102014: {
+ graveyard: {
+ text: t('backgroundGraveyardText'),
+ notes: t('backgroundGraveyardNotes')
+ },
+ haunted_house: {
+ text: t('backgroundHauntedHouseText'),
+ notes: t('backgroundHauntedHouseNotes')
+ },
+ pumpkin_patch: {
+ text: t('backgroundPumpkinPatchText'),
+ notes: t('backgroundPumpkinPatchNotes')
+ }
+ },
+ backgrounds112014: {
+ harvest_feast: {
+ text: t('backgroundHarvestFeastText'),
+ notes: t('backgroundHarvestFeastNotes')
+ },
+ sunset_meadow: {
+ text: t('backgroundSunsetMeadowText'),
+ notes: t('backgroundSunsetMeadowNotes')
+ },
+ starry_skies: {
+ text: t('backgroundStarrySkiesText'),
+ notes: t('backgroundStarrySkiesNotes')
+ }
+ },
+ backgrounds122014: {
+ iceberg: {
+ text: t('backgroundIcebergText'),
+ notes: t('backgroundIcebergNotes')
+ },
+ twinkly_lights: {
+ text: t('backgroundTwinklyLightsText'),
+ notes: t('backgroundTwinklyLightsNotes')
+ },
+ south_pole: {
+ text: t('backgroundSouthPoleText'),
+ notes: t('backgroundSouthPoleNotes')
+ }
+ },
+ backgrounds012015: {
+ ice_cave: {
+ text: t('backgroundIceCaveText'),
+ notes: t('backgroundIceCaveNotes')
+ },
+ frigid_peak: {
+ text: t('backgroundFrigidPeakText'),
+ notes: t('backgroundFrigidPeakNotes')
+ },
+ snowy_pines: {
+ text: t('backgroundSnowyPinesText'),
+ notes: t('backgroundSnowyPinesNotes')
+ }
+ },
+ backgrounds022015: {
+ blacksmithy: {
+ text: t('backgroundBlacksmithyText'),
+ notes: t('backgroundBlacksmithyNotes')
+ },
+ crystal_cave: {
+ text: t('backgroundCrystalCaveText'),
+ notes: t('backgroundCrystalCaveNotes')
+ },
+ distant_castle: {
+ text: t('backgroundDistantCastleText'),
+ notes: t('backgroundDistantCastleNotes')
+ }
+ },
+ backgrounds032015: {
+ spring_rain: {
+ text: t('backgroundSpringRainText'),
+ notes: t('backgroundSpringRainNotes')
+ },
+ stained_glass: {
+ text: t('backgroundStainedGlassText'),
+ notes: t('backgroundStainedGlassNotes')
+ },
+ rolling_hills: {
+ text: t('backgroundRollingHillsText'),
+ notes: t('backgroundRollingHillsNotes')
+ }
+ },
+ backgrounds042015: {
+ cherry_trees: {
+ text: t('backgroundCherryTreesText'),
+ notes: t('backgroundCherryTreesNotes')
+ },
+ floral_meadow: {
+ text: t('backgroundFloralMeadowText'),
+ notes: t('backgroundFloralMeadowNotes')
+ },
+ gumdrop_land: {
+ text: t('backgroundGumdropLandText'),
+ notes: t('backgroundGumdropLandNotes')
+ }
+ },
+ backgrounds052015: {
+ marble_temple: {
+ text: t('backgroundMarbleTempleText'),
+ notes: t('backgroundMarbleTempleNotes')
+ },
+ mountain_lake: {
+ text: t('backgroundMountainLakeText'),
+ notes: t('backgroundMountainLakeNotes')
+ },
+ pagodas: {
+ text: t('backgroundPagodasText'),
+ notes: t('backgroundPagodasNotes')
+ }
+ },
+ backgrounds062015: {
+ drifting_raft: {
+ text: t('backgroundDriftingRaftText'),
+ notes: t('backgroundDriftingRaftNotes')
+ },
+ shimmery_bubbles: {
+ text: t('backgroundShimmeryBubblesText'),
+ notes: t('backgroundShimmeryBubblesNotes')
+ },
+ island_waterfalls: {
+ text: t('backgroundIslandWaterfallsText'),
+ notes: t('backgroundIslandWaterfallsNotes')
+ }
+ },
+ backgrounds072015: {
+ dilatory_ruins: {
+ text: t('backgroundDilatoryRuinsText'),
+ notes: t('backgroundDilatoryRuinsNotes')
+ },
+ giant_wave: {
+ text: t('backgroundGiantWaveText'),
+ notes: t('backgroundGiantWaveNotes')
+ },
+ sunken_ship: {
+ text: t('backgroundSunkenShipText'),
+ notes: t('backgroundSunkenShipNotes')
+ }
+ },
+ backgrounds082015: {
+ pyramids: {
+ text: t('backgroundPyramidsText'),
+ notes: t('backgroundPyramidsNotes')
+ },
+ sunset_savannah: {
+ text: t('backgroundSunsetSavannahText'),
+ notes: t('backgroundSunsetSavannahNotes')
+ },
+ twinkly_party_lights: {
+ text: t('backgroundTwinklyPartyLightsText'),
+ notes: t('backgroundTwinklyPartyLightsNotes')
+ }
+ },
+ backgrounds092015: {
+ market: {
+ text: t('backgroundMarketText'),
+ notes: t('backgroundMarketNotes')
+ },
+ stable: {
+ text: t('backgroundStableText'),
+ notes: t('backgroundStableNotes')
+ },
+ tavern: {
+ text: t('backgroundTavernText'),
+ notes: t('backgroundTavernNotes')
+ }
+ },
+ backgrounds102015: {
+ harvest_moon: {
+ text: t('backgroundHarvestMoonText'),
+ notes: t('backgroundHarvestMoonNotes')
+ },
+ slimy_swamp: {
+ text: t('backgroundSlimySwampText'),
+ notes: t('backgroundSlimySwampNotes')
+ },
+ swarming_darkness: {
+ text: t('backgroundSwarmingDarknessText'),
+ notes: t('backgroundSwarmingDarknessNotes')
+ }
+ },
+ backgrounds112015: {
+ floating_islands: {
+ text: t('backgroundFloatingIslandsText'),
+ notes: t('backgroundFloatingIslandsNotes')
+ },
+ night_dunes: {
+ text: t('backgroundNightDunesText'),
+ notes: t('backgroundNightDunesNotes')
+ },
+ sunset_oasis: {
+ text: t('backgroundSunsetOasisText'),
+ notes: t('backgroundSunsetOasisNotes')
+ }
+ }
+};
+
+api.subscriptionBlocks = {
+ basic_earned: {
+ months: 1,
+ price: 5
+ },
+ basic_3mo: {
+ months: 3,
+ price: 15
+ },
+ basic_6mo: {
+ months: 6,
+ price: 30
+ },
+ google_6mo: {
+ months: 6,
+ price: 24,
+ discount: true,
+ original: 30
+ },
+ basic_12mo: {
+ months: 12,
+ price: 48
+ }
+};
+
+_.each(api.subscriptionBlocks, function(b, k) {
+ return b.key = k;
+});
+
+api.userDefaults = {
+ habits: [
+ {
+ type: 'habit',
+ text: t('defaultHabit1Text'),
+ value: 0,
+ up: true,
+ down: false,
+ attribute: 'per'
+ }, {
+ type: 'habit',
+ text: t('defaultHabit2Text'),
+ value: 0,
+ up: false,
+ down: true,
+ attribute: 'str'
+ }, {
+ type: 'habit',
+ text: t('defaultHabit3Text'),
+ value: 0,
+ up: true,
+ down: true,
+ attribute: 'str'
+ }
+ ],
+ dailys: [],
+ todos: [
+ {
+ type: 'todo',
+ text: t('defaultTodo1Text'),
+ notes: t('defaultTodoNotes'),
+ completed: false,
+ attribute: 'int'
+ }
+ ],
+ rewards: [
+ {
+ type: 'reward',
+ text: t('defaultReward1Text'),
+ value: 10
+ }
+ ],
+ tags: [
+ {
+ name: t('defaultTag1')
+ }, {
+ name: t('defaultTag2')
+ }, {
+ name: t('defaultTag3')
+ }
+ ]
+};
+
+api.faq = require('./faq.js');
\ No newline at end of file
diff --git a/common/script/content/translation.js b/common/script/content/translation.js
index e0e26450c5..da478c2546 100644
--- a/common/script/content/translation.js
+++ b/common/script/content/translation.js
@@ -1,7 +1,6 @@
'use strict';
-require('coffee-script');
-var i18n = require('../i18n.coffee');
+var i18n = require('../i18n');
var t = function(string, vars) {
var func = function(lang) {
diff --git a/common/script/count.js b/common/script/count.js
index de2bbf1227..77a2974c8a 100644
--- a/common/script/count.js
+++ b/common/script/count.js
@@ -1,8 +1,7 @@
'use strict';
-require('coffee-script');
var _ = require('lodash');
-var content = require('./content/index.coffee');
+var content = require('./content/index');
var DROP_ANIMALS = _.keys(content.pets);
diff --git a/common/script/i18n.coffee b/common/script/i18n.coffee
deleted file mode 100644
index edc3ae02c8..0000000000
--- a/common/script/i18n.coffee
+++ /dev/null
@@ -1,44 +0,0 @@
-_ = require 'lodash'
-
-module.exports =
- strings: null, # Strings for one single language
- translations: {} # Strings for multiple languages {en: strings, de: strings, ...}
- t: (stringName) -> # Other parameters allowed are vars (Object) and locale (String)
- vars = arguments[1]
-
- if _.isString(arguments[1])
- vars = null
- locale = arguments[1]
- else if arguments[2]?
- vars = arguments[1]
- locale = arguments[2]
-
- locale = 'en' if (!locale? or (!module.exports.strings and !module.exports.translations[locale]))
-
- if module.exports.strings
- string = module.exports.strings[stringName]
- else
- string =
- module.exports.translations[locale] and
- module.exports.translations[locale][stringName]
-
- clonedVars = _.clone(vars) or {}
- clonedVars.locale = locale
-
- if string
- try
- _.template(string, (clonedVars))
- catch e
- 'Error processing the string. Please see Help > Report a Bug.'
- else
- if module.exports.strings
- stringNotFound = module.exports.strings.stringNotFound
- else if module.exports.translations[locale]
- stringNotFound =
- module.exports.translations[locale] and
- module.exports.translations[locale].stringNotFound
-
- try
- _.template(stringNotFound, {string: stringName})
- catch e
- 'Error processing the string. Please see Help > Report a Bug.'
diff --git a/common/script/i18n.js b/common/script/i18n.js
new file mode 100644
index 0000000000..59e5e358da
--- /dev/null
+++ b/common/script/i18n.js
@@ -0,0 +1,51 @@
+var _;
+
+_ = require('lodash');
+
+module.exports = {
+ strings: null,
+ translations: {},
+ t: function(stringName) {
+ var clonedVars, e, locale, string, stringNotFound, vars;
+ vars = arguments[1];
+ if (_.isString(arguments[1])) {
+ vars = null;
+ locale = arguments[1];
+ } else if (arguments[2] != null) {
+ vars = arguments[1];
+ locale = arguments[2];
+ }
+ if ((locale == null) || (!module.exports.strings && !module.exports.translations[locale])) {
+ locale = 'en';
+ }
+ if (module.exports.strings) {
+ string = module.exports.strings[stringName];
+ } else {
+ string = module.exports.translations[locale] && module.exports.translations[locale][stringName];
+ }
+ clonedVars = _.clone(vars) || {};
+ clonedVars.locale = locale;
+ if (string) {
+ try {
+ return _.template(string, clonedVars);
+ } catch (_error) {
+ e = _error;
+ return 'Error processing the string. Please see Help > Report a Bug.';
+ }
+ } else {
+ if (module.exports.strings) {
+ stringNotFound = module.exports.strings.stringNotFound;
+ } else if (module.exports.translations[locale]) {
+ stringNotFound = module.exports.translations[locale] && module.exports.translations[locale].stringNotFound;
+ }
+ try {
+ return _.template(stringNotFound, {
+ string: stringName
+ });
+ } catch (_error) {
+ e = _error;
+ return 'Error processing the string. Please see Help > Report a Bug.';
+ }
+ }
+ }
+};
diff --git a/common/script/index.coffee b/common/script/index.coffee
deleted file mode 100644
index a69f64fcaf..0000000000
--- a/common/script/index.coffee
+++ /dev/null
@@ -1,1936 +0,0 @@
-moment = require('moment')
-_ = require('lodash')
-content = require('./content/index.coffee')
-i18n = require('./i18n.coffee')
-api = module.exports = {}
-
-api.i18n = i18n
-
-# little helper for large arrays of strings. %w"this that another" equivalent from Ruby, I really miss that function
-$w = api.$w = (s)->s.split(' ')
-
-api.dotSet = (obj,path,val)->
- arr = path.split('.')
- _.reduce arr, (curr, next, index) =>
- if (arr.length - 1) == index
- curr[next] = val
- (curr[next] ?= {})
- , obj
-api.dotGet = (obj,path)->
- _.reduce path.split('.'), ((curr, next) => curr?[next]), obj
-
-###
- Reflists are arrays, but stored as objects. Mongoose has a helluvatime working with arrays (the main problem for our
- syncing issues) - so the goal is to move away from arrays to objects, since mongoose can reference elements by ID
- no problem. To maintain sorting, we use these helper functions:
-###
-api.refPush = (reflist, item, prune=0) ->
- item.sort = if _.isEmpty(reflist) then 0 else _.max(reflist,'sort').sort+1
- item.id = api.uuid() unless item.id and !reflist[item.id]
- reflist[item.id] = item
-
-api.planGemLimits =
- convRate: 20 #how much does a gem cost?
- convCap: 25 #how many gems can be converted / month?
-
-###
- ------------------------------------------------------
- Time / Day
- ------------------------------------------------------
-###
-
-###
- Each time we're performing date math (cron, task-due-days, etc), we need to take user preferences into consideration.
- Specifically {dayStart} (custom day start) and {timezoneOffset}. This function sanitizes / defaults those values.
- {now} is also passed in for various purposes, one example being the test scripts scripts testing different "now" times
-###
-sanitizeOptions = (o) ->
- dayStart = if (!_.isNaN(+o.dayStart) and 0 <= +o.dayStart <= 24) then +o.dayStart else 0
- timezoneOffset = if o.timezoneOffset then +(o.timezoneOffset) else +moment().zone()
- now = if o.now then moment(o.now).zone(timezoneOffset) else moment(+new Date).zone(timezoneOffset)
- # return a new object, we don't want to add "now" to user object
- {dayStart, timezoneOffset, now}
-
-api.startOfWeek = api.startOfWeek = (options={}) ->
- o = sanitizeOptions(options)
- moment(o.now).startOf('week')
-
-api.startOfDay = (options={}) ->
- # This is designed for use with any date that has an important time portion (e.g., when comparing the current date-time with the previous cron's date-time for determing if cron should run now).
- # It changes the time portion of the date-time to be the Custom Day Start hour, so that the date-time is now the user's correct start of day.
- # It SUBTRACTS a day if the date-time's original hour is before CDS (e.g., if your CDS is 5am and it's currently 4am, it's still the previous day).
- # This is NOT suitable for manipulating any dates that are displayed to the user as a date with no time portion, such as a Daily's Start Dates (e.g., a Start Date of today shows only the date, so it should be considered to be today even if the hidden time portion is before CDS).
- o = sanitizeOptions(options)
- dayStart = moment(o.now).startOf('day').add({hours:o.dayStart})
- if moment(o.now).hour() < o.dayStart
- dayStart.subtract({days:1})
- dayStart
-
-api.dayMapping = {0:'su',1:'m',2:'t',3:'w',4:'th',5:'f',6:'s'}
-
-###
- Absolute diff from "yesterday" till now
-###
-api.daysSince = (yesterday, options = {}) ->
- o = sanitizeOptions options
- api.startOfDay(_.defaults {now:o.now}, o).diff(api.startOfDay(_.defaults {now:yesterday}, o), 'days')
-
-###
- Should the user do this task on this date, given the task's repeat options and user.preferences.dayStart?
-###
-api.shouldDo = (day, dailyTask, options = {}) ->
- return false unless dailyTask.type == 'daily'
- o = sanitizeOptions options
- startOfDayWithCDSTime = api.startOfDay(_.defaults {now:day}, o) # a moment()
-
- taskStartDate = moment(dailyTask.startDate).zone(o.timezoneOffset)
-
- # The time portion of the Start Date is never visible to or modifiable by the user so we must ignore it.
- # Therefore, we must also ignore the time portion of the user's day start (startOfDayWithCDSTime), otherwise the date comparison will be wrong for some times.
- # NB: The user's day start date has already been converted to the PREVIOUS day's date if the time portion was before CDS.
- taskStartDate = moment(taskStartDate).startOf('day')
-
- if taskStartDate > startOfDayWithCDSTime.startOf('day')
- return false # Daily starts in the future
-
- if dailyTask.frequency == 'daily' # "Every X Days"
- if !dailyTask.everyX
- return false # error condition
- daysSinceTaskStart = startOfDayWithCDSTime.startOf('day').diff(taskStartDate, 'days')
- everyXCheck = (daysSinceTaskStart % dailyTask.everyX == 0)
- return everyXCheck
- else if dailyTask.frequency == 'weekly' # "On Certain Days of the Week"
- if !dailyTask.repeat
- return false # error condition
- dayOfWeekNum = startOfDayWithCDSTime.day() # e.g. 0 for Sunday
- dayOfWeekCheck = dailyTask.repeat[api.dayMapping[dayOfWeekNum]]
- return dayOfWeekCheck
- else
- # unexpected frequency string
- return false
-
-###
- ------------------------------------------------------
- Level cap
- ------------------------------------------------------
-###
-
-api.maxLevel = 100
-
-api.capByLevel = (lvl) ->
- if lvl > api.maxLevel then api.maxLevel else lvl
-
-###
- ------------------------------------------------------
- Health cap
- ------------------------------------------------------
-###
-
-api.maxHealth = 50
-
-###
- ------------------------------------------------------
- Scoring
- ------------------------------------------------------
-###
-
-api.tnl = (lvl) ->
- Math.round(((Math.pow(lvl, 2) * 0.25) + (10 * lvl) + 139.75) / 10) * 10
- # round to nearest 10?
-
-###
- A hyperbola function that creates diminishing returns, so you can't go to infinite (eg, with Exp gain).
- {max} The asymptote
- {bonus} All the numbers combined for your point bonus (eg, task.value * user.stats.int * critChance, etc)
- {halfway} (optional) the point at which the graph starts bending
-###
-api.diminishingReturns = (bonus, max, halfway=max/2) ->
- max*(bonus/(bonus+halfway))
-
-api.monod = (bonus, rateOfIncrease, max) ->
- rateOfIncrease*max*bonus/(rateOfIncrease*bonus+max)
-
-###
-Preen history for users with > 7 history entries
-This takes an infinite array of single day entries [day day day day day...], and turns it into a condensed array
-of averages, condensing more the further back in time we go. Eg, 7 entries each for last 7 days; 1 entry each week
-of this month; 1 entry for each month of this year; 1 entry per previous year: [day*7 week*4 month*12 year*infinite]
-###
-preenHistory = (history) ->
- history = _.filter(history, (h) -> !!h) # discard nulls (corrupted somehow)
- newHistory = []
- preen = (amount, groupBy) ->
- groups = _.chain(history)
- .groupBy((h) -> moment(h.date).format groupBy) # get date groupings to average against
- .sortBy((h, k) -> k) # sort by date
- .value() # turn into an array
- groups = groups.slice(-amount)
- groups.pop() # get rid of "this week", "this month", etc (except for case of days)
- _.each groups, (group) ->
- newHistory.push
- date: moment(group[0].date).toDate()
- #date: moment(group[0].date).format('MM/DD/YYYY') # Use this one when testing
- value: _.reduce(group, ((m, obj) -> m + obj.value), 0) / group.length # average
- true
-
- # Keep the last:
- preen 50, "YYYY" # 50 years (habit will toootally be around that long!)
- preen moment().format('MM'), "YYYYMM" # last MM months (eg, if today is 05, keep the last 5 months)
-
- # Then keep all days of this month. Note, the extra logic is to account for Habits, which can be counted multiple times per day
- # FIXME I'd rather keep 1-entry/week of this month, then last 'd' days in this week. However, I'm having issues where the 1st starts mid week
- thisMonth = moment().format('YYYYMM')
- newHistory = newHistory.concat _.filter(history, (h)-> moment(h.date).format('YYYYMM') is thisMonth)
- #preen Math.ceil(moment().format('D')/7), "YYYYww" # last __ weeks (# weeks so far this month)
- #newHistory = newHistory.concat(history.slice -moment().format('D')) # each day of this week
-
- newHistory
-
-
-###
- Preen 3-day past-completed To-Dos from Angular & mobile app
-###
-api.preenTodos = (tasks) ->
- _.where(tasks, (t) -> !t.completed || (t.challenge && t.challenge.id) || moment(t.dateCompleted).isAfter(moment().subtract({days:3})))
-
-###
- Update the in-browser store with new gear. FIXME this was in user.fns, but it was causing strange issues there
-###
-sortOrder = _.reduce content.gearTypes,((m,v,k)->m[v]=k;m), {}
-api.updateStore = (user) ->
- return unless user
- changes= []
- _.each content.gearTypes, (type) ->
- found = _.find content.gear.tree[type][user.stats.class], (item) ->
- !user.items.gear.owned[item.key]
- changes.push(found) if found
- true
- # Add special items (contrib gear, backer gear, etc)
- changes = changes.concat _.filter content.gear.flat, (v) ->
- v.klass in ['special','mystery','armoire'] and !user.items.gear.owned[v.key] and v.canOwn?(user)
- # Return sorted store (array)
- _.sortBy changes, (c)->sortOrder[c.type]
-
-###
-------------------------------------------------------
-Content
-------------------------------------------------------
-###
-
-api.content = content
-
-
-###
-------------------------------------------------------
-Misc Helpers
-------------------------------------------------------
-###
-
-api.uuid = ->
- "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace /[xy]/g, (c) ->
- r = Math.random() * 16 | 0
- v = (if c is "x" then r else (r & 0x3 | 0x8))
- v.toString 16
-
-api.countExists = (items)-> _.reduce(items,((m,v)->m+(if v then 1 else 0)),0)
-
-###
-Even though Mongoose handles task defaults, we want to make sure defaults are set on the client-side before
-sending up to the server for performance
-###
-api.taskDefaults = (task={}) ->
- task.type = 'habit' unless task.type and task.type in ['habit','daily','todo','reward']
- defaults =
- id: api.uuid()
- text: if task.id? then task.id else ''
- notes: ''
- priority: 1
- challenge: {}
- attribute: 'str'
- dateCreated: new Date()
- _.defaults task, defaults
- _.defaults(task, {up:true,down:true}) if task.type is 'habit'
- _.defaults(task, {history: []}) if task.type in ['habit', 'daily']
- _.defaults(task, {completed:false}) if task.type in ['daily', 'todo']
- _.defaults(task, {streak:0, repeat: {su:true,m:true,t:true,w:true,th:true,f:true,s:true}}, startDate: new Date(), everyX: 1, frequency: 'weekly') if task.type is 'daily'
- task._id = task.id # may need this for TaskSchema if we go back to using it, see http://goo.gl/a5irq4
- task.value ?= if task.type is 'reward' then 10 else 0
- task.priority = 1 unless _.isNumber(task.priority) # hotfix for apiv1. once we're off apiv1, we can remove this
- task
-
-api.percent = (x,y, dir) ->
- switch dir
- when "up" then roundFn = Math.ceil
- when "down" then roundFn = Math.floor
- else roundFn = Math.round
- x=1 if x==0
- Math.max(0,roundFn(x/y*100))
-
-###
-Remove whitespace #FIXME are we using this anywwhere? Should we be?
-###
-api.removeWhitespace = (str) ->
- return '' unless str
- str.replace /\s/g, ''
-
-###
-Encode the download link for .ics iCal file
-###
-api.encodeiCalLink = (uid, apiToken) ->
- loc = window?.location.host or process?.env?.BASE_URL or ''
- encodeURIComponent "http://#{loc}/v1/users/#{uid}/calendar.ics?apiToken=#{apiToken}"
-
-###
-Gold amount from their money
-###
-api.gold = (num) ->
- if num
- return Math.floor num
- else
- return "0"
-
-###
-Silver amount from their money
-###
-api.silver = (num) ->
- if num
- ("0" + Math.floor (num - Math.floor(num))*100).slice -2
- else
- return "00"
-
-###
-Task classes given everything about the class
-###
-api.taskClasses = (task, filters=[], dayStart=0, lastCron=+new Date, showCompleted=false, main=false) ->
- return unless task
- {type, completed, value, repeat, priority} = task
-
- # Filters
- if main # only show when on your own list
- if !task._editing # always show task being edited (even when tag removed)
- for filter, enabled of filters
- if enabled and not task.tags?[filter]
- # All the other classes don't matter
- return 'hidden'
-
- classes = type
-
- if task._editing
- classes += " beingEdited" # Assists filtering by third-party themes.
- # Example: theme normally hides daily when not scheduled for today,
- # BUT keeps showing daily when being edited to unschedule it for today
-
- # show as completed if completed (naturally) or not required for today
- if type in ['todo', 'daily']
- if completed or (type is 'daily' and !api.shouldDo(+new Date, task, {dayStart}))
- classes += " completed"
- else
- classes += " uncompleted"
- else if type is 'habit'
- classes += ' habit-wide' if task.down and task.up
- classes += ' habit-narrow' if !task.down and !task.up
-
- if priority == 0.1
- classes += ' difficulty-trivial'
- else if priority == 1
- classes += ' difficulty-easy'
- else if priority == 1.5
- classes += ' difficulty-medium'
- else if priority == 2
- classes += ' difficulty-hard'
-
- if value < -20
- classes += ' color-worst'
- else if value < -10
- classes += ' color-worse'
- else if value < -1
- classes += ' color-bad'
- else if value < 1
- classes += ' color-neutral'
- else if value < 5
- classes += ' color-good'
- else if value < 10
- classes += ' color-better'
- else
- classes += ' color-best'
- return classes
-
-###
-Friendly timestamp
-###
-api.friendlyTimestamp = (timestamp) -> moment(timestamp).format('MM/DD h:mm:ss a')
-
-###
-Does user have new chat messages?
-###
-api.newChatMessages = (messages, lastMessageSeen) ->
- return false unless messages?.length > 0
- messages?[0] and (messages[0].id != lastMessageSeen)
-
-###
-are any tags active?
-###
-api.noTags = (tags) -> _.isEmpty(tags) or _.isEmpty(_.filter(tags, (t)->t))
-
-###
-Are there tags applied?
-###
-api.appliedTags = (userTags, taskTags) ->
- arr = []
- _.each userTags, (t) ->
- return unless t?
- arr.push(t.name) if taskTags?[t.id]
- arr.join(', ')
-
-###
-Various counting functions
-###
-api.count = require('./count')
-
-###
-------------------------------------------------------
-User (prototype wrapper to give it ops, helper funcs, and virtuals
-------------------------------------------------------
-###
-
-###
-User is now wrapped (both on client and server), adding a few new properties:
- * getters (_statsComputed, tasks, etc)
- * user.fns, which is a bunch of helper functions
- These were originally up above, but they make more sense belonging to the user object so we don't have to pass
- the user object all over the place. In fact, we should pull in more functions such as cron(), updateStats(), etc.
- * user.ops, which is super important:
-
-If a function is inside user.ops, it has magical properties. If you call it on the client it updates the user object in
-the browser and when it's done it automatically POSTs to the server, calling src/controllers/user.js#OP_NAME (the exact same name
-of the op function). The first argument req is {query, body, params}, it's what the express controller function
-expects. This means we call our functions as if we were calling an Express route. Eg, instead of score(task, direction),
-we call score({params:{id:task.id,direction:direction}}). This also forces us to think about our routes (whether to use
-params, query, or body for variables). see http://stackoverflow.com/questions/4024271/rest-api-best-practices-where-to-put-parameters
-
-If `src/controllers/user.js#OP_NAME` doesn't exist on the server, it's automatically added. It runs the code in user.ops.OP_NAME
-to update the user model server-side, then performs `user.save()`. You can see this in action for `user.ops.buy`. That
-function doesn't exist on the server - so the client calls it, it updates user in the browser, auto-POSTs to server, server
-handles it by calling `user.ops.buy` again (to update user on the server), and then saves. We can do this for
-everything that doesn't need any code difference from what's in user.ops.OP_NAME for special-handling server-side. If we
-*do* need special handling, just add `src/controllers/user.js#OP_NAME` to override the user.ops.OP_NAME, and be
-sure to call user.ops.OP_NAME at some point within the overridden function.
-
-TODO
- * Is this the best way to wrap the user object? I thought of using user.prototype, but user is an object not a Function.
- user on the server is a Mongoose model, so we can use prototype - but to do it on the client, we'd probably have to
- move to $resource for user
- * Move to $resource!
-###
-api.wrap = (user, main=true) ->
- return if user._wrapped
- user._wrapped = true
-
- # ----------------------------------------------------------------------
- # user.ops shared client/server operations
- # ----------------------------------------------------------------------
-
- if main
- user.ops =
-
- # ------
- # User
- # ------
-
- update: (req, cb) ->
- _.each req.body, (v,k) ->
- user.fns.dotSet(k,v);true
- cb? null, user
-
- sleep: (req, cb) ->
- user.preferences.sleep = !user.preferences.sleep
- cb? null, {}
-
- revive: (req, cb, analytics) ->
- return cb?({code:400, message: "Cannot revive if not dead"}) unless user.stats.hp <= 0
-
- # Reset stats after death
- _.merge user.stats, {hp:50, exp:0, gp:0}
- user.stats.lvl-- if user.stats.lvl > 1
-
- # Lose a stat point
- lostStat = user.fns.randomVal _.reduce(['str','con','per','int'], ((m,k)->m[k]=k if user.stats[k];m), {})
- user.stats[lostStat]-- if lostStat
-
- # Lose a gear piece
- # Free items (value:0) cannot be lost to avoid "pay to win". Subscribers have more free (Mystery) items and so would have a higher chance of losing a free one. The only exception is that the weapon_warrior_0 free item can be lost so that a new player who dies before buying any gear does experience equipment loss.
- # Note ""+k string-casting. Without this, when run on the server Mongoose returns funny objects
- cl = user.stats.class
- gearOwned = user.items.gear.owned.toObject?() or user.items.gear.owned
- losableItems = {}
- _.each gearOwned, (v,k) ->
- if v
- itm = content.gear.flat[''+k]
- if itm
- if (itm.value > 0 || k == 'weapon_warrior_0') && ( itm.klass == cl || ( itm.klass == 'special' && (! itm.specialClass || itm.specialClass == cl) ) || itm.klass == 'armoire' )
- losableItems[''+k]=''+k
- lostItem = user.fns.randomVal losableItems
- if item = content.gear.flat[lostItem]
- user.items.gear.owned[lostItem] = false
- user.items.gear.equipped[item.type] = "#{item.type}_base_0" if user.items.gear.equipped[item.type] is lostItem
- user.items.gear.costume[item.type] = "#{item.type}_base_0" if user.items.gear.costume[item.type] is lostItem
- user.markModified? 'items.gear'
-
- analyticsData = {
- uuid: user._id,
- lostItem: lostItem,
- gaLabel: lostItem,
- category: 'behavior'
- }
- analytics?.track('Death', analyticsData)
-
- cb? (if item then {code:200,message: i18n.t('messageLostItem', {itemText: item.text(req.language)}, req.language)} else null), user
-
- reset: (req, cb) ->
- user.habits = []
- user.dailys = []
- user.todos = []
- user.rewards = []
- user.stats.hp = 50
- user.stats.lvl = 1
- user.stats.gp = 0
- user.stats.exp = 0
- # TODO handle MP
- gear = user.items.gear
- _.each ['equipped', 'costume'], (type) ->
- gear[type].armor = 'armor_base_0'
- gear[type].weapon = 'weapon_base_0'
- gear[type].head = 'head_base_0'
- gear[type].shield = 'shield_base_0'
- gear.owned = {} if typeof gear.owned == 'undefined'
- _.each gear.owned, (v, k)-> gear.owned[k]=false if gear.owned[k];true
- gear.owned.weapon_warrior_0 = true
- user.markModified? 'items.gear.owned'
- user.preferences.costume = false
- cb? null, user
-
- reroll: (req, cb, analytics) ->
- if user.balance < 1
- return cb? {code:401,message: i18n.t('notEnoughGems', req.language)}
- user.balance--
- _.each user.tasks, (task) ->
- unless task.type is 'reward'
- task.value = 0
- user.stats.hp = 50
-
- analyticsData = {
- uuid: user._id,
- acquireMethod: 'Gems',
- gemCost: 4,
- category: 'behavior'
- }
- analytics?.track('Fortify Potion', analyticsData)
-
- cb? null, user
-
- rebirth: (req, cb, analytics) ->
- # Cost is 8 Gems ($2)
- if (user.balance < 2 && user.stats.lvl < api.maxLevel)
- return cb? {code:401,message: i18n.t('notEnoughGems', req.language)}
-
- analyticsData = {
- uuid: user._id,
- category: 'behavior'
- }
- # only charge people if they are under the max level - ryan
- if user.stats.lvl < api.maxLevel
- user.balance -= 2
- analyticsData.acquireMethod = 'Gems'
- analyticsData.gemCost = 8
- else
- analyticsData.gemCost = 0
- analyticsData.acquireMethod = '> 100'
-
- analytics?.track('Rebirth', analyticsData)
-
- # Save off user's level, for calculating achievement eligibility later
- lvl = api.capByLevel(user.stats.lvl)
- # Turn tasks yellow, zero out streaks
- _.each user.tasks, (task) ->
- unless task.type is 'reward'
- task.value = 0
- if task.type is 'daily'
- task.streak = 0
- # Reset all dynamic stats
- stats = user.stats
- stats.buffs = {}
- stats.hp = 50
- stats.lvl = 1
- stats.class = 'warrior'
- _.each ['per','int','con','str','points','gp','exp','mp'], (value) ->
- stats[value] = 0
- # Deequip character, set back to base armor and training sword
- gear = user.items.gear
- _.each ['equipped', 'costume'], (type) ->
- gear[type] = {}; # deequip weapon, eyewear, headAccessory, etc, plus future new types
- gear[type].armor = 'armor_base_0'
- gear[type].weapon = 'weapon_warrior_0'
- gear[type].head = 'head_base_0'
- gear[type].shield = 'shield_base_0'
- if user.items.currentPet then user.ops.equip({params:{type: 'pet', key: user.items.currentPet}})
- if user.items.currentMount then user.ops.equip({params:{type: 'mount', key: user.items.currentMount}})
- # Strip owned gear down to the training sword and free items (zero gold value), but preserve purchase history so user can re-purchase limited edition equipment
- _.each gear.owned, (v, k) -> if gear.owned[k] and content.gear.flat[k].value then gear.owned[k] = false; true
- gear.owned.weapon_warrior_0 = true
- user.markModified? 'items.gear.owned'
- user.preferences.costume = false
- # Remove unlocked features
- flags = user.flags
- if not user.achievements.beastMaster
- flags.rebirthEnabled = false
- flags.itemsEnabled = false
- flags.dropsEnabled = false
- flags.classSelected = false
- flags.levelDrops = {}
- # Award an achievement if this is their first Rebirth, or if they made it further than last time
- if not (user.achievements.rebirths)
- user.achievements.rebirths = 1
- user.achievements.rebirthLevel = lvl
- else if (lvl > user.achievements.rebirthLevel or lvl is 100)
- user.achievements.rebirths++
- user.achievements.rebirthLevel = lvl
- user.stats.buffs = {}
- # user.markModified? 'stats'
- cb? null, user
-
- allocateNow: (req, cb) ->
- _.times user.stats.points, user.fns.autoAllocate
- user.stats.points = 0
- user.markModified? 'stats'
- cb? null, user.stats
-
- # ------
- # Tasks
- # ------
-
- clearCompleted: (req, cb) ->
- _.remove user.todos, (t)-> t.completed and !t.challenge?.id
- user.markModified? 'todos'
- cb? null, user.todos
-
- sortTask: (req, cb) ->
- {id} = req.params
- {to, from} = req.query
- task = user.tasks[id]
- return cb?({code:404, message: i18n.t('messageTaskNotFound', req.language)}) unless task
- return cb?('?to=__&from=__ are required') unless to? and from?
- tasks = user["#{task.type}s"]
- if task.type is 'todo' and tasks[from] isnt task # client indices don't match because of preened tasks
- preenedTasks = api.preenTodos(tasks);
- to = tasks.indexOf(preenedTasks[to]) unless to == -1 # Push To Bottom doesn't require readjustment
- from = tasks.indexOf(preenedTasks[from])
- return cb?({code:404, message: i18n.t('messageTaskNotFound', req.language)}) unless tasks[from] is task
- movedTask = tasks.splice(from, 1)[0]
- if to == -1 # we've used the Push To Bottom feature
- tasks.push(movedTask)
- else # any other sort method uses only positive 'to' values
- tasks.splice(to, 0, movedTask)
- cb? null, tasks
-
- updateTask: (req, cb) ->
- return cb?({code:404,message:i18n.t('messageTaskNotFound', req.language)}) unless task = user.tasks[req.params?.id]
- _.merge task, _.omit(req.body,['checklist','id', 'type'])
- task.checklist = req.body.checklist if req.body.checklist
- task.markModified? 'tags'
- cb? null, task
-
- deleteTask: (req, cb) ->
- task = user.tasks[req.params?.id]
- return cb?({code:404,message:i18n.t('messageTaskNotFound', req.language)}) unless task
- i = user[task.type + "s"].indexOf(task)
- user[task.type + "s"].splice(i, 1) if ~i
- cb? null, {}
-
- addTask: (req, cb) ->
- task = api.taskDefaults(req.body)
- return cb?({code:409,message:i18n.t('messageDuplicateTaskID', req.language)}) if user.tasks[task.id]?
- user["#{task.type}s"].unshift(task)
- if user.preferences.newTaskEdit then task._editing = true
- if user.preferences.tagsCollapsed then task._tags = true
- if !user.preferences.advancedCollapsed then task._advanced = true
- cb? null, task
- task
-
- # ------
- # Tags
- # ------
-
- addTag: (req, cb) ->
- user.tags ?= []
- user.tags.push
- name: req.body.name
- id: req.body.id or api.uuid()
- cb? null, user.tags
-
- sortTag: (req, cb) ->
- {to, from} = req.query
- return cb?('?to=__&from=__ are required') unless to? and from?
- user.tags.splice to, 0, user.tags.splice(from, 1)[0]
- cb? null, user.tags
-
- getTags: (req, cb) ->
- cb? null, user.tags
-
- getTag: (req, cb) ->
- tid = req.params.id
- i = _.findIndex user.tags, {id: tid}
- return cb?({code:404,message:i18n.t('messageTagNotFound', req.language)}) if !~i
- cb? null, user.tags[i]
-
- updateTag: (req, cb) ->
- tid = req.params.id
- i = _.findIndex user.tags, {id: tid}
- return cb?({code:404,message:i18n.t('messageTagNotFound', req.language)}) if !~i
- user.tags[i].name = req.body.name
- cb? null, user.tags[i]
-
- deleteTag: (req, cb) ->
- tid = req.params.id
- i = _.findIndex user.tags, {id: tid}
- return cb?({code:404,message:i18n.t('messageTagNotFound', req.language)}) if !~i
- tag = user.tags[i]
- delete user.filters[tag.id]
- user.tags.splice i, 1
-
- # remove tag from all tasks
- _.each user.tasks, (task) ->
- delete task.tags[tag.id]
-
- _.each ['habits','dailys','todos','rewards'], (type) ->
- user.markModified? type
- cb? null, user.tags
-
- # ------
- # Webhooks
- # ------
- addWebhook: (req, cb) ->
- wh = user.preferences.webhooks
- api.refPush(wh, {url:req.body.url, enabled: req.body.enabled or true, id:req.body.id})
- user.markModified? 'preferences.webhooks'
- cb? null, user.preferences.webhooks
- updateWebhook: (req, cb) ->
- _.merge(user.preferences.webhooks[req.params.id], req.body)
- user.markModified? 'preferences.webhooks'
- cb? null, user.preferences.webhooks
- deleteWebhook: (req, cb) ->
- delete user.preferences.webhooks[req.params.id]
- user.markModified? 'preferences.webhooks'
- cb? null, user.preferences.webhooks
-
- # ------
- # Push Notifications
- # ------
- addPushDevice: (req, cb) ->
- user.pushDevices = [] unless user.pushDevices
- pd = user.pushDevices
- item = {regId:req.body.regId, type:req.body.type}
- i = _.findIndex pd, {regId: item.regId}
-
- pd.push(item) unless i != -1
-
- cb? null, user.pushDevices
-
- # ------
- # Inbox
- # ------
- clearPMs: (req, cb) ->
- user.inbox.messages = {}
- user.markModified? 'inbox.messages'
- cb? null, user.inbox.messages
- deletePM: (req, cb) ->
- delete user.inbox.messages[req.params.id]
- user.markModified? 'inbox.messages.'+req.params.id
- cb? null, user.inbox.messages
- blockUser: (req, cb) ->
- i = user.inbox.blocks.indexOf(req.params.uuid)
- if ~i then user.inbox.blocks.splice(i,1) else user.inbox.blocks.push(req.params.uuid)
- user.markModified? 'inbox.blocks'
- cb? null, user.inbox.blocks
-
- # ------
- # Inventory
- # ------
-
- feed: (req, cb) ->
- {pet,food} = req.params
- food = content.food[food]
- [egg, potion] = pet.split('-')
- userPets = user.items.pets
-
- # Generate pet display name variable
- potionText = if content.hatchingPotions[potion] then content.hatchingPotions[potion].text() else potion
- eggText = if content.eggs[egg] then content.eggs[egg].text() else egg
- petDisplayName = i18n.t('petName', {
- potion: potionText
- egg: eggText
- })
-
- return cb?({code:404, message:i18n.t('messagePetNotFound', req.language)}) unless userPets[pet]
- return cb?({code:404, message:i18n.t('messageFoodNotFound', req.language)}) unless user.items.food?[food.key]
- return cb?({code:401, message:i18n.t('messageCannotFeedPet', req.language)}) if content.specialPets[pet]
- return cb?({code:401, message:i18n.t('messageAlreadyMount', req.language)}) if user.items.mounts[pet]
-
- message = ''
- evolve = ->
- userPets[pet] = -1
- # changed to -1 to mark "owned" pets
- user.items.mounts[pet] = true
- user.items.currentPet = "" if pet is user.items.currentPet
- message = i18n.t('messageEvolve', {egg: petDisplayName}, req.language)
-
- if food.key is 'Saddle'
- evolve()
- else
- if food.target is potion or content.hatchingPotions[potion].premium
- userPets[pet] += 5
- message = i18n.t('messageLikesFood', {egg: petDisplayName, foodText: food.text(req.language)}, req.language)
- else
- userPets[pet] += 2
- message = i18n.t('messageDontEnjoyFood', {egg: petDisplayName, foodText: food.text(req.language)}, req.language)
- if userPets[pet] >= 50 and !user.items.mounts[pet]
- evolve()
- user.items.food[food.key]--
- cb? {code:200, message}, {value: userPets[pet]}
-
- buySpecialSpell: (req,cb) ->
- {key} = req.params
- item = content.special[key]
- return cb?({code:401, message: i18n.t('messageNotEnoughGold', req.language)}) if user.stats.gp < item.value
- user.stats.gp -= item.value
- user.items.special[key] ?= 0
- user.items.special[key]++
- user.markModified? 'items.special'
- message = i18n.t('messageBought', {itemText: item.text(req.language)}, req.language)
- cb? {code:200,message}, _.pick(user,$w 'items stats')
-
- # buy is for using Gold, purchase is for Gems (I know, I know...)
- purchase: (req, cb, analytics) ->
- {type,key} = req.params
-
- if type is 'gems' and key is 'gem'
- {convRate, convCap} = api.planGemLimits
- convCap += user.purchased.plan.consecutive.gemCapExtra
- return cb?({code:401,message:"Must subscribe to purchase gems with GP"},req) unless user.purchased?.plan?.customerId
- return cb?({code:401,message:"Not enough Gold"}) unless user.stats.gp >= convRate
- return cb?({code:401,message:"You've reached the Gold=>Gem conversion cap (#{convCap}) for this month. We have this to prevent abuse / farming. The cap will reset within the first three days of next month."}) if user.purchased.plan.gemsBought >= convCap
- user.balance += .25
- user.purchased.plan.gemsBought++
- user.stats.gp -= convRate
-
- analyticsData = {
- uuid: user._id,
- itemKey: key,
- acquireMethod: 'Gold',
- goldCost: convRate,
- category: 'behavior'
- }
- analytics?.track('purchase gems', analyticsData)
-
- return cb? {code:200,message:"+1 Gem"}, _.pick(user,$w 'stats balance')
-
- return cb?({code:404,message:":type must be in [eggs,hatchingPotions,food,quests,gear]"},req) unless type in ['eggs','hatchingPotions','food','quests','gear']
- if type is 'gear'
- item = content.gear.flat[key]
- return cb?({code:401, message: i18n.t('alreadyHave', req.language)}) if user.items.gear.owned[key]
- price = (if item.twoHanded or item.gearSet is 'animal' then 2 else 1) / 4
- else
- item = content[type][key]
- price = item.value / 4
- return cb?({code:404,message:":key not found for Content.#{type}"},req) unless item
- return cb?({code:403, message: i18n.t('messageNotAvailable', req.language)}) if not item.canBuy(user)
- return cb?({code:403, message: i18n.t('notEnoughGems', req.language)}) if (user.balance < price) or !user.balance
- user.balance -= price
- if type is 'gear' then user.items.gear.owned[key] = true
- else
- user.items[type][key] = 0 unless user.items[type][key] > 0
- user.items[type][key]++
-
- analyticsData = {
- uuid: user._id,
- itemKey: key,
- itemType: 'Market',
- acquireMethod: 'Gems',
- gemCost: item.value,
- category: 'behavior'
- }
- analytics?.track('acquire item', analyticsData)
-
- cb? null, _.pick(user,$w 'items balance')
-
- releasePets: (req, cb, analytics) ->
- if user.balance < 1
- return cb? {code:401,message: i18n.t('notEnoughGems', req.language)}
- else
- user.balance -= 1
- for pet of content.pets
- user.items.pets[pet] = 0
- if not user.achievements.beastMasterCount
- user.achievements.beastMasterCount = 0
- user.achievements.beastMasterCount++
- user.items.currentPet = ""
-
- analyticsData = {
- uuid: user._id,
- acquireMethod: 'Gems',
- gemCost: 4,
- category: 'behavior'
- }
- analytics?.track('release pets', analyticsData)
- cb? null, user
-
- releaseMounts: (req, cb, analytics) ->
- if user.balance < 1
- return cb? {code:401,message: i18n.t('notEnoughGems', req.language)}
- else
- user.balance -= 1
- user.items.currentMount = ""
- for mount of content.pets
- user.items.mounts[mount] = null
- if not user.achievements.mountMasterCount
- user.achievements.mountMasterCount = 0
- user.achievements.mountMasterCount++
-
- analyticsData = {
- uuid: user._id,
- acquireMethod: 'Gems',
- gemCost: 4,
- category: 'behavior'
- }
- analytics?.track('release mounts', analyticsData)
-
- cb? null, user
-
- releaseBoth: (req, cb) ->
- if user.balance < 1.5 and not user.achievements.triadBingo
- return cb? {code:401,message: i18n.t('notEnoughGems', req.language)}
- else
- giveTriadBingo = true
- if not user.achievements.triadBingo
- analyticsData = {
- uuid: user._id,
- acquireMethod: 'Gems',
- gemCost: 6,
- category: 'behavior'
- }
- analytics?.track('release pets & mounts', analyticsData)
- user.balance -= 1.5
- user.items.currentMount = ""
- user.items.currentPet = ""
- for animal of content.pets
- giveTriadBingo = false if user.items.pets[animal] == -1
- user.items.pets[animal] = 0
- user.items.mounts[animal] = null
- if not user.achievements.beastMasterCount
- user.achievements.beastMasterCount = 0
- user.achievements.beastMasterCount++
- if not user.achievements.mountMasterCount
- user.achievements.mountMasterCount = 0
- user.achievements.mountMasterCount++
- if giveTriadBingo
- if not user.achievements.triadBingoCount
- user.achievements.triadBingoCount = 0
- user.achievements.triadBingoCount++
- cb? null, user
-
- # buy is for gear, purchase is for gem-purchaseables (i know, I know...)
- buy: (req, cb, analytics) ->
- {key} = req.params
-
- item = if key is 'potion' then content.potion
- else if key is 'armoire' then content.armoire
- else content.gear.flat[key]
- return cb?({code:404, message:"Item '#{key} not found (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.coffee)"}) unless item
- return cb?({code:401, message: i18n.t('messageNotEnoughGold', req.language)}) if user.stats.gp < item.value
- return cb?({code:401, message: "You can't buy this item"}) if item.canOwn? and !item.canOwn(user)
- armoireResp = undefined
- if item.key is 'potion'
- user.stats.hp += 15
- user.stats.hp = 50 if user.stats.hp > 50
- else if item.key is 'armoire'
- armoireResult = user.fns.predictableRandom(user.stats.gp)
- # We use a different seed to choose the Armoire action than we use
- # to choose the sub-action, otherwise only some of the foods can
- # be given. E.g., if a seed gives armoireResult < .5 (food) then
- # the same seed would give one of the first five foods only.
- eligibleEquipment = _.filter(content.gear.flat, ((i)->i.klass is 'armoire' and !user.items.gear.owned[i.key]))
- if !_.isEmpty(eligibleEquipment) and (armoireResult < .6 or !user.flags.armoireOpened)
- eligibleEquipment.sort() # https://github.com/HabitRPG/habitrpg/issues/5376#issuecomment-111799217
- drop = user.fns.randomVal(eligibleEquipment)
- user.items.gear.owned[drop.key] = true
- user.flags.armoireOpened = true
- message = i18n.t('armoireEquipment', {image: '', dropText: drop.text(req.language)}, req.language)
- if api.count.remainingGearInSet(user.items.gear.owned, 'armoire') is 0 then user.flags.armoireEmpty = true
- armoireResp = {type: "gear", dropKey: drop.key, dropText: drop.text(req.language)}
- else if (!_.isEmpty(eligibleEquipment) and armoireResult < .8) or armoireResult < .5
- drop = user.fns.randomVal _.where(content.food, {canDrop:true})
- user.items.food[drop.key] ?= 0
- user.items.food[drop.key] += 1
- message = i18n.t('armoireFood', {image: '', dropArticle: drop.article, dropText: drop.text(req.language)}, req.language)
- armoireResp = {type: "food", dropKey: drop.key, dropArticle: drop.article, dropText: drop.text(req.language)}
- else
- armoireExp = Math.floor(user.fns.predictableRandom(user.stats.exp) * 40 + 10)
- user.stats.exp += armoireExp
- message = i18n.t('armoireExp', req.language)
- armoireResp = {"type": "experience", "value": armoireExp}
- else
- if user.preferences.autoEquip then user.items.gear.equipped[item.type] = item.key
- user.items.gear.owned[item.key] = true
- message = user.fns.handleTwoHanded(item, null, req)
- message ?= i18n.t('messageBought', {itemText: item.text(req.language)}, req.language)
- if item.last then user.fns.ultimateGear()
- user.stats.gp -= item.value
-
- analyticsData = {
- uuid: user._id,
- itemKey: key,
- acquireMethod: 'Gold',
- goldCost: item.value,
- category: 'behavior'
- }
- analytics?.track('acquire item', analyticsData)
-
- buyResp = _.pick(user,$w 'items achievements stats flags')
- buyResp["armoire"] = armoireResp if armoireResp
- cb? {code:200, message}, buyResp
-
- buyQuest: (req, cb, analytics) ->
- {key} = req.params
- item = content.quests[key]
- return cb?({code:404, message:"Quest '#{key} not found (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.coffee)"}) unless item
- return cb?({code:404, message:"Quest '#{key} is not a Gold-purchasable quest (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.coffee)"}) unless item.category is 'gold' and item.goldValue
- return cb?({code:401, message: i18n.t('messageNotEnoughGold', req.language)}) if user.stats.gp < item.goldValue
- message = i18n.t('messageBought', {itemText: item.text(req.language)}, req.language)
- user.items.quests[item.key] ?= 0
- user.items.quests[item.key] += 1
- user.stats.gp -= item.goldValue
- analyticsData = {
- uuid: user._id,
- itemKey: item.key,
- itemType: 'Market',
- goldCost: item.goldValue,
- acquireMethod: 'Gold',
- category: 'behavior'
- }
- analytics?.track('acquire item', analyticsData)
- cb? {code:200, message}, user.items.quests
-
- buyMysterySet: (req, cb, analytics)->
- return cb?({code:401, message:i18n.t('notEnoughHourglasses', req.language)}) unless user.purchased.plan.consecutive.trinkets > 0
- mysterySet = content.timeTravelerStore(user.items.gear.owned)?[req.params.key]
- if window?.confirm?
- return unless window.confirm(i18n.t('hourglassBuyEquipSetConfirm'))
- return cb?({code:404, message:"Mystery set not found, or set already owned"}) unless mysterySet
- _.each mysterySet.items, (i)->
- user.items.gear.owned[i.key]=true
- analyticsData = {
- uuid: user._id,
- itemKey: i.key,
- itemType: 'Subscriber Gear',
- acquireMethod: 'Hourglass',
- category: 'behavior'
- }
- analytics?.track('acquire item', analyticsData)
-
- user.purchased.plan.consecutive.trinkets--
- cb? {code:200, message:i18n.t('hourglassPurchaseSet', req.language)}, _.pick(user,$w 'items purchased.plan.consecutive')
-
- hourglassPurchase: (req, cb, analytics)->
- {type, key} = req.params
- return cb?({code:403, message:i18n.t('typeNotAllowedHourglass', req.language) + JSON.stringify(_.keys(content.timeTravelStable))}) unless content.timeTravelStable[type]
- return cb?({code:403, message:i18n.t(type+'NotAllowedHourglass', req.language)}) unless _.contains(_.keys(content.timeTravelStable[type]), key)
- return cb?({code:403, message:i18n.t(type+'AlreadyOwned', req.language)}) if user.items[type][key]
- return cb?({code:403, message:i18n.t('notEnoughHourglasses', req.language)}) unless user.purchased.plan.consecutive.trinkets > 0
- user.purchased.plan.consecutive.trinkets--
- if type is 'pets'
- user.items.pets[key] = 5
- if type is 'mounts'
- user.items.mounts[key] = true
- analyticsData = {
- uuid: user._id,
- itemKey: key,
- itemType: type,
- acquireMethod: 'Hourglass',
- category: 'behavior'
- }
- analytics?.track('acquire item', analyticsData)
- cb? {code:200, message:i18n.t('hourglassPurchase', req.language)}, _.pick(user,$w 'items purchased.plan.consecutive')
-
- sell: (req, cb) ->
- {key, type} = req.params
- return cb?({code:404,message:":type not found. Must bes in [eggs, hatchingPotions, food]"}) unless type in ['eggs','hatchingPotions', 'food']
- return cb?({code:404,message:":key not found for user.items.#{type}"}) unless user.items[type][key]
- user.items[type][key]--
- user.stats.gp += content[type][key].value
- cb? null, _.pick(user,$w 'stats items')
-
- equip: (req, cb) ->
- [type, key] = [req.params.type || 'equipped', req.params.key]
- switch type
- when 'mount'
- return cb?({code:404,message:":You do not own this mount."}) unless user.items.mounts[key]
- user.items.currentMount = if user.items.currentMount is key then '' else key
- when 'pet'
- return cb?({code:404,message:":You do not own this pet."}) unless user.items.pets[key]
- user.items.currentPet = if user.items.currentPet is key then '' else key
- when 'costume','equipped'
- item = content.gear.flat[key]
- return cb?({code:404,message:":You do not own this gear."}) unless user.items.gear.owned[key]
- if user.items.gear[type][item.type] is key
- user.items.gear[type][item.type] = "#{item.type}_base_0"
- message = i18n.t('messageUnEquipped', {itemText: item.text(req.language)}, req.language)
- else
- user.items.gear[type][item.type] = item.key
- message = user.fns.handleTwoHanded(item,type,req)
- user.markModified? "items.gear.#{type}"
- cb? (if message then {code:200,message} else null), user.items
-
- hatch: (req, cb) ->
- {egg, hatchingPotion} = req.params
- return cb?({code:400,message:"Please specify query.egg & query.hatchingPotion"}) unless egg and hatchingPotion
- return cb?({code:403,message:i18n.t('messageMissingEggPotion', req.language)}) unless user.items.eggs[egg] > 0 and user.items.hatchingPotions[hatchingPotion] > 0
- return cb?({code:403,message:i18n.t('messageInvalidEggPotionCombo', req.language)}) if content.hatchingPotions[hatchingPotion].premium and not content.dropEggs[egg]
- pet = "#{egg}-#{hatchingPotion}"
- return cb?({code:403, message:i18n.t('messageAlreadyPet', req.language)}) if user.items.pets[pet] and user.items.pets[pet] > 0
- user.items.pets[pet] = 5
- user.items.eggs[egg]--
- user.items.hatchingPotions[hatchingPotion]--
- cb? {code:200, message:i18n.t('messageHatched', req.language)}, user.items
-
- unlock: (req, cb, analytics) ->
- {path} = req.query
- fullSet = ~path.indexOf(",")
- cost =
- # (Backgrounds) 15G per set, 7G per individual
- if ~path.indexOf('background.') # FIXME, store prices of things in content/index.coffee instead of hard-coded here?
- if fullSet then 3.75 else 1.75
- # (Skin, hair, etc) 5G per set, 2G per individual
- else
- if fullSet then 1.25 else 0.5
- alreadyOwns = !fullSet and user.fns.dotGet("purchased." + path) is true
- return cb?({code:401, message: i18n.t('notEnoughGems', req.language)}) if (user.balance < cost or !user.balance) and !alreadyOwns
- if fullSet
- _.each path.split(","), (p) ->
- if ~path.indexOf('gear.')
- user.fns.dotSet("#{p}", true);true
- else
- user.fns.dotSet("purchased.#{p}", true);true
- else
- if alreadyOwns
- split = path.split('.');v=split.pop();k=split.join('.')
- v='' if k is 'background' and v==user.preferences.background
- user.fns.dotSet("preferences.#{k}",v)
- return cb? null, req
- user.fns.dotSet "purchased." + path, true
- user.balance -= cost
- if ~path.indexOf('gear.') then user.markModified? 'gear.owned' else user.markModified? 'purchased'
-
- analyticsData = {
- uuid: user._id,
- itemKey: path,
- itemType: 'customization',
- acquireMethod: 'Gems',
- gemCost: (cost/.25),
- category: 'behavior'
- }
- analytics?.track('acquire item', analyticsData)
-
- cb? null, _.pick(user,$w 'purchased preferences items')
-
- # ------
- # Classes
- # ------
-
- changeClass: (req, cb, analytics) ->
- klass = req.query?.class
- if klass in ['warrior','rogue','wizard','healer']
-
- analyticsData = {
- uuid: user._id,
- class: klass,
- acquireMethod: 'Gems',
- gemCost: 3,
- category: 'behavior'
- }
- analytics?.track('change class', analyticsData)
-
- user.stats.class = klass
- user.flags.classSelected = true
- # Clear their gear and equip their new class's gear (can still equip old gear from inventory)
- # If they've rolled this class before, restore their progress
- _.each ["weapon", "armor", "shield", "head"], (type) ->
- foundKey = false
- _.findLast user.items.gear.owned, (v, k) ->
- return foundKey = k if ~k.indexOf(type + "_" + klass) and v is true
- # restore progress from when they last rolled this class
- # weapon_0 is significant, don't reset to base_0
- # rogues start with an off-hand weapon
- user.items.gear.equipped[type] =
- if foundKey then foundKey
- else if type is "weapon" then "weapon_#{klass}_0"
- else if type is "shield" and klass is "rogue" then "shield_rogue_0"
- else "#{type}_base_0" # naked for the rest!
-
- # Grant them their new class's gear
- user.items.gear.owned["#{type}_#{klass}_0"] = true if type is "weapon" or (type is "shield" and klass is "rogue")
- true
- else
- # Null ?class value means "reset class"
- if user.preferences.disableClasses
- user.preferences.disableClasses = false
- user.preferences.autoAllocate = false
- else
- return cb?({code:401,message:i18n.t('notEnoughGems', req.language)}) unless user.balance >= .75
- user.balance -= .75
- _.merge user.stats, {str: 0, con: 0, per: 0, int: 0, points: api.capByLevel(user.stats.lvl)}
- user.flags.classSelected = false
- #'stats.points': this is handled on the server
- cb? null, _.pick(user,$w 'stats flags items preferences')
-
- disableClasses: (req, cb) ->
- user.stats.class = 'warrior'
- user.flags.classSelected = true
- user.preferences.disableClasses = true
- user.preferences.autoAllocate = true
- user.stats.str = api.capByLevel(user.stats.lvl)
- user.stats.points = 0
- cb? null, _.pick(user,$w 'stats flags preferences')
-
- allocate: (req, cb) ->
- stat = req.query.stat or 'str'
- if user.stats.points > 0
- user.stats[stat]++
- user.stats.points--
- user.stats.mp++ if stat is 'int' #increase their MP along with their max MP
- cb? null, _.pick(user,$w 'stats')
-
- readCard: (req, cb) ->
- {cardType} = req.params
- user.items.special["#{cardType}Received"].shift()
- user.markModified? "items.special.#{cardType}Received"
- user.flags.cardReceived = false
- cb? null, 'items.special flags.cardReceived'
-
- openMysteryItem: (req,cb,analytics) ->
- item = user.purchased.plan?.mysteryItems?.shift()
- return cb?(code:400,message:"Empty") unless item
- item = content.gear.flat[item]
- user.items.gear.owned[item.key] = true
- user.markModified? 'purchased.plan.mysteryItems'
- item.notificationType = 'Mystery' # needed for website/public/js/controllers/notificationCtrl.js line 59 approx.
- analyticsData = {
- uuid: user._id,
- itemKey: item,
- itemType: 'Subscriber Gear',
- acquireMethod: 'Subscriber',
- category: 'behavior'
- }
- analytics?.track('open mystery item', analyticsData)
- (user._tmp?={}).drop = item if typeof window != 'undefined'
- cb? null, user.items.gear.owned
-
- # ------
- # Score
- # ------
-
- score: (req, cb) ->
- {id, direction} = req.params # up or down
- task = user.tasks[id]
- options = req.query or {}; _.defaults(options, {times:1, cron:false})
-
- # This is for setting one-time temporary flags, such as streakBonus or itemDropped. Useful for notifying
- # the API consumer, then cleared afterwards
- user._tmp = {}
-
- # TODO do we need this fail-safe casting anymore? Are we safe now we're off Derby?
- stats = {gp: +user.stats.gp, hp: +user.stats.hp, exp: +user.stats.exp}
- task.value = +task.value; task.streak = ~~task.streak; task.priority ?= 1
-
- # If they're trying to purhcase a too-expensive reward, don't allow them to do that.
- if task.value > stats.gp and task.type is 'reward'
- return cb? {code:401,message:i18n.t('messageNotEnoughGold', req.language)}
-
- delta = 0
-
- calculateDelta = ->
- # Calculates the next task.value based on direction
- # Uses a capped inverse log y=.95^x, y>= -5
-
- # Min/max on task redness
- currVal =
- if task.value < -47.27 then -47.27
- else if task.value > 21.27 then 21.27
- else task.value
- nextDelta = Math.pow(0.9747, currVal) * (if direction is 'down' then -1 else 1)
-
- # Checklists
- if task.checklist?.length > 0
- # If the Daily, only dock them them a portion based on their checklist completion
- if direction is 'down' and task.type is 'daily' and options.cron
- nextDelta *= (1 - _.reduce(task.checklist,((m,i)->m+(if i.completed then 1 else 0)),0) / task.checklist.length)
- # If To-Do, point-match the TD per checklist item completed
- if task.type is 'todo'
- nextDelta *= (1 + _.reduce(task.checklist,((m,i)->m+(if i.completed then 1 else 0)),0))
- nextDelta
-
- calculateReverseDelta = ->
- # Approximates the reverse delta for the task value
- # This is meant to return the task value to its original value when unchecking a task.
- # First, calculate the the value using the normal way for our first guess although
- # it will be a bit off
- currVal =
- if task.value < -47.27 then -47.27
- else if task.value > 21.27 then 21.27
- else task.value
- testVal = currVal + Math.pow(0.9747, currVal) * (if direction is 'down' then -1 else 1)
-
- # Now keep moving closer to the original value until we get "close enough"
- closeEnough = 0.00001
- while true
- # Check how close we are to the original value by computing the delta off our guess
- # and looking at the difference between that and our current value.
- calc = (testVal) + Math.pow(0.9747, testVal)
- diff = currVal-calc
- if Math.abs(diff) < closeEnough
- break
- if diff > 0
- testVal -= diff
- else
- testVal += diff
-
- # When we get close enough, return the difference between our approximated value
- # and the current value. This will be the delta calculated from the original value
- # before the task was checked.
- nextDelta = testVal - currVal
-
- # Checklists
- if task.checklist?.length > 0
- # If To-Do, point-match the TD per checklist item completed
- if task.type is 'todo'
- nextDelta *= (1 + _.reduce(task.checklist,((m,i)->m+(if i.completed then 1 else 0)),0))
- nextDelta
-
-
- changeTaskValue = ->
- # If multiple days have passed, multiply times days missed
- _.times options.times, ->
- # Each iteration calculate the nextDelta, which is then accumulated in the total delta.
- nextDelta = if not options.cron and direction is 'down' then calculateReverseDelta() else calculateDelta()
- unless task.type is 'reward'
- if (user.preferences.automaticAllocation is true and user.preferences.allocationMode is 'taskbased' and !(task.type is 'todo' and direction is 'down')) then user.stats.training[task.attribute] += nextDelta
- if direction is 'up' # Make progress on quest based on STR
- user.party.quest.progress.up = user.party.quest.progress.up || 0
- user.party.quest.progress.up += (nextDelta * (1 + (user._statsComputed.str / 200))) if task.type in ['daily','todo']
- user.party.quest.progress.up += (nextDelta * (0.5 + (user._statsComputed.str / 400))) if task.type is 'habit'
- task.value += nextDelta
- delta += nextDelta
-
- addPoints = ->
- # ===== CRITICAL HITS =====
- # allow critical hit only when checking off a task, not when unchecking it:
- _crit = (if delta > 0 then user.fns.crit() else 1)
- # if there was a crit, alert the user via notification
- user._tmp.crit = _crit if _crit > 1
-
- # Exp Modifier
- # ===== Intelligence =====
- # TODO Increases Experience gain by .2% per point.
- intBonus = 1 + (user._statsComputed.int * .025)
- stats.exp += Math.round (delta * intBonus * task.priority * _crit * 6)
-
- # GP modifier
- # ===== PERCEPTION =====
- # TODO Increases Gold gained from tasks by .3% per point.
- perBonus = (1 + user._statsComputed.per *.02)
- gpMod = (delta * task.priority * _crit * perBonus)
- stats.gp +=
- if task.streak
- currStreak = if direction is 'down' then task.streak-1 else task.streak
- streakBonus = currStreak / 100 + 1 # eg, 1-day streak is 1.01, 2-day is 1.02, etc
- afterStreak = gpMod * streakBonus
- if currStreak > 0
- user._tmp.streakBonus = afterStreak - gpMod if (gpMod > 0) # keep this on-hand for later, so we can notify streak-bonus
- afterStreak
- else gpMod
-
- # HP modifier
- subtractPoints = ->
- # ===== CONSTITUTION =====
- # TODO Decreases HP loss from bad habits / missed dailies by 0.5% per point.
- conBonus = 1 - (user._statsComputed.con / 250)
- conBonus = 0.1 if conBonus < .1
- hpMod = delta * conBonus * task.priority * 2 # constant 2 multiplier for better results
- stats.hp += Math.round(hpMod * 10) / 10 # round to 1dp
-
- gainMP = (delta) ->
- delta *= user._tmp.crit or 1
- user.stats.mp += delta
- user.stats.mp = user._statsComputed.maxMP if user.stats.mp >= user._statsComputed.maxMP
- user.stats.mp = 0 if user.stats.mp < 0
-
- # ===== starting to actually do stuff, most of above was definitions =====
- switch task.type
- when 'habit'
- changeTaskValue()
- # Add habit value to habit-history (if different)
- if (delta > 0) then addPoints() else subtractPoints()
- gainMP(_.max([0.25, (.0025 * user._statsComputed.maxMP)]) * if direction is 'down' then -1 else 1)
-
- # History
- th = (task.history ?= [])
- if th[th.length-1] and moment(th[th.length-1].date).isSame(new Date, 'day')
- th[th.length-1].value = task.value
- else
- th.push {date: +new Date, value: task.value}
- user.markModified? "habits.#{_.findIndex(user.habits, {id:task.id})}.history"
-
- when 'daily'
- if options.cron
- changeTaskValue()
- subtractPoints()
- task.streak = 0 unless user.stats.buffs.streaks
- else
- changeTaskValue()
- if direction is 'down'
- delta = calculateDelta() # recalculate delta for unchecking so the gp and exp come out correctly
- addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
- gainMP(_.max([1, (.01 * user._statsComputed.maxMP)]) * if direction is 'down' then -1 else 1)
- if direction is 'up'
- task.streak = if task.streak then task.streak + 1 else 1
- # Give a streak achievement when the streak is a multiple of 21
- if (task.streak % 21) is 0
- user.achievements.streak = if user.achievements.streak then user.achievements.streak + 1 else 1
- else
- # Remove a streak achievement if streak was a multiple of 21 and the daily was undone
- if (task.streak % 21) is 0
- user.achievements.streak = if user.achievements.streak then user.achievements.streak - 1 else 0
- task.streak = if task.streak then task.streak - 1 else 0
-
- when 'todo'
- if options.cron
- changeTaskValue()
- #don't touch stats on cron
- else
- task.dateCompleted = if direction is 'up' then new Date else undefined
- changeTaskValue()
- if direction is 'down'
- delta = calculateDelta() # recalculate delta for unchecking so the gp and exp come out correctly
- addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
- # MP++ per checklist item in ToDo, bonus per CLI
- multiplier = _.max([(_.reduce(task.checklist,((m,i)->m+(if i.completed then 1 else 0)),1)),1])
- gainMP(_.max([(multiplier), (.01 * user._statsComputed.maxMP * multiplier)]) * if direction is 'down' then -1 else 1)
-
- when 'reward'
- # Don't adjust values for rewards
- changeTaskValue()
- # purchase item
- stats.gp -= Math.abs(task.value)
- num = parseFloat(task.value).toFixed(2)
- # if too expensive, reduce health & zero gp
- if stats.gp < 0
- # hp - gp difference
- stats.hp += stats.gp
- stats.gp = 0
-
- user.fns.updateStats stats, req
-
- # Drop system (don't run on the client, as it would only be discarded since ops are sent to the API, not the results)
- if typeof window is 'undefined'
- user.fns.randomDrop({task, delta}, req) if direction is 'up'
-
- cb? null, user
- return delta
-
- # ----------------------------------------------------------------------
- # user.fns helpers
- # ----------------------------------------------------------------------
-
- user.fns =
-
- getItem: (type) ->
- item = content.gear.flat[user.items.gear.equipped[type]]
- return content.gear.flat["#{type}_base_0"] unless item
- item
-
- handleTwoHanded: (item, type='equipped', req) ->
- # If they're buying a shield and wearing a staff, dequip the staff
- if item.type is "shield" and (weapon = content.gear.flat[user.items.gear[type].weapon])?.twoHanded
- user.items.gear[type].weapon = 'weapon_base_0'
- message = i18n.t('messageTwoHandled', {gearText: weapon.text(req.language)}, req.language)
- # If they're buying a staff and wearing a shield, dequip the shield
- if item.twoHanded
- user.items.gear[type].shield = "shield_base_0"
- message = i18n.t('messageTwoHandled', {gearText: item.text(req.language)}, req.language)
- message
-
- ###
- Because the same op needs to be performed on the client and the server (critical hits, item drops, etc),
- we need things to be "random", but technically predictable so that they don't go out-of-sync
- ###
- predictableRandom: (seed) ->
- # Default seed is all user stats combined. Fairly safe, meh - pass in a good seed for situations where that doesn't work
- seed = _.reduce(user.stats, ((m,v)->if _.isNumber(v) then m+v else m), 0) if !seed or seed is Math.PI
- x = Math.sin(seed++) * 10000
- x - Math.floor(x)
-
- crit: (stat='str', chance=.03) ->
- #console.log("Crit Chance:"+chance*(1+user._statsComputed[stat]/100))
- s = user._statsComputed[stat]
- if user.fns.predictableRandom() <= chance*(1 + s/100)
- 1.5 + 4*s/(s + 200)
- else 1
-
- ###
- Get a random property from an object
- returns random property (the value)
- ###
- randomVal: (obj, options) ->
- array = if options?.key then _.keys(obj) else _.values(obj)
- rand = user.fns.predictableRandom(options?.seed)
- array.sort()
- array[Math.floor(rand * array.length)]
-
- ###
- This allows you to set object properties by dot-path. Eg, you can run pathSet('stats.hp',50,user) which is the same as
- user.stats.hp = 50. This is useful because in our habitrpg-shared functions we're returning changesets as {path:value},
- so that different consumers can implement setters their own way. Derby needs model.set(path, value) for example, where
- Angular sets object properties directly - in which case, this function will be used.
- ###
- dotSet: (path, val)-> api.dotSet user,path,val
- dotGet: (path)-> api.dotGet user,path
-
- # ----------------------------------------------------------------------
- # Scoring
- # ----------------------------------------------------------------------
-
- randomDrop: (modifiers, req) ->
- {task} = modifiers
-
- # % chance of getting a drop
-
- chance = _.min([Math.abs(task.value - 21.27),37.5])/150+.02 # Base drop chance is a percentage based on task value. Typical fresh task: 15%. Very ripe task: 25%. Very blue task: 2%.
-
- chance *=
- task.priority * # Task priority: +50% for Medium, +100% for Hard
- (1 + (task.streak / 100 or 0)) * # Streak bonus: +1% per streak
- (1 + (user._statsComputed.per / 100)) * # PERception: +1% per point
- (1 + (user.contributor.level / 40 or 0)) * # Contrib levels: +2.5% per level
- (1 + (user.achievements.rebirths / 20 or 0)) * # Rebirths: +5% per achievement
- (1 + (user.achievements.streak / 200 or 0)) * # Streak achievements: +0.5% per achievement
- (user._tmp.crit or 1) * # Use the crit multiplier if we got one
- (1 + .5*(_.reduce(task.checklist,((m,i)->m+(if i.completed then 1 else 0)),0) or 0)) # +50% per checklist item complete. TODO: make this into X individual drop chances instead
-
- chance = api.diminishingReturns(chance, 0.75)
-
- #console.log("Drop chance: " + chance)
-
- quest = content.quests[user.party.quest?.key]
- if quest?.collect and user.fns.predictableRandom(user.stats.gp) < chance
- dropK = user.fns.randomVal quest.collect, {key:true}
- user.party.quest.progress.collect[dropK]++
- user.markModified? 'party.quest.progress'
- #console.log {progress:user.party.quest.progress}
-
- dropMultiplier = if user.purchased?.plan?.customerId then 2 else 1
- return if (api.daysSince(user.items.lastDrop.date, user.preferences) is 0) and (user.items.lastDrop.count >= dropMultiplier * (5 + Math.floor(user._statsComputed.per / 25) + (user.contributor.level or 0)))
- if user.flags?.dropsEnabled and user.fns.predictableRandom(user.stats.exp) < chance
-
- rarity = user.fns.predictableRandom(user.stats.gp)
-
- # Food: 40% chance
- if rarity > .6
- drop = user.fns.randomVal _.where(content.food, {canDrop:true})
- 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)}, req.language)
-
- # Eggs: 30% chance
- else if rarity > .3
- drop = user.fns.randomVal content.dropEggs
- 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)}, req.language)
-
- # Hatching Potion, 30% chance - break down by rarity.
- else
- acceptableDrops =
- # Very Rare: 10% (of 30%)
- if rarity < .02 then ['Golden']
- # Rare: 20% (of 30%)
- else if rarity < .09 then ['Zombie', 'CottonCandyPink', 'CottonCandyBlue']
- # Uncommon: 30% (of 30%)
- else if rarity < .18 then ['Red', 'Shade', 'Skeleton']
- # Common: 40% (of 30%)
- else ['Base', 'White', 'Desert']
-
- # No Rarity (@see https://github.com/HabitRPG/habitrpg/issues/1048, we may want to remove rareness when we add mounts)
- #drop = helpers.randomVal hatchingPotions
- drop = user.fns.randomVal _.pick(content.hatchingPotions, ((v,k) -> k in acceptableDrops))
-
- 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)}, req.language)
-
- # if they've dropped something, we want the consuming client to know so they can notify the user. See how the Derby
- # app handles it for example. Would this be better handled as an emit() ?
- user._tmp.drop = drop
-
- user.items.lastDrop.date = +new Date
- user.items.lastDrop.count++
-
- ###
- Updates user stats with new stats. Handles death, leveling up, etc
- {stats} new stats
- {update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately
- ###
- autoAllocate: ->
- user.stats[(->
- switch user.preferences.allocationMode
- when "flat"
- # Favor in order (right-to-left): INT, PER, STR, CON
- stats = _.pick user.stats, $w 'con str per int'
- _.invert(stats)[_.min stats]
- when "classbased"
- # Attributes get 3:2:1:1 per 7 levels.
- lvlDiv7 = user.stats.lvl / 7
- ideal = [(lvlDiv7 * 3), (lvlDiv7 * 2), lvlDiv7, lvlDiv7]
- # Primary, secondary etc. attributes aren't explicitly defined, so hardcode them. In order as above
- preference = switch user.stats.class
- when "wizard" then ["int", "per", "con", "str"]
- when "rogue" then ["per", "str", "int", "con"]
- when "healer" then ["con", "int", "str", "per"]
- else ["str", "con", "per", "int"]
- # Get the difference between the ideal attribute spread according to level, and the user's current spread.
- 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, ((val) -> if val is _.min(diff) then true)) # Returns the index of the first attribute that's furthest behind the ideal
- return if ~suggested then preference[suggested] else "str" # If _.findIndex failed, we'd get a -1...
- when "taskbased"
- suggested = _.invert(user.stats.training)[_.max user.stats.training] # Returns the stat that's been trained up the most this level
- _.merge user.stats.training, {str:0,int:0,con:0,per:0} # Reset training for this level.
- return suggested or "str" # Failed _.findkey gives undefined
- else "str" # if all else fails, dump into STR
- )()]++
-
- updateStats: (stats, req, analytics) ->
- # Game Over (death)
- return user.stats.hp=0 if stats.hp <= 0
-
- user.stats.hp = stats.hp
- user.stats.gp = if stats.gp >= 0 then stats.gp else 0
-
- tnl = api.tnl(user.stats.lvl)
-
- # level up & carry-over exp
- if stats.exp >= tnl
- #silent = true # push through the negative xp silently
- user.stats.exp = stats.exp # push normal + notification
- while stats.exp >= tnl # keep levelling up
- stats.exp -= tnl
- user.stats.lvl++
- tnl = api.tnl(user.stats.lvl)
-
- user.stats.hp = 50
-
- continue if user.stats.lvl > api.maxLevel
-
- # Auto-allocate a point, or give them a new manual point
- if user.preferences.automaticAllocation
- user.fns.autoAllocate()
- else
- # add new allocatable points. We could do user.stats.points++, but this does a fail-safe just in case
- user.stats.points = user.stats.lvl - (user.stats.con + user.stats.str + user.stats.per + user.stats.int)
- if user.stats.points < 0
- user.stats.points = 0
- # This happens after dropping level with Fix Character Values and perhaps from other causes.
- # TODO: Subtract points from attributes in the same manner as on death.
- user.stats.exp = stats.exp
-
- # Set flags when they unlock features
- user.flags ?= {}
- if !user.flags.customizationsNotification and (user.stats.exp > 5 or user.stats.lvl > 1)
- user.flags.customizationsNotification = true
- if !user.flags.itemsEnabled and (user.stats.exp > 10 or user.stats.lvl > 1)
- user.flags.itemsEnabled = true
- if !user.flags.dropsEnabled and user.stats.lvl >= 3
- user.flags.dropsEnabled = true
- if user.items.eggs["Wolf"] > 0 then user.items.eggs["Wolf"]++ else user.items.eggs["Wolf"] = 1
- if !user.flags.classSelected and user.stats.lvl >= 10
- user.flags.classSelected
- # Level Drops
- _.each {vice1:30, atom1:15, moonstone1:60, goldenknight1: 40}, (lvl,k)->
- if !user.flags.levelDrops?[k] and user.stats.lvl >= lvl
- user.items.quests[k] ?= 0
- user.items.quests[k]++
- (user.flags.levelDrops ?= {})[k] = true
- user.markModified? 'flags.levelDrops'
- analyticsData = {
- uuid: user._id,
- itemKey: k,
- acquireMethod: 'Level Drop',
- category: 'behavior'
- }
- analytics?.track('acquire item', analyticsData)
- user._tmp.drop = {type: 'Quest', key: k}
- if !user.flags.rebirthEnabled and (user.stats.lvl >= 50 or user.achievements.beastMaster)
- user.flags.rebirthEnabled = true
-
- ###
- ------------------------------------------------------
- Cron
- ------------------------------------------------------
- ###
-
- ###
- At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
- For incomplete Dailys, deduct experience
- Make sure to run this function once in a while as server will not take care of overnight calculations.
- And you have to run it every time client connects.
- {user}
- ###
- cron: (options={}) ->
- now = +options.now || +new Date
-
- daysMissed = api.daysSince user.lastCron, _.defaults({now}, user.preferences)
- return unless daysMissed > 0
-
- user.auth.timestamps.loggedin = new Date()
-
- user.lastCron = now
-
- # Reset the lastDrop count to zero
- if user.items.lastDrop.count > 0
- user.items.lastDrop.count = 0
-
- # "Perfect Day" achievement for perfect-days
- perfect = true
- clearBuffs = {str:0,int:0,per:0,con:0,stealth:0,streaks:false}
-
- # end-of-month perks for subscribers
- plan = user.purchased?.plan
- if plan?.customerId
- if moment(plan.dateUpdated).format('MMYYYY') != moment().format('MMYYYY')
- plan.gemsBought = 0 # reset gem-cap
- plan.dateUpdated = new Date()
- # For every month, inc their "consecutive months" counter. Give perks based on consecutive blocks
- # If they already got perks for those blocks (eg, 6mo subscription, subscription gifts, etc) - then dec the offset until it hits 0
- # TODO use month diff instead of ++ / --?
- _.defaults plan.consecutive, {count:0, offset:0, trinkets:0, gemCapExtra:0} #fixme see https://github.com/HabitRPG/habitrpg/issues/4317
- plan.consecutive.count++
- if plan.consecutive.offset > 0
- plan.consecutive.offset--
- else if plan.consecutive.count%3==0 # every 3 months
- plan.consecutive.trinkets++
- plan.consecutive.gemCapExtra+=5
- plan.consecutive.gemCapExtra=25 if plan.consecutive.gemCapExtra>25 # cap it at 50 (hard 25 limit + extra 25)
- # If user cancelled subscription, we give them until 30day's end until it terminates
- 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}
- user.markModified? 'purchased.plan'
-
- # User is resting at the inn.
- # On cron, buffs are cleared and all dailies are reset without performing damage
- if user.preferences.sleep is true
- user.stats.buffs = clearBuffs
- user.dailys.forEach (daily) ->
- {completed, repeat} = daily
- thatDay = moment(now).subtract({days: 1})
-
- if api.shouldDo(thatDay.toDate(), daily, user.preferences) || completed
- _.each daily.checklist, ((box)->box.completed=false;true)
- daily.completed = false
- return
-
- multiDaysCountAsOneDay = true
- # If the user does not log in for two or more days, cron (mostly) acts as if it were only one day.
- # When site-wide difficulty settings are introduced, this can be a user preference option.
-
- # Tally each task
- todoTally = 0
- user.todos.forEach (task) -> # make uncompleted todos redder
- return unless task
- {id, completed} = task
- delta = user.ops.score({params:{id:task.id, direction:'down'}, query:{times:(multiDaysCountAsOneDay ? 1 : daysMissed), cron:true}})
- absVal = if (completed) then Math.abs(task.value) else task.value
- todoTally += absVal
-
- dailyChecked = 0 # how many dailies were checked?
- dailyDueUnchecked = 0 # how many dailies were due but not checked?
- user.party.quest.progress.down ?= 0
- user.dailys.forEach (task) ->
- return unless task
- {id, completed} = task
-
- # Deduct points for missed Daily tasks
- EvadeTask = 0
- scheduleMisses = daysMissed
- if completed
- dailyChecked += 1
- else
- # dailys repeat, so need to calculate how many they've missed according to their own schedule
- scheduleMisses = 0
- for n in [0...daysMissed]
- thatDay = moment(now).subtract({days: n + 1})
- if api.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 task.checklist?.length > 0 # Partially completed checklists dock fewer mana points
- fractionChecked = _.reduce(task.checklist,((m,i)->m+(if i.completed then 1 else 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 ? 1 : (scheduleMisses-EvadeTask)), cron:true}})
-
- # Apply damage from a boss, less damage for Trivial priority (difficulty)
- user.party.quest.progress.down += delta * (if task.priority < 1 then task.priority else 1)
- # NB: Medium and Hard priorities do not increase damage from boss. This was by accident
- # initially, and when we realised, we could not fix it because users are used to
- # their Medium and Hard Dailies doing an Easy amount of damage from boss.
- # Easy is task.priority = 1. Anything < 1 will be Trivial (0.1) or any future
- # setting between Trivial and Easy.
-
- (task.history ?= []).push({ date: +new Date, value: task.value })
- task.completed = false
- if completed || (scheduleMisses > 0)
- _.each task.checklist, ((i)->i.completed=false;true) # this should not happen for grey tasks unless they are completed
-
- user.habits.forEach (task) -> # slowly reset 'onlies' value to 0
- if task.up is false or task.down is false
- if Math.abs(task.value) < 0.1
- task.value = 0
- else
- task.value = task.value / 2
-
- # Finished tallying
- ((user.history ?= {}).todos ?= []).push { date: now, value: todoTally }
- # tally experience
- expTally = user.stats.exp
- lvl = 0 #iterator
- while lvl < (user.stats.lvl - 1)
- lvl++
- expTally += api.tnl(lvl)
- (user.history.exp ?= []).push { date: now, value: expTally }
-
- # premium subscribers can keep their full history.
- # TODO figure out something performance-wise
- unless user.purchased?.plan?.customerId
- user.fns.preenUserHistory()
- user.markModified? 'history'
- user.markModified? 'dailys' # covers dailys.*.history
-
- user.stats.buffs =
- if perfect
- user.achievements.perfect ?= 0
- user.achievements.perfect++
- lvlDiv2 = Math.ceil(api.capByLevel(user.stats.lvl) / 2)
- {str:lvlDiv2,int:lvlDiv2,per:lvlDiv2,con:lvlDiv2,stealth:0,streaks:false}
- else clearBuffs
-
- # Add 10 MP, or 10% of max MP if that'd be more. Perform this after Perfect Day for maximum benefit
- # Adjust for fraction of dailies completed
- dailyChecked=1 if dailyDueUnchecked is 0 and dailyChecked is 0
- user.stats.mp += _.max([10,.1 * user._statsComputed.maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked)
- user.stats.mp = user._statsComputed.maxMP if user.stats.mp > user._statsComputed.maxMP
-
- # After all is said and done, progress up user's effect on quest, return those values & reset the user's
- progress = user.party.quest.progress; _progress = _.cloneDeep progress
- _.merge progress, {down:0,up:0}
- progress.collect = _.transform progress.collect, ((m,v,k)->m[k]=0)
-
- # Analytics
- user.flags.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
- }
- options.analytics?.track('Cron', analyticsData)
-
- _progress
-
- # Registered users with some history
- preenUserHistory: (minHistLen = 7) ->
- _.each user.habits.concat(user.dailys), (task) ->
- task.history = preenHistory(task.history) if task.history?.length > minHistLen
- true
-
- _.defaults user.history, {todos:[], exp: []}
- user.history.exp = preenHistory(user.history.exp) if user.history.exp.length > minHistLen
- user.history.todos = preenHistory(user.history.todos) if user.history.todos.length > minHistLen
- #user.markModified? 'history'
- #user.markModified? 'habits'
- #user.markModified? 'dailys'
-
- # ----------------------------------------------------------------------
- # Achievements
- # ----------------------------------------------------------------------
- ultimateGear: ->
- # on the server this is a Lodash transform, on the client its an object
- owned = if window? then user.items.gear.owned else user.items.gear.owned.toObject()
- user.achievements.ultimateGearSets ?= {healer: false, wizard: false, rogue: false, warrior: false}
- content.classes.forEach (klass) ->
- if user.achievements.ultimateGearSets[klass] isnt true
- user.achievements.ultimateGearSets[klass] = _.reduce ['armor', 'shield', 'head', 'weapon'], (soFarGood, type) ->
- found = _.find content.gear.tree[type][klass], {last:true}
- soFarGood and (!found or owned[found.key]==true) #!found only true when weapon is two-handed (mages)
- , true # start with true, else `and` will fail right away
- user.markModified? 'achievements.ultimateGearSets'
- if _.contains(user.achievements.ultimateGearSets, true) and user.flags.armoireEnabled != true
- user.flags.armoireEnabled = true
- user.markModified? 'flags'
-
- nullify: ->
- user.ops = null
- user.fns = null
- user = null
-
- # ----------------------------------------------------------------------
- # Virtual Attributes
- # ----------------------------------------------------------------------
-
- # Aggregate all intrinsic stats, buffs, weapon, & armor into computed stats
- Object.defineProperty user, '_statsComputed',
- get: ->
- computed = _.reduce ['per','con','str','int'], (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'), (m2,path) =>
- val = user.fns.dotGet(path)
- m2 +
- if ~path.indexOf('items.gear')
- # get the gear stat, and multiply it by 1.5 if it's class-gear
- item = content.gear.flat[val]
- (+item?[stat] or 0) * (if item?.klass is user.stats.class || item?.specialClass is user.stats.class then 1.5 else 1)
- else
- +val[stat] or 0
- , 0
- m[stat] += Math.floor(api.capByLevel(user.stats.lvl) / 2)
- m
- , {}
- computed.maxMP = computed.int*2 + 30
- computed
- Object.defineProperty user, 'tasks',
- get: ->
- tasks = user.habits.concat(user.dailys).concat(user.todos).concat(user.rewards)
- _.object(_.pluck(tasks, "id"), tasks)
diff --git a/common/script/index.js b/common/script/index.js
new file mode 100644
index 0000000000..6a3558f937
--- /dev/null
+++ b/common/script/index.js
@@ -0,0 +1,2864 @@
+var $w, _, api, content, i18n, moment, preenHistory, sanitizeOptions, sortOrder,
+ indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+
+moment = require('moment');
+
+_ = require('lodash');
+
+content = require('./content/index');
+
+i18n = require('./i18n');
+
+api = module.exports = {};
+
+api.i18n = i18n;
+
+$w = api.$w = function(s) {
+ return s.split(' ');
+};
+
+api.dotSet = function(obj, path, val) {
+ var arr;
+ arr = path.split('.');
+ return _.reduce(arr, (function(_this) {
+ return function(curr, next, index) {
+ if ((arr.length - 1) === index) {
+ curr[next] = val;
+ }
+ return curr[next] != null ? curr[next] : curr[next] = {};
+ };
+ })(this), obj);
+};
+
+api.dotGet = function(obj, path) {
+ return _.reduce(path.split('.'), ((function(_this) {
+ return function(curr, next) {
+ return curr != null ? curr[next] : void 0;
+ };
+ })(this)), obj);
+};
+
+
+/*
+ Reflists are arrays, but stored as objects. Mongoose has a helluvatime working with arrays (the main problem for our
+ syncing issues) - so the goal is to move away from arrays to objects, since mongoose can reference elements by ID
+ no problem. To maintain sorting, we use these helper functions:
+ */
+
+api.refPush = function(reflist, item, prune) {
+ if (prune == null) {
+ prune = 0;
+ }
+ item.sort = _.isEmpty(reflist) ? 0 : _.max(reflist, 'sort').sort + 1;
+ if (!(item.id && !reflist[item.id])) {
+ item.id = api.uuid();
+ }
+ return reflist[item.id] = item;
+};
+
+api.planGemLimits = {
+ convRate: 20,
+ convCap: 25
+};
+
+
+/*
+ ------------------------------------------------------
+ Time / Day
+ ------------------------------------------------------
+ */
+
+
+/*
+ Each time we're performing date math (cron, task-due-days, etc), we need to take user preferences into consideration.
+ Specifically {dayStart} (custom day start) and {timezoneOffset}. This function sanitizes / defaults those values.
+ {now} is also passed in for various purposes, one example being the test scripts scripts testing different "now" times
+ */
+
+sanitizeOptions = function(o) {
+ var dayStart, now, ref, timezoneOffset;
+ dayStart = !_.isNaN(+o.dayStart) && (0 <= (ref = +o.dayStart) && ref <= 24) ? +o.dayStart : 0;
+ timezoneOffset = o.timezoneOffset ? +o.timezoneOffset : +moment().zone();
+ now = o.now ? moment(o.now).zone(timezoneOffset) : moment(+(new Date)).zone(timezoneOffset);
+ return {
+ dayStart: dayStart,
+ timezoneOffset: timezoneOffset,
+ now: now
+ };
+};
+
+api.startOfWeek = api.startOfWeek = function(options) {
+ var o;
+ if (options == null) {
+ options = {};
+ }
+ o = sanitizeOptions(options);
+ return moment(o.now).startOf('week');
+};
+
+api.startOfDay = function(options) {
+ var dayStart, o;
+ if (options == null) {
+ options = {};
+ }
+ o = sanitizeOptions(options);
+ dayStart = moment(o.now).startOf('day').add({
+ hours: o.dayStart
+ });
+ if (moment(o.now).hour() < o.dayStart) {
+ dayStart.subtract({
+ days: 1
+ });
+ }
+ return dayStart;
+};
+
+api.dayMapping = {
+ 0: 'su',
+ 1: 'm',
+ 2: 't',
+ 3: 'w',
+ 4: 'th',
+ 5: 'f',
+ 6: 's'
+};
+
+
+/*
+ Absolute diff from "yesterday" till now
+ */
+
+api.daysSince = function(yesterday, options) {
+ var o;
+ if (options == null) {
+ options = {};
+ }
+ o = sanitizeOptions(options);
+ return api.startOfDay(_.defaults({
+ now: o.now
+ }, o)).diff(api.startOfDay(_.defaults({
+ now: yesterday
+ }, o)), 'days');
+};
+
+
+/*
+ Should the user do this task on this date, given the task's repeat options and user.preferences.dayStart?
+ */
+
+api.shouldDo = function(day, dailyTask, options) {
+ var dayOfWeekCheck, dayOfWeekNum, daysSinceTaskStart, everyXCheck, o, startOfDayWithCDSTime, taskStartDate;
+ if (options == null) {
+ options = {};
+ }
+ if (dailyTask.type !== 'daily') {
+ return false;
+ }
+ o = sanitizeOptions(options);
+ startOfDayWithCDSTime = api.startOfDay(_.defaults({
+ now: day
+ }, o));
+ taskStartDate = moment(dailyTask.startDate).zone(o.timezoneOffset);
+ taskStartDate = moment(taskStartDate).startOf('day');
+ if (taskStartDate > startOfDayWithCDSTime.startOf('day')) {
+ return false;
+ }
+ if (dailyTask.frequency === 'daily') {
+ if (!dailyTask.everyX) {
+ return false;
+ }
+ daysSinceTaskStart = startOfDayWithCDSTime.startOf('day').diff(taskStartDate, 'days');
+ everyXCheck = daysSinceTaskStart % dailyTask.everyX === 0;
+ return everyXCheck;
+ } else if (dailyTask.frequency === 'weekly') {
+ if (!dailyTask.repeat) {
+ return false;
+ }
+ dayOfWeekNum = startOfDayWithCDSTime.day();
+ dayOfWeekCheck = dailyTask.repeat[api.dayMapping[dayOfWeekNum]];
+ return dayOfWeekCheck;
+ } else {
+ return false;
+ }
+};
+
+
+/*
+ ------------------------------------------------------
+ Level cap
+ ------------------------------------------------------
+ */
+
+api.maxLevel = 100;
+
+api.capByLevel = function(lvl) {
+ if (lvl > api.maxLevel) {
+ return api.maxLevel;
+ } else {
+ return lvl;
+ }
+};
+
+
+/*
+ ------------------------------------------------------
+ Health cap
+ ------------------------------------------------------
+ */
+
+api.maxHealth = 50;
+
+
+/*
+ ------------------------------------------------------
+ Scoring
+ ------------------------------------------------------
+ */
+
+api.tnl = function(lvl) {
+ return Math.round(((Math.pow(lvl, 2) * 0.25) + (10 * lvl) + 139.75) / 10) * 10;
+};
+
+
+/*
+ A hyperbola function that creates diminishing returns, so you can't go to infinite (eg, with Exp gain).
+ {max} The asymptote
+ {bonus} All the numbers combined for your point bonus (eg, task.value * user.stats.int * critChance, etc)
+ {halfway} (optional) the point at which the graph starts bending
+ */
+
+api.diminishingReturns = function(bonus, max, halfway) {
+ if (halfway == null) {
+ halfway = max / 2;
+ }
+ return max * (bonus / (bonus + halfway));
+};
+
+api.monod = function(bonus, rateOfIncrease, max) {
+ return rateOfIncrease * max * bonus / (rateOfIncrease * bonus + max);
+};
+
+
+/*
+Preen history for users with > 7 history entries
+This takes an infinite array of single day entries [day day day day day...], and turns it into a condensed array
+of averages, condensing more the further back in time we go. Eg, 7 entries each for last 7 days; 1 entry each week
+of this month; 1 entry for each month of this year; 1 entry per previous year: [day*7 week*4 month*12 year*infinite]
+ */
+
+preenHistory = function(history) {
+ var newHistory, preen, thisMonth;
+ history = _.filter(history, function(h) {
+ return !!h;
+ });
+ newHistory = [];
+ preen = function(amount, groupBy) {
+ var groups;
+ groups = _.chain(history).groupBy(function(h) {
+ return moment(h.date).format(groupBy);
+ }).sortBy(function(h, k) {
+ return k;
+ }).value();
+ groups = groups.slice(-amount);
+ groups.pop();
+ return _.each(groups, function(group) {
+ newHistory.push({
+ date: moment(group[0].date).toDate(),
+ value: _.reduce(group, (function(m, obj) {
+ return m + obj.value;
+ }), 0) / group.length
+ });
+ return true;
+ });
+ };
+ preen(50, "YYYY");
+ preen(moment().format('MM'), "YYYYMM");
+ thisMonth = moment().format('YYYYMM');
+ newHistory = newHistory.concat(_.filter(history, function(h) {
+ return moment(h.date).format('YYYYMM') === thisMonth;
+ }));
+ return newHistory;
+};
+
+
+/*
+ Preen 3-day past-completed To-Dos from Angular & mobile app
+ */
+
+api.preenTodos = function(tasks) {
+ return _.where(tasks, function(t) {
+ return !t.completed || (t.challenge && t.challenge.id) || moment(t.dateCompleted).isAfter(moment().subtract({
+ days: 3
+ }));
+ });
+};
+
+
+/*
+ Update the in-browser store with new gear. FIXME this was in user.fns, but it was causing strange issues there
+ */
+
+sortOrder = _.reduce(content.gearTypes, (function(m, v, k) {
+ m[v] = k;
+ return m;
+}), {});
+
+api.updateStore = function(user) {
+ var changes;
+ if (!user) {
+ return;
+ }
+ changes = [];
+ _.each(content.gearTypes, function(type) {
+ var found;
+ found = _.find(content.gear.tree[type][user.stats["class"]], function(item) {
+ return !user.items.gear.owned[item.key];
+ });
+ if (found) {
+ changes.push(found);
+ }
+ return true;
+ });
+ changes = changes.concat(_.filter(content.gear.flat, function(v) {
+ var ref;
+ return ((ref = v.klass) === 'special' || ref === 'mystery' || ref === 'armoire') && !user.items.gear.owned[v.key] && (typeof v.canOwn === "function" ? v.canOwn(user) : void 0);
+ }));
+ return _.sortBy(changes, function(c) {
+ return sortOrder[c.type];
+ });
+};
+
+
+/*
+------------------------------------------------------
+Content
+------------------------------------------------------
+ */
+
+api.content = content;
+
+
+/*
+------------------------------------------------------
+Misc Helpers
+------------------------------------------------------
+ */
+
+api.uuid = function() {
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
+ var r, v;
+ r = Math.random() * 16 | 0;
+ v = (c === "x" ? r : r & 0x3 | 0x8);
+ return v.toString(16);
+ });
+};
+
+api.countExists = function(items) {
+ return _.reduce(items, (function(m, v) {
+ return m + (v ? 1 : 0);
+ }), 0);
+};
+
+
+/*
+Even though Mongoose handles task defaults, we want to make sure defaults are set on the client-side before
+sending up to the server for performance
+ */
+
+api.taskDefaults = function(task) {
+ var defaults, ref, ref1, ref2;
+ if (task == null) {
+ task = {};
+ }
+ if (!(task.type && ((ref = task.type) === 'habit' || ref === 'daily' || ref === 'todo' || ref === 'reward'))) {
+ task.type = 'habit';
+ }
+ defaults = {
+ id: api.uuid(),
+ text: task.id != null ? task.id : '',
+ notes: '',
+ priority: 1,
+ challenge: {},
+ attribute: 'str',
+ dateCreated: new Date()
+ };
+ _.defaults(task, defaults);
+ if (task.type === 'habit') {
+ _.defaults(task, {
+ up: true,
+ down: true
+ });
+ }
+ if ((ref1 = task.type) === 'habit' || ref1 === 'daily') {
+ _.defaults(task, {
+ history: []
+ });
+ }
+ if ((ref2 = task.type) === 'daily' || ref2 === 'todo') {
+ _.defaults(task, {
+ completed: false
+ });
+ }
+ if (task.type === 'daily') {
+ _.defaults(task, {
+ streak: 0,
+ repeat: {
+ su: true,
+ m: true,
+ t: true,
+ w: true,
+ th: true,
+ f: true,
+ s: true
+ }
+ }, {
+ startDate: new Date(),
+ everyX: 1,
+ frequency: 'weekly'
+ });
+ }
+ task._id = task.id;
+ if (task.value == null) {
+ task.value = task.type === 'reward' ? 10 : 0;
+ }
+ if (!_.isNumber(task.priority)) {
+ task.priority = 1;
+ }
+ return task;
+};
+
+api.percent = function(x, y, dir) {
+ var roundFn;
+ switch (dir) {
+ case "up":
+ roundFn = Math.ceil;
+ break;
+ case "down":
+ roundFn = Math.floor;
+ break;
+ default:
+ roundFn = Math.round;
+ }
+ if (x === 0) {
+ x = 1;
+ }
+ return Math.max(0, roundFn(x / y * 100));
+};
+
+
+/*
+Remove whitespace #FIXME are we using this anywwhere? Should we be?
+ */
+
+api.removeWhitespace = function(str) {
+ if (!str) {
+ return '';
+ }
+ return str.replace(/\s/g, '');
+};
+
+
+/*
+Encode the download link for .ics iCal file
+ */
+
+api.encodeiCalLink = function(uid, apiToken) {
+ var loc, ref;
+ loc = (typeof window !== "undefined" && window !== null ? window.location.host : void 0) || (typeof process !== "undefined" && process !== null ? (ref = process.env) != null ? ref.BASE_URL : void 0 : void 0) || '';
+ return encodeURIComponent("http://" + loc + "/v1/users/" + uid + "/calendar.ics?apiToken=" + apiToken);
+};
+
+
+/*
+Gold amount from their money
+ */
+
+api.gold = function(num) {
+ if (num) {
+ return Math.floor(num);
+ } else {
+ return "0";
+ }
+};
+
+
+/*
+Silver amount from their money
+ */
+
+api.silver = function(num) {
+ if (num) {
+ return ("0" + Math.floor((num - Math.floor(num)) * 100)).slice(-2);
+ } else {
+ return "00";
+ }
+};
+
+
+/*
+Task classes given everything about the class
+ */
+
+api.taskClasses = function(task, filters, dayStart, lastCron, showCompleted, main) {
+ var classes, completed, enabled, filter, priority, ref, repeat, type, value;
+ if (filters == null) {
+ filters = [];
+ }
+ if (dayStart == null) {
+ dayStart = 0;
+ }
+ if (lastCron == null) {
+ lastCron = +(new Date);
+ }
+ if (showCompleted == null) {
+ showCompleted = false;
+ }
+ if (main == null) {
+ main = false;
+ }
+ if (!task) {
+ return;
+ }
+ type = task.type, completed = task.completed, value = task.value, repeat = task.repeat, priority = task.priority;
+ if (main) {
+ if (!task._editing) {
+ for (filter in filters) {
+ enabled = filters[filter];
+ if (enabled && !((ref = task.tags) != null ? ref[filter] : void 0)) {
+ return 'hidden';
+ }
+ }
+ }
+ }
+ classes = type;
+ if (task._editing) {
+ classes += " beingEdited";
+ }
+ if (type === 'todo' || type === 'daily') {
+ if (completed || (type === 'daily' && !api.shouldDo(+(new Date), task, {
+ dayStart: dayStart
+ }))) {
+ classes += " completed";
+ } else {
+ classes += " uncompleted";
+ }
+ } else if (type === 'habit') {
+ if (task.down && task.up) {
+ classes += ' habit-wide';
+ }
+ if (!task.down && !task.up) {
+ classes += ' habit-narrow';
+ }
+ }
+ if (priority === 0.1) {
+ classes += ' difficulty-trivial';
+ } else if (priority === 1) {
+ classes += ' difficulty-easy';
+ } else if (priority === 1.5) {
+ classes += ' difficulty-medium';
+ } else if (priority === 2) {
+ classes += ' difficulty-hard';
+ }
+ if (value < -20) {
+ classes += ' color-worst';
+ } else if (value < -10) {
+ classes += ' color-worse';
+ } else if (value < -1) {
+ classes += ' color-bad';
+ } else if (value < 1) {
+ classes += ' color-neutral';
+ } else if (value < 5) {
+ classes += ' color-good';
+ } else if (value < 10) {
+ classes += ' color-better';
+ } else {
+ classes += ' color-best';
+ }
+ return classes;
+};
+
+
+/*
+Friendly timestamp
+ */
+
+api.friendlyTimestamp = function(timestamp) {
+ return moment(timestamp).format('MM/DD h:mm:ss a');
+};
+
+
+/*
+Does user have new chat messages?
+ */
+
+api.newChatMessages = function(messages, lastMessageSeen) {
+ if (!((messages != null ? messages.length : void 0) > 0)) {
+ return false;
+ }
+ return (messages != null ? messages[0] : void 0) && (messages[0].id !== lastMessageSeen);
+};
+
+
+/*
+are any tags active?
+ */
+
+api.noTags = function(tags) {
+ return _.isEmpty(tags) || _.isEmpty(_.filter(tags, function(t) {
+ return t;
+ }));
+};
+
+
+/*
+Are there tags applied?
+ */
+
+api.appliedTags = function(userTags, taskTags) {
+ var arr;
+ arr = [];
+ _.each(userTags, function(t) {
+ if (t == null) {
+ return;
+ }
+ if (taskTags != null ? taskTags[t.id] : void 0) {
+ return arr.push(t.name);
+ }
+ });
+ return arr.join(', ');
+};
+
+
+/*
+Various counting functions
+ */
+
+api.count = require('./count');
+
+
+/*
+------------------------------------------------------
+User (prototype wrapper to give it ops, helper funcs, and virtuals
+------------------------------------------------------
+ */
+
+
+/*
+User is now wrapped (both on client and server), adding a few new properties:
+ * getters (_statsComputed, tasks, etc)
+ * user.fns, which is a bunch of helper functions
+ These were originally up above, but they make more sense belonging to the user object so we don't have to pass
+ the user object all over the place. In fact, we should pull in more functions such as cron(), updateStats(), etc.
+ * user.ops, which is super important:
+
+If a function is inside user.ops, it has magical properties. If you call it on the client it updates the user object in
+the browser and when it's done it automatically POSTs to the server, calling src/controllers/user.js#OP_NAME (the exact same name
+of the op function). The first argument req is {query, body, params}, it's what the express controller function
+expects. This means we call our functions as if we were calling an Express route. Eg, instead of score(task, direction),
+we call score({params:{id:task.id,direction:direction}}). This also forces us to think about our routes (whether to use
+params, query, or body for variables). see http://stackoverflow.com/questions/4024271/rest-api-best-practices-where-to-put-parameters
+
+If `src/controllers/user.js#OP_NAME` doesn't exist on the server, it's automatically added. It runs the code in user.ops.OP_NAME
+to update the user model server-side, then performs `user.save()`. You can see this in action for `user.ops.buy`. That
+function doesn't exist on the server - so the client calls it, it updates user in the browser, auto-POSTs to server, server
+handles it by calling `user.ops.buy` again (to update user on the server), and then saves. We can do this for
+everything that doesn't need any code difference from what's in user.ops.OP_NAME for special-handling server-side. If we
+*do* need special handling, just add `src/controllers/user.js#OP_NAME` to override the user.ops.OP_NAME, and be
+sure to call user.ops.OP_NAME at some point within the overridden function.
+
+TODO
+ * Is this the best way to wrap the user object? I thought of using user.prototype, but user is an object not a Function.
+ user on the server is a Mongoose model, so we can use prototype - but to do it on the client, we'd probably have to
+ move to $resource for user
+ * Move to $resource!
+ */
+
+api.wrap = function(user, main) {
+ if (main == null) {
+ main = true;
+ }
+ if (user._wrapped) {
+ return;
+ }
+ user._wrapped = true;
+ if (main) {
+ user.ops = {
+ update: function(req, cb) {
+ _.each(req.body, function(v, k) {
+ user.fns.dotSet(k, v);
+ return true;
+ });
+ return typeof cb === "function" ? cb(null, user) : void 0;
+ },
+ sleep: function(req, cb) {
+ user.preferences.sleep = !user.preferences.sleep;
+ return typeof cb === "function" ? cb(null, {}) : void 0;
+ },
+ revive: function(req, cb, analytics) {
+ var analyticsData, base, cl, gearOwned, item, losableItems, lostItem, lostStat;
+ if (!(user.stats.hp <= 0)) {
+ return typeof cb === "function" ? cb({
+ code: 400,
+ message: "Cannot revive if not dead"
+ }) : void 0;
+ }
+ _.merge(user.stats, {
+ hp: 50,
+ exp: 0,
+ gp: 0
+ });
+ if (user.stats.lvl > 1) {
+ user.stats.lvl--;
+ }
+ lostStat = user.fns.randomVal(_.reduce(['str', 'con', 'per', 'int'], (function(m, k) {
+ if (user.stats[k]) {
+ m[k] = k;
+ }
+ return m;
+ }), {}));
+ if (lostStat) {
+ user.stats[lostStat]--;
+ }
+ cl = user.stats["class"];
+ gearOwned = (typeof (base = user.items.gear.owned).toObject === "function" ? base.toObject() : void 0) || user.items.gear.owned;
+ losableItems = {};
+ _.each(gearOwned, function(v, k) {
+ var itm;
+ if (v) {
+ itm = content.gear.flat['' + k];
+ if (itm) {
+ if ((itm.value > 0 || k === 'weapon_warrior_0') && (itm.klass === cl || (itm.klass === 'special' && (!itm.specialClass || itm.specialClass === cl)) || itm.klass === 'armoire')) {
+ return losableItems['' + k] = '' + k;
+ }
+ }
+ }
+ });
+ lostItem = user.fns.randomVal(losableItems);
+ if (item = content.gear.flat[lostItem]) {
+ user.items.gear.owned[lostItem] = false;
+ if (user.items.gear.equipped[item.type] === lostItem) {
+ user.items.gear.equipped[item.type] = item.type + "_base_0";
+ }
+ if (user.items.gear.costume[item.type] === lostItem) {
+ user.items.gear.costume[item.type] = item.type + "_base_0";
+ }
+ }
+ if (typeof user.markModified === "function") {
+ user.markModified('items.gear');
+ }
+ analyticsData = {
+ uuid: user._id,
+ lostItem: lostItem,
+ gaLabel: lostItem,
+ category: 'behavior'
+ };
+ if (analytics != null) {
+ analytics.track('Death', analyticsData);
+ }
+ return typeof cb === "function" ? cb((item ? {
+ code: 200,
+ message: i18n.t('messageLostItem', {
+ itemText: item.text(req.language)
+ }, req.language)
+ } : null), user) : void 0;
+ },
+ reset: function(req, cb) {
+ var gear;
+ user.habits = [];
+ user.dailys = [];
+ user.todos = [];
+ user.rewards = [];
+ user.stats.hp = 50;
+ user.stats.lvl = 1;
+ user.stats.gp = 0;
+ user.stats.exp = 0;
+ gear = user.items.gear;
+ _.each(['equipped', 'costume'], function(type) {
+ gear[type].armor = 'armor_base_0';
+ gear[type].weapon = 'weapon_base_0';
+ gear[type].head = 'head_base_0';
+ return gear[type].shield = 'shield_base_0';
+ });
+ if (typeof gear.owned === 'undefined') {
+ gear.owned = {};
+ }
+ _.each(gear.owned, function(v, k) {
+ if (gear.owned[k]) {
+ gear.owned[k] = false;
+ }
+ return true;
+ });
+ gear.owned.weapon_warrior_0 = true;
+ if (typeof user.markModified === "function") {
+ user.markModified('items.gear.owned');
+ }
+ user.preferences.costume = false;
+ return typeof cb === "function" ? cb(null, user) : void 0;
+ },
+ reroll: function(req, cb, analytics) {
+ var analyticsData;
+ if (user.balance < 1) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: i18n.t('notEnoughGems', req.language)
+ }) : void 0;
+ }
+ user.balance--;
+ _.each(user.tasks, function(task) {
+ if (task.type !== 'reward') {
+ return task.value = 0;
+ }
+ });
+ user.stats.hp = 50;
+ analyticsData = {
+ uuid: user._id,
+ acquireMethod: 'Gems',
+ gemCost: 4,
+ category: 'behavior'
+ };
+ if (analytics != null) {
+ analytics.track('Fortify Potion', analyticsData);
+ }
+ return typeof cb === "function" ? cb(null, user) : void 0;
+ },
+ rebirth: function(req, cb, analytics) {
+ var analyticsData, flags, gear, lvl, stats;
+ if (user.balance < 2 && user.stats.lvl < api.maxLevel) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: i18n.t('notEnoughGems', req.language)
+ }) : void 0;
+ }
+ analyticsData = {
+ uuid: user._id,
+ category: 'behavior'
+ };
+ if (user.stats.lvl < api.maxLevel) {
+ user.balance -= 2;
+ analyticsData.acquireMethod = 'Gems';
+ analyticsData.gemCost = 8;
+ } else {
+ analyticsData.gemCost = 0;
+ analyticsData.acquireMethod = '> 100';
+ }
+ if (analytics != null) {
+ analytics.track('Rebirth', analyticsData);
+ }
+ lvl = api.capByLevel(user.stats.lvl);
+ _.each(user.tasks, function(task) {
+ if (task.type !== 'reward') {
+ task.value = 0;
+ }
+ if (task.type === 'daily') {
+ return task.streak = 0;
+ }
+ });
+ stats = user.stats;
+ stats.buffs = {};
+ stats.hp = 50;
+ stats.lvl = 1;
+ stats["class"] = 'warrior';
+ _.each(['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp'], function(value) {
+ return stats[value] = 0;
+ });
+ gear = user.items.gear;
+ _.each(['equipped', 'costume'], function(type) {
+ gear[type] = {};
+ gear[type].armor = 'armor_base_0';
+ gear[type].weapon = 'weapon_warrior_0';
+ gear[type].head = 'head_base_0';
+ return gear[type].shield = 'shield_base_0';
+ });
+ if (user.items.currentPet) {
+ user.ops.equip({
+ params: {
+ type: 'pet',
+ key: user.items.currentPet
+ }
+ });
+ }
+ if (user.items.currentMount) {
+ user.ops.equip({
+ params: {
+ type: 'mount',
+ key: user.items.currentMount
+ }
+ });
+ }
+ _.each(gear.owned, function(v, k) {
+ if (gear.owned[k] && content.gear.flat[k].value) {
+ gear.owned[k] = false;
+ return true;
+ }
+ });
+ gear.owned.weapon_warrior_0 = true;
+ if (typeof user.markModified === "function") {
+ user.markModified('items.gear.owned');
+ }
+ user.preferences.costume = false;
+ flags = user.flags;
+ if (!user.achievements.beastMaster) {
+ flags.rebirthEnabled = false;
+ }
+ flags.itemsEnabled = false;
+ flags.dropsEnabled = false;
+ flags.classSelected = false;
+ flags.levelDrops = {};
+ if (!user.achievements.rebirths) {
+ user.achievements.rebirths = 1;
+ user.achievements.rebirthLevel = lvl;
+ } else if (lvl > user.achievements.rebirthLevel || lvl === 100) {
+ user.achievements.rebirths++;
+ user.achievements.rebirthLevel = lvl;
+ }
+ user.stats.buffs = {};
+ return typeof cb === "function" ? cb(null, user) : void 0;
+ },
+ allocateNow: function(req, cb) {
+ _.times(user.stats.points, user.fns.autoAllocate);
+ user.stats.points = 0;
+ if (typeof user.markModified === "function") {
+ user.markModified('stats');
+ }
+ return typeof cb === "function" ? cb(null, user.stats) : void 0;
+ },
+ clearCompleted: function(req, cb) {
+ _.remove(user.todos, function(t) {
+ var ref;
+ return t.completed && !((ref = t.challenge) != null ? ref.id : void 0);
+ });
+ if (typeof user.markModified === "function") {
+ user.markModified('todos');
+ }
+ return typeof cb === "function" ? cb(null, user.todos) : void 0;
+ },
+ sortTask: function(req, cb) {
+ var from, id, movedTask, preenedTasks, ref, task, tasks, to;
+ id = req.params.id;
+ ref = req.query, to = ref.to, from = ref.from;
+ task = user.tasks[id];
+ if (!task) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: i18n.t('messageTaskNotFound', req.language)
+ }) : void 0;
+ }
+ if (!((to != null) && (from != null))) {
+ return typeof cb === "function" ? cb('?to=__&from=__ are required') : void 0;
+ }
+ tasks = user[task.type + "s"];
+ if (task.type === 'todo' && tasks[from] !== task) {
+ preenedTasks = api.preenTodos(tasks);
+ if (to !== -1) {
+ to = tasks.indexOf(preenedTasks[to]);
+ }
+ from = tasks.indexOf(preenedTasks[from]);
+ }
+ if (tasks[from] !== task) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: i18n.t('messageTaskNotFound', req.language)
+ }) : void 0;
+ }
+ movedTask = tasks.splice(from, 1)[0];
+ if (to === -1) {
+ tasks.push(movedTask);
+ } else {
+ tasks.splice(to, 0, movedTask);
+ }
+ return typeof cb === "function" ? cb(null, tasks) : void 0;
+ },
+ updateTask: function(req, cb) {
+ var ref, task;
+ if (!(task = user.tasks[(ref = req.params) != null ? ref.id : void 0])) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: i18n.t('messageTaskNotFound', req.language)
+ }) : void 0;
+ }
+ _.merge(task, _.omit(req.body, ['checklist', 'id', 'type']));
+ if (req.body.checklist) {
+ task.checklist = req.body.checklist;
+ }
+ if (typeof task.markModified === "function") {
+ task.markModified('tags');
+ }
+ return typeof cb === "function" ? cb(null, task) : void 0;
+ },
+ deleteTask: function(req, cb) {
+ var i, ref, task;
+ task = user.tasks[(ref = req.params) != null ? ref.id : void 0];
+ if (!task) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: i18n.t('messageTaskNotFound', req.language)
+ }) : void 0;
+ }
+ i = user[task.type + "s"].indexOf(task);
+ if (~i) {
+ user[task.type + "s"].splice(i, 1);
+ }
+ return typeof cb === "function" ? cb(null, {}) : void 0;
+ },
+ addTask: function(req, cb) {
+ var task;
+ task = api.taskDefaults(req.body);
+ if (user.tasks[task.id] != null) {
+ return typeof cb === "function" ? cb({
+ code: 409,
+ message: i18n.t('messageDuplicateTaskID', req.language)
+ }) : void 0;
+ }
+ user[task.type + "s"].unshift(task);
+ if (user.preferences.newTaskEdit) {
+ task._editing = true;
+ }
+ if (user.preferences.tagsCollapsed) {
+ task._tags = true;
+ }
+ if (!user.preferences.advancedCollapsed) {
+ task._advanced = true;
+ }
+ if (typeof cb === "function") {
+ cb(null, task);
+ }
+ return task;
+ },
+ addTag: function(req, cb) {
+ if (user.tags == null) {
+ user.tags = [];
+ }
+ user.tags.push({
+ name: req.body.name,
+ id: req.body.id || api.uuid()
+ });
+ return typeof cb === "function" ? cb(null, user.tags) : void 0;
+ },
+ sortTag: function(req, cb) {
+ var from, ref, to;
+ ref = req.query, to = ref.to, from = ref.from;
+ if (!((to != null) && (from != null))) {
+ return typeof cb === "function" ? cb('?to=__&from=__ are required') : void 0;
+ }
+ user.tags.splice(to, 0, user.tags.splice(from, 1)[0]);
+ return typeof cb === "function" ? cb(null, user.tags) : void 0;
+ },
+ getTags: function(req, cb) {
+ return typeof cb === "function" ? cb(null, user.tags) : void 0;
+ },
+ getTag: function(req, cb) {
+ var i, tid;
+ tid = req.params.id;
+ i = _.findIndex(user.tags, {
+ id: tid
+ });
+ if (!~i) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: i18n.t('messageTagNotFound', req.language)
+ }) : void 0;
+ }
+ return typeof cb === "function" ? cb(null, user.tags[i]) : void 0;
+ },
+ updateTag: function(req, cb) {
+ var i, tid;
+ tid = req.params.id;
+ i = _.findIndex(user.tags, {
+ id: tid
+ });
+ if (!~i) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: i18n.t('messageTagNotFound', req.language)
+ }) : void 0;
+ }
+ user.tags[i].name = req.body.name;
+ return typeof cb === "function" ? cb(null, user.tags[i]) : void 0;
+ },
+ deleteTag: function(req, cb) {
+ var i, tag, tid;
+ tid = req.params.id;
+ i = _.findIndex(user.tags, {
+ id: tid
+ });
+ if (!~i) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: i18n.t('messageTagNotFound', req.language)
+ }) : void 0;
+ }
+ tag = user.tags[i];
+ delete user.filters[tag.id];
+ user.tags.splice(i, 1);
+ _.each(user.tasks, function(task) {
+ return delete task.tags[tag.id];
+ });
+ _.each(['habits', 'dailys', 'todos', 'rewards'], function(type) {
+ return typeof user.markModified === "function" ? user.markModified(type) : void 0;
+ });
+ return typeof cb === "function" ? cb(null, user.tags) : void 0;
+ },
+ addWebhook: function(req, cb) {
+ var wh;
+ wh = user.preferences.webhooks;
+ api.refPush(wh, {
+ url: req.body.url,
+ enabled: req.body.enabled || true,
+ id: req.body.id
+ });
+ if (typeof user.markModified === "function") {
+ user.markModified('preferences.webhooks');
+ }
+ return typeof cb === "function" ? cb(null, user.preferences.webhooks) : void 0;
+ },
+ updateWebhook: function(req, cb) {
+ _.merge(user.preferences.webhooks[req.params.id], req.body);
+ if (typeof user.markModified === "function") {
+ user.markModified('preferences.webhooks');
+ }
+ return typeof cb === "function" ? cb(null, user.preferences.webhooks) : void 0;
+ },
+ deleteWebhook: function(req, cb) {
+ delete user.preferences.webhooks[req.params.id];
+ if (typeof user.markModified === "function") {
+ user.markModified('preferences.webhooks');
+ }
+ return typeof cb === "function" ? cb(null, user.preferences.webhooks) : void 0;
+ },
+ addPushDevice: function(req, cb) {
+ var i, item, pd;
+ if (!user.pushDevices) {
+ user.pushDevices = [];
+ }
+ pd = user.pushDevices;
+ item = {
+ regId: req.body.regId,
+ type: req.body.type
+ };
+ i = _.findIndex(pd, {
+ regId: item.regId
+ });
+ if (i === -1) {
+ pd.push(item);
+ }
+ return typeof cb === "function" ? cb(null, user.pushDevices) : void 0;
+ },
+ clearPMs: function(req, cb) {
+ user.inbox.messages = {};
+ if (typeof user.markModified === "function") {
+ user.markModified('inbox.messages');
+ }
+ return typeof cb === "function" ? cb(null, user.inbox.messages) : void 0;
+ },
+ deletePM: function(req, cb) {
+ delete user.inbox.messages[req.params.id];
+ if (typeof user.markModified === "function") {
+ user.markModified('inbox.messages.' + req.params.id);
+ }
+ return typeof cb === "function" ? cb(null, user.inbox.messages) : void 0;
+ },
+ blockUser: function(req, cb) {
+ var i;
+ i = user.inbox.blocks.indexOf(req.params.uuid);
+ if (~i) {
+ user.inbox.blocks.splice(i, 1);
+ } else {
+ user.inbox.blocks.push(req.params.uuid);
+ }
+ if (typeof user.markModified === "function") {
+ user.markModified('inbox.blocks');
+ }
+ return typeof cb === "function" ? cb(null, user.inbox.blocks) : void 0;
+ },
+ feed: function(req, cb) {
+ var egg, eggText, evolve, food, message, pet, petDisplayName, potion, potionText, ref, ref1, ref2, userPets;
+ ref = req.params, pet = ref.pet, food = ref.food;
+ food = content.food[food];
+ ref1 = pet.split('-'), egg = ref1[0], potion = ref1[1];
+ userPets = user.items.pets;
+ potionText = content.hatchingPotions[potion] ? content.hatchingPotions[potion].text() : potion;
+ eggText = content.eggs[egg] ? content.eggs[egg].text() : egg;
+ petDisplayName = i18n.t('petName', {
+ potion: potionText,
+ egg: eggText
+ });
+ if (!userPets[pet]) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: i18n.t('messagePetNotFound', req.language)
+ }) : void 0;
+ }
+ if (!((ref2 = user.items.food) != null ? ref2[food.key] : void 0)) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: i18n.t('messageFoodNotFound', req.language)
+ }) : void 0;
+ }
+ if (content.specialPets[pet]) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: i18n.t('messageCannotFeedPet', req.language)
+ }) : void 0;
+ }
+ if (user.items.mounts[pet]) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: i18n.t('messageAlreadyMount', req.language)
+ }) : void 0;
+ }
+ message = '';
+ evolve = function() {
+ userPets[pet] = -1;
+ user.items.mounts[pet] = true;
+ if (pet === user.items.currentPet) {
+ user.items.currentPet = "";
+ }
+ return message = i18n.t('messageEvolve', {
+ egg: petDisplayName
+ }, req.language);
+ };
+ if (food.key === 'Saddle') {
+ evolve();
+ } else {
+ if (food.target === potion || content.hatchingPotions[potion].premium) {
+ userPets[pet] += 5;
+ message = i18n.t('messageLikesFood', {
+ egg: petDisplayName,
+ foodText: food.text(req.language)
+ }, req.language);
+ } else {
+ userPets[pet] += 2;
+ message = i18n.t('messageDontEnjoyFood', {
+ egg: petDisplayName,
+ foodText: food.text(req.language)
+ }, req.language);
+ }
+ if (userPets[pet] >= 50 && !user.items.mounts[pet]) {
+ evolve();
+ }
+ }
+ user.items.food[food.key]--;
+ return typeof cb === "function" ? cb({
+ code: 200,
+ message: message
+ }, {
+ value: userPets[pet]
+ }) : void 0;
+ },
+ buySpecialSpell: function(req, cb) {
+ var base, item, key, message;
+ key = req.params.key;
+ item = content.special[key];
+ if (user.stats.gp < item.value) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: i18n.t('messageNotEnoughGold', req.language)
+ }) : void 0;
+ }
+ user.stats.gp -= item.value;
+ if ((base = user.items.special)[key] == null) {
+ base[key] = 0;
+ }
+ user.items.special[key]++;
+ if (typeof user.markModified === "function") {
+ user.markModified('items.special');
+ }
+ message = i18n.t('messageBought', {
+ itemText: item.text(req.language)
+ }, req.language);
+ return typeof cb === "function" ? cb({
+ code: 200,
+ message: message
+ }, _.pick(user, $w('items stats'))) : void 0;
+ },
+ purchase: function(req, cb, analytics) {
+ var analyticsData, convCap, convRate, item, key, price, ref, ref1, ref2, ref3, type;
+ ref = req.params, type = ref.type, key = ref.key;
+ if (type === 'gems' && key === 'gem') {
+ ref1 = api.planGemLimits, convRate = ref1.convRate, convCap = ref1.convCap;
+ convCap += user.purchased.plan.consecutive.gemCapExtra;
+ if (!((ref2 = user.purchased) != null ? (ref3 = ref2.plan) != null ? ref3.customerId : void 0 : void 0)) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: "Must subscribe to purchase gems with GP"
+ }, req) : void 0;
+ }
+ if (!(user.stats.gp >= convRate)) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: "Not enough Gold"
+ }) : void 0;
+ }
+ if (user.purchased.plan.gemsBought >= convCap) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: "You've reached the Gold=>Gem conversion cap (" + convCap + ") for this month. We have this to prevent abuse / farming. The cap will reset within the first three days of next month."
+ }) : void 0;
+ }
+ user.balance += .25;
+ user.purchased.plan.gemsBought++;
+ user.stats.gp -= convRate;
+ analyticsData = {
+ uuid: user._id,
+ itemKey: key,
+ acquireMethod: 'Gold',
+ goldCost: convRate,
+ category: 'behavior'
+ };
+ if (analytics != null) {
+ analytics.track('purchase gems', analyticsData);
+ }
+ return typeof cb === "function" ? cb({
+ code: 200,
+ message: "+1 Gem"
+ }, _.pick(user, $w('stats balance'))) : void 0;
+ }
+ if (type !== 'eggs' && type !== 'hatchingPotions' && type !== 'food' && type !== 'quests' && type !== 'gear') {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: ":type must be in [eggs,hatchingPotions,food,quests,gear]"
+ }, req) : void 0;
+ }
+ if (type === 'gear') {
+ item = content.gear.flat[key];
+ if (user.items.gear.owned[key]) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: i18n.t('alreadyHave', req.language)
+ }) : void 0;
+ }
+ price = (item.twoHanded || item.gearSet === 'animal' ? 2 : 1) / 4;
+ } else {
+ item = content[type][key];
+ price = item.value / 4;
+ }
+ if (!item) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: ":key not found for Content." + type
+ }, req) : void 0;
+ }
+ if (!item.canBuy(user)) {
+ return typeof cb === "function" ? cb({
+ code: 403,
+ message: i18n.t('messageNotAvailable', req.language)
+ }) : void 0;
+ }
+ if ((user.balance < price) || !user.balance) {
+ return typeof cb === "function" ? cb({
+ code: 403,
+ message: i18n.t('notEnoughGems', req.language)
+ }) : void 0;
+ }
+ user.balance -= price;
+ if (type === 'gear') {
+ user.items.gear.owned[key] = true;
+ } else {
+ if (!(user.items[type][key] > 0)) {
+ user.items[type][key] = 0;
+ }
+ user.items[type][key]++;
+ }
+ analyticsData = {
+ uuid: user._id,
+ itemKey: key,
+ itemType: 'Market',
+ acquireMethod: 'Gems',
+ gemCost: item.value,
+ category: 'behavior'
+ };
+ if (analytics != null) {
+ analytics.track('acquire item', analyticsData);
+ }
+ return typeof cb === "function" ? cb(null, _.pick(user, $w('items balance'))) : void 0;
+ },
+ releasePets: function(req, cb, analytics) {
+ var analyticsData, pet;
+ if (user.balance < 1) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: i18n.t('notEnoughGems', req.language)
+ }) : void 0;
+ } else {
+ user.balance -= 1;
+ for (pet in content.pets) {
+ user.items.pets[pet] = 0;
+ }
+ if (!user.achievements.beastMasterCount) {
+ user.achievements.beastMasterCount = 0;
+ }
+ user.achievements.beastMasterCount++;
+ user.items.currentPet = "";
+ }
+ analyticsData = {
+ uuid: user._id,
+ acquireMethod: 'Gems',
+ gemCost: 4,
+ category: 'behavior'
+ };
+ if (analytics != null) {
+ analytics.track('release pets', analyticsData);
+ }
+ return typeof cb === "function" ? cb(null, user) : void 0;
+ },
+ releaseMounts: function(req, cb, analytics) {
+ var analyticsData, mount;
+ if (user.balance < 1) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: i18n.t('notEnoughGems', req.language)
+ }) : void 0;
+ } else {
+ user.balance -= 1;
+ user.items.currentMount = "";
+ for (mount in content.pets) {
+ user.items.mounts[mount] = null;
+ }
+ if (!user.achievements.mountMasterCount) {
+ user.achievements.mountMasterCount = 0;
+ }
+ user.achievements.mountMasterCount++;
+ }
+ analyticsData = {
+ uuid: user._id,
+ acquireMethod: 'Gems',
+ gemCost: 4,
+ category: 'behavior'
+ };
+ if (analytics != null) {
+ analytics.track('release mounts', analyticsData);
+ }
+ return typeof cb === "function" ? cb(null, user) : void 0;
+ },
+ releaseBoth: function(req, cb) {
+ var analyticsData, animal, giveTriadBingo;
+ if (user.balance < 1.5 && !user.achievements.triadBingo) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: i18n.t('notEnoughGems', req.language)
+ }) : void 0;
+ } else {
+ giveTriadBingo = true;
+ if (!user.achievements.triadBingo) {
+ analyticsData = {
+ uuid: user._id,
+ acquireMethod: 'Gems',
+ gemCost: 6,
+ category: 'behavior'
+ };
+ if (typeof analytics !== "undefined" && analytics !== null) {
+ analytics.track('release pets & mounts', analyticsData);
+ }
+ user.balance -= 1.5;
+ }
+ user.items.currentMount = "";
+ user.items.currentPet = "";
+ for (animal in content.pets) {
+ if (user.items.pets[animal] === -1) {
+ giveTriadBingo = false;
+ }
+ user.items.pets[animal] = 0;
+ user.items.mounts[animal] = null;
+ }
+ if (!user.achievements.beastMasterCount) {
+ user.achievements.beastMasterCount = 0;
+ }
+ user.achievements.beastMasterCount++;
+ if (!user.achievements.mountMasterCount) {
+ user.achievements.mountMasterCount = 0;
+ }
+ user.achievements.mountMasterCount++;
+ if (giveTriadBingo) {
+ if (!user.achievements.triadBingoCount) {
+ user.achievements.triadBingoCount = 0;
+ }
+ user.achievements.triadBingoCount++;
+ }
+ }
+ return typeof cb === "function" ? cb(null, user) : void 0;
+ },
+ buy: function(req, cb, analytics) {
+ var analyticsData, armoireExp, armoireResp, armoireResult, base, buyResp, drop, eligibleEquipment, item, key, message, name;
+ key = req.params.key;
+ item = key === 'potion' ? content.potion : key === 'armoire' ? content.armoire : content.gear.flat[key];
+ if (!item) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: "Item '" + key + " not found (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.js)"
+ }) : void 0;
+ }
+ if (user.stats.gp < item.value) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: i18n.t('messageNotEnoughGold', req.language)
+ }) : void 0;
+ }
+ if ((item.canOwn != null) && !item.canOwn(user)) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: "You can't buy this item"
+ }) : void 0;
+ }
+ armoireResp = void 0;
+ if (item.key === 'potion') {
+ user.stats.hp += 15;
+ if (user.stats.hp > 50) {
+ user.stats.hp = 50;
+ }
+ } else if (item.key === 'armoire') {
+ armoireResult = user.fns.predictableRandom(user.stats.gp);
+ eligibleEquipment = _.filter(content.gear.flat, (function(i) {
+ return i.klass === 'armoire' && !user.items.gear.owned[i.key];
+ }));
+ if (!_.isEmpty(eligibleEquipment) && (armoireResult < .6 || !user.flags.armoireOpened)) {
+ eligibleEquipment.sort();
+ drop = user.fns.randomVal(eligibleEquipment);
+ user.items.gear.owned[drop.key] = true;
+ user.flags.armoireOpened = true;
+ message = i18n.t('armoireEquipment', {
+ image: '',
+ dropText: drop.text(req.language)
+ }, req.language);
+ if (api.count.remainingGearInSet(user.items.gear.owned, 'armoire') === 0) {
+ user.flags.armoireEmpty = true;
+ }
+ armoireResp = {
+ type: "gear",
+ dropKey: drop.key,
+ dropText: drop.text(req.language)
+ };
+ } else if ((!_.isEmpty(eligibleEquipment) && armoireResult < .8) || armoireResult < .5) {
+ drop = user.fns.randomVal(_.where(content.food, {
+ canDrop: true
+ }));
+ if ((base = user.items.food)[name = drop.key] == null) {
+ base[name] = 0;
+ }
+ user.items.food[drop.key] += 1;
+ message = i18n.t('armoireFood', {
+ image: '',
+ dropArticle: drop.article,
+ dropText: drop.text(req.language)
+ }, req.language);
+ armoireResp = {
+ type: "food",
+ dropKey: drop.key,
+ dropArticle: drop.article,
+ dropText: drop.text(req.language)
+ };
+ } else {
+ armoireExp = Math.floor(user.fns.predictableRandom(user.stats.exp) * 40 + 10);
+ user.stats.exp += armoireExp;
+ message = i18n.t('armoireExp', req.language);
+ armoireResp = {
+ "type": "experience",
+ "value": armoireExp
+ };
+ }
+ } else {
+ if (user.preferences.autoEquip) {
+ user.items.gear.equipped[item.type] = item.key;
+ }
+ user.items.gear.owned[item.key] = true;
+ message = user.fns.handleTwoHanded(item, null, req);
+ if (message == null) {
+ message = i18n.t('messageBought', {
+ itemText: item.text(req.language)
+ }, req.language);
+ }
+ if (item.last) {
+ user.fns.ultimateGear();
+ }
+ }
+ user.stats.gp -= item.value;
+ analyticsData = {
+ uuid: user._id,
+ itemKey: key,
+ acquireMethod: 'Gold',
+ goldCost: item.value,
+ category: 'behavior'
+ };
+ if (analytics != null) {
+ analytics.track('acquire item', analyticsData);
+ }
+ buyResp = _.pick(user, $w('items achievements stats flags'));
+ if (armoireResp) {
+ buyResp["armoire"] = armoireResp;
+ }
+ return typeof cb === "function" ? cb({
+ code: 200,
+ message: message
+ }, buyResp) : void 0;
+ },
+ buyQuest: function(req, cb, analytics) {
+ var analyticsData, base, item, key, message, name;
+ key = req.params.key;
+ item = content.quests[key];
+ if (!item) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: "Quest '" + key + " not found (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.js)"
+ }) : void 0;
+ }
+ if (!(item.category === 'gold' && item.goldValue)) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: "Quest '" + key + " is not a Gold-purchasable quest (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.js)"
+ }) : void 0;
+ }
+ if (user.stats.gp < item.goldValue) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: i18n.t('messageNotEnoughGold', req.language)
+ }) : void 0;
+ }
+ message = i18n.t('messageBought', {
+ itemText: item.text(req.language)
+ }, req.language);
+ if ((base = user.items.quests)[name = item.key] == null) {
+ base[name] = 0;
+ }
+ user.items.quests[item.key] += 1;
+ user.stats.gp -= item.goldValue;
+ analyticsData = {
+ uuid: user._id,
+ itemKey: item.key,
+ itemType: 'Market',
+ goldCost: item.goldValue,
+ acquireMethod: 'Gold',
+ category: 'behavior'
+ };
+ if (analytics != null) {
+ analytics.track('acquire item', analyticsData);
+ }
+ return typeof cb === "function" ? cb({
+ code: 200,
+ message: message
+ }, user.items.quests) : void 0;
+ },
+ buyMysterySet: function(req, cb, analytics) {
+ var mysterySet, ref;
+ if (!(user.purchased.plan.consecutive.trinkets > 0)) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: i18n.t('notEnoughHourglasses', req.language)
+ }) : void 0;
+ }
+ mysterySet = (ref = content.timeTravelerStore(user.items.gear.owned)) != null ? ref[req.params.key] : void 0;
+ if ((typeof window !== "undefined" && window !== null ? window.confirm : void 0) != null) {
+ if (!window.confirm(i18n.t('hourglassBuyEquipSetConfirm'))) {
+ return;
+ }
+ }
+ if (!mysterySet) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: "Mystery set not found, or set already owned"
+ }) : void 0;
+ }
+ _.each(mysterySet.items, function(i) {
+ var analyticsData;
+ user.items.gear.owned[i.key] = true;
+ analyticsData = {
+ uuid: user._id,
+ itemKey: i.key,
+ itemType: 'Subscriber Gear',
+ acquireMethod: 'Hourglass',
+ category: 'behavior'
+ };
+ return analytics != null ? analytics.track('acquire item', analyticsData) : void 0;
+ });
+ user.purchased.plan.consecutive.trinkets--;
+ return typeof cb === "function" ? cb({
+ code: 200,
+ message: i18n.t('hourglassPurchaseSet', req.language)
+ }, _.pick(user, $w('items purchased.plan.consecutive'))) : void 0;
+ },
+ hourglassPurchase: function(req, cb, analytics) {
+ var analyticsData, key, ref, type;
+ ref = req.params, type = ref.type, key = ref.key;
+ if (!content.timeTravelStable[type]) {
+ return typeof cb === "function" ? cb({
+ code: 403,
+ message: i18n.t('typeNotAllowedHourglass', req.language) + JSON.stringify(_.keys(content.timeTravelStable))
+ }) : void 0;
+ }
+ if (!_.contains(_.keys(content.timeTravelStable[type]), key)) {
+ return typeof cb === "function" ? cb({
+ code: 403,
+ message: i18n.t(type + 'NotAllowedHourglass', req.language)
+ }) : void 0;
+ }
+ if (user.items[type][key]) {
+ return typeof cb === "function" ? cb({
+ code: 403,
+ message: i18n.t(type + 'AlreadyOwned', req.language)
+ }) : void 0;
+ }
+ if (!(user.purchased.plan.consecutive.trinkets > 0)) {
+ return typeof cb === "function" ? cb({
+ code: 403,
+ message: i18n.t('notEnoughHourglasses', req.language)
+ }) : void 0;
+ }
+ user.purchased.plan.consecutive.trinkets--;
+ if (type === 'pets') {
+ user.items.pets[key] = 5;
+ }
+ if (type === 'mounts') {
+ user.items.mounts[key] = true;
+ }
+ analyticsData = {
+ uuid: user._id,
+ itemKey: key,
+ itemType: type,
+ acquireMethod: 'Hourglass',
+ category: 'behavior'
+ };
+ if (analytics != null) {
+ analytics.track('acquire item', analyticsData);
+ }
+ return typeof cb === "function" ? cb({
+ code: 200,
+ message: i18n.t('hourglassPurchase', req.language)
+ }, _.pick(user, $w('items purchased.plan.consecutive'))) : void 0;
+ },
+ sell: function(req, cb) {
+ var key, ref, type;
+ ref = req.params, key = ref.key, type = ref.type;
+ if (type !== 'eggs' && type !== 'hatchingPotions' && type !== 'food') {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: ":type not found. Must bes in [eggs, hatchingPotions, food]"
+ }) : void 0;
+ }
+ if (!user.items[type][key]) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: ":key not found for user.items." + type
+ }) : void 0;
+ }
+ user.items[type][key]--;
+ user.stats.gp += content[type][key].value;
+ return typeof cb === "function" ? cb(null, _.pick(user, $w('stats items'))) : void 0;
+ },
+ equip: function(req, cb) {
+ var item, key, message, ref, type;
+ ref = [req.params.type || 'equipped', req.params.key], type = ref[0], key = ref[1];
+ switch (type) {
+ case 'mount':
+ if (!user.items.mounts[key]) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: ":You do not own this mount."
+ }) : void 0;
+ }
+ user.items.currentMount = user.items.currentMount === key ? '' : key;
+ break;
+ case 'pet':
+ if (!user.items.pets[key]) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: ":You do not own this pet."
+ }) : void 0;
+ }
+ user.items.currentPet = user.items.currentPet === key ? '' : key;
+ break;
+ case 'costume':
+ case 'equipped':
+ item = content.gear.flat[key];
+ if (!user.items.gear.owned[key]) {
+ return typeof cb === "function" ? cb({
+ code: 404,
+ message: ":You do not own this gear."
+ }) : void 0;
+ }
+ if (user.items.gear[type][item.type] === key) {
+ user.items.gear[type][item.type] = item.type + "_base_0";
+ message = i18n.t('messageUnEquipped', {
+ itemText: item.text(req.language)
+ }, req.language);
+ } else {
+ user.items.gear[type][item.type] = item.key;
+ message = user.fns.handleTwoHanded(item, type, req);
+ }
+ if (typeof user.markModified === "function") {
+ user.markModified("items.gear." + type);
+ }
+ }
+ return typeof cb === "function" ? cb((message ? {
+ code: 200,
+ message: message
+ } : null), user.items) : void 0;
+ },
+ hatch: function(req, cb) {
+ var egg, hatchingPotion, pet, ref;
+ ref = req.params, egg = ref.egg, hatchingPotion = ref.hatchingPotion;
+ if (!(egg && hatchingPotion)) {
+ return typeof cb === "function" ? cb({
+ code: 400,
+ message: "Please specify query.egg & query.hatchingPotion"
+ }) : void 0;
+ }
+ if (!(user.items.eggs[egg] > 0 && user.items.hatchingPotions[hatchingPotion] > 0)) {
+ return typeof cb === "function" ? cb({
+ code: 403,
+ message: i18n.t('messageMissingEggPotion', req.language)
+ }) : void 0;
+ }
+ if (content.hatchingPotions[hatchingPotion].premium && !content.dropEggs[egg]) {
+ return typeof cb === "function" ? cb({
+ code: 403,
+ message: i18n.t('messageInvalidEggPotionCombo', req.language)
+ }) : void 0;
+ }
+ pet = egg + "-" + hatchingPotion;
+ if (user.items.pets[pet] && user.items.pets[pet] > 0) {
+ return typeof cb === "function" ? cb({
+ code: 403,
+ message: i18n.t('messageAlreadyPet', req.language)
+ }) : void 0;
+ }
+ user.items.pets[pet] = 5;
+ user.items.eggs[egg]--;
+ user.items.hatchingPotions[hatchingPotion]--;
+ return typeof cb === "function" ? cb({
+ code: 200,
+ message: i18n.t('messageHatched', req.language)
+ }, user.items) : void 0;
+ },
+ unlock: function(req, cb, analytics) {
+ var alreadyOwns, analyticsData, cost, fullSet, k, path, split, v;
+ path = req.query.path;
+ fullSet = ~path.indexOf(",");
+ cost = ~path.indexOf('background.') ? fullSet ? 3.75 : 1.75 : fullSet ? 1.25 : 0.5;
+ alreadyOwns = !fullSet && user.fns.dotGet("purchased." + path) === true;
+ if ((user.balance < cost || !user.balance) && !alreadyOwns) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: i18n.t('notEnoughGems', req.language)
+ }) : void 0;
+ }
+ if (fullSet) {
+ _.each(path.split(","), function(p) {
+ if (~path.indexOf('gear.')) {
+ user.fns.dotSet("" + p, true);
+ true;
+ } else {
+
+ }
+ user.fns.dotSet("purchased." + p, true);
+ return true;
+ });
+ } else {
+ if (alreadyOwns) {
+ split = path.split('.');
+ v = split.pop();
+ k = split.join('.');
+ if (k === 'background' && v === user.preferences.background) {
+ v = '';
+ }
+ user.fns.dotSet("preferences." + k, v);
+ return typeof cb === "function" ? cb(null, req) : void 0;
+ }
+ user.fns.dotSet("purchased." + path, true);
+ }
+ user.balance -= cost;
+ if (~path.indexOf('gear.')) {
+ if (typeof user.markModified === "function") {
+ user.markModified('gear.owned');
+ }
+ } else {
+ if (typeof user.markModified === "function") {
+ user.markModified('purchased');
+ }
+ }
+ analyticsData = {
+ uuid: user._id,
+ itemKey: path,
+ itemType: 'customization',
+ acquireMethod: 'Gems',
+ gemCost: cost / .25,
+ category: 'behavior'
+ };
+ if (analytics != null) {
+ analytics.track('acquire item', analyticsData);
+ }
+ return typeof cb === "function" ? cb(null, _.pick(user, $w('purchased preferences items'))) : void 0;
+ },
+ changeClass: function(req, cb, analytics) {
+ var analyticsData, klass, ref;
+ klass = (ref = req.query) != null ? ref["class"] : void 0;
+ if (klass === 'warrior' || klass === 'rogue' || klass === 'wizard' || klass === 'healer') {
+ analyticsData = {
+ uuid: user._id,
+ "class": klass,
+ acquireMethod: 'Gems',
+ gemCost: 3,
+ category: 'behavior'
+ };
+ if (analytics != null) {
+ analytics.track('change class', analyticsData);
+ }
+ user.stats["class"] = klass;
+ user.flags.classSelected = true;
+ _.each(["weapon", "armor", "shield", "head"], function(type) {
+ var foundKey;
+ foundKey = false;
+ _.findLast(user.items.gear.owned, function(v, k) {
+ if (~k.indexOf(type + "_" + klass) && v === true) {
+ return foundKey = k;
+ }
+ });
+ user.items.gear.equipped[type] = foundKey ? foundKey : type === "weapon" ? "weapon_" + klass + "_0" : type === "shield" && klass === "rogue" ? "shield_rogue_0" : type + "_base_0";
+ if (type === "weapon" || (type === "shield" && klass === "rogue")) {
+ user.items.gear.owned[type + "_" + klass + "_0"] = true;
+ }
+ return true;
+ });
+ } else {
+ if (user.preferences.disableClasses) {
+ user.preferences.disableClasses = false;
+ user.preferences.autoAllocate = false;
+ } else {
+ if (!(user.balance >= .75)) {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: i18n.t('notEnoughGems', req.language)
+ }) : void 0;
+ }
+ user.balance -= .75;
+ }
+ _.merge(user.stats, {
+ str: 0,
+ con: 0,
+ per: 0,
+ int: 0,
+ points: api.capByLevel(user.stats.lvl)
+ });
+ user.flags.classSelected = false;
+ }
+ return typeof cb === "function" ? cb(null, _.pick(user, $w('stats flags items preferences'))) : void 0;
+ },
+ disableClasses: function(req, cb) {
+ user.stats["class"] = 'warrior';
+ user.flags.classSelected = true;
+ user.preferences.disableClasses = true;
+ user.preferences.autoAllocate = true;
+ user.stats.str = api.capByLevel(user.stats.lvl);
+ user.stats.points = 0;
+ return typeof cb === "function" ? cb(null, _.pick(user, $w('stats flags preferences'))) : void 0;
+ },
+ allocate: function(req, cb) {
+ var stat;
+ stat = req.query.stat || 'str';
+ if (user.stats.points > 0) {
+ user.stats[stat]++;
+ user.stats.points--;
+ if (stat === 'int') {
+ user.stats.mp++;
+ }
+ }
+ return typeof cb === "function" ? cb(null, _.pick(user, $w('stats'))) : void 0;
+ },
+ readCard: function(req, cb) {
+ var cardType;
+ cardType = req.params.cardType;
+ user.items.special[cardType + "Received"].shift();
+ if (typeof user.markModified === "function") {
+ user.markModified("items.special." + cardType + "Received");
+ }
+ user.flags.cardReceived = false;
+ return typeof cb === "function" ? cb(null, 'items.special flags.cardReceived') : void 0;
+ },
+ openMysteryItem: function(req, cb, analytics) {
+ var analyticsData, item, ref, ref1;
+ item = (ref = user.purchased.plan) != null ? (ref1 = ref.mysteryItems) != null ? ref1.shift() : void 0 : void 0;
+ if (!item) {
+ return typeof cb === "function" ? cb({
+ code: 400,
+ message: "Empty"
+ }) : void 0;
+ }
+ item = content.gear.flat[item];
+ user.items.gear.owned[item.key] = true;
+ if (typeof user.markModified === "function") {
+ user.markModified('purchased.plan.mysteryItems');
+ }
+ item.notificationType = 'Mystery';
+ analyticsData = {
+ uuid: user._id,
+ itemKey: item,
+ itemType: 'Subscriber Gear',
+ acquireMethod: 'Subscriber',
+ category: 'behavior'
+ };
+ if (analytics != null) {
+ analytics.track('open mystery item', analyticsData);
+ }
+ if (typeof window !== 'undefined') {
+ (user._tmp != null ? user._tmp : user._tmp = {}).drop = item;
+ }
+ return typeof cb === "function" ? cb(null, user.items.gear.owned) : void 0;
+ },
+ score: function(req, cb) {
+ var addPoints, calculateDelta, calculateReverseDelta, changeTaskValue, delta, direction, gainMP, id, multiplier, num, options, ref, stats, subtractPoints, task, th;
+ ref = req.params, id = ref.id, direction = ref.direction;
+ task = user.tasks[id];
+ options = req.query || {};
+ _.defaults(options, {
+ times: 1,
+ cron: false
+ });
+ user._tmp = {};
+ stats = {
+ gp: +user.stats.gp,
+ hp: +user.stats.hp,
+ exp: +user.stats.exp
+ };
+ task.value = +task.value;
+ task.streak = ~~task.streak;
+ if (task.priority == null) {
+ task.priority = 1;
+ }
+ if (task.value > stats.gp && task.type === 'reward') {
+ return typeof cb === "function" ? cb({
+ code: 401,
+ message: i18n.t('messageNotEnoughGold', req.language)
+ }) : void 0;
+ }
+ delta = 0;
+ calculateDelta = function() {
+ var currVal, nextDelta, ref1;
+ currVal = task.value < -47.27 ? -47.27 : task.value > 21.27 ? 21.27 : task.value;
+ nextDelta = Math.pow(0.9747, currVal) * (direction === 'down' ? -1 : 1);
+ if (((ref1 = task.checklist) != null ? ref1.length : void 0) > 0) {
+ if (direction === 'down' && task.type === 'daily' && options.cron) {
+ nextDelta *= 1 - _.reduce(task.checklist, (function(m, i) {
+ return m + (i.completed ? 1 : 0);
+ }), 0) / task.checklist.length;
+ }
+ if (task.type === 'todo') {
+ nextDelta *= 1 + _.reduce(task.checklist, (function(m, i) {
+ return m + (i.completed ? 1 : 0);
+ }), 0);
+ }
+ }
+ return nextDelta;
+ };
+ calculateReverseDelta = function() {
+ var calc, closeEnough, currVal, diff, nextDelta, ref1, testVal;
+ currVal = task.value < -47.27 ? -47.27 : task.value > 21.27 ? 21.27 : task.value;
+ testVal = currVal + Math.pow(0.9747, currVal) * (direction === 'down' ? -1 : 1);
+ closeEnough = 0.00001;
+ while (true) {
+ calc = testVal + Math.pow(0.9747, testVal);
+ diff = currVal - calc;
+ if (Math.abs(diff) < closeEnough) {
+ break;
+ }
+ if (diff > 0) {
+ testVal -= diff;
+ } else {
+ testVal += diff;
+ }
+ }
+ nextDelta = testVal - currVal;
+ if (((ref1 = task.checklist) != null ? ref1.length : void 0) > 0) {
+ if (task.type === 'todo') {
+ nextDelta *= 1 + _.reduce(task.checklist, (function(m, i) {
+ return m + (i.completed ? 1 : 0);
+ }), 0);
+ }
+ }
+ return nextDelta;
+ };
+ changeTaskValue = function() {
+ return _.times(options.times, function() {
+ var nextDelta, ref1;
+ nextDelta = !options.cron && direction === 'down' ? calculateReverseDelta() : calculateDelta();
+ if (task.type !== 'reward') {
+ if (user.preferences.automaticAllocation === true && user.preferences.allocationMode === 'taskbased' && !(task.type === 'todo' && direction === 'down')) {
+ user.stats.training[task.attribute] += nextDelta;
+ }
+ if (direction === 'up') {
+ user.party.quest.progress.up = user.party.quest.progress.up || 0;
+ if ((ref1 = task.type) === 'daily' || ref1 === 'todo') {
+ user.party.quest.progress.up += nextDelta * (1 + (user._statsComputed.str / 200));
+ }
+ if (task.type === 'habit') {
+ user.party.quest.progress.up += nextDelta * (0.5 + (user._statsComputed.str / 400));
+ }
+ }
+ task.value += nextDelta;
+ }
+ return delta += nextDelta;
+ });
+ };
+ addPoints = function() {
+ var _crit, afterStreak, currStreak, gpMod, intBonus, perBonus, streakBonus;
+ _crit = (delta > 0 ? user.fns.crit() : 1);
+ if (_crit > 1) {
+ user._tmp.crit = _crit;
+ }
+ intBonus = 1 + (user._statsComputed.int * .025);
+ stats.exp += Math.round(delta * intBonus * task.priority * _crit * 6);
+ perBonus = 1 + user._statsComputed.per * .02;
+ gpMod = delta * task.priority * _crit * perBonus;
+ return stats.gp += task.streak ? (currStreak = direction === 'down' ? task.streak - 1 : task.streak, streakBonus = currStreak / 100 + 1, afterStreak = gpMod * streakBonus, currStreak > 0 ? gpMod > 0 ? user._tmp.streakBonus = afterStreak - gpMod : void 0 : void 0, afterStreak) : gpMod;
+ };
+ subtractPoints = function() {
+ var conBonus, hpMod;
+ conBonus = 1 - (user._statsComputed.con / 250);
+ if (conBonus < .1) {
+ conBonus = 0.1;
+ }
+ hpMod = delta * conBonus * task.priority * 2;
+ return stats.hp += Math.round(hpMod * 10) / 10;
+ };
+ gainMP = function(delta) {
+ delta *= user._tmp.crit || 1;
+ user.stats.mp += delta;
+ if (user.stats.mp >= user._statsComputed.maxMP) {
+ user.stats.mp = user._statsComputed.maxMP;
+ }
+ if (user.stats.mp < 0) {
+ return user.stats.mp = 0;
+ }
+ };
+ switch (task.type) {
+ case 'habit':
+ changeTaskValue();
+ if (delta > 0) {
+ addPoints();
+ } else {
+ subtractPoints();
+ }
+ gainMP(_.max([0.25, .0025 * user._statsComputed.maxMP]) * (direction === 'down' ? -1 : 1));
+ th = (task.history != null ? task.history : task.history = []);
+ if (th[th.length - 1] && moment(th[th.length - 1].date).isSame(new Date, 'day')) {
+ th[th.length - 1].value = task.value;
+ } else {
+ th.push({
+ date: +(new Date),
+ value: task.value
+ });
+ }
+ if (typeof user.markModified === "function") {
+ user.markModified("habits." + (_.findIndex(user.habits, {
+ id: task.id
+ })) + ".history");
+ }
+ break;
+ case 'daily':
+ if (options.cron) {
+ changeTaskValue();
+ subtractPoints();
+ if (!user.stats.buffs.streaks) {
+ task.streak = 0;
+ }
+ } else {
+ changeTaskValue();
+ if (direction === 'down') {
+ delta = calculateDelta();
+ }
+ addPoints();
+ gainMP(_.max([1, .01 * user._statsComputed.maxMP]) * (direction === 'down' ? -1 : 1));
+ if (direction === 'up') {
+ task.streak = task.streak ? task.streak + 1 : 1;
+ if ((task.streak % 21) === 0) {
+ user.achievements.streak = user.achievements.streak ? user.achievements.streak + 1 : 1;
+ }
+ } else {
+ if ((task.streak % 21) === 0) {
+ user.achievements.streak = user.achievements.streak ? user.achievements.streak - 1 : 0;
+ }
+ task.streak = task.streak ? task.streak - 1 : 0;
+ }
+ }
+ break;
+ case 'todo':
+ if (options.cron) {
+ changeTaskValue();
+ } else {
+ task.dateCompleted = direction === 'up' ? new Date : void 0;
+ changeTaskValue();
+ if (direction === 'down') {
+ delta = calculateDelta();
+ }
+ addPoints();
+ multiplier = _.max([
+ _.reduce(task.checklist, (function(m, i) {
+ return m + (i.completed ? 1 : 0);
+ }), 1), 1
+ ]);
+ gainMP(_.max([multiplier, .01 * user._statsComputed.maxMP * multiplier]) * (direction === 'down' ? -1 : 1));
+ }
+ break;
+ case 'reward':
+ changeTaskValue();
+ stats.gp -= Math.abs(task.value);
+ num = parseFloat(task.value).toFixed(2);
+ if (stats.gp < 0) {
+ stats.hp += stats.gp;
+ stats.gp = 0;
+ }
+ }
+ user.fns.updateStats(stats, req);
+ if (typeof window === 'undefined') {
+ if (direction === 'up') {
+ user.fns.randomDrop({
+ task: task,
+ delta: delta
+ }, req);
+ }
+ }
+ if (typeof cb === "function") {
+ cb(null, user);
+ }
+ return delta;
+ }
+ };
+ }
+ user.fns = {
+ getItem: function(type) {
+ var item;
+ item = content.gear.flat[user.items.gear.equipped[type]];
+ if (!item) {
+ return content.gear.flat[type + "_base_0"];
+ }
+ return item;
+ },
+ handleTwoHanded: function(item, type, req) {
+ var message, ref, weapon;
+ if (type == null) {
+ type = 'equipped';
+ }
+ if (item.type === "shield" && ((ref = (weapon = content.gear.flat[user.items.gear[type].weapon])) != null ? ref.twoHanded : void 0)) {
+ user.items.gear[type].weapon = 'weapon_base_0';
+ message = i18n.t('messageTwoHandled', {
+ gearText: weapon.text(req.language)
+ }, req.language);
+ }
+ if (item.twoHanded) {
+ user.items.gear[type].shield = "shield_base_0";
+ message = i18n.t('messageTwoHandled', {
+ gearText: item.text(req.language)
+ }, req.language);
+ }
+ return message;
+ },
+
+ /*
+ Because the same op needs to be performed on the client and the server (critical hits, item drops, etc),
+ we need things to be "random", but technically predictable so that they don't go out-of-sync
+ */
+ predictableRandom: function(seed) {
+ var x;
+ if (!seed || seed === Math.PI) {
+ seed = _.reduce(user.stats, (function(m, v) {
+ if (_.isNumber(v)) {
+ return m + v;
+ } else {
+ return m;
+ }
+ }), 0);
+ }
+ x = Math.sin(seed++) * 10000;
+ return x - Math.floor(x);
+ },
+ crit: function(stat, chance) {
+ var s;
+ if (stat == null) {
+ stat = 'str';
+ }
+ if (chance == null) {
+ chance = .03;
+ }
+ s = user._statsComputed[stat];
+ if (user.fns.predictableRandom() <= chance * (1 + s / 100)) {
+ return 1.5 + 4 * s / (s + 200);
+ } else {
+ return 1;
+ }
+ },
+
+ /*
+ Get a random property from an object
+ returns random property (the value)
+ */
+ randomVal: function(obj, options) {
+ var array, rand;
+ array = (options != null ? options.key : void 0) ? _.keys(obj) : _.values(obj);
+ rand = user.fns.predictableRandom(options != null ? options.seed : void 0);
+ array.sort();
+ return array[Math.floor(rand * array.length)];
+ },
+
+ /*
+ This allows you to set object properties by dot-path. Eg, you can run pathSet('stats.hp',50,user) which is the same as
+ user.stats.hp = 50. This is useful because in our habitrpg-shared functions we're returning changesets as {path:value},
+ so that different consumers can implement setters their own way. Derby needs model.set(path, value) for example, where
+ Angular sets object properties directly - in which case, this function will be used.
+ */
+ dotSet: function(path, val) {
+ return api.dotSet(user, path, val);
+ },
+ dotGet: function(path) {
+ return api.dotGet(user, path);
+ },
+ randomDrop: function(modifiers, req) {
+ var acceptableDrops, base, base1, base2, chance, drop, dropK, dropMultiplier, name, name1, name2, quest, rarity, ref, ref1, ref2, ref3, task;
+ task = modifiers.task;
+ chance = _.min([Math.abs(task.value - 21.27), 37.5]) / 150 + .02;
+ chance *= task.priority * (1 + (task.streak / 100 || 0)) * (1 + (user._statsComputed.per / 100)) * (1 + (user.contributor.level / 40 || 0)) * (1 + (user.achievements.rebirths / 20 || 0)) * (1 + (user.achievements.streak / 200 || 0)) * (user._tmp.crit || 1) * (1 + .5 * (_.reduce(task.checklist, (function(m, i) {
+ return m + (i.completed ? 1 : 0);
+ }), 0) || 0));
+ chance = api.diminishingReturns(chance, 0.75);
+ quest = content.quests[(ref = user.party.quest) != null ? ref.key : void 0];
+ if ((quest != null ? quest.collect : void 0) && user.fns.predictableRandom(user.stats.gp) < chance) {
+ dropK = user.fns.randomVal(quest.collect, {
+ key: true
+ });
+ user.party.quest.progress.collect[dropK]++;
+ if (typeof user.markModified === "function") {
+ user.markModified('party.quest.progress');
+ }
+ }
+ dropMultiplier = ((ref1 = user.purchased) != null ? (ref2 = ref1.plan) != null ? ref2.customerId : void 0 : void 0) ? 2 : 1;
+ if ((api.daysSince(user.items.lastDrop.date, user.preferences) === 0) && (user.items.lastDrop.count >= dropMultiplier * (5 + Math.floor(user._statsComputed.per / 25) + (user.contributor.level || 0)))) {
+ return;
+ }
+ if (((ref3 = user.flags) != null ? ref3.dropsEnabled : void 0) && user.fns.predictableRandom(user.stats.exp) < chance) {
+ rarity = user.fns.predictableRandom(user.stats.gp);
+ if (rarity > .6) {
+ drop = user.fns.randomVal(_.where(content.food, {
+ canDrop: true
+ }));
+ if ((base = user.items.food)[name = drop.key] == null) {
+ base[name] = 0;
+ }
+ user.items.food[drop.key] += 1;
+ drop.type = 'Food';
+ drop.dialog = i18n.t('messageDropFood', {
+ dropArticle: drop.article,
+ dropText: drop.text(req.language),
+ dropNotes: drop.notes(req.language)
+ }, req.language);
+ } else if (rarity > .3) {
+ drop = user.fns.randomVal(content.dropEggs);
+ if ((base1 = user.items.eggs)[name1 = drop.key] == null) {
+ base1[name1] = 0;
+ }
+ user.items.eggs[drop.key]++;
+ drop.type = 'Egg';
+ drop.dialog = i18n.t('messageDropEgg', {
+ dropText: drop.text(req.language),
+ dropNotes: drop.notes(req.language)
+ }, req.language);
+ } else {
+ acceptableDrops = rarity < .02 ? ['Golden'] : rarity < .09 ? ['Zombie', 'CottonCandyPink', 'CottonCandyBlue'] : rarity < .18 ? ['Red', 'Shade', 'Skeleton'] : ['Base', 'White', 'Desert'];
+ drop = user.fns.randomVal(_.pick(content.hatchingPotions, (function(v, k) {
+ return indexOf.call(acceptableDrops, k) >= 0;
+ })));
+ if ((base2 = user.items.hatchingPotions)[name2 = drop.key] == null) {
+ base2[name2] = 0;
+ }
+ user.items.hatchingPotions[drop.key]++;
+ drop.type = 'HatchingPotion';
+ drop.dialog = i18n.t('messageDropPotion', {
+ dropText: drop.text(req.language),
+ dropNotes: drop.notes(req.language)
+ }, req.language);
+ }
+ user._tmp.drop = drop;
+ user.items.lastDrop.date = +(new Date);
+ return user.items.lastDrop.count++;
+ }
+ },
+
+ /*
+ Updates user stats with new stats. Handles death, leveling up, etc
+ {stats} new stats
+ {update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately
+ */
+ autoAllocate: function() {
+ return user.stats[(function() {
+ var diff, ideal, lvlDiv7, preference, stats, suggested;
+ switch (user.preferences.allocationMode) {
+ case "flat":
+ stats = _.pick(user.stats, $w('con str per int'));
+ return _.invert(stats)[_.min(stats)];
+ case "classbased":
+ lvlDiv7 = user.stats.lvl / 7;
+ ideal = [lvlDiv7 * 3, lvlDiv7 * 2, lvlDiv7, lvlDiv7];
+ preference = (function() {
+ switch (user.stats["class"]) {
+ case "wizard":
+ return ["int", "per", "con", "str"];
+ case "rogue":
+ return ["per", "str", "int", "con"];
+ case "healer":
+ return ["con", "int", "str", "per"];
+ default:
+ return ["str", "con", "per", "int"];
+ }
+ })();
+ diff = [user.stats[preference[0]] - ideal[0], user.stats[preference[1]] - ideal[1], user.stats[preference[2]] - ideal[2], user.stats[preference[3]] - ideal[3]];
+ suggested = _.findIndex(diff, (function(val) {
+ if (val === _.min(diff)) {
+ return true;
+ }
+ }));
+ if (~suggested) {
+ return preference[suggested];
+ } else {
+ return "str";
+ }
+ case "taskbased":
+ suggested = _.invert(user.stats.training)[_.max(user.stats.training)];
+ _.merge(user.stats.training, {
+ str: 0,
+ int: 0,
+ con: 0,
+ per: 0
+ });
+ return suggested || "str";
+ default:
+ return "str";
+ }
+ })()]++;
+ },
+ updateStats: function(stats, req, analytics) {
+ var tnl;
+ if (stats.hp <= 0) {
+ return user.stats.hp = 0;
+ }
+ user.stats.hp = stats.hp;
+ user.stats.gp = stats.gp >= 0 ? stats.gp : 0;
+ tnl = api.tnl(user.stats.lvl);
+ if (stats.exp >= tnl) {
+ user.stats.exp = stats.exp;
+ while (stats.exp >= tnl) {
+ stats.exp -= tnl;
+ user.stats.lvl++;
+ tnl = api.tnl(user.stats.lvl);
+ user.stats.hp = 50;
+ if (user.stats.lvl > api.maxLevel) {
+ continue;
+ }
+ if (user.preferences.automaticAllocation) {
+ user.fns.autoAllocate();
+ } else {
+ user.stats.points = user.stats.lvl - (user.stats.con + user.stats.str + user.stats.per + user.stats.int);
+ if (user.stats.points < 0) {
+ user.stats.points = 0;
+ }
+ }
+ }
+ }
+ user.stats.exp = stats.exp;
+ if (user.flags == null) {
+ user.flags = {};
+ }
+ if (!user.flags.customizationsNotification && (user.stats.exp > 5 || user.stats.lvl > 1)) {
+ user.flags.customizationsNotification = true;
+ }
+ if (!user.flags.itemsEnabled && (user.stats.exp > 10 || user.stats.lvl > 1)) {
+ user.flags.itemsEnabled = true;
+ }
+ if (!user.flags.dropsEnabled && user.stats.lvl >= 3) {
+ user.flags.dropsEnabled = true;
+ if (user.items.eggs["Wolf"] > 0) {
+ user.items.eggs["Wolf"]++;
+ } else {
+ user.items.eggs["Wolf"] = 1;
+ }
+ }
+ if (!user.flags.classSelected && user.stats.lvl >= 10) {
+ user.flags.classSelected;
+ }
+ _.each({
+ vice1: 30,
+ atom1: 15,
+ moonstone1: 60,
+ goldenknight1: 40
+ }, function(lvl, k) {
+ var analyticsData, base, base1, ref;
+ if (!((ref = user.flags.levelDrops) != null ? ref[k] : void 0) && user.stats.lvl >= lvl) {
+ if ((base = user.items.quests)[k] == null) {
+ base[k] = 0;
+ }
+ user.items.quests[k]++;
+ ((base1 = user.flags).levelDrops != null ? base1.levelDrops : base1.levelDrops = {})[k] = true;
+ if (typeof user.markModified === "function") {
+ user.markModified('flags.levelDrops');
+ }
+ analyticsData = {
+ uuid: user._id,
+ itemKey: k,
+ acquireMethod: 'Level Drop',
+ category: 'behavior'
+ };
+ if (analytics != null) {
+ analytics.track('acquire item', analyticsData);
+ }
+ return user._tmp.drop = {
+ type: 'Quest',
+ key: k
+ };
+ }
+ });
+ if (!user.flags.rebirthEnabled && (user.stats.lvl >= 50 || user.achievements.beastMaster)) {
+ return user.flags.rebirthEnabled = true;
+ }
+ },
+
+ /*
+ ------------------------------------------------------
+ Cron
+ ------------------------------------------------------
+ */
+
+ /*
+ At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
+ For incomplete Dailys, deduct experience
+ Make sure to run this function once in a while as server will not take care of overnight calculations.
+ And you have to run it every time client connects.
+ {user}
+ */
+ cron: function(options) {
+ var _progress, analyticsData, base, base1, base2, base3, base4, clearBuffs, dailyChecked, dailyDueUnchecked, daysMissed, expTally, lvl, lvlDiv2, multiDaysCountAsOneDay, now, perfect, plan, progress, ref, ref1, ref2, ref3, todoTally;
+ if (options == null) {
+ options = {};
+ }
+ now = +options.now || +(new Date);
+ daysMissed = api.daysSince(user.lastCron, _.defaults({
+ now: now
+ }, user.preferences));
+ if (!(daysMissed > 0)) {
+ return;
+ }
+ user.auth.timestamps.loggedin = new Date();
+ user.lastCron = now;
+ if (user.items.lastDrop.count > 0) {
+ user.items.lastDrop.count = 0;
+ }
+ perfect = true;
+ clearBuffs = {
+ str: 0,
+ int: 0,
+ per: 0,
+ con: 0,
+ stealth: 0,
+ streaks: false
+ };
+ plan = (ref = user.purchased) != null ? ref.plan : void 0;
+ if (plan != null ? plan.customerId : void 0) {
+ if (moment(plan.dateUpdated).format('MMYYYY') !== moment().format('MMYYYY')) {
+ plan.gemsBought = 0;
+ plan.dateUpdated = new Date();
+ _.defaults(plan.consecutive, {
+ count: 0,
+ offset: 0,
+ trinkets: 0,
+ gemCapExtra: 0
+ });
+ plan.consecutive.count++;
+ if (plan.consecutive.offset > 0) {
+ plan.consecutive.offset--;
+ } else if (plan.consecutive.count % 3 === 0) {
+ plan.consecutive.trinkets++;
+ plan.consecutive.gemCapExtra += 5;
+ if (plan.consecutive.gemCapExtra > 25) {
+ plan.consecutive.gemCapExtra = 25;
+ }
+ }
+ }
+ if (plan.dateTerminated && moment(plan.dateTerminated).isBefore(+(new Date))) {
+ _.merge(plan, {
+ planId: null,
+ customerId: null,
+ paymentMethod: null
+ });
+ _.merge(plan.consecutive, {
+ count: 0,
+ offset: 0,
+ gemCapExtra: 0
+ });
+ if (typeof user.markModified === "function") {
+ user.markModified('purchased.plan');
+ }
+ }
+ }
+ if (user.preferences.sleep === true) {
+ user.stats.buffs = clearBuffs;
+ user.dailys.forEach(function(daily) {
+ var completed, repeat, thatDay;
+ completed = daily.completed, repeat = daily.repeat;
+ thatDay = moment(now).subtract({
+ days: 1
+ });
+ if (api.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 (api.shouldDo(thatDay.toDate(), task, user.preferences)) {
+ scheduleMisses++;
+ if (user.stats.buffs.stealth) {
+ user.stats.buffs.stealth--;
+ EvadeTask++;
+ }
+ if (multiDaysCountAsOneDay) {
+ break;
+ }
+ }
+ }
+ if (scheduleMisses > EvadeTask) {
+ perfect = false;
+ if (((ref2 = task.checklist) != null ? ref2.length : void 0) > 0) {
+ fractionChecked = _.reduce(task.checklist, (function(m, i) {
+ return m + (i.completed ? 1 : 0);
+ }), 0) / task.checklist.length;
+ dailyDueUnchecked += 1 - fractionChecked;
+ dailyChecked += fractionChecked;
+ } else {
+ dailyDueUnchecked += 1;
+ }
+ delta = user.ops.score({
+ params: {
+ id: task.id,
+ direction: 'down'
+ },
+ query: {
+ times: multiDaysCountAsOneDay != null ? multiDaysCountAsOneDay : {
+ 1: scheduleMisses - EvadeTask
+ },
+ cron: true
+ }
+ });
+ user.party.quest.progress.down += delta * (task.priority < 1 ? task.priority : 1);
+ }
+ }
+ (task.history != null ? task.history : task.history = []).push({
+ date: +(new Date),
+ value: task.value
+ });
+ task.completed = false;
+ if (completed || (scheduleMisses > 0)) {
+ return _.each(task.checklist, (function(i) {
+ i.completed = false;
+ return true;
+ }));
+ }
+ });
+ user.habits.forEach(function(task) {
+ if (task.up === false || task.down === false) {
+ if (Math.abs(task.value) < 0.1) {
+ return task.value = 0;
+ } else {
+ return task.value = task.value / 2;
+ }
+ }
+ });
+ ((base1 = (user.history != null ? user.history : user.history = {})).todos != null ? base1.todos : base1.todos = []).push({
+ date: now,
+ value: todoTally
+ });
+ expTally = user.stats.exp;
+ lvl = 0;
+ while (lvl < (user.stats.lvl - 1)) {
+ lvl++;
+ expTally += api.tnl(lvl);
+ }
+ ((base2 = user.history).exp != null ? base2.exp : base2.exp = []).push({
+ date: now,
+ value: expTally
+ });
+ if (!((ref1 = user.purchased) != null ? (ref2 = ref1.plan) != null ? ref2.customerId : void 0 : void 0)) {
+ user.fns.preenUserHistory();
+ if (typeof user.markModified === "function") {
+ user.markModified('history');
+ }
+ if (typeof user.markModified === "function") {
+ user.markModified('dailys');
+ }
+ }
+ user.stats.buffs = perfect ? ((base3 = user.achievements).perfect != null ? base3.perfect : base3.perfect = 0, user.achievements.perfect++, lvlDiv2 = Math.ceil(api.capByLevel(user.stats.lvl) / 2), {
+ str: lvlDiv2,
+ int: lvlDiv2,
+ per: lvlDiv2,
+ con: lvlDiv2,
+ stealth: 0,
+ streaks: false
+ }) : clearBuffs;
+ if (dailyDueUnchecked === 0 && dailyChecked === 0) {
+ dailyChecked = 1;
+ }
+ user.stats.mp += _.max([10, .1 * user._statsComputed.maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked);
+ if (user.stats.mp > user._statsComputed.maxMP) {
+ user.stats.mp = user._statsComputed.maxMP;
+ }
+ progress = user.party.quest.progress;
+ _progress = _.cloneDeep(progress);
+ _.merge(progress, {
+ down: 0,
+ up: 0
+ });
+ progress.collect = _.transform(progress.collect, (function(m, v, k) {
+ return m[k] = 0;
+ }));
+ if ((base4 = user.flags).cronCount == null) {
+ base4.cronCount = 0;
+ }
+ user.flags.cronCount++;
+ analyticsData = {
+ category: 'behavior',
+ gaLabel: 'Cron Count',
+ gaValue: user.flags.cronCount,
+ uuid: user._id,
+ user: user,
+ resting: user.preferences.sleep,
+ cronCount: user.flags.cronCount,
+ progressUp: _.min([_progress.up, 900]),
+ progressDown: _progress.down
+ };
+ if ((ref3 = options.analytics) != null) {
+ ref3.track('Cron', analyticsData);
+ }
+ return _progress;
+ },
+ preenUserHistory: function(minHistLen) {
+ if (minHistLen == null) {
+ minHistLen = 7;
+ }
+ _.each(user.habits.concat(user.dailys), function(task) {
+ var ref;
+ if (((ref = task.history) != null ? ref.length : void 0) > minHistLen) {
+ task.history = preenHistory(task.history);
+ }
+ return true;
+ });
+ _.defaults(user.history, {
+ todos: [],
+ exp: []
+ });
+ if (user.history.exp.length > minHistLen) {
+ user.history.exp = preenHistory(user.history.exp);
+ }
+ if (user.history.todos.length > minHistLen) {
+ return user.history.todos = preenHistory(user.history.todos);
+ }
+ },
+ ultimateGear: function() {
+ var base, owned;
+ owned = typeof window !== "undefined" && window !== null ? user.items.gear.owned : user.items.gear.owned.toObject();
+ if ((base = user.achievements).ultimateGearSets == null) {
+ base.ultimateGearSets = {
+ healer: false,
+ wizard: false,
+ rogue: false,
+ warrior: false
+ };
+ }
+ content.classes.forEach(function(klass) {
+ if (user.achievements.ultimateGearSets[klass] !== true) {
+ return user.achievements.ultimateGearSets[klass] = _.reduce(['armor', 'shield', 'head', 'weapon'], function(soFarGood, type) {
+ var found;
+ found = _.find(content.gear.tree[type][klass], {
+ last: true
+ });
+ return soFarGood && (!found || owned[found.key] === true);
+ }, true);
+ }
+ });
+ if (typeof user.markModified === "function") {
+ user.markModified('achievements.ultimateGearSets');
+ }
+ if (_.contains(user.achievements.ultimateGearSets, true) && user.flags.armoireEnabled !== true) {
+ user.flags.armoireEnabled = true;
+ return typeof user.markModified === "function" ? user.markModified('flags') : void 0;
+ }
+ },
+ nullify: function() {
+ user.ops = null;
+ user.fns = null;
+ return user = null;
+ }
+ };
+ Object.defineProperty(user, '_statsComputed', {
+ get: function() {
+ var computed;
+ computed = _.reduce(['per', 'con', 'str', 'int'], (function(_this) {
+ return function(m, stat) {
+ m[stat] = _.reduce($w('stats stats.buffs items.gear.equipped.weapon items.gear.equipped.armor items.gear.equipped.head items.gear.equipped.shield'), function(m2, path) {
+ var item, val;
+ val = user.fns.dotGet(path);
+ return m2 + (~path.indexOf('items.gear') ? (item = content.gear.flat[val], (+(item != null ? item[stat] : void 0) || 0) * ((item != null ? item.klass : void 0) === user.stats["class"] || (item != null ? item.specialClass : void 0) === user.stats["class"] ? 1.5 : 1)) : +val[stat] || 0);
+ }, 0);
+ m[stat] += Math.floor(api.capByLevel(user.stats.lvl) / 2);
+ return m;
+ };
+ })(this), {});
+ computed.maxMP = computed.int * 2 + 30;
+ return computed;
+ }
+ });
+ return Object.defineProperty(user, 'tasks', {
+ get: function() {
+ var tasks;
+ tasks = user.habits.concat(user.dailys).concat(user.todos).concat(user.rewards);
+ return _.object(_.pluck(tasks, "id"), tasks);
+ }
+ });
+};
diff --git a/package.json b/package.json
index 8b163aa925..56b2faa3a9 100644
--- a/package.json
+++ b/package.json
@@ -9,11 +9,10 @@
"async": "~0.9.0",
"aws-sdk": "^2.0.25",
"babel": "^5.5.4",
- "gulp-babel": "^5.2.1",
+ "babelify": "^7.2.0",
"bower": "~1.3.12",
"browserify": "~12.0.1",
"coffee-script": "1.6.x",
- "coffeeify": "0.6.0",
"connect-ratelimit": "0.0.7",
"coupon-code": "~0.3.0",
"domain-middleware": "~0.1.0",
@@ -23,7 +22,6 @@
"firebase-token-generator": "^2.0.0",
"glob": "^4.3.5",
"grunt": "~0.4.1",
- "grunt-browserify": "^3.3.0",
"grunt-cli": "~0.1.9",
"grunt-contrib-clean": "~0.6.0",
"grunt-contrib-copy": "~0.6.0",
@@ -38,6 +36,8 @@
"gulp-grunt": "^0.5.2",
"gulp-imagemin": "^2.3.0",
"gulp-nodemon": "^2.0.4",
+ "gulp-sourcemaps": "^1.6.0",
+ "gulp-uglify": "^1.4.2",
"gulp.spritesmith": "^4.1.0",
"icalendar": "lefnire/node-icalendar#e06da0e55901f0ba940dfadc42c158ed0b1fead9",
"image-size": "~0.3.2",
@@ -75,6 +75,8 @@
"swagger-node-express": "lefnire/swagger-node-express#habitrpg",
"universal-analytics": "~0.3.2",
"validator": "~3.19.0",
+ "vinyl-buffer": "^1.0.0",
+ "vinyl-source-stream": "^1.1.0",
"winston": "~0.8.0",
"winston-mail": "~0.2.9",
"winston-newrelic": "~0.1.4"
@@ -94,7 +96,6 @@
"devDependencies": {
"chai": "^3.4.0",
"chai-as-promised": "^5.1.0",
- "coffee-coverage": "~0.4.2",
"coveralls": "^2.11.2",
"csv": "~0.3.6",
"deep-diff": "~0.1.4",
diff --git a/register-handlers.js b/register-handlers.js
deleted file mode 100644
index 7a6c57a91a..0000000000
--- a/register-handlers.js
+++ /dev/null
@@ -1,7 +0,0 @@
-if(process.env.COVERAGE) {
- require('coffee-coverage').register({
- path: 'abbr',
- basePath: __dirname + '/common/script',
- initAll: true,
- });
-}
diff --git a/tasks/gulp-babelify.js b/tasks/gulp-babelify.js
index a77be47ecc..a5bd7af203 100644
--- a/tasks/gulp-babelify.js
+++ b/tasks/gulp-babelify.js
@@ -1,16 +1,31 @@
import gulp from 'gulp';
-import babel from 'gulp-babel';
+import browserify from 'browserify';
+import source from 'vinyl-source-stream';
+import buffer from 'vinyl-buffer';
+import uglify from 'gulp-uglify';
+import sourcemaps from 'gulp-sourcemaps';
+import babel from 'babelify';
-const ES2015_SOURCE = 'common/script/src/**/*.js';
-const ES2015_DIST = 'common/dist/scripts/';
+gulp.task('browserify', function () {
+ let bundler = browserify({
+ entries: './common/index.js',
+ debug: true,
+ transform: [[babel, { compact: false }]]
+ });
-gulp.task('babel:common', () => {
- return gulp.src(ES2015_SOURCE)
- .pipe(babel())
- .pipe(gulp.dest(ES2015_DIST));
+ return bundler.bundle()
+ .pipe(source('habitrpg-shared.js'))
+ .pipe(buffer())
+ .pipe(sourcemaps.init({loadMaps: true}))
+ .pipe(uglify())
+ .on('error', function (err) {
+ console.error(err);
+ this.emit('end');
+ })
+ .pipe(sourcemaps.write('./'))
+ .pipe(gulp.dest('./common/dist/scripts/'));
});
-gulp.task('babel:common:watch', () => {
- gulp.watch([ES2015_SOURCE], ['babel:common']);
+gulp.task('browserify:watch', () => {
+ gulp.watch('./common/script/**/*.js', ['browserify']);
});
-
diff --git a/tasks/gulp-build.js b/tasks/gulp-build.js
index 8b0b752ca2..599d577b10 100644
--- a/tasks/gulp-build.js
+++ b/tasks/gulp-build.js
@@ -9,7 +9,7 @@ gulp.task('build', () => {
}
});
-gulp.task('build:dev', ['babel:common', 'prepare:staticNewStuff'], (done) => {
+gulp.task('build:dev', ['browserify', 'prepare:staticNewStuff'], (done) => {
gulp.start('grunt-build:dev', done);
});
@@ -17,6 +17,6 @@ gulp.task('build:dev:watch', ['build:dev'], () => {
gulp.watch(['website/public/**/*.styl', 'common/script/*']);
});
-gulp.task('build:prod', ['babel:common', 'prepare:staticNewStuff'], (done) => {
+gulp.task('build:prod', ['browserify', 'prepare:staticNewStuff'], (done) => {
gulp.start('grunt-build:prod', done);
});
diff --git a/tasks/gulp-console.js b/tasks/gulp-console.js
index 95a1962fd4..07512c6357 100644
--- a/tasks/gulp-console.js
+++ b/tasks/gulp-console.js
@@ -1,5 +1,3 @@
-import 'coffee-script';
-
import mongoose from 'mongoose';
import autoinc from 'mongoose-id-autoinc';
import logging from '../website/src/libs/logging';
diff --git a/tasks/gulp-eslint.js b/tasks/gulp-eslint.js
index ec8866c4dc..92dd25947c 100644
--- a/tasks/gulp-eslint.js
+++ b/tasks/gulp-eslint.js
@@ -2,7 +2,7 @@ import gulp from 'gulp';
import eslint from 'gulp-eslint';
import _ from 'lodash';
-// TODO remove once we upgrade to lodash 3
+// TODO remove once we upgrade to lodash 3
const defaultsDeep = _.partialRight(_.merge, _.defaults);
const shared = {
@@ -19,7 +19,6 @@ const shared = {
};
gulp.task('lint:client', () => {
- // Ignore .coffee files
return gulp.src(['./website/public/js/**/*.js'])
.pipe(eslint(defaultsDeep({
env: {
@@ -31,7 +30,6 @@ gulp.task('lint:client', () => {
});
gulp.task('lint:server', () => {
- // Ignore .coffee files
return gulp.src(['./website/src/**/*.js'])
.pipe(eslint(defaultsDeep({
env: {
@@ -42,4 +40,4 @@ gulp.task('lint:server', () => {
.pipe(eslint.failAfterError());
});
-gulp.task('lint', ['lint:server', 'lint:client']);
\ No newline at end of file
+gulp.task('lint', ['lint:server', 'lint:client']);
diff --git a/test/api-legacy/api-helper.js b/test/api-legacy/api-helper.js
index 3dcaf5241c..9e61773211 100644
--- a/test/api-legacy/api-helper.js
+++ b/test/api-legacy/api-helper.js
@@ -1,4 +1,3 @@
-require('coffee-script');
var path, superagentDefaults;
superagentDefaults = require("superagent-defaults");
diff --git a/test/archive/README.md b/test/archive/README.md
deleted file mode 100644
index 27eaee4c77..0000000000
--- a/test/archive/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-These are tests which are no longer used. While we could delete them and depend on git history for later lookup,
-I think it's important these are visible to developers since they may have valuable tests which haven't been ported
-to our new setup. Once a file is ported or determined useless, feel free to delete.
\ No newline at end of file
diff --git a/test/archive/api.mocha.coffee b/test/archive/api.mocha.coffee
deleted file mode 100644
index cf9d276f03..0000000000
--- a/test/archive/api.mocha.coffee
+++ /dev/null
@@ -1,656 +0,0 @@
-_ = require 'lodash'
-expect = require 'expect.js'
-require 'coffee-script'
-async = require 'async'
-superagentDefaults = require 'superagent-defaults'
-
-request = superagentDefaults()
-
-conf = require("nconf")
-conf.argv().env().file({file: __dirname + '../config.json'}).defaults
-conf.set('port','1337')
-
-# Override normal ENV values with nconf ENV values (ENV values are used the same way without nconf)
-#FIXME can't get nconf file above to load...
-process.env.BASE_URL = conf.get("BASE_URL")
-process.env.FACEBOOK_KEY = conf.get("FACEBOOK_KEY")
-process.env.FACEBOOK_SECRET = conf.get("FACEBOOK_SECRET")
-process.env.NODE_DB_URI = 'mongodb://localhost/habitrpg'
-
-User = require('../../src/models/user').model
-Group = require('../../src/models/group').model
-Challenge = require('../../src/models/challenge').model
-
-app = require '../../src/server'
-
-## monkey-patch expect.js for better diffs on mocha
-## see: https://github.com/LearnBoost/expect.js/pull/34
-#origBe = expect.Assertion::be
-#expect.Assertion::be = expect.Assertion::equal = (obj) ->
-# @_expected = obj
-# origBe.call this, obj
-
-# Custom modules
-shared = require 'habitrpg-shared'
-
-###### Helpers & Variables ######
-
-model = null
-uuid = null
-taskPath = null
-baseURL = 'http://localhost:3000/api/v2'
-
-###
- expect().eql expects object keys to be in the correct order, this sorts that out
-###
-
-expectUserEqual = (u1, u2) ->
- [u1, u2] = _.map [u1, u2], (obj) ->
- 'update__ stats.toNextLevel stats.maxHealth __v'.split(' ').forEach (path) ->
- helpers.dotSet path, null, obj
- sorted = {}
- _.each _.keys(obj).sort(), (k) -> sorted[k] = obj[k]
- sorted.tasks = _.sortBy sorted.tasks, 'id'
- sorted
-# console.log {u1, u2}
- expect(u1).to.eql(u2)
-
-expectSameValues = (obj1, obj2, paths) ->
- _.each paths, (k) ->
- expect(helpers.dotGet(k,obj1)).to.eql helpers.dotGet(k,obj2)
-
-expectCode = (res, code) ->
- expect(res.body.err).to.be undefined if code is 200
- expect(res.statusCode).to.be code
-
-###### Specs ######
-
-describe 'API', ->
- user = null
- _id = null
- apiToken = null
- username = null
- password = null
-
- registerNewUser = (cb, main=true)->
- randomID = shared.uuid()
- [username,password] = [randomID,randomID] if main
- request.post("#{baseURL}/register")
- .set('Accept', 'application/json')
- .send({
- username: randomID
- password: randomID
- confirmPassword: randomID
- email: "#{randomID}@gmail.com"
- })
- .end (res) ->
- return cb(null,res.body) unless main
- {_id,apiToken} = res.body
- console.log {_id,apiToken}
- User.findOne {_id, apiToken}, (err, _user) ->
- expect(err).to.not.be.ok
- user = _user
- request
- .set('Accept', 'application/json')
- .set('X-API-User', _id)
- .set('X-API-Key', apiToken)
- cb null, res.body
-
- before (done)->
- require '../../src/server' #start the server
- # then wait for it to do it's thing. TODO make a cb-compatible export of server
- setTimeout done, 2000
-
- describe 'Without token or user id', ->
- it '/api/v2/status', (done) ->
- request.get("#{baseURL}/status")
- .set('Accept', 'application/json')
- .end (res) ->
- expect(res.statusCode).to.be 200
- expect(res.body.status).to.be 'up'
- done()
-
- it '/api/v2/user', (done) ->
- request.get("#{baseURL}/user")
- .set('Accept', 'application/json')
- .end (res) ->
- expect(res.statusCode).to.be 401
- expect(res.body.err).to.be 'You must include a token and uid (user id) in your request'
- done()
-
- describe 'With token and user id', ->
- currentUser = null
-
- before (done) ->
- registerNewUser(done,true)
-
- beforeEach (done) ->
- User.findById _id, (err,_user) ->
- currentUser = _user
- done()
-
- ############
- # Groups
- ############
-
- describe 'Groups', ->
- group = undefined
-
- before (done) ->
- request.post("#{baseURL}/groups")
- .send({name:"TestGroup", type:"party"})
- .end (res) ->
- expectCode res, 200
- group = res.body
- expect(group.members.length).to.be 1
- expect(group.leader).to.be user._id
- done()
-
- describe 'Challenges', ->
- challenge = undefined
- updateTodo = undefined
-
- it 'Creates a challenge', (done) ->
- request.post("#{baseURL}/challenges")
- .send({
- group:group._id
- dailys: [{type:'daily',text:'Challenge Daily'}]
- todos: [{type:'todo', text:'Challenge Todo', notes:'Challenge Notes'}]
- rewards: []
- habits: []
- official: true
- })
- .end (res) ->
- expectCode res, 200
- async.parallel [
- (cb) -> User.findById _id, cb
- (cb) -> Challenge.findById res.body._id, cb
- ], (err, results) ->
- [_user,challenge] = [results[0],results[1]]
- expect(_user.dailys[_user.dailys.length-1].text).to.be('Challenge Daily')
- updateTodo = _user.todos[_user.todos.length-1]
- expect(updateTodo.text).to.be('Challenge Todo')
- expect(challenge.official).to.be false
- done()
-
- it 'User updates challenge notes', (done) ->
- updateTodo.notes = "User overriden notes"
- request.put("#{baseURL}/user/tasks/#{updateTodo.id}")
- .send(updateTodo)
- .end (res) ->
- done() #we'll do the check down below
-
- it 'Change challenge daily', (done) ->
- challenge.dailys[0].text = 'Updated Daily'
- challenge.todos[0].notes = 'Challenge Updated Todo Notes'
- request.post("#{baseURL}/challenges/#{challenge._id}")
- .send(challenge)
- .end (res) ->
- setTimeout ->
- User.findById _id, (err,_user) ->
- expectCode res, 200
- expect(_user.dailys[_user.dailys.length-1].text).to.be('Updated Daily')
- expect(res.body.todos[0].notes).to.be('Challenge Updated Todo Notes')
- expect(_user.todos[_user.todos.length-1].notes).to.be('User overriden notes')
- currentUser = _user
- done()
- , 500 # we have to wait a while for users' tasks to be updated, called async on server
-
- it 'Shows user notes on challenge page', (done) ->
- request.get("#{baseURL}/challenges/#{challenge._id}/member/#{_id}")
- .end (res) ->
- expect(res.body.todos[res.body.todos.length-1].notes).to.be('User overriden notes')
- done()
-
- it 'Complete To-Dos', (done) ->
- u = currentUser
- request.post("#{baseURL}/user/tasks/#{u.todos[0].id}/up").end (res) ->
- request.post("#{baseURL}/user/tasks/#{u.todos[1].id}/up").end (res) ->
- request.post("#{baseURL}/user/tasks/").send({type:'todo'}).end (res) ->
- request.post("#{baseURL}/user/tasks/clear-completed").end (res) ->
- expect(_.size res.body).to.be 2
- done()
-
- it 'Admin creates a challenge', (done) ->
- User.findByIdAndUpdate _id, {$set:{'contributor.admin':true}}, (err,_user) ->
- expect(err).to.not.be.ok
-
- async.parallel [
- (cb)->
- request.post("#{baseURL}/challenges")
- .send({group:group._id, dailys: [], todos: [], rewards: [], habits: [], official: false}).end (res) ->
- expect(res.body.official).to.be false
- cb()
- (cb)->
- request.post("#{baseURL}/challenges")
- .send({group:group._id, dailys: [], todos: [], rewards: [], habits: [], official: true}).end (res) ->
- expect(res.body.official).to.be true
- cb()
- ], done
-
-
- describe 'Quests', ->
- party = undefined
- participating = []
- notParticipating = []
-
- it 'Invites some members', (done) ->
- async.waterfall [
-
- # Register new users
- (cb) ->
- async.parallel [
- (cb2) -> registerNewUser(cb2,false)
- (cb2) -> registerNewUser(cb2,false)
- (cb2) -> registerNewUser(cb2,false)
- ], cb
-
- # Send them invitations
- (_party, cb) ->
- party = _party
- async.parallel [
- (cb2) -> request.post("#{baseURL}/groups/#{group._id}/invite?uuid=#{party[0]._id}").end (-> cb2())
- (cb2) -> request.post("#{baseURL}/groups/#{group._id}/invite?uuid=#{party[1]._id}").end (-> cb2())
- (cb2) -> request.post("#{baseURL}/groups/#{group._id}/invite?uuid=#{party[2]._id}").end (-> cb2())
- ], cb
-
- # Accept / Reject
- (results, cb) ->
- #series since they'll be modifying the same group record
- async.series (_.reduce party, (m,v,i) ->
- m.push (cb2) ->
- request.post("#{baseURL}/groups/#{group._id}/join")
- .set('X-API-User', party[i]._id)
- .set('X-API-Key', party[i].apiToken)
- .end (res) -> cb2()
- m
- , []), cb
-
- # Make sure the invites stuck
- (whatever, cb) ->
- Group.findById group._id, (err, g) ->
- expect(g.members.length).to.be 4
- cb()
-
- ], (err, results) ->
- expect(err).to.be.ok
- done()
-
- it 'Starts a quest', (done) ->
- async.waterfall [
- (cb)->
- request.post("#{baseURL}/groups/#{group._id}/questAccept?key=evilsanta")
- .end (res) ->
- expectCode(res, 401)
- User.findByIdAndUpdate _id, {$set:'items.quests.evilsanta':1}, cb
- (_user,cb)->
- request.post("#{baseURL}/groups/#{group._id}/questAccept?key=evilsanta")
- .end (res) ->
- expectCode(res, 200)
- Group.findById group._id,cb
- (_group,cb)->
- group = _group #refresh local group
- expect(group.quest.key).to.be 'evilsanta'
-
- async.series (_.reduce party, (m,v,i) ->
- m.push (cb2) ->
- request.post("#{baseURL}/groups/#{group._id}/questAccept")
- .set('X-API-User', party[i]._id)
- .set('X-API-Key', party[i].apiToken)
- .end (res) -> cb2()
- m
- , []), cb
-
- ], done
-
- it "Doesn't include people who aren't participating"
-
-
-# ############
-# # Batch Update
-# ############
-#
-# describe 'Batch Update', ->
-#
-# it 'POST /api/v1/batch-update', (done) ->
-# userBefore = _.cloneDeep(currentUser)
-#
-# ops = [
-# # Good scores
-# op: 'score', params: {id:user.habits[0].id, direction: 'up'}
-# op: 'score', params: {id:user.habits[1].id, direction: 'down'}
-# op: 'score', params: {id:user.dailys[0].id, direction: 'up'}
-# op: 'score', params: {id:user.todos[0].id, direction: 'up'}
-# ]
-#
-# request.post("#{baseURL}/user/batch-update")
-# .send(ops)
-# .end (res) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 200
-# #expectUserEqual(userBefore, res.body)
-# done()
-#
-#
-# ############
-# # To Be Updated (these are old v1 tests which haven't been touched in over 6 months, need to be portd to new API tests or deleted)
-# ############
-#
-# it.skip 'POST /api/v2/batch-update (handles corrupt values)', (done) ->
-# registerNewUser (_res) ->
-# # corrupt the tasks, and let's see how the server handles this
-# ids = _res.dailyIds
-# _res.tasks[ids[0]].value = NaN
-# _res.tasks[ids[1]].value = undefined
-# _res.tasks[ids[2]] = {}
-# _res.tasks["undefined"] = {}
-#
-# _res.stats.hp = _res.stats.gp = NaN
-#
-# _res.lastCron = +new Date('08/13/2013')
-#
-# ops = [
-# op: 'score', task: _res.tasks[ids[0]], dir: 'up'
-# ]
-#
-# model.set "users.#{_res.id}", _res, ->
-# request.post("#{baseURL}/user/batch-update")
-# .set('Accept', 'application/json')
-# .set('X-API-User', _res.id)
-# .set('X-API-Key', _res.apiToken)
-# .send(ops)
-# .end (res) ->
-# expect(res.statusCode).to.be 200
-# console.log {stats:res.body.stats, tasks:res.body.tasks}
-# done()
-#
-#
-# #FIXME figure out how to compare the objects
-# it.skip 'GET /api/v1/user', (done) ->
-# request.get("#{baseURL}/user")
-# .end (res) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 200
-# expect(res.body.id).not.to.be.empty()
-# self = _.clone(currentUser)
-# delete self.apiToken
-# self.stats.toNextLevel = 150
-# self.stats.maxHealth = 50
-#
-# expectUserEqual(res.body, self)
-# done()
-#
-# it.skip 'GET /api/v1/user/task/:id', (done) ->
-# tid = _.pluck(currentUser.tasks, 'id')[0]
-# request.get("#{baseURL}/user/task/#{tid}")
-# .end (res) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 200
-# expect(res.body).to.eql currentUser.tasks[tid]
-# done()
-#
-# it.skip 'POST /api/v1/user/task', (done) ->
-# request.post("#{baseURL}/user/task")
-# .send({title: 'Title', text: 'Text', type: 'habit'})
-# .end (res) ->
-# query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken)
-# query.fetch (err, user) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 201
-# expect(res.body.id).not.to.be.empty()
-# # Ensure that user owns the newly created object
-# saved = user.get("tasks.#{res.body.id}")
-# expect(saved).to.be.an('object')
-# done()
-#
-# it.skip 'POST /api/v1/user/task (without type)', (done) ->
-# request.post("#{baseURL}/user/task")
-# .send({})
-# .end (res) ->
-# expect(res.body.err).to.be 'type must be habit, todo, daily, or reward'
-# expect(res.statusCode).to.be 400
-# done()
-#
-# it.skip 'POST /api/v1/user/task (only type)', (done) ->
-# request.post("#{baseURL}/user/task")
-# .send(type: 'habit')
-# .end (res) ->
-# query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken)
-# query.fetch (err, user) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 201
-# expect(res.body.id).not.to.be.empty()
-# # Ensure that user owns the newly created object
-# expect(user.get().tasks[res.body.id]).to.be.an('object')
-# # Ensure that value gets set to 0 since not otherwise specified
-# expect(user.get().tasks[res.body.id].value).to.be.equal(0)
-# done()
-#
-# it.skip 'PUT /api/v1/user/task/:id', (done) ->
-# tid = _.pluck(currentUser.tasks, 'id')[0]
-# request.put("#{baseURL}/user/task/#{tid}")
-# .send(text: 'bye')
-# .end (res) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 200
-# currentUser.tasks[tid].text = 'bye'
-# expectSameValues res.body, currentUser.tasks[tid], ['id','type','text']
-# #expect(res.body).to.eql currentUser.tasks[tid]
-# done()
-#
-# it.skip 'PUT /api/v1/user/task/:id (shouldnt update type)', (done) ->
-# tid = _.pluck(currentUser.tasks, 'id')[1]
-# type = if currentUser.tasks[tid].type is 'habit' then 'daily' else 'habit'
-# request.put("#{baseURL}/user/task/#{tid}")
-# .send(type: type, text: 'fishman')
-# .end (res) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 200
-# currentUser.tasks[tid].text = 'fishman'
-# expect(res.body).to.eql currentUser.tasks[tid]
-# done()
-#
-# it.skip 'PUT /api/v1/user/task/:id (update notes)', (done) ->
-# tid = _.pluck(currentUser.tasks, 'id')[2]
-# request.put("#{baseURL}/user/task/#{tid}")
-# .send(text: 'hi',notes:'foobar matey')
-# .end (res) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 200
-# currentUser.tasks[tid].text = 'hi'
-# currentUser.tasks[tid].notes = 'foobar matey'
-# expect(res.body).to.eql currentUser.tasks[tid]
-# done()
-#
-# it.skip 'GET /api/v1/user/tasks', (done) ->
-# request.get("#{baseURL}/user/tasks")
-# .end (res) ->
-# query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken)
-# query.fetch (err, user) ->
-# expect(res.body.err).to.be undefined
-# expect(user.get()).to.be.ok()
-# expect(res.statusCode).to.be 200
-# model.ref '_user', user
-# tasks = []
-# for type in ['habit','todo','daily','reward']
-# model.refList "_#{type}List", "_user.tasks", "_user.#{type}Ids"
-# tasks = tasks.concat model.get("_#{type}List")
-# # Ensure that user owns the tasks
-# expect(res.body.length).to.equal tasks.length
-# # Ensure that the two sets are equal
-# expect(_.difference(_.pluck(res.body,'id'), _.pluck(tasks,'id')).length).to.equal 0
-# done()
-#
-# it.skip 'GET /api/v1/user/tasks (todos)', (done) ->
-# request.get("#{baseURL}/user/tasks")
-# .query(type:'todo')
-# .end (res) ->
-# query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken)
-# query.fetch (err, user) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 200
-# model.ref '_user', user
-# model.refList "_todoList", "_user.tasks", "_user.todoIds"
-# tasks = model.get("_todoList")
-# # Ensure that user owns the tasks
-# expect(res.body.length).to.equal tasks.length
-# # Ensure that the two sets are equal
-# expect(_.difference(_.pluck(res.body,'id'), _.pluck(tasks,'id')).length).to.equal 0
-# done()
-#
-# it.skip 'DELETE /api/v1/user/task/:id', (done) ->
-# tid = currentUser.habitIds[2]
-# request.del("#{baseURL}/user/task/#{tid}")
-# .end (res) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 204
-# query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken)
-# query.fetch (err, user) ->
-# expect(user.get('habitIds').indexOf(tid)).to.be -1
-# expect(user.get("tasks.#{tid}")).to.be undefined
-# done()
-#
-# it.skip 'DELETE /api/v1/user/task/:id (no task found)', (done) ->
-# tid = "adsfasdfjunkshouldntbeatask"
-# request.del("#{baseURL}/user/task/#{tid}")
-# .end (res) ->
-# expect(res.statusCode).to.be 400
-# expect(res.body.err).to.be 'No task found.'
-# done()
-#
-# it.skip 'POST /api/v1/user/task/:id/up (habit)', (done) ->
-# tid = currentUser.habitIds[0]
-# request.post("#{baseURL}/user/task/#{tid}/up")
-# .send({})
-# .end (res) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 200
-# expect(res.body).to.eql { gp: 1, exp: 7.5, lvl: 1, hp: 50, delta: 1 }
-# done()
-#
-# it.skip 'POST /api/v1/user/task/:id/up (daily)', (done) ->
-# tid = currentUser.dailyIds[0]
-# request.post("#{baseURL}/user/task/#{tid}/up")
-# .send({})
-# .end (res) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 200
-# expect(res.body).to.eql { gp: 2, exp: 15, lvl: 1, hp: 50, delta: 1 }
-# query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken)
-# query.fetch (err, user) ->
-# expect(user.get("tasks.#{tid}.completed")).to.be true
-# done()
-#
-# it.skip 'POST /api/v1/user/task (array)', (done) ->
-# habitId = currentUser.habitIds[0]
-# dailyId = currentUser.dailyIds[0]
-# arr = [{
-# id: habitId
-# text: 'hello'
-# notes: 'note'
-# },{
-# text: 'new task'
-# notes: 'notes!'
-# },{
-# id: dailyId
-# del: true
-# }]
-#
-# request.post("#{baseURL}/user/tasks")
-# .send(arr)
-# .end (res) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 201
-#
-# expectSameValues res.body[0], {id: habitId,text: 'hello',notes: 'note'}, ['id','text','notes']
-# expect(res.body[1].id).to.be.a 'string'
-# expect(res.body[1].text).to.be 'new task'
-# expect(res.body[1].notes).to.be 'notes!'
-# expect(res.body[2]).to.eql deleted: true
-#
-# query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken)
-# query.fetch (err, user) ->
-# expectSameValues user.get("tasks.#{habitId}"), {id: habitId,text: 'hello',notes: 'note'}, ['id','text','notes']
-# expect(user.get("tasks.#{dailyId}")).to.be undefined
-# expectSameValues user.get("tasks.#{res.body[1].id}"), {id: res.body[1].id, text: 'new task', notes: 'notes!'}, ['id','text','notes']
-# done()
-#
-# it.skip 'PUT /api/v1/user (bad path)', (done) ->
-# # These updates should not save, as per the API changes
-# userUpdates =
-# stats: hp: 30
-# flags: itemsEnabled: true
-# tasks: [{
-# text: 'hello2'
-# notes: 'note2'
-# }]
-#
-# request.put("#{baseURL}/user")
-# .send(userUpdates)
-# .end (res) ->
-# expect(res.body.err).to.be.ok()
-# expect(res.statusCode).to.be 500
-# done()
-#
-# it.skip 'PUT /api/v1/user', (done) ->
-# userBefore = {}
-# query = model.query('users').withIdAndToken(currentUser.id, currentUser.apiToken)
-# query.fetch (err, user) ->
-# userBefore = user.get()
-#
-# habitId = currentUser.habitIds[0]
-# dailyId = currentUser.dailyIds[0]
-# updates = {}
-# updates['stats.hp'] = 30
-# updates['flags.itemsEnabled'] = true
-# updates["tasks.#{habitId}.text"] = 'hello2'
-# updates["tasks.#{habitId}.notes"] = 'note2'
-#
-# request.put("#{baseURL}/user")
-# .send(updates)
-# .end (res) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 200
-# changesWereMade = (obj) ->
-# expect(obj.stats.hp).to.be 30
-# expect(obj.flags.itemsEnabled).to.be true
-# expectSameValues _.find(obj.tasks,{id:habitId}), {id: habitId,text: 'hello2',notes: 'note2'}, ['id','text','notes']
-# changesWereMade res.body
-# query.fetch (err, user) ->
-# changesWereMade user.get()
-# done()
-#
-# it.skip 'POST /api/v1/user/auth/local', (done) ->
-# userAuth = {username, password}
-# request.post("#{baseURL}/user/auth/local")
-# .set('Accept', 'application/json')
-# .send(userAuth)
-# .end (res) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 200
-# expect(res.body.id).to.be currentUser.id
-# expect(res.body.token).to.be currentUser.apiToken
-# done()
-#
-# it.skip 'POST /api/v1/user/auth/facebook', (done) ->
-# id = shared.uuid()
-# userAuth = facebook_id: 12345, name: 'Tyler Renelle', email: 'x@y.com'
-# newUser = helpers.newUser(true)
-# newUser.id = id
-# newUser.auth = facebook:
-# id: userAuth.facebook_id
-# name: userAuth.name
-# email: userAuth.email
-# model.set "users.#{id}", newUser, ->
-#
-# request.post("#{baseURL}/user/auth/facebook")
-# .set('Accept', 'application/json')
-# .send(userAuth)
-# .end (res) ->
-# expect(res.body.err).to.be undefined
-# expect(res.statusCode).to.be 200
-# expect(res.body.id).to.be newUser.id
-# #expect(res.body.token).to.be newUser.apiToken
-# done()
-#
-#
diff --git a/test/archive/casper/dailies.casper.coffee b/test/archive/casper/dailies.casper.coffee
deleted file mode 100644
index 3730b91ad9..0000000000
--- a/test/archive/casper/dailies.casper.coffee
+++ /dev/null
@@ -1,45 +0,0 @@
-helpers = new require('./test/casper/helpers')()
-casper = helpers.casper
-utils = helpers.utils
-url = helpers.playUrl
-
-casper.start url
-
-# ---------- Daily ------------
-casper.then ->
- helpers.reset()
- helpers.addTasks(['daily'])
-
-# Gained exp on +daily
-casper.then ->
- helpers.modelBeforeAfter (-> casper.click '.dailys input[type="checkbox"]'), (model) ->
- casper.test.assertEquals model.before._user.stats.hp, model.after._user.stats.hp, '+daily =hp'
- casper.test.assert model.before._user.stats.exp < model.after._user.stats.exp, '+daily +exp'
- casper.test.assert model.before._user.stats.gp < model.after._user.stats.gp, '+daily +gp'
-
-# -daily acts as undo
-casper.then ->
- helpers.modelBeforeAfter (-> casper.click '.dailys input[type="checkbox"]'), (model) ->
- casper.test.assertEquals model.before._user.stats.hp, model.after._user.stats.hp, '-daily =hp'
- casper.test.assert model.before._user.stats.exp > model.after._user.stats.exp, '-daily -exp'
- casper.test.assert model.before._user.stats.gp > model.after._user.stats.gp, '-daily -gp'
-
-# ---------- Cron ------------
-casper.then ->
- helpers.reset()
- helpers.addTasks(['daily'])
-
-casper.then ->
- helpers.cronBeforeAfter (model) ->
- casper.then ->
- casper.test.assert model.before._user.stats.hp > model.after._user.stats.hp, 'daily:cron:hp lost value'
-
- # Go through all the dailys, all of them are uncompleted, so should all get a negative value
- casper.echo "Testing all uncompleted dailys after cron"
- for id in model.before._user.dailyIds
- casper.test.assertEquals model.before._user.tasks[id].value, 0, "daily:cron:daily value before was 0"
- casper.test.assert model.after._user.tasks[id].value < 0, "daily:cron:daily value after is negative"
-
-# ---------- Run ------------
-casper.run ->
- casper.test.renderResults true
\ No newline at end of file
diff --git a/test/archive/casper/general.casper.coffee b/test/archive/casper/general.casper.coffee
deleted file mode 100644
index 8ce7280ab8..0000000000
--- a/test/archive/casper/general.casper.coffee
+++ /dev/null
@@ -1,38 +0,0 @@
-helpers = new require('./test/casper/helpers')()
-casper = helpers.casper
-utils = helpers.utils
-url = helpers.playUrl
-
-# ---------- Basic Reset Test ------------
-
-casper.start url, ->
- casper.test.assertTitle 'HabitRPG | Gamify Your Life', 'Page Title'
-
-# Gain some GP and lose some HP
-casper.then ->
- casper.repeat 5, -> casper.click '.habits a[data-direction="down"]'
- casper.repeat 5, -> casper.click '.habits a[data-direction="up"]'
-
-# Reset
-casper.then ->
- helpers.reset()
-
-# Test that reset worked
-casper.then ->
- model = helpers.getModelDelayed (model) ->
- casper.echo 'testing user after reset'
- casper.test.assertEqual model._user.tasks, {}, 'no tasks'
- casper.test.assertEqual model._user.stats, {hp:50, gp:0, exp:0, lvl:1}, 'stats'
-
-
-# ---------- Misc Pages ------------
-
-casper.thenOpen "#{helpers.baseUrl}/terms", ->
- casper.test.assertTitle "Terms Of Use", "terms page works"
-
-casper.thenOpen "#{helpers.baseUrl}/privacy", ->
- casper.test.assertTitle "Privacy Policy", "privacy page works"
-
-# ---------- Run ------------
-casper.run ->
- casper.test.renderResults true
\ No newline at end of file
diff --git a/test/archive/casper/habits.casper.coffee b/test/archive/casper/habits.casper.coffee
deleted file mode 100644
index 075b2ba434..0000000000
--- a/test/archive/casper/habits.casper.coffee
+++ /dev/null
@@ -1,40 +0,0 @@
-helpers = new require('./test/casper/helpers')()
-casper = helpers.casper
-utils = helpers.utils
-url = helpers.playUrl
-
-casper.start url
-
-# # ---------- Register ------------
-# casper.then -> helpers.register()
-# casper.then -> user = helpers.getUser()
-
-# ---------- Habits ------------
-casper.then ->
- helpers.reset()
- helpers.addTasks(['habit'])
-
-casper.then ->
- helpers.modelBeforeAfter (-> casper.click '.habits a[data-direction="down"]'), (model) ->
- casper.test.assert model.before._user.stats.hp > model.after._user.stats.hp, '-habit -hp'
- casper.test.assertEquals model.before._user.stats.exp, model.after._user.stats.exp, '-habit =exp'
- casper.test.assertEquals model.before._user.stats.gp, model.after._user.stats.gp, '-habit =gp'
-
-casper.then ->
- helpers.modelBeforeAfter (-> casper.click '.habits a[data-direction="up"]'), (model) ->
- casper.test.assert model.before._user.stats.exp < model.after._user.stats.exp, '+habit +exp'
- casper.test.assert model.before._user.stats.gp < model.after._user.stats.gp, '+habit +gp'
- casper.test.assertEquals model.before._user.stats.hp, model.after._user.stats.hp, '+habit =hp'
-
-# Test Death
-casper.then ->
- casper.repeat 50, (-> casper.click '.habits a[data-direction="down"]')
- casper.then ->
- helpers.getModelDelayed (model) ->
- casper.test.assertEquals model._user.stats.hp, 0, 'hp==0 (death by habits)'
- casper.test.assertEquals model._user.stats.lvl, 0, 'lvl==0 (death by habits)'
- casper.test.assertVisible '#dead-modal', 'Revive Modal Visible'
-
-# ---------- Run ------------
-casper.run ->
- casper.test.renderResults true
\ No newline at end of file
diff --git a/test/archive/casper/helpers.coffee b/test/archive/casper/helpers.coffee
deleted file mode 100644
index 138da7d03f..0000000000
--- a/test/archive/casper/helpers.coffee
+++ /dev/null
@@ -1,106 +0,0 @@
-utils = require('utils')
-
-module.exports = ->
-
- SYNC_WAIT_TIME = 40
-
- baseUrl = 'http://localhost:3000'
-
- random = Math.random().toString(36).substring(7)
-
- casper = require("casper").create
- clientScripts: 'test/includes/lodash.min.js'
-
- getModel = ->
- casper.evaluate ->
- model = window.DERBY.app.model
- {
- _userId: model.get('_userId')
- _user: model.get('_user')
- _todoList: model.get('_todoList')
- _dailyList: model.get('_dailyList')
- _rewardList: model.get('_rewardList')
- _habitList: model.get('_habitList')
- }
-
- {
- casper: casper
-
- baseUrl: baseUrl
- playUrl: baseUrl + '/?play=1'
-
- utils: utils
-
- addTasks: (types = 'all', num = 5)->
- if types == 'all'
- types = ['habit', 'daily', 'todo', 'reward']
- types.forEach (type) ->
- i = 0
- casper.repeat num, ->
- casper.fill "form#new-#{type}", {'new-task': "#{type}-#{i}"} # why can't I use true here?
- casper.click "form#new-#{type} input[type=submit]"
-
- reset: ->
- casper.evaluate -> window.DERBY.app.reset()
-
- getModelDelayed: (cb) ->
- # This time is needed for derby to have enough time to update all it's data.
- # It still happens sometimes that the retrieved model does not contain any
- # data. It might be worth to do some basic checks on the model here, and if
- # it doesn't look OK, wait a bit longer and get it again.
- casper.wait SYNC_WAIT_TIME, ->
- cb(getModel())
-
- modelBeforeAfter: (between_cb, done_cb) ->
- that = @
- model = {}
- @getModelDelayed (before) ->
- model.before = before
- casper.then ->
- between_cb()
- that.getModelDelayed (after) ->
- model.after = after
- casper.then -> done_cb(model)
-
- runCron: ->
- casper.evaluate ->
- yesterday = new Date()
- yesterday.setDate(yesterday.getDate() - 1);
- window.DERBY.app.model.set('_user.lastCron', yesterday)
- casper.then -> casper.reload()
-
- cronBeforeAfter: (cb) ->
- that = @
- model = {}
- @getModelDelayed (before) ->
- model.before = before
- casper.then -> that.runCron()
- casper.then ->
- casper.wait 1100, -> # user's hp is updated after 1s for animation
- model.after = getModel()
- casper.then ->
- casper.test.assertEqual model.before._user.id, model.after._user.id, 'user id equal after cron'
- casper.test.assertEqual model.before._user.tasks.length, model.after._user.tasks.length, "Didn't lose anything on cron"
- cb(model)
-
- register: ->
- casper.fill 'form#derby-auth-register',
- username: random
- email: random + '@example.com'
- password: random
- 'password-confirmation': random
- , true
-
- login: ->
- casper.fill 'form#derby-auth-login',
- username: random
- password: random
- , true
-
- deleteOne: (list, typeSelector) ->
- selector = "#{typeSelector} a[data-original-title=\"Delete\"]"
- @modelBeforeAfter (-> casper.click selector), (model) ->
- casper.test.assertEquals Object.keys(model.before._user.tasks).length - 1, Object.keys(model.after._user.tasks).length, "1 #{typeSelector} deleted from user.tasks"
- casper.test.assertEquals model.before._user["#{list}Ids"].length - 1, model.after._user["#{list}Ids"].length, "1 #{typeSelector} deleted from user._typeIds"
- casper.test.assertEquals model.before["_#{list}List"].length - 1, model.after["_#{list}List"].length, "1 #{typeSelector} deleted from _typeList"
- }
diff --git a/test/archive/casper/items.casper.coffee b/test/archive/casper/items.casper.coffee
deleted file mode 100644
index 853aa3f127..0000000000
--- a/test/archive/casper/items.casper.coffee
+++ /dev/null
@@ -1,22 +0,0 @@
-helpers = new require('./test/casper/helpers')()
-casper = helpers.casper
-utils = helpers.utils
-url = helpers.playUrl
-
-casper.start url
-
-# ---------- Items (in-game rewards) ------------
-casper.then ->
- helpers.reset()
- helpers.addTasks(['habit'], 1)
-
-casper.then -> casper.test.assertDoesntExist 'ul.items', 'no items after reset'
-casper.then -> casper.repeat 70, ->
- casper.click '.habits a[data-direction="up"]'
-casper.then ->
- casper.test.assertVisible '.item-store-popover', 'store popover visible'
- casper.test.assertExists 'ul.items', 'items appear after lvl up'
-
-# ---------- Run ------------
-casper.run ->
- casper.test.renderResults true
\ No newline at end of file
diff --git a/test/archive/casper/memoryleak.casper.coffee b/test/archive/casper/memoryleak.casper.coffee
deleted file mode 100644
index ff5357a90f..0000000000
--- a/test/archive/casper/memoryleak.casper.coffee
+++ /dev/null
@@ -1,12 +0,0 @@
-helpers = new require('./test/casper/helpers')()
-casper = helpers.casper
-utils = helpers.utils
-url = helpers.playUrl
-
-casper.start url
-
-casper.repeat 100, ->
- casper.reload()
-
-casper.run ->
- casper.test.renderResults true
\ No newline at end of file
diff --git a/test/archive/casper/registration.casper.coffee b/test/archive/casper/registration.casper.coffee
deleted file mode 100644
index e3e1a53574..0000000000
--- a/test/archive/casper/registration.casper.coffee
+++ /dev/null
@@ -1,35 +0,0 @@
-helpers = new require('./test/casper/helpers')()
-casper = helpers.casper
-utils = helpers.utils
-url = helpers.playUrl
-
-casper.start url
-
-# ---------- Register ------------
-registeredUser = undefined
-casper.then -> helpers.register()
-casper.then ->
- helpers.getModelDelayed (model) ->
- registeredUser = model
-
-casper.then -> casper.reload()
-casper.then ->
- helpers.getModelDelayed (nowModel) ->
- casper.test.assertEqual registeredUser._userId, nowModel._userId, 'user registered and maintained session'
-
-# ---------- Log Out ------------
-casper.thenOpen helpers.baseUrl + '/logout'
-casper.thenOpen helpers.playUrl
-casper.then ->
- helpers.getModelDelayed (nowModel) ->
- casper.test.assertNotEquals registeredUser._userId, nowModel._userId, 'user logged out'
-
-# ---------- Login ------------
-casper.then -> helpers.login()
-casper.then ->
- helpers.getModelDelayed (nowModel) ->
- casper.test.assertEqual registeredUser._userId, nowModel._userId, 'user logged out'
-
-# ---------- Run ------------
-casper.run ->
- casper.test.renderResults true
\ No newline at end of file
diff --git a/test/archive/casper/rest.casper.coffee b/test/archive/casper/rest.casper.coffee
deleted file mode 100644
index 767d93489e..0000000000
--- a/test/archive/casper/rest.casper.coffee
+++ /dev/null
@@ -1,74 +0,0 @@
-url = 'http://localhost:3000'
-utils = require('utils')
-casper = require("casper").create()
-
-user1 = {}
-user2 = {}
-
-# ---------- Main Stuff ------------
-
-casper.start "#{url}/?play=1", ->
- user1 = casper.evaluate -> window.DERBY.app.model.get("_user")
- @fill 'form#derby-auth-register',
- username: user1.id
- email: "{user1.id}@gmail.com"
- password: 'habitrpg123'
- 'password-confirmation': "habitrpg123"
- , true
-casper.thenOpen "#{url}/logout"
-casper.thenOpen "#{url}/?play=1", ->
- user2 = @evaluate -> window.DERBY.app.model.get("_user")
- casper.then -> @test.assertNotEquals user1.id, user2.id, '2 new users created'
-
-
- # ---------- REST API ------------
-
- # casper.thenOpen "#{url}/users/#{user.id}"
- # casper.thenOpen "#{url}/users/#{user.id}/tasks"
- # casper.thenOpen "#{url}/users/#{user.id}/tasks/{taskId}"
-
- taskId = 'productivity'
- pomodoro = {
- 'title': 'Stay Focused',
- 'service': 'pomodoro',
- 'icon': 'http://www.veryicon.com/icon/16/Food%20%26%20Drinks/Paradise%20Fruits/Tomato.png'
- }
-
- # ---------- v1 ------------
-
- @thenOpen "#{url}/users/#{user2.id}/tasks/#{taskId}/up", {
- method: 'post',
- data: pomodoro
- }, ->
- result = JSON.parse @getPageContent()
- @test.assertEqual user2.stats.hp, result.hp, 'REST +habit =hp'
- @test.assert user2.stats.exp < result.exp, 'REST +habit +exp'
- @test.assert user2.stats.money < result.money, 'REST +habit +money'
- utils.dump result
-
- @thenOpen "#{url}/users/#{user1.id}/tasks/#{taskId}/down", {
- method: 'post',
- data: pomodoro
- }, ->
- result = JSON.parse @getPageContent()
- @test.assert user1.stats.hp > result.hp, 'REST -habit -hp'
- @test.assertEqual user1.stats.exp, result.exp, 'REST -habit =exp'
- @test.assertEqual user1.stats.money, result.money, 'REST -habit =money'
- utils.dump result
-
-casper.thenOpen "#{url}/?play=1", ->
- # User2 is logged in by now. Make sure we don't get logged in as user1 since that was the last REST call
- current = casper.evaluate -> window.DERBY.app.model.get('_user')
- casper.then -> casper.test.assertEqual current.id, user2.id, "session remains user2's"
-
- # ---------- v2 ------------
-
-# @thenOpen "#{url}/v2/users/#{uid}/tasks/#{taskId}/down", {
-# method: 'post',
-# data: pomodoro
-# }
-
-# ---------- Run ------------
-
-casper.run ->
- @test.renderResults true
\ No newline at end of file
diff --git a/test/archive/casper/todos.casper.coffee b/test/archive/casper/todos.casper.coffee
deleted file mode 100644
index f898f68b12..0000000000
--- a/test/archive/casper/todos.casper.coffee
+++ /dev/null
@@ -1,59 +0,0 @@
-helpers = new require('./test/casper/helpers')()
-casper = helpers.casper
-utils = helpers.utils
-url = helpers.playUrl
-
-casper.start url
-
-# ---------- Todos ------------
-casper.then ->
- helpers.reset()
- helpers.addTasks(['todo'])
-
-# Completing todo gives exp and gp
-casper.then ->
- helpers.modelBeforeAfter (-> casper.click '.todos .uncompleted input[type="checkbox"]'), (model) ->
- casper.test.assertEquals model.before._user.stats.hp, 50, 'todo:hp starts at 50'
- casper.test.assertEquals model.before._user.stats.hp, model.after._user.stats.hp, '+todo =hp'
- casper.test.assertEquals model.after._user.stats.exp, 1, '+todo exp=1'
- casper.test.assertEquals model.after._user.stats.gp, 1, '+todo gp=1'
-
- casper.test.assert model.before._user.stats.exp < model.after._user.stats.exp, '+todo +exp'
- casper.test.assert model.before._user.stats.gp < model.after._user.stats.gp, '+todo +gp'
-
-# Can delete completed
-casper.then -> helpers.deleteOne('todo', '.todos .completed')
-
-# Can delete uncompleted
-casper.then -> helpers.deleteOne('todo', '.todos .uncompleted')
-
-# Uncompleting subtracts exp and gp
-casper.then ->
- casper.click '.todos .uncompleted input[type="checkbox"]'
- helpers.modelBeforeAfter (-> casper.click '.todos .completed input[type="checkbox"]'), (model) ->
- casper.test.assertEquals model.before._user.stats.hp, model.after._user.stats.hp, '-todo =hp'
- casper.test.assert model.before._user.stats.exp > model.after._user.stats.exp, '-todo -exp'
- casper.test.assert model.before._user.stats.gp > model.after._user.stats.gp, '-todo -gp'
-
-
-
-# ---------- Cron ------------
-casper.then ->
- helpers.reset()
- helpers.addTasks(['todo'])
-
-casper.then ->
- helpers.cronBeforeAfter (model) ->
- casper.then ->
- casper.test.assertEqual model.before._user.stats.hp, model.after._user.stats.hp, 'todo:cron:hp no change'
-
- # Go through all the todos, all of them are uncompleted, so should all get a negative value
- casper.echo "Testing all uncompleted todos after cron"
- for id in model.before._user.todoIds
- casper.test.assertEquals model.before._user.tasks[id].value, 0, "todo:cron:todo value before was 0"
- casper.test.assert model.after._user.tasks[id].value < 0, "todo:cron:todo value after is negative"
-
-
-# ---------- Run ------------
-casper.run ->
- casper.test.renderResults true
\ No newline at end of file
diff --git a/test/archive/test2/casper/add.items.coffee b/test/archive/test2/casper/add.items.coffee
deleted file mode 100644
index 9094b67c77..0000000000
--- a/test/archive/test2/casper/add.items.coffee
+++ /dev/null
@@ -1,40 +0,0 @@
-helpers = casper.helpers
-uid = helpers.uid
-# test creation of Todos, Rewards, Habits, etc
-
-casper.start helpers.playUrl, ->
- casper.test.assert(true,'true==true')
-# helpers.getModel (err, model) ->
-# tasksCount=0
-# for own key,value of model.user.tasks
-# tasksCount++
-# utils.dump model.user.tasks
-# casper.fill "form[data-task-type='habit']", {
-# 'new-task': 'Habit' + uid
-# }, true
-# helpers.getModel (err, model) ->
-# newTasksCount=0
-# for own key,value of model.user.tasks
-# newTasksCount++
-# casper.echo "Tasks new: " + tasksCount
-# casper.test.assert(newTasksCount>tasksCount,"Task list increases in length after new habit form submitted")
-#
-#
-##casper.then ->
-## helpers.getModel (err, model) ->
-## casper.test.assertEquals(typeof model.user.stats.exp, "number", 'XP is number')
-## casper.test.assertEquals(model.user.stats.exp, 0, 'XP == 0')
-## casper.click '.habits a[data-direction="up"]'
-## helpers.getModel (err, newModel) ->
-## casper.test.assert(newModel.user.stats.exp > model.user.stats.exp, 'XP has increased after clicking habits "+"')
-## casper.test.assert(newModel.user.stats.gp > model.user.stats.gp, 'GP has increased after clicking habits "+"')
-
-
-# ---------- finish tests ------------
-casper.then ->
- casper.test.done()
-
-
-# ---------- Run ------------
-casper.run ->
- casper.test.renderResults true
diff --git a/test/archive/test2/casper/stats.coffee b/test/archive/test2/casper/stats.coffee
deleted file mode 100644
index d134f518ee..0000000000
--- a/test/archive/test2/casper/stats.coffee
+++ /dev/null
@@ -1,103 +0,0 @@
-#helpers = new require('./lib/helpers')()
-#casper = helpers.casper
-helpers = casper.helpers
-eTest = helpers.evalTest
-getModel = helpers.getModel
-# ---------- Checks if clicking on the buttons changes stats and in right direction ------------
-
-
-casper.start helpers.playUrl, ->
- test = 'EXP and GP increasing after clicking habits "+"'
- getModel (err, model) ->
- casper.test.assertEquals(typeof model.user.stats.exp, "number", 'XP is number')
- casper.test.assertEquals(model.user.stats.exp, 0, 'XP == 0')
- casper.click '.habits a[data-direction="up"]'
- eTest(
- (oldStats)->
- stats = window.DERBY.app.model.get '_user.stats'
- console.log "Was:" + oldStats.exp + "Is: " + stats.exp
- console.log "Was:" + oldStats.gp + "Is: " + stats.gp
- (stats.exp > oldStats.exp && stats.gp > oldStats.gp)
- test
- model.user.stats
- )
-
-
-casper.then ->
- test = 'HP decreasing after clicking habits "-"'
- getModel (err, model) ->
- casper.click '.habits a[data-direction="down"]'
- eTest(
- (oldStats)->
- stats = window.DERBY.app.model.get '_user.stats'
- console.log "Was:" + oldStats.hp + "Is: " + stats.hp
- (stats.hp < oldStats.hp)
- test
- model.user.stats
- )
-
-casper.then ->
- test = 'EXP and GP increasing after clicking .todo.uncompleted'
- getModel (err, model) ->
- casper.click '.task.todo.uncompleted input[type=checkbox]'
- eTest(
- (oldStats)->
- stats = window.DERBY.app.model.get '_user.stats'
- console.log "Was:" + oldStats.exp + "Is: " + stats.exp
- console.log "Was:" + oldStats.gp + "Is: " + stats.gp
- (stats.exp > oldStats.exp && stats.gp > oldStats.gp)
- test
- model.user.stats
- )
-
-casper.then ->
- test = 'EXP and GP decreasing after clicking .todo.completed'
- getModel (err, model) ->
- casper.click '.task.todo.completed input[type=checkbox]'
- eTest(
- (oldStats)->
- stats = window.DERBY.app.model.get '_user.stats'
- console.log "Was:" + oldStats.exp + "Is: " + stats.exp
- console.log "Was:" + oldStats.gp + "Is: " + stats.gp
- (stats.exp < oldStats.exp && stats.gp < oldStats.gp)
- test
- model.user.stats
- )
-
-casper.then ->
- test = 'EXP and GP increasing after clicking .daily.uncompleted'
- getModel (err, model) ->
- casper.click '.task.daily.uncompleted input[type=checkbox]'
- eTest(
- (oldStats)->
- stats = window.DERBY.app.model.get '_user.stats'
- console.log "Was:" + oldStats.exp + "Is: " + stats.exp
- console.log "Was:" + oldStats.gp + "Is: " + stats.gp
- (stats.exp > oldStats.exp && stats.gp > oldStats.gp)
- test
- model.user.stats
- )
-
-casper.then ->
- test = 'EXP and GP decreasing after clicking .daily.completed'
- getModel (err, model) ->
- casper.click '.task.daily.completed input[type=checkbox]'
- eTest(
- (oldStats)->
- stats = window.DERBY.app.model.get '_user.stats'
- console.log "Was:" + oldStats.exp + "Is: " + stats.exp
- console.log "Was:" + oldStats.gp + "Is: " + stats.gp
- (stats.exp < oldStats.exp && stats.gp < oldStats.gp)
- test
- model.user.stats
- )
-
-
-# ---------- finish tests ------------
-casper.then ->
- casper.test.done()
-
-
-# ---------- Run ------------
-casper.run ->
- casper.test.renderResults true
\ No newline at end of file
diff --git a/test/archive/test2/readme.md b/test/archive/test2/readme.md
deleted file mode 100644
index a0567235cb..0000000000
--- a/test/archive/test2/readme.md
+++ /dev/null
@@ -1,9 +0,0 @@
-1) It is a bit verbose
-2) It needs to be refactored using "async" library. Somehow casper.then() is introducing bugs so I'm not going to use it until I figure out why exactly. Leave it as it is, I'll refactor as it grows.
-3) It does not use any timeouts along the way, which is a good thing.
-4) It runs consistently on my side (i.e. always passes).
-5) So far phantomJS is given new userID every time so app.reset,app.revive is not necessary. I'll add it later.
-
-To run the tests run from terminal:
-cd test/test2
-casperjs test ./casper/ --includes=./lib/helpers.coffee
\ No newline at end of file
diff --git a/test/common/algos.mocha.coffee b/test/common/algos.mocha.coffee
index b413c3e000..4e911ca06d 100644
--- a/test/common/algos.mocha.coffee
+++ b/test/common/algos.mocha.coffee
@@ -2,7 +2,7 @@ _ = require 'lodash'
expect = require 'expect.js'
sinon = require 'sinon'
moment = require 'moment'
-shared = require '../../common/script/index.coffee'
+shared = require '../../common/script/index.js'
shared.i18n.translations = require('../../website/src/libs/i18n.js').translations
test_helper = require './test_helper'
test_helper.addCustomMatchers()
diff --git a/test/common/count.js b/test/common/count.js
index b9040a71f1..86d746d3ff 100644
--- a/test/common/count.js
+++ b/test/common/count.js
@@ -3,7 +3,6 @@ var chai = require("chai")
chai.use(require("sinon-chai"))
var expect = chai.expect
-require('coffee-script');
var count = require('../../common/script/count');
describe('count', function() {
@@ -27,9 +26,9 @@ describe('count', function() {
});
it('does not count pets hatched with premium potions', function() {
- var pets = {
- "Wolf-Spooky": 5,
- "Dragon-Spooky": 5,
+ var pets = {
+ "Wolf-Spooky": 5,
+ "Dragon-Spooky": 5,
"FlyingPig-Base": 5
}
var beastMasterTotal = count.beastMasterProgress(pets);
diff --git a/test/common/dailies.coffee b/test/common/dailies.coffee
index e5e3fad81a..71729ff77c 100644
--- a/test/common/dailies.coffee
+++ b/test/common/dailies.coffee
@@ -2,7 +2,7 @@ _ = require 'lodash'
expect = require 'expect.js'
sinon = require 'sinon'
moment = require 'moment'
-shared = require '../../common/script/index.coffee'
+shared = require '../../common/script/index.js'
shared.i18n.translations = require('../../website/src/libs/i18n.js').translations
repeatWithoutLastWeekday = ()->
diff --git a/test/common/simulations/autoAllocate.coffee b/test/common/simulations/autoAllocate.coffee
index 6b21b9ca9d..fec7ccbb31 100644
--- a/test/common/simulations/autoAllocate.coffee
+++ b/test/common/simulations/autoAllocate.coffee
@@ -6,7 +6,7 @@
Current results at https://gist.github.com/lefnire/8049676
###
-shared = require '../../../common/script/index.coffee'
+shared = require '../../../common/script/index.js'
_ = require 'lodash'
$w = (s)->s.split(' ')
diff --git a/test/common/simulations/passive_active_attrs.coffee b/test/common/simulations/passive_active_attrs.coffee
index 11646ae933..0788488ec2 100644
--- a/test/common/simulations/passive_active_attrs.coffee
+++ b/test/common/simulations/passive_active_attrs.coffee
@@ -6,7 +6,7 @@
Current results at https://gist.github.com/lefnire/8049676
###
-shared = require '../../../common/script/index.coffee'
+shared = require '../../../common/script/index.js'
_ = require 'lodash'
id = shared.uuid()
diff --git a/test/common/user.fns.buy.test.js b/test/common/user.fns.buy.test.js
index 05f7abb78d..df29497dc7 100644
--- a/test/common/user.fns.buy.test.js
+++ b/test/common/user.fns.buy.test.js
@@ -4,8 +4,7 @@ chai.use(require("sinon-chai"))
var expect = chai.expect
var _ = require('lodash');
-require('coffee-script');
-var shared = require('../../common/script/index.coffee');
+var shared = require('../../common/script/index.js');
describe('user.fns.buy', function() {
var user;
diff --git a/test/common/user.fns.ultimateGear.test.js b/test/common/user.fns.ultimateGear.test.js
index d2b82f43e8..988313d5cc 100644
--- a/test/common/user.fns.ultimateGear.test.js
+++ b/test/common/user.fns.ultimateGear.test.js
@@ -1,6 +1,6 @@
'use strict';
-var shared = require('../../common/script/index.coffee');
+var shared = require('../../common/script/index.js');
shared.i18n.translations = require('../../website/src/libs/i18n.js').translations
require('./test_helper');
diff --git a/test/common/user.ops.buyMysterySet.test.js b/test/common/user.ops.buyMysterySet.test.js
index f0a4a7b2f8..1f88b45802 100644
--- a/test/common/user.ops.buyMysterySet.test.js
+++ b/test/common/user.ops.buyMysterySet.test.js
@@ -3,9 +3,8 @@ var chai = require("chai")
chai.use(require("sinon-chai"))
var expect = chai.expect
-require('coffee-script');
-var shared = require('../../common/script/index.coffee');
-var Content = require('../../common/script/content/index.coffee');
+var shared = require('../../common/script/index.js');
+var Content = require('../../common/script/content/index.js');
describe('user.ops.buyMysterySet', function() {
var user;
@@ -51,7 +50,7 @@ describe('user.ops.buyMysterySet', function() {
armor_mystery_301404: true,
head_mystery_301404: true,
eyewear_mystery_301404: true
- };
+ };
user.ops.buyMysterySet({params:{key:'301404'}}, function(response) {
expect(response.message).to.eql("Mystery set not found, or set already owned");
diff --git a/test/common/user.ops.hatch.js b/test/common/user.ops.hatch.js
index 2ec1d627d3..b5cf174ff9 100644
--- a/test/common/user.ops.hatch.js
+++ b/test/common/user.ops.hatch.js
@@ -3,9 +3,8 @@ var chai = require('chai');
chai.use(require('sinon-chai'))
var expect = chai.expect
-require('coffee-script');
-var shared = require('../../common/script/index.coffee');
-var content = require('../../common/script/content/index.coffee');
+var shared = require('../../common/script/index.js');
+var content = require('../../common/script/content/index.js');
describe('user.ops.hatch', function() {
var user;
diff --git a/test/common/user.ops.hourglassPurchase.test.js b/test/common/user.ops.hourglassPurchase.test.js
index 458029c6df..76e3f98c9c 100644
--- a/test/common/user.ops.hourglassPurchase.test.js
+++ b/test/common/user.ops.hourglassPurchase.test.js
@@ -3,9 +3,8 @@ var chai = require("chai")
chai.use(require("sinon-chai"))
var expect = chai.expect
-require('coffee-script');
-var shared = require('../../common/script/index.coffee');
-var Content = require('../../common/script/content/index.coffee');
+var shared = require('../../common/script/index.js');
+var Content = require('../../common/script/content/index.js');
describe('user.ops.hourglassPurchase', function() {
var user;
diff --git a/test/common/user.ops.test.js b/test/common/user.ops.test.js
index 6e26d5430a..aee5b795f7 100644
--- a/test/common/user.ops.test.js
+++ b/test/common/user.ops.test.js
@@ -4,8 +4,7 @@ chai.use(require("sinon-chai"))
var expect = chai.expect
var _ = require('lodash');
-require('coffee-script');
-var shared = require('../../common/script/index.coffee');
+var shared = require('../../common/script/index.js');
describe('user.ops', function() {
var user;
diff --git a/test/helpers/api-integration.helper.js b/test/helpers/api-integration.helper.js
index b2b0ac99c6..b6b3464c9b 100644
--- a/test/helpers/api-integration.helper.js
+++ b/test/helpers/api-integration.helper.js
@@ -8,7 +8,6 @@ import {MongoClient as mongo} from 'mongodb';
import {v4 as generateUUID} from 'uuid';
import superagent from 'superagent';
import i18n from '../../common/script/src/i18n';
-require('coffee-script');
i18n.translations = require('../../website/src/libs/i18n.js').translations;
const API_TEST_SERVER_PORT = 3003;
diff --git a/test/helpers/content.helper.js b/test/helpers/content.helper.js
index c54a62d508..db95a1f0b3 100644
--- a/test/helpers/content.helper.js
+++ b/test/helpers/content.helper.js
@@ -2,7 +2,6 @@ require('./globals.helper');
import {each} from 'lodash';
import i18n from '../../common/script/src/i18n';
-require('coffee-script');
i18n.translations = require('../../website/src/libs/i18n.js').translations;
export const STRING_ERROR_MSG = 'Error processing the string. Please see Help > Report a Bug.';
diff --git a/website/src/controllers/api-v2/user.js b/website/src/controllers/api-v2/user.js
index caf3c092a4..5e40f8162f 100644
--- a/website/src/controllers/api-v2/user.js
+++ b/website/src/controllers/api-v2/user.js
@@ -1,5 +1,3 @@
-/* @see ./routes.coffee for routing*/
-
var url = require('url');
var ipn = require('paypal-ipn');
var _ = require('lodash');
@@ -66,10 +64,6 @@ var findTask = function(req, res) {
---------------
*/
-/**
- This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login
- Export it also so we can call it from deprecated.coffee
-*/
api.score = function(req, res, next) {
var id = req.params.id,
direction = req.params.direction,
@@ -546,7 +540,7 @@ api.sessionPartyInvite = function(req,res,next){
}
/**
- * All other user.ops which can easily be mapped to habitrpg-shared/index.coffee, not requiring custom API-wrapping
+ * All other user.ops which can easily be mapped to common/script/index.js, not requiring custom API-wrapping
*/
_.each(shared.wrap({}).ops, function(op,k){
if (!api[k]) {
diff --git a/website/src/controllers/payments/index.js b/website/src/controllers/payments/index.js
index fd7f98a9c2..465fb31d01 100644
--- a/website/src/controllers/payments/index.js
+++ b/website/src/controllers/payments/index.js
@@ -1,4 +1,3 @@
-/* @see ./routes.coffee for routing*/
var _ = require('lodash');
var shared = require('../../../../common');
var nconf = require('nconf');
diff --git a/website/src/controllers/payments/paypalBillingSetup.js b/website/src/controllers/payments/paypalBillingSetup.js
index ce238338f1..8130de99cc 100644
--- a/website/src/controllers/payments/paypalBillingSetup.js
+++ b/website/src/controllers/payments/paypalBillingSetup.js
@@ -2,7 +2,6 @@
// payment plan definitions, instead you have to create it via their REST SDK and keep it updated the same way. So this
// file will be used once for initing your billing plan (then you get the resultant plan.id to store in config.json),
// and once for any time you need to edit the plan thereafter
-require('coffee-script');
var path = require('path');
var nconf = require('nconf');
_ = require('lodash');
@@ -90,4 +89,4 @@ switch(OP) {
});
break;
case "remove": break;
-}
\ No newline at end of file
+}
diff --git a/website/src/libs/analytics.js b/website/src/libs/analytics.js
index 769a5cf8e1..442c4b9b5c 100644
--- a/website/src/libs/analytics.js
+++ b/website/src/libs/analytics.js
@@ -1,4 +1,3 @@
-require('coffee-script');
require('./i18n');
var _ = require('lodash');
diff --git a/website/src/routes/api-v2/swagger.js b/website/src/routes/api-v2/swagger.js
index d93cfe6271..a1500ac39a 100644
--- a/website/src/routes/api-v2/swagger.js
+++ b/website/src/routes/api-v2/swagger.js
@@ -2,13 +2,11 @@
---------- /api/v2 API ------------
see https://github.com/wordnik/swagger-node-express
Every url added to router is prefaced by /api/v2
-Note: Many user-route ops exist in ../../common/script/index.coffee#user.ops, so that they can (1) be called both
+Note: Many user-route ops exist in ../../common/script/index.js#user.ops, so that they can (1) be called both
client and server.
v1 user. Requires x-api-user (user id) and x-api-key (api key) headers, Test with:
-$ mocha test/user.mocha.coffee
*/
-require('coffee-script');
var user = require("../../controllers/api-v2/user");
var groups = require("../../controllers/api-v2/groups");
var members = require("../../controllers/api-v2/members");
@@ -295,7 +293,7 @@ module.exports = function(swagger, v2) {
spec: {
method: 'POST',
description: "Casts a spell on a target.",
- parameters: [path('spell', "The key of the spell to cast (see ../../common#content/index.coffee)", 'string'), query('targetType', "The type of object you're targeting", 'string', ['party', 'self', 'user', 'task']), query('targetId', "The ID of the object you're targeting", 'string')]
+ parameters: [path('spell', "The key of the spell to cast (see ../../common#content/index.js)", 'string'), query('targetType', "The type of object you're targeting", 'string', ['party', 'self', 'user', 'task']), query('targetId', "The ID of the object you're targeting", 'string')]
},
action: user.cast
},
@@ -772,6 +770,6 @@ module.exports = function(swagger, v2) {
swagger["add" + route.spec.method](route);
return true;
});
-
+
return swagger.configure((nconf.get('BASE_URL')) + "/api/v2", "2");
};
diff --git a/website/src/server.js b/website/src/server.js
index 94bffe592b..f166cfb190 100644
--- a/website/src/server.js
+++ b/website/src/server.js
@@ -20,7 +20,6 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
});
} else {
- require('coffee-script'); // remove this once we've fully converted over
var express = require("express");
var http = require("http");
var path = require("path");
@@ -100,7 +99,7 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
// Route requests to the right app
app.use(app.router);
// Matches all request except the ones going to /api/v3/**
- app.all(/^(?!\/api\/v3).+/i, oldApp);
+ app.all(/^(?!\/api\/v3).+/i, oldApp);
// Matches all requests going to /api/v3
app.all('/api/v3', newApp);
@@ -159,4 +158,4 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
});
module.exports = server;
-}
\ No newline at end of file
+}