diff --git a/test/api/unit/models/group.test.js b/test/api/unit/models/group.test.js index 7d120c055a..377c6c6676 100644 --- a/test/api/unit/models/group.test.js +++ b/test/api/unit/models/group.test.js @@ -1882,6 +1882,36 @@ describe('Group Model', () => { expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true); }); + it('gives out other pet-related quest achievements', async () => { + quest = questScrolls.rock; + party.quest.key = quest.key; + + questLeader.achievements.quests = { + mayhemMistiflying1: 1, + yarn: 1, + mayhemMistiflying2: 1, + egg: 1, + mayhemMistiflying3: 1, + slime: 2, + }; + await questLeader.save(); + await party.finishQuest(quest); + + let [ + updatedLeader, + updatedParticipatingMember, + updatedSleepingParticipatingMember, + ] = await Promise.all([ + User.findById(questLeader._id).exec(), + User.findById(participatingMember._id).exec(), + User.findById(sleepingParticipatingMember._id).exec(), + ]); + + expect(updatedLeader.achievements.mindOverMatter).to.eql(true); + expect(updatedParticipatingMember.achievements.mindOverMatter).to.not.eql(true); + expect(updatedSleepingParticipatingMember.achievements.mindOverMatter).to.not.eql(true); + }); + it('gives xp and gold', async () => { await party.finishQuest(quest); diff --git a/test/common/ops/feed.js b/test/common/ops/feed.js index 4aabd119df..84c3713686 100644 --- a/test/common/ops/feed.js +++ b/test/common/ops/feed.js @@ -169,6 +169,24 @@ describe('shared.ops.feed', () => { expect(user.items.pets['Wolf-Base']).to.equal(7); }); + it('awards All Your Base achievement', () => { + user.items.pets['Wolf-Spooky'] = 5; + user.items.food.Milk = 2; + user.items.mounts = { + 'Wolf-Base': true, + 'TigerCub-Base': true, + 'PandaCub-Base': true, + 'LionCub-Base': true, + 'Fox-Base': true, + 'FlyingPig-Base': true, + 'Dragon-Base': true, + 'Cactus-Base': true, + 'BearCub-Base': true, + }; + feed(user, {params: {pet: 'Wolf-Spooky', food: 'Milk'}}); + expect(user.achievements.allYourBase).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 919829d6b8..0339a8fa8d 100644 --- a/test/common/ops/hatch.js +++ b/test/common/ops/hatch.js @@ -159,6 +159,24 @@ describe('shared.ops.hatch', () => { expect(user.items.eggs).to.eql({Wolf: 0}); expect(user.items.hatchingPotions).to.eql({Base: 0}); }); + + it('awards Back to Basics achievement', () => { + user.items.pets = { + 'Wolf-Base': 5, + 'TigerCub-Base': 5, + 'PandaCub-Base': 10, + 'LionCub-Base': 5, + 'Fox-Base': 5, + 'FlyingPig-Base': 5, + 'Dragon-Base': 5, + 'Cactus-Base': 15, + 'BearCub-Base': 5, + }; + user.items.eggs = {Wolf: 1}; + user.items.hatchingPotions = {Spooky: 1}; + hatch(user, {params: {egg: 'Wolf', hatchingPotion: 'Spooky'}}); + expect(user.achievements.backToBasics).to.eql(true); + }); }); }); }); diff --git a/website/common/locales/en/achievements.json b/website/common/locales/en/achievements.json index 4f5dcd77c1..f84b48d46b 100644 --- a/website/common/locales/en/achievements.json +++ b/website/common/locales/en/achievements.json @@ -5,5 +5,13 @@ "levelup": "By accomplishing your real life goals, you leveled up and are now fully healed!", "reachedLevel": "You Reached Level <%= level %>", "achievementLostMasterclasser": "Quest Completionist: Masterclasser Series", - "achievementLostMasterclasserText": "Completed all sixteen quests in the Masterclasser Quest Series and solved the mystery of the Lost Masterclasser!" + "achievementLostMasterclasserText": "Completed all sixteen quests in the Masterclasser Quest Series and solved the mystery of the Lost Masterclasser!", + "achievementMindOverMatter": "Mind Over Matter", + "achievementMindOverMatterText": "Has completed Rock, Egg, Slime, and Yarn pet quests.", + "achievementJustAddWater": "Just Add Water", + "achievementJustAddWaterText": "Has completed Octopus, Seahorse, Cuttlefish, Whale, Turtle, Nudibranch, Sea Serpent, and Dolphin pet quests.", + "achievementBackToBasics": "Back to Basics", + "achievementBackToBasicsText": "Has collected all Base Pets.", + "achievementAllYourBase": "All Your Base", + "achievementAllYourBaseText": "Has tamed all Base Mounts." } diff --git a/website/common/script/content/achievements.js b/website/common/script/content/achievements.js index 6250bde61b..28e9fc88be 100644 --- a/website/common/script/content/achievements.js +++ b/website/common/script/content/achievements.js @@ -127,6 +127,26 @@ let basicAchievs = { titleKey: 'achievementLostMasterclasser', textKey: 'achievementLostMasterclasserText', }, + mindOverMatter: { + icon: 'achievement-mindOverMatter', + titleKey: 'achievementMindOverMatter', + textKey: 'achievementMindOverMatterText', + }, + justAddWater: { + icon: 'achievement-justAddWater', + titleKey: 'achievementJustAddWater', + textKey: 'achievementJustAddWaterText', + }, + backToBasics: { + icon: 'achievement-backToBasics', + titleKey: 'achievementBackToBasics', + textKey: 'achievementBackToBasicsText', + }, + allYourBase: { + icon: 'achievement-allYourBase', + titleKey: 'achievementAllYourBase', + textKey: 'achievementAllYourBaseText', + }, }; Object.assign(achievementsData, basicAchievs); diff --git a/website/common/script/content/constants.js b/website/common/script/content/constants.js index a345985367..f9fa398b81 100644 --- a/website/common/script/content/constants.js +++ b/website/common/script/content/constants.js @@ -212,3 +212,52 @@ export const USER_CAN_OWN_QUEST_CATEGORIES = [ 'hatchingPotion', 'pet', ]; + +export const QUEST_SERIES_ACHIEVEMENTS = { + lostMasterclasser: [ + 'dilatoryDistress1', + 'dilatoryDistress2', + 'dilatoryDistress3', + 'mayhemMistiflying1', + 'mayhemMistiflying2', + 'mayhemMistiflying3', + 'stoikalmCalamity1', + 'stoikalmCalamity2', + 'stoikalmCalamity3', + 'taskwoodsTerror1', + 'taskwoodsTerror2', + 'taskwoodsTerror3', + 'lostMasterclasser1', + 'lostMasterclasser2', + 'lostMasterclasser3', + 'lostMasterclasser4', + ], + mindOverMatter: [ + 'egg', + 'rock', + 'slime', + 'yarn', + ], + justAddWater: [ + 'octopus', + 'dilatory_derby', + 'kraken', + 'whale', + 'turtle', + 'nudibranch', + 'seaserpent', + 'dolphin', + ], +}; + +export const BASE_PETS_MOUNTS = [ + 'Wolf-Base', + 'TigerCub-Base', + 'PandaCub-Base', + 'LionCub-Base', + 'Fox-Base', + 'FlyingPig-Base', + 'Dragon-Base', + 'Cactus-Base', + 'BearCub-Base', +]; diff --git a/website/common/script/content/index.js b/website/common/script/content/index.js index d1b1e2326a..87981b5abd 100644 --- a/website/common/script/content/index.js +++ b/website/common/script/content/index.js @@ -7,6 +7,8 @@ import { CLASSES, GEAR_TYPES, ITEM_LIST, + QUEST_SERIES_ACHIEVEMENTS, + BASE_PETS_MOUNTS, } from './constants'; let api = module.exports; @@ -35,6 +37,8 @@ import loginIncentives from './loginIncentives'; import officialPinnedItems from './officialPinnedItems'; api.achievements = achievements; +api.questSeriesAchievements = QUEST_SERIES_ACHIEVEMENTS; +api.basePetsMounts = BASE_PETS_MOUNTS; api.quests = quests; api.questsByLevel = questsByLevel; diff --git a/website/common/script/libs/achievements.js b/website/common/script/libs/achievements.js index 0793c45ee6..770fda23ef 100644 --- a/website/common/script/libs/achievements.js +++ b/website/common/script/libs/achievements.js @@ -184,6 +184,10 @@ function _getBasicAchievements (user, language) { _addSimple(result, user, {path: 'joinedChallenge', language}); _addSimple(result, user, {path: 'invitedFriend', language}); _addSimple(result, user, {path: 'lostMasterclasser', language}); + _addSimple(result, user, {path: 'mindOverMatter', language}); + _addSimple(result, user, {path: 'justAddWater', language}); + _addSimple(result, user, {path: 'backToBasics', language}); + _addSimple(result, user, {path: 'allYourBase', 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 1a66770d28..e313a0418d 100644 --- a/website/common/script/ops/feed.js +++ b/website/common/script/ops/feed.js @@ -1,5 +1,6 @@ import content from '../content/index'; import i18n from '../i18n'; +import findIndex from 'lodash/findIndex'; import get from 'lodash/get'; import { BadRequest, @@ -89,6 +90,15 @@ 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; + } + } + return [ userPets[pet.key], message, diff --git a/website/common/script/ops/hatch.js b/website/common/script/ops/hatch.js index 672372e125..84c6140fe1 100644 --- a/website/common/script/ops/hatch.js +++ b/website/common/script/ops/hatch.js @@ -1,5 +1,6 @@ import content from '../content/index'; import i18n from '../i18n'; +import findIndex from 'lodash/findIndex'; import get from 'lodash/get'; import { BadRequest, @@ -39,6 +40,15 @@ 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; + } + } + return [ user.items, i18n.t('messageHatched', req.language), diff --git a/website/raw_sprites/spritesmith/achievements/achievement-allYourBase2x.png b/website/raw_sprites/spritesmith/achievements/achievement-allYourBase2x.png new file mode 100644 index 0000000000..dfec6af930 Binary files /dev/null and b/website/raw_sprites/spritesmith/achievements/achievement-allYourBase2x.png differ diff --git a/website/raw_sprites/spritesmith/achievements/achievement-backToBasics2x.png b/website/raw_sprites/spritesmith/achievements/achievement-backToBasics2x.png new file mode 100644 index 0000000000..9eb48e4ce5 Binary files /dev/null and b/website/raw_sprites/spritesmith/achievements/achievement-backToBasics2x.png differ diff --git a/website/raw_sprites/spritesmith/achievements/achievement-justAddWater2x.png b/website/raw_sprites/spritesmith/achievements/achievement-justAddWater2x.png new file mode 100644 index 0000000000..bde84dd783 Binary files /dev/null and b/website/raw_sprites/spritesmith/achievements/achievement-justAddWater2x.png differ diff --git a/website/raw_sprites/spritesmith/achievements/achievement-mindOverMatter2x.png b/website/raw_sprites/spritesmith/achievements/achievement-mindOverMatter2x.png new file mode 100644 index 0000000000..a6e0ac4fff Binary files /dev/null and b/website/raw_sprites/spritesmith/achievements/achievement-mindOverMatter2x.png differ diff --git a/website/server/models/group.js b/website/server/models/group.js index 636eb3ca0e..ef05e89c12 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -41,6 +41,7 @@ import { getGroupChat, translateMessage } from '../libs/chat/group-chat'; import { model as UserNotification } from './userNotification'; const questScrolls = shared.content.quests; +const questSeriesAchievements = shared.content.questSeriesAchievements; const Schema = mongoose.Schema; export const INVITES_LIMIT = 100; // must not be greater than MAX_EMAIL_INVITES_BY_USER @@ -849,25 +850,6 @@ schema.methods.finishQuest = async function finishQuest (quest) { } }); - let masterClasserQuests = [ - 'dilatoryDistress1', - 'dilatoryDistress2', - 'dilatoryDistress3', - 'mayhemMistiflying1', - 'mayhemMistiflying2', - 'mayhemMistiflying3', - 'stoikalmCalamity1', - 'stoikalmCalamity2', - 'stoikalmCalamity3', - 'taskwoodsTerror1', - 'taskwoodsTerror2', - 'taskwoodsTerror3', - 'lostMasterclasser1', - 'lostMasterclasser2', - 'lostMasterclasser3', - 'lostMasterclasser4', - ]; - // Send webhooks in background // @TODO move the find users part to a worker as well, not just the http request User.find({ @@ -893,24 +875,27 @@ schema.methods.finishQuest = async function finishQuest (quest) { }); }); + _.forEach(questSeriesAchievements, async (questList, achievement) => { + if (questList.includes(questK)) { + let questAchievementQuery = {}; + questAchievementQuery[`achievements.${achievement}`] = {$ne: true}; + + _.forEach(questList, (questName) => { + if (questName !== questK) { + questAchievementQuery[`achievements.quests.${questName}`] = {$gt: 0}; + } + }); + + let questAchievementUpdate = {$set: {}}; + questAchievementUpdate.$set[`achievements.${achievement}`] = true; + + promises.push(participants.map(userId => { + return _updateUserWithRetries(userId, questAchievementUpdate, null, questAchievementQuery); + })); + } + }); + await Promise.all(promises); - - if (masterClasserQuests.includes(questK)) { - let lostMasterclasserQuery = { - 'achievements.lostMasterclasser': {$ne: true}, - }; - masterClasserQuests.forEach(questName => { - lostMasterclasserQuery[`achievements.quests.${questName}`] = {$gt: 0}; - }); - let lostMasterclasserUpdate = { - $set: {'achievements.lostMasterclasser': true}, - }; - - let lostMasterClasserPromises = participants.map(userId => { - return _updateUserWithRetries(userId, lostMasterclasserUpdate, null, lostMasterclasserQuery); - }); - await Promise.all(lostMasterClasserPromises); - } }; function _isOnQuest (user, progress, group) { diff --git a/website/server/models/user/schema.js b/website/server/models/user/schema.js index e411bfb437..de53c5a214 100644 --- a/website/server/models/user/schema.js +++ b/website/server/models/user/schema.js @@ -120,6 +120,10 @@ let schema = new Schema({ joinedChallenge: Boolean, invitedFriend: Boolean, lostMasterclasser: Boolean, + mindOverMatter: Boolean, + justAddWater: Boolean, + backToBasics: Boolean, + allYourBase: Boolean, }, backer: {