From 774a42d67e46a89956fdab6bf94735a3d5ae7222 Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Wed, 11 Nov 2015 20:32:00 -0600 Subject: [PATCH] Convert coffee files to JS --- common/index.js | 2 +- common/script/content/faq.js | 1 - common/script/content/index.js | 7005 ++++++++++++++++++++++++++ common/script/content/translation.js | 3 +- common/script/count.js | 3 +- common/script/i18n.js | 51 + common/script/index.coffee | 1936 ------- common/script/index.js | 2864 +++++++++++ 8 files changed, 9923 insertions(+), 1942 deletions(-) create mode 100644 common/script/content/index.js create mode 100644 common/script/i18n.js delete mode 100644 common/script/index.coffee create mode 100644 common/script/index.js 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.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); + } + }); +};