Compare commits

...

307 Commits

Author SHA1 Message Date
Sabe Jones
760c05df5d 5.25.8 2024-06-26 12:36:32 -05:00
Phillip Thelen
26d070f2c3 improve loggly request logging call (#15259) 2024-06-26 12:30:13 -05:00
Sabe Jones
c47b287a89 5.25.7 2024-06-21 13:28:34 -05:00
Sabe Jones
3aa626d2ae fix(sprites): remove potion PNG override 2024-06-21 13:28:29 -05:00
Sabe Jones
647ee2a073 5.25.6 2024-06-21 11:56:07 -05:00
Sabe Jones
2080c3f7b8 fix(content): restore Fungi Potions and Snail Armor 2024-06-21 11:56:01 -05:00
Sabe Jones
6f65c72921 5.25.5 2024-06-21 10:14:57 -05:00
Sabe Jones
22bbdd6a28 Merge branch 'develop' into release 2024-06-21 10:14:54 -05:00
Phillip Thelen
1a3d6f6520 fix mage gear being shown as twohanded (#15254) 2024-06-21 10:14:25 -05:00
Sabe Jones
aa1e78ac94 5.25.4 2024-06-21 09:59:08 -05:00
Sabe Jones
858caa4582 fix(cShop): price display for animal pieces 2024-06-21 09:58:33 -05:00
Sabe Jones
a7e1091f3f 5.25.3 2024-06-21 09:13:32 -05:00
Sabe Jones
78fca804b7 fix(content): repair Rose Gold Potion and Beach Umbrella 2024-06-21 09:12:18 -05:00
Sabe Jones
a919ef99fe 5.25.2 2024-06-21 07:26:01 -05:00
Sabe Jones
de9ca06607 fix(gear): fill special data for seasonal body accessories 2024-06-21 07:25:49 -05:00
Sabe Jones
b5c5990e56 5.25.1 2024-06-21 07:22:23 -05:00
Sabe Jones
756af8aafb fix(gear): fill special data for seasonal eyewear 2024-06-21 07:21:55 -05:00
Sabe Jones
54f84af274 5.25.0 2024-06-21 07:00:58 -05:00
Sabe Jones
c151b6e1bc chore(subproj): update habitica-images 2024-06-21 07:00:55 -05:00
Sabe Jones
effd729222 Merge branch 'schedule-rc' into develop 2024-06-21 06:52:46 -05:00
Weblate
b7cdbc5c94 Translated using Weblate (Indonesian)
Currently translated at 95.7% (223 of 233 strings)

Translated using Weblate (Indonesian)

Currently translated at 98.3% (179 of 182 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (378 of 378 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Indonesian)

Currently translated at 76.0% (197 of 259 strings)

Translated using Weblate (German)

Currently translated at 91.8% (2850 of 3103 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Indonesian)

Currently translated at 74.5% (193 of 259 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (German)

Currently translated at 91.7% (2848 of 3103 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (3080 of 3103 strings)

Translated using Weblate (German)

Currently translated at 91.6% (2844 of 3103 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.8% (3037 of 3103 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.3% (862 of 868 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.2% (164 of 167 strings)

Translated using Weblate (German)

Currently translated at 91.5% (2840 of 3103 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (German)

Currently translated at 91.4% (2838 of 3103 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (378 of 378 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Japanese)

Currently translated at 98.8% (3067 of 3103 strings)

Translated using Weblate (German)

Currently translated at 91.3% (2836 of 3103 strings)

Translated using Weblate (Croatian)

Currently translated at 77.8% (102 of 131 strings)

Translated using Weblate (Korean)

Currently translated at 96.8% (183 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 98.4% (186 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 53.2% (124 of 233 strings)

Translated using Weblate (Korean)

Currently translated at 81.9% (77 of 94 strings)

Translated using Weblate (Croatian)

Currently translated at 67.0% (63 of 94 strings)

Translated using Weblate (Korean)

Currently translated at 51.6% (47 of 91 strings)

Translated using Weblate (Croatian)

Currently translated at 40.6% (37 of 91 strings)

Translated using Weblate (Croatian)

Currently translated at 52.1% (135 of 259 strings)

Translated using Weblate (Slovak)

Currently translated at 51.0% (119 of 233 strings)

Translated using Weblate (Danish)

Currently translated at 63.0% (147 of 233 strings)

Translated using Weblate (Bulgarian)

Currently translated at 68.6% (160 of 233 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Turkish)

Currently translated at 80.9% (106 of 131 strings)

Translated using Weblate (Serbian)

Currently translated at 75.5% (99 of 131 strings)

Translated using Weblate (Slovak)

Currently translated at 75.5% (99 of 131 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Polish)

Currently translated at 90.8% (388 of 427 strings)

Translated using Weblate (Bulgarian)

Currently translated at 55.0% (1709 of 3103 strings)

Translated using Weblate (Spanish)

Currently translated at 84.5% (159 of 188 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Swedish)

Currently translated at 40.6% (37 of 91 strings)

Translated using Weblate (Serbian)

Currently translated at 39.5% (36 of 91 strings)

Translated using Weblate (Slovak)

Currently translated at 39.5% (36 of 91 strings)

Translated using Weblate (Romanian)

Currently translated at 40.6% (37 of 91 strings)

Translated using Weblate (Portuguese)

Currently translated at 52.7% (48 of 91 strings)

Translated using Weblate (Italian)

Currently translated at 49.4% (45 of 91 strings)

Translated using Weblate (Hebrew)

Currently translated at 38.4% (35 of 91 strings)

Translated using Weblate (Czech)

Currently translated at 40.6% (37 of 91 strings)

Translated using Weblate (Bulgarian)

Currently translated at 39.5% (36 of 91 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Serbian)

Currently translated at 86.3% (95 of 110 strings)

Translated using Weblate (Italian)

Currently translated at 92.7% (805 of 868 strings)

Translated using Weblate (Swedish)

Currently translated at 53.6% (139 of 259 strings)

Translated using Weblate (Serbian)

Currently translated at 52.8% (137 of 259 strings)

Translated using Weblate (Slovak)

Currently translated at 51.7% (134 of 259 strings)

Translated using Weblate (Hebrew)

Currently translated at 49.4% (128 of 259 strings)

Co-authored-by: Alberto jor avondet <albertoavondet@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Dragan SIRET <orion.siret0.2@gmail.com>
Co-authored-by: Filip Betko <filipbetko@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: liimee <git.taaa@fedora.email>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/it/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/character/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/bg/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/he/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/it/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/death/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es/
Translate-URL: https://translate.habitica.com/projects/habitica/front/id/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/bg/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/he/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/he/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/id/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/bg/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/da/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/id/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/sk/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Loginincentives
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Quests
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2024-06-21 12:33:19 +02:00
Phillip Thelen
379f41ff04 integration fix 2024-06-19 18:20:25 +02:00
Phillip Thelen
71936c1f0a initialize stub 2024-06-19 18:10:16 +02:00
Phillip Thelen
19c79ce510 fix buy test 2024-06-19 18:04:01 +02:00
Phillip Thelen
0ba14c18b1 fix stub reference 2024-06-19 18:03:36 +02:00
Phillip Thelen
d2a0ab684a test fixes 2024-06-19 18:01:27 +02:00
Phillip Thelen
12d38fa813 fix content tests 2024-06-19 17:45:01 +02:00
Phillip Thelen
db41e00990 fix debug tests 2024-06-19 17:30:37 +02:00
Phillip Thelen
5d5275ce70 lint fixes 2024-06-19 17:30:29 +02:00
Phillip Thelen
e39c63700e remove duplicate keys 2024-06-19 17:30:16 +02:00
Phillip Thelen
550ac2db9d fix tests 2024-06-19 17:14:32 +02:00
Sabe Jones
983e01cb3f chore(sprites): update css 2024-06-17 15:02:23 -05:00
Sabe Jones
a55ede9175 fix(migration): couple of vet pet issues 2024-06-17 14:45:57 -05:00
Sabe Jones
28491cb01d fix(content): Umbrella is PER not STR 2024-06-17 10:47:22 -05:00
Sabe Jones
b49dddeb47 fix(shops): post merge cleanup 2024-06-14 18:12:22 -05:00
Sabe Jones
31e501f65a Merge branch 'release' into schedule-rc 2024-06-14 10:18:16 -05:00
Sabe Jones
050c227e6f 5.25.2 2024-06-14 08:54:38 -05:00
Weblate
ddb8725052 Merge branch 'origin/develop' into Weblate. 2024-06-14 15:53:32 +02:00
Weblate
e11b9ebe26 Translated using Weblate (Japanese)
Currently translated at 98.8% (3066 of 3103 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (German)

Currently translated at 91.3% (2834 of 3103 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3103 of 3103 strings)

Translated using Weblate (Japanese)

Currently translated at 97.2% (3018 of 3103 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (233 of 233 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Japanese)

Currently translated at 98.0% (851 of 868 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Korean)

Currently translated at 71.8% (624 of 868 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (German)

Currently translated at 91.2% (2832 of 3103 strings)

Translated using Weblate (Italian)

Currently translated at 4.2% (8 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Danish)

Currently translated at 87.2% (96 of 110 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (French)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Japanese)

Currently translated at 97.2% (3018 of 3103 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (233 of 233 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (763 of 773 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (763 of 773 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Russian)

Currently translated at 98.2% (853 of 868 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Korean)

Currently translated at 91.4% (128 of 140 strings)

Translated using Weblate (Korean)

Currently translated at 79.8% (617 of 773 strings)

Translated using Weblate (Korean)

Currently translated at 75.0% (6 of 8 strings)

Translated using Weblate (Korean)

Currently translated at 70.7% (302 of 427 strings)

Translated using Weblate (Korean)

Currently translated at 51.6% (47 of 91 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3103 of 3103 strings)

Co-authored-by: Dragan SIRET <orion.siret0.2@gmail.com>
Co-authored-by: Filip Betko <filipbetko@gmail.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: razil <boss.razmarin@gmail.com>
Co-authored-by: taesub park <dungfly75@gmail.com>
Co-authored-by: Юрий Артамонов <zilberstein2211@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/da/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/character/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/death/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/death/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/it/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ko/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Inventory
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2024-06-14 15:53:21 +02:00
Phillip Thelen
4da53f83c9 Add option to log every request start and end to loggly (#15243) 2024-06-14 08:51:10 -05:00
Sabe Jones
dde675fdc1 5.25.1 2024-06-11 13:22:43 -05:00
Weblate
d34502bba2 Merge branch 'origin/develop' into Weblate. 2024-06-11 20:21:52 +02:00
Weblate
309954eb44 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (German)

Currently translated at 98.4% (129 of 131 strings)

Translated using Weblate (German)

Currently translated at 91.2% (2830 of 3103 strings)

Translated using Weblate (German)

Currently translated at 61.5% (56 of 91 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (233 of 233 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (233 of 233 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (233 of 233 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (German)

Currently translated at 91.1% (2828 of 3103 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.5% (232 of 233 strings)

Translated using Weblate (German)

Currently translated at 91.0% (2826 of 3103 strings)

Translated using Weblate (Ukrainian)

Currently translated at 75.5% (142 of 188 strings)

Translated using Weblate (Spanish)

Currently translated at 80.8% (152 of 188 strings)

Translated using Weblate (German)

Currently translated at 91.0% (2824 of 3103 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 97.2% (3018 of 3103 strings)

Translated using Weblate (Japanese)

Currently translated at 98.0% (851 of 868 strings)

Translated using Weblate (German)

Currently translated at 90.8% (2820 of 3103 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Russian)

Currently translated at 99.2% (130 of 131 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Russian)

Currently translated at 95.7% (2972 of 3103 strings)

Translated using Weblate (Russian)

Currently translated at 95.7% (2972 of 3103 strings)

Translated using Weblate (German)

Currently translated at 90.6% (2814 of 3103 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Spanish)

Currently translated at 67.5% (127 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (German)

Currently translated at 90.6% (2813 of 3103 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Ukrainian)

Currently translated at 72.8% (137 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Ukrainian)

Currently translated at 72.8% (137 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 72.8% (137 of 188 strings)

Translated using Weblate (German)

Currently translated at 90.5% (2810 of 3103 strings)

Translated using Weblate (Japanese)

Currently translated at 95.9% (2976 of 3103 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (770 of 773 strings)

Translated using Weblate (Japanese)

Currently translated at 97.2% (844 of 868 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3103 of 3103 strings)

Translated using Weblate (German)

Currently translated at 90.4% (2808 of 3103 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (224 of 224 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Ukrainian)

Currently translated at 72.8% (137 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (French)

Currently translated at 100.0% (233 of 233 strings)

Translated using Weblate (Russian)

Currently translated at 99.2% (130 of 131 strings)

Translated using Weblate (French)

Currently translated at 99.6% (3092 of 3103 strings)

Translated using Weblate (German)

Currently translated at 90.4% (2807 of 3103 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (233 of 233 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (3103 of 3103 strings)

Translated using Weblate (German)

Currently translated at 90.3% (2805 of 3103 strings)

Translated using Weblate (German)

Currently translated at 98.9% (186 of 188 strings)

Translated using Weblate (Russian)

Currently translated at 94.5% (2934 of 3103 strings)

Translated using Weblate (Russian)

Currently translated at 32.4% (61 of 188 strings)

Translated using Weblate (Russian)

Currently translated at 96.7% (748 of 773 strings)

Translated using Weblate (Russian)

Currently translated at 94.7% (822 of 868 strings)

Translated using Weblate (Russian)

Currently translated at 94.7% (822 of 868 strings)

Translated using Weblate (German)

Currently translated at 98.4% (185 of 188 strings)

Translated using Weblate (Japanese)

Currently translated at 95.8% (2975 of 3103 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (233 of 233 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Ukrainian)

Currently translated at 72.8% (137 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (German)

Currently translated at 98.7% (230 of 233 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Russian)

Currently translated at 99.2% (130 of 131 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Russian)

Currently translated at 95.4% (273 of 286 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Russian)

Currently translated at 96.0% (410 of 427 strings)

Translated using Weblate (Russian)

Currently translated at 96.0% (410 of 427 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Russian)

Currently translated at 94.5% (2933 of 3103 strings)

Translated using Weblate (German)

Currently translated at 90.2% (2802 of 3103 strings)

Translated using Weblate (Russian)

Currently translated at 29.7% (56 of 188 strings)

Translated using Weblate (Russian)

Currently translated at 29.7% (56 of 188 strings)

Translated using Weblate (Russian)

Currently translated at 29.7% (56 of 188 strings)

Translated using Weblate (Russian)

Currently translated at 96.6% (747 of 773 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Russian)

Currently translated at 98.8% (256 of 259 strings)

Translated using Weblate (Russian)

Currently translated at 24.4% (46 of 188 strings)

Translated using Weblate (Russian)

Currently translated at 24.4% (46 of 188 strings)

Translated using Weblate (Russian)

Currently translated at 96.4% (161 of 167 strings)

Translated using Weblate (Russian)

Currently translated at 96.4% (161 of 167 strings)

Translated using Weblate (Russian)

Currently translated at 79.5% (206 of 259 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Russian)

Currently translated at 23.4% (44 of 188 strings)

Translated using Weblate (Russian)

Currently translated at 23.4% (44 of 188 strings)

Co-authored-by: Alberto jor avondet <albertoavondet@gmail.com>
Co-authored-by: Daniel Baez <lajbelms@gmail.com>
Co-authored-by: Egor Pakhomov <yaestgrut75@gmail.com>
Co-authored-by: Felix Zelinsky <jockstrap_either179@sl.felky.de>
Co-authored-by: Inferno <mishaad051@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Mencius <beautyalinap@gmail.com>
Co-authored-by: Miroslav <entferner@yandex.com>
Co-authored-by: Nikita Maximov <ruvemaximus@gmail.com>
Co-authored-by: Romane <luoma.nei72@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: nate <njarisch@outlook.com>
Co-authored-by: razil <boss.razmarin@gmail.com>
Co-authored-by: Євген Хеддок <jevhed@proton.me>
Co-authored-by: Юрий Артамонов <zilberstein2211@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/de/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/de/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/es/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/de/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ru/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Contrib
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Inventory
Translation: Habitica/Limited
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2024-06-11 20:21:37 +02:00
Phillip Thelen
98f9d2a8f4 Update to new method for fcm (#15238)
* begin moving to new fcm library

* Add error handling

* Add opening notification to correct screen

* Fix tests and make async

* lint fix

* Rename pushNotificationstest..js to pushNotifications.test.js

* fix(potions): remove Fungi Potion time banner

* 5.24.3

* update(content): add 2024-06 content prebuild (#15231)

* update sprites

* add 2024-06 content

* add 2024-06 enchanted armoire items

* update sprites

* update sprites

* fix errors found in testing

* Fix liveliness probes being rate limited (#15236)

* Do not rate limit any liveliness probes

* update example config

* Translated using Weblate (German)

Currently translated at 96.2% (181 of 188 strings)

Translated using Weblate (Japanese)

Currently translated at 99.4% (769 of 773 strings)

Translated using Weblate (German)

Currently translated at 93.6% (176 of 188 strings)

Translated using Weblate (Japanese)

Currently translated at 96.2% (2972 of 3089 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (232 of 232 strings)

Translated using Weblate (Japanese)

Currently translated at 96.8% (841 of 868 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (German)

Currently translated at 86.7% (163 of 188 strings)

Translated using Weblate (German)

Currently translated at 85.1% (160 of 188 strings)

Translated using Weblate (German)

Currently translated at 84.0% (158 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (German)

Currently translated at 83.5% (157 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (German)

Currently translated at 82.9% (156 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (German)

Currently translated at 81.9% (154 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (German)

Currently translated at 79.2% (149 of 188 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (German)

Currently translated at 90.6% (2799 of 3089 strings)

Translated using Weblate (German)

Currently translated at 77.6% (146 of 188 strings)

Translated using Weblate (German)

Currently translated at 90.5% (2797 of 3089 strings)

Translated using Weblate (German)

Currently translated at 90.4% (2794 of 3089 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (German)

Currently translated at 90.1% (2786 of 3089 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (German)

Currently translated at 77.1% (145 of 188 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.7% (763 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (German)

Currently translated at 90.0% (2782 of 3089 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (378 of 378 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (French)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (German)

Currently translated at 75.0% (141 of 188 strings)

Translated using Weblate (Spanish)

Currently translated at 99.0% (766 of 773 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Japanese)

Currently translated at 98.8% (764 of 773 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (258 of 259 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (378 of 378 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Ukrainian)

Currently translated at 62.5% (1931 of 3089 strings)

Translated using Weblate (German)

Currently translated at 89.8% (2777 of 3089 strings)

Translated using Weblate (French)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (762 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (French)

Currently translated at 82.9% (156 of 188 strings)

Translated using Weblate (German)

Currently translated at 93.0% (241 of 259 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (762 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (257 of 259 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (762 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (German)

Currently translated at 92.2% (239 of 259 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (German)

Currently translated at 91.8% (238 of 259 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (762 of 773 strings)

Translated using Weblate (German)

Currently translated at 90.3% (234 of 259 strings)

Co-authored-by: Finrod <963505255@qq.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Kem Kembo <medamamef@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/de/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/de/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/zh_Hans/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Character
Translation: Habitica/Content
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Inventory
Translation: Habitica/Limited
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks

* 5.25.0

* Fix dockerfile (#15241)

* Fix issue with l4p not resetting properly (#15240)

* actually clear out seeking field on user. Even when creating a party

* Add tests to ensure party.seeking is cleared

* fix(lint): don't assign unused const

---------

Co-authored-by: Sabe Jones <sabe@habitica.com>

---------

Co-authored-by: Sabe Jones <sabe@habitica.com>
Co-authored-by: Natalie <78037386+CuriousMagpie@users.noreply.github.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Finrod <963505255@qq.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Kem Kembo <medamamef@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Rafał Jagielski <jagielski.rafal.uwm@gmail.com>
2024-06-11 13:19:03 -05:00
Sabe Jones
5719e5e996 fix(chat): validate group membership, by @phillipthelen 2024-06-11 13:14:47 -05:00
Phillip Thelen
39add61618 Revert "lint"
This reverts commit 544d67e7e5.
2024-06-11 19:10:50 +02:00
Sabe Jones
1c1543f012 fix(layout): tighten up various margins 2024-06-11 12:04:21 -05:00
Phillip Thelen
10a27354bb REVERT ._. 2024-06-11 19:00:34 +02:00
Phillip Thelen
a00f199d18 Revert "Revert "allow hatching potions to have a release date""
This reverts commit 485584c144.

# Conflicts:
#	website/common/script/content/hatching-potions.js
2024-06-11 18:54:39 +02:00
Phillip Thelen
6c5bff7843 put armoire test back 2024-06-11 18:20:48 +02:00
Phillip Thelen
388c3d38ed Revert "Improve test coverage"
This reverts commit d4ba96796c.
2024-06-11 18:19:29 +02:00
Phillip Thelen
485584c144 Revert "allow hatching potions to have a release date"
This reverts commit ebdac0b388.
2024-06-11 18:19:21 +02:00
Phillip Thelen
b83f62bd82 Revert "allow eggs to have a release date"
This reverts commit 6d13a257dd.
2024-06-11 18:19:11 +02:00
Phillip Thelen
758b6138c2 Fix issue with l4p not resetting properly (#15240)
* actually clear out seeking field on user. Even when creating a party

* Add tests to ensure party.seeking is cleared

* fix(lint): don't assign unused const

---------

Co-authored-by: Sabe Jones <sabe@habitica.com>
2024-06-10 17:00:31 -05:00
Rafał Jagielski
37f08c4534 Fix dockerfile (#15241) 2024-06-10 16:41:26 -05:00
Phillip Thelen
544d67e7e5 lint 2024-06-10 14:53:23 +02:00
Phillip Thelen
1f0a4dad23 fix content caching 2024-06-10 14:46:39 +02:00
Phillip Thelen
eb3220c96b fix lint 2024-06-10 14:46:32 +02:00
Phillip Thelen
d4ba96796c Improve test coverage 2024-06-10 14:44:21 +02:00
Phillip Thelen
ebdac0b388 allow hatching potions to have a release date 2024-06-10 14:24:59 +02:00
Phillip Thelen
6d13a257dd allow eggs to have a release date 2024-06-10 14:11:38 +02:00
Phillip Thelen
c2b370f4d3 handle error from invalid input 2024-06-10 11:23:46 +02:00
Phillip Thelen
3313584d60 actually clear out seeking field on user. Even when creating a party 2024-06-07 08:36:08 -05:00
Phillip Thelen
b76585cce3 add additional way to get content switchover time for client 2024-06-07 11:10:48 +02:00
Sabe Jones
8d9af82521 fix(import): add missing nconf 2024-06-06 14:55:37 -05:00
Phillip Thelen
dcf25c0b4a fix tests 2024-06-06 21:51:07 +02:00
Phillip Thelen
31036ad9e4 return actually correct quests 2024-06-06 21:51:07 +02:00
Phillip Thelen
1ba85b403f clear content api cache each day 2024-06-06 21:51:07 +02:00
Phillip Thelen
c26f410cc3 add fallback for seasonal gear 2024-06-06 12:21:34 +02:00
Phillip Thelen
242e64cedc add content time offset to client 2024-06-06 10:56:56 +02:00
Sabe Jones
a44418c4fa fix(backgrounds): correct event and tt loading 2024-06-05 08:14:13 -05:00
Phillip Thelen
ea66e4e4a1 Add end dates to categories in customizationshop 2024-06-05 10:34:17 +02:00
Phillip Thelen
6889b65123 fix questshop featured items 2024-06-05 10:12:47 +02:00
Sabe Jones
fd5bc8f0b9 fix(shops): various cleanup 2024-06-04 11:50:26 -05:00
Sabe Jones
f33aff577c fix(build): skip some polyfills 2024-05-31 15:54:06 -05:00
Sabe Jones
9ba986f5e5 fix(layout): Justin positioning and shopdown 2024-05-31 15:37:00 -05:00
Sabe Jones
bf46c798a6 fix(migration): log transaction for awarded Gems 2024-05-31 11:54:27 -05:00
Phillip Thelen
54b1afd5b4 make content releases use a given offset to the time. 2024-05-29 19:05:17 +02:00
Sabe Jones
3cd966bc03 5.25.0 2024-05-29 11:39:42 -05:00
Sabe Jones
28531f3e2a Merge branch 'develop' into release 2024-05-29 11:39:34 -05:00
Weblate
7ecaf098cd Merge branch 'origin/develop' into Weblate. 2024-05-29 18:36:31 +02:00
Weblate
ef1912d571 Translated using Weblate (German)
Currently translated at 96.2% (181 of 188 strings)

Translated using Weblate (Japanese)

Currently translated at 99.4% (769 of 773 strings)

Translated using Weblate (German)

Currently translated at 93.6% (176 of 188 strings)

Translated using Weblate (Japanese)

Currently translated at 96.2% (2972 of 3089 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (232 of 232 strings)

Translated using Weblate (Japanese)

Currently translated at 96.8% (841 of 868 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (German)

Currently translated at 86.7% (163 of 188 strings)

Translated using Weblate (German)

Currently translated at 85.1% (160 of 188 strings)

Translated using Weblate (German)

Currently translated at 84.0% (158 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (German)

Currently translated at 83.5% (157 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (German)

Currently translated at 82.9% (156 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (German)

Currently translated at 81.9% (154 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (German)

Currently translated at 79.2% (149 of 188 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (German)

Currently translated at 90.6% (2799 of 3089 strings)

Translated using Weblate (German)

Currently translated at 77.6% (146 of 188 strings)

Translated using Weblate (German)

Currently translated at 90.5% (2797 of 3089 strings)

Translated using Weblate (German)

Currently translated at 90.4% (2794 of 3089 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (German)

Currently translated at 90.1% (2786 of 3089 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (German)

Currently translated at 77.1% (145 of 188 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.7% (763 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (German)

Currently translated at 90.0% (2782 of 3089 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (378 of 378 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (French)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (German)

Currently translated at 75.0% (141 of 188 strings)

Translated using Weblate (Spanish)

Currently translated at 99.0% (766 of 773 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Japanese)

Currently translated at 98.8% (764 of 773 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (258 of 259 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (378 of 378 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Ukrainian)

Currently translated at 62.5% (1931 of 3089 strings)

Translated using Weblate (German)

Currently translated at 89.8% (2777 of 3089 strings)

Translated using Weblate (French)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (762 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (French)

Currently translated at 82.9% (156 of 188 strings)

Translated using Weblate (German)

Currently translated at 93.0% (241 of 259 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (762 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (257 of 259 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (762 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (German)

Currently translated at 92.2% (239 of 259 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (German)

Currently translated at 91.8% (238 of 259 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (762 of 773 strings)

Translated using Weblate (German)

Currently translated at 90.3% (234 of 259 strings)

Co-authored-by: Finrod <963505255@qq.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Kem Kembo <medamamef@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/de/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/de/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/zh_Hans/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Character
Translation: Habitica/Content
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Inventory
Translation: Habitica/Limited
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2024-05-29 18:36:22 +02:00
Phillip Thelen
cd685c1b2b Fix liveliness probes being rate limited (#15236)
* Do not rate limit any liveliness probes

* update example config
2024-05-29 11:34:40 -05:00
Sabe Jones
b501e06f27 fix(pets): finish Veteran Cactus 2024-05-29 09:03:36 -05:00
Sabe Jones
2062c68877 WIP(shops): better Fennec and bg loading, add migration 2024-05-28 17:57:13 -05:00
Natalie
b2dde8a977 update(content): add 2024-06 content prebuild (#15231)
* update sprites

* add 2024-06 content

* add 2024-06 enchanted armoire items

* update sprites

* update sprites

* fix errors found in testing
2024-05-28 15:23:17 -05:00
Sabe Jones
19521b1894 fix(avatar): render first bg addition and Justin 2024-05-24 17:19:49 -05:00
Sabe Jones
688e7181f0 fix(customizations): responsive updates 2024-05-23 17:50:16 -05:00
Sabe Jones
a53a9be4b7 fix(css): flex gap is a thing 2024-05-22 15:53:24 -05:00
Sabe Jones
66c56225a4 fix(shops): border radius and typo 2024-05-22 13:41:49 -05:00
Sabe Jones
cabc08c04b fix(lint): remove console log 2024-05-22 09:27:35 -05:00
Sabe Jones
9ae6063f78 fix(npm): move moment-locales to main deps 2024-05-22 09:23:30 -05:00
Sabe Jones
7936677fd8 WIP(shops): empty states and deselect UX 2024-05-22 09:16:13 -05:00
Phillip Thelen
88a0b57335 add end date to seasonal gear in market 2024-05-22 15:47:02 +02:00
Phillip Thelen
2303d5de32 Fix quest premium hatching potions in market 2024-05-22 12:21:51 +02:00
Phillip Thelen
1f5de1ab42 remove sinon and moment locales to shrink client bundle 2024-05-22 11:24:31 +02:00
Phillip Thelen
435047cace strip sinon, nise and bootstrap-vue icons 2024-05-21 20:18:57 +02:00
Sabe Jones
c0d6338eba WIP(shops): safer debug mode 2024-05-21 09:11:18 -05:00
Phillip Thelen
36b589e92d make it easier to unequip backgrounds 2024-05-21 15:53:14 +02:00
Phillip Thelen
42cafbeaab make market return unlocked quest potions 2024-05-21 15:39:59 +02:00
Phillip Thelen
5b5d5a39a4 remove date 2024-05-21 14:52:01 +02:00
Sabe Jones
8d479e358d WIP(shops): more fixes 2024-05-17 20:29:14 -05:00
Sabe Jones
3bb1cceed1 chore(testing): DO NOT MERGE THIS 2024-05-17 14:14:04 -05:00
Sabe Jones
0756d36fb3 fix(admin): let panel break item 2024-05-17 14:13:48 -05:00
Sabe Jones
c709adeaec 5.24.3 2024-05-17 10:50:01 -05:00
Sabe Jones
c5dcd089a6 fix(potions): remove Fungi Potion time banner 2024-05-17 10:49:55 -05:00
Phillip Thelen
57deadaa5c Fix purchasing some hatching potions 2024-05-17 15:44:08 +02:00
Phillip Thelen
becdf640b5 fix mystery gear 2024-05-17 12:54:21 +02:00
Phillip Thelen
756e99c089 add more purchase related tests 2024-05-17 11:35:22 +02:00
Phillip Thelen
709a14fd51 add some spell related tests 2024-05-17 11:35:14 +02:00
Phillip Thelen
33181c0ac4 Add purchase related test cases 2024-05-17 10:57:33 +02:00
Phillip Thelen
ee974dfa19 fix seasonal quest purchasing 2024-05-17 10:30:15 +02:00
Phillip Thelen
b697598d75 fix showing seasonal gear in market 2024-05-17 10:03:59 +02:00
Sabe Jones
6e5b13668a WIP(shops): fixes and Hall of Heroes update 2024-05-16 18:49:56 -05:00
Phillip Thelen
4c13f3193e fix backgrounds not showing on time traveler shop 2024-05-16 20:38:58 +02:00
Phillip Thelen
15cea33c4b fix import 2024-05-16 20:29:03 +02:00
Phillip Thelen
cac0a84763 Add some tests for time travelers content 2024-05-16 20:19:26 +02:00
Phillip Thelen
c56c07a0f8 fix buying transformation items 2024-05-16 20:19:26 +02:00
Phillip Thelen
929778bdad fix code coverage 2024-05-16 20:19:26 +02:00
Phillip Thelen
d6dba9767d fix set name 2024-05-16 20:19:26 +02:00
Sabe Jones
7e7ce44c77 fix(shops): lots of layout corrections 2024-05-15 17:28:39 -05:00
Phillip Thelen
4d38880249 Refactor armoire content to be cached by day 2024-05-15 16:51:09 +02:00
Phillip Thelen
46d164ddd1 Fix seasonal npc sprite 2024-05-15 13:18:32 +02:00
Phillip Thelen
31e4c51c3f Add June Subscriber Gear 2024-05-15 12:54:53 +02:00
Phillip Thelen
eebfb81bd2 add armoire items for june 2024-05-15 12:51:01 +02:00
Phillip Thelen
06623991b3 lint 2024-05-15 12:17:18 +02:00
Phillip Thelen
fe697898ee Fix featured items 2024-05-15 12:05:30 +02:00
Phillip Thelen
f3fc14bd53 fix seasonal gear 2024-05-15 11:56:40 +02:00
Phillip Thelen
7f0b0a3909 fix seasonal gold price 2024-05-15 11:35:27 +02:00
Phillip Thelen
0f395bcc3e Fix some sets not showing 2024-05-15 11:35:19 +02:00
Phillip Thelen
d366b2cde1 remove old date strings 2024-05-15 11:25:13 +02:00
Phillip Thelen
7d8611bae2 remove availability data from strings 2024-05-15 11:13:23 +02:00
Sabe Jones
17964c0ab7 fix(build): add assert, le sigh 2024-05-14 14:34:53 -05:00
Phillip Thelen
5b6cc23fb7 event code cleanup 2024-05-14 12:41:49 +02:00
Phillip Thelen
efe8cff1ad fix seasonal gear filtering 2024-05-14 11:56:04 +02:00
Phillip Thelen
6591f6780c test fixes 2024-05-14 11:07:15 +02:00
Phillip Thelen
592c320d1d remove only 2024-05-14 11:07:05 +02:00
Phillip Thelen
df641d0866 remove express.js 2024-05-14 11:06:27 +02:00
Phillip Thelen
da4606df5e fix seasonal gear 2024-05-14 11:06:17 +02:00
Phillip Thelen
ceb6b93dc1 Add background 2024-05-13 15:24:04 +02:00
Phillip Thelen
e006f3f7de Add cactus veteran pet 2024-05-13 14:56:04 +02:00
Phillip Thelen
246cc25b6d add seasonal sets to schedule 2024-05-13 14:51:35 +02:00
Phillip Thelen
a1bb61793b add sprite definitions 2024-05-13 14:51:29 +02:00
Phillip Thelen
6523ed08cd Add Koi Hatchingpotion 2024-05-13 14:49:39 +02:00
Phillip Thelen
ab34257c03 Add Summer Splash Gear 2024-05-13 14:37:33 +02:00
Phillip Thelen
6afdffae92 fixes 2024-05-13 14:00:44 +02:00
Phillip Thelen
c3b17e3db0 Squashed commit of the following:
commit 934b85d716
Author: Sabe Jones <sabe@habitica.com>
Date:   Thu May 9 09:27:28 2024 -0500

    5.24.2

commit c6df34a7fc
Author: Sabe Jones <sabe@habitica.com>
Date:   Thu May 9 09:27:24 2024 -0500

    chore(subproj): update habitica-images

commit c51c90ba41
Author: Sabe Jones <sabe@habitica.com>
Date:   Thu May 9 09:26:49 2024 -0500

    Squashed commit of the following:

    commit 7d6320ee2d6e1dac5ac025c188162cba35ed49bf
    Author: Sabe Jones <sabe@habitica.com>
    Date:   Mon May 6 16:22:53 2024 -0500

        fix(faq): copy updates

    commit 234870a7b2bc3b23ba2a044a1010fdc9b417bc45
    Author: Sabe Jones <sabe@habitica.com>
    Date:   Fri May 3 16:06:48 2024 -0500

        fix(faq): cleaner layout

    commit 06f162cc7a6a2b94b916ae0514b08ede09e7a2dc
    Author: Sabe Jones <sabe@habitica.com>
    Date:   Tue Apr 30 17:21:50 2024 -0500

        feat(faq): Content Schedule notes

commit d3f420144c
Author: Weblate <noreply@weblate.org>
Date:   Thu May 9 16:22:33 2024 +0200

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3089 of 3089 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (15 of 15 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.5% (762 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (378 of 378 strings)

    Translated using Weblate (German)

    Currently translated at 82.6% (214 of 259 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.5% (762 of 773 strings)

    Translated using Weblate (German)

    Currently translated at 97.4% (753 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.5% (762 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (131 of 131 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (2 of 2 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3089 of 3089 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (868 of 868 strings)

    Translated using Weblate (German)

    Currently translated at 75.2% (195 of 259 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (113 of 113 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (239 of 239 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.5% (762 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (189 of 189 strings)

    Translated using Weblate (German)

    Currently translated at 97.1% (751 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (140 of 140 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (113 of 113 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (427 of 427 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (239 of 239 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.5% (762 of 773 strings)

    Translated using Weblate (German)

    Currently translated at 96.2% (744 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (110 of 110 strings)

    Translated using Weblate (German)

    Currently translated at 99.8% (867 of 868 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (German)

    Currently translated at 98.7% (229 of 232 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (378 of 378 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (140 of 140 strings)

    Translated using Weblate (German)

    Currently translated at 89.5% (2766 of 3089 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.5% (762 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (868 of 868 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (German)

    Currently translated at 89.3% (2760 of 3089 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (3089 of 3089 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (232 of 232 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (286 of 286 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3089 of 3089 strings)

    Translated using Weblate (French)

    Currently translated at 99.9% (3088 of 3089 strings)

    Translated using Weblate (German)

    Currently translated at 89.1% (2754 of 3089 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (182 of 182 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.4% (761 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (868 of 868 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (868 of 868 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (232 of 232 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3089 of 3089 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.4% (761 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (378 of 378 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.3% (862 of 868 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (427 of 427 strings)

    Translated using Weblate (German)

    Currently translated at 89.0% (2752 of 3089 strings)

    Translated using Weblate (German)

    Currently translated at 89.0% (2750 of 3089 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (232 of 232 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (3089 of 3089 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (773 of 773 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (773 of 773 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 99.7% (377 of 378 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (378 of 378 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (378 of 378 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (868 of 868 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (German)

    Currently translated at 88.9% (2747 of 3089 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (German)

    Currently translated at 88.7% (2740 of 3089 strings)

    Translated using Weblate (German)

    Currently translated at 88.8% (2734 of 3077 strings)

    Translated using Weblate (Korean)

    Currently translated at 79.8% (131 of 164 strings)

    Translated using Weblate (Korean)

    Currently translated at 79.8% (131 of 164 strings)

    Co-authored-by: Finrod <963505255@qq.com>
    Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
    Co-authored-by: Lapin <sirocuro01@gmail.com>
    Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
    Co-authored-by: Tetiana <merekka13@gmail.com>
    Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
    Co-authored-by: Weblate <noreply@weblate.org>
    Co-authored-by: 박동훈 <creator98@naver.com>
    Translate-URL: https://translate.habitica.com/projects/habitica/achievements/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fr/
    Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ko/
    Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
    Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
    Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
    Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/challenge/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/content/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/content/es/
    Translate-URL: https://translate.habitica.com/projects/habitica/content/fr/
    Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
    Translate-URL: https://translate.habitica.com/projects/habitica/content/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/death/uk/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/groups/uk/
    Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/noscript/uk/
    Translate-URL: https://translate.habitica.com/projects/habitica/npc/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/pets/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/es/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/fr/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/tasks/zh_Hans/
    Translation: Habitica/Achievements
    Translation: Habitica/Backgrounds
    Translation: Habitica/Challenge
    Translation: Habitica/Character
    Translation: Habitica/Content
    Translation: Habitica/Death
    Translation: Habitica/Front
    Translation: Habitica/Gear
    Translation: Habitica/Generic
    Translation: Habitica/Groups
    Translation: Habitica/Limited
    Translation: Habitica/Noscript
    Translation: Habitica/Npc
    Translation: Habitica/Pets
    Translation: Habitica/Questscontent
    Translation: Habitica/Settings
    Translation: Habitica/Subscriber
    Translation: Habitica/Tasks

commit 1567f1c283
Author: Natalie <78037386+CuriousMagpie@users.noreply.github.com>
Date:   Tue May 7 17:21:06 2024 -0400

    remove dempendabot.yml (#15193)

commit 3e19b8aa96
Author: Sabe Jones <sabe@habitica.com>
Date:   Fri Apr 26 15:26:23 2024 -0500

    5.24.1

commit d1bc1ab05a
Merge: 2d4ee636ae 13149d4acf
Author: Sabe Jones <sabe@habitica.com>
Date:   Fri Apr 26 15:25:51 2024 -0500

    Merge branch 'develop' into release

commit 13149d4acf
Merge: 42964c91f3 4b796fae5d
Author: Weblate <noreply@weblate.org>
Date:   Fri Apr 26 22:24:45 2024 +0200

    Merge branch 'origin/develop' into Weblate.

commit 2d4ee636ae
Author: Sabe Jones <sabe@habitica.com>
Date:   Fri Apr 26 15:22:08 2024 -0500

    5.24.0

commit 42964c91f3
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Apr 26 22:15:18 2024 +0200

    Fix issue with gift sub processing (#15184)

    * Fix issue with gift sub processing

    * Update cron.js

commit de62207504
Author: Natalie <78037386+CuriousMagpie@users.noreply.github.com>
Date:   Fri Apr 26 16:14:45 2024 -0400

    May 2024 Content Prebuild (#15185)

    * 2024-05 css update

    * add May subscriber items, enchanted armoire (text placeholders), potions, and quest bundles

    * typo correction

    * add May achievement

    * content fixes after local testing

    * canonical date fix

    * fix potion descriptions, add periods to background descriptions

    * fix canonical date

    * updated armoire items

    * fix stat display on item

    * Fixing merge conflicts

    * resolve merge conflicts

    * add leading zero to mp drain for mushroom quest

    * fix timezones

    * proofreading pass

    * fix linting errors

    * date fixes & linter fixes

    * correct armoire expression at end of file

    * fix(autolint): roll back Prettier change

    ---------

    Co-authored-by: Sabe Jones <sabe@habitica.com>

commit 4b796fae5d
Author: Weblate <noreply@weblate.org>
Date:   Fri Apr 26 11:41:11 2024 +0200

    Translated using Weblate (German)

    Currently translated at 88.7% (2731 of 3077 strings)

    Translated using Weblate (German)

    Currently translated at 88.6% (2729 of 3077 strings)

    Translated using Weblate (German)

    Currently translated at 88.6% (2727 of 3077 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (286 of 286 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (110 of 110 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 99.8% (860 of 861 strings)

    Translated using Weblate (German)

    Currently translated at 88.5% (2726 of 3077 strings)

    Translated using Weblate (Portuguese (Brazil))

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Portuguese (Brazil))

    Currently translated at 100.0% (861 of 861 strings)

    Translated using Weblate (German)

    Currently translated at 88.5% (2724 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (861 of 861 strings)

    Translated using Weblate (German)

    Currently translated at 98.9% (283 of 286 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 97.5% (160 of 164 strings)

    Translated using Weblate (German)

    Currently translated at 89.8% (257 of 286 strings)

    Translated using Weblate (German)

    Currently translated at 87.7% (251 of 286 strings)

    Translated using Weblate (German)

    Currently translated at 99.8% (860 of 861 strings)

    Translated using Weblate (German)

    Currently translated at 97.6% (841 of 861 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 98.8% (256 of 259 strings)

    Translated using Weblate (German)

    Currently translated at 91.8% (392 of 427 strings)

    Translated using Weblate (German)

    Currently translated at 91.1% (389 of 427 strings)

    Translated using Weblate (German)

    Currently translated at 88.7% (379 of 427 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (3077 of 3077 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (231 of 231 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (286 of 286 strings)

    Translated using Weblate (French)

    Currently translated at 99.1% (3050 of 3077 strings)

    Translated using Weblate (German)

    Currently translated at 88.0% (376 of 427 strings)

    Translated using Weblate (German)

    Currently translated at 98.7% (228 of 231 strings)

    Translated using Weblate (Dutch)

    Currently translated at 100.0% (113 of 113 strings)

    Translated using Weblate (Dutch)

    Currently translated at 84.5% (2602 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (164 of 164 strings)

    Translated using Weblate (Romanian)

    Currently translated at 92.8% (130 of 140 strings)

    Translated using Weblate (German)

    Currently translated at 97.8% (226 of 231 strings)

    Translated using Weblate (Romanian)

    Currently translated at 75.0% (6 of 8 strings)

    Translated using Weblate (Romanian)

    Currently translated at 96.6% (58 of 60 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (861 of 861 strings)

    Translated using Weblate (German)

    Currently translated at 95.2% (220 of 231 strings)

    Translated using Weblate (French)

    Currently translated at 98.7% (3040 of 3077 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (861 of 861 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (94 of 94 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (13 of 13 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (286 of 286 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.6% (761 of 764 strings)

    Translated using Weblate (German)

    Currently translated at 54.9% (50 of 91 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (286 of 286 strings)

    Translated using Weblate (Portuguese (Brazil))

    Currently translated at 98.7% (3037 of 3077 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (3077 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (239 of 239 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.6% (761 of 764 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (189 of 189 strings)

    Translated using Weblate (Portuguese (Brazil))

    Currently translated at 100.0% (861 of 861 strings)

    Translated using Weblate (Portuguese)

    Currently translated at 99.3% (163 of 164 strings)

    Translated using Weblate (German)

    Currently translated at 94.8% (219 of 231 strings)

    Translated using Weblate (German)

    Currently translated at 84.2% (241 of 286 strings)

    Translated using Weblate (German)

    Currently translated at 51.6% (47 of 91 strings)

    Translated using Weblate (Portuguese)

    Currently translated at 98.1% (161 of 164 strings)

    Translated using Weblate (Spanish)

    Currently translated at 99.8% (3072 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (94 of 94 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (286 of 286 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (239 of 239 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.6% (761 of 764 strings)

    Translated using Weblate (German)

    Currently translated at 49.4% (45 of 91 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Spanish (Latin America))

    Currently translated at 89.7% (253 of 282 strings)

    Translated using Weblate (Spanish (Latin America))

    Currently translated at 2.1% (3 of 137 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3077 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (239 of 239 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.6% (761 of 764 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (427 of 427 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.6% (761 of 764 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (231 of 231 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (56 of 56 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3077 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.9% (756 of 764 strings)

    Translated using Weblate (German)

    Currently translated at 48.3% (44 of 91 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (861 of 861 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.0% (749 of 764 strings)

    Translated using Weblate (German)

    Currently translated at 97.3% (744 of 764 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (164 of 164 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (231 of 231 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (113 of 113 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (131 of 131 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (22 of 22 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (8 of 8 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3077 of 3077 strings)

    Translated using Weblate (Spanish)

    Currently translated at 99.8% (3071 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.1% (3051 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (861 of 861 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (140 of 140 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (230 of 230 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (94 of 94 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (131 of 131 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (427 of 427 strings)

    Translated using Weblate (German)

    Currently translated at 86.6% (370 of 427 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3035 of 3035 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (3035 of 3035 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (239 of 239 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (182 of 182 strings)

    Translated using Weblate (Russian)

    Currently translated at 29.9% (41 of 137 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.0% (749 of 764 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (764 of 764 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (91 of 91 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (110 of 110 strings)

    Translated using Weblate (German)

    Currently translated at 97.8% (836 of 854 strings)

    Co-authored-by: Antonio Spinelli <tonicospinelli@users.noreply.translate.habitica.com>
    Co-authored-by: Céu <marcel.ufscar@gmail.com>
    Co-authored-by: Ellen A M <ellen_a_m@hotmail.com>
    Co-authored-by: Finrod <963505255@qq.com>
    Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
    Co-authored-by: Julian Brito <hackoogamer0852@gmail.com>
    Co-authored-by: Luã Fhelyp Guimarães <fhelypg@gmail.com>
    Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
    Co-authored-by: Tetiana <merekka13@gmail.com>
    Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
    Co-authored-by: Weblate <noreply@weblate.org>
    Co-authored-by: Χρήστος Joia <hristosjoia@gmail.com>
    Co-authored-by: Катя Скибицкая <katerrina9993@gmail.com>
    Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt/
    Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
    Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
    Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
    Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
    Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
    Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/challenge/uk/
    Translate-URL: https://translate.habitica.com/projects/habitica/challenge/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
    Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
    Translate-URL: https://translate.habitica.com/projects/habitica/content/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/faq/es_419/
    Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
    Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/nl/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
    Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/groups/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/inventory/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/limited/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
    Translate-URL: https://translate.habitica.com/projects/habitica/limited/es_419/
    Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
    Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
    Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/messages/ro/
    Translate-URL: https://translate.habitica.com/projects/habitica/npc/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/overview/ro/
    Translate-URL: https://translate.habitica.com/projects/habitica/pets/nl/
    Translate-URL: https://translate.habitica.com/projects/habitica/pets/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/quests/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
    Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/
    Translate-URL: https://translate.habitica.com/projects/habitica/spells/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
    Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
    Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ro/
    Translate-URL: https://translate.habitica.com/projects/habitica/tasks/zh_Hans/
    Translation: Habitica/Achievements
    Translation: Habitica/Backgrounds
    Translation: Habitica/Challenge
    Translation: Habitica/Character
    Translation: Habitica/Communityguidelines
    Translation: Habitica/Content
    Translation: Habitica/Faq
    Translation: Habitica/Front
    Translation: Habitica/Gear
    Translation: Habitica/Generic
    Translation: Habitica/Groups
    Translation: Habitica/Inventory
    Translation: Habitica/Limited
    Translation: Habitica/Loginincentives
    Translation: Habitica/Messages
    Translation: Habitica/Npc
    Translation: Habitica/Overview
    Translation: Habitica/Pets
    Translation: Habitica/Quests
    Translation: Habitica/Questscontent
    Translation: Habitica/Rebirth
    Translation: Habitica/Settings
    Translation: Habitica/Spells
    Translation: Habitica/Subscriber
    Translation: Habitica/Tasks

commit 2e9573ef92
Author: Yeah Jack <95103974+Yeah-Jack@users.noreply.github.com>
Date:   Thu Apr 25 21:25:33 2024 +0200

    Update README.md for better grammar (#15103)

commit 384bfce3eb
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Thu Apr 25 14:34:51 2024 -0400

    chore(deps): bump express from 4.18.2 to 4.19.2 in /website/client (#15189)

    Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
    - [Release notes](https://github.com/expressjs/express/releases)
    - [Changelog](https://github.com/expressjs/express/blob/master/History.md)
    - [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

    ---
    updated-dependencies:
    - dependency-name: express
      dependency-type: indirect
    ...

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 5a8c7fb924
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Thu Apr 25 14:34:24 2024 -0400

    chore(deps): bump webpack-dev-middleware in /website/client (#15188)

    Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4.
    - [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
    - [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
    - [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4)

    ---
    updated-dependencies:
    - dependency-name: webpack-dev-middleware
      dependency-type: indirect
    ...

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 246775256e
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Thu Apr 25 14:34:01 2024 -0400

    chore(deps): bump express from 4.18.2 to 4.19.2 (#15190)

    Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
    - [Release notes](https://github.com/expressjs/express/releases)
    - [Changelog](https://github.com/expressjs/express/blob/master/History.md)
    - [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

    ---
    updated-dependencies:
    - dependency-name: express
      dependency-type: direct:production
    ...

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit fa4cd8dd5a
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Thu Apr 25 14:32:59 2024 -0400

    chore(deps): bump tar from 6.2.0 to 6.2.1 (#15191)

    Bumps [tar](https://github.com/isaacs/node-tar) from 6.2.0 to 6.2.1.
    - [Release notes](https://github.com/isaacs/node-tar/releases)
    - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
    - [Commits](https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1)

    ---
    updated-dependencies:
    - dependency-name: tar
      dependency-type: indirect
    ...

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 5224e063f7
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Thu Apr 25 14:32:36 2024 -0400

    chore(deps): bump axios from 0.21.4 to 1.6.8 (#15192)

    Bumps [axios](https://github.com/axios/axios) from 0.21.4 to 1.6.8.
    - [Release notes](https://github.com/axios/axios/releases)
    - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
    - [Commits](https://github.com/axios/axios/compare/v0.21.4...v1.6.8)

    ---
    updated-dependencies:
    - dependency-name: axios
      dependency-type: indirect
    ...

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit e5e8b9a7ec
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Thu Apr 25 14:19:21 2024 -0400

    build(deps): bump chai from 4.3.7 to 5.1.0 in /website/client (#15144)

    Bumps [chai](https://github.com/chaijs/chai) from 4.3.7 to 5.1.0.
    - [Release notes](https://github.com/chaijs/chai/releases)
    - [Changelog](https://github.com/chaijs/chai/blob/main/History.md)
    - [Commits](https://github.com/chaijs/chai/compare/v4.3.7...v5.1.0)

    ---
    updated-dependencies:
    - dependency-name: chai
      dependency-type: direct:production
      update-type: version-update:semver-major
    ...

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 7cd76c50eb
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Thu Apr 25 14:18:07 2024 -0400

    build(deps): bump axios from 0.27.2 to 0.28.0 in /website/client (#15148)

    Bumps [axios](https://github.com/axios/axios) from 0.27.2 to 0.28.0.
    - [Release notes](https://github.com/axios/axios/releases)
    - [Changelog](https://github.com/axios/axios/blob/v0.28.0/CHANGELOG.md)
    - [Commits](https://github.com/axios/axios/compare/v0.27.2...v0.28.0)

    ---
    updated-dependencies:
    - dependency-name: axios
      dependency-type: direct:production
      update-type: version-update:semver-minor
    ...

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit b520202544
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Thu Apr 25 14:17:17 2024 -0400

    build(deps): bump sass-loader from 8.0.2 to 14.1.1 in /website/client (#15159)

    Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 8.0.2 to 14.1.1.
    - [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
    - [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/webpack-contrib/sass-loader/compare/v8.0.2...v14.1.1)

    ---
    updated-dependencies:
    - dependency-name: sass-loader
      dependency-type: direct:production
      update-type: version-update:semver-major
    ...

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit bbae882eda
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Thu Apr 25 14:12:46 2024 -0400

    chore(deps): bump follow-redirects in /website/client (#15179)

    Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.6.
    - [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
    - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.6)

    ---
    updated-dependencies:
    - dependency-name: follow-redirects
      dependency-type: indirect
    ...

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit ee93c8bec5
Author: Sabe Jones <sabe@habitica.com>
Date:   Mon Apr 8 15:49:20 2024 -0500

    5.23.0

commit c65e93e514
Author: Sabe Jones <sabe@habitica.com>
Date:   Mon Apr 8 15:49:17 2024 -0500

    chore(git): update subproject

commit 0fd808727c
Author: Sabe Jones <sabe@habitica.com>
Date:   Mon Apr 8 15:48:32 2024 -0500

    Squashed commit of the following:

    commit 3c3787091a2e8a94857352c3655f60138a3b20b7
    Merge: 76a00d6308 76d7f02fe8
    Author: Sabe Jones <sabe@habitica.com>
    Date:   Mon Apr 8 15:47:35 2024 -0500

        Merge branch 'release' into 2024-04-april-fool-items

    commit 76a00d6308997c50ae5f5e9d6170a09a1a8cbac7
    Author: Sabe Jones <sabe@habitica.com>
    Date:   Thu Mar 28 16:08:39 2024 -0500

        fix(quest): revise rage text

    commit c2e13f8af245993f61eb614d6be609c833c2e711
    Author: Sabe Jones <sabe@habitica.com>
    Date:   Thu Mar 28 12:57:17 2024 -0500

        fix(quest): correct rage and "guscompletion"

    commit ecdeb82df998ab75eceae3e877c1758187b1d22c
    Author: Sabe Jones <sabe@habitica.com>
    Date:   Thu Mar 28 08:55:18 2024 -0500

        fix(events): correct dates and times

    commit 232de436bbe7ba4c13f04ed66cc3b8a4a794d884
    Author: Sabe Jones <sabe@habitica.com>
    Date:   Wed Mar 27 09:03:06 2024 -0500

        fix(content): a few more unruly Fungus

    commit 955e6e73387788797d1a83a037f73aa472515cec
    Author: Sabe Jones <sabe@habitica.com>
    Date:   Wed Mar 27 09:00:03 2024 -0500

        feat(content): wacky potions 2024 by @CuriousMagpie

    commit 34b72be4f447f00c31169c4cd6e8ba79655adf8a
    Author: Sabe Jones <sabe@habitica.com>
    Date:   Wed Mar 27 08:56:27 2024 -0500

        feat(event): AF by @CuriousMagpie
2024-05-13 12:29:40 +02:00
Sabe Jones
b9e128b387 feat(customizations): more icons 2024-05-10 15:37:07 -05:00
Sabe Jones
cac14ab2cc Merge branch 'sabrecat/subs-private' into subs-private 2024-05-10 08:55:16 -05:00
Phillip Thelen
c64a6eb66e remove log 2024-05-10 13:11:05 +02:00
Phillip Thelen
cd33a539cf fix error in time travelers shop 2024-05-10 13:09:30 +02:00
Sabe Jones
934b85d716 5.24.2 2024-05-09 09:27:28 -05:00
Sabe Jones
c6df34a7fc chore(subproj): update habitica-images 2024-05-09 09:27:24 -05:00
Sabe Jones
c51c90ba41 Squashed commit of the following:
commit 7d6320ee2d6e1dac5ac025c188162cba35ed49bf
Author: Sabe Jones <sabe@habitica.com>
Date:   Mon May 6 16:22:53 2024 -0500

    fix(faq): copy updates

commit 234870a7b2bc3b23ba2a044a1010fdc9b417bc45
Author: Sabe Jones <sabe@habitica.com>
Date:   Fri May 3 16:06:48 2024 -0500

    fix(faq): cleaner layout

commit 06f162cc7a6a2b94b916ae0514b08ede09e7a2dc
Author: Sabe Jones <sabe@habitica.com>
Date:   Tue Apr 30 17:21:50 2024 -0500

    feat(faq): Content Schedule notes
2024-05-09 09:26:49 -05:00
Weblate
d3f420144c Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (3089 of 3089 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (762 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (378 of 378 strings)

Translated using Weblate (German)

Currently translated at 82.6% (214 of 259 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (762 of 773 strings)

Translated using Weblate (German)

Currently translated at 97.4% (753 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (762 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (3089 of 3089 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (German)

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (German)

Currently translated at 75.2% (195 of 259 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (762 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (German)

Currently translated at 97.1% (751 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (762 of 773 strings)

Translated using Weblate (German)

Currently translated at 96.2% (744 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (German)

Currently translated at 99.8% (867 of 868 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (German)

Currently translated at 98.7% (229 of 232 strings)

Translated using Weblate (German)

Currently translated at 100.0% (378 of 378 strings)

Translated using Weblate (German)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (German)

Currently translated at 89.5% (2766 of 3089 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (762 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (German)

Currently translated at 89.3% (2760 of 3089 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3089 of 3089 strings)

Translated using Weblate (French)

Currently translated at 100.0% (232 of 232 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (3089 of 3089 strings)

Translated using Weblate (French)

Currently translated at 99.9% (3088 of 3089 strings)

Translated using Weblate (German)

Currently translated at 89.1% (2754 of 3089 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.4% (761 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (French)

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (232 of 232 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (3089 of 3089 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.4% (761 of 773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (378 of 378 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.3% (862 of 868 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (German)

Currently translated at 89.0% (2752 of 3089 strings)

Translated using Weblate (German)

Currently translated at 89.0% (2750 of 3089 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (232 of 232 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (3089 of 3089 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (French)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.7% (377 of 378 strings)

Translated using Weblate (French)

Currently translated at 100.0% (378 of 378 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (378 of 378 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (French)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (German)

Currently translated at 88.9% (2747 of 3089 strings)

Translated using Weblate (French)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (German)

Currently translated at 88.7% (2740 of 3089 strings)

Translated using Weblate (German)

Currently translated at 88.8% (2734 of 3077 strings)

Translated using Weblate (Korean)

Currently translated at 79.8% (131 of 164 strings)

Translated using Weblate (Korean)

Currently translated at 79.8% (131 of 164 strings)

Co-authored-by: Finrod <963505255@qq.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Lapin <sirocuro01@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 박동훈 <creator98@naver.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/de/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/content/de/
Translate-URL: https://translate.habitica.com/projects/habitica/content/es/
Translate-URL: https://translate.habitica.com/projects/habitica/content/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/death/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/noscript/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/de/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/es/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/zh_Hans/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Content
Translation: Habitica/Death
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Noscript
Translation: Habitica/Npc
Translation: Habitica/Pets
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2024-05-09 16:22:33 +02:00
Phillip Thelen
ec76757f93 remove log 2024-05-09 14:38:16 +02:00
Phillip Thelen
03adff80f7 fix seasonal gear not showing as available 2024-05-09 13:55:39 +02:00
Phillip Thelen
a73abcca74 don’t return event data anymore for items 2024-05-09 13:55:24 +02:00
Phillip Thelen
a8c8fffa7c make sure schedule end dates are set as utc 2024-05-09 13:54:59 +02:00
Phillip Thelen
f835cf2761 fix world state 2024-05-09 12:02:23 +02:00
Phillip Thelen
96fed21fbd add gear field to gala events 2024-05-09 10:45:37 +02:00
Phillip Thelen
7d62c87de3 optimize world state api call 2024-05-09 10:45:37 +02:00
Sabe Jones
b28251dc9e WIP(shops): more layouts and fixes 2024-05-08 20:27:27 -05:00
Phillip Thelen
b713e10c14 lint fixes 2024-05-08 17:48:34 +02:00
Phillip Thelen
fce5371fce fix typos 2024-05-08 17:46:40 +02:00
Phillip Thelen
a9cefd284a support april fools resale in schedule 2024-05-08 17:42:40 +02:00
Phillip Thelen
6ed422cd28 Improve repeating events handling 2024-05-08 17:42:24 +02:00
Phillip Thelen
02914685dc improve armoire release test cases 2024-05-08 17:05:09 +02:00
Phillip Thelen
856ed24dcb Add tests to check for correct food content populating 2024-05-08 16:46:42 +02:00
Phillip Thelen
4a9ec734c1 validate that schedule keys refer to existing content 2024-05-08 16:06:51 +02:00
Sabe Jones
61d151d2bb chore(testing): enable non prod analytics 2024-05-07 20:15:16 -05:00
Sabe Jones
32cb201b81 WIP(schedule): add June 2024 content 2024-05-07 19:36:39 -05:00
Sabe Jones
44a7006295 fix(backgrounds): correct wrapping 2024-05-07 17:19:14 -05:00
Natalie
1567f1c283 remove dempendabot.yml (#15193) 2024-05-07 16:21:06 -05:00
Sabe Jones
21a0bf7d65 WIP(shops): update avatar modal style 2024-05-07 09:06:23 -05:00
Phillip Thelen
0089506165 Fix setting end date 2024-05-07 15:17:22 +02:00
Phillip Thelen
fbce5aae32 fix buying animal gear 2024-05-07 14:50:12 +02:00
Sabe Jones
a8726eee0b fix(faq): copy updates 2024-05-06 16:23:33 -05:00
Sabe Jones
9efe370d33 fix(faq): cleaner layout 2024-05-03 16:07:35 -05:00
Phillip Thelen
47df62e716 remove logs 2024-05-03 11:14:49 +02:00
Phillip Thelen
dac792dd27 add end date to more items/categories 2024-05-03 11:14:23 +02:00
Phillip Thelen
eacf6de19a fix warning 2024-05-03 11:13:52 +02:00
Sabe Jones
c1ca4e84b8 fix(layouts): May 2 updates 2024-05-03 00:14:33 -05:00
Phillip Thelen
87fc01cb81 improve armoire release process 2024-05-02 14:37:14 +02:00
Phillip Thelen
71f21c643c remove only 2024-05-02 14:37:14 +02:00
Phillip Thelen
1ced4a18d6 add handling and tests for new timetravelers schedule 2024-05-02 14:37:14 +02:00
Phillip Thelen
9dabe79d5e Add handling and tests for new background schedule 2024-05-02 14:37:14 +02:00
Phillip Thelen
4c2cdfe5b8 fix customization shop display on mobile 2024-05-02 14:37:14 +02:00
Sabe Jones
f05888b116 fix(time-travelers): add countdowns 2024-04-30 17:26:22 -05:00
Sabe Jones
ae8607c0c3 feat(faq): Content Schedule notes 2024-04-30 17:22:59 -05:00
Sabe Jones
3e19b8aa96 5.24.1 2024-04-26 15:26:23 -05:00
Sabe Jones
d1bc1ab05a Merge branch 'develop' into release 2024-04-26 15:25:51 -05:00
Weblate
13149d4acf Merge branch 'origin/develop' into Weblate. 2024-04-26 22:24:45 +02:00
Sabe Jones
2d4ee636ae 5.24.0 2024-04-26 15:22:08 -05:00
Phillip Thelen
42964c91f3 Fix issue with gift sub processing (#15184)
* Fix issue with gift sub processing

* Update cron.js
2024-04-26 15:15:18 -05:00
Natalie
de62207504 May 2024 Content Prebuild (#15185)
* 2024-05 css update

* add May subscriber items, enchanted armoire (text placeholders), potions, and quest bundles

* typo correction

* add May achievement

* content fixes after local testing

* canonical date fix

* fix potion descriptions, add periods to background descriptions

* fix canonical date

* updated armoire items

* fix stat display on item

* Fixing merge conflicts

* resolve merge conflicts

* add leading zero to mp drain for mushroom quest

* fix timezones

* proofreading pass

* fix linting errors

* date fixes & linter fixes

* correct armoire expression at end of file

* fix(autolint): roll back Prettier change

---------

Co-authored-by: Sabe Jones <sabe@habitica.com>
2024-04-26 15:14:45 -05:00
Phillip Thelen
d6cabeedb4 fix tests 2024-04-26 13:44:51 +02:00
Phillip Thelen
99a7b90247 Fix linting issues 2024-04-26 13:15:30 +02:00
Phillip Thelen
fbdaa50fcf handle schedule end date for galas 2024-04-26 13:03:34 +02:00
Phillip Thelen
30e81297da Add comments to clarify schedule 2024-04-26 12:18:26 +02:00
Phillip Thelen
b373eaff39 Fix merge issue 2024-04-26 12:09:04 +02:00
Phillip Thelen
80a212683d Fix assigning end date to content schedule items 2024-04-26 12:09:04 +02:00
Sabe Jones
b0a4ed30d4 WIP(shops): dates and fixes 2024-04-26 12:08:30 +02:00
Phillip Thelen
c2cb37ffe6 Cleanup pinned items that are no longer for purchase 2024-04-26 12:08:11 +02:00
Sabe Jones
558894fafd WIP(shops): cShop reconciled to schedule backend 2024-04-26 12:08:11 +02:00
Phillip Thelen
4bbdf27f48 add option to reset TT 2024-04-26 12:05:30 +02:00
Phillip Thelen
daa296f2af fix some quests scheduling 2024-04-26 12:05:30 +02:00
Phillip Thelen
4c51212315 Cleanup pinned items that are no longer for purchase 2024-04-26 12:05:27 +02:00
Weblate
4b796fae5d Translated using Weblate (German)
Currently translated at 88.7% (2731 of 3077 strings)

Translated using Weblate (German)

Currently translated at 88.6% (2729 of 3077 strings)

Translated using Weblate (German)

Currently translated at 88.6% (2727 of 3077 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (860 of 861 strings)

Translated using Weblate (German)

Currently translated at 88.5% (2726 of 3077 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (377 of 377 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (861 of 861 strings)

Translated using Weblate (German)

Currently translated at 88.5% (2724 of 3077 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (861 of 861 strings)

Translated using Weblate (German)

Currently translated at 98.9% (283 of 286 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (377 of 377 strings)

Translated using Weblate (Ukrainian)

Currently translated at 97.5% (160 of 164 strings)

Translated using Weblate (German)

Currently translated at 89.8% (257 of 286 strings)

Translated using Weblate (German)

Currently translated at 87.7% (251 of 286 strings)

Translated using Weblate (German)

Currently translated at 99.8% (860 of 861 strings)

Translated using Weblate (German)

Currently translated at 97.6% (841 of 861 strings)

Translated using Weblate (Ukrainian)

Currently translated at 98.8% (256 of 259 strings)

Translated using Weblate (German)

Currently translated at 91.8% (392 of 427 strings)

Translated using Weblate (German)

Currently translated at 91.1% (389 of 427 strings)

Translated using Weblate (German)

Currently translated at 88.7% (379 of 427 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3077 of 3077 strings)

Translated using Weblate (French)

Currently translated at 100.0% (231 of 231 strings)

Translated using Weblate (French)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (French)

Currently translated at 99.1% (3050 of 3077 strings)

Translated using Weblate (German)

Currently translated at 88.0% (376 of 427 strings)

Translated using Weblate (German)

Currently translated at 98.7% (228 of 231 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Dutch)

Currently translated at 84.5% (2602 of 3077 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (164 of 164 strings)

Translated using Weblate (Romanian)

Currently translated at 92.8% (130 of 140 strings)

Translated using Weblate (German)

Currently translated at 97.8% (226 of 231 strings)

Translated using Weblate (Romanian)

Currently translated at 75.0% (6 of 8 strings)

Translated using Weblate (Romanian)

Currently translated at 96.6% (58 of 60 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (861 of 861 strings)

Translated using Weblate (German)

Currently translated at 95.2% (220 of 231 strings)

Translated using Weblate (French)

Currently translated at 98.7% (3040 of 3077 strings)

Translated using Weblate (French)

Currently translated at 100.0% (861 of 861 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (761 of 764 strings)

Translated using Weblate (German)

Currently translated at 54.9% (50 of 91 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.7% (3037 of 3077 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (3077 of 3077 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (761 of 764 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (861 of 861 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.3% (163 of 164 strings)

Translated using Weblate (German)

Currently translated at 94.8% (219 of 231 strings)

Translated using Weblate (German)

Currently translated at 84.2% (241 of 286 strings)

Translated using Weblate (German)

Currently translated at 51.6% (47 of 91 strings)

Translated using Weblate (Portuguese)

Currently translated at 98.1% (161 of 164 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (3072 of 3077 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (761 of 764 strings)

Translated using Weblate (German)

Currently translated at 49.4% (45 of 91 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (377 of 377 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 89.7% (253 of 282 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 2.1% (3 of 137 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (3077 of 3077 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (761 of 764 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (761 of 764 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (231 of 231 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (3077 of 3077 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.9% (756 of 764 strings)

Translated using Weblate (German)

Currently translated at 48.3% (44 of 91 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (377 of 377 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (861 of 861 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.0% (749 of 764 strings)

Translated using Weblate (German)

Currently translated at 97.3% (744 of 764 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (377 of 377 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (164 of 164 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (231 of 231 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (3077 of 3077 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (3071 of 3077 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (377 of 377 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.1% (3051 of 3077 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (377 of 377 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (861 of 861 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (230 of 230 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (German)

Currently translated at 86.6% (370 of 427 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (3035 of 3035 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (3035 of 3035 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Russian)

Currently translated at 29.9% (41 of 137 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.0% (749 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (377 of 377 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (German)

Currently translated at 97.8% (836 of 854 strings)

Co-authored-by: Antonio Spinelli <tonicospinelli@users.noreply.translate.habitica.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Ellen A M <ellen_a_m@hotmail.com>
Co-authored-by: Finrod <963505255@qq.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Julian Brito <hackoogamer0852@gmail.com>
Co-authored-by: Luã Fhelyp Guimarães <fhelypg@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Χρήστος Joia <hristosjoia@gmail.com>
Co-authored-by: Катя Скибицкая <katerrina9993@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/de/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/de/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/de/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/zh_Hans/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Inventory
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2024-04-26 11:41:11 +02:00
Yeah Jack
2e9573ef92 Update README.md for better grammar (#15103) 2024-04-25 15:25:33 -04:00
dependabot[bot]
384bfce3eb chore(deps): bump express from 4.18.2 to 4.19.2 in /website/client (#15189)
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 14:34:51 -04:00
dependabot[bot]
5a8c7fb924 chore(deps): bump webpack-dev-middleware in /website/client (#15188)
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 14:34:24 -04:00
dependabot[bot]
246775256e chore(deps): bump express from 4.18.2 to 4.19.2 (#15190)
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 14:34:01 -04:00
dependabot[bot]
fa4cd8dd5a chore(deps): bump tar from 6.2.0 to 6.2.1 (#15191)
Bumps [tar](https://github.com/isaacs/node-tar) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 14:32:59 -04:00
dependabot[bot]
5224e063f7 chore(deps): bump axios from 0.21.4 to 1.6.8 (#15192)
Bumps [axios](https://github.com/axios/axios) from 0.21.4 to 1.6.8.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.4...v1.6.8)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 14:32:36 -04:00
dependabot[bot]
e5e8b9a7ec build(deps): bump chai from 4.3.7 to 5.1.0 in /website/client (#15144)
Bumps [chai](https://github.com/chaijs/chai) from 4.3.7 to 5.1.0.
- [Release notes](https://github.com/chaijs/chai/releases)
- [Changelog](https://github.com/chaijs/chai/blob/main/History.md)
- [Commits](https://github.com/chaijs/chai/compare/v4.3.7...v5.1.0)

---
updated-dependencies:
- dependency-name: chai
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 14:19:21 -04:00
dependabot[bot]
7cd76c50eb build(deps): bump axios from 0.27.2 to 0.28.0 in /website/client (#15148)
Bumps [axios](https://github.com/axios/axios) from 0.27.2 to 0.28.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.28.0/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.27.2...v0.28.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 14:18:07 -04:00
dependabot[bot]
b520202544 build(deps): bump sass-loader from 8.0.2 to 14.1.1 in /website/client (#15159)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 8.0.2 to 14.1.1.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v8.0.2...v14.1.1)

---
updated-dependencies:
- dependency-name: sass-loader
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 14:17:17 -04:00
dependabot[bot]
bbae882eda chore(deps): bump follow-redirects in /website/client (#15179)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 14:12:46 -04:00
Sabe Jones
372763c57c fix(shops): various 2024-04-24 17:58:20 -05:00
Sabe Jones
3bf323032c WIP(shops): first full test version 2024-04-24 00:02:41 -05:00
Sabe Jones
7a50b2d2ff Merge branch 'sabrecat/cshop' into subs-private 2024-04-23 10:52:05 -05:00
Sabe Jones
8df326bf92 WIP(shop): add timings to categories 2024-04-23 10:50:45 -05:00
Phillip Thelen
2d6555fe0f fix merge 2024-04-23 17:24:17 +02:00
Phillip Thelen
b0d6f7722b allow non-admins to observe timetravel 2024-04-23 17:20:21 +02:00
Phillip Thelen
aedbe7d333 content schedule tweaks 2024-04-23 17:20:21 +02:00
Phillip Thelen
6079dd4af6 feature items according to content schedule 2024-04-23 17:20:21 +02:00
Phillip Thelen
5c448188cf fix potion scheduling 2024-04-23 17:20:21 +02:00
Phillip Thelen
d7dc878b1c Cleanup pinned items that are no longer for purchase 2024-04-23 17:20:20 +02:00
Phillip Thelen
7baec4e48e Fix assigning end date to content schedule items 2024-04-23 14:40:10 +02:00
Sabe Jones
5cd58d4119 WIP(shops): dates and fixes 2024-04-22 17:52:07 -05:00
Sabe Jones
4cdfefd92b Merge branch 'sabrecat/cshop' into subs-private 2024-04-19 20:12:35 -05:00
Sabe Jones
28b936e2d1 WIP(shops): cShop reconciled to schedule backend 2024-04-19 20:11:17 -05:00
Sabe Jones
e4ec7e3e1e Merge branch 'gsf' into subs-private 2024-04-19 08:42:16 -05:00
Phillip Thelen
3c7ecef6a8 Update cron.js 2024-04-18 18:17:16 +02:00
Sabe Jones
e2a5a1ab39 Merge branch 'sabrecat/cshop' into subs-private 2024-04-17 18:03:11 -05:00
Sabe Jones
5f64b2fb25 WIP(customizations): revised backgrounds modal 2024-04-17 18:02:35 -05:00
Sabe Jones
f2a2d4cde5 Merge branch 'gsf' into subs-private 2024-04-17 09:05:36 -05:00
Sabe Jones
ee93c8bec5 5.23.0 2024-04-08 15:49:20 -05:00
Sabe Jones
c65e93e514 chore(git): update subproject 2024-04-08 15:49:17 -05:00
Sabe Jones
0fd808727c Squashed commit of the following:
commit 3c3787091a2e8a94857352c3655f60138a3b20b7
Merge: 76a00d6308 76d7f02fe8
Author: Sabe Jones <sabe@habitica.com>
Date:   Mon Apr 8 15:47:35 2024 -0500

    Merge branch 'release' into 2024-04-april-fool-items

commit 76a00d6308997c50ae5f5e9d6170a09a1a8cbac7
Author: Sabe Jones <sabe@habitica.com>
Date:   Thu Mar 28 16:08:39 2024 -0500

    fix(quest): revise rage text

commit c2e13f8af245993f61eb614d6be609c833c2e711
Author: Sabe Jones <sabe@habitica.com>
Date:   Thu Mar 28 12:57:17 2024 -0500

    fix(quest): correct rage and "guscompletion"

commit ecdeb82df998ab75eceae3e877c1758187b1d22c
Author: Sabe Jones <sabe@habitica.com>
Date:   Thu Mar 28 08:55:18 2024 -0500

    fix(events): correct dates and times

commit 232de436bbe7ba4c13f04ed66cc3b8a4a794d884
Author: Sabe Jones <sabe@habitica.com>
Date:   Wed Mar 27 09:03:06 2024 -0500

    fix(content): a few more unruly Fungus

commit 955e6e73387788797d1a83a037f73aa472515cec
Author: Sabe Jones <sabe@habitica.com>
Date:   Wed Mar 27 09:00:03 2024 -0500

    feat(content): wacky potions 2024 by @CuriousMagpie

commit 34b72be4f447f00c31169c4cd6e8ba79655adf8a
Author: Sabe Jones <sabe@habitica.com>
Date:   Wed Mar 27 08:56:27 2024 -0500

    feat(event): AF by @CuriousMagpie
2024-04-08 15:48:32 -05:00
Phillip Thelen
edc3c58876 Fix issue with gift sub processing 2024-04-05 13:30:15 +02:00
Sabe Jones
4277c08324 fix(scheduling): lint and code review 2024-04-04 22:42:28 -05:00
Sabe Jones
424f29a82b fix(lint): curly space 2024-04-04 22:02:31 -05:00
Sabe Jones
e52c7ff9ce WIP(shop): empty states for categories 2024-04-04 22:02:19 -05:00
Sabe Jones
fdc709d1c2 fix(lint): remove console log 2024-04-03 10:33:43 -05:00
Sabe Jones
ea750571a0 fix(packages): add timers-browserify 2024-04-03 10:30:20 -05:00
Phillip Thelen
f8fbea4654 quest shop fix 2024-04-03 16:06:08 +02:00
Phillip Thelen
e0f4a4ecb8 merge in shop schedule loading 2024-04-03 16:06:08 +02:00
Phillip Thelen
5ce12d97be fix errors 2024-04-03 16:06:08 +02:00
Sabe Jones
4fdd064cd6 Merge branch 'sabrecat/customizations' into subs-private 2024-04-03 08:36:25 -05:00
Sabe Jones
37731b236a WIP(shop): add item names and unlocking 2024-04-02 17:58:06 -05:00
Sabe Jones
b06c708480 Merge branch 'release' into sabrecat/customizations 2024-04-02 15:39:58 -05:00
Phillip Thelen
f8c452ae3f Remove duplicate menu entry 2024-04-02 17:12:38 +02:00
Phillip Thelen
9befbec2b0 Update index.vue 2024-04-02 17:11:18 +02:00
Phillip Thelen
7b46f3bc23 Fix method name 2024-04-02 17:07:28 +02:00
Sabe Jones
64a500987c fix(packages): move sinon to main 2024-04-02 09:32:44 -05:00
Sabe Jones
92eaece5eb fix(packages): add assert 2024-04-01 19:13:29 -05:00
Sabe Jones
ee73d5b628 fix(packages): add util 2024-04-01 19:10:59 -05:00
Sabe Jones
a7eda1355b fix(packages): add sinon to client 2024-04-01 19:08:07 -05:00
Sabe Jones
8b9b79db8e fix(potions): remove EVENT 2024-04-01 19:04:57 -05:00
Sabe Jones
69c0488335 Merge branch 'sabrecat/customizations' into subs-private 2024-04-01 18:57:36 -05:00
Phillip Thelen
c18e06f071 changes 2024-04-01 18:38:04 -05:00
Phillip Thelen
a50c0eb1e7 fix test 2024-04-01 18:37:57 -05:00
Phillip Thelen
fb56f7df20 Fix various tests 2024-04-01 18:37:46 -05:00
Phillip Thelen
3540a274b3 fix issue with seasonal quest scheduling 2024-04-01 18:36:44 -05:00
Phillip Thelen
fe2c02679e Fix buying some items 2024-04-01 18:36:33 -05:00
Phillip Thelen
bca3e96e9c Implement food seasons 2024-04-01 18:36:24 -05:00
Phillip Thelen
041edb3042 Preen old scheduling code 2024-04-01 18:13:40 -05:00
Phillip Thelen
6e96085f99 add cards to event content cycle 2024-04-01 18:07:41 -05:00
Phillip Thelen
249394b4ad Implement events throughout the year 2024-04-01 18:07:10 -05:00
Phillip Thelen
2a84561e00 fix some date issues with scheduling 2024-04-01 18:06:55 -05:00
Phillip Thelen
962456204e improve updating UI when time traveling 2024-04-01 18:06:00 -05:00
Phillip Thelen
593524905e allow admins to manipulate time on test servers 2024-04-01 18:05:41 -05:00
Phillip Thelen
1b12e9d8b7 proof of concept for time travel 2024-04-01 18:04:38 -05:00
Phillip Thelen
127f105934 typo 2024-04-01 17:56:24 -05:00
Phillip Thelen
2dfe5585eb fix 2024-04-01 17:56:15 -05:00
Phillip Thelen
93011f182f Fix lint and test 2024-04-01 17:56:06 -05:00
Phillip Thelen
d11e95ab26 change mobile filter defaults to a list 2024-04-01 17:55:58 -05:00
Phillip Thelen
f99ddbe60f display customizations in new shop 2024-04-01 17:54:07 -05:00
Phillip Thelen
982069df36 remove logs 2024-04-01 17:52:46 -05:00
Phillip Thelen
db4bec37e3 Implement new schedule system for seasonal shop 2024-04-01 17:52:40 -05:00
Phillip Thelen
736ef16430 simplify schedule matching usage 2024-04-01 17:52:29 -05:00
Phillip Thelen
129cb7627c Implement new content schedule for magic hatching potions 2024-04-01 17:52:21 -05:00
Phillip Thelen
f223b5dd2a Implement schedule for quest bundles 2024-04-01 17:52:12 -05:00
Phillip Thelen
b3521be629 Implement new content schedule for potion and pet quests 2024-04-01 17:52:02 -05:00
Phillip Thelen
17db6a1772 Implement new release schedule for backgrounds 2024-04-01 17:51:47 -05:00
Phillip Thelen
278d9b74f9 Implement new scheduling for time travelers shop 2024-04-01 17:51:38 -05:00
Phillip Thelen
ce796fa1d9 begin building recurring content scheduling 2024-04-01 17:51:24 -05:00
Phillip Thelen
ec0275e6f6 Add tests for content filtering 2024-04-01 17:50:33 -05:00
Phillip Thelen
39252c7828 allow clients to filter content api call 2024-04-01 17:50:03 -05:00
Sabe Jones
75a88ab25a WIP(shops): style fixes 2024-03-29 16:14:14 -05:00
Sabe Jones
70434b17cc WIP(shop): show customs in buy modal 2024-03-14 17:29:15 -05:00
Sabe Jones
a921a8bc61 Merge branch 'release' into sabrecat/customizations 2024-03-14 14:55:56 -05:00
Sabe Jones
0aa9d4d1d5 WIP(shops): start wiring up buy modals 2024-03-11 16:09:16 -05:00
Sabe Jones
0ead06937b Merge branch 'release' into sabrecat/customizations 2024-03-11 14:54:34 -05:00
Sabe Jones
037fb6737d Merge branch 'release' into sabrecat/customizations 2024-03-04 14:25:53 -06:00
Sabe Jones
4e0d8cba51 Merge branch 'release' into sabrecat/customizations 2024-03-01 14:31:28 -06:00
Sabe Jones
ecc8a65d28 WIP(customizations): animal bits 2024-02-29 15:59:31 -06:00
Sabe Jones
28fef8df86 WIP(shops): shirts vs skins 2024-02-27 15:17:10 -06:00
Sabe Jones
33b54a734e WIP(shop): more CSS, add hair styles 2024-02-22 17:03:07 -06:00
Sabe Jones
1f8aa7d778 Merge branch 'release' into sabrecat/customizations 2024-02-22 15:26:01 -06:00
Sabe Jones
09ff3ee865 WIP(customizations): load hair colors 2024-02-20 17:56:56 -06:00
Sabe Jones
cbfeb18517 WIP(shop): backgrounds appear 2024-02-16 17:58:11 -06:00
Sabe Jones
63e7ace693 WIP(shops): customization categories skeleton 2024-02-15 17:51:14 -06:00
Sabe Jones
0f9cf48b55 Merge branch 'release' into sabrecat/customizations 2024-02-15 15:26:05 -06:00
Sabe Jones
5a48436eff WIP(shops): customizations route 2024-02-14 17:49:26 -06:00
454 changed files with 28588 additions and 9702 deletions

150
.github/dependabot.yml vendored
View File

@@ -1,150 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
time: "06:00"
timezone: Europe/Rome
open-pull-requests-limit: 99
ignore:
- dependency-name: express-validator
versions:
- 6.10.0
- 6.10.1
- 6.9.2
- dependency-name: "@babel/core"
versions:
- 7.12.13
- 7.12.16
- 7.12.17
- 7.13.1
- 7.13.10
- 7.13.13
- 7.13.14
- 7.13.15
- 7.13.8
- dependency-name: redis
versions:
- 3.1.0
- dependency-name: stripe
versions:
- 8.134.0
- 8.135.0
- 8.137.0
- 8.138.0
- 8.140.0
- 8.142.0
- dependency-name: "@babel/register"
versions:
- 7.12.13
- 7.13.14
- 7.13.8
- dependency-name: mongoose
versions:
- 5.11.14
- 5.11.15
- 5.11.16
- 5.11.17
- 5.11.18
- 5.11.19
- 5.12.0
- 5.12.1
- 5.12.2
- 5.12.3
- dependency-name: jwks-rsa
versions:
- 1.12.3
- 2.0.1
- 2.0.2
- dependency-name: "@babel/preset-env"
versions:
- 7.12.13
- 7.12.16
- 7.12.17
- 7.13.10
- 7.13.12
- 7.13.8
- 7.13.9
- dependency-name: image-size
versions:
- 0.9.4
- 0.9.5
- 0.9.7
- dependency-name: winston-loggly-bulk
versions:
- 3.2.0
- dependency-name: chai
versions:
- 4.3.0
- 4.3.3
- dependency-name: mocha
versions:
- 8.2.1
- 8.3.0
- 8.3.1
- dependency-name: "@google-cloud/trace-agent"
versions:
- 5.1.2
- dependency-name: monk
versions:
- 7.3.3
- package-ecosystem: npm
directory: "/website/client"
schedule:
interval: weekly
time: "06:00"
timezone: Europe/Rome
open-pull-requests-limit: 99
ignore:
- dependency-name: eslint-plugin-vue
versions:
- 7.5.0
- 7.6.0
- 7.7.0
- 7.8.0
- 7.9.0
- dependency-name: core-js
versions:
- 3.10.0
- 3.10.1
- 3.9.0
- 3.9.1
- dependency-name: bootstrap
versions:
- 4.6.0
- dependency-name: y18n
versions:
- 4.0.1
- dependency-name: hellojs
versions:
- 1.18.8
- 1.19.2
- dependency-name: chai
versions:
- 4.3.0
- 4.3.3
- dependency-name: amplitude-js
versions:
- 7.4.2
- 7.4.3
- 7.4.4
- dependency-name: pug
versions:
- 3.0.2
- dependency-name: sass
versions:
- 1.32.6
- 1.32.7
- 1.32.8
- dependency-name: "@vue/test-utils"
versions:
- 1.1.2
- 1.1.3
- dependency-name: intro.js
versions:
- 3.2.1
- 3.3.1
- dependency-name: sass-loader
versions:
- 10.1.1

3
.gitignore vendored
View File

@@ -8,7 +8,7 @@ i18n_cache
apidoc/html
*.swp
.idea*
config.json
config*.json
npm-debug.log*
lib
newrelic_agent.log
@@ -48,3 +48,4 @@ webpack.webstorm.config
# mongodb replica set for local dev
mongodb-*.tgz
/mongodb-data
/.nyc_output

View File

@@ -7,8 +7,6 @@ RUN npm install -g gulp-cli mocha
# dependencies.
WORKDIR /usr/src/habitica
COPY ["package.json", "package-lock.json", "./"]
RUN npm install
# Copy the remaining source files in.
COPY . /usr/src/habitica
RUN npm run postinstall
RUN npm install

View File

@@ -1,7 +1,7 @@
Habitica ![Build Status](https://github.com/HabitRPG/habitica/workflows/Test/badge.svg) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
===============
[Habitica](https://habitica.com) is an open source habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.
[Habitica](https://habitica.com) is an open-source habit-building program that treats your life like a role-playing game. Level up as you succeed, lose HP as you fail, and earn money to buy weapons and armor.
**We need more programmers!** Your assistance will be greatly appreciated. The wiki pages below and the additional pages they link to will tell you how to get started on contributing code and where you can go to seek further help or ask questions:
* [Guidance for Blacksmiths](https://habitica.fandom.com/wiki/Guidance_for_Blacksmiths) - an introduction to the technologies used and how the software is organized.

View File

@@ -32,6 +32,7 @@
"LOGGLY_CLIENT_TOKEN": "token",
"LOGGLY_SUBDOMAIN": "example-subdomain",
"LOGGLY_TOKEN": "example-token",
"LOG_REQUESTS_EXCESSIVE_MODE": "false",
"MAINTENANCE_MODE": "false",
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
@@ -84,8 +85,12 @@
"BLOCKED_IPS": "",
"LOG_AMPLITUDE_EVENTS": "false",
"RATE_LIMITER_ENABLED": "false",
"LIVELINESS_PROBE_KEY": "",
"REDIS_HOST": "aaabbbcccdddeeefff",
"REDIS_PORT": "1234",
"REDIS_PASSWORD": "12345678",
"TRUSTED_DOMAINS": "localhost,https://habitica.com"
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
"TIME_TRAVEL_ENABLED": "false",
"DEBUG_ENABLED": "false",
"CONTENT_SWITCHOVER_TIME_OFFSET": 8
}

View File

@@ -44,8 +44,8 @@ function runInChildProcess (command, options = {}, envVariables = '') {
return done => pipe(exec(testBin(command, envVariables), options, done));
}
function integrationTestCommand (testDir, coverageDir) {
return `istanbul cover --dir coverage/${coverageDir} --report lcovonly node_modules/mocha/bin/_mocha -- ${testDir} --recursive --require ./test/helpers/start-server`;
function integrationTestCommand (testDir) {
return `nyc --silent --no-clean mocha ${testDir} --recursive --require ./test/helpers/start-server`;
}
/* Test task definitions */
@@ -148,7 +148,7 @@ gulp.task('test:content:safe', gulp.series('test:prepare:build', cb => {
gulp.task(
'test:api:unit:run',
runInChildProcess(integrationTestCommand('test/api/unit', 'coverage/api-unit')),
runInChildProcess(integrationTestCommand('test/api/unit')),
);
gulp.task('test:api:unit:watch', () => gulp.watch(['website/server/libs/*', 'test/api/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit:run', done => done())));
@@ -156,7 +156,7 @@ gulp.task('test:api:unit:watch', () => gulp.watch(['website/server/libs/*', 'tes
gulp.task('test:api-v3:integration', gulp.series(
'test:prepare:mongo',
runInChildProcess(
integrationTestCommand('test/api/v3/integration', 'coverage/api-v3-integration'),
integrationTestCommand('test/api/v3/integration'),
LIMIT_MAX_BUFFER_OPTIONS,
),
));
@@ -175,7 +175,7 @@ gulp.task('test:api-v3:integration:separate-server', runInChildProcess(
gulp.task('test:api-v4:integration', gulp.series(
'test:prepare:mongo',
runInChildProcess(
integrationTestCommand('test/api/v4', 'api-v4-integration'),
integrationTestCommand('test/api/v4'),
LIMIT_MAX_BUFFER_OPTIONS,
),
));

View File

@@ -0,0 +1,99 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '202405_pet_group_achievements';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
let set = {
migration: MIGRATION_NAME,
};
if (user && user.items && user.items.pets) {
const pets = user.items.pets;
if (pets['LionCub-Zombie'] > 0
&& pets['LionCub-Skeleton'] > 0
&& pets['LionCub-Base'] > 0
&& pets['LionCub-Desert'] > 0
&& pets['LionCub-Red'] > 0
&& pets['LionCub-Shade'] > 0
&& pets['LionCub-White']> 0
&& pets['LionCub-Golden'] > 0
&& pets['LionCub-CottonCandyBlue'] > 0
&& pets['LionCub-CottonCandyPink'] > 0
&& pets['TigerCub-Zombie'] > 0
&& pets['TigerCub-Skeleton'] > 0
&& pets['TigerCub-Base'] > 0
&& pets['TigerCub-Desert'] > 0
&& pets['TigerCub-Red'] > 0
&& pets['TigerCub-Shade'] > 0
&& pets['TigerCub-White'] > 0
&& pets['TigerCub-Golden'] > 0
&& pets['TigerCub-CottonCandyBlue'] > 0
&& pets['TigerCub-CottonCandyPink'] > 0
&& pets['Sabretooth-Zombie'] > 0
&& pets['Sabretooth-Skeleton'] > 0
&& pets['Sabretooth-Base'] > 0
&& pets['Sabretooth-Desert'] > 0
&& pets['Sabretooth-Red'] > 0
&& pets['Sabretooth-Shade'] > 0
&& pets['Sabretooth-White'] > 0
&& pets['Sabretooth-Golden'] > 0
&& pets['Sabretooth-CottonCandyBlue'] > 0
&& pets['Sabretooth-CottonCandyPink'] > 0
&& pets['Cheetah-Zombie'] > 0
&& pets['Cheetah-Skeleton'] > 0
&& pets['Cheetah-Base'] > 0
&& pets['Cheetah-Desert'] > 0
&& pets['Cheetah-Red'] > 0
&& pets['Cheetah-Shade'] > 0
&& pets['Cheetah-White'] > 0
&& pets['Cheetah-Golden'] > 0
&& pets['Cheetah-CottonCandyBlue'] > 0
&& pets['Cheetah-CottonCandyPink'] > 0 ) {
set['achievements.cats'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.updateOne({ _id: user._id }, { $set: set }).exec();
}
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2024-03-01') },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,149 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20240621_veteran_pet_ladder';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {};
let push = { notifications: { $each: [] }};
set.migration = MIGRATION_NAME;
if (user.items.pets['Dragon-Veteran']) {
set['items.pets.Cactus-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_cactus',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Cactus and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Fox-Veteran']) {
set['items.pets.Dragon-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_dragon',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Dragon and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Bear-Veteran']) {
set['items.pets.Fox-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_fox',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Fox and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Lion-Veteran']) {
set['items.pets.Bear-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_bear',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Bear and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Tiger-Veteran']) {
set['items.pets.Lion-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_lion',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Lion and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Wolf-Veteran']) {
set['items.pets.Tiger-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_tiger',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Tiger and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else {
set['items.pets.Wolf-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_wolf',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Wolf and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
await user.updateBalance(
6,
'admin_update_balance',
'',
'Veteran Ladder award',
);
return await User.updateOne(
{ _id: user._id },
{ $set: set, $push: push, $inc: { balance: 6 } },
).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': { $gt: new Date('2024-05-21') },
};
const fields = {
_id: 1,
items: 1,
migration: 1,
contributor: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

2651
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.22.3",
"version": "5.25.8",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
@@ -27,9 +27,10 @@
"eslint": "^8.55.0",
"eslint-config-habitrpg": "^6.2.3",
"eslint-plugin-mocha": "^5.0.0",
"express": "^4.18.2",
"express": "^4.19.2",
"express-basic-auth": "^1.2.1",
"express-validator": "^5.2.0",
"firebase-admin": "^12.1.1",
"glob": "^8.1.0",
"got": "^11.8.6",
"gulp": "^4.0.0",
@@ -66,6 +67,7 @@
"remove-markdown": "^0.5.0",
"rimraf": "^3.0.2",
"short-uuid": "^4.2.2",
"sinon": "^15.2.0",
"stripe": "^12.18.0",
"superagent": "^8.1.2",
"universal-analytics": "^0.5.3",
@@ -92,11 +94,11 @@
"test:api-v3:integration:separate-server": "NODE_ENV=test gulp test:api-v3:integration:separate-server",
"test:api-v4:integration": "gulp test:api-v4:integration",
"test:api-v4:integration:separate-server": "NODE_ENV=test gulp test:api-v4:integration:separate-server",
"test:sanity": "istanbul cover --dir coverage/sanity --report lcovonly node_modules/mocha/bin/_mocha -- test/sanity --recursive",
"test:common": "istanbul cover --dir coverage/common --report lcovonly node_modules/mocha/bin/_mocha -- test/common --recursive",
"test:content": "istanbul cover --dir coverage/content --report lcovonly node_modules/mocha/bin/_mocha -- test/content --recursive",
"test:sanity": "nyc --silent --no-clean mocha test/sanity --recursive",
"test:common": "nyc --silent --no-clean mocha test/common --recursive",
"test:content": "nyc --silent --no-clean mocha test/content --recursive",
"test:nodemon": "gulp test:nodemon",
"coverage": "COVERAGE=true mocha --require register-handlers.js --reporter html-cov > coverage.html; open coverage.html",
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
"sprites": "gulp sprites:compile",
"client:dev": "cd website/client && npm run serve",
"client:build": "cd website/client && npm run build",
@@ -114,13 +116,11 @@
"chai-moment": "^0.1.0",
"chalk": "^5.3.0",
"cross-spawn": "^7.0.3",
"expect.js": "^0.3.1",
"istanbul": "^1.1.0-alpha.1",
"mocha": "^5.1.1",
"monk": "^7.3.4",
"nyc": "^15.1.0",
"require-again": "^2.0.0",
"run-rs": "^0.7.7",
"sinon": "^15.2.0",
"sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0"
}

View File

@@ -1,5 +1,9 @@
import fs from 'fs';
import * as contentLib from '../../../../website/server/libs/content';
import content from '../../../../website/common/script/content';
import {
generateRes,
} from '../../../helpers/api-unit.helper';
describe('contentLib', () => {
describe('CONTENT_CACHE_PATH', () => {
@@ -13,5 +17,90 @@ describe('contentLib', () => {
contentLib.getLocalizedContentResponse();
expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function');
});
it('removes keys from the content data', () => {
const response = contentLib.localizeContentData(content, 'en', { backgroundsFlat: true, dropHatchingPotions: true });
expect(response.backgroundsFlat).to.not.exist;
expect(response.backgrounds).to.exist;
expect(response.dropHatchingPotions).to.not.exist;
expect(response.hatchingPotions).to.exist;
});
it('removes nested keys from the content data', () => {
const response = contentLib.localizeContentData(content, 'en', { gear: { tree: true } });
expect(response.gear.tree).to.not.exist;
expect(response.gear.flat).to.exist;
});
});
it('generates a hash for a filter', () => {
const hash = contentLib.hashForFilter('backgroundsFlat,gear.flat');
expect(hash).to.equal('-1791877526');
});
it('serves content', () => {
const resSpy = generateRes();
contentLib.serveContent(resSpy, 'en', '', false);
expect(resSpy.send).to.have.been.calledOnce;
});
it('serves filtered content', () => {
const resSpy = generateRes();
contentLib.serveContent(resSpy, 'en', 'backgroundsFlat,gear.flat', false);
expect(resSpy.send).to.have.been.calledOnce;
});
describe('caches content', async () => {
let resSpy;
beforeEach(() => {
resSpy = generateRes();
if (fs.existsSync(contentLib.CONTENT_CACHE_PATH)) {
fs.rmdirSync(contentLib.CONTENT_CACHE_PATH, { recursive: true });
}
fs.mkdirSync(contentLib.CONTENT_CACHE_PATH);
});
it('does not cache requests in development mode', async () => {
contentLib.serveContent(resSpy, 'en', '', false);
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.false;
});
it('caches unfiltered requests', async () => {
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.false;
contentLib.serveContent(resSpy, 'en', '', true);
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.true;
});
it('serves cached requests', async () => {
fs.writeFileSync(
`${contentLib.CONTENT_CACHE_PATH}en.json`,
'{"success": true, "data": {"all": {}}}',
'utf8',
);
contentLib.serveContent(resSpy, 'en', '', true);
expect(resSpy.sendFile).to.have.been.calledOnce;
expect(resSpy.sendFile).to.have.been.calledWith(`${contentLib.CONTENT_CACHE_PATH}en.json`);
});
it('caches filtered requests', async () => {
const filter = 'backgroundsFlat,gear.flat';
const hash = contentLib.hashForFilter(filter);
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`)).to.be.false;
contentLib.serveContent(resSpy, 'en', filter, true);
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`)).to.be.true;
});
it('serves filtered cached requests', async () => {
const filter = 'backgroundsFlat,gear.flat';
const hash = contentLib.hashForFilter(filter);
fs.writeFileSync(
`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`,
'{"success": true, "data": {}}',
'utf8',
);
contentLib.serveContent(resSpy, 'en', filter, true);
expect(resSpy.sendFile).to.have.been.calledOnce;
expect(resSpy.sendFile).to.have.been.calledWith(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`);
});
});
});

View File

@@ -117,7 +117,7 @@ describe('Items Utils', () => {
it('converts values for owned gear to true/false', () => {
expect(castItemVal('items.gear.owned.shield_warrior_0', 'true')).to.equal(true);
expect(castItemVal('items.gear.owned.invalid', 'false')).to.equal(false);
expect(castItemVal('items.gear.owned.invalid', 'null')).to.equal(false);
expect(castItemVal('items.gear.owned.invalid', 'null')).to.equal(undefined);
expect(castItemVal('items.gear.owned.invalid', 'truthy')).to.equal(true);
expect(castItemVal('items.gear.owned.invalid', 0)).to.equal(false);
});

View File

@@ -1,184 +0,0 @@
import apn from '@parse/node-apn/mock';
import _ from 'lodash';
import nconf from 'nconf';
import gcmLib from 'node-gcm'; // works with FCM notifications too
import { model as User } from '../../../../website/server/models/user';
import {
sendNotification as sendPushNotification,
MAX_MESSAGE_LENGTH,
} from '../../../../website/server/libs/pushNotifications';
describe('pushNotifications', () => {
let user;
let fcmSendSpy;
let apnSendSpy;
const identifier = 'identifier';
const title = 'title';
const message = 'message';
beforeEach(() => {
user = new User();
fcmSendSpy = sinon.spy();
apnSendSpy = sinon.spy();
sandbox.stub(nconf, 'get').returns('true-key');
sandbox.stub(gcmLib.Sender.prototype, 'send').callsFake(fcmSendSpy);
sandbox.stub(apn.Provider.prototype, 'send').returns({
on: () => null,
send: apnSendSpy,
});
});
afterEach(() => {
sandbox.restore();
});
it('throws if user is not supplied', () => {
expect(sendPushNotification).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if user.preferences.pushNotifications.unsubscribeFromAll is true', () => {
user.preferences.pushNotifications.unsubscribeFromAll = true;
expect(() => sendPushNotification(user)).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.identifier is not supplied', () => {
expect(() => sendPushNotification(user, {
title,
message,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.title is not supplied', () => {
expect(() => sendPushNotification(user, {
identifier,
message,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.message is not supplied', () => {
expect(() => sendPushNotification(user, {
identifier,
title,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('returns if no device is registered', () => {
sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('cuts the message to 300 chars', () => {
const longMessage = `12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
const details = {
identifier,
title,
message: longMessage,
payload: {
message: longMessage,
},
};
sendPushNotification(user, details);
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.payload.message)
.to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
expect(details.payload.message.length).to.equal(MAX_MESSAGE_LENGTH);
});
it('cuts the message to 300 chars (no payload)', () => {
const longMessage = `12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
const details = {
identifier,
title,
message: longMessage,
};
sendPushNotification(user, details);
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
});
// TODO disabled because APN relies on a Promise
xit('uses APN for iOS devices', () => {
user.pushDevices.push({
type: 'ios',
regId: '123',
});
const details = {
identifier,
title,
message,
category: 'fun',
payload: {
a: true,
b: true,
},
};
const expectedNotification = new apn.Notification({
alert: message,
sound: 'default',
category: 'fun',
payload: {
identifier,
a: true,
b: true,
},
});
sendPushNotification(user, details);
expect(apnSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.have.been.calledWithMatch(expectedNotification, '123');
expect(fcmSendSpy).to.not.have.been.called;
});
});

View File

@@ -0,0 +1,354 @@
import apn from '@parse/node-apn';
import _ from 'lodash';
import nconf from 'nconf';
import admin from 'firebase-admin';
import { model as User } from '../../../../website/server/models/user';
import {
MAX_MESSAGE_LENGTH,
} from '../../../../website/server/libs/pushNotifications';
let sendPushNotification;
describe('pushNotifications', () => {
let user;
let fcmSendSpy;
let apnSendSpy;
let updateStub;
let classStubbedInstance;
const identifier = 'identifier';
const title = 'title';
const message = 'message';
beforeEach(() => {
user = new User();
fcmSendSpy = sinon.stub().returns(Promise.resolve('success'));
apnSendSpy = sinon.stub().returns(Promise.resolve());
nconf.set('PUSH_CONFIGS_APN_ENABLED', 'true');
classStubbedInstance = sandbox.createStubInstance(apn.Provider, {
send: apnSendSpy,
});
sandbox.stub(apn, 'Provider').returns(classStubbedInstance);
delete require.cache[require.resolve('../../../../website/server/libs/pushNotifications')];
// eslint-disable-next-line global-require
sendPushNotification = require('../../../../website/server/libs/pushNotifications').sendNotification;
updateStub = sandbox.stub(User, 'updateOne').resolves();
sandbox.stub(admin, 'messaging').get(() => () => ({ send: fcmSendSpy }));
});
afterEach(() => {
sandbox.restore();
});
describe('validates supplied data', () => {
it('throws if user is not supplied', () => {
expect(sendPushNotification).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if user.preferences.pushNotifications.unsubscribeFromAll is true', () => {
user.preferences.pushNotifications.unsubscribeFromAll = true;
expect(() => sendPushNotification(user)).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.identifier is not supplied', () => {
expect(() => sendPushNotification(user, {
title,
message,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.title is not supplied', () => {
expect(() => sendPushNotification(user, {
identifier,
message,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('throws if details.message is not supplied', () => {
expect(() => sendPushNotification(user, {
identifier,
title,
})).to.throw;
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
it('returns if no device is registered', () => {
sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.not.have.been.called;
});
});
it('cuts the message to 300 chars', () => {
const longMessage = `12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
const details = {
identifier,
title,
message: longMessage,
payload: {
message: longMessage,
},
};
sendPushNotification(user, details);
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.payload.message)
.to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
expect(details.payload.message.length).to.equal(MAX_MESSAGE_LENGTH);
});
it('cuts the message to 300 chars (no payload)', () => {
const longMessage = `12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
const details = {
identifier,
title,
message: longMessage,
};
sendPushNotification(user, details);
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
});
describe('sends notifications', () => {
let details;
beforeEach(() => {
details = {
identifier,
title,
message,
category: 'fun',
payload: {
a: true,
b: true,
},
};
});
it('uses APN for iOS devices', async () => {
user.pushDevices.push({
type: 'ios',
regId: '123',
});
const expectedNotification = new apn.Notification({
alert: {
title,
body: message,
},
sound: 'default',
category: 'fun',
payload: {
identifier,
a: true,
b: true,
},
});
await sendPushNotification(user, details);
expect(apnSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.have.been.calledWithMatch(expectedNotification, '123');
expect(fcmSendSpy).to.not.have.been.called;
});
it('uses FCM for Android devices', async () => {
user.pushDevices.push({
type: 'android',
regId: '123',
});
const expectedMessage = {
notification: {
title,
body: message,
},
data: {
identifier,
notificationIdentifier: identifier,
},
token: '123',
};
await sendPushNotification(user, details);
expect(fcmSendSpy).to.have.been.calledOnce;
expect(fcmSendSpy).to.have.been.calledWithMatch(expectedMessage);
expect(apnSendSpy).to.not.have.been.called;
});
it('handles multiple devices', async () => {
user.pushDevices.push({
type: 'android',
regId: '123',
});
user.pushDevices.push({
type: 'ios',
regId: '456',
});
user.pushDevices.push({
type: 'android',
regId: '789',
});
await sendPushNotification(user, details);
expect(fcmSendSpy).to.have.been.calledTwice;
expect(apnSendSpy).to.have.been.calledOnce;
});
});
describe('handles sending errors', () => {
let clock;
beforeEach(() => {
clock = sinon.useFakeTimers();
});
afterEach(() => {
clock.restore();
});
it('removes unregistered fcm devices', async () => {
user.pushDevices.push({
type: 'android',
regId: '123',
});
const error = new Error();
error.code = 'messaging/registration-token-not-registered';
fcmSendSpy.rejects(error);
await sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.not.have.been.called;
await clock.tick(10);
expect(updateStub).to.have.been.calledOnce;
});
it('removes invalid fcm devices', async () => {
user.pushDevices.push({
type: 'android',
regId: '123',
});
const error = new Error();
error.code = 'messaging/registration-token-not-registered';
fcmSendSpy.rejects(error);
await sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.have.been.calledOnce;
expect(apnSendSpy).to.not.have.been.called;
expect(updateStub).to.have.been.calledOnce;
});
it('removes unregistered apn devices', async () => {
user.pushDevices.push({
type: 'ios',
regId: '123',
});
const error = {
failed: [
{
device: '123',
response: { reason: 'Unregistered' },
},
],
};
apnSendSpy.resolves(error);
await sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.have.been.calledOnce;
expect(updateStub).to.have.been.calledOnce;
});
it('removes invalid apn devices', async () => {
user.pushDevices.push({
type: 'ios',
regId: '123',
});
const error = {
failed: [
{
device: '123',
response: { reason: 'BadDeviceToken' },
},
],
};
apnSendSpy.resolves(error);
await sendPushNotification(user, {
identifier,
title,
message,
});
expect(fcmSendSpy).to.not.have.been.called;
expect(apnSendSpy).to.have.been.calledOnce;
expect(updateStub).to.have.been.calledOnce;
});
});
});

View File

@@ -0,0 +1,51 @@
/* eslint-disable global-require */
import nconf from 'nconf';
import {
generateRes,
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
import ensureDevelopmentMode from '../../../../website/server/middlewares/ensureDevelopmentMode';
import { NotFound } from '../../../../website/server/libs/errors';
describe('developmentMode middleware', () => {
let res; let req; let next;
let nconfStub;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
nconfStub = sandbox.stub(nconf, 'get');
});
it('returns not found when on production URL', () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
ensureDevelopmentMode(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('returns not found when intentionally disabled', () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
ensureDevelopmentMode(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('passes when enabled and on non-production URL', () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
ensureDevelopmentMode(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
});
});

View File

@@ -1,38 +0,0 @@
/* eslint-disable global-require */
import nconf from 'nconf';
import {
generateRes,
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
import ensureDevelpmentMode from '../../../../website/server/middlewares/ensureDevelpmentMode';
import { NotFound } from '../../../../website/server/libs/errors';
describe('developmentMode middleware', () => {
let res; let req; let
next;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
it('returns not found when in production mode', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
ensureDevelpmentMode(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('passes when not in production', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
ensureDevelpmentMode(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
});
});

View File

@@ -0,0 +1,51 @@
/* eslint-disable global-require */
import nconf from 'nconf';
import {
generateRes,
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
import { NotFound } from '../../../../website/server/libs/errors';
import ensureTimeTravelMode from '../../../../website/server/middlewares/ensureTimeTravelMode';
describe('timetravelMode middleware', () => {
let res; let req; let next;
let nconfStub;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
nconfStub = sandbox.stub(nconf, 'get');
});
it('returns not found when using production URL', () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
ensureTimeTravelMode(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('returns not found when not in time travel mode', () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
ensureTimeTravelMode(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('passes when in time travel mode', () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
ensureTimeTravelMode(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
});
});

View File

@@ -87,6 +87,67 @@ describe('rateLimiter middleware', () => {
expect(logger.error).to.have.been.calledWithMatch(Error, 'Rate Limiter Error');
});
it('does not throw when LIVELINESS_PROBE_KEY is correct', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.query.liveliness = 'abc';
await attachRateLimiter(req, res, next);
expect(next).to.have.been.calledOnce;
const calledWith = next.getCall(0).args;
expect(typeof calledWith[0] === 'undefined').to.equal(true);
expect(res.set).to.not.have.been.called;
});
it('limits when LIVELINESS_PROBE_KEY is incorrect', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.query.liveliness = 'das';
await attachRateLimiter(req, res, next);
expect(next).to.have.been.calledOnce;
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 29,
'X-RateLimit-Reset': sinon.match(Date),
});
});
it('limits when LIVELINESS_PROBE_KEY is not set', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns(undefined);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
await attachRateLimiter(req, res, next);
expect(next).to.have.been.calledOnce;
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 29,
'X-RateLimit-Reset': sinon.match(Date),
});
});
it('throws when LIVELINESS_PROBE_KEY is blank', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('');
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.query.liveliness = '';
await attachRateLimiter(req, res, next);
expect(next).to.have.been.calledOnce;
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 29,
'X-RateLimit-Reset': sinon.match(Date),
});
});
it('throws when there are no available points remaining', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
const attachRateLimiter = requireAgain(pathToRateLimiter).default;

View File

@@ -0,0 +1,37 @@
/* eslint-disable global-require */
import requireAgain from 'require-again';
import {
generateRes,
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
describe('requestLogHandler middleware', () => {
let res; let req; let
next;
const pathToMiddleWare = '../../../../website/server/middlewares/requestLogHandler';
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
it('attaches start time and request ID object to req', () => {
const middleware = requireAgain(pathToMiddleWare);
middleware.logRequestData(req, res, next);
expect(req.requestStartTime).to.exist;
expect(req.requestStartTime).to.be.a('number');
expect(req.requestIdentifier).to.exist;
expect(req.requestIdentifier).to.be.a('string');
});
it('calls next', () => {
const middleware = requireAgain(pathToMiddleWare);
const spy = sinon.spy();
middleware.logRequestData(req, res, spy);
expect(spy.calledOnce).to.be.true;
});
});

View File

@@ -1362,8 +1362,8 @@ describe('Group Model', () => {
sandbox.spy(User, 'updateMany');
});
it('formats message', () => {
const chatMessage = party.sendChat({
it('formats message', async () => {
const chatMessage = await party.sendChat({
message: 'a _new_ message with *markdown*',
user: {
_id: 'user-id',
@@ -1396,8 +1396,8 @@ describe('Group Model', () => {
expect(chat.user).to.eql('user name');
});
it('formats message as system if no user is passed in', () => {
const chat = party.sendChat({ message: 'a system message' });
it('formats message as system if no user is passed in', async () => {
const chat = await party.sendChat({ message: 'a system message' });
expect(chat.text).to.eql('a system message');
expect(validator.isUUID(chat.id)).to.eql(true);
@@ -1411,8 +1411,8 @@ describe('Group Model', () => {
expect(chat.user).to.not.exist;
});
it('updates users about new messages in party', () => {
party.sendChat({ message: 'message' });
it('updates users about new messages in party', async () => {
await party.sendChat({ message: 'message' });
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({
@@ -1421,12 +1421,12 @@ describe('Group Model', () => {
});
});
it('updates users about new messages in group', () => {
it('updates users about new messages in group', async () => {
const group = new Group({
type: 'guild',
});
group.sendChat({ message: 'message' });
await group.sendChat({ message: 'message' });
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({
@@ -1435,8 +1435,8 @@ describe('Group Model', () => {
});
});
it('does not send update to user that sent the message', () => {
party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } });
it('does not send update to user that sent the message', async () => {
await party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } });
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({
@@ -1445,18 +1445,18 @@ describe('Group Model', () => {
});
});
it('skips sending new message notification for guilds with > 5000 members', () => {
it('skips sending new message notification for guilds with > 5000 members', async () => {
party.memberCount = 5001;
party.sendChat({ message: 'message' });
await party.sendChat({ message: 'message' });
expect(User.updateMany).to.not.be.called;
});
it('skips sending messages to the tavern', () => {
it('skips sending messages to the tavern', async () => {
party._id = TAVERN_ID;
party.sendChat({ message: 'message' });
await party.sendChat({ message: 'message' });
expect(User.updateMany).to.not.be.called;
});
@@ -2326,7 +2326,7 @@ describe('Group Model', () => {
await guild.save();
const groupMessage = guild.sendChat({ message: 'Test message.' });
const groupMessage = await guild.sendChat({ message: 'Test message.' });
await groupMessage.save();
await sleep();

View File

@@ -223,4 +223,23 @@ describe('POST /chat/:chatId/flag', () => {
expect(auMessageToCheck).to.not.exist;
});
it('validates that the message belongs to the passed group', async () => {
const { group: anotherGroup, groupLeader: anotherLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Another Guild',
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
});
const message = await anotherUser.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE });
await expect(anotherLeader.post(`/groups/${anotherGroup._id}/chat/${message.message.id}/flag`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageGroupChatNotFound'),
});
});
});

View File

@@ -1,5 +1,6 @@
import { find } from 'lodash';
import {
generateUser,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -79,4 +80,35 @@ describe('POST /chat/:chatId/like', () => {
const messageToCheck = find(groupWithoutChatLikes.chat, { id: message.message.id });
expect(messageToCheck.likes[user._id]).to.equal(false);
});
it('validates that the message belongs to the passed group', async () => {
const { group: anotherGroup, groupLeader: anotherLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Another Guild',
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
});
const message = await anotherUser.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
await expect(anotherLeader.post(`/groups/${anotherGroup._id}/chat/${message.message.id}/like`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageGroupChatNotFound'),
});
});
it('does not like a message if the user is not in the group', async () => {
const thirdUser = await generateUser();
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
await expect(thirdUser.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
});

View File

@@ -22,4 +22,38 @@ describe('GET /content', () => {
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
});
it('does not filter content for regular requests', async () => {
const res = await requester().get('/content');
expect(res).to.have.nested.property('backgrounds.backgrounds062014');
expect(res).to.have.nested.property('gear.tree');
});
it('filters content automatically for iOS requests', async () => {
const res = await requester(null, { 'x-client': 'habitica-ios' }).get('/content');
expect(res).to.have.nested.property('appearances.background.beach');
expect(res).to.not.have.nested.property('backgrounds.backgrounds062014');
expect(res).to.not.have.property('backgroundsFlat');
expect(res).to.not.have.nested.property('gear.tree');
});
it('filters content automatically for Android requests', async () => {
const res = await requester(null, { 'x-client': 'habitica-android' }).get('/content');
expect(res).to.not.have.nested.property('appearances.background.beach');
expect(res).to.have.nested.property('backgrounds.backgrounds062014');
expect(res).to.not.have.property('backgroundsFlat');
expect(res).to.not.have.nested.property('gear.tree');
});
it('filters content if the request specifies a filter', async () => {
const res = await requester().get('/content?filter=backgroundsFlat,gear.flat');
expect(res).to.not.have.property('backgroundsFlat');
expect(res).to.have.nested.property('gear.tree');
expect(res).to.not.have.nested.property('gear.flat');
});
it('filters content if the request contains invalid filters', async () => {
const res = await requester().get('/content?filter=backgroundsFlat,invalid');
expect(res).to.not.have.property('backgroundsFlat');
});
});

View File

@@ -0,0 +1,46 @@
import nconf from 'nconf';
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('GET /debug/time-travel-time', () => {
let user;
let nconfStub;
before(async () => {
user = await generateUser({ permissions: { fullAccess: true } });
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
});
afterEach(() => {
nconfStub.restore();
});
it('returns the shifted time', async () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
const result = await user.get('/debug/time-travel-time');
expect(result.time).to.exist;
await user.post('/debug/jump-time', { disable: true });
});
it('returns shifted when the user is not an admin', async () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
const regularUser = await generateUser();
const result = await regularUser.get('/debug/time-travel-time');
expect(result.time).to.exist;
});
it('returns error when not in time travel mode', async () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
await expect(user.get('/debug/time-travel-time'))
.eventually.be.rejected.and.to.deep.equal({
code: 404,
error: 'NotFound',
message: 'Not found.',
});
});
});

View File

@@ -5,16 +5,23 @@ import {
describe('POST /debug/add-hourglass', () => {
let userToGetHourGlass;
let nconfStub;
before(async () => {
userToGetHourGlass = await generateUser();
});
after(() => {
nconf.set('IS_PROD', false);
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconfStub.restore();
});
it('adds Hourglass to the current user', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
await userToGetHourGlass.post('/debug/add-hourglass');
const userWithHourGlass = await userToGetHourGlass.get('/user');
@@ -23,7 +30,7 @@ describe('POST /debug/add-hourglass', () => {
});
it('returns error when not in production mode', async () => {
nconf.set('IS_PROD', true);
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
await expect(userToGetHourGlass.post('/debug/add-hourglass'))
.eventually.be.rejected.and.to.deep.equal({

View File

@@ -5,16 +5,23 @@ import {
describe('POST /debug/add-ten-gems', () => {
let userToGainTenGems;
let nconfStub;
before(async () => {
userToGainTenGems = await generateUser();
});
after(() => {
nconf.set('IS_PROD', false);
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconfStub.restore();
});
it('adds ten gems to the current user', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
await userToGainTenGems.post('/debug/add-ten-gems');
const userWithTenGems = await userToGainTenGems.get('/user');
@@ -23,7 +30,7 @@ describe('POST /debug/add-ten-gems', () => {
});
it('returns error when not in production mode', async () => {
nconf.set('IS_PROD', true);
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
await expect(userToGainTenGems.post('/debug/add-ten-gems'))
.eventually.be.rejected.and.to.deep.equal({

View File

@@ -0,0 +1,82 @@
import nconf from 'nconf';
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('POST /debug/jump-time', () => {
let user;
let today;
let nconfStub;
before(async () => {
user = await generateUser({ permissions: { fullAccess: true } });
today = new Date();
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
});
afterEach(() => {
nconfStub.restore();
});
after(async () => {
nconf.set('TIME_TRAVEL_ENABLED', true);
await user.post('/debug/jump-time', { disable: true });
nconf.set('TIME_TRAVEL_ENABLED', false);
});
it('Jumps forward', async () => {
const resultDate = new Date((await user.post('/debug/jump-time', { reset: true })).time);
expect(resultDate.getDate()).to.eql(today.getDate());
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 1 })).time);
expect(newResultDate.getDate()).to.eql(today.getDate() + 1);
expect(newResultDate.getMonth()).to.eql(today.getMonth());
expect(newResultDate.getFullYear()).to.eql(today.getFullYear());
});
it('jumps back', async () => {
const resultDate = new Date((await user.post('/debug/jump-time', { reset: true })).time);
expect(resultDate.getDate()).to.eql(today.getDate());
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: -1 })).time);
expect(newResultDate.getDate()).to.eql(today.getDate() - 1);
expect(newResultDate.getMonth()).to.eql(today.getMonth());
expect(newResultDate.getFullYear()).to.eql(today.getFullYear());
});
it('can jump a lot', async () => {
const resultDate = new Date((await user.post('/debug/jump-time', { reset: true })).time);
expect(resultDate.getDate()).to.eql(today.getDate());
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 355 })).time);
expect(newResultDate.getFullYear()).to.eql(today.getFullYear() + 1);
});
it('returns error when the user is not an admin', async () => {
const regularUser = await generateUser();
await expect(regularUser.post('/debug/jump-time', { offsetDays: 1 }))
.eventually.be.rejected.and.to.deep.equal({
code: 400,
error: 'BadRequest',
message: 'You do not have permission to time travel.',
});
});
it('returns error when not in time travel mode', async () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
await expect(user.post('/debug/jump-time', { offsetDays: 1 }))
.eventually.be.rejected.and.to.deep.equal({
code: 404,
error: 'NotFound',
message: 'Not found.',
});
});
});

View File

@@ -5,16 +5,23 @@ import {
describe('POST /debug/make-admin', () => {
let user;
let nconfStub;
before(async () => {
user = await generateUser();
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconf.set('IS_PROD', false);
nconfStub.restore();
});
it('makes user an admin', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
await user.post('/debug/make-admin');
await user.sync();
@@ -23,7 +30,7 @@ describe('POST /debug/make-admin', () => {
});
it('returns error when not in production mode', async () => {
nconf.set('IS_PROD', true);
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
await expect(user.post('/debug/make-admin'))
.eventually.be.rejected.and.to.deep.equal({

View File

@@ -8,6 +8,7 @@ import {
describe('POST /debug/modify-inventory', () => {
let user; let
originalItems;
let nconfStub;
before(async () => {
originalItems = {
@@ -39,8 +40,14 @@ describe('POST /debug/modify-inventory', () => {
});
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconf.set('IS_PROD', false);
nconfStub.restore();
});
it('sets equipment', async () => {
@@ -149,7 +156,7 @@ describe('POST /debug/modify-inventory', () => {
});
it('returns error when not in production mode', async () => {
nconf.set('IS_PROD', true);
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
await expect(user.post('/debug/modify-inventory'))
.eventually.be.rejected.and.to.deep.equal({

View File

@@ -5,13 +5,20 @@ import {
describe('POST /debug/quest-progress', () => {
let user;
let nconfStub;
beforeEach(async () => {
user = await generateUser();
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconf.set('IS_PROD', false);
nconfStub.restore();
});
it('errors if user is not on a quest', async () => {
@@ -48,7 +55,7 @@ describe('POST /debug/quest-progress', () => {
});
it('returns error when not in production mode', async () => {
nconf.set('IS_PROD', true);
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
await expect(user.post('/debug/quest-progress'))
.eventually.be.rejected.and.to.deep.equal({

View File

@@ -5,13 +5,20 @@ import {
describe('POST /debug/set-cron', () => {
let user;
let nconfStub;
before(async () => {
user = await generateUser();
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconf.set('IS_PROD', false);
nconfStub.restore();
});
it('sets last cron', async () => {
@@ -27,7 +34,7 @@ describe('POST /debug/set-cron', () => {
});
it('returns error when not in production mode', async () => {
nconf.set('IS_PROD', true);
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
await expect(user.post('/debug/set-cron'))
.eventually.be.rejected.and.to.deep.equal({

View File

@@ -145,6 +145,18 @@ describe('POST /group', () => {
expect(updatedUser.party._id).to.eql(party._id);
});
it('removes seeking from user', async () => {
await user.updateOne({ 'party.seeking': new Date() });
await user.post('/groups', {
name: partyName,
type: partyType,
});
const updatedUser = await user.get('/user');
expect(updatedUser.party.seeking).to.not.exist;
});
it('does not award Party Up achievement to solo partier', async () => {
await user.post('/groups', {
name: partyName,

View File

@@ -178,6 +178,15 @@ describe('POST /group/:groupId/join', () => {
await expect(invitedUser.get('/user')).to.eventually.not.have.nested.property('invitations.parties[0].id');
});
it('clears party.seeking from user when joining party', async () => {
await invitedUser.updateOne({ 'party.seeking': new Date() });
await invitedUser.post(`/groups/${party._id}/join`);
const updatedUser = await invitedUser.get('/user');
await expect(updatedUser.party.seeking).to.not.exist;
});
it('increments memberCount when joining party', async () => {
const oldMemberCount = party.memberCount;

View File

@@ -17,9 +17,5 @@ describe('GET /shops/backgrounds', () => {
expect(shop.notes).to.eql(t('backgroundShop'));
expect(shop.imageName).to.equal('background_shop');
expect(shop.sets).to.be.an('array');
const sets = shop.sets.map(set => set.identifier);
expect(sets).to.include('incentiveBackgrounds');
expect(sets).to.include('backgrounds062014');
});
});

View File

@@ -5,9 +5,15 @@ import {
describe('GET /shops/time-travelers', () => {
let user;
let clock;
beforeEach(async () => {
user = await generateUser();
clock = sinon.useFakeTimers(new Date('2024-06-08'));
});
afterEach(() => {
clock.restore();
});
it('returns a valid shop object', async () => {

View File

@@ -33,6 +33,20 @@ describe('POST /user/purchase/:type/:key', () => {
expect(user.items[type][key]).to.equal(1);
});
it('purchases animal ears', async () => {
await user.post('/user/purchase/gear/headAccessory_special_tigerEars');
await user.sync();
expect(user.items.gear.owned.headAccessory_special_tigerEars).to.equal(true);
});
it('purchases animal tails', async () => {
await user.post('/user/purchase/gear/back_special_pandaTail');
await user.sync();
expect(user.items.gear.owned.back_special_pandaTail).to.equal(true);
});
it('can convert gold to gems if subscribed', async () => {
const oldBalance = user.balance;
await user.updateOne({

View File

@@ -5,7 +5,7 @@ import {
describe('POST /user/unlock', () => {
let user;
const unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
const unlockPath = 'shirt.convict,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
const unlockCost = 1.25;
const usersStartingGems = 5;

View File

@@ -274,6 +274,14 @@ describe('PUT /user', () => {
expect(get(updatedUser.preferences, type)).to.eql(item);
});
});
it('updates user when background is unequipped', async () => {
expect(get(user.preferences, 'background')).to.not.eql('');
const updatedUser = await user.put('/user', { 'preferences.background': '' });
expect(get(updatedUser.preferences, 'background')).to.eql('');
});
});
context('Improvement Categories', () => {

View File

@@ -108,6 +108,20 @@ describe('PUT /user/auth/update-email', () => {
const isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
expect(isValidPassword).to.equal(true);
});
it('invalidates any outstanding password reset code', async () => {
await user.updateOne({
'auth.local.passwordResetCode': 'impossible-reset-code',
});
await user.put(ENDPOINT, {
newEmail: 'bogo@example.com',
password: oldPassword,
});
await user.sync();
expect(user.auth.local.passwordResetCode).to.not.exist;
});
});
context('Social Login User', async () => {

View File

@@ -11,6 +11,7 @@ const { content } = shared;
describe('POST /user/buy/:key', () => {
let user;
let clock;
beforeEach(async () => {
user = await generateUser({
@@ -18,6 +19,12 @@ describe('POST /user/buy/:key', () => {
});
});
afterEach(() => {
if (clock) {
clock.restore();
}
});
// More tests in common code unit tests
it('returns an error if the item is not found', async () => {
@@ -68,9 +75,9 @@ describe('POST /user/buy/:key', () => {
});
it('buys a special spell', async () => {
clock = sinon.useFakeTimers(new Date('2024-10-31T00:00:00Z'));
const key = 'spookySparkles';
const item = content.special[key];
const stub = sinon.stub(item, 'canOwn').returns(true);
await user.updateOne({ 'stats.gp': 250 });
const res = await user.post(`/user/buy/${key}`);
@@ -83,8 +90,6 @@ describe('POST /user/buy/:key', () => {
expect(res.message).to.equal(t('messageBought', {
itemText: item.text(),
}));
stub.restore();
});
it('allows for bulk purchases', async () => {

View File

@@ -1,5 +1,3 @@
import { TAVERN_ID } from '../../../../../website/server/models/group';
import { updateDocument } from '../../../../helpers/mongo';
import {
requester,
resetHabiticaDB,
@@ -18,7 +16,9 @@ describe('GET /world-state', () => {
});
it('returns Tavern quest data when world boss is active', async () => {
await updateDocument('groups', { _id: TAVERN_ID }, { quest: { active: true, key: 'dysheartener', progress: { hp: 50000, rage: 9999 } } });
sinon.stub(worldState, 'getWorldBoss').returns({
active: true, extra: {}, key: 'dysheartener', progress: { hp: 50000, rage: 9999, collect: {} },
});
const res = await requester().get('/world-state');
expect(res).to.have.nested.property('worldBoss');
@@ -33,15 +33,29 @@ describe('GET /world-state', () => {
rage: 9999,
},
});
worldState.getWorldBoss.restore();
});
it('calls getRepeatingEvents for data', async () => {
const getRepeatingEventsOnDate = sinon.stub(common.content, 'getRepeatingEventsOnDate').returns([]);
const getCurrentGalaEvent = sinon.stub(common.schedule, 'getCurrentGalaEvent').returns({});
await requester().get('/world-state');
expect(getRepeatingEventsOnDate).to.have.been.calledOnce;
expect(getCurrentGalaEvent).to.have.been.calledOnce;
getRepeatingEventsOnDate.restore();
getCurrentGalaEvent.restore();
});
context('no current event', () => {
beforeEach(async () => {
sinon.stub(worldState, 'getCurrentEvent').returns(null);
sinon.stub(worldState, 'getCurrentEventList').returns([]);
});
afterEach(() => {
worldState.getCurrentEvent.restore();
worldState.getCurrentEventList.restore();
});
it('returns null for the current event when there is none active', async () => {
@@ -51,18 +65,18 @@ describe('GET /world-state', () => {
});
});
context('no current event', () => {
context('active event', () => {
const evt = {
...common.content.events.fall2020,
event: 'fall2020',
};
beforeEach(async () => {
sinon.stub(worldState, 'getCurrentEvent').returns(evt);
sinon.stub(worldState, 'getCurrentEventList').returns([evt]);
});
afterEach(() => {
worldState.getCurrentEvent.restore();
worldState.getCurrentEventList.restore();
});
it('returns the current event when there is an active one', async () => {
@@ -71,4 +85,45 @@ describe('GET /world-state', () => {
expect(res.currentEvent).to.eql(evt);
});
});
context('active event with NPC image suffix', () => {
const evt = {
...common.content.events.fall2020,
event: 'fall2020',
npcImageSuffix: 'fall',
};
beforeEach(async () => {
sinon.stub(worldState, 'getCurrentEventList').returns([evt]);
});
afterEach(() => {
worldState.getCurrentEventList.restore();
});
it('returns the NPC image suffix when present', async () => {
const res = await requester().get('/world-state');
expect(res.npcImageSuffix).to.equal('fall');
});
it('returns the NPC image suffix with multiple events present', async () => {
const evt2 = {
...common.content.events.winter2020,
event: 'test',
};
const evt3 = {
...common.content.events.winter2020,
event: 'winter2020',
npcImageSuffix: 'winter',
};
worldState.getCurrentEventList.returns([evt, evt2, evt3]);
const res = await requester().get('/world-state');
expect(res.npcImageSuffix).to.equal('winter');
});
});
});

View File

@@ -0,0 +1,67 @@
/* eslint-disable global-require */
import { expect } from 'chai';
import nconf from 'nconf';
const SWITCHOVER_TIME = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0;
describe('datedMemoize', () => {
it('should return a function that returns a function', () => {
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
const memoized = datedMemoize(() => {});
expect(memoized).to.be.a('function');
});
it('should not call multiple times', () => {
const stub = sandbox.stub().returns({});
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
const memoized = datedMemoize(stub);
memoized(1, 2);
memoized(1, 3);
expect(stub).to.have.been.calledOnce;
});
it('call multiple times for different identifiers', () => {
const stub = sandbox.stub().returns({});
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
const memoized = datedMemoize(stub);
memoized({ identifier: 'a', memoizeConfig: true }, 1, 2);
memoized({ identifier: 'b', memoizeConfig: true }, 1, 2);
expect(stub).to.have.been.calledTwice;
});
it('call once for the same identifier', () => {
const stub = sandbox.stub().returns({});
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
const memoized = datedMemoize(stub);
memoized({ identifier: 'a', memoizeConfig: true }, 1, 2);
memoized({ identifier: 'a', memoizeConfig: true }, 1, 2);
expect(stub).to.have.been.calledOnce;
});
it('call once on the same day', () => {
const stub = sandbox.stub().returns({});
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
const memoized = datedMemoize(stub);
memoized({ date: new Date('2024-01-01'), memoizeConfig: true }, 1, 2);
memoized({ date: new Date('2024-01-01'), memoizeConfig: true }, 1, 2);
expect(stub).to.have.been.calledOnce;
});
it('call multiple times on different days', () => {
const stub = sandbox.stub().returns({});
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
const memoized = datedMemoize(stub);
memoized({ date: new Date('2024-01-01'), memoizeConfig: true }, 1, 2);
memoized({ date: new Date('2024-01-02'), memoizeConfig: true }, 1, 2);
expect(stub).to.have.been.calledTwice;
});
it('respects switchover time', () => {
const stub = sandbox.stub().returns({});
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
const memoized = datedMemoize(stub);
memoized({ date: new Date('2024-01-01T00:00:00.000Z'), memoizeConfig: true }, 1, 2);
memoized({ date: new Date(`2024-01-01T${String(SWITCHOVER_TIME).padStart(2, '0')}`), memoizeConfig: true }, 1, 2);
expect(stub).to.have.been.calledTwice;
});
});

View File

@@ -0,0 +1,123 @@
import {
generateUser,
} from '../../helpers/common.helper';
import cleanupPinnedItems from '../../../website/common/script/libs/cleanupPinnedItems';
describe('cleanupPinnedItems', () => {
let user;
let testPinnedItems;
let clock;
beforeEach(() => {
user = generateUser();
clock = sinon.useFakeTimers(new Date('2024-04-08'));
testPinnedItems = [
{ type: 'armoire', path: 'armoire' },
{ type: 'potion', path: 'potion' },
{ type: 'background', path: 'backgrounds.backgrounds042020.heather_field' },
{ type: 'background', path: 'backgrounds.backgrounds042021.heather_field' },
{ type: 'premiumHatchingPotion', path: 'premiumHatchingPotions.Rainbow' },
{ type: 'premiumHatchingPotion', path: 'premiumHatchingPotions.StainedGlass' },
{ type: 'quests', path: 'quests.rat' },
{ type: 'quests', path: 'quests.spider' },
{ type: 'quests', path: 'quests.moon1' },
{ type: 'quests', path: 'quests.silver' },
{ type: 'marketGear', path: 'gear.flat.head_special_nye2021' },
{ type: 'gear', path: 'gear.flat.armor_special_spring2019Rogue' },
{ type: 'gear', path: 'gear.flat.armor_special_winter2021Rogue' },
{ type: 'mystery_set', path: 'mystery.201804' },
{ type: 'mystery_set', path: 'mystery.201506' },
{ type: 'bundles', path: 'bundles.farmFriends' },
{ type: 'bundles', path: 'bundles.birdBuddies' },
{ type: 'customization', path: 'skin.birdBuddies' },
];
});
afterEach(() => {
clock.restore();
});
it('always keeps armoire and potion', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'armoire')).to.exist;
expect(_.find(result, item => item.path === 'potion')).to.exist;
});
it('removes simple items that are no longer available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'backgrounds.backgrounds042021.heather_field')).to.not.exist;
expect(_.find(result, item => item.path === 'premiumHatchingPotions.Rainbow')).to.not.exist;
});
it('keeps simple items that are still available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'backgrounds.backgrounds042020.heather_field')).to.exist;
expect(_.find(result, item => item.path === 'premiumHatchingPotions.StainedGlass')).to.exist;
});
it('removes gear that is no longer available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'gear.flat.armor_special_winter2021Rogue')).to.not.exist;
});
it('keeps gear that is still available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'gear.flat.armor_special_spring2019Rogue')).to.exist;
});
it('keeps gear that is not seasonal', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'gear.flat.head_special_nye2021')).to.exist;
});
it('removes time traveler gear that is no longer available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'mystery.201506')).to.not.exist;
});
it('keeps time traveler gear that is still available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'mystery.201804')).to.exist;
});
it('removes quests that are no longer available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'quests.rat')).to.not.exist;
expect(_.find(result, item => item.path === 'quests.silver')).to.not.exist;
});
it('keeps quests that are still available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'quests.spider')).to.exist;
});
it('keeps quests that are not seasonal', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'quests.moon1')).to.exist;
});
it('removes bundles that are no longer available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'bundles.farmFriends')).to.not.exist;
});
it('keeps bundles that are still available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'bundles.birdBuddies')).to.exist;
});
});

View File

@@ -1,219 +0,0 @@
import shared from '../../../website/common';
import {
generateUser,
} from '../../helpers/common.helper';
describe('shops', () => {
const user = generateUser();
describe('market', () => {
const shopCategories = shared.shops.getMarketCategories(user);
it('contains at least the 3 default categories', () => {
expect(shopCategories.length).to.be.greaterThan(2);
});
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'class'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
});
});
it('shows relevant non class gear in special category', () => {
const contributor = generateUser({
contributor: {
level: 7,
critical: true,
},
items: {
gear: {
owned: {
weapon_armoire_basicCrossbow: true, // eslint-disable-line camelcase
},
},
},
});
const gearCategories = shared.shops.getMarketGearCategories(contributor);
const specialCategory = gearCategories.find(o => o.identifier === 'none');
expect(specialCategory.items.find(item => item.key === 'weapon_special_1'));
expect(specialCategory.items.find(item => item.key === 'armor_special_1'));
expect(specialCategory.items.find(item => item.key === 'head_special_1'));
expect(specialCategory.items.find(item => item.key === 'shield_special_1'));
expect(specialCategory.items.find(item => item.key === 'weapon_special_critical'));
expect(specialCategory.items.find(item => item.key === 'weapon_armoire_basicCrossbow'));// eslint-disable-line camelcase
});
it('does not show gear when it is all owned', () => {
const userWithItems = generateUser({
stats: {
class: 'wizard',
},
items: {
gear: {
owned: {
weapon_wizard_0: true, // eslint-disable-line camelcase
weapon_wizard_1: true, // eslint-disable-line camelcase
weapon_wizard_2: true, // eslint-disable-line camelcase
weapon_wizard_3: true, // eslint-disable-line camelcase
weapon_wizard_4: true, // eslint-disable-line camelcase
weapon_wizard_5: true, // eslint-disable-line camelcase
weapon_wizard_6: true, // eslint-disable-line camelcase
armor_wizard_1: true, // eslint-disable-line camelcase
armor_wizard_2: true, // eslint-disable-line camelcase
armor_wizard_3: true, // eslint-disable-line camelcase
armor_wizard_4: true, // eslint-disable-line camelcase
armor_wizard_5: true, // eslint-disable-line camelcase
head_wizard_1: true, // eslint-disable-line camelcase
head_wizard_2: true, // eslint-disable-line camelcase
head_wizard_3: true, // eslint-disable-line camelcase
head_wizard_4: true, // eslint-disable-line camelcase
head_wizard_5: true, // eslint-disable-line camelcase
},
},
},
});
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
expect(shopWizardItems.length).to.eql(0);
});
it('shows available gear not yet purchased and previously owned', () => {
const userWithItems = generateUser({
stats: {
class: 'wizard',
},
items: {
gear: {
owned: {
weapon_wizard_0: true, // eslint-disable-line camelcase
weapon_wizard_1: true, // eslint-disable-line camelcase
weapon_wizard_2: true, // eslint-disable-line camelcase
weapon_wizard_3: true, // eslint-disable-line camelcase
weapon_wizard_4: true, // eslint-disable-line camelcase
armor_wizard_1: true, // eslint-disable-line camelcase
armor_wizard_2: true, // eslint-disable-line camelcase
armor_wizard_3: false, // eslint-disable-line camelcase
armor_wizard_4: false, // eslint-disable-line camelcase
head_wizard_1: true, // eslint-disable-line camelcase
head_wizard_2: false, // eslint-disable-line camelcase
head_wizard_3: true, // eslint-disable-line camelcase
head_wizard_4: false, // eslint-disable-line camelcase
head_wizard_5: true, // eslint-disable-line camelcase
},
},
},
});
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_5').locked).to.eql(false);
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_6').locked).to.eql(true);
expect(shopWizardItems.find(item => item.key === 'armor_wizard_3').locked).to.eql(false);
expect(shopWizardItems.find(item => item.key === 'armor_wizard_4').locked).to.eql(true);
expect(shopWizardItems.find(item => item.key === 'head_wizard_2').locked).to.eql(false);
expect(shopWizardItems.find(item => item.key === 'head_wizard_4').locked).to.eql(true);
});
});
describe('questShop', () => {
const shopCategories = shared.shops.getQuestShopCategories(user);
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
if (category.identifier === 'bundle') {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'purchaseType', 'class'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
} else {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
}
});
});
});
describe('timeTravelers', () => {
const shopCategories = shared.shops.getTimeTravelersCategories(user);
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
_.each(category.items, item => {
_.each(['key', 'text', 'value', 'currency', 'locked', 'purchaseType', 'class', 'notes', 'class'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
});
});
});
describe('seasonalShop', () => {
const shopCategories = shared.shops.getSeasonalShopCategories(user);
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'type'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
});
});
});
});

View File

@@ -0,0 +1,430 @@
import shared from '../../../website/common';
import {
generateUser,
} from '../../helpers/common.helper';
import seasonalConfig from '../../../website/common/script/libs/shops-seasonal.config';
describe('shops', () => {
const user = generateUser();
let clock;
afterEach(() => {
if (clock) {
clock.restore();
}
user.achievements.quests = {};
});
describe('market', () => {
const shopCategories = shared.shops.getMarketCategories(user);
it('contains at least the 3 default categories', () => {
expect(shopCategories.length).to.be.greaterThan(2);
});
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'class'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
});
});
describe('premium hatching potions', () => {
it('contains current scheduled premium hatching potions', async () => {
clock = sinon.useFakeTimers(new Date('2024-04-01'));
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.length).to.eql(2);
});
it('does not contain past scheduled premium hatching potions', async () => {
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.filter(x => x.key === 'Aquatic' || x.key === 'Celestial').length).to.eql(0);
});
it('returns end date for scheduled premium potions', async () => {
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
potions.items.forEach(potion => {
expect(potion.end).to.exist;
});
});
it('contains unlocked quest premium hatching potions', async () => {
user.achievements.quests = {
bronze: 1,
blackPearl: 1,
};
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').length).to.eql(2);
});
it('does not contain locked quest premium hatching potions', async () => {
clock = sinon.useFakeTimers(new Date('2024-04-01'));
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.length).to.eql(2);
expect(potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').length).to.eql(0);
});
it('does not return end date for quest premium potions', async () => {
user.achievements.quests = {
bronze: 1,
blackPearl: 1,
};
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').forEach(potion => {
expect(potion.end).to.not.exist;
});
});
});
it('does not return items with event data', async () => {
shopCategories.forEach(category => {
category.items.forEach(item => {
expect(item.event).to.not.exist;
expect(item.season).to.not.exist;
});
});
});
it('shows relevant non class gear in special category', () => {
const contributor = generateUser({
contributor: {
level: 7,
critical: true,
},
items: {
gear: {
owned: {
weapon_armoire_basicCrossbow: true, // eslint-disable-line camelcase
},
},
},
});
const gearCategories = shared.shops.getMarketGearCategories(contributor);
const specialCategory = gearCategories.find(o => o.identifier === 'none');
expect(specialCategory.items.find(item => item.key === 'weapon_special_1'), 'weapon_special_1');
expect(specialCategory.items.find(item => item.key === 'armor_special_1'), 'armor_special_1');
expect(specialCategory.items.find(item => item.key === 'head_special_1'), 'head_special_1');
expect(specialCategory.items.find(item => item.key === 'shield_special_1'), 'shield_special_1');
expect(specialCategory.items.find(item => item.key === 'weapon_special_critical'), 'weapon_special_critical');
expect(specialCategory.items.find(item => item.key === 'weapon_armoire_basicCrossbow'), 'weapon_armoire_basicCrossbow');// eslint-disable-line camelcase
});
describe('handles seasonal gear', () => {
beforeEach(() => {
clock = sinon.useFakeTimers(new Date('2024-04-01'));
});
it('shows current seasonal gear for warriors', () => {
const warriorItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'warrior').items.filter(x => x.key.indexOf('spring2024') !== -1);
expect(warriorItems.length, 'Warrior seasonal gear').to.eql(4);
});
it('shows current seasonal gear for mages', () => {
const mageItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'wizard').items.filter(x => x.key.indexOf('spring2024') !== -1);
expect(mageItems.length, 'Mage seasonal gear').to.eql(3);
});
it('shows current seasonal gear for healers', () => {
const healerItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'healer').items.filter(x => x.key.indexOf('spring2024') !== -1);
expect(healerItems.length, 'Healer seasonal gear').to.eql(4);
});
it('shows current seasonal gear for rogues', () => {
const rogueItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'rogue').items.filter(x => x.key.indexOf('spring2024') !== -1);
expect(rogueItems.length, 'Rogue seasonal gear').to.eql(4);
});
it('seasonal gear contains end date', () => {
const categories = shared.shops.getMarketGearCategories(user);
categories.forEach(category => {
category.items.filter(x => x.key.indexOf('spring2024') !== -1).forEach(item => {
expect(item.end, item.key).to.exist;
});
});
});
it('only shows gear for the current season', () => {
const categories = shared.shops.getMarketGearCategories(user);
categories.forEach(category => {
const otherSeasons = category.items.filter(item => item.key.indexOf('winter') !== -1 || item.key.indexOf('summer') !== -1 || item.key.indexOf('fall') !== -1);
expect(otherSeasons.length).to.eql(0);
});
});
it('does not show gear from past seasons', () => {
const categories = shared.shops.getMarketGearCategories(user);
categories.forEach(category => {
const otherYears = category.items.filter(item => item.key.indexOf('spring') !== -1 && item.key.indexOf('2024') === -1);
expect(otherYears.length).to.eql(0);
});
});
});
it('does not show gear when it is all owned', () => {
const userWithItems = generateUser({
stats: {
class: 'wizard',
},
items: {
gear: {
owned: {
weapon_wizard_0: true, // eslint-disable-line camelcase
weapon_wizard_1: true, // eslint-disable-line camelcase
weapon_wizard_2: true, // eslint-disable-line camelcase
weapon_wizard_3: true, // eslint-disable-line camelcase
weapon_wizard_4: true, // eslint-disable-line camelcase
weapon_wizard_5: true, // eslint-disable-line camelcase
weapon_wizard_6: true, // eslint-disable-line camelcase
armor_wizard_1: true, // eslint-disable-line camelcase
armor_wizard_2: true, // eslint-disable-line camelcase
armor_wizard_3: true, // eslint-disable-line camelcase
armor_wizard_4: true, // eslint-disable-line camelcase
armor_wizard_5: true, // eslint-disable-line camelcase
head_wizard_1: true, // eslint-disable-line camelcase
head_wizard_2: true, // eslint-disable-line camelcase
head_wizard_3: true, // eslint-disable-line camelcase
head_wizard_4: true, // eslint-disable-line camelcase
head_wizard_5: true, // eslint-disable-line camelcase
},
},
},
});
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
expect(shopWizardItems.length).to.eql(0);
});
it('shows available gear not yet purchased and previously owned', () => {
const userWithItems = generateUser({
stats: {
class: 'wizard',
},
items: {
gear: {
owned: {
weapon_wizard_0: true, // eslint-disable-line camelcase
weapon_wizard_1: true, // eslint-disable-line camelcase
weapon_wizard_2: true, // eslint-disable-line camelcase
weapon_wizard_3: true, // eslint-disable-line camelcase
weapon_wizard_4: true, // eslint-disable-line camelcase
armor_wizard_1: true, // eslint-disable-line camelcase
armor_wizard_2: true, // eslint-disable-line camelcase
armor_wizard_3: false, // eslint-disable-line camelcase
armor_wizard_4: false, // eslint-disable-line camelcase
head_wizard_1: true, // eslint-disable-line camelcase
head_wizard_2: false, // eslint-disable-line camelcase
head_wizard_3: true, // eslint-disable-line camelcase
head_wizard_4: false, // eslint-disable-line camelcase
head_wizard_5: true, // eslint-disable-line camelcase
},
},
},
});
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_5').locked).to.eql(false);
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_6').locked).to.eql(true);
expect(shopWizardItems.find(item => item.key === 'armor_wizard_3').locked).to.eql(false);
expect(shopWizardItems.find(item => item.key === 'armor_wizard_4').locked).to.eql(true);
expect(shopWizardItems.find(item => item.key === 'head_wizard_2').locked).to.eql(false);
expect(shopWizardItems.find(item => item.key === 'head_wizard_4').locked).to.eql(true);
});
});
describe('questShop', () => {
const shopCategories = shared.shops.getQuestShopCategories(user);
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length, category.identifier).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
if (category.identifier === 'bundle') {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'purchaseType', 'class'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
} else {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
}
});
});
it('does not return items with event data', async () => {
shopCategories.forEach(category => {
category.items.forEach(item => {
expect(item.event).to.not.exist;
});
});
});
});
describe('timeTravelers', () => {
const shopCategories = shared.shops.getTimeTravelersCategories(user);
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
_.each(category.items, item => {
_.each(['key', 'text', 'value', 'currency', 'locked', 'purchaseType', 'class', 'notes', 'class'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
});
});
it('does not return items with event data', async () => {
shopCategories.forEach(category => {
category.items.forEach(item => {
expect(item.event).to.not.exist;
});
});
});
it('returns pets', () => {
const pets = shopCategories.find(cat => cat.identifier === 'pets').items;
expect(pets.length).to.be.greaterThan(0);
});
it('returns mounts', () => {
const mounts = shopCategories.find(cat => cat.identifier === 'mounts').items;
expect(mounts.length).to.be.greaterThan(0);
});
it('returns quests', () => {
const quests = shopCategories.find(cat => cat.identifier === 'quests').items;
expect(quests.length).to.be.greaterThan(0);
});
it('returns backgrounds', () => {
const backgrounds = shopCategories.find(cat => cat.identifier === 'backgrounds').items;
expect(backgrounds.length).to.be.greaterThan(0);
});
});
describe('customizationShop', () => {
const shopCategories = shared.shops.getCustomizationsShopCategories(user, null);
it('does not return items with event data', async () => {
shopCategories.forEach(category => {
category.items.forEach(item => {
expect(item.event, item.key).to.not.exist;
});
});
});
it('backgrounds category contains end date', () => {
const backgroundCategory = shopCategories.find(cat => cat.identifier === 'backgrounds');
expect(backgroundCategory.end).to.exist;
expect(backgroundCategory.end).to.be.greaterThan(new Date());
});
it('hair color category contains end date', () => {
const colorCategory = shopCategories.find(cat => cat.identifier === 'color');
expect(colorCategory.end).to.exist;
expect(colorCategory.end).to.be.greaterThan(new Date());
});
it('skin category contains end date', () => {
const colorCategory = shopCategories.find(cat => cat.identifier === 'color');
expect(colorCategory.end).to.exist;
expect(colorCategory.end).to.be.greaterThan(new Date());
});
});
describe('seasonalShop', () => {
const shopCategories = shared.shops.getSeasonalShopCategories(user, null, seasonalConfig());
const today = new Date();
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('does not return items with event data', async () => {
shopCategories.forEach(category => {
category.items.forEach(item => {
expect(item.event, item.key).to.not.exist;
});
});
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'type'], key => {
expect(_.has(item, key), item.key).to.eql(true);
});
});
});
});
it('items have a valid end date', () => {
shopCategories.forEach(category => {
category.items.forEach(item => {
expect(item.end, item.key).to.be.a('date');
expect(item.end, item.key).to.be.greaterThan(today);
});
});
});
it('items match current season', () => {
const currentSeason = seasonalConfig().currentSeason.toLowerCase();
shopCategories.forEach(category => {
category.items.forEach(item => {
if (item.klass === 'special') {
expect(item.season, item.key).to.eql(currentSeason);
}
});
});
});
});
});

View File

@@ -1,11 +1,10 @@
import * as armoireSet from '../../../website/common/script/content/gear/sets/armoire';
import armoireSet from '../../../website/common/script/content/gear/sets/armoire';
describe('armoireSet items', () => {
it('checks if canOwn has the same id', () => {
Object.keys(armoireSet).forEach(type => {
Object.keys(armoireSet[type]).forEach(itemKey => {
const ownedKey = `${type}_armoire_${itemKey}`;
expect(armoireSet[type][itemKey].canOwn({
items: {
gear: {

View File

@@ -49,7 +49,7 @@ describe('shared.ops.buy', () => {
}
});
it('recovers 15 hp', async () => {
it('buys health potion', async () => {
user.stats.hp = 30;
await buy(user, { params: { key: 'potion' } }, analytics);
expect(user.stats.hp).to.eql(45);

View File

@@ -17,9 +17,7 @@ function getFullArmoire () {
_.each(content.gearTypes, type => {
_.each(content.gear.tree[type].armoire, gearObject => {
if (gearObject.released) {
fullArmoire[gearObject.key] = true;
}
fullArmoire[gearObject.key] = true;
});
});

View File

@@ -22,6 +22,7 @@ async function buyGear (user, req, analytics) {
describe('shared.ops.buyMarketGear', () => {
let user;
const analytics = { track () {} };
let clock;
beforeEach(() => {
user = generateUser({
@@ -54,6 +55,10 @@ describe('shared.ops.buyMarketGear', () => {
shared.fns.predictableRandom.restore();
shared.onboarding.checkOnboardingStatus.restore();
analytics.track.restore();
if (clock) {
clock.restore();
}
});
context('Gear', () => {
@@ -184,30 +189,28 @@ describe('shared.ops.buyMarketGear', () => {
});
// TODO after user.ops.equip is done
xit('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', async () => {
it('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', async () => {
user.stats.gp = 100;
user.preferences.autoEquip = true;
await buyGear(user, { params: { key: 'shield_warrior_1' } });
user.ops.equip({ params: { key: 'shield_warrior_1' } });
await buyGear(user, { params: { key: 'weapon_warrior_1' } });
user.ops.equip({ params: { key: 'weapon_warrior_1' } });
user.items.gear.equipped.weapon = 'weapon_warrior_1';
user.items.gear.equipped.shield = 'shield_warrior_1';
user.stats.class = 'wizard';
await buyGear(user, { params: { key: 'weapon_wizard_1' } });
await buyGear(user, { params: { key: 'weapon_wizard_0' } });
expect(user.items.gear.equipped).to.have.property('shield', 'shield_base_0');
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_wizard_1');
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_wizard_0');
});
// TODO after user.ops.equip is done
xit('buyGears two-handed equipment but does not automatically remove sword or shield', async () => {
it('buyGears two-handed equipment but does not automatically remove sword or shield', async () => {
user.stats.gp = 100;
user.preferences.autoEquip = false;
await buyGear(user, { params: { key: 'shield_warrior_1' } });
user.ops.equip({ params: { key: 'shield_warrior_1' } });
await buyGear(user, { params: { key: 'weapon_warrior_1' } });
user.ops.equip({ params: { key: 'weapon_warrior_1' } });
user.items.gear.equipped.weapon = 'weapon_warrior_1';
user.items.gear.equipped.shield = 'shield_warrior_1';
user.stats.class = 'wizard';
await buyGear(user, { params: { key: 'weapon_wizard_1' } });
await buyGear(user, { params: { key: 'weapon_wizard_0' } });
expect(user.items.gear.equipped).to.have.property('shield', 'shield_warrior_1');
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_warrior_1');
@@ -283,5 +286,40 @@ describe('shared.ops.buyMarketGear', () => {
expect(user.items.gear.owned).to.have.property('shield_armoire_ramHornShield', true);
});
it('buys current seasonal gear', async () => {
user.stats.gp = 200;
clock = sinon.useFakeTimers(new Date('2024-01-01'));
await buyGear(user, { params: { key: 'armor_special_winter2024Warrior' } });
expect(user.items.gear.owned).to.have.property('armor_special_winter2024Warrior', true);
});
it('errors when buying past seasonal gear', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-01'));
user.stats.gp = 200;
try {
await buyGear(user, { params: { key: 'armor_special_winter2023Warrior' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notAvailable'));
expect(user.items.gear.owned).to.not.have.property('armor_special_winter2023Warrior');
}
});
it('errors when buying gear from wrong season', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-01'));
user.stats.gp = 200;
try {
await buyGear(user, { params: { key: 'weapon_special_spring2024Warrior' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notAvailable'));
expect(user.items.gear.owned).to.not.have.property('weapon_special_spring2024Warrior');
}
});
});
});

View File

@@ -15,6 +15,7 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buyMysterySet', () => {
let user;
const analytics = { track () {} };
let clock;
beforeEach(() => {
user = generateUser({
@@ -31,6 +32,9 @@ describe('shared.ops.buyMysterySet', () => {
afterEach(() => {
analytics.track.restore();
if (clock) {
clock.restore();
}
});
context('Mystery Sets', () => {
@@ -41,7 +45,7 @@ describe('shared.ops.buyMysterySet', () => {
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.eql(i18n.t('notEnoughHourglasses'));
expect(user.items.gear.owned).to.have.property('weapon_warrior_0', true);
expect(user.items.gear.owned).to.not.have.property('armor_mystery_201501');
}
});
@@ -72,6 +76,18 @@ describe('shared.ops.buyMysterySet', () => {
expect(err.message).to.equal(errorMessage('missingKeyParam'));
}
});
it('returns error if the set is not available', async () => {
user.purchased.plan.consecutive.trinkets = 1;
clock = sinon.useFakeTimers(new Date('2024-01-16'));
try {
await buyMysterySet(user, { params: { key: '201501' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.eql(i18n.t('notAvailable'));
expect(user.items.gear.owned).to.not.have.property('armor_mystery_201501');
}
});
});
context('successful purchases', () => {
@@ -86,6 +102,16 @@ describe('shared.ops.buyMysterySet', () => {
expect(user.items.gear.owned).to.have.property('head_mystery_301404', true);
expect(user.items.gear.owned).to.have.property('eyewear_mystery_301404', true);
});
it('buys mystery set if it is available', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-16'));
user.purchased.plan.consecutive.trinkets = 1;
await buyMysterySet(user, { params: { key: '201601' } }, analytics);
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
expect(user.items.gear.owned).to.have.property('shield_mystery_201601', true);
expect(user.items.gear.owned).to.have.property('head_mystery_201601', true);
});
});
});
});

View File

@@ -10,6 +10,7 @@ import { BuyQuestWithGemOperation } from '../../../../website/common/script/ops/
describe('shared.ops.buyQuestGems', () => {
let user;
let clock;
const goldPoints = 40;
const analytics = { track () {} };
@@ -26,11 +27,13 @@ describe('shared.ops.buyQuestGems', () => {
beforeEach(() => {
sinon.stub(analytics, 'track');
sinon.spy(pinnedGearUtils, 'removeItemByPath');
clock = sinon.useFakeTimers(new Date('2024-01-16'));
});
afterEach(() => {
analytics.track.restore();
pinnedGearUtils.removeItemByPath.restore();
clock.restore();
});
context('single purchase', () => {
@@ -44,7 +47,7 @@ describe('shared.ops.buyQuestGems', () => {
user.pinnedItems.push({ type: 'quests', key: 'gryphon' });
});
it('successfully purchases quest', async () => {
it('successfully purchases pet quest', async () => {
const key = 'gryphon';
await buyQuest(user, { params: { key } });
@@ -52,6 +55,61 @@ describe('shared.ops.buyQuestGems', () => {
expect(user.items.quests[key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('successfully purchases hatching potion quest', async () => {
const key = 'silver';
await buyQuest(user, { params: { key } });
expect(user.items.quests[key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('successfully purchases seasonal quest', async () => {
const key = 'evilsanta';
await buyQuest(user, { params: { key } });
expect(user.items.quests[key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('errors if the pet quest is not available', async () => {
const key = 'sabretooth';
try {
await buyQuest(user, { params: { key } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notAvailable'));
expect(user.items.quests[key]).to.eql(undefined);
}
});
it('errors if the hatching potion quest is not available', async () => {
const key = 'ruby';
try {
await buyQuest(user, { params: { key } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notAvailable'));
expect(user.items.quests[key]).to.eql(undefined);
}
});
it('errors if the seasonal quest is not available', async () => {
const key = 'egg';
try {
await buyQuest(user, { params: { key } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notAvailable'));
expect(user.items.quests[key]).to.eql(undefined);
}
});
it('if a user\'s count of a quest scroll is negative, it will be reset to 0 before incrementing when they buy a new one.', async () => {
const key = 'dustbunnies';
user.items.quests[key] = -1;
@@ -61,6 +119,7 @@ describe('shared.ops.buyQuestGems', () => {
expect(user.items.quests[key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('errors if the user has not completed prerequisite quests', async () => {
const key = 'atom3';
user.achievements.quests.atom1 = 1;
@@ -73,6 +132,7 @@ describe('shared.ops.buyQuestGems', () => {
expect(user.items.quests[key]).to.eql(undefined);
}
});
it('successfully purchases quest if user has completed all prerequisite quests', async () => {
const key = 'atom3';
user.achievements.quests.atom1 = 1;

View File

@@ -1,89 +0,0 @@
import { BuySpellOperation } from '../../../../website/common/script/ops/buy/buySpell';
import {
BadRequest,
NotFound,
NotAuthorized,
} from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n';
import {
generateUser,
} from '../../../helpers/common.helper';
import content from '../../../../website/common/script/content/index';
import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
describe('shared.ops.buySpecialSpell', () => {
let user;
const analytics = { track () {} };
async function buySpecialSpell (_user, _req, _analytics) {
const buyOp = new BuySpellOperation(_user, _req, _analytics);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
it('throws an error if params.key is missing', async () => {
try {
await buySpecialSpell(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(errorMessage('missingKeyParam'));
}
});
it('throws an error if the spell doesn\'t exists', async () => {
try {
await buySpecialSpell(user, {
params: {
key: 'notExisting',
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotFound);
expect(err.message).to.equal(errorMessage('spellNotFound', { spellId: 'notExisting' }));
}
});
it('throws an error if the user doesn\'t have enough gold', async () => {
user.stats.gp = 1;
try {
await buySpecialSpell(user, {
params: {
key: 'thankyou',
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
}
});
it('buys an item', async () => {
user.stats.gp = 11;
const item = content.special.thankyou;
const [data, message] = await buySpecialSpell(user, {
params: {
key: 'thankyou',
},
}, analytics);
expect(user.stats.gp).to.equal(1);
expect(user.items.special.thankyou).to.equal(1);
expect(data).to.eql({
items: user.items,
stats: user.stats,
});
expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(),
}));
expect(analytics.track).to.be.calledOnce;
});
});

View File

@@ -0,0 +1,172 @@
import { BuySpellOperation } from '../../../../website/common/script/ops/buy/buySpell';
import {
BadRequest,
NotFound,
NotAuthorized,
} from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n';
import {
generateUser,
} from '../../../helpers/common.helper';
import content from '../../../../website/common/script/content/index';
import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
describe('shared.ops.buySpecialSpell', () => {
let user;
let clock;
const analytics = { track () {} };
async function buySpecialSpell (_user, _req, _analytics) {
const buyOp = new BuySpellOperation(_user, _req, _analytics);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
if (clock) {
clock.restore();
}
});
it('throws an error if params.key is missing', async () => {
try {
await buySpecialSpell(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(errorMessage('missingKeyParam'));
}
});
it('throws an error if the item doesn\'t exists', async () => {
try {
await buySpecialSpell(user, {
params: {
key: 'notExisting',
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotFound);
expect(err.message).to.equal(errorMessage('spellNotFound', { spellId: 'notExisting' }));
}
});
it('throws an error if the user doesn\'t have enough gold', async () => {
user.stats.gp = 1;
try {
await buySpecialSpell(user, {
params: {
key: 'thankyou',
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
}
});
describe('buying cards', () => {
it('buys a card that is always available', async () => {
user.stats.gp = 11;
const item = content.special.thankyou;
const [data, message] = await buySpecialSpell(user, {
params: {
key: 'thankyou',
},
}, analytics);
expect(user.stats.gp).to.equal(1);
expect(user.items.special.thankyou).to.equal(1);
expect(data).to.eql({
items: user.items,
stats: user.stats,
});
expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(),
}));
expect(analytics.track).to.be.calledOnce;
});
it('buys a limited card when it is available', async () => {
user.stats.gp = 11;
const item = content.special.nye;
clock = sinon.useFakeTimers(new Date('2024-01-01'));
const [data, message] = await buySpecialSpell(user, {
params: {
key: 'nye',
},
}, analytics);
expect(user.stats.gp).to.equal(1);
expect(user.items.special.nye).to.equal(1);
expect(data).to.eql({
items: user.items,
stats: user.stats,
});
expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(),
}));
expect(analytics.track).to.be.calledOnce;
});
it('throws an error if the card is not currently available', async () => {
user.stats.gp = 11;
clock = sinon.useFakeTimers(new Date('2024-06-01'));
try {
await buySpecialSpell(user, {
params: {
key: 'nye',
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('cannotBuyItem'));
}
});
});
describe('buying spells', () => {
it('buys a spell if it is currently available', async () => {
user.stats.gp = 16;
clock = sinon.useFakeTimers(new Date('2024-06-22'));
const item = content.special.seafoam;
const [data, message] = await buySpecialSpell(user, {
params: {
key: 'seafoam',
},
}, analytics);
expect(user.stats.gp).to.equal(1);
expect(user.items.special.seafoam).to.equal(1);
expect(data).to.eql({
items: user.items,
stats: user.stats,
});
expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(),
}));
expect(analytics.track).to.be.calledOnce;
});
it('throws an error if the spell is not currently available', async () => {
user.stats.gp = 50;
clock = sinon.useFakeTimers(new Date('2024-01-22'));
try {
await buySpecialSpell(user, {
params: {
key: 'seafoam',
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('cannotBuyItem'));
}
});
});
});

View File

@@ -15,6 +15,7 @@ import {
describe('shared.ops.purchase', () => {
const SEASONAL_FOOD = moment().isBefore('2021-11-02T20:00-04:00') ? 'Candy_Base' : 'Meat';
let user;
let clock;
const goldPoints = 40;
const analytics = { track () {} };
@@ -25,11 +26,13 @@ describe('shared.ops.purchase', () => {
beforeEach(() => {
sinon.stub(analytics, 'track');
sinon.spy(pinnedGearUtils, 'removeItemByPath');
clock = sandbox.useFakeTimers(new Date('2024-01-10'));
});
afterEach(() => {
analytics.track.restore();
pinnedGearUtils.removeItemByPath.restore();
clock.restore();
});
context('failure conditions', () => {
@@ -82,13 +85,77 @@ describe('shared.ops.purchase', () => {
it('returns error when user does not have enough gems to buy an item', async () => {
try {
await purchase(user, { params: { type: 'gear', key: 'headAccessory_special_wolfEars' } });
await purchase(user, { params: { type: 'gear', key: 'shield_special_winter2019Healer' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notEnoughGems'));
}
});
it('returns error when gear is not available', async () => {
try {
await purchase(user, { params: { type: 'gear', key: 'shield_special_spring2019Healer' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
}
});
it('returns an error when purchasing current seasonal gear', async () => {
user.balance = 2;
try {
await purchase(user, { params: { type: 'gear', key: 'shield_special_winter2024Healer' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
}
});
it('returns error when hatching potion is not available', async () => {
try {
await purchase(user, { params: { type: 'hatchingPotions', key: 'PolkaDot' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
}
});
it('returns error when quest for hatching potion was not yet completed', async () => {
try {
await purchase(user, { params: { type: 'hatchingPotions', key: 'BlackPearl' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
}
});
it('returns error when quest for egg was not yet completed', async () => {
try {
await purchase(user, { params: { type: 'eggs', key: 'Octopus' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
}
});
it('returns error when bundle is not available', async () => {
try {
await purchase(user, { params: { type: 'bundles', key: 'forestFriends' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notAvailable'));
}
});
it('returns error when gear is not gem purchasable', async () => {
try {
await purchase(user, { params: { type: 'gear', key: 'shield_healer_3' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
}
});
it('returns error when item is not found', async () => {
const params = { key: 'notExisting', type: 'food' };
@@ -99,44 +166,6 @@ describe('shared.ops.purchase', () => {
expect(err.message).to.equal(i18n.t('contentKeyNotFound', params));
}
});
it('returns error when user supplies a non-numeric quantity', async () => {
const type = 'eggs';
const key = 'Wolf';
try {
await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
}
});
it('returns error when user supplies a negative quantity', async () => {
const type = 'eggs';
const key = 'Wolf';
user.balance = 10;
try {
await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
}
});
it('returns error when user supplies a decimal quantity', async () => {
const type = 'eggs';
const key = 'Wolf';
user.balance = 10;
try {
await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
}
});
});
context('successful purchase', () => {
@@ -150,7 +179,7 @@ describe('shared.ops.purchase', () => {
user.pinnedItems.push({ type: 'eggs', key: 'Wolf' });
user.pinnedItems.push({ type: 'hatchingPotions', key: 'Base' });
user.pinnedItems.push({ type: 'food', key: SEASONAL_FOOD });
user.pinnedItems.push({ type: 'gear', key: 'headAccessory_special_tigerEars' });
user.pinnedItems.push({ type: 'gear', key: 'shield_special_winter2019Healer' });
user.pinnedItems.push({ type: 'bundles', key: 'featheredFriends' });
});
@@ -185,9 +214,9 @@ describe('shared.ops.purchase', () => {
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('purchases gear', async () => {
it('purchases past seasonal gear', async () => {
const type = 'gear';
const key = 'headAccessory_special_tigerEars';
const key = 'shield_special_winter2019Healer';
await purchase(user, { params: { type, key } });
@@ -195,9 +224,39 @@ describe('shared.ops.purchase', () => {
expect(pinnedGearUtils.removeItemByPath.calledOnce).to.equal(true);
});
it('purchases hatching potion', async () => {
const type = 'hatchingPotions';
const key = 'Peppermint';
await purchase(user, { params: { type, key } });
expect(user.items.hatchingPotions[key]).to.eql(1);
});
it('purchases hatching potion if user completed quest', async () => {
const type = 'hatchingPotions';
const key = 'Bronze';
user.achievements.quests.bronze = 1;
await purchase(user, { params: { type, key } });
expect(user.items.hatchingPotions[key]).to.eql(1);
});
it('purchases egg if user completed quest', async () => {
const type = 'eggs';
const key = 'Deer';
user.achievements.quests.ghost_stag = 1;
await purchase(user, { params: { type, key } });
expect(user.items.eggs[key]).to.eql(1);
});
it('purchases quest bundles', async () => {
const startingBalance = user.balance;
const clock = sandbox.useFakeTimers(moment('2024-03-20').valueOf());
clock.restore();
clock = sandbox.useFakeTimers(moment('2022-03-10').valueOf());
const type = 'bundles';
const key = 'cuddleBuddies';
const price = 1.75;
@@ -216,7 +275,6 @@ describe('shared.ops.purchase', () => {
expect(user.balance).to.equal(startingBalance - price);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
clock.restore();
});
});
@@ -257,5 +315,43 @@ describe('shared.ops.purchase', () => {
expect(user.items[type][key]).to.equal(2);
});
it('returns error when user supplies a non-numeric quantity', async () => {
const type = 'eggs';
const key = 'Wolf';
try {
await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
}
});
it('returns error when user supplies a negative quantity', async () => {
const type = 'eggs';
const key = 'Wolf';
user.balance = 10;
try {
await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
}
});
it('returns error when user supplies a decimal quantity', async () => {
const type = 'eggs';
const key = 'Wolf';
user.balance = 10;
try {
await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
}
});
});
});

View File

@@ -2,14 +2,17 @@ import get from 'lodash/get';
import unlock from '../../../website/common/script/ops/unlock';
import i18n from '../../../website/common/script/i18n';
import { generateUser } from '../../helpers/common.helper';
import { NotAuthorized, BadRequest } from '../../../website/common/script/libs/errors';
import {
NotAuthorized,
BadRequest,
} from '../../../website/common/script/libs/errors';
describe('shared.ops.unlock', () => {
let user;
const unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
let clock;
const unlockPath = 'shirt.convict,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
const backgroundUnlockPath = 'background.giant_florals';
const backgroundSetUnlockPath = 'background.archery_range,background.giant_florals,background.rainbows_end';
const hairUnlockPath = 'hair.color.rainbow,hair.color.yellow,hair.color.green,hair.color.purple,hair.color.blue,hair.color.TRUred';
const facialHairUnlockPath = 'hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3';
const usersStartingGems = 50 / 4;
@@ -17,6 +20,11 @@ describe('shared.ops.unlock', () => {
beforeEach(() => {
user = generateUser();
user.balance = usersStartingGems;
clock = sandbox.useFakeTimers(new Date('2024-04-10'));
});
afterEach(() => {
clock.restore();
});
it('returns an error when path is not provided', async () => {
@@ -31,7 +39,9 @@ describe('shared.ops.unlock', () => {
it('does not unlock lost gear', async () => {
user.items.gear.owned.headAccessory_special_bearEars = false;
await unlock(user, { query: { path: 'items.gear.owned.headAccessory_special_bearEars' } });
await unlock(user, {
query: { path: 'items.gear.owned.headAccessory_special_bearEars' },
});
expect(user.balance).to.equal(usersStartingGems);
});
@@ -95,7 +105,9 @@ describe('shared.ops.unlock', () => {
it('returns an error if gear is not from the animal set', async () => {
try {
await unlock(user, { query: { path: 'items.gear.owned.back_mystery_202004' } });
await unlock(user, {
query: { path: 'items.gear.owned.back_mystery_202004' },
});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
@@ -153,7 +165,6 @@ describe('shared.ops.unlock', () => {
await unlock(user, { query: { path: partialUnlockPaths[4] } });
await unlock(user, { query: { path: partialUnlockPaths[5] } });
await unlock(user, { query: { path: partialUnlockPaths[6] } });
await unlock(user, { query: { path: partialUnlockPaths[7] } });
await unlock(user, { query: { path: unlockPath } });
});
@@ -163,7 +174,9 @@ describe('shared.ops.unlock', () => {
await unlock(user, { query: { path: backgroundUnlockPath } });
const afterBalance = user.balance;
const response = await unlock(user, { query: { path: backgroundUnlockPath } });
const response = await unlock(user, {
query: { path: backgroundUnlockPath },
});
expect(user.balance).to.equal(afterBalance); // do not bill twice
expect(response.message).to.not.exist;
@@ -176,7 +189,9 @@ describe('shared.ops.unlock', () => {
await unlock(user, { query: { path: backgroundUnlockPath } }); // unlock
const afterBalance = user.balance;
await unlock(user, { query: { path: backgroundUnlockPath } }); // equip
const response = await unlock(user, { query: { path: backgroundUnlockPath } });
const response = await unlock(user, {
query: { path: backgroundUnlockPath },
});
expect(user.balance).to.equal(afterBalance); // do not bill twice
expect(response.message).to.not.exist;
@@ -192,8 +207,9 @@ describe('shared.ops.unlock', () => {
individualPaths.forEach(path => {
expect(get(user.purchased, path)).to.be.true;
});
expect(Object.keys(user.purchased.shirt).length)
.to.equal(initialShirts + individualPaths.length);
expect(Object.keys(user.purchased.shirt).length).to.equal(
initialShirts + individualPaths.length,
);
expect(user.balance).to.equal(usersStartingGems - 1.25);
});
@@ -208,8 +224,9 @@ describe('shared.ops.unlock', () => {
individualPaths.forEach(path => {
expect(get(user.purchased, path)).to.be.true;
});
expect(Object.keys(user.purchased.hair.color).length)
.to.equal(initialHairColors + individualPaths.length);
expect(Object.keys(user.purchased.hair.color).length).to.equal(
initialHairColors + individualPaths.length,
);
expect(user.balance).to.equal(usersStartingGems - 1.25);
});
@@ -219,21 +236,28 @@ describe('shared.ops.unlock', () => {
const initialMustache = Object.keys(user.purchased.hair.mustache).length;
const initialBeard = Object.keys(user.purchased.hair.mustache).length;
const [, message] = await unlock(user, { query: { path: facialHairUnlockPath } });
const [, message] = await unlock(user, {
query: { path: facialHairUnlockPath },
});
expect(message).to.equal(i18n.t('unlocked'));
const individualPaths = facialHairUnlockPath.split(',');
individualPaths.forEach(path => {
expect(get(user.purchased, path)).to.be.true;
});
expect(Object.keys(user.purchased.hair.mustache).length + Object.keys(user.purchased.hair.beard).length) // eslint-disable-line max-len
expect(
Object.keys(user.purchased.hair.mustache).length
+ Object.keys(user.purchased.hair.beard).length,
) // eslint-disable-line max-len
.to.equal(initialMustache + initialBeard + individualPaths.length);
expect(user.balance).to.equal(usersStartingGems - 1.25);
});
it('unlocks a full set of gear', async () => {
const initialGear = Object.keys(user.items.gear.owned).length;
const [, message] = await unlock(user, { query: { path: unlockGearSetPath } });
const [, message] = await unlock(user, {
query: { path: unlockGearSetPath },
});
expect(message).to.equal(i18n.t('unlocked'));
@@ -241,32 +265,21 @@ describe('shared.ops.unlock', () => {
individualPaths.forEach(path => {
expect(get(user, path)).to.be.true;
});
expect(Object.keys(user.items.gear.owned).length)
.to.equal(initialGear + individualPaths.length);
expect(Object.keys(user.items.gear.owned).length).to.equal(
initialGear + individualPaths.length,
);
expect(user.balance).to.equal(usersStartingGems - 1.25);
});
it('unlocks a full set of backgrounds', async () => {
const initialBackgrounds = Object.keys(user.purchased.background).length;
const [, message] = await unlock(user, { query: { path: backgroundSetUnlockPath } });
expect(message).to.equal(i18n.t('unlocked'));
const individualPaths = backgroundSetUnlockPath.split(',');
individualPaths.forEach(path => {
expect(get(user.purchased, path)).to.be.true;
});
expect(Object.keys(user.purchased.background).length)
.to.equal(initialBackgrounds + individualPaths.length);
expect(user.balance).to.equal(usersStartingGems - 3.75);
});
it('unlocks an item (appearance)', async () => {
const path = unlockPath.split(',')[0];
const initialShirts = Object.keys(user.purchased.shirt).length;
const [, message] = await unlock(user, { query: { path } });
expect(message).to.equal(i18n.t('unlocked'));
expect(Object.keys(user.purchased.shirt).length).to.equal(initialShirts + 1);
expect(Object.keys(user.purchased.shirt).length).to.equal(
initialShirts + 1,
);
expect(get(user.purchased, path)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 0.5);
});
@@ -279,7 +292,9 @@ describe('shared.ops.unlock', () => {
const [, message] = await unlock(user, { query: { path } });
expect(message).to.equal(i18n.t('unlocked'));
expect(Object.keys(user.purchased.hair.color).length).to.equal(initialColorHair + 1);
expect(Object.keys(user.purchased.hair.color).length).to.equal(
initialColorHair + 1,
);
expect(get(user.purchased, path)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 0.5);
});
@@ -295,8 +310,12 @@ describe('shared.ops.unlock', () => {
expect(message).to.equal(i18n.t('unlocked'));
expect(Object.keys(user.purchased.hair.mustache).length).to.equal(initialMustache + 1);
expect(Object.keys(user.purchased.hair.beard).length).to.equal(initialBeard);
expect(Object.keys(user.purchased.hair.mustache).length).to.equal(
initialMustache + 1,
);
expect(Object.keys(user.purchased.hair.beard).length).to.equal(
initialBeard,
);
expect(get(user.purchased, path)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 0.5);
@@ -315,11 +334,24 @@ describe('shared.ops.unlock', () => {
it('unlocks an item (background)', async () => {
const initialBackgrounds = Object.keys(user.purchased.background).length;
const [, message] = await unlock(user, { query: { path: backgroundUnlockPath } });
const [, message] = await unlock(user, {
query: { path: backgroundUnlockPath },
});
expect(message).to.equal(i18n.t('unlocked'));
expect(Object.keys(user.purchased.background).length).to.equal(initialBackgrounds + 1);
expect(Object.keys(user.purchased.background).length).to.equal(
initialBackgrounds + 1,
);
expect(get(user.purchased, backgroundUnlockPath)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 1.75);
});
it('handles an invalid hair path gracefully', async () => {
try {
await unlock(user, { query: { path: 'hair.invalid' } });
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
}
});
});

View File

@@ -1,27 +0,0 @@
/* eslint-disable prefer-template, no-shadow, func-names, import/no-commonjs */
const expect = require('expect.js');
module.exports.addCustomMatchers = function () {
const { Assertion } = expect;
Assertion.prototype.toHaveGP = function (gp) {
const actual = this.obj.stats.gp;
return this.assert(actual === gp, () => 'expected user to have ' + gp + ' gp, but got ' + actual, () => 'expected user to not have ' + gp + ' gp');
};
Assertion.prototype.toHaveHP = function (hp) {
const actual = this.obj.stats.hp;
return this.assert(actual === hp, () => 'expected user to have ' + hp + ' hp, but got ' + actual, () => 'expected user to not have ' + hp + ' hp');
};
Assertion.prototype.toHaveExp = function (exp) {
const actual = this.obj.stats.exp;
return this.assert(actual === exp, () => 'expected user to have ' + exp + ' experience points, but got ' + actual, () => 'expected user to not have ' + exp + ' experience points');
};
Assertion.prototype.toHaveLevel = function (lvl) {
const actual = this.obj.stats.lvl;
return this.assert(actual === lvl, () => 'expected user to be level ' + lvl + ', but got ' + actual, () => 'expected user to not be level ' + lvl);
};
Assertion.prototype.toHaveMaxMP = function (mp) {
const actual = this.obj._statsComputed.maxMP;
return this.assert(actual === mp, () => 'expected user to have ' + mp + ' max mp, but got ' + actual, () => 'expected user to not have ' + mp + ' max mp');
};
};

View File

@@ -0,0 +1,83 @@
/* eslint-disable global-require */
import forEach from 'lodash/forEach';
import {
expectValidTranslationString,
} from '../helpers/content.helper';
function makeArmoireIitemList () {
const armoire = require('../../website/common/script/content/gear/sets/armoire').default;
const items = [];
items.push(...Object.values(armoire.armor));
items.push(...Object.values(armoire.body));
items.push(...Object.values(armoire.eyewear));
items.push(...Object.values(armoire.head));
items.push(...Object.values(armoire.headAccessory));
items.push(...Object.values(armoire.shield));
items.push(...Object.values(armoire.weapon));
return items;
}
describe('armoire', () => {
let clock;
beforeEach(() => {
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
});
afterEach(() => {
clock.restore();
});
it('does not return unreleased gear', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-02'));
const items = makeArmoireIitemList();
expect(items.length).to.equal(377);
expect(items.filter(item => item.set === 'pottersSet' || item.set === 'optimistSet' || item.set === 'schoolUniform')).to.be.an('array').that.is.empty;
});
it('released gear has all required properties', async () => {
clock = sinon.useFakeTimers(new Date('2024-05-08'));
const items = makeArmoireIitemList();
expect(items.length).to.equal(396);
forEach(items, item => {
if (item.set !== undefined) {
expect(item.set, item.key).to.be.a('string');
expect(item.set, item.key).to.not.be.empty;
}
expectValidTranslationString(item.text);
expect(item.value, item.key).to.be.a('number');
});
});
it('releases gear when appropriate', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-01T00:00:00.000Z'));
const items = makeArmoireIitemList();
expect(items.length).to.equal(377);
clock.restore();
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
clock = sinon.useFakeTimers(new Date('2024-01-08'));
const januaryItems = makeArmoireIitemList();
expect(januaryItems.length).to.equal(381);
clock.restore();
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
clock = sinon.useFakeTimers(new Date('2024-02-07'));
const januaryItems2 = makeArmoireIitemList();
expect(januaryItems2.length).to.equal(381);
clock.restore();
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
clock = sinon.useFakeTimers(new Date('2024-02-07T16:00:00.000Z'));
const febuaryItems = makeArmoireIitemList();
expect(febuaryItems.length).to.equal(384);
});
it('sets have at least 2 items', () => {
const armoire = makeArmoireIitemList();
const setMap = {};
forEach(armoire, item => {
if (setMap[item.set] === undefined) {
setMap[item.set] = 0;
}
setMap[item.set] += 1;
});
Object.keys(setMap).forEach(set => {
expect(setMap[set], set).to.be.at.least(2);
});
});
});

View File

@@ -0,0 +1,40 @@
import { getRepeatingEvents } from '../../website/common/script/content/constants/events';
describe('events', () => {
let clock;
afterEach(() => {
if (clock) {
clock.restore();
}
});
it('returns empty array when no events are active', () => {
clock = sinon.useFakeTimers(new Date('2024-01-06'));
const events = getRepeatingEvents();
expect(events).to.be.empty;
});
it('returns events when active', () => {
clock = sinon.useFakeTimers(new Date('2024-01-31'));
const events = getRepeatingEvents();
expect(events).to.have.length(1);
expect(events[0].key).to.equal('birthday');
expect(events[0].end).to.be.greaterThan(new Date());
expect(events[0].start).to.be.lessThan(new Date());
});
it('returns nye event at beginning of the year', () => {
clock = sinon.useFakeTimers(new Date('2025-01-01'));
const events = getRepeatingEvents();
expect(events).to.have.length(1);
expect(events[0].key).to.equal('nye');
});
it('returns nye event at end of the year', () => {
clock = sinon.useFakeTimers(new Date('2024-12-30'));
const events = getRepeatingEvents();
expect(events).to.have.length(1);
expect(events[0].key).to.equal('nye');
});
});

94
test/content/food.test.js Normal file
View File

@@ -0,0 +1,94 @@
/* eslint-disable global-require */
import {
each,
} from 'lodash';
import {
expectValidTranslationString,
} from '../helpers/content.helper';
import content from '../../website/common/script/content';
describe('food', () => {
let clock;
afterEach(() => {
if (clock) {
clock.restore();
}
delete require.cache[require.resolve('../../website/common/script/content')];
});
describe('all', () => {
it('contains basic information about each food item', () => {
each(content.food, (foodItem, key) => {
if (foodItem.key === 'Saddle') {
expectValidTranslationString(foodItem.sellWarningNote);
} else {
expectValidTranslationString(foodItem.textA);
expectValidTranslationString(foodItem.textThe);
expect(foodItem.target).to.be.a('string');
}
expectValidTranslationString(foodItem.text);
expectValidTranslationString(foodItem.notes);
expect(foodItem.canBuy).to.be.a('function');
expect(foodItem.value).to.be.a('number');
expect(foodItem.key).to.equal(key);
});
});
it('sets canDrop for normal food if there is no food season', () => {
clock = sinon.useFakeTimers(new Date(2024, 5, 8));
const datedContent = require('../../website/common/script/content').default;
each(datedContent.food, foodItem => {
if (foodItem.key.indexOf('Cake') === -1 && foodItem.key.indexOf('Candy_') === -1 && foodItem.key.indexOf('Pie_') === -1 && foodItem.key !== 'Saddle') {
expect(foodItem.canDrop).to.equal(true);
} else {
expect(foodItem.canDrop).to.equal(false);
}
});
});
it('sets canDrop for candy if it is candy season', () => {
clock = sinon.useFakeTimers(new Date(2024, 9, 31));
const datedContent = require('../../website/common/script/content').default;
each(datedContent.food, foodItem => {
if (foodItem.key.indexOf('Candy_') !== -1) {
expect(foodItem.canDrop).to.equal(true);
} else {
expect(foodItem.canDrop).to.equal(false);
}
});
});
it('sets canDrop for cake if it is cake season', () => {
clock = sinon.useFakeTimers(new Date(2024, 0, 31));
const datedContent = require('../../website/common/script/content').default;
each(datedContent.food, foodItem => {
if (foodItem.key.indexOf('Cake_') !== -1) {
expect(foodItem.canDrop).to.equal(true);
} else {
expect(foodItem.canDrop).to.equal(false);
}
});
});
it('sets canDrop for pie if it is pie season', () => {
clock = sinon.useFakeTimers(new Date(2024, 2, 14));
const datedContent = require('../../website/common/script/content').default;
each(datedContent.food, foodItem => {
if (foodItem.key.indexOf('Pie_') !== -1) {
expect(foodItem.canDrop).to.equal(true);
} else {
expect(foodItem.canDrop).to.equal(false);
}
});
});
});
it('sets correct values for saddles', () => {
const saddle = content.food.Saddle;
expect(saddle.canBuy).to.be.a('function');
expect(saddle.value).to.equal(5);
expect(saddle.key).to.equal('Saddle');
expect(saddle.canDrop).to.equal(false);
});
});

View File

@@ -4,6 +4,8 @@ import {
expectValidTranslationString,
} from '../helpers/content.helper';
import { CLASSES } from '../../website/common/script/content/constants';
import gearData from '../../website/common/script/content/gear';
import * as backerGear from '../../website/common/script/content/gear/sets/special/special-backer';
import * as contributorGear from '../../website/common/script/content/gear/sets/special/special-contributor';
@@ -17,35 +19,48 @@ describe('Gear', () => {
context(`${klass} ${gearType}s`, () => {
it('have a value of at least 0 for each stat', () => {
each(items, gear => {
expect(gear.con).to.be.at.least(0);
expect(gear.int).to.be.at.least(0);
expect(gear.per).to.be.at.least(0);
expect(gear.str).to.be.at.least(0);
expect(gear.con, gear.key).to.be.at.least(0);
expect(gear.int, gear.key).to.be.at.least(0);
expect(gear.per, gear.key).to.be.at.least(0);
expect(gear.str, gear.key).to.be.at.least(0);
});
});
it('have a purchase value of at least 0', () => {
each(items, gear => {
expect(gear.value).to.be.at.least(0);
expect(gear.value, gear.key).to.be.at.least(0);
});
});
it('has a canBuy function', () => {
each(items, gear => {
expect(gear.canBuy).to.be.a('function');
expect(gear.canBuy, gear.key).to.be.a('function');
});
});
it('have valid translation strings for text and notes', () => {
each(items, gear => {
expectValidTranslationString(gear.text);
expectValidTranslationString(gear.notes);
expectValidTranslationString(gear.text, gear.key);
expectValidTranslationString(gear.notes, gear.key);
});
});
it('has a set attribue', () => {
each(items, gear => {
expect(gear.set).to.exist;
expect(gear.set, gear.key).to.exist;
});
});
it('has a valid value for klass or specialClass', () => {
const validClassValues = CLASSES + ['base', 'mystery', 'armoire'];
each(items, gear => {
const field = gear.klass === 'special' ? gear.specialClass : gear.klass;
if (gear.klass === 'special' && field === undefined) {
// some special gear doesn't have a klass
return;
}
expect(field, gear.key).to.exist;
expect(validClassValues, gear.key).to.include(field);
});
});
});
@@ -53,6 +68,16 @@ describe('Gear', () => {
});
});
it('only assigns mage weapons twoHanded', () => {
each([allGear.armor.special, allGear.head.special, allGear.shield.special], gearType => {
each(gearType, gear => {
if (gear.specialClass === 'wizard') {
expect(gear.twoHanded, gear.key).to.not.eql(true);
}
});
});
});
describe('backer gear', () => {
let user;

View File

@@ -5,27 +5,33 @@ import {
expectValidTranslationString,
} from '../helpers/content.helper';
import * as hatchingPotions from '../../website/common/script/content/hatching-potions';
import { all } from '../../website/common/script/content/hatching-potions';
describe('hatchingPotions', () => {
describe('all', () => {
it('is a combination of drop, premium, and wacky potions', () => {
const dropNumber = Object.keys(hatchingPotions.drops).length;
const premiumNumber = Object.keys(hatchingPotions.premium).length;
const wackyNumber = Object.keys(hatchingPotions.wacky).length;
const allNumber = Object.keys(hatchingPotions.all).length;
let clock;
expect(allNumber).to.be.greaterThan(0);
expect(allNumber).to.equal(dropNumber + premiumNumber + wackyNumber);
});
afterEach(() => {
if (clock) {
clock.restore();
}
});
it('contains basic information about each potion', () => {
each(hatchingPotions.all, (potion, key) => {
expectValidTranslationString(potion.text);
expectValidTranslationString(potion.notes);
expect(potion.canBuy).to.be.a('function');
expect(potion.value).to.be.a('number');
expect(potion.key).to.equal(key);
const potionTypes = [
'drops',
'quests',
'premium',
'wacky',
];
potionTypes.forEach(potionType => {
describe(potionType, () => {
it('contains basic information about each potion', () => {
each(all, (potion, key) => {
expectValidTranslationString(potion.text);
expectValidTranslationString(potion.notes);
expect(potion.canBuy).to.be.a('function');
expect(potion.value).to.be.a('number');
expect(potion.key).to.equal(key);
});
});
});
});

View File

@@ -0,0 +1,271 @@
// eslint-disable-next-line max-len
import moment from 'moment';
import nconf from 'nconf';
import {
getAllScheduleMatchingGroups, clearCachedMatchers, MONTHLY_SCHEDULE, GALA_SCHEDULE,
} from '../../website/common/script/content/constants/schedule';
import QUEST_PETS from '../../website/common/script/content/quests/pets';
import QUEST_HATCHINGPOTIONS from '../../website/common/script/content/quests/potions';
import QUEST_BUNDLES from '../../website/common/script/content/bundles';
import { premium } from '../../website/common/script/content/hatching-potions';
import SPELLS from '../../website/common/script/content/spells';
import QUEST_SEASONAL from '../../website/common/script/content/quests/seasonal';
function validateMatcher (matcher, checkedDate) {
expect(matcher.end).to.be.a('date');
expect(matcher.end).to.be.greaterThan(checkedDate);
}
describe('Content Schedule', () => {
let switchoverTime;
beforeEach(() => {
switchoverTime = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0;
clearCachedMatchers();
});
it('assembles scheduled items on january 15th', () => {
const date = new Date('2024-01-15');
const matchers = getAllScheduleMatchingGroups(date);
for (const key in matchers) {
if (matchers[key]) {
validateMatcher(matchers[key], date);
}
}
});
it('assembles scheduled items on january 31th', () => {
const date = new Date('2024-01-31');
const matchers = getAllScheduleMatchingGroups(date);
for (const key in matchers) {
if (matchers[key]) {
validateMatcher(matchers[key], date);
}
}
});
it('assembles scheduled items on march 2nd', () => {
const date = new Date('2024-03-02');
const matchers = getAllScheduleMatchingGroups(date);
for (const key in matchers) {
if (matchers[key]) {
validateMatcher(matchers[key], date);
}
}
});
it('assembles scheduled items on march 22st', () => {
const date = new Date('2024-03-22');
const matchers = getAllScheduleMatchingGroups(date);
for (const key in matchers) {
if (matchers[key]) {
validateMatcher(matchers[key], date);
}
}
});
it('assembles scheduled items on october 7th', () => {
const date = new Date('2024-10-07');
const matchers = getAllScheduleMatchingGroups(date);
for (const key in matchers) {
if (matchers[key]) {
validateMatcher(matchers[key], date);
}
}
});
it('assembles scheduled items on november 1th', () => {
const date = new Date('2024-11-01');
const matchers = getAllScheduleMatchingGroups(date);
for (const key in matchers) {
if (matchers[key]) {
validateMatcher(matchers[key], date);
}
}
});
it('assembles scheduled items on december 20th', () => {
const date = new Date('2024-12-20');
const matchers = getAllScheduleMatchingGroups(date);
for (const key in matchers) {
if (matchers[key]) {
validateMatcher(matchers[key], date);
}
}
});
it('sets the end date if its in the same month', () => {
const date = new Date('2024-04-03');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-04-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date if its in the next day', () => {
const date = new Date('2024-05-06T14:00:00.000Z');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-05-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date if its on the release day', () => {
const date = new Date('2024-05-07T07:00:00.000Z');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-06-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date if its next month', () => {
const date = new Date('2024-05-20T01:00:00.000Z');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-06-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date for a gala', () => {
const date = new Date('2024-05-20');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('contains content for repeating events', () => {
const date = new Date('2024-04-15');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.premiumHatchingPotions).to.exist;
expect(matchers.premiumHatchingPotions.items.length).to.equal(4);
expect(matchers.premiumHatchingPotions.items.indexOf('Garden')).to.not.equal(-1);
expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1);
});
describe('only contains valid keys for', () => {
it('pet quests', () => {
const petKeys = Object.keys(QUEST_PETS);
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
const petQuests = MONTHLY_SCHEDULE[key][14].find(item => item.type === 'petQuests');
for (const petQuest of petQuests.items) {
expect(petQuest).to.be.a('string');
expect(petKeys).to.include(petQuest);
}
});
});
it('hatchingpotion quests', () => {
const potionKeys = Object.keys(QUEST_HATCHINGPOTIONS);
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
const potionQuests = MONTHLY_SCHEDULE[key][14].find(item => item.type === 'hatchingPotionQuests');
for (const potionQuest of potionQuests.items) {
expect(potionQuest).to.be.a('string');
expect(potionKeys).to.include(potionQuest);
}
});
});
it('bundles', () => {
const bundleKeys = Object.keys(QUEST_BUNDLES);
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
const bundles = MONTHLY_SCHEDULE[key][14].find(item => item.type === 'bundles');
for (const bundle of bundles.items) {
expect(bundle).to.be.a('string');
expect(bundleKeys).to.include(bundle);
}
});
});
it('premium hatching potions', () => {
const potionKeys = Object.keys(premium);
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
const monthlyPotions = MONTHLY_SCHEDULE[key][21].find(item => item.type === 'premiumHatchingPotions');
for (const potion of monthlyPotions.items) {
expect(potion).to.be.a('string');
expect(potionKeys).to.include(potion);
}
});
});
it('seasonal quests', () => {
const questKeys = Object.keys(QUEST_SEASONAL);
Object.keys(GALA_SCHEDULE).forEach(key => {
const quests = GALA_SCHEDULE[key].matchers.find(item => item.type === 'seasonalQuests');
for (const quest of quests.items) {
expect(quest).to.be.a('string');
expect(questKeys).to.include(quest);
}
});
});
it('seasonal spells', () => {
const spellKeys = Object.keys(SPELLS.special);
Object.keys(GALA_SCHEDULE).forEach(key => {
const petQuests = GALA_SCHEDULE[key].matchers.find(item => item.type === 'seasonalSpells');
for (const petQuest of petQuests.items) {
expect(petQuest).to.be.a('string');
expect(spellKeys).to.include(petQuest);
}
});
});
});
describe('backgrounds matcher', () => {
it('allows background matching the month for new backgrounds', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey072024')).to.be.true;
});
it('disallows background in the future', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey072025')).to.be.false;
});
it('disallows background for the inverse month for new backgrounds', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey012024')).to.be.false;
});
it('allows background for the inverse month for old backgrounds', () => {
const date = new Date('2024-08-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey022023')).to.be.true;
expect(matcher.match('backgroundkey022021')).to.be.true;
});
it('allows background even yeared backgrounds in first half of year', () => {
const date = new Date('2025-02-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey022024')).to.be.true;
expect(matcher.match('backgroundkey082022')).to.be.true;
});
it('allows background odd yeared backgrounds in second half of year', () => {
const date = new Date('2024-08-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey022023')).to.be.true;
expect(matcher.match('backgroundkey082021')).to.be.true;
});
});
describe('timeTravelers matcher', () => {
it('allows sets matching the month', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202307')).to.be.true;
expect(matcher.match('202207')).to.be.true;
});
it('disallows sets not matching the month', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202306')).to.be.false;
expect(matcher.match('202402')).to.be.false;
});
it('disallows sets from current month', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202407')).to.be.false;
});
it('disallows sets from the future', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('202507')).to.be.false;
});
});
});

View File

@@ -0,0 +1,52 @@
import Sinon from 'sinon';
import featuredItems from '../../website/common/script/content/shop-featuredItems';
describe('Shop Featured Items', () => {
let clock;
afterEach(() => {
if (clock !== undefined) {
clock.restore();
clock = undefined;
}
});
describe('Market', () => {
it('contains armoire', () => {
const items = featuredItems.market();
expect(_.find(items, item => item.path === 'armoire')).to.exist;
});
it('contains the current premium hatching potions', () => {
clock = Sinon.useFakeTimers(new Date('2024-04-08'));
const items = featuredItems.market();
expect(_.find(items, item => item.path === 'premiumHatchingPotions.Porcelain')).to.exist;
});
it('is featuring 4 items', () => {
clock = Sinon.useFakeTimers(new Date('2024-02-08'));
const items = featuredItems.market();
expect(items.length).to.eql(4);
});
});
describe('Quest Shop', () => {
it('contains bundle', () => {
clock = Sinon.useFakeTimers(new Date('2024-03-08'));
const items = featuredItems.quests();
expect(_.find(items, item => item.path === 'quests.pinkMarble')).to.exist;
});
it('contains pet quests', () => {
clock = Sinon.useFakeTimers(new Date('2024-04-08'));
const items = featuredItems.quests();
expect(_.find(items, item => item.path === 'quests.frog')).to.exist;
});
it('is featuring 3 items', () => {
clock = Sinon.useFakeTimers(new Date('2024-02-08'));
const items = featuredItems.quests();
expect(items.length).to.eql(3);
});
});
});

View File

@@ -0,0 +1,63 @@
import {
generateUser,
} from '../helpers/common.helper';
import spells from '../../website/common/script/content/spells';
import {
expectValidTranslationString,
} from '../helpers/content.helper';
import { TRANSFORMATION_DEBUFFS_LIST } from '../../website/common/script/constants';
// TODO complete the test suite...
describe('shared.ops.spells', () => {
let user;
let target;
beforeEach(() => {
user = generateUser();
target = generateUser();
});
it('all spells have required properties', () => {
for (const category of Object.values(spells)) {
for (const spell of Object.values(category)) {
expectValidTranslationString(spell.text, spell.key);
expectValidTranslationString(spell.notes);
expect(spell.target, spell.key).to.be.oneOf(['self', 'party', 'task', 'tasks', 'user']);
}
}
});
it('all special spells have a working cast method', async () => {
for (const s of Object.values(spells.special)) {
user.items.special[s.key] = 1;
s.cast(user, target, { language: 'en' });
}
});
it('all debuff spells cost 5 gold', () => {
for (const s of Object.values(spells.special)) {
if (s.purchaseType === 'debuffPotion') {
user.stats.gp = 5;
s.cast(user);
expect(user.stats.gp).to.equal(0);
}
}
});
it('all debuff spells remove the buff', () => {
const debuffMapping = {};
Object.keys(TRANSFORMATION_DEBUFFS_LIST).forEach(key => {
debuffMapping[TRANSFORMATION_DEBUFFS_LIST[key]] = key;
});
for (const s of Object.values(spells.special)) {
if (s.purchaseType === 'debuffPotion') {
user.stats.gp = 5;
user.stats.buffs[debuffMapping[s.key]] = true;
expect(user.stats.buffs[debuffMapping[s.key]]).to.equal(true);
s.cast(user);
expect(user.stats.buffs[debuffMapping[s.key]]).to.equal(false);
}
}
});
});

View File

@@ -6,23 +6,105 @@ import timeTravelers from '../../website/common/script/content/time-travelers';
describe('time-travelers store', () => {
let user;
let date;
beforeEach(() => {
user = generateUser();
});
it('removes owned sets from the time travelers store', () => {
user.items.gear.owned.head_mystery_201602 = true; // eslint-disable-line camelcase
expect(timeTravelers.timeTravelerStore(user)['201602']).to.not.exist;
expect(timeTravelers.timeTravelerStore(user)['201603']).to.exist;
describe('on january 15th', () => {
beforeEach(() => {
date = new Date('2024-01-15');
});
it('returns the correct gear', () => {
const items = timeTravelers.timeTravelerStore(user, date);
for (const [key] of Object.entries(items)) {
if (key.startsWith('20')) {
expect(key).to.match(/20[0-9]{2}(01|07)/);
}
}
});
it('removes owned sets from the time travelers store', () => {
user.items.gear.owned.head_mystery_201601 = true; // eslint-disable-line camelcase
const items = timeTravelers.timeTravelerStore(user, date);
expect(items['201601']).to.not.exist;
expect(items['201801']).to.exist;
expect(items['202207']).to.exist;
});
it('removes unopened mystery item sets from the time travelers store', () => {
user.purchased = {
plan: {
mysteryItems: ['head_mystery_201601'],
},
};
const items = timeTravelers.timeTravelerStore(user, date);
expect(items['201601']).to.not.exist;
expect(items['201607']).to.exist;
});
});
it('removes unopened mystery item sets from the time travelers store', () => {
user.purchased = {
plan: {
mysteryItems: ['head_mystery_201602'],
},
};
expect(timeTravelers.timeTravelerStore(user)['201602']).to.not.exist;
expect(timeTravelers.timeTravelerStore(user)['201603']).to.exist;
describe('on may 1st', () => {
beforeEach(() => {
date = new Date('2024-05-01');
});
it('returns the correct gear', () => {
const items = timeTravelers.timeTravelerStore(user, date);
for (const [key] of Object.entries(items)) {
if (key.startsWith('20')) {
expect(key).to.match(/20[0-9]{2}(05|11)/);
}
}
});
it('removes owned sets from the time travelers store', () => {
user.items.gear.owned.head_mystery_201705 = true; // eslint-disable-line camelcase
const items = timeTravelers.timeTravelerStore(user, date);
expect(items['201705']).to.not.exist;
expect(items['201805']).to.exist;
expect(items['202211']).to.exist;
});
it('removes unopened mystery item sets from the time travelers store', () => {
user.purchased = {
plan: {
mysteryItems: ['head_mystery_201705'],
},
};
const items = timeTravelers.timeTravelerStore(user, date);
expect(items['201705']).to.not.exist;
expect(items['201611']).to.exist;
});
});
describe('on october 21st', () => {
beforeEach(() => {
date = new Date('2024-10-21');
});
it('returns the correct gear', () => {
const items = timeTravelers.timeTravelerStore(user, date);
for (const [key] of Object.entries(items)) {
if (key.startsWith('20')) {
expect(key).to.match(/20[0-9]{2}(10|04)/);
}
}
});
it('removes owned sets from the time travelers store', () => {
user.items.gear.owned.head_mystery_201810 = true; // eslint-disable-line camelcase
user.items.gear.owned.armor_mystery_201810 = true; // eslint-disable-line camelcase
const items = timeTravelers.timeTravelerStore(user, date);
expect(items['201810']).to.not.exist;
expect(items['201910']).to.exist;
expect(items['202204']).to.exist;
});
it('removes unopened mystery item sets from the time travelers store', () => {
user.purchased = {
plan: {
mysteryItems: ['armor_mystery_201710'],
},
};
const items = timeTravelers.timeTravelerStore(user, date);
expect(items['201710']).to.not.exist;
expect(items['201604']).to.exist;
});
});
});

View File

@@ -40,6 +40,7 @@ export function generateRes (options = {}) {
redirect: sandbox.stub(),
render: sandbox.stub(),
send: sandbox.stub(),
sendFile: sandbox.stub(),
sendStatus: sandbox.stub().returnsThis(),
set: sandbox.stub(),
status: sandbox.stub().returnsThis(),
@@ -59,7 +60,17 @@ export function generateReq (options = {}) {
header (header) {
return this.headers[header];
},
listeners: {},
session: {},
on (key, func) {
if (!this.listeners[key]) {
this.listeners[key] = [];
}
this.listeners[key].push(func);
},
end () {
this.listeners.close.forEach(func => func());
},
};
const req = defaultsDeep(options, defaultReq);

View File

@@ -7,13 +7,13 @@ i18n.translations = translations;
export const STRING_ERROR_MSG = /^Error processing the string ".*". Please see Help > Report a Bug.$/;
export const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
export function expectValidTranslationString (attribute) {
expect(attribute).to.be.a('function');
export function expectValidTranslationString (attribute, contextKey) {
expect(attribute, contextKey).to.be.a('function');
const translatedString = attribute();
expect(translatedString.trim()).to.not.be.empty;
expect(translatedString).to.not.contain('function func(lang)');
expect(translatedString).to.not.eql(STRING_ERROR_MSG);
expect(translatedString).to.not.match(STRING_DOES_NOT_EXIST_MSG);
expect(translatedString.trim(), contextKey).to.not.be.empty;
expect(translatedString, contextKey).to.not.contain('function func(lang)');
expect(translatedString, contextKey).to.not.eql(STRING_ERROR_MSG);
expect(translatedString, contextKey).to.not.match(STRING_DOES_NOT_EXIST_MSG);
}

View File

@@ -16,12 +16,12 @@
"@vue/cli-service": "^5.0.8",
"@vue/test-utils": "1.0.0-beta.29",
"amplitude-js": "^8.21.3",
"axios": "^0.27.2",
"assert": "^2.1.0",
"axios": "^0.28.0",
"axios-progress-bar": "^1.2.0",
"babel-eslint": "^10.1.0",
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.23.1",
"chai": "^4.3.7",
"core-js": "^3.33.1",
"dompurify": "^3.0.3",
"eslint": "7.32.0",
@@ -30,16 +30,18 @@
"eslint-plugin-vue": "7.20.0",
"habitica-markdown": "^3.0.0",
"hellojs": "^1.20.0",
"inspectpack": "^4.7.1",
"intro.js": "^7.2.0",
"jquery": "^3.7.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"moment-locales-webpack-plugin": "^1.2.0",
"nconf": "^0.12.1",
"sass": "^1.63.4",
"sass-loader": "^8.0.2",
"sass-loader": "^14.1.1",
"sinon": "^17.0.1",
"smartbanner.js": "^1.19.3",
"stopword": "^2.0.8",
"timers-browserify": "^2.0.12",
"uuid": "^9.0.1",
"validator": "^13.9.0",
"vue": "^2.7.10",
@@ -49,11 +51,15 @@
"vue-template-babel-compiler": "^2.0.0",
"vue-template-compiler": "^2.7.10",
"vuedraggable": "^2.24.3",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
"webpack": "^5.89.0"
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.21.0"
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"babel-plugin-lodash": "^3.3.4",
"chai": "^5.1.0",
"inspectpack": "^4.7.1",
"terser-webpack-plugin": "^5.3.10",
"webpack": "^5.89.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -2120,6 +2126,45 @@
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
},
"node_modules/@sinonjs/commons": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
"integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/@sinonjs/fake-timers": {
"version": "11.2.2",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
"integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
"dependencies": {
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@sinonjs/samsam": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz",
"integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==",
"dependencies": {
"@sinonjs/commons": "^2.0.0",
"lodash.get": "^4.4.2",
"type-detect": "^4.0.8"
}
},
"node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/@sinonjs/text-encoding": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
"integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ=="
},
"node_modules/@soda/friendly-errors-webpack-plugin": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz",
@@ -3601,12 +3646,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/assert": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
"integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
"dependencies": {
"call-bind": "^1.0.2",
"is-nan": "^1.3.2",
"object-is": "^1.1.5",
"object.assign": "^4.1.4",
"util": "^0.12.5"
}
},
"node_modules/assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
"engines": {
"node": "*"
"node": ">=12"
}
},
"node_modules/astral-regex": {
@@ -3683,12 +3741,13 @@
}
},
"node_modules/axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.28.1.tgz",
"integrity": "sha512-iUcGA5a7p0mVb4Gm/sy+FSECNkPFT4y7wt6OM/CDpO/OnNCvSs3PoMG8ibrC9jRoGYU0gUK5pXVC4NPXq6lHRQ==",
"dependencies": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios-progress-bar": {
@@ -3758,6 +3817,19 @@
"object.assign": "^4.1.0"
}
},
"node_modules/babel-plugin-lodash": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/babel-plugin-lodash/-/babel-plugin-lodash-3.3.4.tgz",
"integrity": "sha512-yDZLjK7TCkWl1gpBeBGmuaDIFhZKmkoL+Cu2MUUjv5VxUZx/z7tBGBCBcQs5RI1Bkz5LLmNdjx7paOyQtMovyg==",
"dev": true,
"dependencies": {
"@babel/helper-module-imports": "^7.0.0-beta.49",
"@babel/types": "^7.0.0-beta.49",
"glob": "^7.1.1",
"lodash": "^4.17.10",
"require-package-name": "^2.0.1"
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz",
@@ -3873,12 +3945,12 @@
"integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w=="
},
"node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
@@ -3886,7 +3958,7 @@
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.11.0",
"raw-body": "2.5.1",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
},
@@ -4062,13 +4134,18 @@
}
},
"node_modules/call-bind": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1",
"set-function-length": "^1.1.1"
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -4141,20 +4218,19 @@
}
},
"node_modules/chai": {
"version": "4.3.10",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz",
"integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
"integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
"dev": true,
"dependencies": {
"assertion-error": "^1.1.0",
"check-error": "^1.0.3",
"deep-eql": "^4.1.3",
"get-func-name": "^2.0.2",
"loupe": "^2.3.6",
"pathval": "^1.1.1",
"type-detect": "^4.0.8"
"assertion-error": "^2.0.1",
"check-error": "^2.1.1",
"deep-eql": "^5.0.1",
"loupe": "^3.1.0",
"pathval": "^2.0.0"
},
"engines": {
"node": ">=4"
"node": ">=12"
}
},
"node_modules/chalk": {
@@ -4171,14 +4247,12 @@
}
},
"node_modules/check-error": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
"integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
"dependencies": {
"get-func-name": "^2.0.2"
},
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
"dev": true,
"engines": {
"node": "*"
"node": ">= 16"
}
},
"node_modules/chokidar": {
@@ -4553,9 +4627,9 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
},
"node_modules/cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"engines": {
"node": ">= 0.6"
}
@@ -5047,12 +5121,10 @@
}
},
"node_modules/deep-eql": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
"integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
"dependencies": {
"type-detect": "^4.0.0"
},
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.1.tgz",
"integrity": "sha512-nwQCf6ne2gez3o1MxWifqkciwt0zhl0LO1/UwVu4uMBuPmflWM4oQ70XMqHqnBJA+nhzncaqL9HVL6KkHJ28lw==",
"dev": true,
"engines": {
"node": ">=6"
}
@@ -5148,16 +5220,19 @@
}
},
"node_modules/define-data-property": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dependencies": {
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-lazy-prop": {
@@ -5528,6 +5603,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
@@ -6617,16 +6711,16 @@
}
},
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.1",
"body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.5.0",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -6886,9 +6980,9 @@
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ=="
},
"node_modules/follow-redirects": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
@@ -6936,7 +7030,8 @@
"node_modules/fp-ts": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz",
"integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA=="
"integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==",
"dev": true
},
"node_modules/fraction.js": {
"version": "4.3.7",
@@ -6982,19 +7077,6 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -7053,20 +7135,25 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -7260,11 +7347,11 @@
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dependencies": {
"get-intrinsic": "^1.2.2"
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -7667,6 +7754,7 @@
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/inspectpack/-/inspectpack-4.7.1.tgz",
"integrity": "sha512-XoDJbKSM9I2KA+8+OLFJHm8m4NM2pMEgsDD2hze6swVfynEed9ngCx36mRR+otzOsskwnxIZWXjI23FTW1uHqA==",
"dev": true,
"dependencies": {
"chalk": "^4.1.0",
"fp-ts": "^2.6.1",
@@ -7687,6 +7775,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -7701,6 +7790,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -7716,6 +7806,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -7726,12 +7817,14 @@
"node_modules/inspectpack/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/inspectpack/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -7740,6 +7833,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -7777,6 +7871,7 @@
"version": "2.2.21",
"resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.21.tgz",
"integrity": "sha512-zz2Z69v9ZIC3mMLYWIeoUcwWD6f+O7yP92FMVVaXEOSZH1jnVBmET/urd/uoarD1WGBY4rCj8TAyMPzsGNzMFQ==",
"dev": true,
"peerDependencies": {
"fp-ts": "^2.5.0"
}
@@ -7785,6 +7880,7 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/io-ts-reporters/-/io-ts-reporters-1.2.2.tgz",
"integrity": "sha512-igASwWWkDY757OutNcM6zTtdJf/eTZYkoe2ymsX2qpm5bKZLo74FJYjsCtMQOEdY7dRHLLEulCyFQwdN69GBCg==",
"dev": true,
"peerDependencies": {
"fp-ts": "^2.0.2",
"io-ts": "^2.0.0"
@@ -7798,6 +7894,21 @@
"node": ">= 10"
}
},
"node_modules/is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
"dependencies": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
@@ -7938,6 +8049,20 @@
"node": ">=8"
}
},
"node_modules/is-generator-function": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
"integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
"dependencies": {
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -7957,6 +8082,21 @@
"node": ">=8"
}
},
"node_modules/is-nan": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
"integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
"dependencies": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-negative-zero": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
@@ -8342,6 +8482,11 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/just-extend": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz",
"integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw=="
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -8477,6 +8622,16 @@
"resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz",
"integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA=="
},
"node_modules/lodash.difference": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="
},
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
},
"node_modules/lodash.kebabcase": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
@@ -8689,9 +8844,10 @@
}
},
"node_modules/loupe": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
"integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
"integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
"dev": true,
"dependencies": {
"get-func-name": "^2.0.1"
}
@@ -9454,6 +9610,18 @@
"node": "*"
}
},
"node_modules/moment-locales-webpack-plugin": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/moment-locales-webpack-plugin/-/moment-locales-webpack-plugin-1.2.0.tgz",
"integrity": "sha512-QAi5v0OlPUP7GXviKMtxnpBAo8WmTHrUNN7iciAhNOEAd9evCOvuN0g1N7ThIg3q11GLCkjY1zQ2saRcf/43nQ==",
"dependencies": {
"lodash.difference": "^4.5.0"
},
"peerDependencies": {
"moment": "^2.8.0",
"webpack": "^1 || ^2 || ^3 || ^4 || ^5"
}
},
"node_modules/mrmime": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
@@ -9537,6 +9705,23 @@
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
},
"node_modules/nise": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz",
"integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==",
"dependencies": {
"@sinonjs/commons": "^3.0.0",
"@sinonjs/fake-timers": "^11.2.2",
"@sinonjs/text-encoding": "^0.7.2",
"just-extend": "^6.2.0",
"path-to-regexp": "^6.2.1"
}
},
"node_modules/nise/node_modules/path-to-regexp": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
},
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
@@ -9700,6 +9885,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-is": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
@@ -10139,11 +10339,12 @@
}
},
"node_modules/pathval": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
"dev": true,
"engines": {
"node": "*"
"node": ">= 14.16"
}
},
"node_modules/picocolors": {
@@ -10166,6 +10367,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
"integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==",
"dev": true,
"engines": {
"node": ">=10"
},
@@ -10905,6 +11107,11 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@@ -11013,9 +11220,9 @@
}
},
"node_modules/raw-body": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
"integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
@@ -11277,6 +11484,12 @@
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"node_modules/require-package-name": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz",
"integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==",
"dev": true
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -11436,31 +11649,28 @@
}
},
"node_modules/sass-loader": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.2.tgz",
"integrity": "sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ==",
"version": "14.2.1",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.2.1.tgz",
"integrity": "sha512-G0VcnMYU18a4N7VoNDegg2OuMjYtxnqzQWARVWCIVSZwJeiL9kg8QMsuIZOplsJgTzZLF6jGxI3AClj8I9nRdQ==",
"dependencies": {
"clone-deep": "^4.0.1",
"loader-utils": "^1.2.3",
"neo-async": "^2.6.1",
"schema-utils": "^2.6.1",
"semver": "^6.3.0"
"neo-async": "^2.6.2"
},
"engines": {
"node": ">= 8.9.0"
"node": ">= 18.12.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"fibers": ">= 3.1.0",
"node-sass": "^4.0.0",
"@rspack/core": "0.x || 1.x",
"node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
"sass": "^1.3.0",
"webpack": "^4.36.0 || ^5.0.0"
"sass-embedded": "*",
"webpack": "^5.0.0"
},
"peerDependenciesMeta": {
"fibers": {
"@rspack/core": {
"optional": true
},
"node-sass": {
@@ -11468,6 +11678,12 @@
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"webpack": {
"optional": true
}
}
},
@@ -11532,7 +11748,8 @@
"node_modules/semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"dev": true
},
"node_modules/send": {
"version": "0.18.0",
@@ -11673,15 +11890,16 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
"node_modules/set-function-length": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dependencies": {
"define-data-property": "^1.1.1",
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.1"
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -11700,6 +11918,11 @@
"node": ">= 0.4"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -11761,6 +11984,50 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"node_modules/sinon": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
"integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
"dependencies": {
"@sinonjs/commons": "^3.0.0",
"@sinonjs/fake-timers": "^11.2.2",
"@sinonjs/samsam": "^8.0.0",
"diff": "^5.1.0",
"nise": "^5.1.5",
"supports-color": "^7.2.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/sinon"
}
},
"node_modules/sinon/node_modules/diff": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/sinon/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/sinon/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/sirv": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
@@ -12260,15 +12527,15 @@
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.9",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz",
"integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==",
"version": "5.3.10",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
"integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.17",
"@jridgewell/trace-mapping": "^0.3.20",
"jest-worker": "^27.4.5",
"schema-utils": "^3.1.1",
"serialize-javascript": "^6.0.1",
"terser": "^5.16.8"
"terser": "^5.26.0"
},
"engines": {
"node": ">= 10.13.0"
@@ -12403,6 +12670,17 @@
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="
},
"node_modules/timers-browserify": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
"integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==",
"dependencies": {
"setimmediate": "^1.0.4"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -12725,6 +13003,18 @@
"requires-port": "^1.0.0"
}
},
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
"dependencies": {
"inherits": "^2.0.3",
"is-arguments": "^1.0.4",
"is-generator-function": "^1.0.7",
"is-typed-array": "^1.1.3",
"which-typed-array": "^1.1.2"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -13228,9 +13518,9 @@
}
},
"node_modules/webpack-dev-middleware": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz",
"integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==",
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz",
"integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==",
"dependencies": {
"colorette": "^2.0.10",
"memfs": "^3.4.3",

View File

@@ -18,12 +18,12 @@
"@vue/cli-service": "^5.0.8",
"@vue/test-utils": "1.0.0-beta.29",
"amplitude-js": "^8.21.3",
"axios": "^0.27.2",
"assert": "^2.1.0",
"axios": "^0.28.0",
"axios-progress-bar": "^1.2.0",
"babel-eslint": "^10.1.0",
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.23.1",
"chai": "^4.3.7",
"core-js": "^3.33.1",
"dompurify": "^3.0.3",
"eslint": "7.32.0",
@@ -32,16 +32,18 @@
"eslint-plugin-vue": "7.20.0",
"habitica-markdown": "^3.0.0",
"hellojs": "^1.20.0",
"inspectpack": "^4.7.1",
"intro.js": "^7.2.0",
"jquery": "^3.7.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"moment-locales-webpack-plugin": "^1.2.0",
"nconf": "^0.12.1",
"sass": "^1.63.4",
"sass-loader": "^8.0.2",
"sass-loader": "^14.1.1",
"sinon": "^17.0.1",
"smartbanner.js": "^1.19.3",
"stopword": "^2.0.8",
"timers-browserify": "^2.0.12",
"uuid": "^9.0.1",
"validator": "^13.9.0",
"vue": "^2.7.10",
@@ -51,10 +53,14 @@
"vue-template-babel-compiler": "^2.0.0",
"vue-template-compiler": "^2.7.10",
"vuedraggable": "^2.24.3",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
"webpack": "^5.89.0"
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.21.0"
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"babel-plugin-lodash": "^3.3.4",
"chai": "^5.1.0",
"inspectpack": "^4.7.1",
"terser-webpack-plugin": "^5.3.10",
"webpack": "^5.89.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -217,16 +217,13 @@
.btn-show-more {
display: block;
width: 50%;
max-width: 448px;
margin: 0 auto;
margin-top: 12px;
width: 100%;
padding: 8px;
font-size: 14px;
line-height: 1.43;
font-weight: bold;
text-align: center;
background: $gray-600;
background: $gray-500;
color: $gray-200 !important; // Otherwise it gets ignored when used on an A element
box-shadow: none;

View File

@@ -0,0 +1,52 @@
@import '~@/assets/scss/colors.scss';
h1 {
margin-top: 0px;
line-height: 1.33;
}
li {
padding-bottom: 4px;
li {
&:first-of-type {
padding-top: 4px;
}
li li {
list-style-type: disc;
}
}
}
p {
margin-bottom: 21px;
}
ul {
padding-left: 20px;
}
.top-container {
width: 66.67%;
margin-top: 80px;
display: flex;
@media (max-width: 1024px) {
flex-wrap: wrap;
}
}
.main-text {
h3 {
font-size: 1.15em;
font-weight: 400;
line-height: 1.75;
color: $purple-200;
}
.body-text {
font-size: 1em;
color: $gray-10;
line-height: 1.71;
}
}

View File

@@ -12,7 +12,7 @@
}
&.color {
svg path {
svg path, svg polygon {
fill: currentColor;
}
}

View File

@@ -1,5 +1,11 @@
// TODO move to item component?
.item, .item-wrapper, .item > div > div {
&:focus-visible {
outline: none;
}
}
.items > div {
display: inline-block;
margin-right: 24px;
@@ -9,34 +15,22 @@
position: relative;
display: inline-block;
margin-bottom: 12px;
&:focus {
outline: 2px solid $purple-400;
border-radius: 2px;
}
&:hover {
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
border-radius: 4px;
}
}
.items-one-line .item-wrapper {
margin-bottom: 8px;
}
.item.pet-slot {
// Desktop XL (1440)
@media only screen and (min-width: 1440px){
margin-right: 1.71em;
}
// Desktop L (1280)
@media only screen and (min-width: 1280px) and (max-width: 1439px) {
margin-right: 0.43em;
}
// Desktop M (1024)
@media only screen and (min-width: 1024px) and (max-width: 1279px) {
margin-right: 0.86em;
}
// Tablets and mobile
@media only screen and (max-width: 1023px) {
margin-right: 1.71em;
}
}
.item {
position: relative;
width: 94px;
@@ -56,11 +50,6 @@
background: $purple-500;
}
&:hover {
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
border-color: $purple-400;
}
&.highlight {
box-shadow: 0 0 8px 8px rgba($black, 0.16), 0 5px 10px 0 rgba($black, 0.12) !important;
}
@@ -70,9 +59,15 @@
}
}
.flat .item {
box-shadow: none;
border: none;
.flat {
.item {
box-shadow: none;
border: none;
}
.item-wrapper:hover {
box-shadow: none;
}
}
.bordered-item .item {

View File

@@ -0,0 +1,90 @@
.featured-label {
margin: 24px auto;
}
.group {
display: inline-block;
width: 33%;
margin-bottom: 24px;
.items {
border-radius: 2px;
background-color: #edecee;
display: inline-block;
padding: 8px;
}
.item-wrapper {
margin-bottom: 0;
}
.items > div:not(:last-of-type) {
margin-right: 16px;
}
}
.timeTravelers {
.standard-page {
position: relative;
}
.badge-pin:not(.pinned) {
display: none;
}
.item:hover .badge-pin {
display: block;
}
.avatar {
cursor: default;
margin: 0 auto;
}
.featuredItems {
height: 192px;
.background {
background-repeat: repeat-x;
width: 100%;
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.background-open, .background-closed {
height: 216px;
}
.content {
display: flex;
flex-direction: column;
}
.npc {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 216px;
background-repeat: no-repeat;
&.closed {
background-repeat: no-repeat;
}
.featured-label {
position: absolute;
bottom: -14px;
margin: 0;
left: 79px;
}
}
}
}

View File

@@ -158,7 +158,6 @@ function collateItemData (self) {
if (
// ignore items the user owns because we captured them above:
!(key in ownedItems)
&& allItems[key].price > 0
) {
const item = allItems[key];
itemData.push({

View File

@@ -282,20 +282,16 @@ export default {
item.modified = true;
// for non-integer items, toggle through the allowed values:
if (item.itemType === 'gear') {
// Allowed starting values are true, false, and '' (never owned)
// Allowed values to switch to are true and false
item.value = !item.value;
} else if (item.itemType === 'mounts') {
// Allowed starting values are true, null, and "never owned"
// Allowed values to switch to are true and null
if (item.value === true) {
item.value = null;
if (item.itemType === 'gear' || item.itemType === 'mounts') {
// Allowed starting values are true, false, and undefined (never owned)
if (item.value && item.value !== '') {
item.value = false;
} else if (typeof item.value === 'boolean') {
item.value = '';
} else {
item.value = true;
}
}
// @TODO add a delete option
},
},
};

View File

@@ -28,15 +28,15 @@
<div class="form-group">
<label>About</label>
<div class="row about-row">
<textarea
v-model="hero.profile.blurb"
class="form-control col"
rows="10"
></textarea>
<div
v-markdown="hero.profile.blurb"
class="markdownPreview col"
></div>
<textarea
v-model="hero.profile.blurb"
class="form-control col"
rows="10"
></textarea>
<div
v-markdown="hero.profile.blurb"
class="markdownPreview col"
></div>
</div>
</div>
<input

View File

@@ -291,7 +291,44 @@
</div>
<div
v-if="!IS_PRODUCTION && isUserLoaded"
v-if="TIME_TRAVEL_ENABLED && user.permissions && user.permissions.fullAccess"
:key="lastTimeJump"
>
<a
class="btn btn-secondary mr-1"
@click="jumpTime(-1)"
>-1 Day</a>
<a
class="btn btn-secondary mr-1"
@click="jumpTime(-7)"
>-7 Days</a>
<a
class="btn btn-secondary mr-1"
@click="jumpTime(-30)"
>-30 Days</a>
<div class="my-2">
Time Traveling! It is {{ new Date().toLocaleDateString() }}
<a
class="btn btn-warning mr-1"
@click="resetTime()"
>Reset</a>
</div>
<a
class="btn btn-secondary mr-1"
@click="jumpTime(1)"
>+1 Day</a>
<a
class="btn btn-secondary mr-1"
@click="jumpTime(7)"
>+7 Days</a>
<a
class="btn btn-secondary mr-1"
@click="jumpTime(30)"
>+30 Days</a>
</div>
<div
v-if="DEBUG_ENABLED && isUserLoaded"
class="debug-toggle"
>
<button
@@ -772,6 +809,7 @@ h3 {
// modules
import axios from 'axios';
import moment from 'moment';
import Vue from 'vue';
// images
import melior from '@/assets/svg/melior.svg';
@@ -785,13 +823,24 @@ import heart from '@/assets/svg/heart.svg';
import { mapState } from '@/libs/store';
import buyGemsModal from './payments/buyGemsModal.vue';
import reportBug from '@/mixins/reportBug.js';
import { worldStateMixin } from '@/mixins/worldState';
const DEBUG_ENABLED = process.env.DEBUG_ENABLED === 'true'; // eslint-disable-line no-process-env
const TIME_TRAVEL_ENABLED = process.env.TIME_TRAVEL_ENABLED === 'true'; // eslint-disable-line no-process-env
let sinon;
if (TIME_TRAVEL_ENABLED) {
// eslint-disable-next-line global-require
sinon = await import('sinon');
}
const IS_PRODUCTION = process.env.NODE_ENV === 'production'; // eslint-disable-line no-process-env
export default {
components: {
buyGemsModal,
},
mixins: [reportBug],
mixins: [
reportBug,
worldStateMixin,
],
data () {
return {
icons: Object.freeze({
@@ -803,7 +852,9 @@ export default {
heart,
}),
debugMenuShown: false,
IS_PRODUCTION,
DEBUG_ENABLED,
TIME_TRAVEL_ENABLED,
lastTimeJump: null,
};
},
computed: {
@@ -865,6 +916,27 @@ export default {
'stats.mp': this.user.stats.mp + 10000,
});
},
async jumpTime (amount) {
const response = await axios.post('/api/v4/debug/jump-time', { offsetDays: amount });
if (amount > 0) {
Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000);
} else {
Vue.config.clock.setSystemTime(moment().add(amount, 'days').toDate());
}
this.lastTimeJump = response.data.data.time;
this.triggerGetWorldState(true);
},
async resetTime () {
const response = await axios.post('/api/v4/debug/jump-time', { reset: true });
const time = new Date(response.data.data.time);
Vue.config.clock.restore();
Vue.config.clock = sinon.useFakeTimers({
now: time,
shouldAdvanceTime: true,
});
this.lastTimeJump = response.data.data.time;
this.triggerGetWorldState(true);
},
addExp () {
// @TODO: Name these variables better
let exp = 0;

View File

@@ -35,7 +35,7 @@
<span :class="[skinClass, specialMountClass]"></span>
<!-- eslint-disable max-len-->
<span
:class="[member.preferences.size + '_shirt_' + member.preferences.shirt, specialMountClass]"
:class="[shirtClass, specialMountClass]"
></span>
<!-- eslint-enable max-len-->
<span :class="['head_0', specialMountClass]"></span>
@@ -46,12 +46,10 @@
<template
v-for="type in ['bangs', 'base', 'mustache', 'beard']"
>
<!-- eslint-disable max-len-->
<span
:key="type"
:class="['hair_' + type + '_' + member.preferences.hair[type] + '_' + member.preferences.hair.color, specialMountClass]"
:class="[hairClass(type), specialMountClass]"
></span>
<!-- eslint-enable max-len-->
</template>
<span :class="[getGearClass('body'), specialMountClass]"></span>
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
@@ -233,10 +231,20 @@ export default {
},
skinClass () {
if (!this.member) return '';
if (this.overrideAvatarGear?.skin) {
return `skin_${this.overrideAvatarGear.skin}`;
}
const baseClass = `skin_${this.member.preferences.skin}`;
return `${baseClass}${this.member.preferences.sleep ? '_sleep' : ''}`;
},
shirtClass () {
if (!this.member) return '';
if (this.overrideAvatarGear?.shirt) {
return `${this.member.preferences.size}_shirt_${this.overrideAvatarGear.shirt}`;
}
return `${this.member.preferences.size}_shirt_${this.member.preferences.shirt}`;
},
costumeClass () {
return this.member?.preferences.costume ? 'costume' : 'equipped';
},
@@ -269,6 +277,17 @@ export default {
return result;
},
hairClass (slot) {
if (this.overrideAvatarGear?.hair) {
if (this.overrideAvatarGear.hair[slot]) {
return `hair_${slot}_${this.overrideAvatarGear.hair[slot]}_${this.member.preferences.hair.color}`;
}
if (this.overrideAvatarGear.hair.color) {
return `hair_${slot}_${this.member.preferences.hair[slot]}_${this.overrideAvatarGear.hair.color}`;
}
}
return `hair_${slot}_${this.member.preferences.hair[slot]}_${this.member.preferences.hair.color}`;
},
hideGear (gearType) {
if (!this.member) return true;
if (gearType === 'weapon') {

View File

@@ -1,7 +1,8 @@
<template>
<div
id="body"
class="section customize-section"
class="customize-section d-flex flex-column"
:class="{ 'justify-content-between': editing }"
>
<sub-menu
class="text-center"
@@ -17,17 +18,11 @@
</div>
<div v-if="activeSubPage === 'shirt'">
<customize-options
:items="freeShirts"
:items="userShirts"
:current-value="user.preferences.shirt"
/>
<customize-options
v-if="editing"
:items="specialShirts"
:current-value="user.preferences.shirt"
:full-set="!userOwnsSet('shirt', specialShirtKeys)"
@unlock="unlock(`shirt.${specialShirtKeys.join(',shirt.')}`)"
/>
</div>
<customize-banner v-if="editing" />
</div>
</template>
@@ -35,33 +30,27 @@
import appearance from '@/../../common/script/content/appearance';
import { subPageMixin } from '../../mixins/subPage';
import { userStateMixin } from '../../mixins/userState';
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
import subMenu from './sub-menu';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
import customizeBanner from './customize-banner.vue';
import customizeOptions from './customize-options';
import gem from '@/assets/svg/gem.svg';
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
import subMenu from './sub-menu';
export default {
components: {
subMenu,
customizeBanner,
customizeOptions,
},
mixins: [
subPageMixin,
userStateMixin,
avatarEditorUtilies,
avatarEditorUtilities,
],
props: [
'editing',
],
data () {
return {
specialShirtKeys,
icons: Object.freeze({
gem,
}),
items: [
{
id: 'size',
@@ -78,25 +67,19 @@ export default {
sizes () {
return ['slim', 'broad'].map(s => this.mapKeysToFreeOption(s, 'size'));
},
freeShirts () {
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
},
specialShirts () {
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.specialShirtKeys;
const options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
return options;
userShirts () {
const freeShirts = Object.keys(appearance.shirt)
.filter(k => appearance.shirt[k].price === 0)
.map(s => this.mapKeysToFreeOption(s, 'shirt'));
const ownedShirts = Object.keys(this.user.purchased.shirt)
.filter(k => this.user.purchased.shirt[k])
.map(s => this.mapKeysToFreeOption(s, 'shirt'));
return [...freeShirts, ...ownedShirts];
},
},
mounted () {
this.changeSubPage('size');
},
methods: {
},
};
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,69 @@
<template>
<div class="bottom-banner">
<div class="d-flex justify-content-center align-items-center mt-3">
<span
class="svg svg-icon sparkles"
v-html="icons.sparkles"
></span>
<strong
v-once
class="mx-2"
> {{ $t('lookingForMore') }}
</strong>
<span
v-once
class="svg svg-icon sparkles mirror"
v-html="icons.sparkles"
></span>
</div>
<div
class="check-link"
>
<span>Check out the </span>
<a href="/shops/customizations">Customizations Shop</a>
<span> for even more ways to customize your avatar!</span>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.bottom-banner {
background: linear-gradient(114.26deg, $purple-300 0%, $purple-200 100%);
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
color: $white;
height: 80px;
line-height: 24px;
.check-link, a {
color: $purple-600;
}
a {
text-decoration: underline;
}
}
.sparkles {
width: 32px;
&.mirror {
transform: scaleX(-1);
}
}
</style>
<script>
import sparkles from '@/assets/svg/sparkles-left.svg';
export default {
data () {
return {
icons: Object.freeze({
sparkles,
}),
};
},
};
</script>

View File

@@ -1,14 +1,13 @@
<template>
<div
class="customize-options"
:class="{'background-set': fullSet}"
v-if="items.length > 1"
class="customize-options mb-4"
>
<div
v-for="option in items"
:key="option.key"
class="outer-option-background"
:class="{
locked: option.gemLocked || option.goldLocked,
premium: Boolean(option.gem),
active: option.active || currentValue === option.key,
none: option.none,
@@ -28,38 +27,6 @@
</div>
</div>
</div>
<div
v-if="option.gemLocked"
class="gem-lock"
>
<div
class="svg-icon gem"
v-html="icons.gem"
></div>
<span>{{ option.gem }}</span>
</div>
<div
v-if="option.goldLocked"
class="gold-lock"
>
<div
class="svg-icon gold"
v-html="icons.gold"
></div>
<span>{{ option.gold }}</span>
</div>
</div>
<div
v-if="fullSet"
class="purchase-set"
@click="unlock()"
>
<span class="label">{{ $t('purchaseAll') }}</span>
<div
class="svg-icon gem"
v-html="icons.gem"
></div>
<span class="price">5</span>
</div>
</div>
</template>
@@ -67,13 +34,13 @@
<script>
import gem from '@/assets/svg/gem.svg';
import gold from '@/assets/svg/gold.svg';
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
export default {
mixins: [
avatarEditorUtilies,
avatarEditorUtilities,
],
props: ['items', 'currentValue', 'fullSet'],
props: ['items', 'currentValue'],
data () {
return {
icons: Object.freeze({
@@ -150,7 +117,7 @@ export default {
&:not(.locked):not(.active) {
.option:hover {
background-color: rgba(213, 200, 255, .32);
background-color: rgba($purple-300, .25);
}
}
@@ -216,9 +183,6 @@ export default {
margin-top: 0;
margin-left: 0;
&.color-bangs {
margin-top: 3px;
}
&.skin {
margin-top: -4px;
margin-left: -4px;
@@ -237,14 +201,14 @@ export default {
margin-top: -5px;
}
}
&.color, &.bangs {
margin-top: 4px;
margin-left: -3px;
&.color, &.bangs, &.beard, &.flower, &.mustache {
background-position-x: -6px;
background-position-y: -12px;
}
&.hair.base {
margin-top: 0px;
margin-left: -5px;
background-position-x: -6px;
background-position-y: -4px;
}
&.headAccessory {
@@ -258,89 +222,4 @@ export default {
}
}
}
.text-center {
.gem-lock, .gold-lock {
display: inline-block;
margin: 0 auto 8px;
vertical-align: bottom;
}
}
.gem-lock, .gold-lock {
.svg-icon {
width: 16px;
}
span {
font-weight: bold;
margin-left: .5em;
}
.svg-icon, span {
display: inline-block;
vertical-align: bottom;
}
}
.gem-lock span {
color: $green-10
}
.purchase-set {
background: #fff;
padding: 0.5em;
border-radius: 0 0 2px 2px;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
cursor: pointer;
span {
font-weight: bold;
font-size: 12px;
}
span.price {
color: #24cc8f;
}
.gem, .coin {
width: 16px;
}
&.single {
width: 141px;
}
width: 100%;
span {
font-size: 14px;
}
.gem, .coin {
width: 20px;
margin: 0 .5em;
display: inline-block;
vertical-align: bottom;
}
}
.background-set {
background-color: #edecee;
border-radius: 2px;
padding-top: 12px;
margin-left: 12px;
margin-right: 12px;
margin-bottom: 12px;
width: calc(100% - 24px);
padding-left: 0;
padding-right: 0;
max-width: unset; // disable col12 styling
flex: unset;
}
</style>

View File

@@ -1,7 +1,8 @@
<template>
<div
id="extra"
class="section container customize-section"
class="customize-section d-flex flex-column"
:class="{ 'justify-content-between': !showEmptySection}"
>
<sub-menu
class="text-center"
@@ -20,9 +21,8 @@
id="animal-ears"
>
<customize-options
v-if="animalItems('back').length > 0"
:items="animalItems('headAccessory')"
:full-set="!animalItemsOwned('headAccessory')"
@unlock="unlock(animalItemsUnlockString('headAccessory'))"
/>
</div>
<div
@@ -30,9 +30,8 @@
id="animal-tails"
>
<customize-options
v-if="animalItems('back').length > 0"
:items="animalItems('back')"
:full-set="!animalItemsOwned('back')"
@unlock="unlock(animalItemsUnlockString('back'))"
/>
</div>
<div
@@ -53,6 +52,24 @@
>
<customize-options :items="flowers" />
</div>
<div
v-if="showEmptySection"
class="my-5"
>
<h3
v-once
>
{{ $t('noItemsOwned') }}
</h3>
<p
v-once
class="w-50 mx-auto"
v-html="$t('visitCustomizationsShop')"
></p>
</div>
<customize-banner
v-else-if="editing"
/>
</div>
</template>
@@ -60,23 +77,24 @@
import appearance from '@/../../common/script/content/appearance';
import { subPageMixin } from '../../mixins/subPage';
import { userStateMixin } from '../../mixins/userState';
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
import subMenu from './sub-menu';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
import customizeBanner from './customize-banner';
import customizeOptions from './customize-options';
import gem from '@/assets/svg/gem.svg';
import subMenu from './sub-menu';
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
export default {
components: {
subMenu,
customizeBanner,
customizeOptions,
subMenu,
},
mixins: [
subPageMixin,
userStateMixin,
avatarEditorUtilies,
avatarEditorUtilities,
],
props: [
'editing',
@@ -89,9 +107,6 @@ export default {
},
chairKeys: ['none', 'black', 'blue', 'green', 'pink', 'red', 'yellow', 'handleless_black', 'handleless_blue', 'handleless_green', 'handleless_pink', 'handleless_red', 'handleless_yellow'],
specialShirtKeys,
icons: Object.freeze({
gem,
}),
items: [
{
id: 'size',
@@ -178,7 +193,7 @@ export default {
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
},
specialShirts () {
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.specialShirtKeys;
const options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
return options;
@@ -193,6 +208,11 @@ export default {
for (const key of keys) {
const option = this.createGearItem(key, 'headAccessory', 'special', 'headband');
const newKey = `headAccessory_special_${key}`;
option.click = () => {
const type = this.user.preferences.costume ? 'costume' : 'equipped';
return this.equip(newKey, type);
};
options.push(option);
}
@@ -222,12 +242,22 @@ export default {
option.none = true;
}
option.active = this.user.preferences.hair.flower === key;
option.class = `hair_flower_${key} flower`;
option.class = `icon_hair_flower_${key} flower`;
option.click = () => this.set({ 'preferences.hair.flower': key });
return option;
});
return options;
},
showEmptySection () {
switch (this.activeSubPage) {
case 'ears':
return this.editing && this.animalItems('headAccessory').length === 1;
case 'tails':
return this.editing && this.animalItems('back').length === 1;
default:
return false;
}
},
},
mounted () {
this.changeSubPage(this.extraSubMenuItems[0].id);
@@ -236,7 +266,7 @@ export default {
animalItems (category) {
// @TODO: For some resonse when I use $set on the
// user purchases object, this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.animalItemKeys[category];
const noneOption = this.createGearItem(0, category, 'base', category);
@@ -248,36 +278,22 @@ export default {
for (const key of keys) {
const newKey = `${category}_special_${key}`;
const userPurchased = this.user.items.gear.owned[newKey];
const option = {};
option.key = key;
option.active = this.user.preferences.costume
? this.user.items.gear.costume[category] === newKey
: this.user.items.gear.equipped[category] === newKey;
option.class = `headAccessory_special_${option.key} ${category}`;
if (category === 'back') {
option.class = `icon_back_special_${option.key} back`;
}
option.gemLocked = userPurchased === undefined;
option.goldLocked = userPurchased === false;
if (option.goldLocked) {
option.gold = 20;
}
if (option.gemLocked) {
option.gem = 2;
}
option.locked = option.gemLocked || option.goldLocked;
option.click = () => {
if (option.gemLocked) {
return this.unlock(`items.gear.owned.${newKey}`);
} if (option.goldLocked) {
return this.buy(newKey);
if (userPurchased) {
const option = {};
option.key = key;
option.active = this.user.preferences.costume
? this.user.items.gear.costume[category] === newKey
: this.user.items.gear.equipped[category] === newKey;
option.class = `headAccessory_special_${option.key} ${category}`;
if (category === 'back') {
option.class = `icon_back_special_${option.key} back`;
}
const type = this.user.preferences.costume ? 'costume' : 'equipped';
return this.equip(newKey, type);
};
options.push(option);
option.click = () => {
const type = this.user.preferences.costume ? 'costume' : 'equipped';
return this.equip(newKey, type);
};
options.push(option);
}
}
return options;
@@ -287,17 +303,6 @@ export default {
return keys.join(',');
},
animalItemsOwned (category) {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
let own = true;
this.animalItemKeys[category].forEach(key => {
if (this.user.items.gear.owned[`${category}_special_${key}`] === undefined) own = false;
});
return own;
},
createGearItem (key, gearType, subGearType, additionalClass) {
const newKey = `${gearType}_${subGearType ? `${subGearType}_` : ''}${key}`;
const option = {};
@@ -339,7 +344,3 @@ export default {
},
};
</script>
<style scoped>
</style>

View File

@@ -1,7 +1,8 @@
<template>
<div
id="hair"
class="section customize-section"
class="customize-section d-flex flex-column"
:class="{ 'justify-content-between': editing && !showEmptySection}"
>
<sub-menu
class="text-center"
@@ -14,37 +15,9 @@
id="hair-color"
>
<customize-options
:items="freeHairColors"
:items="userHairColors"
:current-value="user.preferences.hair.color"
/>
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
<div
v-for="set in seasonalHairColors"
v-if="editing && set.key !== 'undefined'"
:key="set.key"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<customize-options
:items="set.options"
:current-value="user.preferences.hair.color"
:full-set="!hideSet(set.key) && !userOwnsSet('hair', set.keys, 'color')"
@unlock="unlock(`hair.color.${set.keys.join(',hair.color.')}`)"
/>
</div>
</div>
<div
v-if="activeSubPage === 'style'"
id="style"
>
<!-- eslint-disable vue/require-v-for-key NO KEY AVAILABLE HERE -->
<div v-for="set in styleSets">
<customize-options
:items="set.options"
:full-set="set.fullSet"
@unlock="set.unlock()"
/>
</div>
<!-- eslint-enable vue/require-v-for-key -->
</div>
<div
v-if="activeSubPage === 'bangs'"
@@ -55,67 +28,73 @@
:current-value="user.preferences.hair.bangs"
/>
</div>
<div
v-if="activeSubPage === 'style'"
id="style"
>
<customize-options
:items="userHairStyles"
:current-value="user.preferences.hair.base"
/>
</div>
<div
v-if="activeSubPage === 'facialhair'"
id="facialhair"
>
<customize-options
v-if="editing"
:items="mustacheList"
v-if="userMustaches.length > 1"
:items="userMustaches"
/>
<!-- eslint-disable max-len -->
<customize-options
v-if="editing"
:items="beardList"
:full-set="isPurchaseAllNeeded('hair', ['baseHair5', 'baseHair6'], ['mustache', 'beard'])"
@unlock="unlock(`hair.mustache.${baseHair5Keys.join(',hair.mustache.')},hair.beard.${baseHair6Keys.join(',hair.beard.')}`)"
v-if="userBeards.length > 1"
:items="userBeards"
/>
<!-- eslint-enable max-len -->
<div
v-if="showEmptySection"
class="my-5"
>
<h3
v-once
>
{{ $t('noItemsOwned') }}
</h3>
<p
v-once
class="w-50 mx-auto"
v-html="$t('visitCustomizationsShop')"
></p>
</div>
</div>
<customize-banner
v-if="editing && !showEmptySection"
/>
</div>
</template>
<script>
import groupBy from 'lodash/groupBy';
import appearance from '@/../../common/script/content/appearance';
import appearanceSets from '@/../../common/script/content/appearance/sets';
import { subPageMixin } from '../../mixins/subPage';
import { userStateMixin } from '../../mixins/userState';
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
import subMenu from './sub-menu';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
import customizeBanner from './customize-banner';
import customizeOptions from './customize-options';
import gem from '@/assets/svg/gem.svg';
const hairColorBySet = groupBy(appearance.hair.color, 'set.key');
const freeHairColorKeys = hairColorBySet[undefined].map(s => s.key);
import subMenu from './sub-menu';
export default {
components: {
subMenu,
customizeBanner,
customizeOptions,
subMenu,
},
mixins: [
subPageMixin,
userStateMixin,
avatarEditorUtilies,
avatarEditorUtilities,
],
props: [
'editing',
],
data () {
return {
freeHairColorKeys,
icons: Object.freeze({
gem,
}),
baseHair1: [1, 3],
baseHair2Keys: [2, 4, 5, 6, 7, 8],
baseHair3Keys: [9, 10, 11, 12, 13, 14],
baseHair4Keys: [15, 16, 17, 18, 19, 20],
baseHair5Keys: [1, 2],
baseHair6Keys: [1, 2, 3],
};
},
computed: {
hairSubMenuItems () {
const items = [
@@ -142,91 +121,46 @@ export default {
return items;
},
freeHairColors () {
return freeHairColorKeys.map(s => this.mapKeysToFreeOption(s, 'hair', 'color'));
userHairColors () {
const freeHairColors = groupBy(appearance.hair.color, 'set.key')[undefined]
.map(s => s.key).map(s => this.mapKeysToFreeOption(s, 'hair', 'color'));
const ownedHairColors = Object.keys(this.user.purchased.hair.color || {})
.filter(k => this.user.purchased.hair.color[k])
.map(h => this.mapKeysToFreeOption(h, 'hair', 'color'));
return [...freeHairColors, ...ownedHairColors];
},
seasonalHairColors () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
userHairStyles () {
const emptyHairStyle = {
...this.mapKeysToFreeOption(0, 'hair', 'base'),
none: true,
};
const freeHairStyles = [1, 3].map(s => this.mapKeysToFreeOption(s, 'hair', 'base'));
const ownedHairStyles = Object.keys(this.user.purchased.hair.base || {})
.filter(k => this.user.purchased.hair.base[k])
.map(h => this.mapKeysToFreeOption(h, 'hair', 'base'));
return [emptyHairStyle, ...freeHairStyles, ...ownedHairStyles];
},
userMustaches () {
const emptyMustache = {
...this.mapKeysToFreeOption(0, 'hair', 'mustache'),
none: true,
};
const ownedMustaches = Object.keys(this.user.purchased.hair.mustache || {})
.filter(k => this.user.purchased.hair.mustache[k])
.map(h => this.mapKeysToFreeOption(h, 'hair', 'mustache'));
const seasonalHairColors = [];
for (const key of Object.keys(hairColorBySet)) {
const set = hairColorBySet[key];
return [emptyMustache, ...ownedMustaches];
},
userBeards () {
const emptyBeard = {
...this.mapKeysToFreeOption(0, 'hair', 'beard'),
none: true,
};
const ownedBeards = Object.keys(this.user.purchased.hair.beard || {})
.filter(k => this.user.purchased.hair.beard[k])
.map(h => this.mapKeysToFreeOption(h, 'hair', 'beard'));
const keys = set.map(item => item.key);
const options = keys.map(optionKey => {
const option = this.mapKeysToOption(optionKey, 'hair', 'color', key);
return option;
});
let text = this.$t(key);
if (appearanceSets[key] && appearanceSets[key].text) {
text = appearanceSets[key].text();
}
const compiledSet = {
key,
options,
keys,
text,
};
seasonalHairColors.push(compiledSet);
}
return seasonalHairColors;
},
premiumHairColors () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.premiumHairColorKeys;
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'color'));
return options;
},
baseHair2 () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.baseHair2Keys;
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'base'));
return options;
},
baseHair3 () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.baseHair3Keys;
const options = keys.map(key => {
const option = this.mapKeysToOption(key, 'hair', 'base');
return option;
});
return options;
},
baseHair4 () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.baseHair4Keys;
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'base'));
return options;
},
baseHair5 () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.baseHair5Keys;
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'mustache'));
return options;
},
baseHair6 () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.baseHair6Keys;
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'beard'));
return options;
return [emptyBeard, ...ownedBeards];
},
hairBangs () {
const none = this.mapKeysToFreeOption(0, 'hair', 'bangs');
@@ -236,136 +170,13 @@ export default {
return [none, ...options];
},
mustacheList () {
const noneOption = this.mapKeysToFreeOption(0, 'hair', 'mustache');
noneOption.none = true;
return [noneOption, ...this.baseHair5];
},
beardList () {
const noneOption = this.mapKeysToFreeOption(0, 'hair', 'beard');
noneOption.none = true;
return [noneOption, ...this.baseHair6];
},
styleSets () {
const sets = [];
const emptyHairBase = {
...this.mapKeysToFreeOption(0, 'hair', 'base'),
none: true,
};
sets.push({
options: [
emptyHairBase,
...this.baseHair1.map(key => this.mapKeysToFreeOption(key, 'hair', 'base')),
],
});
if (this.editing) {
sets.push({
fullSet: !this.userOwnsSet('hair', this.baseHair3Keys, 'base'),
unlock: () => this.unlock(`hair.base.${this.baseHair3Keys.join(',hair.base.')}`),
options: [
...this.baseHair3,
],
});
sets.push({
fullSet: !this.userOwnsSet('hair', this.baseHair4Keys, 'base'),
unlock: () => this.unlock(`hair.base.${this.baseHair4Keys.join(',hair.base.')}`),
options: [
...this.baseHair4,
],
});
}
if (this.editing) {
sets.push({
fullSet: !this.userOwnsSet('hair', this.baseHair2Keys, 'base'),
unlock: () => this.unlock(`hair.base.${this.baseHair2Keys.join(',hair.base.')}`),
options: [
...this.baseHair2,
],
});
}
return sets;
showEmptySection () {
return this.activeSubPage === 'facialhair'
&& this.userMustaches.length === 1 && this.userBeards.length === 1;
},
},
mounted () {
this.changeSubPage('color');
},
methods: {
/**
* Allows you to find out whether you need the "Purchase All" button or not.
* If there are more than 2 unpurchased items, returns true, otherwise returns false.
* @param {string} category - The selected category.
* @param {string[]} keySets - The items keySets.
* @param {string[]} [types] - The items types (subcategories). Optional.
* @returns {boolean} - Determines whether the "Purchase All" button
* is needed (true) or not (false).
*/
isPurchaseAllNeeded (category, keySets, types) {
const purchasedItemsLengths = [];
// If item types are specified, count them
if (types && types.length > 0) {
// Types can be undefined, so we must check them.
types.forEach(type => {
if (this.user.purchased[category][type]) {
purchasedItemsLengths
.push(Object.keys(this.user.purchased[category][type]).length);
}
});
} else {
let purchasedItemsCounter = 0;
// If types are not specified, recursively
// search for purchased items in the category
const findPurchasedItems = item => {
if (typeof item === 'object') {
Object.values(item)
.forEach(innerItem => {
if (typeof innerItem === 'boolean' && innerItem === true) {
purchasedItemsCounter += 1;
}
return findPurchasedItems(innerItem);
});
}
return purchasedItemsCounter;
};
findPurchasedItems(this.user.purchased[category]);
if (purchasedItemsCounter > 0) {
purchasedItemsLengths.push(purchasedItemsCounter);
}
}
// We don't need to count the key sets (below)
// if there are no purchased items at all.
if (purchasedItemsLengths.length === 0) {
return true;
}
const allItemsLengths = [];
// Key sets must be specify correctly.
keySets.forEach(keySet => {
allItemsLengths.push(Object.keys(this[keySet]).length);
});
// Simply sum all the length values and
// write them into variables for the convenience.
const allItems = allItemsLengths.reduce((acc, val) => acc + val);
const purchasedItems = purchasedItemsLengths.reduce((acc, val) => acc + val);
const unpurchasedItems = allItems - purchasedItems;
return unpurchasedItems > 2;
},
},
};
</script>
<style scoped>
</style>

View File

@@ -1,7 +1,8 @@
<template>
<div
id="skin"
class="section customize-section"
class="customize-section d-flex flex-column"
:class="{ 'justify-content-between': editing }"
>
<sub-menu
class="text-center"
@@ -10,63 +11,39 @@
@changeSubPage="changeSubPage($event)"
/>
<customize-options
:items="freeSkins"
:items="userSkins"
:current-value="user.preferences.skin"
/>
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
<div
v-for="set in seasonalSkins"
v-if="editing && set.key !== 'undefined'"
:key="set.key"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<customize-options
:items="set.options"
:current-value="user.preferences.skin"
:full-set="!hideSet(set.key) && !userOwnsSet('skin', set.keys)"
@unlock="unlock(`skin.${set.keys.join(',skin.')}`)"
/>
</div>
<customize-banner v-if="editing" />
</div>
</template>
<script>
import groupBy from 'lodash/groupBy';
import appearance from '@/../../common/script/content/appearance';
import appearanceSets from '@/../../common/script/content/appearance/sets';
import { subPageMixin } from '../../mixins/subPage';
import { userStateMixin } from '../../mixins/userState';
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
import subMenu from './sub-menu';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
import customizeBanner from './customize-banner.vue';
import customizeOptions from './customize-options';
import gem from '@/assets/svg/gem.svg';
const skinsBySet = groupBy(appearance.skin, 'set.key');
const freeSkinKeys = skinsBySet[undefined].map(s => s.key);
// const specialSkinKeys = Object.keys(appearance.shirt)
// .filter(k => appearance.shirt[k].price !== 0);
import subMenu from './sub-menu';
export default {
components: {
subMenu,
customizeBanner,
customizeOptions,
},
mixins: [
subPageMixin,
userStateMixin,
avatarEditorUtilies,
avatarEditorUtilities,
],
props: [
'editing',
],
data () {
return {
freeSkinKeys,
icons: Object.freeze({
gem,
}),
skinSubMenuItems: [
{
id: 'color',
@@ -76,41 +53,13 @@ export default {
};
},
computed: {
freeSkins () {
return freeSkinKeys.map(s => this.mapKeysToFreeOption(s, 'skin'));
},
seasonalSkins () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const seasonalSkins = [];
for (const setKey of Object.keys(skinsBySet)) {
const set = skinsBySet[setKey];
const keys = set.map(item => item.key);
const options = keys.map(optionKey => {
const option = this.mapKeysToOption(optionKey, 'skin', '', setKey);
return option;
});
let text = this.$t(setKey);
if (appearanceSets[setKey] && appearanceSets[setKey].text) {
text = appearanceSets[setKey].text();
}
const compiledSet = {
key: setKey,
options,
keys,
text,
};
seasonalSkins.push(compiledSet);
}
return seasonalSkins;
userSkins () {
const freeSkins = groupBy(appearance.skin, 'set.key')[undefined]
.map(s => s.key).map(s => this.mapKeysToFreeOption(s, 'skin'));
const ownedSkins = Object.keys(this.user.purchased.skin)
.filter(k => this.user.purchased.skin[k])
.map(s => this.mapKeysToFreeOption(s, 'skin'));
return [...freeSkins, ...ownedSkins];
},
},
mounted () {

File diff suppressed because it is too large Load Diff

View File

@@ -134,6 +134,12 @@
>
{{ $t('quests') }}
</router-link>
<router-link
class="topbar-dropdown-item dropdown-item"
:to="{name: 'customizations'}"
>
{{ $t('customizations') }}
</router-link>
<router-link
class="topbar-dropdown-item dropdown-item"
:to="{name: 'seasonal'}"

View File

@@ -35,13 +35,9 @@
/>
</a>
<a
class="topbar-dropdown-item dropdown-item"
class="topbar-dropdown-item dropdown-item dropdown-separated"
@click="showAvatar('body', 'size')"
>{{ $t('editAvatar') }}</a>
<a
class="topbar-dropdown-item dropdown-item dropdown-separated"
@click="showAvatar('backgrounds', '2024')"
>{{ $t('backgrounds') }}</a>
<a
class="topbar-dropdown-item dropdown-item"
@click="showProfile('profile')"

View File

@@ -66,6 +66,7 @@
:right="true"
:hide-icon="false"
:inline-dropdown="false"
:direct-select="true"
@select="groupBy = $event"
>
<template #item="{ item }">

View File

@@ -444,7 +444,7 @@ export default {
const isSearched = !searchText || item.text()
.toLowerCase()
.indexOf(searchText) !== -1;
if (isSearched) {
if (isSearched && item) {
itemsArray.push({
...item,
class: `${group.classPrefix}${item.key}`,

View File

@@ -134,56 +134,57 @@
v-for="(petGroup) in petGroups"
v-if="!anyFilterSelected || viewOptions[petGroup.key].selected"
:key="petGroup.key"
:class="{ hide: viewOptions[petGroup.key].animalCount === 0 }"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<h4 v-if="viewOptions[petGroup.key].animalCount !== 0">
{{ petGroup.label }}
</h4>
<!-- eslint-disable vue/no-use-v-if-with-v-for, max-len -->
<div
v-for="(group, key, index) in pets(petGroup, hideMissing, selectedSortBy, searchTextThrottled)"
v-if="index === 0 || $_openedItemRows_isToggled(petGroup.key)"
:key="key"
class="pet-row d-flex"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<div class="d-inline-flex flex-column">
<div
v-for="item in group"
v-show="show('pet', item)"
:key="item.key"
v-drag.drop.food="item.key"
class="pet-group"
:class="{'last': item.isLastInRow}"
@itemDragOver="onDragOver($event, item)"
@itemDropped="onDrop($event, item)"
@itemDragLeave="onDragLeave()"
v-for="(group, key, index) in pets(petGroup, hideMissing, selectedSortBy, searchTextThrottled)"
v-if="index === 0 || $_openedItemRows_isToggled(petGroup.key)"
:key="key"
class="pet-row d-flex"
>
<petItem
:item="item"
:popover-position="'top'"
:show-popover="currentDraggingFood == null"
:highlight-border="highlightPet == item.key"
@click="petClicked(item)"
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<div
v-for="item in group"
v-show="show('pet', item)"
:key="item.key"
v-drag.drop.food="item.key"
class="pet-group"
@itemDragOver="onDragOver($event, item)"
@itemDropped="onDrop($event, item)"
@itemDragLeave="onDragLeave()"
>
<template
slot="itemBadge"
slot-scope="context"
<petItem
:item="item"
:popover-position="'top'"
:show-popover="currentDraggingFood == null"
:highlight-border="highlightPet == item.key"
@click="petClicked(item)"
>
<equip-badge
:equipped="context.item.key === currentPet"
:show="isOwned('pet', context.item)"
@click="selectPet(context.item)"
/>
</template>
</petItem>
<template
slot="itemBadge"
slot-scope="context"
>
<equip-badge
:equipped="context.item.key === currentPet"
:show="isOwned('pet', context.item)"
@click="selectPet(context.item)"
/>
</template>
</petItem>
</div>
</div>
<show-more-button
v-if="petRowCount[petGroup.key] > 1 && petGroup.key !== 'specialPets' && !(petGroup.key === 'wackyPets' && selectedSortBy !== 'sortByColor')"
:show-all="$_openedItemRows_isToggled(petGroup.key)"
@click="setShowMore(petGroup.key)"
/>
</div>
<show-more-button
v-if="petRowCount[petGroup.key] > 1 && petGroup.key !== 'specialPets' && !(petGroup.key === 'wackyPets' && selectedSortBy !== 'sortByColor')"
:show-all="$_openedItemRows_isToggled(petGroup.key)"
class="show-more-button"
@click="setShowMore(petGroup.key)"
/>
</div>
<h2>
{{ $t('mounts') }}
@@ -196,52 +197,55 @@
v-for="mountGroup in mountGroups"
v-if="!anyFilterSelected || viewOptions[mountGroup.key].selected"
:key="mountGroup.key"
:class="{ hide: viewOptions[mountGroup.key].animalCount === 0 }"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<h4 v-if="viewOptions[mountGroup.key].animalCount != 0">
{{ mountGroup.label }}
</h4>
<!-- eslint-disable vue/no-use-v-if-with-v-for, max-len -->
<div
v-for="(group, key, index) in mounts(mountGroup, hideMissing, selectedSortBy, searchTextThrottled)"
v-if="index === 0 || $_openedItemRows_isToggled(mountGroup.key)"
:key="key"
class="pet-row d-flex"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<div class="d-inline-flex flex-column">
<div
v-for="item in group"
v-show="show('mount', item)"
:key="item.key"
class="pet-group"
v-for="(group, key, index) in mounts(mountGroup, hideMissing, selectedSortBy, searchTextThrottled)"
v-if="index === 0 || $_openedItemRows_isToggled(mountGroup.key)"
:key="key"
class="pet-row d-flex"
>
<mountItem
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<div
v-for="item in group"
v-show="show('mount', item)"
:key="item.key"
:item="item"
:popover-position="'top'"
:show-popover="true"
@click="selectMount(item)"
class="pet-group"
>
<span slot="popoverContent">
<h4 class="popover-content-title">{{ item.name }}</h4>
</span>
<template
slot="itemBadge"
<mountItem
:key="item.key"
:item="item"
:popover-position="'top'"
:show-popover="true"
@click="selectMount(item)"
>
<equip-badge
:equipped="item.key === currentMount"
:show="isOwned('mount', item)"
@click="selectMount(item)"
/>
</template>
</mountItem>
<span slot="popoverContent">
<h4 class="popover-content-title">{{ item.name }}</h4>
</span>
<template
slot="itemBadge"
>
<equip-badge
:equipped="item.key === currentMount"
:show="isOwned('mount', item)"
@click="selectMount(item)"
/>
</template>
</mountItem>
</div>
</div>
<show-more-button
v-if="mountRowCount[mountGroup.key] > 1 && mountGroup.key !== 'specialMounts'"
:show-all="$_openedItemRows_isToggled(mountGroup.key)"
@click="setShowMore(mountGroup.key)"
/>
</div>
<show-more-button
v-if="mountRowCount[mountGroup.key] > 1 && mountGroup.key !== 'specialMounts'"
:show-all="$_openedItemRows_isToggled(mountGroup.key)"
@click="setShowMore(mountGroup.key)"
/>
</div>
<inventoryDrawer>
<template
@@ -310,13 +314,8 @@
overflow: hidden;
}
.pet-row {
max-width: 100%;
flex-wrap: wrap;
.item {
margin-right: .5em;
}
.hide {
height: 0px;
}
</style>
@@ -330,6 +329,14 @@
display: inline-block;
}
.pet-row {
flex-wrap: wrap;
.pet-group:not(:last-of-type) {
margin-right: 24px;
}
}
.GreyedOut {
opacity: 0.3;
}
@@ -343,24 +350,11 @@
}
.stable {
.standard-page {
padding-right:0;
}
.standard-page .clearfix .float-right {
margin-right: 24px;
}
.svg-icon.inline.icon-16 {
vertical-align: bottom;
}
}
.last {
margin-right: 0 !important;
}
.no-focus:focus {
background-color: inherit;
color: inherit;

View File

@@ -0,0 +1,376 @@
<template>
<div class="sidebar py-4 d-flex flex-column">
<!-- staff -->
<div class="ml-4">
<h2>
{{ $t('staff') }}
</h2>
<div class="d-flex flex-wrap">
<div
v-for="user in staff"
:key="user.uuid"
class="staff col-6 p-0"
>
<div class="d-flex">
<router-link
class="title"
:to="{'name': 'userProfile', 'params': {'userId': user.uuid}}"
>
{{ user.name }}
</router-link>
<div
v-if="user.type === 'Staff'"
class="svg-icon staff-icon ml-1"
v-html="icons.tierStaff"
></div>
</div>
</div>
</div>
</div>
<!-- player tiers -->
<div class="ml-4">
<h2 class="mt-4 mb-1">
{{ $t('playerTiers') }}
</h2>
<ul class="tier-list">
<li
v-once
class="tier1 d-flex justify-content-center"
>
{{ $t('tier1') }}
<div
class="svg-icon ml-1"
v-html="icons.tier1"
></div>
</li>
<li
v-once
class="tier2 d-flex justify-content-center"
>
{{ $t('tier2') }}
<div
class="svg-icon ml-1"
v-html="icons.tier2"
></div>
</li>
<li
v-once
class="tier3 d-flex justify-content-center"
>
{{ $t('tier3') }}
<div
class="svg-icon ml-1"
v-html="icons.tier3"
></div>
</li>
<li
v-once
class="tier4 d-flex justify-content-center"
>
{{ $t('tier4') }}
<div
class="svg-icon ml-1"
v-html="icons.tier4"
></div>
</li>
<li
v-once
class="tier5 d-flex justify-content-center"
>
{{ $t('tier5') }}
<div
class="svg-icon ml-1"
v-html="icons.tier5"
></div>
</li>
<li
v-once
class="tier6 d-flex justify-content-center"
>
{{ $t('tier6') }}
<div
class="svg-icon ml-1"
v-html="icons.tier6"
></div>
</li>
<li
v-once
class="tier7 d-flex justify-content-center"
>
{{ $t('tier7') }}
<div
class="svg-icon ml-1"
v-html="icons.tier7"
></div>
</li>
<li
v-once
class="moderator d-flex justify-content-center"
>
{{ $t('tierModerator') }}
<div
class="svg-icon ml-1"
v-html="icons.tierMod"
></div>
</li>
<li
v-once
class="staff d-flex justify-content-center"
>
{{ $t('tierStaff') }}
<div
class="svg-icon ml-1"
v-html="icons.tierStaff"
></div>
</li>
<li
v-once
class="npc d-flex justify-content-center"
>
{{ $t('tierNPC') }}
</li>
</ul>
</div>
<!-- Daniel in sweet, sweet retirement with Jorts -->
<div>
<div class="gradient">
</div>
<div
class="grassy-meadow-backdrop"
:style="{'background-image': imageURLs.background}"
>
<div
class="daniel_front"
:style="{'background-image': imageURLs.npc}"
></div>
<div
class="pixel-border"
:style="{'background-image': imageURLs.pixel_border}"
></div>
</div>
</div>
<!-- email admin -->
<div class="d-flex flex-column justify-content-center">
<div class="question mx-auto">
{{ $t('anotherQuestion') }}
</div>
<div
class="contact mx-auto"
>
<p v-html="$t('contactAdmin')"></p>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.sidebar {
height: fit-content;
width: 330px;
background-color: $gray-700;
border-radius: 16px;
h2 {
color: $gray-10;
font-family: Roboto;
font-size: 14px;
font-weight: bold;
line-height: 1.71;
}
.staff {
.staff-icon {
width: 10px;
margin-top: 5px;
}
.title {
height: 24px;
color: $purple-300;
font-weight: bold;
display: inline-block;
margin-bottom: 4px;
}
}
.tier-list {
list-style-type: none;
padding: 0;
width: 282px;
font-size: 1em !important;
li {
height: 40px;
border-radius: 4px;
border: solid 1px $gray-500;
text-align: center;
padding: 8px 0;
margin-bottom: 8px;
margin-right: 4px;
font-weight: bold;
line-height: 1.71;
}
.tier1 {
color: #c42870;
.svg-icon {
width: 11px;
margin-top: 5px;
}
}
.tier2 {
color: #b01515;
.svg-icon {
width: 11px;
margin-top: 5px;
}
}
.tier3 {
color: #d70e14;
.svg-icon {
width: 13px;
margin-top: 4px;
}
}
.tier4 {
color: #c24d00;
.svg-icon {
width: 13px;
margin-top: 4px;
}
}
.tier5 {
color: #9e650f;
.svg-icon {
width: 8px;
margin-top: 7px;
}
}
.tier6 {
color: #2b8363;
.svg-icon {
width: 8px;
margin-top: 7px;
}
}
.tier7 {
color: #167e87;
.svg-icon {
width: 12px;
margin-top: 4px;
}
}
.moderator {
color: #277eab;
.svg-icon {
width: 13px;
margin-top: 3px;
}
}
.staff {
color: #6133b4;
.svg-icon {
width: 10px;
margin-top: 7px;
}
}
.npc {
color: $black;
}
}
.gradient {
position: absolute;
width: 330px;
height: 100px;
margin: -1px 0 116px;
background-image: linear-gradient(to bottom, $gray-700 0%, rgba(249, 249, 249, 0) 100%);
}
.grassy-meadow-backdrop {
background-repeat: repeat-x;
width: 330px;
height: 246px;
}
.daniel_front {
height: 246px;
width: 330px;
background-repeat: no-repeat;
margin: 0 auto;
}
.pixel-border {
width: 330px;
height: 30px;
background-repeat: no-repeat;
position: absolute;
margin-top: -30px;
}
.question {
font-size: 1em;
font-weight: bold;
line-height: 1.71;
color: $gray-10;
margin-top: 24px;
}
.contact p {
font-size: 1em;
margin-bottom: 0px;
}
}
</style>
<script>
import tier1 from '@/assets/svg/tier-1.svg';
import tier2 from '@/assets/svg/tier-2.svg';
import tier3 from '@/assets/svg/tier-3.svg';
import tier4 from '@/assets/svg/tier-4.svg';
import tier5 from '@/assets/svg/tier-5.svg';
import tier6 from '@/assets/svg/tier-6.svg';
import tier7 from '@/assets/svg/tier-7.svg';
import tierMod from '@/assets/svg/tier-mod.svg';
import tierNPC from '@/assets/svg/tier-npc.svg';
import tierStaff from '@/assets/svg/tier-staff.svg';
import staffList from '../../libs/staffList';
export default {
data () {
return {
icons: Object.freeze({
tier1,
tier2,
tier3,
tier4,
tier5,
tier6,
tier7,
tierMod,
tierNPC,
tierStaff,
}),
imageURLs: {
background: 'url(/static/npc/normal/tavern_background.png)',
npc: 'url(/static/npc/normal/tavern_npc.png)',
pixel_border: 'url(/static/npc/normal/pixel_border.png)',
},
staff: staffList,
};
},
};
</script>

Some files were not shown because too many files have changed in this diff Show More