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',