diff --git a/migrations/20170120_missing_incentive.js b/migrations/20170120_missing_incentive.js new file mode 100644 index 0000000000..8ee3c82c1b --- /dev/null +++ b/migrations/20170120_missing_incentive.js @@ -0,0 +1,113 @@ +var migrationName = '20170120_missing_incentive.js'; +var authorName = 'Sabe'; // in case script author needs to know when their ... +var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done + +/* + * Award missing Royal Purple Hatching Potion to users with 55+ check-ins + * Reduce users with impossible check-in counts to a reasonable number + */ + +import monk from 'monk'; +import common from '../website/common'; + +var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE +var dbUsers = monk(connectionString).get('users', { castIds: false }); + +function processUsers(lastId) { + // specify a query to limit the affected users (empty for all users): + var query = { + 'loginIncentives': {$gt:54}, + 'migration': {$ne: migrationName}, + }; + + if (lastId) { + query._id = { + $gt: lastId + } + } + + dbUsers.find(query, { + sort: {_id: 1}, + limit: 250, + fields: [] // specify fields we are interested in to limit retrieved data (empty if we're not reading data): + }) + .then(updateUsers) + .catch(function (err) { + console.log(err); + return exiting(1, 'ERROR! ' + err); + }); +} + +var progressCount = 1000; +var count = 0; + +function updateUsers (users) { + if (!users || users.length === 0) { + console.warn('All appropriate users found and modified.'); + displayData(); + return; + } + + var userPromises = users.map(updateUser); + var lastUser = users[users.length - 1]; + + return Promise.all(userPromises) + .then(function () { + processUsers(lastUser._id); + }); +} + +function updateUser (user) { + count++; + + var language = user.preferences.language || 'en'; + var set = {'migration': migrationName}; + var inc = {'items.hatchingPotions.RoyalPurple': 1}; + if (user.loginIncentives > 58) { + set = {'migration': migrationName, 'loginIncentives': 58}; + } + var push = { + 'notifications': { + 'type': 'LOGIN_INCENTIVE', + 'data': { + 'nextRewardAt': 60, + 'rewardKey': [ + 'Pet_HatchingPotion_Purple', + ], + 'rewardText': common.i18n.t('potion', {potionType: common.i18n.t('hatchingPotionRoyalPurple', language)}, language), + 'reward': [ + { + 'premium': true, + 'key': 'RoyalPurple', + 'limited': true, + 'value': 2, + } + ], + 'message': common.i18n.t('unlockedCheckInReward', language), + }, + 'id': common.uuid(), + } + }; + + dbUsers.update({_id: user._id}, {$set:set, $push:push, $inc:inc}); + + if (count % progressCount == 0) console.warn(count + ' ' + user._id); + if (user._id == authorUuid) console.warn(authorName + ' processed'); +} + +function displayData() { + console.warn('\n' + count + ' users processed\n'); + return exiting(0); +} + +function exiting(code, msg) { + code = code || 0; // 0 = success + if (code && !msg) { msg = 'ERROR!'; } + if (msg) { + if (code) { console.error(msg); } + else { console.log( msg); } + } + process.exit(code); +} + +module.exports = processUsers; diff --git a/website/assets/sprites/spritesmith/gear/armor/broad_armor_special_pageArmor.png b/website/assets/sprites/spritesmith/gear/armor/broad_armor_special_pageArmor.png new file mode 100644 index 0000000000..ed7fac9aff Binary files /dev/null and b/website/assets/sprites/spritesmith/gear/armor/broad_armor_special_pageArmor.png differ diff --git a/website/assets/sprites/spritesmith/gear/armor/shop/shop_armor_special_pageArmor.png b/website/assets/sprites/spritesmith/gear/armor/shop/shop_armor_special_pageArmor.png new file mode 100644 index 0000000000..16bb46d8cf Binary files /dev/null and b/website/assets/sprites/spritesmith/gear/armor/shop/shop_armor_special_pageArmor.png differ diff --git a/website/assets/sprites/spritesmith/gear/armor/slim_armor_special_pageArmor.png b/website/assets/sprites/spritesmith/gear/armor/slim_armor_special_pageArmor.png new file mode 100644 index 0000000000..1246830bd3 Binary files /dev/null and b/website/assets/sprites/spritesmith/gear/armor/slim_armor_special_pageArmor.png differ diff --git a/website/assets/sprites/spritesmith/gear/head/head_special_pageHelm.png b/website/assets/sprites/spritesmith/gear/head/head_special_pageHelm.png new file mode 100644 index 0000000000..7f6cffe818 Binary files /dev/null and b/website/assets/sprites/spritesmith/gear/head/head_special_pageHelm.png differ diff --git a/website/assets/sprites/spritesmith/gear/head/shop/shop_head_special_pageHelm.png b/website/assets/sprites/spritesmith/gear/head/shop/shop_head_special_pageHelm.png new file mode 100644 index 0000000000..b01e957e12 Binary files /dev/null and b/website/assets/sprites/spritesmith/gear/head/shop/shop_head_special_pageHelm.png differ diff --git a/website/assets/sprites/spritesmith/gear/shield/shield_special_diamondStave.png b/website/assets/sprites/spritesmith/gear/shield/shield_special_diamondStave.png new file mode 100644 index 0000000000..5cde1d23a8 Binary files /dev/null and b/website/assets/sprites/spritesmith/gear/shield/shield_special_diamondStave.png differ diff --git a/website/assets/sprites/spritesmith/gear/shield/shop/shop_shield_special_diamondStave.png b/website/assets/sprites/spritesmith/gear/shield/shop/shop_shield_special_diamondStave.png new file mode 100644 index 0000000000..93d3c27d11 Binary files /dev/null and b/website/assets/sprites/spritesmith/gear/shield/shop/shop_shield_special_diamondStave.png differ diff --git a/website/assets/sprites/spritesmith/gear/weapon/shop/shop_weapon_special_pageBanner.png b/website/assets/sprites/spritesmith/gear/weapon/shop/shop_weapon_special_pageBanner.png new file mode 100644 index 0000000000..733772d887 Binary files /dev/null and b/website/assets/sprites/spritesmith/gear/weapon/shop/shop_weapon_special_pageBanner.png differ diff --git a/website/assets/sprites/spritesmith/gear/weapon/weapon_special_pageBanner.png b/website/assets/sprites/spritesmith/gear/weapon/weapon_special_pageBanner.png new file mode 100644 index 0000000000..3e28864fae Binary files /dev/null and b/website/assets/sprites/spritesmith/gear/weapon/weapon_special_pageBanner.png differ diff --git a/website/client-old/js/controllers/userCtrl.js b/website/client-old/js/controllers/userCtrl.js index f5eb24e5b2..8044f6e0a4 100644 --- a/website/client-old/js/controllers/userCtrl.js +++ b/website/client-old/js/controllers/userCtrl.js @@ -95,6 +95,7 @@ habitrpg.controller("UserCtrl", ['$rootScope', '$scope', '$location', 'User', '$ var currentLoginDay = Content.loginIncentives[$scope.profile.loginIncentives]; if (!currentLoginDay) return env.t('moreIncentivesComingSoon'); var nextRewardAt = currentLoginDay.nextRewardAt; + if (!nextRewardAt) return env.t('moreIncentivesComingSoon'); if (!currentLoginDay.prevRewardKey) currentLoginDay.prevRewardKey = 0; return env.t('checkinProgressTitle') + ' ' + ($scope.profile.loginIncentives - currentLoginDay.prevRewardKey) + '/' + (nextRewardAt - currentLoginDay.prevRewardKey); }; diff --git a/website/common/locales/en/gear.json b/website/common/locales/en/gear.json index d7fa0a8153..91e107a912 100644 --- a/website/common/locales/en/gear.json +++ b/website/common/locales/en/gear.json @@ -93,6 +93,8 @@ "weaponSpecialLunarScytheNotes": "Wax this scythe regularly, or its power will wane. Increases Strength and Perception by <%= attrs %> each.", "weaponSpecialMammothRiderSpearText": "Mammoth Rider Spear", "weaponSpecialMammothRiderSpearNotes": "This rose quartz-tipped spear will imbue you with ancient spell-casting power. Increases Intelligence by <%= int %>.", + "weaponSpecialPageBannerText": "Page Banner", + "weaponSpecialPageBannerNotes": "Wave your banner high to inspire confidence! Increases Strength by <%= str %>.", "weaponSpecialYetiText": "Yeti-Tamer Spear", "weaponSpecialYetiNotes": "This spear allows its user to command any yeti. Increases Strength by <%= str %>. Limited Edition 2013-2014 Winter Gear.", @@ -333,6 +335,8 @@ "armorSpecialLunarWarriorArmorNotes": "This armor is forged of moonstone and magical steel. Increases Strength and Constitution by <%= attrs %> each.", "armorSpecialMammothRiderArmorText": "Mammoth Rider Armor", "armorSpecialMammothRiderArmorNotes": "This suit of fur and leather includes a snazzy cape studded with rose quartz gems. It will protect you from bitter winds as you adventure in the coldest climes. Increases Constitution by <%= con %>.", + "armorSpecialPageArmorText": "Page Armor", + "armorSpecialPageArmorNotes": "Carry everything you need in your perfect pack! Increases Constitution by <%= con %>.", "armorSpecialYetiText": "Yeti-Tamer Robe", "armorSpecialYetiNotes": "Fuzzy and fierce. Increases Constitution by <%= con %>. Limited Edition 2013-2014 Winter Gear.", @@ -630,6 +634,8 @@ "headSpecialLunarWarriorHelmNotes": "The power of the moon will strengthen you in battle! Increases Strength and Intelligence by <%= attrs %> each.", "headSpecialMammothRiderHelmText": "Mammoth Rider Helm", "headSpecialMammothRiderHelmNotes": "Don't let its fluffiness fool you--this hat will grant you piercing powers of perception! Increases Perception by <%= per %>.", + "headSpecialPageHelmText": "Page Helm", + "headSpecialPageHelmNotes": "Chainmail: for the stylish AND the practical. Increases Perception by <%= per %>.", "headSpecialNyeText": "Absurd Party Hat", "headSpecialNyeNotes": "You've received an Absurd Party Hat! Wear it with pride while ringing in the New Year! Confers no benefit.", @@ -915,6 +921,8 @@ "shieldSpecialMoonpearlShieldNotes": "Designed for fast swimming, and also some defense. Increases Constitution by <%= con %>.", "shieldSpecialMammothRiderHornText": "Mammoth Rider's Horn", "shieldSpecialMammothRiderHornNotes": "One blow on this mighty rose quartz horn and you'll summon powerful magical forces. Increases Strength by <%= str %>.", + "shieldSpecialDiamondStaveText": "Diamond Stave", + "shieldSpecialDiamondStaveNotes": "This valuable stave has mystical powers. Increases Intelligence by <%= int %>.", "shieldSpecialGoldenknightText": "Mustaine's Milestone Mashing Morning Star", "shieldSpecialGoldenknightNotes": "Meetings, monsters, malaise: managed! Mash! Increases Constitution and Perception by <%= attrs %> each.", diff --git a/website/common/script/constants.js b/website/common/script/constants.js index f58cc7f8fb..865614295e 100644 --- a/website/common/script/constants.js +++ b/website/common/script/constants.js @@ -2,6 +2,7 @@ export const MAX_HEALTH = 50; export const MAX_LEVEL = 100; export const MAX_STAT_POINTS = MAX_LEVEL; export const ATTRIBUTES = ['str', 'int', 'per', 'con']; +export const MAX_INCENTIVES = 100; export const TAVERN_ID = '00000000-0000-4000-A000-000000000000'; export const LARGE_GROUP_COUNT_MESSAGE_CUTOFF = 5000; diff --git a/website/common/script/content/gear/sets/special/index.js b/website/common/script/content/gear/sets/special/index.js index 18b5a07c9d..3b5e55eafc 100644 --- a/website/common/script/content/gear/sets/special/index.js +++ b/website/common/script/content/gear/sets/special/index.js @@ -49,6 +49,13 @@ let armor = { value: 130, canOwn: ownsItem('armor_special_mammothRiderArmor'), }, + pageArmor: { + text: t('armorSpecialPageArmorText'), + notes: t('armorSpecialPageArmorNotes', { con: 16 }), + con: 16, + value: 0, + canOwn: ownsItem('armor_special_pageArmor'), + }, yeti: { event: EVENTS.winter, specialClass: 'warrior', @@ -690,6 +697,13 @@ let head = { value: 130, canOwn: ownsItem('head_special_mammothRiderHelm'), }, + pageHelm: { + text: t('headSpecialPageHelmText'), + notes: t('headSpecialPageHelmNotes', { per: 16 }), + per: 16, + value: 0, + canOwn: ownsItem('head_special_pageHelm'), + }, nye: { event: EVENTS.nye, text: t('headSpecialNyeText'), @@ -1368,6 +1382,13 @@ let shield = { value: 130, canOwn: ownsItem('shield_special_mammothRiderHorn'), }, + diamondStave: { + text: t('shieldSpecialDiamondStaveText'), + notes: t('shieldSpecialDiamondStaveNotes', { int: 16 }), + int: 16, + value: 0, + canOwn: ownsItem('shield_special_diamondStave'), + }, yeti: { event: EVENTS.winter, specialClass: 'warrior', @@ -1757,6 +1778,13 @@ let weapon = { value: 130, canOwn: ownsItem('weapon_special_mammothRiderSpear'), }, + pageBanner: { + text: t('weaponSpecialPageBannerText'), + notes: t('weaponSpecialPageBannerNotes', { str: 16 }), + str: 16, + value: 0, + canOwn: ownsItem('weapon_special_pageBanner'), + }, yeti: { event: EVENTS.winter, specialClass: 'warrior', diff --git a/website/common/script/content/loginIncentives.js b/website/common/script/content/loginIncentives.js index 6b205aad7c..ec93fdc09f 100644 --- a/website/common/script/content/loginIncentives.js +++ b/website/common/script/content/loginIncentives.js @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { MAX_INCENTIVES } from '../constants'; module.exports = function getLoginIncentives (api) { let loginIncentives = { @@ -145,13 +146,89 @@ module.exports = function getLoginIncentives (api) { user.items.food.Saddle += 1; }, }, + 55: { + rewardKey: ['Pet_HatchingPotion_Purple'], + reward: [api.hatchingPotions.RoyalPurple], + assignReward: function assignReward (user) { + if (!user.items.hatchingPotions.RoyalPurple) user.items.hatchingPotions.RoyalPurple = 0; + user.items.hatchingPotions.RoyalPurple += 1; + }, + }, + 60: { + rewardKey: ['slim_armor_special_pageArmor'], + reward: [api.gear.flat.armor_special_pageArmor], + assignReward: function assignReward (user) { + user.items.gear.owned.armor_special_pageArmor = true; // eslint-disable-line camelcase + }, + }, + 65: { + rewardKey: ['Pet_HatchingPotion_Purple'], + reward: [api.hatchingPotions.RoyalPurple], + assignReward: function assignReward (user) { + if (!user.items.hatchingPotions.RoyalPurple) user.items.hatchingPotions.RoyalPurple = 0; + user.items.hatchingPotions.RoyalPurple += 1; + }, + }, + 70: { + rewardKey: ['head_special_pageHelm'], + reward: [api.gear.flat.head_special_pageHelm], + assignReward: function assignReward (user) { + user.items.gear.owned.head_special_pageHelm = true; // eslint-disable-line camelcase + }, + }, + 75: { + rewardKey: ['Pet_HatchingPotion_Purple'], + reward: [api.hatchingPotions.RoyalPurple], + assignReward: function assignReward (user) { + if (!user.items.hatchingPotions.RoyalPurple) user.items.hatchingPotions.RoyalPurple = 0; + user.items.hatchingPotions.RoyalPurple += 1; + }, + }, + 80: { + rewardKey: ['weapon_special_pageBanner'], + reward: [api.gear.flat.weapon_special_pageBanner], + assignReward: function assignReward (user) { + user.items.gear.owned.weapon_special_pageBanner = true; // eslint-disable-line camelcase + }, + }, + 85: { + rewardKey: ['Pet_HatchingPotion_Purple'], + reward: [api.hatchingPotions.RoyalPurple], + assignReward: function assignReward (user) { + if (!user.items.hatchingPotions.RoyalPurple) user.items.hatchingPotions.RoyalPurple = 0; + user.items.hatchingPotions.RoyalPurple += 1; + }, + }, + 90: { + rewardKey: ['shield_special_diamondStave'], + reward: [api.gear.flat.shield_special_diamondStave], + assignReward: function assignReward (user) { + user.items.gear.owned.shield_special_diamondStave = true; // eslint-disable-line camelcase + }, + }, + 95: { + rewardKey: ['Pet_HatchingPotion_Purple'], + reward: [api.hatchingPotions.RoyalPurple], + assignReward: function assignReward (user) { + if (!user.items.hatchingPotions.RoyalPurple) user.items.hatchingPotions.RoyalPurple = 0; + user.items.hatchingPotions.RoyalPurple += 1; + }, + }, + 100: { + rewardKey: ['Pet_Food_Saddle'], + reward: [api.food.Saddle], + assignReward: function assignReward (user) { + if (!user.items.food.Saddle) user.items.food.Saddle = 0; + user.items.food.Saddle += 1; + }, + }, }; // Add refence link to next reward and add filler days so we have a map to refernce the next reward from any day // We could also, use a list, but then we would be cloning each of the rewards. // Create a new array if we want the loginIncentives to be immutable in the future let nextRewardKey; - _.range(51).reverse().forEach(function addNextRewardLink (index) { + _.range(MAX_INCENTIVES + 1).reverse().forEach(function addNextRewardLink (index) { if (loginIncentives[index] && loginIncentives[index].rewardKey) { loginIncentives[index].nextRewardAt = nextRewardKey; nextRewardKey = index; @@ -165,7 +242,7 @@ module.exports = function getLoginIncentives (api) { }); let prevRewardKey; - _.range(51).forEach(function addPrevRewardLink (index) { + _.range(MAX_INCENTIVES + 1).forEach(function addPrevRewardLink (index) { loginIncentives[index].prevRewardKey = prevRewardKey; if (loginIncentives[index].rewardKey) prevRewardKey = index; }); diff --git a/website/common/script/index.js b/website/common/script/index.js index 39cc348cd0..232f4ecc3a 100644 --- a/website/common/script/index.js +++ b/website/common/script/index.js @@ -21,12 +21,14 @@ import { MAX_HEALTH, MAX_LEVEL, MAX_STAT_POINTS, + MAX_INCENTIVES, TAVERN_ID, LARGE_GROUP_COUNT_MESSAGE_CUTOFF, SUPPORTED_SOCIAL_NETWORKS, } from './constants'; api.constants = { + MAX_INCENTIVES, LARGE_GROUP_COUNT_MESSAGE_CUTOFF, SUPPORTED_SOCIAL_NETWORKS, }; diff --git a/website/server/libs/cron.js b/website/server/libs/cron.js index 7364158258..842303d51b 100644 --- a/website/server/libs/cron.js +++ b/website/server/libs/cron.js @@ -8,6 +8,7 @@ import nconf from 'nconf'; const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true'; const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true'; +const MAX_INCENTIVES = common.constants.MAX_INCENTIVES; const shouldDo = common.shouldDo; const scoreTask = common.ops.scoreTask; const i18n = common.i18n; @@ -129,7 +130,7 @@ function trackCronAnalytics (analytics, user, _progress, options) { } function awardLoginIncentives (user) { - if (user.loginIncentives > 50) return; + if (user.loginIncentives > MAX_INCENTIVES) return; // A/B test 2016-12-21: Should we deliver notifications for upcoming incentives on days when users don't receive rewards? if (!loginIncentives[user.loginIncentives].rewardKey && user._ABtests && user._ABtests.checkInModals === '20161221_noCheckInPreviews') return;