diff --git a/migrations/archive/2019/20190917_pet_color_achievements.js b/migrations/archive/2019/20190917_pet_color_achievements.js new file mode 100644 index 0000000000..ea96b07afb --- /dev/null +++ b/migrations/archive/2019/20190917_pet_color_achievements.js @@ -0,0 +1,104 @@ +/* eslint-disable no-console */ +const MIGRATION_NAME = '20190917_pet_color_achievements'; +import { model as User } from '../../../website/server/models/user'; + +const progressCount = 1000; +let count = 0; + +async function updateUser (user) { + count++; + + let set = { + migration: MIGRATION_NAME, + }; + + if (user && user.items && user.items.pets) { + const pets = user.items.pets; + if (pets['Wolf-Base'] > 0 + && pets['TigerCub-Base'] > 0 + && pets['PandaCub-Base'] > 0 + && pets['LionCub-Base'] > 0 + && pets['Fox-Base'] > 0 + && pets['FlyingPig-Base'] > 0 + && pets['Dragon-Base'] > 0 + && pets['Cactus-Base'] > 0 + && pets['BearCub-Base'] > 0) { + set['achievements.backToBasics'] = true; + } + if (pets['Wolf-Desert'] > 0 + && pets['TigerCub-Desert'] > 0 + && pets['PandaCub-Desert'] > 0 + && pets['LionCub-Desert'] > 0 + && pets['Fox-Desert'] > 0 + && pets['FlyingPig-Desert'] > 0 + && pets['Dragon-Desert'] > 0 + && pets['Cactus-Desert'] > 0 + && pets['BearCub-Desert'] > 0) { + set['achievements.dustDevil'] = true; + } + } + + if (user && user.items && user.items.mounts) { + const mounts = user.items.mounts; + if (mounts['Wolf-Base'] + && mounts['TigerCub-Base'] + && mounts['PandaCub-Base'] + && mounts['LionCub-Base'] + && mounts['Fox-Base'] + && mounts['FlyingPig-Base'] + && mounts['Dragon-Base'] + && mounts['Cactus-Base'] + && mounts['BearCub-Base'] ) { + set['achievements.allYourBase'] = true; + } + if (mounts['Wolf-Desert'] + && mounts['TigerCub-Desert'] + && mounts['PandaCub-Desert'] + && mounts['LionCub-Desert'] + && mounts['Fox-Desert'] + && mounts['FlyingPig-Desert'] + && mounts['Dragon-Desert'] + && mounts['Cactus-Desert'] + && mounts['BearCub-Desert'] ) { + set['achievements.aridAuthority'] = true; + } + } + + if (count % progressCount === 0) console.warn(`${count} ${user._id}`); + + return await User.update({ _id: user._id }, { $set: set }).exec(); +} + +module.exports = async function processUsers () { + let query = { + migration: { $ne: MIGRATION_NAME }, + 'auth.timestamps.loggedin': { $gt: new Date('2019-09-01') }, + }; + + const fields = { + _id: 1, + items: 1, + }; + + while (true) { // eslint-disable-line no-constant-condition + const users = await User // eslint-disable-line no-await-in-loop + .find(query) + .limit(250) + .sort({_id: 1}) + .select(fields) + .lean() + .exec(); + + if (users.length === 0) { + console.warn('All appropriate users found and modified.'); + console.warn(`\n${count} users processed\n`); + break; + } else { + query._id = { + $gt: users[users.length - 1]._id, + }; + } + + await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop + } +}; diff --git a/test/common/ops/feed.js b/test/common/ops/feed.js index 84c3713686..241fe0fbbc 100644 --- a/test/common/ops/feed.js +++ b/test/common/ops/feed.js @@ -187,6 +187,24 @@ describe('shared.ops.feed', () => { expect(user.achievements.allYourBase).to.eql(true); }); + it('awards Arid Authority achievement', () => { + user.items.pets['Wolf-Spooky'] = 5; + user.items.food.Milk = 2; + user.items.mounts = { + 'Wolf-Desert': true, + 'TigerCub-Desert': true, + 'PandaCub-Desert': true, + 'LionCub-Desert': true, + 'Fox-Desert': true, + 'FlyingPig-Desert': true, + 'Dragon-Desert': true, + 'Cactus-Desert': true, + 'BearCub-Desert': true, + }; + feed(user, {params: {pet: 'Wolf-Spooky', food: 'Milk'}}); + expect(user.achievements.aridAuthority).to.eql(true); + }); + it('evolves the pet into a mount when feeding user.items.pets[pet] >= 50', () => { user.items.pets['Wolf-Base'] = 49; user.items.food.Milk = 2; diff --git a/test/common/ops/hatch.js b/test/common/ops/hatch.js index 0339a8fa8d..b1459ba9d9 100644 --- a/test/common/ops/hatch.js +++ b/test/common/ops/hatch.js @@ -177,6 +177,24 @@ describe('shared.ops.hatch', () => { hatch(user, {params: {egg: 'Wolf', hatchingPotion: 'Spooky'}}); expect(user.achievements.backToBasics).to.eql(true); }); + + it('awards Dust Devil achievement', () => { + user.items.pets = { + 'Wolf-Desert': 5, + 'TigerCub-Desert': 5, + 'PandaCub-Desert': 10, + 'LionCub-Desert': 5, + 'Fox-Desert': 5, + 'FlyingPig-Desert': 5, + 'Dragon-Desert': 5, + 'Cactus-Desert': 15, + 'BearCub-Desert': 5, + }; + user.items.eggs = {Wolf: 1}; + user.items.hatchingPotions = {Spooky: 1}; + hatch(user, {params: {egg: 'Wolf', hatchingPotion: 'Spooky'}}); + expect(user.achievements.dustDevil).to.eql(true); + }); }); }); }); diff --git a/website/client/components/notifications.vue b/website/client/components/notifications.vue index 00a8a6baa4..b5e688c1ad 100644 --- a/website/client/components/notifications.vue +++ b/website/client/components/notifications.vue @@ -165,6 +165,16 @@ const NOTIFICATIONS = { label: ($t) => `${$t('achievement')}: ${$t('achievementBackToBasics')}`, modalId: 'generic-achievement', }, + ACHIEVEMENT_DUST_DEVIL: { + achievement: true, + label: ($t) => `${$t('achievement')}: ${$t('achievementDustDevil')}`, + modalId: 'generic-achievement', + }, + ACHIEVEMENT_ARID_AUTHORITY: { + achievement: true, + label: ($t) => `${$t('achievement')}: ${$t('achievementAridAuthority')}`, + modalId: 'generic-achievement', + }, }; export default { @@ -220,7 +230,7 @@ export default { 'ULTIMATE_GEAR_ACHIEVEMENT', 'REBIRTH_ACHIEVEMENT', 'GUILD_JOINED_ACHIEVEMENT', 'CHALLENGE_JOINED_ACHIEVEMENT', 'INVITED_FRIEND_ACHIEVEMENT', 'NEW_CONTRIBUTOR_LEVEL', 'CRON', 'SCORED_TASK', 'LOGIN_INCENTIVE', 'ACHIEVEMENT_ALL_YOUR_BASE', 'ACHIEVEMENT_BACK_TO_BASICS', - 'GENERIC_ACHIEVEMENT', + 'ACHIEVEMENT_DUST_DEVIL', 'ACHIEVEMENT_ARID_AUTHORITY', 'GENERIC_ACHIEVEMENT', ].forEach(type => { handledNotifications[type] = true; }); @@ -595,6 +605,8 @@ export default { case 'NEW_CONTRIBUTOR_LEVEL': case 'ACHIEVEMENT_ALL_YOUR_BASE': case 'ACHIEVEMENT_BACK_TO_BASICS': + case 'ACHIEVEMENT_DUST_DEVIL': + case 'ACHIEVEMENT_ARID_AUTHORITY': case 'GENERIC_ACHIEVEMENT': this.showNotificationWithModal(notification); break; diff --git a/website/common/locales/en/achievements.json b/website/common/locales/en/achievements.json index a1595a3347..db8b78ea2b 100644 --- a/website/common/locales/en/achievements.json +++ b/website/common/locales/en/achievements.json @@ -18,5 +18,11 @@ "achievementBackToBasicsModalText": "You collected all the Base Pets!", "achievementAllYourBase": "All Your Base", "achievementAllYourBaseText": "Has tamed all Base Mounts.", - "achievementAllYourBaseModalText": "You tamed all the Base Mounts!" + "achievementAllYourBaseModalText": "You tamed all the Base Mounts!", + "achievementDustDevil": "Dust Devil", + "achievementDustDevilText": "Has collected all Desert Pets.", + "achievementDustDevilModalText": "You collected all the Desert Pets!", + "achievementAridAuthority": "Arid Authority", + "achievementAridAuthorityText": "Has tamed all Desert Mounts.", + "achievementAridAuthorityModalText": "You tamed all the Desert Mounts!" } diff --git a/website/common/script/content/achievements.js b/website/common/script/content/achievements.js index 28e9fc88be..154bf78011 100644 --- a/website/common/script/content/achievements.js +++ b/website/common/script/content/achievements.js @@ -147,6 +147,16 @@ let basicAchievs = { titleKey: 'achievementAllYourBase', textKey: 'achievementAllYourBaseText', }, + dustDevil: { + icon: 'achievement-dustDevil', + titleKey: 'achievementDustDevil', + textKey: 'achievementDustDevilText', + }, + aridAuthority: { + icon: 'achievement-aridAuthority', + titleKey: 'achievementAridAuthority', + textKey: 'achievementAridAuthorityText', + }, }; Object.assign(achievementsData, basicAchievs); diff --git a/website/common/script/content/constants.js b/website/common/script/content/constants.js index 66d55142d8..09d8f8a148 100644 --- a/website/common/script/content/constants.js +++ b/website/common/script/content/constants.js @@ -256,14 +256,7 @@ export const QUEST_SERIES_ACHIEVEMENTS = { ], }; -export const BASE_PETS_MOUNTS = [ - 'Wolf-Base', - 'TigerCub-Base', - 'PandaCub-Base', - 'LionCub-Base', - 'Fox-Base', - 'FlyingPig-Base', - 'Dragon-Base', - 'Cactus-Base', - 'BearCub-Base', +export const ANIMAL_COLOR_ACHIEVEMENTS = [ + {color: 'Base', petAchievement: 'backToBasics', petNotificationType: 'ACHIEVEMENT_BACK_TO_BASICS', mountAchievement: 'allYourBase', mountNotificationType: 'ACHIEVEMENT_ALL_YOUR_BASE'}, + {color: 'Desert', petAchievement: 'dustDevil', petNotificationType: 'ACHIEVEMENT_DUST_DEVIL', mountAchievement: 'aridAuthority', mountNotificationType: 'ACHIEVEMENT_ARID_AUTHORITY'}, ]; diff --git a/website/common/script/content/index.js b/website/common/script/content/index.js index f85bf3d546..eedb5c8cd9 100644 --- a/website/common/script/content/index.js +++ b/website/common/script/content/index.js @@ -8,7 +8,7 @@ import { GEAR_TYPES, ITEM_LIST, QUEST_SERIES_ACHIEVEMENTS, - BASE_PETS_MOUNTS, + ANIMAL_COLOR_ACHIEVEMENTS, } from './constants'; let api = module.exports; @@ -38,7 +38,7 @@ import officialPinnedItems from './officialPinnedItems'; api.achievements = achievements; api.questSeriesAchievements = QUEST_SERIES_ACHIEVEMENTS; -api.basePetsMounts = BASE_PETS_MOUNTS; +api.animalColorAchievements = ANIMAL_COLOR_ACHIEVEMENTS; api.quests = quests; api.questsByLevel = questsByLevel; diff --git a/website/common/script/libs/achievements.js b/website/common/script/libs/achievements.js index 770fda23ef..fe386361e6 100644 --- a/website/common/script/libs/achievements.js +++ b/website/common/script/libs/achievements.js @@ -188,6 +188,8 @@ function _getBasicAchievements (user, language) { _addSimple(result, user, {path: 'justAddWater', language}); _addSimple(result, user, {path: 'backToBasics', language}); _addSimple(result, user, {path: 'allYourBase', language}); + _addSimple(result, user, {path: 'dustDevil', language}); + _addSimple(result, user, {path: 'aridAuthority', language}); _addSimpleWithMasterCount(result, user, {path: 'beastMaster', language}); _addSimpleWithMasterCount(result, user, {path: 'mountMaster', language}); diff --git a/website/common/script/ops/feed.js b/website/common/script/ops/feed.js index 4a45c737eb..c91eb42c94 100644 --- a/website/common/script/ops/feed.js +++ b/website/common/script/ops/feed.js @@ -1,7 +1,10 @@ import content from '../content/index'; import i18n from '../i18n'; +import forEach from 'lodash/forEach'; import findIndex from 'lodash/findIndex'; import get from 'lodash/get'; +import keys from 'lodash/keys'; +import upperFirst from 'lodash/upperFirst'; import { BadRequest, NotAuthorized, @@ -90,21 +93,24 @@ module.exports = function feed (user, req = {}) { user.items.food[food.key]--; if (user.markModified) user.markModified('items.food'); - if (!user.achievements.allYourBase) { - const mountIndex = findIndex(content.basePetsMounts, (animal) => { - return !user.items.mounts[animal]; - }); - if (mountIndex === -1) { - user.achievements.allYourBase = true; - if (user.addNotification) { - user.addNotification('ACHIEVEMENT_ALL_YOUR_BASE', { - achievement: 'allYourBase', - message: `${i18n.t('modalAchievement')} ${i18n.t('achievementAllYourBase')}`, - modalText: i18n.t('achievementAllYourBaseModalText'), - }); + forEach(content.animalColorAchievements, (achievement) => { + if (!user.achievements[achievement.mountAchievement]) { + const mountIndex = findIndex(keys(content.dropEggs), (animal) => { + return !user.items.mounts[`${animal}-${achievement.color}`]; + }); + if (mountIndex === -1) { + user.achievements[achievement.mountAchievement] = true; + if (user.addNotification) { + const achievementString = `achievement${upperFirst(achievement.mountAchievement)}`; + user.addNotification(achievement.mountNotificationType, { + achievement: achievement.mountAchievement, + message: `${i18n.t('modalAchievement')} ${i18n.t(achievementString)}`, + modalText: i18n.t(`${achievementString}ModalText`), + }); + } } } - } + }); return [ userPets[pet.key], diff --git a/website/common/script/ops/hatch.js b/website/common/script/ops/hatch.js index 6a8036d9e1..88ebb96f36 100644 --- a/website/common/script/ops/hatch.js +++ b/website/common/script/ops/hatch.js @@ -1,7 +1,10 @@ import content from '../content/index'; import i18n from '../i18n'; import findIndex from 'lodash/findIndex'; +import forEach from 'lodash/forEach'; import get from 'lodash/get'; +import keys from 'lodash/keys'; +import upperFirst from 'lodash/upperFirst'; import { BadRequest, NotAuthorized, @@ -40,21 +43,24 @@ module.exports = function hatch (user, req = {}) { user.markModified('items.hatchingPotions'); } - if (!user.achievements.backToBasics) { - const petIndex = findIndex(content.basePetsMounts, (animal) => { - return isNaN(user.items.pets[animal]) || user.items.pets[animal] <= 0; - }); - if (petIndex === -1) { - user.achievements.backToBasics = true; - if (user.addNotification) { - user.addNotification('ACHIEVEMENT_BACK_TO_BASICS', { - achievement: 'backToBasics', - message: `${i18n.t('modalAchievement')} ${i18n.t('achievementBackToBasics')}`, - modalText: i18n.t('achievementBackToBasicsModalText'), - }); + forEach(content.animalColorAchievements, (achievement) => { + if (!user.achievements[achievement.petAchievement]) { + const petIndex = findIndex(keys(content.dropEggs), (animal) => { + return isNaN(user.items.pets[`${animal}-${achievement.color}`]) || user.items.pets[`${animal}-${achievement.color}`] <= 0; + }); + if (petIndex === -1) { + user.achievements[achievement.petAchievement] = true; + if (user.addNotification) { + const achievementString = `achievement${upperFirst(achievement.petAchievement)}`; + user.addNotification(achievement.petNotificationType, { + achievement: achievement.petAchievement, + message: `${i18n.t('modalAchievement')} ${i18n.t(achievementString)}`, + modalText: i18n.t(`${achievementString}ModalText`), + }); + } } } - } + }); return [ user.items, diff --git a/website/raw_sprites/spritesmith/achievements/achievement-aridAuthority2x.png b/website/raw_sprites/spritesmith/achievements/achievement-aridAuthority2x.png new file mode 100644 index 0000000000..3e721fff8c Binary files /dev/null and b/website/raw_sprites/spritesmith/achievements/achievement-aridAuthority2x.png differ diff --git a/website/raw_sprites/spritesmith/achievements/achievement-dustDevil2x.png b/website/raw_sprites/spritesmith/achievements/achievement-dustDevil2x.png new file mode 100644 index 0000000000..4917092baf Binary files /dev/null and b/website/raw_sprites/spritesmith/achievements/achievement-dustDevil2x.png differ diff --git a/website/raw_sprites/spritesmith_large/promo_desert_pet_achievements.png b/website/raw_sprites/spritesmith_large/promo_desert_pet_achievements.png new file mode 100644 index 0000000000..a836c92263 Binary files /dev/null and b/website/raw_sprites/spritesmith_large/promo_desert_pet_achievements.png differ diff --git a/website/server/controllers/api-v3/news.js b/website/server/controllers/api-v3/news.js index 30ad7fe9b0..a56892e945 100644 --- a/website/server/controllers/api-v3/news.js +++ b/website/server/controllers/api-v3/news.js @@ -3,7 +3,7 @@ import { authWithHeaders } from '../../middlewares/auth'; let api = {}; // @TODO export this const, cannot export it from here because only routes are exported from controllers -const LAST_ANNOUNCEMENT_TITLE = 'NEW DISCOUNTED PET QUEST BUNDLE: ROCKING REPTILES'; +const LAST_ANNOUNCEMENT_TITLE = 'NEW PET COLLECTION BADGES!'; const worldDmg = { // @TODO bailey: false, }; @@ -30,15 +30,14 @@ api.getNews = {
If you're looking to add some scaly friends to your Habitica stable, you're in luck! From now until Sept 30, you can purchase the Rocking Reptiles Pet Quest Bundle and receive the Alligator, Snake, and Velociraptor quests, all for only 7 Gems! That's a discount of 5 Gems from the price of purchasing them separately. Check it out in the Quest Shop today!
-If snakes are something you'd prefer not to see in Habitica due to a phobia, check out the Phobia Protection Extension which will hide any pets, mounts, backgrounds, quest bosses, or equipment featuring snakes (as well as spiders, rats, bees, zombies, skeletons, or any combination thereof). We hope that it helps make everyone's Habitica experience fun!
+ +We're releasing a new achievement so you can celebrate your successes in the world of Habitican pet collecting! Earn the Dust Devil and Arid Authority achievements by collecting Desert pets and mounts and you'll earn a nifty badge for your profile.
+If you already have all the Desert pets and/or mounts in your stable, you'll receive the badge automatically! Check your profile and celebrate your new achievement with pride.
+