From 31b2781333ee7d3283dcbfc971dc5f3b70768c2e Mon Sep 17 00:00:00 2001 From: Kalista Payne Date: Thu, 19 Mar 2026 15:09:32 -0500 Subject: [PATCH] Squashed commit of the following: commit 866f074a15512e2f897d007ad1a5379ffee456d7 Author: Kalista Payne Date: Wed Mar 18 15:51:18 2026 -0500 fix(quests): remove backticks from text commit d06fc2825e8638d5c127277f92ef94f26f3daba0 Author: Kalista Payne Date: Wed Mar 18 13:16:32 2026 -0500 fix(background): add missing data commit 55156c5f808e3ad9bf16d6465b5b7596fce12631 Author: Kalista Payne Date: Wed Mar 18 13:00:43 2026 -0500 fix(lint): max-len commit db88092acfceac7c465c1e01638ae18bd1572ee3 Author: Kalista Payne Date: Wed Mar 18 12:56:30 2026 -0500 fix(customization): show event backgrounds for AF commit d6fd1ce7fa1b15771786518656a6663e46ba42bc Author: Phillip Thelen Date: Tue Mar 17 15:55:53 2026 +0100 set release date commit b876d8c78928283a81fadfeb1116e09c73f3e25f Author: Phillip Thelen Date: Tue Mar 17 11:40:17 2026 +0100 Improve swap handling commit f75a4d01473b492ef28b2324f2defe46cb3845f8 Author: Phillip Thelen Date: Tue Mar 17 11:22:42 2026 +0100 use correct name for bear sprites commit 195db1b1328a73043e1a1c9014e9ce22a3df72f3 Author: Phillip Thelen Date: Mon Mar 16 23:01:35 2026 +0100 more fix commit a42d3f08d749abf2a072805e4c54f67564c1e320 Author: Phillip Thelen Date: Mon Mar 16 18:43:21 2026 +0100 fix test commit 9c06299c343fd29c5dcddfe62e83d410f6c93115 Author: Phillip Thelen Date: Mon Mar 16 09:13:13 2026 +0100 AF tweaks commit 5465e23e679cad1d81404a0911204aaf18c231b7 Author: Kalista Payne Date: Wed Mar 11 19:43:01 2026 -0500 fix(sprites): add missing gif redirects commit 5721ecc6f2b21fd6049a028fddc79fb6dea0434e Author: Kalista Payne Date: Tue Mar 10 16:34:26 2026 -0500 chore(css): run sprites commit 2184ff2e6937459c404f3067c9d0662c9d908074 Author: Kalista Payne Date: Tue Mar 10 10:38:34 2026 -0500 fix(test): date commit d684a7297c2f86f417677a1f0fa7ae47c8d4d1d6 Author: Kalista Payne Date: Tue Mar 10 10:32:04 2026 -0500 fix(test): update for 2026, also more lint commit 82e7947fd7bc99d1dd33ff08490f2bda0ff74d2b Author: Kalista Payne Date: Tue Mar 10 10:22:05 2026 -0500 fix(event): lint and missing pieces commit 96818b2d778848fdbff99afdf50b9f305c25b1a9 Author: Kalista Payne Date: Mon Mar 9 20:11:22 2026 -0500 feat(event): finished Alien build commit 4c9763b676f5873d83c4c9c249e7e0ffb5905a2e Author: Kalista Payne Date: Fri Mar 6 16:30:36 2026 -0600 wip(event): April Fools 2026 build commit 2f16a016e63f523c81a9425d5d7c49e9138c46a5 Author: Phillip Thelen Date: Wed Feb 4 14:16:50 2026 +0100 add april fools tests commit cd1d926c9898f5faf4fe82e4ecccc4d801248f06 Author: Phillip Thelen Date: Wed Feb 4 14:09:16 2026 +0100 make april fools cycle through commit d8a4216c412437448ea15f8fd456765ebd5f53f9 Author: Phillip Thelen Date: Mon Feb 2 14:41:26 2026 +0100 fix lint commit 8265a15923f34ad1afdaab05992c3def5cb4d202 Author: Phillip Thelen Date: Mon Feb 2 12:34:46 2026 +0100 name key more generic commit 9c7bde8ad563bd346b548fdf59fb0454910ae256 Author: Phillip Thelen Date: Mon Feb 2 12:26:43 2026 +0100 right date for april fools commit c2b92c6311d60a609e8b8743fc0efe3cbac853b6 Author: Phillip Thelen Date: Mon Feb 2 12:26:19 2026 +0100 rework how april fools works --- test/content/aprilFools.test.js | 41 ++++++ website/client/src/assets/css/sprites.css | 125 +----------------- .../assets/css/sprites/spritesmith-main.css | 54 +++++++- website/client/src/components/avatar.vue | 9 +- .../client/src/components/creatorIntro.vue | 3 +- .../components/inventory/stable/petItem.vue | 10 +- website/client/src/mixins/foolPet.js | 58 ++------ website/client/src/router/index.js | 14 ++ website/common/locales/en/backgrounds.json | 2 + website/common/locales/en/content.json | 1 + website/common/locales/en/questsContent.json | 34 +++-- .../script/content/appearance/backgrounds.js | 3 + .../script/content/constants/aprilFools.js | 39 ++++++ .../common/script/content/constants/events.js | 4 +- .../script/content/constants/gifSprites.js | 3 + .../script/content/constants/releaseDates.js | 1 + .../common/script/content/hatching-potions.js | 4 + .../common/script/content/quests/seasonal.js | 39 ++++++ website/common/script/content/stable.js | 4 +- 19 files changed, 246 insertions(+), 202 deletions(-) create mode 100644 test/content/aprilFools.test.js create mode 100644 website/common/script/content/constants/aprilFools.js diff --git a/test/content/aprilFools.test.js b/test/content/aprilFools.test.js new file mode 100644 index 0000000000..64fc100df6 --- /dev/null +++ b/test/content/aprilFools.test.js @@ -0,0 +1,41 @@ +import { getMatchingSwap, makeSubstitutionMap } from '../../website/common/script/content/constants/aprilFools'; + +describe('April Fools', () => { + describe('getMatchingSwap', () => { + it('returns Veggie for 2020', () => { + const swap = getMatchingSwap(new Date('2020-04-01')); + expect(swap).to.equal('Veggie'); + }); + it('returns Alien for 2026', () => { + const swap = getMatchingSwap(new Date('2026-04-01')); + expect(swap).to.equal('Alien'); + }); + it('Cycles through swaps correctly', () => { + const swap = getMatchingSwap(new Date('2027-04-01')); + expect(swap).to.equal('Veggie'); + }); + }); + + describe('makeSubstitutionMap', () => { + it('returns correct substitution for Veggie', () => { + const substitutions = makeSubstitutionMap('Veggie'); + expect(substitutions.pets['Pet-Wolf-']).to.equal('Pet-Wolf-Veggie'); + expect(substitutions.pets['Pet-TigerCub-']).to.equal('Pet-TigerCub-Veggie'); + expect(substitutions.pets['Pet-Yarn-']).to.equal('Pet-BearCub-Veggie'); + expect(substitutions.pets.default).to.equal('Pet-Dragon-Veggie'); + expect(substitutions.pets.noPet).to.equal('Pet-Wolf-Veggie'); + expect(substitutions.pets.noPetIOS).to.equal('Pet-TigerCub-Veggie'); + expect(substitutions.pets.noPetAndroid).to.equal('Pet-Cactus-Veggie'); + }); + + it('returns correct substitution for Cryptid', () => { + const substitutions = makeSubstitutionMap('Cryptid'); + expect(substitutions.pets['Pet-Fox-']).to.equal('Pet-Fox-Cryptid'); + expect(substitutions.pets['Pet-FlyingPig-']).to.equal('Pet-FlyingPig-Cryptid'); + expect(substitutions.pets['Pet-Yarn-']).to.equal('Pet-BearCub-Cryptid'); + expect(substitutions.pets.default).to.equal('Pet-Dragon-Cryptid'); + expect(substitutions.pets.noPet).to.equal('Pet-Wolf-Cryptid'); + expect(substitutions.pets.noPetAndroid).to.equal('Pet-Cactus-Cryptid'); + }); + }); +}); diff --git a/website/client/src/assets/css/sprites.css b/website/client/src/assets/css/sprites.css index 1c21bf44f1..aa64fe9ef5 100755 --- a/website/client/src/assets/css/sprites.css +++ b/website/client/src/assets/css/sprites.css @@ -1,57 +1,3 @@ -.quest_lostMasterclasser4 { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_lostMasterclasser4.gif") no-repeat; - width: 219px; - height: 219px; -} - -.quest_windup { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_windup.gif") no-repeat; - width: 219px; - height: 219px; -} - -.quest_solarSystem { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_solarSystem.gif") no-repeat; - width: 219px; - height: 219px; -} - -.quest_virtualpet { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_virtualpet.gif") no-repeat; - width: 219px; - height: 219px; -} - -.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup, -.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid { - width: 68px; - height: 68px; -} - -.Pet_HatchingPotion_Dessert { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Dessert.gif") no-repeat; -} - -.Pet_HatchingPotion_Veggie { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Veggie.gif") no-repeat; -} - -.Pet_HatchingPotion_Windup { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Windup.gif") no-repeat; -} - -.Pet_HatchingPotion_VirtualPet { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_VirtualPet.gif") no-repeat; -} - -.Pet_HatchingPotion_Fungi { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat; -} - -.Pet_HatchingPotion_Cryptid { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat; -} - .Gems { display:inline-block; margin-right:5px; @@ -80,6 +26,7 @@ margin-left: -3px; margin-top: -18px; } + .slim_armor_special_0, .broad_armor_special_0, .shield_special_0 { width: 90px; height: 90px; @@ -87,7 +34,6 @@ /* Critical */ .weapon_special_critical { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_critical.gif") no-repeat; width: 90px; height: 90px; margin-left:-12px; @@ -98,6 +44,7 @@ .weapon_special_1 { margin-left: -12px; } + .broad_armor_special_1, .slim_armor_special_1, .head_special_1 { width: 90px; height: 90px; @@ -106,36 +53,15 @@ .back_special_heroicAureole { width: 114px; height: 90px; - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_special_heroicAureole.gif") no-repeat; } -.head_special_0 { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeHelmet.gif") no-repeat; -} .head_special_1 { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalHelmet.gif") no-repeat; margin-top: 3px; } -.broad_armor_special_0,.slim_armor_special_0 { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeArmor.gif") no-repeat; -} -.broad_armor_special_1,.slim_armor_special_1 { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalArmor.gif") no-repeat; -} - -.shield_special_0 { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Shield-TormentedSkull.gif") no-repeat; -} - -.weapon_special_0 { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Weapon-DarkSoulsBlade.gif") no-repeat; -} - .Pet-Wolf-Cerberus { width: 105px; height: 72px; - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-CerberusPup.gif") no-repeat; } .broad_armor_special_ks2019, .slim_armor_special_ks2019, .eyewear_special_ks2019, .head_special_ks2019, .shield_special_ks2019 { @@ -143,36 +69,17 @@ height: 120px; } -.broad_armor_special_ks2019, .slim_armor_special_ks2019 { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonArmor.gif") no-repeat; -} - -.eyewear_special_ks2019 { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonVisor.gif") no-repeat; -} - -.head_special_ks2019 { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonHelm.gif") no-repeat; -} - -.shield_special_ks2019 { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonShield.gif") no-repeat; -} - .weapon_special_ks2019 { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonGlaive.gif") no-repeat; width: 120px; height: 120px; } .Pet-Gryphon-Gryphatrice { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-Gryphatrice.gif") no-repeat; width: 81px; height: 99px; } .Pet-Gryphatrice-Jubilant { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Gryphatrice-Jubilant.gif") no-repeat; width: 81px; height: 96px; } @@ -182,39 +89,11 @@ height: 135px; } -.Mount_Head_Gryphon-Gryphatrice { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Head-Gryphatrice.gif") no-repeat; -} - -.Mount_Body_Gryphon-Gryphatrice { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat; -} - -.Mount_Head_Dragon-Hydra { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Hydra.gif") no-repeat; -} - -.Mount_Body_Dragon-Hydra { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Hydra.gif") no-repeat; -} - .background_airship, .background_clocktower, .background_steamworks { width: 141px; height: 147px; } -.background_airship { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_airship.gif") no-repeat; -} - -.background_clocktower { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_clocktower.gif") no-repeat; -} - -.background_steamworks { - background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_steamworks.gif") no-repeat; -} - [class*="Mount_Head_"], [class*="Mount_Body_"] { margin-top:18px; /* Sprite accommodates 105x123 box */ diff --git a/website/client/src/assets/css/sprites/spritesmith-main.css b/website/client/src/assets/css/sprites/spritesmith-main.css index 13638101c7..3b9c721c49 100644 --- a/website/client/src/assets/css/sprites/spritesmith-main.css +++ b/website/client/src/assets/css/sprites/spritesmith-main.css @@ -1801,6 +1801,11 @@ width: 141px; height: 147px; } +.background_on_a_strange_planet { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_a_strange_planet.png'); + width: 141px; + height: 147px; +} .background_on_tree_branch { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_tree_branch.png'); width: 141px; @@ -31485,8 +31490,8 @@ width: 114px; height: 90px; } -.slim_armor_armoire_handstandOutfit { - background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_handstandOutfit .png'); +.slim_armor_armoire_handstandOutfit { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_handstandOutfit.png'); width: 114px; height: 90px; } @@ -53223,6 +53228,11 @@ width: 81px; height: 99px; } +.Pet-BearCub-Alien { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Alien.png'); + width: 81px; + height: 99px; +} .Pet-BearCub-Amber { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Amber.png'); width: 81px; @@ -53713,6 +53723,11 @@ width: 81px; height: 99px; } +.Pet-Cactus-Alien { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Alien.png'); + width: 81px; + height: 99px; +} .Pet-Cactus-Amber { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Amber.png'); width: 81px; @@ -54503,6 +54518,11 @@ width: 81px; height: 99px; } +.Pet-Dragon-Alien { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Alien.png'); + width: 81px; + height: 99px; +} .Pet-Dragon-Amber { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Amber.png'); width: 81px; @@ -54998,6 +55018,11 @@ width: 81px; height: 99px; } +.Pet-FlyingPig-Alien { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Alien.png'); + width: 81px; + height: 99px; +} .Pet-FlyingPig-Amber { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Amber.png'); width: 81px; @@ -55333,6 +55358,11 @@ width: 81px; height: 99px; } +.Pet-Fox-Alien { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Alien.png'); + width: 81px; + height: 99px; +} .Pet-Fox-Amber { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Amber.png'); width: 81px; @@ -56113,6 +56143,11 @@ width: 81px; height: 99px; } +.Pet-LionCub-Alien { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Alien.png'); + width: 81px; + height: 99px; +} .Pet-LionCub-Amber { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Amber.png'); width: 81px; @@ -56718,6 +56753,11 @@ width: 81px; height: 99px; } +.Pet-PandaCub-Alien { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Alien.png'); + width: 81px; + height: 99px; +} .Pet-PandaCub-Amber { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Amber.png'); width: 81px; @@ -58113,6 +58153,11 @@ width: 81px; height: 99px; } +.Pet-TigerCub-Alien { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Alien.png'); + width: 81px; + height: 99px; +} .Pet-TigerCub-Amber { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Amber.png'); width: 81px; @@ -58758,6 +58803,11 @@ width: 81px; height: 99px; } +.Pet-Wolf-Alien { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Alien.png'); + width: 81px; + height: 99px; +} .Pet-Wolf-Amber { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Amber.png'); width: 81px; diff --git a/website/client/src/components/avatar.vue b/website/client/src/components/avatar.vue index b0094e6549..b39125c5af 100644 --- a/website/client/src/components/avatar.vue +++ b/website/client/src/components/avatar.vue @@ -321,10 +321,11 @@ export default { return null; }, petClass () { - const foolEvent = this.currentEventList?.find(event => event.aprilFools && moment() - .isBetween(event.start, event.end)); - if (foolEvent) { - return this.foolPet(this.member.items.currentPet, foolEvent.aprilFools); + const substitutionEvent = this.currentEventList?.find(event => event.spriteSubstitutions + && moment().isBetween(event.start, event.end)); + if (substitutionEvent && substitutionEvent.spriteSubstitutions.pets) { + return this.foolPet(`Pet-${this.member.items.currentPet}`, + substitutionEvent.spriteSubstitutions.pets); } if (this.member?.items.currentPet) return `Pet-${this.member.items.currentPet}`; return ''; diff --git a/website/client/src/components/creatorIntro.vue b/website/client/src/components/creatorIntro.vue index e0e566cdd5..67d358a221 100644 --- a/website/client/src/components/creatorIntro.vue +++ b/website/client/src/components/creatorIntro.vue @@ -187,7 +187,8 @@
moment() - .isBetween(event.start, event.end) && event.aprilFools); - if (this.isOwned() && foolEvent) { - if (this.isSpecial()) return `stable_${this.foolPet(this.item.key, foolEvent.aprilFools)}`; - const petString = `${this.item.eggKey}-${this.item.key}`; - return `stable_${this.foolPet(petString, foolEvent.aprilFools)}`; + const substitutionEvent = this.currentEventList?.find(event => moment() + .isBetween(event.start, event.end) && event.spriteSubstitutions); + if (this.isOwned() && substitutionEvent && substitutionEvent.spriteSubstitutions.pets) { + return `stable_${this.foolPet(`Pet-${this.item.key}`, substitutionEvent.spriteSubstitutions.pets)}`; } if (this.isOwned() || (this.mountOwned() && this.isHatchable())) { diff --git a/website/client/src/mixins/foolPet.js b/website/client/src/mixins/foolPet.js index 0a44b1c7ad..e779b83277 100644 --- a/website/client/src/mixins/foolPet.js +++ b/website/client/src/mixins/foolPet.js @@ -1,56 +1,14 @@ -import includes from 'lodash/includes'; - export default { methods: { - foolPet (pet, prank) { - const SPECIAL_PETS = [ - 'Bear-Veteran', - 'BearCub-Polar', - 'Cactus-Veteran', - 'Dragon-Hydra', - 'Dragon-Veteran', - 'Fox-Veteran', - 'Gryphatrice-Jubilant', - 'Gryphon-Gryphatrice', - 'Gryphon-RoyalPurple', - 'Hippogriff-Hopeful', - 'Jackalope-RoyalPurple', - 'JackOLantern-Base', - 'JackOLantern-Ghost', - 'JackOLantern-Glow', - 'JackOLantern-RoyalPurple', - 'Lion-Veteran', - 'MagicalBee-Base', - 'Mammoth-Base', - 'MantisShrimp-Base', - 'Orca-Base', - 'Phoenix-Base', - 'Tiger-Veteran', - 'Turkey-Base', - 'Turkey-Gilded', - 'Wolf-Cerberus', - 'Wolf-Veteran', - ]; - const BASE_PETS = [ - 'BearCub', - 'Cactus', - 'Dragon', - 'FlyingPig', - 'Fox', - 'LionCub', - 'PandaCub', - 'TigerCub', - 'Wolf', - ]; - if (!pet) return `Pet-TigerCub-${prank}`; - if (SPECIAL_PETS.indexOf(pet) !== -1) { - return `Pet-Dragon-${prank}`; + foolPet (pet, substitutions) { + if (!pet || pet === 'Pet-') return substitutions.noPet; + if (substitutions[pet]) return substitutions[pet]; + for (const key in substitutions) { + if (pet.startsWith(key)) { + return substitutions[key]; + } } - const species = pet.slice(0, pet.indexOf('-')); - if (includes(BASE_PETS, species)) { - return `Pet-${species}-${prank}`; - } - return `Pet-BearCub-${prank}`; + return substitutions.default; }, }, }; diff --git a/website/client/src/router/index.js b/website/client/src/router/index.js index 2ca833f996..8032fce93f 100644 --- a/website/client/src/router/index.js +++ b/website/client/src/router/index.js @@ -85,6 +85,13 @@ const router = new VueRouter({ props: true, }, { name: 'profile', path: '/user/profile' }, + { + name: 'avatar', + path: '/avatar', + children: [ + { name: 'backgrounds', path: 'backgrounds' }, + ], + }, { name: 'stats', path: '/user/stats' }, { name: 'achievements', path: '/user/achievements' }, { @@ -410,6 +417,13 @@ router.beforeEach(async (to, from, next) => { router.app.$root.$emit('bv::hide::modal', 'profile'); } + if (to.name === 'backgrounds') { + store.state.avatarEditorOptions.editingUser = true; + store.state.avatarEditorOptions.startingPage = 'backgrounds'; + router.app.$root.$emit('bv::show::modal', 'avatar-modal'); + return null; + } + return next(); }); diff --git a/website/common/locales/en/backgrounds.json b/website/common/locales/en/backgrounds.json index 19c76945c4..8765a6142c 100644 --- a/website/common/locales/en/backgrounds.json +++ b/website/common/locales/en/backgrounds.json @@ -1086,6 +1086,8 @@ "eventBackgrounds": "Event Backgrounds", "backgroundBirthdayBashText": "Birthday Bash", "backgroundBirthdayBashNotes": "Habitica's having a birthday party, and everyone's invited!", + "backgroundOnAStrangePlanetText": "On a Strange Planet", + "backgroundOnAStrangePlanetNotes": "Venture where no Habitican has gone before: On a Strange Planet.", "monthlyBackgrounds": "Monthly Backgrounds" } diff --git a/website/common/locales/en/content.json b/website/common/locales/en/content.json index c6cd132481..0f139af809 100644 --- a/website/common/locales/en/content.json +++ b/website/common/locales/en/content.json @@ -356,6 +356,7 @@ "hatchingPotionBalloon": "Balloon", "hatchingPotionCryptid": "Cryptid", "hatchingPotionOpal": "Opal", + "hatchingPotionAlien": "Alien", "hatchingPotionNotes": "Pour this on an egg, and it will hatch as a <%= potText %> Pet.", "premiumPotionUnlimitedNotes": "Not usable on Quest Pet eggs.", diff --git a/website/common/locales/en/questsContent.json b/website/common/locales/en/questsContent.json index 336e3d29ba..cd5c77b589 100644 --- a/website/common/locales/en/questsContent.json +++ b/website/common/locales/en/questsContent.json @@ -221,7 +221,7 @@ "questTRexUndeadBoss": "Skeletal Tyrannosaur", "questTRexUndeadRageTitle": "Skeleton Healing", "questTRexUndeadRageDescription": "This bar fills when you don't complete your Dailies. When it is full, the Skeletal Tyrannosaur will heal 30% of its remaining health!", - "questTRexUndeadRageEffect": "`Skeletal Tyrannosaur uses SKELETON HEALING!`\n\nThe monster lets forth an unearthly roar, and some of its damaged bones knit back together!", + "questTRexUndeadRageEffect": "Skeletal Tyrannosaur uses SKELETON HEALING!\n\nThe monster lets forth an unearthly roar, and some of its damaged bones knit back together!", "questTRexDropTRexEgg": "Tyrannosaur (Egg)", "questTRexUnlockText": "Unlocks Tyrannosaur Eggs for purchase in the Market", @@ -282,7 +282,7 @@ "questDilatoryDistress2Boss": "Water Skull Swarm", "questDilatoryDistress2RageTitle": "Swarm Respawn", "questDilatoryDistress2RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Water Skull Swarm will heal 30% of its remaining health!", - "questDilatoryDistress2RageEffect": "`Water Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls pour forth from the crevasse, bolstering the swarm!", + "questDilatoryDistress2RageEffect": "Water Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls pour forth from the crevasse, bolstering the swarm!", "questDilatoryDistress2DropSkeletonPotion": "Skeleton Hatching Potion", "questDilatoryDistress2DropCottonCandyBluePotion": "Cotton Candy Blue Hatching Potion", "questDilatoryDistress2DropHeadgear": "Fire Coral Circlet (Headgear)", @@ -398,7 +398,7 @@ "questAxolotlUnlockText": "Unlocks Axolotl Eggs for purchase in the Market", "questAxolotlRageTitle": "Axolotl Regeneration", "questAxolotlRageDescription": "This bar fills when you don't complete your Dailies. When it is full, the Magical Axolotl will heal 30% of its remaining health!", - "questAxolotlRageEffect": "`Magical Axolotl uses AXOLOTL REGENERATION!`\n\n`A curtain of colorful bubbles obscures the monster for a moment, and when it clears, some of its wounds have vanished!`", + "questAxolotlRageEffect": "Magical Axolotl uses AXOLOTL REGENERATION!\n\nA curtain of colorful bubbles obscures the monster for a moment, and when it clears, some of its wounds have vanished!", "questTurtleText": "Guide the Turtle", "questTurtleNotes": "Help! This giant sea turtle cannot find her way to her nesting beach. She returns there every year to lay her eggs, but this year Inkomplete Bay is filled with toxic Task Flotsam made of red Dailies and unchecked To Do's. \"She's thrashing in a panic!\" @JessicaChase says.

@UncommonCriminal nods. \"It's because her guiding senses are fogged and confused.\"

@Scarabsi grabs your arm. \"Can you help clear the Task Flotsam blocking her path? It may be hazardous, but we have to help her!\"", @@ -435,7 +435,7 @@ "questTaskwoodsTerror1Boss": "Fire Skull Swarm", "questTaskwoodsTerror1RageTitle": "Swarm Respawn", "questTaskwoodsTerror1RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Fire Skull Swarm will heal 30% of its remaining health!", - "questTaskwoodsTerror1RageEffect": "`Fire Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls swirl around you in a gout of flame!", + "questTaskwoodsTerror1RageEffect": "Fire Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls swirl around you in a gout of flame!", "questTaskwoodsTerror1DropSkeletonPotion": "Skeleton Hatching Potion", "questTaskwoodsTerror1DropRedPotion": "Red Hatching Potion", "questTaskwoodsTerror1DropHeadgear": "Pyromancer's Turban (Headgear)", @@ -507,7 +507,7 @@ "questStoikalmCalamity1Boss": "Earth Skull Swarm", "questStoikalmCalamity1RageTitle": "Swarm Respawn", "questStoikalmCalamity1RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Earth Skull Swarm will heal 30% of its remaining health!", - "questStoikalmCalamity1RageEffect": "`Earth Skull Swarm uses SWARM RESPAWN!`\n\nMore skulls break free from the ground, their teeth chattering in the cold!", + "questStoikalmCalamity1RageEffect": "Earth Skull Swarm uses SWARM RESPAWN!\n\nMore skulls break free from the ground, their teeth chattering in the cold!", "questStoikalmCalamity1DropSkeletonPotion": "Skeleton Hatching Potion", "questStoikalmCalamity1DropDesertPotion": "Desert Hatching Potion", "questStoikalmCalamity1DropArmor": "Mammoth Rider Armor", @@ -554,7 +554,7 @@ "questMayhemMistiflying1Boss": "Air Skull Swarm", "questMayhemMistiflying1RageTitle": "Swarm Respawn", "questMayhemMistiflying1RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Air Skull Swarm will heal 30% of its remaining health!", - "questMayhemMistiflying1RageEffect": "`Air Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls come whirling out of the clouds!", + "questMayhemMistiflying1RageEffect": "Air Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls come whirling out of the clouds!", "questMayhemMistiflying1DropSkeletonPotion": "Skeleton Hatching Potion", "questMayhemMistiflying1DropWhitePotion": "White Hatching Potion", "questMayhemMistiflying1DropArmor": "Roguish Rainbow Messenger Robes (Armor)", @@ -623,7 +623,7 @@ "questLostMasterclasser3Boss": "Void Skull Swarm", "questLostMasterclasser3RageTitle": "Swarm Respawn", "questLostMasterclasser3RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Void Skull Swarm will heal 30% of its remaining health!", - "questLostMasterclasser3RageEffect": "`Void Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls scream down from the heavens, bolstering the swarm!", + "questLostMasterclasser3RageEffect": "Void Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls scream down from the heavens, bolstering the swarm!", "questLostMasterclasser3DropBodyAccessory": "Aether Amulet (Body Accessory)", "questLostMasterclasser3DropBasePotion": "Base Hatching Potion", "questLostMasterclasser3DropGoldenPotion": "Golden Hatching Potion", @@ -637,7 +637,7 @@ "questLostMasterclasser4Boss": "Anti'zinnya", "questLostMasterclasser4RageTitle": "Siphoning Void", "questLostMasterclasser4RageDescription": "Siphoning Void: This bar fills when you don't complete your Dailies. When it is full, Anti'zinnya will remove the party's Mana!", - "questLostMasterclasser4RageEffect": "`Anti'zinnya uses SIPHONING VOID!` In a twisted inversion of the Ethereal Surge spell, you feel your magic drain away into the darkness!", + "questLostMasterclasser4RageEffect": "Anti'zinnya uses SIPHONING VOID! In a twisted inversion of the Ethereal Surge spell, you feel your magic drain away into the darkness!", "questLostMasterclasser4DropBackAccessory": "Aether Cloak (Back Accessory)", "questLostMasterclasser4DropWeapon": "Aether Crystals (Two-Handed Weapon)", "questLostMasterclasser4DropMount": "Invisible Aether Mount", @@ -804,7 +804,7 @@ "questWaffleBoss": "Awful Waffle", "questWaffleRageTitle": "Maple Mire", "questWaffleRageDescription": "Maple Mire: This bar fills when you don't complete your Dailies. When it is full, the Awful Waffle will subtract from the pending damage that party members have built up!", - "questWaffleRageEffect": "`Awful Waffle uses MAPLE MIRE!` Sticky sappy syrup slows your swings and spells! Pending damage reduced.", + "questWaffleRageEffect": "Awful Waffle uses MAPLE MIRE! Sticky sappy syrup slows your swings and spells! Pending damage reduced.", "questWaffleDropDessertPotion": "Confection Hatching Potion", "questWaffleUnlockText": "Unlocks Confection Hatching Potions for purchase in the Market", @@ -875,7 +875,7 @@ "questVirtualPetBoss": "Wotchimon", "questVirtualPetRageTitle": "The Beepening", "questVirtualPetRageDescription": "This bar fills when you don't complete your Dailies. When it is full, the Wotchimon will take away some of your party's pending damage!", - "questVirtualPetRageEffect": "`Wotchimon uses Bothersome Beep!` Wotchimon sounds a bothersome beep, and its happiness bar suddenly disappears! Pending damage reduced.", + "questVirtualPetRageEffect": "Wotchimon uses Bothersome Beep! Wotchimon sounds a bothersome beep, and its happiness bar suddenly disappears! Pending damage reduced.", "questVirtualPetDropVirtualPetPotion": "Virtual Pet Hatching Potion", "questVirtualPetUnlockText": "Unlocks Virtual Pet Hatching Potion for purchase in the Market", @@ -885,7 +885,7 @@ "questPinkMarbleBoss": "Cupido", "questPinkMarbleRageTitle": "Pink Punch", "questPinkMarbleRageDescription": "This bar fills when you don't complete your Dailies. When it is full, Cupido will take away some of your party's pending damage!", - "questPinkMarbleRageEffect": "`Cupido uses Pink Punch!` That wasn't affectionate at all! Your partymates are taken aback. Pending damage reduced.", + "questPinkMarbleRageEffect": "Cupido uses Pink Punch! That wasn't affectionate at all! Your partymates are taken aback. Pending damage reduced.", "questPinkMarbleDropPinkMarblePotion": "Pink Marble Hatching Potion", "questPinkMarbleUnlockText": "Unlocks Pink Marble Hatching Potions for purchase in the Market.", @@ -997,5 +997,15 @@ "questFungiRageDescription": "This bar fills when you don't complete your Dailies. When it's full, the Moody Mushroom will take away some of your party's pending damage", "questFungiRageEffect": "A Mist emanates from the Moody Mushroom and surrounds your party, dampening the mood and subduing your magic. The party's MP is reduced!", "questFungiDropFungiPotion": "Fungi Hatching Potion", - "questFungiUnlockText": "Unlocks Fungi Hatching Potions for purchase in the Market." + "questFungiUnlockText": "Unlocks Fungi Hatching Potions for purchase in the Market.", + + "questAlienText": "Invasion of the Motivation Snatchers", + "questAlienNotes": "It’s been a strange few days in Habitica. The great flying saucer still hovers near the Flourishing Fields. It hums oddly. Why is it lingering? April Fool’s Day has passed, and the Master of Rogues' time in the spotlight has ended.

You wander toward the light of the space ship. You may as well check it out and get a few steps in while you’re at it.

As you get closer you see the April Fool, looking a bit grim. His face appears greenish in the light of the ship’s beam.

”'Twas my plan to get some potions for everyone, a little gift so all can enjoy their little extraterrestrial pals again! But I just can’t work up the gumption… I do believe I know why,” the Fool says, nodding toward the beam.

Little symbols are being sucked up into the ship. It’s all your checked off tasks! No wonder your motivation’s been lackluster.

”Our motivation is being abducted!” you exclaim. “We have to rescue it before it ends up in deep space somewhere!”

The Fool smiles. “Concentrate your thoughts on the tasks you know you need to finish! I’ll do the rest with a bit of magic.”", + "questAlienCompletion": "You’ve managed to wrestle back the stolen motivation with your determination and the Fool’s magic power. As you feel your drive returning, the UFO descends, and a ramp slowly comes out along with a large, green, one-eyed creature. While strange-looking, it doesn’t seem threatening.

“Looks like we went a little far trying to harvest a little extra encouragement from your fine city,” it says. “Apologies for that, and fantastic work getting it back. The extra aura of your efforts actually charged up the ship’s engine enough to get us home! Please, take these with our thanks.”

“Ooh potions,” says the Fool, “how delightful, and how convenient for me that you have them all ready to go!”", + "questAlienBoss": "Encouragement Thief, the Extraterrestrial", + "questAlienRageTitle": "Intergalactic Impediment", + "questAlienRageDescription": "This bar fills when you don't complete your Dailies. When it is full, the Extraterrestrial will discourage you by recovering some of its Health!", + "questAlienRageEffect": "Encouragement Thief uses Intergalactic Impediment! You've backslid right through hyperspace. Your opponent recovers HP!", + "questAlienDropAlienPotion": "Alien Hatching Potion", + "questAlienUnlockText": "Unlocks Alien Hatching Potion for purchase in the Market" } diff --git a/website/common/script/content/appearance/backgrounds.js b/website/common/script/content/appearance/backgrounds.js index d2ccd3ad0b..3ea7a03ece 100644 --- a/website/common/script/content/appearance/backgrounds.js +++ b/website/common/script/content/appearance/backgrounds.js @@ -696,6 +696,9 @@ const backgrounds = { birthday_bash: { price: 0, }, + on_a_strange_planet: { + price: 0, + }, }, timeTravelBackgrounds: { airship: { diff --git a/website/common/script/content/constants/aprilFools.js b/website/common/script/content/constants/aprilFools.js new file mode 100644 index 0000000000..0b88d402fb --- /dev/null +++ b/website/common/script/content/constants/aprilFools.js @@ -0,0 +1,39 @@ +import eggs from '../eggs'; +import stable from '../stable'; + +const SWAPS = [ + 'Veggie', + 'Dessert', + 'VirtualPet', + 'TeaShop', + 'Fungi', + 'Cryptid', + 'Alien', +]; + +export function getMatchingSwap (date = new Date()) { + const year = date.getFullYear(); + const diff = year - 2020; + return SWAPS[diff % SWAPS.length]; +} + +export function makeSubstitutionMap (swappedPotion) { + const substitutions = { + pets: { + default: `Pet-Dragon-${swappedPotion}`, + noPet: `Pet-Wolf-${swappedPotion}`, + noPetIOS: `Pet-TigerCub-${swappedPotion}`, + noPetAndroid: `Pet-Cactus-${swappedPotion}`, + }, + }; + for (const pet of Object.keys(stable.specialPets)) { + substitutions.pets[`Pet-${pet}`] = `Pet-Dragon-${swappedPotion}`; + } + for (const egg of Object.keys(eggs.drops)) { + substitutions.pets[`Pet-${egg}-`] = `Pet-${egg}-${swappedPotion}`; + } + for (const egg of Object.keys(eggs.quests)) { + substitutions.pets[`Pet-${egg}-`] = `Pet-BearCub-${swappedPotion}`; + } + return substitutions; +} diff --git a/website/common/script/content/constants/events.js b/website/common/script/content/constants/events.js index 8a3766ba10..4c44b40ecd 100644 --- a/website/common/script/content/constants/events.js +++ b/website/common/script/content/constants/events.js @@ -1,5 +1,6 @@ /* eslint-disable key-spacing */ import moment from 'moment'; +import { getMatchingSwap, makeSubstitutionMap } from './aprilFools'; // gem block: number of gems const gemsPromo = { @@ -53,7 +54,7 @@ export const REPEATING_EVENTS = { aprilFools: { start: new Date('1970-04-01T04:00-04:00'), end: new Date('1970-04-02T03:59-04:00'), - aprilFools: 'Cryptid', + spriteSubstitutions: makeSubstitutionMap(getMatchingSwap()), }, aprilFoolsResale: { start: new Date('1970-04-03T04:00-04:00'), @@ -65,6 +66,7 @@ export const REPEATING_EVENTS = { 'virtualpet', 'waffle', 'fungi', + 'alien', ], }, { diff --git a/website/common/script/content/constants/gifSprites.js b/website/common/script/content/constants/gifSprites.js index 726df62367..6e0f7f16b8 100644 --- a/website/common/script/content/constants/gifSprites.js +++ b/website/common/script/content/constants/gifSprites.js @@ -15,6 +15,7 @@ export default [ 'Mount_Body_Gryphon-Gryphatrice', 'Mount_Head_Dragon-Hydra', 'Mount_Head_Gryphon-Gryphatrice', + 'Pet_HatchingPotion_Alien', 'Pet_HatchingPotion_Cryptid', 'Pet_HatchingPotion_Dessert', 'Pet_HatchingPotion_Fungi', @@ -24,6 +25,8 @@ export default [ 'Pet-Gryphatrice-Jubilant', 'Pet-Gryphon-Gryphatrice', 'Pet-Wolf-Cerberus', + 'quest_alien', + 'quest_lostMasterclasser4', 'quest_solarSystem', 'quest_virtualpet', 'quest_windup', diff --git a/website/common/script/content/constants/releaseDates.js b/website/common/script/content/constants/releaseDates.js index 17c33c3b88..79bbf0bd31 100644 --- a/website/common/script/content/constants/releaseDates.js +++ b/website/common/script/content/constants/releaseDates.js @@ -53,4 +53,5 @@ export const HATCHING_POTIONS_RELEASE_DATES = { Cryptid: { year: 2025, month: 4, day: 3 }, Balloon: { year: 2025, month: 4, day: 21 }, Opal: { year: 2025, month: 5, day: 14 }, + Alien: { year: 2026, month: 4, day: 3 }, }; diff --git a/website/common/script/content/hatching-potions.js b/website/common/script/content/hatching-potions.js index 7572666c05..de7681d6f0 100644 --- a/website/common/script/content/hatching-potions.js +++ b/website/common/script/content/hatching-potions.js @@ -155,6 +155,10 @@ const wacky = { canBuy: hasQuestAchievementFunction('fungi'), }, Cryptid: {}, + Alien: { + questPotion: true, + canBuy: hasQuestAchievementFunction('alien'), + }, }; each(drops, (pot, key) => { diff --git a/website/common/script/content/quests/seasonal.js b/website/common/script/content/quests/seasonal.js index b4be4eb88e..b7f494d2b2 100644 --- a/website/common/script/content/quests/seasonal.js +++ b/website/common/script/content/quests/seasonal.js @@ -233,6 +233,45 @@ const QUEST_SEASONAL = { unlock: t('questFungiUnlockText'), }, }, + alien: { + text: t('questAlienText'), + notes: t('questAlienNotes'), + completion: t('questAlienCompletion'), + value: 4, + category: 'hatchingPotion', + boss: { + name: t('questAlienBoss'), + hp: 500, + str: 2, + rage: { + title: t('questAlienRageTitle'), + description: t('questAlienRageDescription'), + value: 50, + healing: 0.3, + effect: t('questAlienRageEffect'), + }, + }, + drop: { + items: [ + { + type: 'hatchingPotions', + key: 'Alien', + text: t('questAlienDropAlienPotion'), + }, { + type: 'hatchingPotions', + key: 'Alien', + text: t('questAlienDropAlienPotion'), + }, { + type: 'hatchingPotions', + key: 'Alien', + text: t('questAlienDropAlienPotion'), + }, + ], + gp: 40, + exp: 500, + unlock: t('questAlienUnlockText'), + }, + }, }; export default QUEST_SEASONAL; diff --git a/website/common/script/content/stable.js b/website/common/script/content/stable.js index 378b16e69a..e4bbc4f9ec 100644 --- a/website/common/script/content/stable.js +++ b/website/common/script/content/stable.js @@ -1,6 +1,4 @@ import each from 'lodash/each'; -import moment from 'moment'; -import { EVENTS } from './constants/events'; import allEggs from './eggs'; import allPotions from './hatching-potions'; import t from './translation'; @@ -193,7 +191,7 @@ function buildInfo () { Object.assign(petInfo['Gryphatrice-Jubilant'], { canBuy () { - return moment().isBetween(EVENTS.birthday10.start, EVENTS.birthday10.end); + return false; }, currency: 'gems', event: 'birthday10',