Compare commits

...

27 Commits

Author SHA1 Message Date
Kalista Payne a92999fc11 5.47.6 2026-04-10 11:49:43 -05:00
Kalista Payne 3489b88752 fix(auth): downgrade helmet 2026-04-10 11:42:27 -05:00
Kalista Payne 94bda30385 5.47.5 2026-04-09 12:54:31 -05:00
Kalista Payne e8bbdc2cb8 fix(auth): disable broken CSP for now 2026-04-09 12:44:27 -05:00
Kalista Payne d465efaf96 fix(test): we have 1 item sets now 2026-04-08 15:35:47 -05:00
Kalista Payne 3a08de7ab3 fix(sorting): rerender task column for accuracy 2026-04-08 15:35:47 -05:00
Phillip Thelen e6ffd69148 feat(analytics): initial Habitica-owned solution 2026-04-08 15:35:47 -05:00
Kalista Payne 746fcfff49 Warning to avoid SPI (sensitive personal information) (#15638)
* feat(tasks): warn about adding SPI

* fix(links): unmangle, distinct jumps

* fix(spi): unlink

* fix(lint): punctuations
2026-04-08 15:35:47 -05:00
Kalista Payne 8aa343d390 5.47.4 2026-04-08 15:35:33 -05:00
Kalista Payne d80c43c82a test(ipn): log IPN data for troubleshooting 2026-04-08 15:35:03 -05:00
Kalista Payne 80e4b8617a fix(csp): habitica.com isn't *.habitica.com 2026-04-07 16:00:36 -05:00
Kalista Payne b3fac011a9 5.47.3 2026-04-07 15:04:01 -05:00
Weblate 8f4d871911 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (French)

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.0% (854 of 871 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.8% (852 of 871 strings)

Translated using Weblate (Japanese)

Currently translated at 97.7% (3472 of 3551 strings)

Translated using Weblate (Japanese)

Currently translated at 97.7% (432 of 442 strings)

Translated using Weblate (Japanese)

Currently translated at 97.6% (3467 of 3551 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (868 of 871 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (251 of 253 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 65.0% (13 of 20 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 97.2% (284 of 292 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 72.4% (2572 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.4% (188 of 191 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 96.8% (245 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.7% (860 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 96.5% (195 of 202 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 97.3% (111 of 114 strings)

Translated using Weblate (Japanese)

Currently translated at 97.2% (284 of 292 strings)

Translated using Weblate (Japanese)

Currently translated at 99.3% (144 of 145 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (868 of 871 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.9% (187 of 189 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 96.8% (245 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 95.9% (836 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 95.0% (192 of 202 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (943 of 943 strings)

Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Jitske van Meerten <jitskevmeerten@hotmail.com>
Co-authored-by: Serhii <serzh.photograf@gmail.com>
Co-authored-by: Sonia <sophishport@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/character/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/
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/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/front/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/front/fr/
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/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/en_GB/
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/questscontent/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ja/
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Content
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2026-04-07 16:51:25 +02:00
Hafiz 19373ce84d remove comment 2026-04-06 16:14:24 -05:00
Hafiz 55bfca20d9 comment 2026-04-06 16:04:43 -05:00
Kalista Payne 2ee2b05d1c Implement Content-Security-Policy (#15567)
* feat(security): implement CSP

* fix(csp): update helmet version to latest

* Squashed commit of the following:

commit cc6a35e61db07759c1f32716185543bc48bce760
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 17:27:50 2025 -0600

    fix(CSP): more Amazon domains

commit 985b86c29af866b2df942c21217d99390a2c6e92
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 17:18:08 2025 -0600

    fix(csp): more loggly allowance

commit 166bd315272f9c3a42652f3a026c27a88ed1a549
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 17:12:00 2025 -0600

    fix(csp): data, inline, some refactoring

commit 1a0a6c1806a53d43a7199bb2ef72cff610e908be
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 17:05:44 2025 -0600

    fix(CSP): override default script-src

commit 023d9886c835989da9c5901c168d66b572097dcf
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:56:24 2025 -0600

    fix(CSP): unsafe-eval in default-src

commit f51f0a0c93b60dfec7ce02be0ecd2587fc882fe0
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:52:14 2025 -0600

    fix(CSP): move trusted list to default-src

commit 83b2ba7688dea38abb651cf5c27482a7a3648374
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:38:05 2025 -0600

    fix(CSP): explicit habitica/aws in script-src

commit d5ca5172d5ad2fd8cec9402d7d2c9452c6ece7a1
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:31:38 2025 -0600

    fix(CSP): need escaped single quotes

commit c677a1ffeff5793b6da228924e68d2c2e47794b2
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:27:46 2025 -0600

    fix(CSP): unsafe-eval

commit 6ef35c3f7281c8426d9c333686be6bb65f00b3a8
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:15:07 2025 -0600

    fix(CSP): might need to skip entirely in dev but try no 'self'

commit 5759fb37d82fa61b474f01e9ce5e2dc461f6ceba
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 15:51:26 2025 -0600

    fix(csp): permit AWS in default-src

commit 9f238abf9373bc29715657b945b2247ef23c9224
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 5 17:22:25 2025 -0600

    fix(csp): update helmet version to latest

commit 9462e90f4f3058f4014137b3178b9751c5280e97
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Nov 25 09:27:05 2025 -0600

    feat(security): implement CSP

commit 72539f9ba3
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Dec 10 14:16:53 2025 -0600

    5.42.2

commit dabd466719
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Dec 10 14:16:48 2025 -0600

    Revert "Chat optimization (#15545)"

    This reverts commit 2917955ef0.

commit 8bf2304330
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Dec 10 14:15:48 2025 -0600

    chore(event): G1G1 date tweaks

commit 6937dc4e4e
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Dec 8 16:37:04 2025 -0600

    fix(subscription): couple more layout tweaks

* fix(csp): move unsafe-eval to default? ig?

* Revert "fix(csp): move unsafe-eval to default? ig?"

This reverts commit 90476cbf6c.

* fix(security): no unsafe! yay!

* fix(packages): remove webpack

* fix(lint): object destructuring

* fix(csp): remove Vue-Fragment

* wip(i18n): load Moment locale from cache

* fix(gulp): remove unneeded cache task

* fix(i18n): add Moment weekday abbrevs to translations

* fix(lint): destructuring
...why is this happening here and not develop lol

* fix(csp): add amplitude to whitelist

---------

Co-authored-by: Phillip Thelen <phillip@thelen.space>
2026-04-02 15:19:35 -05:00
Kalista Payne 3d3db1bdd9 Require email in social reg edge case (#15634)
* apply email passed via body if it is missing from apple profile

* Add Web UI to set email if apple does not provide one

* fix lint

* remove trailing space

* fix(register): show field if social auth without email

* fix(ux): add explanatory text

* fix(lint): max-len

* fix(data): remove unused field

* fix(auth): pass email around as necessary in Apple flow

* fix(auth): still wrong place argh

* Fix(auth): handle email in Apple registration flow

---------

Co-authored-by: Phillip Thelen <phillip@habitica.com>
Co-authored-by: Hafiz <hafizbhamidi@gmail.com>
2026-04-02 15:16:43 -05:00
Kalista Payne a5dff99fa1 5.47.2 2026-04-02 15:07:13 -05:00
Weblate 25435218e1 Translated using Weblate (Portuguese)
Currently translated at 59.5% (2116 of 3551 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 81.8% (239 of 292 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 66.9% (2379 of 3551 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (20 of 20 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 80.8% (236 of 292 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 66.3% (2357 of 3551 strings)

Translated using Weblate (Russian)

Currently translated at 82.0% (2912 of 3551 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (939 of 943 strings)

Translated using Weblate (Dutch)

Currently translated at 80.2% (699 of 871 strings)

Translated using Weblate (French)

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (20 of 20 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Korean)

Currently translated at 90.3% (103 of 114 strings)

Translated using Weblate (Korean)

Currently translated at 60.0% (12 of 20 strings)

Co-authored-by: Aleksandr <aichernyaev@yandex.ru>
Co-authored-by: Jildau Bras <jildaubras@gmail.com>
Co-authored-by: Maria Morant <luisa.morant@yahoo.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Viktor Révész <rviktor@ivankapal.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 윤태연 <bestpow123@naver.com>
Co-authored-by: ? <importantdata78@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/content/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/content/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hu/
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Content
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Subscriber
2026-04-01 16:15:29 +02:00
Fiz 7746049bb4 fix(groups): expired guild plans not showing in upgrade modal (#15637)
use dateCreated instead of customerId to identify previously upgraded groups. Team cron clears customerId on expired plans as a query optimization, which makes them invisible to the upgrade flow. dateCreated is set on every group plan since the feature was introduced and is never cleared, so it reliably indicates a group was previously upgraded.
2026-03-31 12:17:42 -05:00
Kalista Payne 036bf43cf3 5.47.1 2026-03-27 14:55:06 -05:00
Weblate c4e0127a37 Merge branch 'origin/develop' into Weblate. 2026-03-27 20:51:22 +01:00
Kalista Payne 7ae059e243 fix(sprites): restore missing animations 2026-03-27 14:44:57 -05:00
Weblate 81b9a0b92d Translated using Weblate (Dutch)
Currently translated at 72.9% (2589 of 3551 strings)

Translated using Weblate (Czech)

Currently translated at 92.9% (106 of 114 strings)

Translated using Weblate (Czech)

Currently translated at 99.4% (166 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.0% (18 of 20 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.4% (270 of 292 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.3% (439 of 442 strings)

Translated using Weblate (Portuguese)

Currently translated at 59.5% (2116 of 3551 strings)

Translated using Weblate (German)

Currently translated at 98.5% (3498 of 3551 strings)

Translated using Weblate (French)

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Portuguese)

Currently translated at 54.4% (152 of 279 strings)

Translated using Weblate (Portuguese)

Currently translated at 59.5% (2116 of 3551 strings)

Translated using Weblate (Portuguese)

Currently translated at 45.8% (116 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 45.8% (116 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 93.3% (14 of 15 strings)

Co-authored-by: Azuthrax <azuthrax@gmail.com>
Co-authored-by: Daniel Costa Carvalho <danielcostacarvalho@gmail.com>
Co-authored-by: Maria Morant <luisa.morant@yahoo.com>
Co-authored-by: Matej Boura <B.Matej@email.cz>
Co-authored-by: Mausam <mausam_b@protonmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@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/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/death/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt/
Translation: Habitica/Achievements
Translation: Habitica/Challenge
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Subscriber
2026-03-27 19:07:01 +01:00
Kalista Payne a6e87452a6 fix(background): add price for mobile logic 2026-03-27 09:42:06 -05:00
Kalista Payne c40d384913 5.47.0 2026-03-24 12:58:53 -05:00
Weblate 65144aef28 Translated using Weblate (Spanish)
Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (20 of 20 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 90.7% (401 of 442 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 93.6% (816 of 871 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 78.3% (2782 of 3551 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 91.5% (185 of 202 strings)

Translated using Weblate (French)

Currently translated at 99.7% (869 of 871 strings)

Translated using Weblate (French)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 75.8% (2693 of 3551 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (941 of 941 strings)

Translated using Weblate (French)

Currently translated at 99.0% (863 of 871 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 51.6% (47 of 91 strings)

Translated using Weblate (French)

Currently translated at 98.0% (854 of 871 strings)

Translated using Weblate (French)

Currently translated at 100.0% (20 of 20 strings)

Translated using Weblate (Japanese)

Currently translated at 99.3% (865 of 871 strings)

Translated using Weblate (Swedish)

Currently translated at 52.3% (146 of 279 strings)

Translated using Weblate (Swedish)

Currently translated at 80.0% (16 of 20 strings)

Translated using Weblate (Swedish)

Currently translated at 49.6% (1762 of 3551 strings)

Translated using Weblate (Swedish)

Currently translated at 81.6% (200 of 245 strings)

Translated using Weblate (Swedish)

Currently translated at 5.5% (14 of 253 strings)

Translated using Weblate (Swedish)

Currently translated at 70.7% (610 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 70.0% (14 of 20 strings)

Co-authored-by: Dayra G.R <ale.ro.bless1501@gmail.com>
Co-authored-by: Isabela de França <ifranceg@gmail.com>
Co-authored-by: Sam WIlson <sam.wils.2008@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/es/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/es/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/sv/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Subscriber
2026-03-24 01:38:59 +01:00
216 changed files with 2492 additions and 3970 deletions
+1
View File
@@ -21,3 +21,4 @@ services:
timeout: 30s
start_period: 0s
retries: 30
+169 -281
View File
@@ -1,12 +1,12 @@
{
"name": "habitica",
"version": "5.46.4",
"version": "5.47.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "habitica",
"version": "5.46.4",
"version": "5.47.6",
"hasInstallScript": true,
"dependencies": {
"@babel/core": "^7.22.10",
@@ -57,7 +57,7 @@
"micromustache": "^8.0.3",
"moment": "^2.29.4",
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
"mongoose": "^8.9.5",
"mongoose": "^8.23.0",
"morgan": "^1.10.1",
"nan": "^2.25.0",
"nconf": "^0.12.1",
@@ -81,7 +81,6 @@
"useragent": "^2.1.9",
"uuid": "^9.0.0",
"validator": "^13.11.0",
"webpack-bundle-analyzer": "^4.10.2",
"winston": "^3.10.0",
"winston-loggly-bulk": "^3.3.0",
"xml2js": "^0.6.2"
@@ -2624,6 +2623,19 @@
"node": ">=12.0.0"
}
},
"node_modules/@google-cloud/trace-agent/node_modules/gcp-metadata": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^5.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@google-cloud/trace-agent/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -3052,9 +3064,9 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/@mongodb-js/saslprep": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz",
"integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==",
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz",
"integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==",
"license": "MIT",
"dependencies": {
"sparse-bitfield": "^3.0.3"
@@ -3265,11 +3277,6 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/@polka/url": {
"version": "1.0.0-next.25",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz",
"integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ=="
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -3932,17 +3939,6 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.3",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz",
"integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -6316,6 +6312,7 @@
"resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
"integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"readable-stream": "^2.3.5",
"safe-buffer": "^5.1.1"
@@ -6325,13 +6322,15 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/bl/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -6347,6 +6346,7 @@
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
@@ -7751,11 +7751,6 @@
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-0.0.3.tgz",
"integrity": "sha512-Cp+jOa8QJef5nXS5hU7M1DWzXPEIoVR3kbV0dQuVGwROZg8bGf1DcCnkmajBTnvghTtSNMUdRrPjgaT6ZQucbw=="
},
"node_modules/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -11229,18 +11224,6 @@
"node": ">=12"
}
},
"node_modules/gcp-metadata": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
"dependencies": {
"gaxios": "^5.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -11984,6 +11967,19 @@
"node": ">=12"
}
},
"node_modules/google-auth-library/node_modules/gcp-metadata": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^5.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/google-auth-library/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -12516,20 +12512,6 @@
"node": ">= 0.10"
}
},
"node_modules/gzip-size": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
"integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
"dependencies": {
"duplexer": "^0.1.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/habitica-markdown": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/habitica-markdown/-/habitica-markdown-4.1.0.tgz",
@@ -12819,6 +12801,7 @@
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-4.6.0.tgz",
"integrity": "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
@@ -12871,7 +12854,8 @@
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
@@ -15557,128 +15541,14 @@
}
},
"node_modules/mongodb": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.4.tgz",
"integrity": "sha512-K5q8aBqEXMwWdVNh94UQTwZ6BejVbFhh1uB6c5FKtPE9eUMZPUO3sRZdgIEcHSrAWmxzpG/FeODDKL388sqRmw==",
"dev": true,
"dependencies": {
"bl": "^2.2.1",
"bson": "^1.1.4",
"denque": "^1.4.1",
"optional-require": "^1.1.8",
"safe-buffer": "^5.1.2"
},
"engines": {
"node": ">=4"
},
"optionalDependencies": {
"saslprep": "^1.0.0"
},
"peerDependenciesMeta": {
"aws4": {
"optional": true
},
"bson-ext": {
"optional": true
},
"kerberos": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"mongodb-extjson": {
"optional": true
},
"snappy": {
"optional": true
}
}
},
"node_modules/mongodb-connection-string-url": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz",
"integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==",
"license": "Apache-2.0",
"dependencies": {
"@types/whatwg-url": "^11.0.2",
"whatwg-url": "^14.1.0 || ^13.0.0"
}
},
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
"integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz",
"integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==",
"license": "MIT",
"dependencies": {
"tr46": "^5.0.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/mongodb/node_modules/bson": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz",
"integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==",
"dev": true,
"engines": {
"node": ">=0.6.19"
}
},
"node_modules/mongoose": {
"version": "8.9.7",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.7.tgz",
"integrity": "sha512-mvNXmU0V8qZzMR/qoK2mjT4Ti2ALdtfS0teK+twxhlGkwzOD76V02/zWajTu2MJ7QyEmZe9OWvnJsIY0iAuX3Q==",
"license": "MIT",
"dependencies": {
"bson": "^6.10.1",
"kareem": "2.6.3",
"mongodb": "~6.12.0",
"mpath": "0.9.0",
"mquery": "5.0.0",
"ms": "2.1.3",
"sift": "17.1.3"
},
"engines": {
"node": ">=16.20.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mongoose"
}
},
"node_modules/mongoose/node_modules/mongodb": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz",
"integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==",
"license": "Apache-2.0",
"dependencies": {
"@mongodb-js/saslprep": "^1.1.9",
"bson": "^6.10.1",
"mongodb-connection-string-url": "^3.0.0"
"@mongodb-js/saslprep": "^1.3.0",
"bson": "^6.10.4",
"mongodb-connection-string-url": "^3.0.2"
},
"engines": {
"node": ">=16.20.1"
@@ -15689,7 +15559,7 @@
"gcp-metadata": "^5.2.0",
"kerberos": "^2.0.1",
"mongodb-client-encryption": ">=6.0.0 <7",
"snappy": "^7.2.2",
"snappy": "^7.3.2",
"socks": "^2.7.1"
},
"peerDependenciesMeta": {
@@ -15716,6 +15586,72 @@
}
}
},
"node_modules/mongodb-connection-string-url": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
"license": "Apache-2.0",
"dependencies": {
"@types/whatwg-url": "^11.0.2",
"whatwg-url": "^14.1.0 || ^13.0.0"
}
},
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/mongoose": {
"version": "8.23.0",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.23.0.tgz",
"integrity": "sha512-Bul4Ha6J8IqzFrb0B1xpVzkC3S0sk43dmLSnhFOn8eJlZiLwL5WO6cRymmjaADdCMjUcCpj2ce8hZI6O4ZFSug==",
"license": "MIT",
"dependencies": {
"bson": "^6.10.4",
"kareem": "2.6.3",
"mongodb": "~6.20.0",
"mpath": "0.9.0",
"mquery": "5.0.0",
"ms": "2.1.3",
"sift": "17.1.3"
},
"engines": {
"node": ">=16.20.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mongoose"
}
},
"node_modules/mongoose/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -15775,6 +15711,56 @@
"integrity": "sha512-jSTz73B/+pGTTvhu5Ym8xsG6+QqaWab53UXnXdNNlTijTdLvcHABCLJXudQiJxob5N1Mzr5EOSx5ziwn2sihPQ==",
"dev": true
},
"node_modules/monk/node_modules/bson": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz",
"integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=0.6.19"
}
},
"node_modules/monk/node_modules/mongodb": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.4.tgz",
"integrity": "sha512-K5q8aBqEXMwWdVNh94UQTwZ6BejVbFhh1uB6c5FKtPE9eUMZPUO3sRZdgIEcHSrAWmxzpG/FeODDKL388sqRmw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bl": "^2.2.1",
"bson": "^1.1.4",
"denque": "^1.4.1",
"optional-require": "^1.1.8",
"safe-buffer": "^5.1.2"
},
"engines": {
"node": ">=4"
},
"optionalDependencies": {
"saslprep": "^1.0.0"
},
"peerDependenciesMeta": {
"aws4": {
"optional": true
},
"bson-ext": {
"optional": true
},
"kerberos": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"mongodb-extjson": {
"optional": true
},
"snappy": {
"optional": true
}
}
},
"node_modules/morgan": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
@@ -15852,14 +15838,6 @@
"node": ">=14.0.0"
}
},
"node_modules/mrmime": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
"integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==",
"engines": {
"node": ">=10"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -17128,19 +17106,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/opener": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
"bin": {
"opener": "bin/opener-bin.js"
}
},
"node_modules/optional-require": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.8.tgz",
"integrity": "sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA==",
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.10.tgz",
"integrity": "sha512-0r3OB9EIQsP+a5HVATHq2ExIy2q/Vaffoo4IAikW1spCYswhLxqWQS0i3GwS3AdY/OIP4SWZHLGz8CMU558PGw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"require-at": "^1.0.6"
},
@@ -18622,6 +18593,7 @@
"resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz",
"integrity": "sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=4"
}
@@ -18920,6 +18892,7 @@
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"sparse-bitfield": "^3.0.3"
@@ -19346,19 +19319,6 @@
"node": ">=8"
}
},
"node_modules/sirv": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
"integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
"dependencies": {
"@polka/url": "^1.0.0-next.24",
"mrmime": "^2.0.0",
"totalist": "^3.0.0"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -20849,14 +20809,6 @@
"node": ">=0.6"
}
},
"node_modules/totalist": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/touch": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
@@ -21870,50 +21822,6 @@
}
}
},
"node_modules/webpack-bundle-analyzer": {
"version": "4.10.2",
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz",
"integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==",
"dependencies": {
"@discoveryjs/json-ext": "0.5.7",
"acorn": "^8.0.4",
"acorn-walk": "^8.0.0",
"commander": "^7.2.0",
"debounce": "^1.2.1",
"escape-string-regexp": "^4.0.0",
"gzip-size": "^6.0.0",
"html-escaper": "^2.0.2",
"opener": "^1.5.2",
"picocolors": "^1.0.0",
"sirv": "^2.0.3",
"ws": "^7.3.1"
},
"bin": {
"webpack-bundle-analyzer": "lib/bin/analyzer.js"
},
"engines": {
"node": ">= 10.13.0"
}
},
"node_modules/webpack-bundle-analyzer/node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"engines": {
"node": ">= 10"
}
},
"node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/webpack-cli": {
"version": "4.10.0",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz",
@@ -22279,26 +22187,6 @@
"typedarray-to-buffer": "^3.1.5"
}
},
"node_modules/ws": {
"version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml-crypto": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-0.10.1.tgz",
+2 -3
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.46.4",
"version": "5.47.6",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
@@ -52,7 +52,7 @@
"micromustache": "^8.0.3",
"moment": "^2.29.4",
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
"mongoose": "^8.9.5",
"mongoose": "^8.23.0",
"morgan": "^1.10.1",
"nan": "^2.25.0",
"nconf": "^0.12.1",
@@ -76,7 +76,6 @@
"useragent": "^2.1.9",
"uuid": "^9.0.0",
"validator": "^13.11.0",
"webpack-bundle-analyzer": "^4.10.2",
"winston": "^3.10.0",
"winston-loggly-bulk": "^3.3.0",
"xml2js": "^0.6.2"
-560
View File
@@ -1,560 +0,0 @@
/* eslint-disable camelcase */
import nconf from 'nconf';
import Amplitude from 'amplitude';
import * as analyticsService from '../../../../website/server/libs/analyticsService';
describe('analyticsService', () => {
beforeEach(() => {
sandbox.stub(Amplitude.prototype, 'track').returns(Promise.resolve());
});
afterEach(() => {
sandbox.restore();
});
describe('#getServiceByEnvironment', () => {
it('returns mock methods when not in production', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
expect(analyticsService.getAnalyticsServiceByEnvironment())
.to.equal(analyticsService.mockAnalyticsService);
});
it('returns real methods when in production', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
expect(analyticsService.getAnalyticsServiceByEnvironment().track)
.to.equal(analyticsService.track);
expect(analyticsService.getAnalyticsServiceByEnvironment().trackPurchase)
.to.equal(analyticsService.trackPurchase);
});
});
describe('#track', () => {
let eventType; let
data;
beforeEach(() => {
eventType = 'Cron';
data = {
category: 'behavior',
uuid: 'unique-user-id',
resting: true,
cronCount: 5,
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
user: {
preferences: {
analyticsConsent: true,
},
},
};
});
context('Amplitude', () => {
it('calls out to amplitude', () => analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledOnce;
}));
it('uses a dummy user id if none is provided', () => {
delete data.uuid;
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
user_id: 'no-user-id-was-provided',
});
});
});
context('platform', () => {
it('logs web platform', () => {
data.headers = { 'x-client': 'habitica-web' };
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Web',
});
});
});
it('logs iOS platform', () => {
data.headers = { 'x-client': 'habitica-ios' };
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'iOS',
});
});
});
it('logs Android platform', () => {
data.headers = { 'x-client': 'habitica-android' };
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Android',
});
});
});
it('logs 3rd Party platform', () => {
data.headers = { 'x-client': 'some-third-party' };
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: '3rd Party',
});
});
});
it('logs unknown if headers are not passed in', () => {
delete data.headers;
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Unknown',
});
});
});
});
context('Operating System', () => {
it('sets default', () => {
data.headers = {
'x-client': 'third-party',
'user-agent': 'foo',
};
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'Other',
os_version: '0',
});
});
});
it('sets iOS', () => {
data.headers = {
'x-client': 'habitica-ios',
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
};
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'iOS',
os_version: '9.3.0',
});
});
});
it('sets Android', () => {
data.headers = {
'x-client': 'habitica-android',
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
};
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'Android',
os_version: '4.0.4',
});
});
});
it('sets Unknown if headers are not passed in', () => {
delete data.headers;
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: undefined,
os_version: undefined,
});
});
});
});
it('sends details about event', () => analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
category: 'behavior',
resting: true,
cronCount: 5,
},
});
}));
it('sends english item name for gear if itemKey is provided', () => {
data.itemKey = 'headAccessory_special_foxEars';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Fox Ears',
},
});
});
});
it('sends english item name for egg if itemKey is provided', () => {
data.itemKey = 'Wolf';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Wolf Egg',
},
});
});
});
it('sends english item name for food if itemKey is provided', () => {
data.itemKey = 'Cake_Skeleton';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Bare Bones Cake',
},
});
});
});
it('sends english item name for hatching potion if itemKey is provided', () => {
data.itemKey = 'Golden';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Golden Hatching Potion',
},
});
});
});
it('sends english item name for quest if itemKey is provided', () => {
data.itemKey = 'atom1';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Attack of the Mundane, Part 1: Dish Disaster!',
},
});
});
});
it('sends english item name for purchased spell if itemKey is provided', () => {
data.itemKey = 'seafoam';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Seafoam',
},
});
});
});
it('sends user data if provided', () => {
const stats = {
class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30,
};
const user = {
stats,
contributor: { level: 1 },
purchased: { plan: { planId: 'foo-plan' } },
flags: { tour: { intro: -2 } },
habits: [{ _id: 'habit' }],
dailys: [{ _id: 'daily' }],
todos: [{ _id: 'todo' }],
rewards: [{ _id: 'reward' }],
balance: 12,
loginIncentives: 1,
preferences: {
analyticsConsent: true,
},
};
data.user = user;
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
user_properties: {
Class: 'wizard',
Experience: 5,
Gold: 23,
Health: 10,
Level: 4,
Mana: 30,
tutorialComplete: true,
'Number Of Tasks': {
habits: 1,
dailys: 1,
todos: 1,
rewards: 1,
},
contributorLevel: 1,
subscription: 'foo-plan',
balance: 12,
balanceGemAmount: 48,
loginIncentives: 1,
},
});
});
});
});
});
describe('#trackPurchase', () => {
let data;
beforeEach(() => {
data = {
uuid: 'user-id',
sku: 'paypal-checkout',
paymentMethod: 'PayPal',
itemPurchased: 'Gems',
purchaseValue: 8,
purchaseType: 'checkout',
gift: false,
quantity: 1,
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
user: {
preferences: {
analyticsConsent: true,
},
},
};
});
context('Amplitude', () => {
it('calls out to amplitude', () => analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledOnce;
}));
it('uses a dummy user id if none is provided', () => {
delete data.uuid;
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
user_id: 'no-user-id-was-provided',
});
});
});
context('platform', () => {
it('logs web platform', () => {
data.headers = { 'x-client': 'habitica-web' };
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Web',
});
});
});
it('logs iOS platform', () => {
data.headers = { 'x-client': 'habitica-ios' };
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'iOS',
});
});
});
it('logs Android platform', () => {
data.headers = { 'x-client': 'habitica-android' };
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Android',
});
});
});
it('logs 3rd Party platform', () => {
data.headers = { 'x-client': 'some-third-party' };
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: '3rd Party',
});
});
});
it('logs unknown if headers are not passed in', () => {
delete data.headers;
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Unknown',
});
});
});
});
context('Operating System', () => {
it('sets default', () => {
data.headers = {
'x-client': 'third-party',
'user-agent': 'foo',
};
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'Other',
os_version: '0',
});
});
});
it('sets iOS', () => {
data.headers = {
'x-client': 'habitica-ios',
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
};
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'iOS',
os_version: '9.3.0',
});
});
});
it('sets Android', () => {
data.headers = {
'x-client': 'habitica-android',
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
};
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'Android',
os_version: '4.0.4',
});
});
});
it('sets Unknown if headers are not passed in', () => {
delete data.headers;
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: undefined,
os_version: undefined,
});
});
});
});
it('sends details about purchase', () => analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
gift: false,
itemPurchased: 'Gems',
paymentMethod: 'PayPal',
purchaseType: 'checkout',
quantity: 1,
sku: 'paypal-checkout',
},
});
}));
it('sends user data if provided', () => {
const stats = {
class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30,
};
const user = {
stats,
contributor: { level: 1 },
purchased: { plan: { planId: 'foo-plan' } },
flags: { tour: { intro: -2 } },
habits: [{ _id: 'habit' }],
dailys: [{ _id: 'daily' }],
todos: [{ _id: 'todo' }],
rewards: [{ _id: 'reward' }],
preferences: {
analyticsConsent: true,
},
};
data.user = user;
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
user_properties: {
Class: 'wizard',
Experience: 5,
Gold: 23,
Health: 10,
Level: 4,
Mana: 30,
tutorialComplete: true,
'Number Of Tasks': {
habits: 1,
dailys: 1,
todos: 1,
rewards: 1,
},
contributorLevel: 1,
subscription: 'foo-plan',
},
});
});
});
});
});
describe('mockAnalyticsService', () => {
it('has stubbed track method', () => {
expect(analyticsService.mockAnalyticsService).to.respondTo('track');
});
it('has stubbed trackPurchase method', () => {
expect(analyticsService.mockAnalyticsService).to.respondTo('trackPurchase');
});
});
});
+116 -136
View File
@@ -13,7 +13,6 @@ import { cron, cronWrapper } from '../../../../website/server/libs/cron';
import { model as User } from '../../../../website/server/models/user';
import * as Tasks from '../../../../website/server/models/task';
import common from '../../../../website/common';
import * as analytics from '../../../../website/server/libs/analyticsService';
import { model as Group } from '../../../../website/server/models/group';
const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime();
@@ -41,20 +40,17 @@ describe('cron', async () => {
},
},
});
sinon.spy(analytics, 'track');
});
afterEach(async () => {
if (clock !== null) clock.restore();
analytics.track.restore();
});
it('updates user.preferences.timezoneOffsetAtLastCron', async () => {
const timezoneUtcOffsetFromUserPrefs = -1;
await cron({
user, tasksByType, daysMissed, analytics, timezoneUtcOffsetFromUserPrefs,
user, tasksByType, daysMissed, timezoneUtcOffsetFromUserPrefs,
});
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(1);
@@ -63,7 +59,7 @@ describe('cron', async () => {
it('resets user.items.lastDrop.count', async () => {
user.items.lastDrop.count = 4;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.items.lastDrop.count).to.equal(0);
});
@@ -71,26 +67,11 @@ describe('cron', async () => {
it('increments user cron count', async () => {
const cronCountBefore = user.flags.cronCount;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.flags.cronCount).to.be.greaterThan(cronCountBefore);
});
it('calls analytics', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
});
expect(analytics.track.callCount).to.equal(1);
});
it('calls analytics when user is sleeping', async () => {
user.preferences.sleep = true;
await cron({
user, tasksByType, daysMissed, analytics,
});
expect(analytics.track.callCount).to.equal(1);
});
describe('end of the month perks', async () => {
beforeEach(async () => {
user.purchased.plan.customerId = 'subscribedId';
@@ -101,7 +82,7 @@ describe('cron', async () => {
user.purchased.plan.dateUpdated = new Date('2018-12-11');
clock = sinon.useFakeTimers(new Date('2019-01-29'));
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.mysteryItems.length).to.eql(2);
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
@@ -112,7 +93,7 @@ describe('cron', async () => {
user.purchased.plan.dateUpdated = new Date('2018-11-11');
clock = sinon.useFakeTimers(new Date('2019-01-29'));
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.mysteryItems.length).to.eql(4);
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
@@ -122,7 +103,7 @@ describe('cron', async () => {
it('resets plan.gemsBought on a new month', async () => {
user.purchased.plan.gemsBought = 10;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.gemsBought).to.equal(0);
});
@@ -131,7 +112,7 @@ describe('cron', async () => {
user.purchased.plan.gemsBought = 10;
user.purchased.plan.dateUpdated = undefined;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.gemsBought).to.equal(0);
});
@@ -142,7 +123,7 @@ describe('cron', async () => {
user.purchased.plan.gemsBought = 10;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.gemsBought).to.equal(10);
});
@@ -150,7 +131,7 @@ describe('cron', async () => {
it('resets plan.dateUpdated on a new month', async () => {
const currentMonth = moment().startOf('month');
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(moment(user.purchased.plan.dateUpdated).startOf('month').isSame(currentMonth)).to.eql(true);
});
@@ -158,7 +139,7 @@ describe('cron', async () => {
it('increments plan.consecutive.count', async () => {
user.purchased.plan.consecutive.count = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.count).to.equal(1);
});
@@ -166,7 +147,7 @@ describe('cron', async () => {
it('increments plan.cumulativeCount', async () => {
user.purchased.plan.cumulativeCount = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.cumulativeCount).to.equal(1);
});
@@ -175,7 +156,7 @@ describe('cron', async () => {
user.purchased.plan.dateUpdated = moment().subtract(2, 'months').toDate();
user.purchased.plan.consecutive.count = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.count).to.equal(2);
});
@@ -184,7 +165,7 @@ describe('cron', async () => {
user.purchased.plan.dateUpdated = moment().subtract(3, 'months').toDate();
user.purchased.plan.cumulativeCount = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.cumulativeCount).to.equal(3);
});
@@ -196,7 +177,7 @@ describe('cron', async () => {
user.purchased.plan.consecutive.trinkets = 1;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
@@ -206,7 +187,7 @@ describe('cron', async () => {
user.purchased.plan.consecutive.gemCapExtra = 26;
user.purchased.plan.consecutive.count = 5;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
});
@@ -214,7 +195,7 @@ describe('cron', async () => {
it('does not reset plan stats if we are before the last day of the cancelled month', async () => {
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.customerId).to.exist;
});
@@ -225,7 +206,7 @@ describe('cron', async () => {
user.purchased.plan.consecutive.count = 5;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.customerId).to.not.exist;
@@ -264,7 +245,7 @@ describe('cron', async () => {
// Add 2 days so that we're sure we're not affected by any start-of-month effects
// e.g., from time zone oddness.
await cron({
user: user1, tasksByType, daysMissed, analytics,
user: user1, tasksByType, daysMissed,
});
expect(user1.purchased.plan.consecutive.count).to.equal(1);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -276,7 +257,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user1, tasksByType, daysMissed, analytics,
user: user1, tasksByType, daysMissed,
});
expect(user1.purchased.plan.consecutive.count).to.equal(10);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(11);
@@ -311,7 +292,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user3, tasksByType, daysMissed, analytics,
user: user3, tasksByType, daysMissed,
});
expect(user3.purchased.plan.consecutive.count).to.equal(1);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -323,7 +304,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user3, tasksByType, daysMissed, analytics,
user: user3, tasksByType, daysMissed,
});
expect(user3.purchased.plan.consecutive.count).to.equal(10);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(11);
@@ -358,7 +339,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user6, tasksByType, daysMissed, analytics,
user: user6, tasksByType, daysMissed,
});
expect(user6.purchased.plan.consecutive.count).to.equal(1);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -391,7 +372,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user12, tasksByType, daysMissed, analytics,
user: user12, tasksByType, daysMissed,
});
expect(user12.purchased.plan.consecutive.count).to.equal(1);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -403,7 +384,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user12, tasksByType, daysMissed, analytics,
user: user12, tasksByType, daysMissed,
});
expect(user12.purchased.plan.consecutive.count).to.equal(10);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(11);
@@ -439,7 +420,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user3g, tasksByType, daysMissed, analytics,
user: user3g, tasksByType, daysMissed,
});
expect(user3g.purchased.plan.consecutive.count).to.equal(1);
expect(user3g.purchased.plan.cumulativeCount).to.equal(1);
@@ -452,7 +433,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user3g, tasksByType, daysMissed, analytics,
user: user3g, tasksByType, daysMissed,
});
// subscription has been erased by now
expect(user3g.purchased.plan.consecutive.count).to.equal(0);
@@ -471,7 +452,7 @@ describe('cron', async () => {
it('resets plan.gemsBought on a new month', async () => {
user.purchased.plan.gemsBought = 10;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.gemsBought).to.equal(0);
});
@@ -482,14 +463,14 @@ describe('cron', async () => {
user.purchased.plan.gemsBought = 10;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.gemsBought).to.equal(10);
});
it('does not reset plan.dateUpdated on a new month', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.dateUpdated).to.be.empty;
});
@@ -497,7 +478,7 @@ describe('cron', async () => {
it('does not increment plan.consecutive.count', async () => {
user.purchased.plan.consecutive.count = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.count).to.equal(0);
});
@@ -505,7 +486,7 @@ describe('cron', async () => {
it('does not increment plan.cumulativeCount', async () => {
user.purchased.plan.cumulativeCount = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.cumulativeCount).to.equal(0);
});
@@ -513,7 +494,7 @@ describe('cron', async () => {
it('does not increment plan.consecutive.trinkets when user has reached a month that is a multiple of 3', async () => {
user.purchased.plan.consecutive.count = 5;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.trinkets).to.equal(0);
});
@@ -521,7 +502,7 @@ describe('cron', async () => {
it('does not increment plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', async () => {
user.purchased.plan.consecutive.count = 5;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(0);
});
@@ -530,7 +511,7 @@ describe('cron', async () => {
user.purchased.plan.consecutive.gemCapExtra = 26;
user.purchased.plan.consecutive.count = 5;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
});
@@ -538,7 +519,7 @@ describe('cron', async () => {
it('does nothing to plan stats if we are before the last day of the cancelled month', async () => {
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.customerId).to.not.exist;
});
@@ -564,7 +545,7 @@ describe('cron', async () => {
it('should make uncompleted todos redder', async () => {
const valueBefore = tasksByType.todos[0].value;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.todos[0].value).to.be.lessThan(valueBefore);
});
@@ -573,7 +554,7 @@ describe('cron', async () => {
tasksByType.todos[0].completed = true;
const valueBefore = tasksByType.todos[0].value;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.todos[0].value).to.equal(valueBefore);
});
@@ -582,7 +563,7 @@ describe('cron', async () => {
tasksByType.todos[0].completed = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.history.todos).to.be.lengthOf(1);
@@ -608,7 +589,7 @@ describe('cron', async () => {
expect(user.tasksOrder.todos).to.be.lengthOf(3);
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
// user.tasksOrder.todos should be filtered while tasks by type remains unchanged
@@ -635,7 +616,7 @@ describe('cron', async () => {
const original = user.tasksOrder.todos; // Preserve the original order
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
let listsAreEqual = true;
@@ -675,7 +656,7 @@ describe('cron', async () => {
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].isDue).to.be.false;
});
@@ -686,7 +667,7 @@ describe('cron', async () => {
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().toDate();
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].isDue).to.exist;
});
@@ -696,14 +677,14 @@ describe('cron', async () => {
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].nextDue.length).to.eql(6);
});
it('should add history', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
});
@@ -711,7 +692,7 @@ describe('cron', async () => {
it('should set tasks completed to false', async () => {
tasksByType.dailys[0].completed = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].completed).to.be.false;
});
@@ -720,7 +701,7 @@ describe('cron', async () => {
user.preferences.sleep = true;
tasksByType.dailys[0].completed = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].completed).to.be.false;
});
@@ -729,7 +710,7 @@ describe('cron', async () => {
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
tasksByType.dailys[0].completed = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
@@ -739,7 +720,7 @@ describe('cron', async () => {
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
tasksByType.dailys[0].completed = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
@@ -749,7 +730,7 @@ describe('cron', async () => {
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
@@ -759,7 +740,7 @@ describe('cron', async () => {
const hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.hp).to.be.lessThan(hpBefore);
});
@@ -770,7 +751,7 @@ describe('cron', async () => {
const hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.hp).to.equal(hpBefore);
});
@@ -784,7 +765,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
cronOverride({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.hp).to.equal(hpBefore);
@@ -797,7 +778,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.hp).to.equal(hpBefore);
@@ -808,7 +789,7 @@ describe('cron', async () => {
let hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
const hpDifferenceOfFullyIncompleteDaily = hpBefore - user.stats.hp;
@@ -816,7 +797,7 @@ describe('cron', async () => {
tasksByType.dailys[0].checklist.push({ title: 'test', completed: true });
tasksByType.dailys[0].checklist.push({ title: 'test2', completed: false });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
const hpDifferenceOfPartiallyIncompleteDaily = hpBefore - user.stats.hp;
@@ -829,7 +810,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
const progress = await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(progress.down).to.equal(-1);
@@ -841,7 +822,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
const progress = await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(progress.down).to.equal(0);
@@ -862,7 +843,7 @@ describe('cron', async () => {
tasksByType.dailys[1].frequency = 'daily';
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.hp).to.equal(48);
@@ -886,7 +867,7 @@ describe('cron', async () => {
tasksByType.habits[0].down = false;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].value).to.be.lessThan(1);
@@ -897,7 +878,7 @@ describe('cron', async () => {
tasksByType.habits[0].up = false;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].value).to.be.lessThan(1);
@@ -909,7 +890,7 @@ describe('cron', async () => {
tasksByType.habits[0].down = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].value).to.equal(1);
@@ -928,7 +909,7 @@ describe('cron', async () => {
tasksByType.habits[0].counterDown = 1;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -941,7 +922,7 @@ describe('cron', async () => {
tasksByType.habits[0].counterDown = 1;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -955,7 +936,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -964,7 +945,7 @@ describe('cron', async () => {
// should reset
daysMissed = 8;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -988,7 +969,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1002,7 +983,7 @@ describe('cron', async () => {
// should reset after user CDS
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1026,7 +1007,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1036,7 +1017,7 @@ describe('cron', async () => {
// should reset
daysMissed = 2;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1060,7 +1041,7 @@ describe('cron', async () => {
// should reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1084,7 +1065,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1098,7 +1079,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1107,7 +1088,7 @@ describe('cron', async () => {
// should reset
daysMissed = 32;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1132,7 +1113,7 @@ describe('cron', async () => {
// should reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1156,7 +1137,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1166,7 +1147,7 @@ describe('cron', async () => {
// should reset
daysMissed = 2;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1199,7 +1180,7 @@ describe('cron', async () => {
user.stats.lvl = 2;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.history.exp).to.have.lengthOf(1);
@@ -1212,7 +1193,7 @@ describe('cron', async () => {
tasksByType.dailys[0].isDue = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.achievements.perfect).to.equal(1);
@@ -1224,7 +1205,7 @@ describe('cron', async () => {
tasksByType.dailys[0].isDue = false;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.achievements.perfect).to.equal(0);
@@ -1238,7 +1219,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject();
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1256,7 +1237,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject();
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1280,7 +1261,7 @@ describe('cron', async () => {
};
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.equal(0);
@@ -1307,7 +1288,7 @@ describe('cron', async () => {
};
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.equal(0);
@@ -1333,7 +1314,7 @@ describe('cron', async () => {
};
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.equal(0);
@@ -1360,7 +1341,7 @@ describe('cron', async () => {
};
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.equal(0);
@@ -1381,7 +1362,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject();
cronOverride({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1401,7 +1382,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject();
cronOverride({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1420,7 +1401,7 @@ describe('cron', async () => {
tasksByType.dailys[0].completed = true;
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.mp).to.be.greaterThan(mpBefore);
@@ -1436,7 +1417,7 @@ describe('cron', async () => {
tasksByType.dailys[0].completed = true;
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.mp).to.equal(mpBefore);
@@ -1449,7 +1430,7 @@ describe('cron', async () => {
user.stats.mp = 120;
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.mp).to.equal(common.statsComputed(user).maxMP);
@@ -1482,7 +1463,7 @@ describe('cron', async () => {
it('resets user progress', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.party.quest.progress.up).to.equal(0);
expect(user.party.quest.progress.down).to.equal(0);
@@ -1491,7 +1472,7 @@ describe('cron', async () => {
it('applies the user progress', async () => {
const progress = await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(progress.down).to.equal(-1);
});
@@ -1529,19 +1510,19 @@ describe('cron', async () => {
describe('login incentives', async () => {
it('increments incentive counter each cron', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(1);
user.lastCron = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(2);
});
it('pushes a notification of the day\'s incentive each cron', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.notifications.length).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
@@ -1549,13 +1530,13 @@ describe('cron', async () => {
it('replaces previous notifications', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
const filteredNotifications = user.notifications.filter(n => n.type === 'LOGIN_INCENTIVE');
@@ -1566,7 +1547,7 @@ describe('cron', async () => {
it('increments loginIncentives by 1 even if days are skipped in between', async () => {
daysMissed = 3;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(1);
});
@@ -1574,14 +1555,14 @@ describe('cron', async () => {
it('increments loginIncentives by 1 even if user is sleeping', async () => {
user.preferences.sleep = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(1);
});
it('awards user bard robes if login incentive is 1', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(1);
expect(user.items.gear.owned.armor_special_bardRobes).to.eql(true);
@@ -1591,7 +1572,7 @@ describe('cron', async () => {
it('awards user incentive backgrounds if login incentive is 2', async () => {
user.loginIncentives = 1;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(2);
expect(user.purchased.background.blue).to.eql(true);
@@ -1605,7 +1586,7 @@ describe('cron', async () => {
it('awards user Bard Hat if login incentive is 3', async () => {
user.loginIncentives = 2;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(3);
expect(user.items.gear.owned.head_special_bardHat).to.eql(true);
@@ -1615,7 +1596,7 @@ describe('cron', async () => {
it('awards user RoyalPurple Hatching Potion if login incentive is 4', async () => {
user.loginIncentives = 3;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(4);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
@@ -1625,7 +1606,7 @@ describe('cron', async () => {
it('awards user a Chocolate, Meat and Pink Contton Candy if login incentive is 5', async () => {
user.loginIncentives = 4;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(5);
@@ -1639,7 +1620,7 @@ describe('cron', async () => {
it('awards user moon quest if login incentive is 7', async () => {
user.loginIncentives = 6;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(7);
expect(user.items.quests.moon1).to.eql(1);
@@ -1649,7 +1630,7 @@ describe('cron', async () => {
it('awards user RoyalPurple Hatching Potion if login incentive is 10', async () => {
user.loginIncentives = 9;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(10);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
@@ -1659,7 +1640,7 @@ describe('cron', async () => {
it('awards user a Strawberry, Patato and Blue Contton Candy if login incentive is 14', async () => {
user.loginIncentives = 13;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(14);
@@ -1673,7 +1654,7 @@ describe('cron', async () => {
it('awards user a bard instrument if login incentive is 18', async () => {
user.loginIncentives = 17;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(18);
expect(user.items.gear.owned.weapon_special_bardInstrument).to.eql(true);
@@ -1683,7 +1664,7 @@ describe('cron', async () => {
it('awards user second moon quest if login incentive is 22', async () => {
user.loginIncentives = 21;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(22);
expect(user.items.quests.moon2).to.eql(1);
@@ -1693,7 +1674,7 @@ describe('cron', async () => {
it('awards user a RoyalPurple hatching potion if login incentive is 26', async () => {
user.loginIncentives = 25;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(26);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
@@ -1703,7 +1684,7 @@ describe('cron', async () => {
it('awards user Fish, Milk, Rotten Meat and Honey if login incentive is 30', async () => {
user.loginIncentives = 29;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(30);
@@ -1718,7 +1699,7 @@ describe('cron', async () => {
it('awards user a RoyalPurple hatching potion if login incentive is 35', async () => {
user.loginIncentives = 34;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(35);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
@@ -1728,7 +1709,7 @@ describe('cron', async () => {
it('awards user the third moon quest if login incentive is 40', async () => {
user.loginIncentives = 39;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(40);
expect(user.items.quests.moon3).to.eql(1);
@@ -1738,7 +1719,7 @@ describe('cron', async () => {
it('awards user a RoyalPurple hatching potion if login incentive is 45', async () => {
user.loginIncentives = 44;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(45);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
@@ -1748,7 +1729,7 @@ describe('cron', async () => {
it('awards user a saddle if login incentive is 50', async () => {
user.loginIncentives = 49;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(50);
expect(user.items.food.Saddle).to.eql(1);
@@ -1766,7 +1747,6 @@ describe('cron wrapper', () => {
res = generateRes();
req = generateReq();
user = await res.locals.user.save();
res.analytics = analytics;
});
afterEach(() => {
+100
View File
@@ -0,0 +1,100 @@
import nconf from 'nconf';
import requireAgain from 'require-again';
import { model as User } from '../../../../website/server/models/user';
import { RegistrationEventModel } from '../../../../website/server/models/analytics/registrationEvent';
import { SubscriptionEventModel } from '../../../../website/server/models/analytics/subscriptionEvent';
describe('localAnalytics', () => {
let user;
let localAnalytics;
before(() => {
const nconfGetStub = sandbox.stub(nconf, 'get');
nconfGetStub.withArgs('ANALYTICS_DB').returns('analytics');
nconfGetStub.withArgs('DISABLE_LOCAL_ANALYTICS').returns(false);
localAnalytics = requireAgain('../../../../website/server/libs/localAnalytics');
});
beforeEach(async () => {
user = new User({
auth: {
local: {
username: 'username',
email: 'email@example.com',
},
},
registeredThrough: 'habitica-web',
});
});
describe('trackRegistrationEvent', () => {
afterEach(async () => {
await RegistrationEventModel.deleteMany({});
});
it('creates a registration event when a user registers', async () => {
user._id = '00000000-0000-0000-0000-000000000001';
await localAnalytics.trackRegistrationEvent({ user, ipAddress: '127.0.0.1' });
const registrationEvents = await RegistrationEventModel.find({ userId: user._id });
expect(registrationEvents).to.have.lengthOf(1);
expect(registrationEvents[0]).to.have.property('userId', user._id);
expect(registrationEvents[0]).to.have.property('ipAddress', '127.0.0.1');
});
it('saves the correct data to the database', async () => {
user._id = '00000000-0000-0000-0000-000000000002';
user.auth.google = { id: 'abc', emails: [{ value: 'email@example.com' }] };
await localAnalytics.trackRegistrationEvent({ user, ipAddress: '127.0.0.2' });
const registrationEvent = await RegistrationEventModel.findOne({ userId: user._id });
expect(registrationEvent).to.have.property('userId', user._id);
expect(registrationEvent).to.have.property('ipAddress', '127.0.0.2');
expect(registrationEvent).to.have.property('authenticationMethod', 'google');
});
});
describe('trackSubscriptionEvent', () => {
afterEach(async () => {
await SubscriptionEventModel.deleteMany({});
});
it('creates a subscription event when a user subscribes', async () => {
user._id = '00000000-0000-0000-0000-000000000003';
await localAnalytics.trackSubscriptionEvent({
eventType: 'subscribed',
user,
paymentMethod: 'stripe',
customerId: 'cus_123',
planId: 'plan_123',
});
const subscriptionEvents = await SubscriptionEventModel.find({ userId: user._id });
expect(subscriptionEvents).to.have.lengthOf(1);
expect(subscriptionEvents[0]).to.have.property('userId', user._id);
expect(subscriptionEvents[0]).to.have.property('eventType', 'subscribed');
expect(subscriptionEvents[0]).to.have.property('paymentMethod', 'stripe');
expect(subscriptionEvents[0]).to.have.property('customerId', 'cus_123');
expect(subscriptionEvents[0]).to.have.property('planId', 'plan_123');
});
it('creates a subscription event with cancellation reason when a user cancels', async () => {
user._id = '00000000-0000-0000-0000-000000000004';
await localAnalytics.trackSubscriptionEvent({
eventType: 'cancelled',
user,
paymentMethod: 'stripe',
customerId: 'cus_456',
planId: 'plan_456',
cancellationReason: 'No longer needed',
});
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
expect(subscriptionEvent).to.have.property('userId', user._id);
expect(subscriptionEvent).to.have.property('eventType', 'cancelled');
expect(subscriptionEvent).to.have.property('paymentMethod', 'stripe');
expect(subscriptionEvent).to.have.property('customerId', 'cus_456');
expect(subscriptionEvent).to.have.property('planId', 'plan_456');
expect(subscriptionEvent).to.have.property('cancellationReason', 'No longer needed');
});
});
});
+64 -47
View File
@@ -3,7 +3,6 @@ import moment from 'moment';
import * as sender from '../../../../../website/server/libs/email';
import common from '../../../../../website/common';
import api from '../../../../../website/server/libs/payments/payments';
import * as analytics from '../../../../../website/server/libs/analyticsService';
import * as notifications from '../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../website/server/models/user';
import { translate as t } from '../../../../helpers/api-integration/v3';
@@ -13,6 +12,7 @@ import {
import * as worldState from '../../../../../website/server/libs/worldState';
import { TransactionModel } from '../../../../../website/server/models/transaction';
import { REPEATING_EVENTS } from '../../../../../website/common/script/content/constants/events';
import { SubscriptionEventModel } from '../../../../../website/server/models/analytics/subscriptionEvent';
describe('payments/index', () => {
let user;
@@ -36,8 +36,6 @@ describe('payments/index', () => {
sandbox.stub(sender, 'sendTxn');
sandbox.stub(user, 'sendMessage');
sandbox.stub(analytics.mockAnalyticsService, 'trackPurchase');
sandbox.stub(analytics.mockAnalyticsService, 'track');
sandbox.stub(notifications, 'sendNotification');
data = {
@@ -97,6 +95,16 @@ describe('payments/index', () => {
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
});
it('tracks subscription events', async () => {
await api.createSubscription(data);
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: recipient._id });
expect(subscriptionEvent).to.exist;
expect(subscriptionEvent).to.have.property('eventType', 'subscribed');
expect(subscriptionEvent).to.have.property('userId', recipient._id);
expect(subscriptionEvent).to.have.property('planId', 'basic_3mo');
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
});
it('adds extra months to an existing subscription', async () => {
recipient.purchased.plan = plan;
@@ -298,28 +306,6 @@ describe('payments/index', () => {
expect(notifications.sendNotification).to.be.calledOnce;
});
it('tracks subscription purchase as gift', async () => {
await api.createSubscription(data);
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
uuid: user._id,
groupId: undefined,
itemPurchased: 'Subscription',
sku: 'payment method-subscription',
purchaseType: 'subscribe',
paymentMethod: data.paymentMethod,
quantity: 1,
gift: true,
purchaseValue: 15,
firstPurchase: true,
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
});
});
context('No Active Promotion', () => {
beforeEach(() => {
sinon.stub(worldState, 'getCurrentEventList').returns([]);
@@ -455,6 +441,16 @@ describe('payments/index', () => {
expect(user.purchased.plan.dateCreated).to.exist;
});
it('tracks subscription events', async () => {
await api.createSubscription(data);
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
expect(subscriptionEvent).to.exist;
expect(subscriptionEvent).to.have.property('userId', user._id);
expect(subscriptionEvent).to.have.property('ipAddress');
expect(subscriptionEvent).to.have.property('planId', 'basic_3mo');
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
});
it('sets plan.dateCreated if it did not previously exist', async () => {
expect(user.purchased.plan.dateCreated).to.not.exist;
@@ -543,29 +539,24 @@ describe('payments/index', () => {
expect(sender.sendTxn).to.be.calledWith(data.user, 'subscription-begins');
});
it('tracks subscription purchase', async () => {
await api.createSubscription(data);
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
uuid: user._id,
groupId: undefined,
itemPurchased: 'Subscription',
sku: 'payment method-subscription',
purchaseType: 'subscribe',
paymentMethod: data.paymentMethod,
quantity: 1,
gift: false,
purchaseValue: 15,
firstPurchase: true,
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
});
});
context('Upgrades subscription', () => {
it('tracks subscription events', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
data.sub.key = 'basic_6mo';
data.updatedFrom = { key: 'basic_earned' };
await api.createSubscription(data);
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id, planId: 'basic_6mo' });
expect(subscriptionEvent).to.exist;
expect(subscriptionEvent).to.have.property('eventType', 'upgraded');
expect(subscriptionEvent).to.have.property('userId', user._id);
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
});
it('from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
@@ -608,6 +599,23 @@ describe('payments/index', () => {
});
context('Downgrades subscription', () => {
it('tracks subscription events', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
data.sub.key = 'basic_earned';
data.updatedFrom = { key: 'basic_6mo' };
await api.createSubscription(data);
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id, planId: 'basic_earned' });
expect(subscriptionEvent).to.exist;
expect(subscriptionEvent).to.have.property('eventType', 'downgraded');
expect(subscriptionEvent).to.have.property('userId', user._id);
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
});
it('from basic_6mo to basic_earned', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
@@ -1136,6 +1144,15 @@ describe('payments/index', () => {
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
});
it('tracks subscription events', async () => {
await api.cancelSubscription(data);
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
expect(subscriptionEvent).to.exist;
expect(subscriptionEvent).to.have.property('eventType', 'cancelled');
expect(subscriptionEvent).to.have.property('userId', user._id);
});
it('adds extraMonths to dateTerminated value', async () => {
user.purchased.plan.extraMonths = 2;
@@ -1,50 +0,0 @@
/* eslint-disable global-require */
import nconf from 'nconf';
import requireAgain from 'require-again';
import {
generateRes,
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
import * as analyticsService from '../../../../website/server/libs/analyticsService';
describe('analytics middleware', () => {
let res; let req; let
next;
const pathToAnalyticsMiddleware = '../../../../website/server/middlewares/analytics';
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
it('attaches analytics object to res', () => {
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
attachAnalytics(req, res, next);
expect(res.analytics).to.exist;
});
it('attaches stubbed methods for non-prod environments', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
attachAnalytics(req, res, next);
expect(res.analytics.track).to.eql(analyticsService.mockAnalyticsService.track);
expect(res.analytics.trackPurchase).to.eql(analyticsService.mockAnalyticsService.trackPurchase);
});
it('attaches real methods for prod environments', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
attachAnalytics(req, res, next);
expect(res.analytics.track).to.eql(analyticsService.track);
expect(res.analytics.trackPurchase).to.eql(analyticsService.trackPurchase);
});
});
@@ -1,19 +0,0 @@
import {
generateUser,
requester,
} from '../../../../helpers/api-integration/v3';
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
describe('POST /analytics/track/:eventName', () => {
it('calls res.analytics', async () => {
const user = await generateUser();
sandbox.spy(analytics, 'track');
const requestWithHeaders = requester(user, { 'x-client': 'habitica-web' });
await requestWithHeaders.post('/analytics/track/eventName', { data: 'example' }, { 'x-client': 'habitica-web' });
expect(analytics.track).to.be.calledOnce;
expect(analytics.track).to.be.calledWith('eventName', sandbox.match({ data: 'example' }));
sandbox.restore();
});
});
@@ -1,7 +1,6 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
describe('POST /user/sleep', () => {
let user;
@@ -23,15 +22,4 @@ describe('POST /user/sleep', () => {
await user.sync();
expect(user.preferences.sleep).to.be.false;
});
it('sends sleep status to analytics service', async () => {
sandbox.spy(analytics, 'track');
await user.post('/user/sleep');
await user.sync();
expect(analytics.track).to.be.calledOnce;
expect(analytics.track).to.be.calledWith('sleep', sandbox.match.has('status', user.preferences.sleep));
sandbox.restore();
});
});
@@ -9,6 +9,7 @@ import {
} from '../../../../../helpers/api-integration/v3';
import { ApiUser } from '../../../../../helpers/api-integration/api-classes';
import { encrypt } from '../../../../../../website/server/libs/encryption';
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
function generateRandomUserName () {
return (Date.now() + uuid()).substring(0, 20);
@@ -41,6 +42,25 @@ describe('POST /user/auth/local/register', () => {
expect(user.newUser).to.eql(true);
});
it('tracks a registration event', async () => {
const username = generateRandomUserName();
const email = `${username}@example.com`;
const password = 'password';
const user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
const registrationEvent = await RegistrationEventModel.findOne({ userId: user._id });
expect(registrationEvent).to.exist;
expect(registrationEvent).to.have.property('userId', user._id);
expect(registrationEvent).to.have.property('ipAddress');
expect(registrationEvent).to.have.property('authenticationMethod', 'local');
});
it('registers a new user and sets verifiedUsername to true', async () => {
const username = generateRandomUserName();
const email = `${username}@example.com`;
@@ -7,6 +7,7 @@ import {
getProperty,
} from '../../../../../helpers/api-integration/v3';
import apiErrorMessages from '../../../../../../website/common/script/errors/apiErrorMessages';
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
describe('POST /user/auth/social', () => {
let api;
@@ -65,6 +66,19 @@ describe('POST /user/auth/social', () => {
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
});
it('tracks a registration event', async () => {
const socialUser = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
const registrationEvent = await RegistrationEventModel.findOne({ userId: socialUser.id });
expect(registrationEvent).to.exist;
expect(registrationEvent).to.have.property('userId', socialUser.id);
expect(registrationEvent).to.have.property('ipAddress');
expect(registrationEvent).to.have.property('authenticationMethod', 'google');
});
it('includes sanitized version of provided username', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -231,6 +245,17 @@ describe('POST /user/auth/social', () => {
expect(response.newUser).to.be.false;
});
it('does not track a registration event for existing users', async () => {
const beforeEvents = await RegistrationEventModel.find({ userId: user._id });
await user.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
const registrationEvents = await RegistrationEventModel.find({ userId: user._id });
expect(registrationEvents).to.have.lengthOf(beforeEvents.length);
});
it('does not log into other account if social auth already exists', async () => {
const registerResponse = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
+1 -10
View File
@@ -13,7 +13,6 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buy', () => {
let user;
const analytics = { track () {} };
beforeEach(() => {
user = generateUser({
@@ -32,12 +31,6 @@ describe('shared.ops.buy', () => {
},
},
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
it('returns error when key is not provided', async () => {
@@ -51,10 +44,8 @@ describe('shared.ops.buy', () => {
it('buys health potion', async () => {
user.stats.hp = 30;
await buy(user, { params: { key: 'potion' } }, analytics);
await buy(user, { params: { key: 'potion' } });
expect(user.stats.hp).to.eql(45);
expect(analytics.track).to.be.calledOnce;
});
it('adds equipment to inventory', async () => {
+3 -7
View File
@@ -29,10 +29,9 @@ describe('shared.ops.buyArmoire', () => {
const YIELD_EQUIPMENT = 0.5;
const YIELD_FOOD = 0.7;
const YIELD_EXP = 0.9;
const analytics = { track () {} };
async function buyArmoire (_user, _req, _analytics) {
const buyOp = new BuyArmoireOperation(_user, _req, _analytics);
async function buyArmoire (_user, _req) {
const buyOp = new BuyArmoireOperation(_user, _req);
return buyOp.purchase();
}
@@ -50,12 +49,10 @@ describe('shared.ops.buyArmoire', () => {
user.items.food = {};
sandbox.stub(randomValFns, 'trueRandom');
sinon.stub(analytics, 'track');
});
afterEach(() => {
randomValFns.trueRandom.restore();
analytics.track.restore();
});
context('failure conditions', () => {
@@ -147,7 +144,7 @@ describe('shared.ops.buyArmoire', () => {
expect(_.size(user.items.gear.owned)).to.equal(2);
await buyArmoire(user, {}, analytics);
await buyArmoire(user, {});
expect(_.size(user.items.gear.owned)).to.equal(3);
@@ -155,7 +152,6 @@ describe('shared.ops.buyArmoire', () => {
expect(armoireCount).to.eql(_.size(getFullArmoire()) - 2);
expect(user.stats.gp).to.eql(100);
expect(analytics.track).to.be.calledTwice;
});
});
});
+3 -12
View File
@@ -1,6 +1,5 @@
/* eslint-disable camelcase */
import sinon from 'sinon'; // eslint-disable-line no-shadow
import {
generateUser,
} from '../../../helpers/common.helper';
@@ -11,15 +10,14 @@ import i18n from '../../../../website/common/script/i18n';
import { BuyGemOperation } from '../../../../website/common/script/ops/buy/buyGem';
import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
async function buyGem (user, req, analytics) {
const buyOp = new BuyGemOperation(user, req, analytics);
async function buyGem (user, req) {
const buyOp = new BuyGemOperation(user, req);
return buyOp.purchase();
}
describe('shared.ops.buyGem', () => {
let user;
const analytics = { track () {} };
const goldPoints = 40;
const gemsBought = 40;
const userGemAmount = 10;
@@ -35,23 +33,16 @@ describe('shared.ops.buyGem', () => {
},
},
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
context('Gems', () => {
it('purchases gems', async () => {
const [, message] = await buyGem(user, { params: { type: 'gems', key: 'gem' } }, analytics);
const [, message] = await buyGem(user, { params: { type: 'gems', key: 'gem' } });
expect(message).to.equal(i18n.t('plusGem', { count: 1 }));
expect(user.balance).to.equal(userGemAmount + 0.25);
expect(user.purchased.plan.gemsBought).to.equal(1);
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
expect(analytics.track).to.be.calledOnce;
});
it('purchases gems with a different language than the default', async () => {
+3 -10
View File
@@ -10,10 +10,9 @@ import i18n from '../../../../website/common/script/i18n';
describe('shared.ops.buyHealthPotion', () => {
let user;
const analytics = { track () {} };
async function buyHealthPotion (_user, _req, _analytics) {
const buyOp = new BuyHealthPotionOperation(_user, _req, _analytics);
async function buyHealthPotion (_user, _req) {
const buyOp = new BuyHealthPotionOperation(_user, _req);
return buyOp.purchase();
}
@@ -32,19 +31,13 @@ describe('shared.ops.buyHealthPotion', () => {
},
stats: { gp: 200 },
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
context('Potion', () => {
it('recovers 15 hp', async () => {
user.stats.hp = 30;
await buyHealthPotion(user, {}, analytics);
await buyHealthPotion(user, {});
expect(user.stats.hp).to.eql(45);
expect(analytics.track).to.be.calledOnce;
});
it('does not increase hp above 50', async () => {
+5 -9
View File
@@ -13,15 +13,14 @@ import {
import i18n from '../../../../website/common/script/i18n';
import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
async function buyGear (user, req, analytics) {
const buyOp = new BuyMarketGearOperation(user, req, analytics);
async function buyGear (user, req) {
const buyOp = new BuyMarketGearOperation(user, req);
return buyOp.purchase();
}
describe('shared.ops.buyMarketGear', () => {
let user;
const analytics = { track () {} };
let clock;
beforeEach(() => {
@@ -47,14 +46,12 @@ describe('shared.ops.buyMarketGear', () => {
sinon.stub(shared, 'randomVal');
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
sinon.stub(shared.fns, 'predictableRandom');
sinon.stub(analytics, 'track');
});
afterEach(() => {
shared.randomVal.restore();
shared.fns.predictableRandom.restore();
shared.onboarding.checkOnboardingStatus.restore();
analytics.track.restore();
if (clock) {
clock.restore();
@@ -65,7 +62,7 @@ describe('shared.ops.buyMarketGear', () => {
it('adds equipment to inventory', async () => {
user.stats.gp = 31;
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
await buyGear(user, { params: { key: 'armor_warrior_1' } });
expect(user.items.gear.owned).to.eql({
weapon_warrior_0: true,
@@ -92,13 +89,12 @@ describe('shared.ops.buyMarketGear', () => {
eyewear_special_whiteHalfMoon: true,
eyewear_special_yellowHalfMoon: true,
});
expect(analytics.track).to.be.calledOnce;
});
it('adds the onboarding achievement to the user and checks the onboarding status', async () => {
user.stats.gp = 31;
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
await buyGear(user, { params: { key: 'armor_warrior_1' } });
expect(user.addAchievement).to.be.calledOnce;
expect(user.addAchievement).to.be.calledWith('purchasedEquipment');
@@ -111,7 +107,7 @@ describe('shared.ops.buyMarketGear', () => {
user.stats.gp = 31;
user.achievements.purchasedEquipment = true;
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
await buyGear(user, { params: { key: 'armor_warrior_1' } });
expect(user.addAchievement).to.not.be.called;
});
+2 -5
View File
@@ -14,7 +14,6 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buyMysterySet', () => {
let user;
const analytics = { track () {} };
let clock;
beforeEach(() => {
@@ -27,11 +26,9 @@ describe('shared.ops.buyMysterySet', () => {
},
},
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
if (clock) {
clock.restore();
}
@@ -93,7 +90,7 @@ describe('shared.ops.buyMysterySet', () => {
context('successful purchases', () => {
it('buys Steampunk Accessories Set', async () => {
user.purchased.plan.consecutive.trinkets = 1;
await buyMysterySet(user, { params: { key: '301404' } }, analytics);
await buyMysterySet(user, { params: { key: '301404' } });
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
expect(user.items.gear.owned).to.have.property('weapon_warrior_0', true);
@@ -106,7 +103,7 @@ describe('shared.ops.buyMysterySet', () => {
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);
await buyMysterySet(user, { params: { key: '201601' } });
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
expect(user.items.gear.owned).to.have.property('shield_mystery_201601', true);
+2 -5
View File
@@ -12,10 +12,9 @@ describe('shared.ops.buyQuestGems', () => {
let user;
let clock;
const goldPoints = 40;
const analytics = { track () {} };
async function buyQuest (_user, _req, _analytics) {
const buyOp = new BuyQuestWithGemOperation(_user, _req, _analytics);
async function buyQuest (_user, _req) {
const buyOp = new BuyQuestWithGemOperation(_user, _req);
return buyOp.purchase();
}
@@ -25,13 +24,11 @@ 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();
});
+8 -17
View File
@@ -12,21 +12,15 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buyQuest', () => {
let user;
const analytics = { track () {} };
async function buyQuest (_user, _req, _analytics) {
const buyOp = new BuyQuestWithGoldOperation(_user, _req, _analytics);
async function buyQuest (_user, _req) {
const buyOp = new BuyQuestWithGoldOperation(_user, _req);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
it('buys a Quest scroll', async () => {
@@ -35,12 +29,11 @@ describe('shared.ops.buyQuest', () => {
params: {
key: 'dilatoryDistress1',
},
}, analytics);
});
expect(user.items.quests).to.eql({
dilatoryDistress1: 1,
});
expect(user.stats.gp).to.equal(5);
expect(analytics.track).to.be.calledOnce;
});
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 () => {
@@ -49,10 +42,9 @@ describe('shared.ops.buyQuest', () => {
user.items.quests[key] = -1;
await buyQuest(user, {
params: { key },
}, analytics);
});
expect(user.items.quests[key]).to.equal(1);
expect(user.stats.gp).to.equal(5);
expect(analytics.track).to.be.calledOnce;
});
it('buys a Quest scroll with the right quantity if a string is passed for quantity', async () => {
@@ -61,13 +53,13 @@ describe('shared.ops.buyQuest', () => {
params: {
key: 'dilatoryDistress1',
},
}, analytics);
});
await buyQuest(user, {
params: {
key: 'dilatoryDistress1',
},
quantity: '3',
}, analytics);
});
expect(user.items.quests).to.eql({
dilatoryDistress1: 4,
@@ -82,7 +74,7 @@ describe('shared.ops.buyQuest', () => {
key: 'dilatoryDistress1',
},
quantity: 'a',
}, analytics);
});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
@@ -187,12 +179,11 @@ describe('shared.ops.buyQuest', () => {
params: {
key: 'dilatoryDistress3',
},
}, analytics);
});
expect(user.items.quests).to.eql({
dilatoryDistress3: 1,
});
expect(user.stats.gp).to.equal(100);
expect(analytics.track).to.be.calledOnce;
});
});
+5 -11
View File
@@ -14,20 +14,17 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buySpecialSpell', () => {
let user;
let clock;
const analytics = { track () {} };
async function buySpecialSpell (_user, _req, _analytics) {
const buyOp = new BuySpellOperation(_user, _req, _analytics);
async function buySpecialSpell (_user, _req) {
const buyOp = new BuySpellOperation(_user, _req);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
if (clock) {
clock.restore();
}
@@ -78,7 +75,7 @@ describe('shared.ops.buySpecialSpell', () => {
params: {
key: 'thankyou',
},
}, analytics);
});
expect(user.stats.gp).to.equal(1);
expect(user.items.special.thankyou).to.equal(1);
@@ -89,7 +86,6 @@ describe('shared.ops.buySpecialSpell', () => {
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 () => {
@@ -101,7 +97,7 @@ describe('shared.ops.buySpecialSpell', () => {
params: {
key: 'nye',
},
}, analytics);
});
expect(user.stats.gp).to.equal(1);
expect(user.items.special.nye).to.equal(1);
@@ -112,7 +108,6 @@ describe('shared.ops.buySpecialSpell', () => {
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 () => {
@@ -140,7 +135,7 @@ describe('shared.ops.buySpecialSpell', () => {
params: {
key: 'seafoam',
},
}, analytics);
});
expect(user.stats.gp).to.equal(1);
expect(user.items.special.seafoam).to.equal(1);
@@ -151,7 +146,6 @@ describe('shared.ops.buySpecialSpell', () => {
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 () => {
+3 -10
View File
@@ -13,21 +13,15 @@ import { BuyHourglassMountOperation } from '../../../../website/common/script/op
describe('common.ops.hourglassPurchase', () => {
let user;
const analytics = { track () {} };
async function buyMount (_user, _req, _analytics) {
const buyOp = new BuyHourglassMountOperation(_user, _req, _analytics);
async function buyMount (_user, _req) {
const buyOp = new BuyHourglassMountOperation(_user, _req);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
context('failure conditions', () => {
@@ -131,12 +125,11 @@ describe('common.ops.hourglassPurchase', () => {
it('buys a pet', async () => {
user.purchased.plan.consecutive.trinkets = 2;
const [, message] = await hourglassPurchase(user, { params: { type: 'pets', key: 'MantisShrimp-Base' } }, analytics);
const [, message] = await hourglassPurchase(user, { params: { type: 'pets', key: 'MantisShrimp-Base' } });
expect(message).to.eql(i18n.t('hourglassPurchase'));
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.items.pets).to.eql({ 'MantisShrimp-Base': 5 });
expect(analytics.track).to.be.calledOnce;
});
it('buys a mount', async () => {
+4 -8
View File
@@ -17,20 +17,17 @@ describe('shared.ops.purchase', () => {
let user;
let clock;
const goldPoints = 40;
const analytics = { track () {} };
before(() => {
user = generateUser({ 'stats.class': 'rogue' });
});
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();
});
@@ -187,11 +184,10 @@ describe('shared.ops.purchase', () => {
const type = 'eggs';
const key = 'Wolf';
await purchase(user, { params: { type, key } }, analytics);
await purchase(user, { params: { type, key } });
expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
expect(analytics.track).to.be.calledOnce;
});
it('purchases hatchingPotions', async () => {
@@ -332,7 +328,7 @@ describe('shared.ops.purchase', () => {
const key = 'Wolf';
try {
await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
await purchase(user, { params: { type, key }, quantity: 'jamboree' });
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
@@ -345,7 +341,7 @@ describe('shared.ops.purchase', () => {
user.balance = 10;
try {
await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
await purchase(user, { params: { type, key }, quantity: -2 });
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
@@ -358,7 +354,7 @@ describe('shared.ops.purchase', () => {
user.balance = 10;
try {
await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
await purchase(user, { params: { type, key }, quantity: 2.9 });
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
-15
View File
@@ -54,19 +54,4 @@ describe('armoire', () => {
const febuaryItems = armoire.all;
expect(febuaryItems.length).to.equal(384);
});
it('sets have at least 2 items', () => {
const setMap = {};
forEach(armoire.all, item => {
// Gotta have one outlier
if (!item.set || item.set.startsWith('armoire-')) return;
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);
});
});
});
@@ -40,7 +40,6 @@ function _requestMaker (user, method, additionalSets = {}) {
|| route.indexOf('/paypal') === 0
|| route.indexOf('/amazon') === 0
|| route.indexOf('/stripe') === 0
|| route.indexOf('/analytics') === 0
) {
url += `${route}`;
} else {
-8
View File
@@ -12,20 +12,12 @@ module.exports = {
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
// TODO find a way to let eslint understand webpack aliases
'import/no-unresolved': 'off',
'import/no-extraneous-dependencies': 'off',
'import/extensions': 'off',
'prefer-regex-literals': 'warn',
'vue/no-v-html': 'off',
'vue/no-mutating-props': 'warn',
// this creates issues with the current way we have to push the process.env vars to webpack
// https://github.com/eslint/eslint/issues/14918
// https://github.com/webpack/webpack/issues/5392
// off for now, because any eslint --fix will then still do it anyway
// maybe this can be turned on again once we switch to newer vue/vite
// Important! process.env.XYZ should not be destructured
'prefer-destructuring': 'off',
'vue/html-self-closing': ['error', {
html: {
void: 'never',
+20 -589
View File
@@ -41,7 +41,6 @@
"vite": "^6.3.6",
"vite-plugin-compression2": "^1.3.3",
"vue": "^2.7.10",
"vue-fragment": "^1.6.0",
"vue-mugen-scroll": "^0.2.6",
"vue-router": "^3.6.5",
"vuedraggable": "^2.24.3",
@@ -55,9 +54,7 @@
"jsdom": "^26.0.0",
"mocha": "^11.1.0",
"playwright": "^1.50.1",
"terser-webpack-plugin": "^5.3.10",
"vitest": "^3.0.5",
"webpack": "^5.94.0"
"vitest": "^3.0.5"
}
},
"node_modules/@amplitude/analytics-connector": {
@@ -2111,8 +2108,9 @@
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
@@ -3634,41 +3632,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
}
},
"node_modules/@types/eslint-scope": {
"version": "3.7.7",
"resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
"integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint": "*",
"@types/estree": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -3679,8 +3648,9 @@
"version": "24.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -3876,181 +3846,6 @@
"vue-template-compiler": "^2.x"
}
},
"node_modules/@webassemblyjs/ast": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
"integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/helper-numbers": "1.13.2",
"@webassemblyjs/helper-wasm-bytecode": "1.13.2"
}
},
"node_modules/@webassemblyjs/floating-point-hex-parser": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
"integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-api-error": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
"integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-buffer": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
"integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-numbers": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
"integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/floating-point-hex-parser": "1.13.2",
"@webassemblyjs/helper-api-error": "1.13.2",
"@xtuc/long": "4.2.2"
}
},
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
"integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-wasm-section": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
"integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-buffer": "1.14.1",
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
"@webassemblyjs/wasm-gen": "1.14.1"
}
},
"node_modules/@webassemblyjs/ieee754": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
"integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@xtuc/ieee754": "^1.2.0"
}
},
"node_modules/@webassemblyjs/leb128": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
"integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@xtuc/long": "4.2.2"
}
},
"node_modules/@webassemblyjs/utf8": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
"integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/wasm-edit": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
"integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-buffer": "1.14.1",
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
"@webassemblyjs/helper-wasm-section": "1.14.1",
"@webassemblyjs/wasm-gen": "1.14.1",
"@webassemblyjs/wasm-opt": "1.14.1",
"@webassemblyjs/wasm-parser": "1.14.1",
"@webassemblyjs/wast-printer": "1.14.1"
}
},
"node_modules/@webassemblyjs/wasm-gen": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
"integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
"@webassemblyjs/ieee754": "1.13.2",
"@webassemblyjs/leb128": "1.13.2",
"@webassemblyjs/utf8": "1.13.2"
}
},
"node_modules/@webassemblyjs/wasm-opt": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
"integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-buffer": "1.14.1",
"@webassemblyjs/wasm-gen": "1.14.1",
"@webassemblyjs/wasm-parser": "1.14.1"
}
},
"node_modules/@webassemblyjs/wasm-parser": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
"integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-api-error": "1.13.2",
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
"@webassemblyjs/ieee754": "1.13.2",
"@webassemblyjs/leb128": "1.13.2",
"@webassemblyjs/utf8": "1.13.2"
}
},
"node_modules/@webassemblyjs/wast-printer": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
"integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@xtuc/long": "4.2.2"
}
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@xtuc/long": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
@@ -4098,48 +3893,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/amplitude-js": {
"version": "8.21.9",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.21.9.tgz",
@@ -4617,8 +4370,9 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"devOptional": true,
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/cac": {
"version": "6.7.14",
@@ -4783,16 +4537,6 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/chrome-trace-event": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0"
}
},
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -4859,8 +4603,9 @@
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"devOptional": true,
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/concat-map": {
"version": "0.0.1",
@@ -5196,20 +4941,6 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/enhanced-resolve": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/enquirer": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
@@ -6384,16 +6115,6 @@
"node": ">=0.10.0"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/expect-type": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
@@ -6871,13 +6592,6 @@
"node": ">= 6"
}
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/globals": {
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
@@ -6921,13 +6635,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/habitica-markdown": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/habitica-markdown/-/habitica-markdown-4.1.0.tgz",
@@ -7726,37 +7433,6 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jest-worker": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"merge-stream": "^2.0.0",
"supports-color": "^8.0.0"
},
"engines": {
"node": ">= 10.13.0"
}
},
"node_modules/jest-worker/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/jquery": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
@@ -7841,13 +7517,6 @@
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"license": "MIT"
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -7911,20 +7580,6 @@
"uc.micro": "^2.0.0"
}
},
"node_modules/loader-runner": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
"integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.11.5"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -8128,13 +7783,6 @@
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"license": "MIT"
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true,
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -8458,13 +8106,6 @@
"node": ">= 0.4.0"
}
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true,
"license": "MIT"
},
"node_modules/nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@@ -9504,63 +9145,6 @@
"node": ">=v12.22.7"
}
},
"node_modules/schema-utils": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.9.0",
"ajv-formats": "^2.1.1",
"ajv-keywords": "^5.1.0"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/schema-utils/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/schema-utils/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3"
},
"peerDependencies": {
"ajv": "^8.8.2"
}
},
"node_modules/schema-utils/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/secure-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/secure-keys/-/secure-keys-1.0.0.tgz",
@@ -9838,8 +9422,9 @@
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@@ -10130,20 +9715,6 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tar-mini": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/tar-mini/-/tar-mini-0.2.0.tgz",
@@ -10154,8 +9725,9 @@
"version": "5.44.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"devOptional": true,
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@@ -10169,47 +9741,13 @@
"node": ">=10"
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.14",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
"integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
"schema-utils": "^4.3.0",
"serialize-javascript": "^6.0.2",
"terser": "^5.31.1"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"webpack": "^5.1.0"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"esbuild": {
"optional": true
},
"uglify-js": {
"optional": true
}
}
},
"node_modules/terser/node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -10585,8 +10123,9 @@
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"devOptional": true,
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/update-browserslist-db": {
"version": "1.1.4",
@@ -10965,15 +10504,6 @@
"node": ">=6.0.0"
}
},
"node_modules/vue-fragment": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vue-fragment/-/vue-fragment-1.6.0.tgz",
"integrity": "sha512-a5T8ZZZK/EQzgVShEl374HbobUJ0a7v12BzOzS6Z/wd/5EE/5SffcyHC+7bf9hP3L7Yc0hhY/GhMdwFQ25O/8A==",
"license": "MIT",
"peerDependencies": {
"vue": "^2.5.16"
}
},
"node_modules/vue-functional-data-merge": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz",
@@ -11041,20 +10571,6 @@
"node": ">=18"
}
},
"node_modules/watchpack": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
"integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -11065,91 +10581,6 @@
"node": ">=12"
}
},
"node_modules/webpack": {
"version": "5.102.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
"integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1",
"acorn": "^8.15.0",
"acorn-import-phases": "^1.0.3",
"browserslist": "^4.26.3",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.17.3",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.2.11",
"json-parse-even-better-errors": "^2.3.1",
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
"schema-utils": "^4.3.3",
"tapable": "^2.3.0",
"terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.4",
"webpack-sources": "^3.3.3"
},
"bin": {
"webpack": "bin/webpack.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependenciesMeta": {
"webpack-cli": {
"optional": true
}
}
},
"node_modules/webpack-sources": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/webpack/node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/webpack/node_modules/acorn-import-phases": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.13.0"
},
"peerDependencies": {
"acorn": "^8.14.0"
}
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+1 -4
View File
@@ -46,7 +46,6 @@
"vite": "^6.3.6",
"vite-plugin-compression2": "^1.3.3",
"vue": "^2.7.10",
"vue-fragment": "^1.6.0",
"vue-mugen-scroll": "^0.2.6",
"vue-router": "^3.6.5",
"vuedraggable": "^2.24.3",
@@ -60,8 +59,6 @@
"jsdom": "^26.0.0",
"mocha": "^11.1.0",
"playwright": "^1.50.1",
"terser-webpack-plugin": "^5.3.10",
"vitest": "^3.0.5",
"webpack": "^5.94.0"
"vitest": "^3.0.5"
}
}
+134 -2
View File
@@ -1,3 +1,68 @@
.quest_lostMasterclasser4 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_lostMasterclasser4.gif") no-repeat;
width: 219px;
height: 219px;
}
.quest_windup {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_windup.gif") no-repeat;
width: 219px;
height: 219px;
}
.quest_solarSystem {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_solarSystem.gif") no-repeat;
width: 219px;
height: 219px;
}
.quest_virtualpet {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_virtualpet.gif") no-repeat;
width: 219px;
height: 219px;
}
.quest_alien {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_alien.gif") no-repeat;
width: 219px;
height: 219px;
}
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid,
.Pet_HatchingPotion_Alien {
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Dessert {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Dessert.gif") no-repeat;
}
.Pet_HatchingPotion_Veggie {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Veggie.gif") no-repeat;
}
.Pet_HatchingPotion_Windup {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Windup.gif") no-repeat;
}
.Pet_HatchingPotion_VirtualPet {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_VirtualPet.gif") no-repeat;
}
.Pet_HatchingPotion_Fungi {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
}
.Pet_HatchingPotion_Cryptid {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
}
.Pet_HatchingPotion_Alien {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Alien.gif") no-repeat;
}
.Gems {
display:inline-block;
margin-right:5px;
@@ -26,7 +91,6 @@
margin-left: -3px;
margin-top: -18px;
}
.slim_armor_special_0, .broad_armor_special_0, .shield_special_0 {
width: 90px;
height: 90px;
@@ -34,6 +98,7 @@
/* Critical */
.weapon_special_critical {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_critical.gif") no-repeat;
width: 90px;
height: 90px;
margin-left:-12px;
@@ -44,7 +109,6 @@
.weapon_special_1 {
margin-left: -12px;
}
.broad_armor_special_1, .slim_armor_special_1, .head_special_1 {
width: 90px;
height: 90px;
@@ -53,15 +117,36 @@
.back_special_heroicAureole {
width: 114px;
height: 90px;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_special_heroicAureole.gif") no-repeat;
}
.head_special_0 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeHelmet.gif") no-repeat;
}
.head_special_1 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalHelmet.gif") no-repeat;
margin-top: 3px;
}
.broad_armor_special_0,.slim_armor_special_0 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeArmor.gif") no-repeat;
}
.broad_armor_special_1,.slim_armor_special_1 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalArmor.gif") no-repeat;
}
.shield_special_0 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Shield-TormentedSkull.gif") no-repeat;
}
.weapon_special_0 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Weapon-DarkSoulsBlade.gif") no-repeat;
}
.Pet-Wolf-Cerberus {
width: 105px;
height: 72px;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-CerberusPup.gif") no-repeat;
}
.broad_armor_special_ks2019, .slim_armor_special_ks2019, .eyewear_special_ks2019, .head_special_ks2019, .shield_special_ks2019 {
@@ -69,17 +154,36 @@
height: 120px;
}
.broad_armor_special_ks2019, .slim_armor_special_ks2019 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonArmor.gif") no-repeat;
}
.eyewear_special_ks2019 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonVisor.gif") no-repeat;
}
.head_special_ks2019 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonHelm.gif") no-repeat;
}
.shield_special_ks2019 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonShield.gif") no-repeat;
}
.weapon_special_ks2019 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonGlaive.gif") no-repeat;
width: 120px;
height: 120px;
}
.Pet-Gryphon-Gryphatrice {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-Gryphatrice.gif") no-repeat;
width: 81px;
height: 99px;
}
.Pet-Gryphatrice-Jubilant {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Gryphatrice-Jubilant.gif") no-repeat;
width: 81px;
height: 96px;
}
@@ -89,11 +193,39 @@
height: 135px;
}
.Mount_Head_Gryphon-Gryphatrice {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Head-Gryphatrice.gif") no-repeat;
}
.Mount_Body_Gryphon-Gryphatrice {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat;
}
.Mount_Head_Dragon-Hydra {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Hydra.gif") no-repeat;
}
.Mount_Body_Dragon-Hydra {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Hydra.gif") no-repeat;
}
.background_airship, .background_clocktower, .background_steamworks {
width: 141px;
height: 147px;
}
.background_airship {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_airship.gif") no-repeat;
}
.background_clocktower {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_clocktower.gif") no-repeat;
}
.background_steamworks {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_steamworks.gif") no-repeat;
}
[class*="Mount_Head_"],
[class*="Mount_Body_"] {
margin-top:18px; /* Sprite accommodates 105x123 box */
+4
View File
@@ -16,6 +16,10 @@
border-bottom: 0;
}
.d-content {
display: contents;
}
* {
transition: none;
}
@@ -108,15 +108,15 @@ export default {
const allEmails = [];
if (user.auth.local.email) allEmails.push(user.auth.local.email);
if (user.auth.google && user.auth.google.emails) {
const emails = user.auth.google.emails;
const { emails } = user.auth.google;
allEmails.push(...this.findSocialEmails(emails));
}
if (user.auth.apple && user.auth.apple.emails) {
const emails = user.auth.apple.emails;
const { emails } = user.auth.apple;
allEmails.push(...this.findSocialEmails(emails));
}
if (user.auth.facebook && user.auth.facebook.emails) {
const emails = user.auth.facebook.emails;
const { emails } = user.auth.facebook;
allEmails.push(...this.findSocialEmails(emails));
}
return allEmails;
@@ -609,7 +609,7 @@ import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks
import saveHero from '../mixins/saveHero';
import LoadingSpinner from '@/components/ui/loadingSpinner';
const PLAY_CONSOLE_ORDERS_BASE_URL = import.meta.env.PLAY_CONSOLE_ORDERS_BASE_URL;
const { PLAY_CONSOLE_ORDERS_BASE_URL } = import.meta.env;
const humanReadablePaymentDetails = {
customerId: {
@@ -20,6 +20,29 @@
class="form mx-auto"
@submit.prevent.stop="register()"
>
<div v-if="needsEmailField">
<input
id="emailInput"
v-model="email"
class="form-control dark"
type="text"
:placeholder="$t('emailAddress')"
:class="{
'mb-3': !emailError,
'input-invalid input-with-error mb-2': emailError,
'input-valid': email && emailValid,
}"
>
<div
v-if="emailError"
class="input-error"
>
{{ emailError }}
</div>
<p class="purple-600 mb-3">
{{ $t('emailRequiredForSupport') }}
</p>
</div>
<input
id="usernameInput"
v-model="username"
@@ -58,8 +81,9 @@
></label>
</div>
<button
class="btn btn-info d-block w-100 sign-up mx-auto mb-5"
:disabled="!username || usernameInvalid || !privacyAccepted"
class="btn btn-info d-flex justify-content-center
align-items-center w-100 sign-up mx-auto mb-5"
:disabled="!email || emailError || !username || usernameInvalid || !privacyAccepted"
type="submit"
>
{{ $t('getStarted') }}
@@ -133,10 +157,12 @@
border: 2px solid transparent;
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
&:focus, &:active {
background-color: $blue-50;
border: 2px solid $purple-400;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
&:not(:disabled):not(.disabled) {
&:focus, &:active {
background-color: $blue-50;
border: 2px solid $purple-400;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
}
}
}
@@ -148,23 +174,19 @@
<script>
import debounce from 'lodash/debounce';
import PrivacyBanner from '@/components/header/banners/privacy';
import accountCreation from '@/mixins/accountCreation';
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
export default {
components: {
PrivacyBanner,
},
mixins: [sanitizeRedirect],
mixins: [accountCreation, sanitizeRedirect],
data () {
return {
authData: {},
email: '',
password: '',
passwordConfirm: '',
privacyAccepted: false,
registrationMethod: null,
username: '',
usernameIssues: [],
needsEmailField: false,
};
},
computed: {
@@ -183,30 +205,40 @@ export default {
},
},
mounted () {
if (window.sessionStorage.getItem('apple-token')) {
this.registrationMethod = 'apple';
} else if (!this.$store.state.registrationOptions.registrationMethod) {
this.$router.push('/');
} else {
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
}
this.authData = this.$store.state.registrationOptions.authData;
this.email = this.$store.state.registrationOptions.email;
this.username = this.$store.state.registrationOptions.username;
this.password = this.$store.state.registrationOptions.password;
this.passwordConfirm = this.$store.state.registrationOptions.passwordConfirm;
if (!this.email) {
if (window.sessionStorage.getItem('apple-token')) {
this.registrationMethod = 'apple';
if (!this.email) {
this.email = window.sessionStorage.getItem('apple-email');
}
} else if (!this.$store.state.registrationOptions.registrationMethod) {
this.$router.push('/');
} else {
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
}
if (!this.email && this.registrationMethod !== 'apple') {
return;
}
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
this.$store.dispatch('auth:verifyUsername', {
username: usernameToCheck,
}).then(res => {
if (!res.issues) {
this.username = usernameToCheck;
}
});
if ((!this.email || this.email === '') && this.registrationMethod === 'apple') {
this.needsEmailField = true;
}
if (this.email) {
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
this.$store.dispatch('auth:verifyUsername', {
username: usernameToCheck,
}).then(res => {
if (!res.issues) {
this.username = usernameToCheck;
}
});
}
document.getElementById('usernameInput').focus();
},
methods: {
@@ -237,6 +269,7 @@ export default {
idToken: window.sessionStorage.getItem('apple-token'),
name: window.sessionStorage.getItem('apple-name'),
username: this.username,
email: this.email,
allowRegister: true,
});
} else {
@@ -189,7 +189,7 @@ export default {
this.cancel();
return [];
}
this.currentSearch = regexRes[1];
this.currentSearch = regexRes[1]; // eslint-disable-line prefer-destructuring
if (this.currentSearch.length === 0) return [];
@@ -470,7 +470,7 @@ export default {
return this.userGuilds.filter(group => {
const leaderId = group.leader?._id || group.leader;
if (leaderId !== this.user._id) return false;
const purchased = group.purchased;
const { purchased } = group;
if (!purchased?.wasUpgraded) return false;
if (this.activeGroupPlanIds.includes(group._id)) return false;
if (!purchased.dateTerminated) return false;
@@ -492,7 +492,7 @@ export default {
},
isPartyPreviouslyUpgraded () {
if (!this.userParty) return false;
const purchased = this.userParty.purchased;
const { purchased } = this.userParty;
if (!purchased?.wasUpgraded) return false;
if (!purchased.dateTerminated) return false;
return new Date(purchased.dateTerminated) < new Date();
@@ -533,7 +533,7 @@ export default {
this.$nextTick(() => {
if (this.upgradeableGuilds.length > 0) {
this.selectedOption = this.upgradeableGuilds[0];
[this.selectedOption] = this.upgradeableGuilds;
} else if (this.upgradeableParty) {
this.selectedOption = this.upgradeableParty;
} else {
@@ -198,7 +198,6 @@ import dailyIcon from '@/assets/svg/daily.svg?raw';
import todoIcon from '@/assets/svg/todo.svg?raw';
import rewardIcon from '@/assets/svg/reward.svg?raw';
import * as Analytics from '@/libs/analytics';
import { mapState } from '@/libs/store';
export default {
@@ -438,14 +437,6 @@ export default {
return false;
},
changeMirrorPreference (newVal) {
Analytics.track({
eventName: 'mirror tasks',
eventAction: 'mirror tasks',
eventCategory: 'behavior',
hitType: 'event',
mirror: newVal,
group: this.group._id,
}, { trackOnClient: true });
const groupsToMirror = this.user.preferences.tasks.mirrorGroupTasks || [];
if (newVal) { // we're turning copy ON for this group
groupsToMirror.push(this.group._id);
@@ -240,7 +240,6 @@
<script>
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import notifications from '@/mixins/notifications';
import closeX from '../ui/closeX';
@@ -276,11 +275,6 @@ export default {
this.$store.state.party.data = party;
this.user.party._id = party._id;
Analytics.updateUser({
partyID: party._id,
partySize: 1,
});
this.$root.$emit('bv::hide::modal', 'create-party-modal');
await this.$router.push('/party');
},
@@ -314,7 +314,6 @@ import extend from 'lodash/extend';
import groupUtilities from '@/mixins/groupsUtilities';
import styleHelper from '@/mixins/styleHelper';
import { mapGetters } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import participantListModal from './participantListModal';
import groupFormModal from './groupFormModal';
import groupGemsModal from '@/components/groups/groupGemsModal';
@@ -560,7 +559,6 @@ export default {
if (this.isParty) {
data.type = 'party';
Analytics.updateUser({ partySize: null, partyID: null });
this.$store.state.partyMembers = [];
}
@@ -334,7 +334,6 @@ import orderBy from 'lodash/orderBy';
import * as quests from '@/../../common/script/content/quests';
import getItemInfo from '@/../../common/script/libs/getItemInfo';
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import navigationBack from '@/assets/svg/navigation_back.svg?raw';
import questDialogContent from '../shops/quests/questDialogContent';
@@ -421,11 +420,6 @@ export default {
async questInit () {
this.loading = true;
Analytics.updateUser({
partyID: this.group._id,
partySize: this.group.memberCount,
});
const groupId = this.group._id || this.user.party._id;
const key = this.selectedQuest;
@@ -123,7 +123,6 @@
<script>
import orderBy from 'lodash/orderBy';
import * as Analytics from '@/libs/analytics';
import { mapGetters, mapActions } from '@/libs/store';
import MemberDetails from '../memberDetails';
import createPartyModal from '../groups/createPartyModal';
@@ -236,22 +235,8 @@ export default {
},
async createOrInviteParty () {
if (this.user.party._id) {
await Analytics.track({
eventName: 'Header Party CTA',
eventAction: 'Header Party CTA',
eventCategory: 'behavior',
hitType: 'event',
state: 'Find Party Members',
});
this.$router.push('/looking-for-party');
} else {
await Analytics.track({
eventName: 'Header Party CTA',
eventAction: 'Header Party CTA',
eventCategory: 'behavior',
hitType: 'event',
state: 'Get Started',
});
this.$root.$emit('bv::show::modal', 'create-party-modal');
}
},
@@ -114,7 +114,6 @@ import { mapState } from '@/libs/store';
import notifications from '@/mixins/notifications';
import guide from '@/mixins/guide';
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
import * as Analytics from '@/libs/analytics';
import yesterdailyModal from './tasks/yesterdailyModal';
import newStuff from './news/modal';
@@ -648,15 +647,6 @@ export default {
// Reset daily analytics actions
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 0);
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 0);
} else {
// Note a failed cron event, for our records and investigation
Analytics.track({
eventName: 'cron failed',
eventAction: 'cron failed',
eventCategory: 'behavior',
hitType: 'event',
responseCode: response.status,
}, { trackOnClient: true });
}
// Sync
@@ -433,9 +433,6 @@ import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import notificationsMixin from '@/mixins/notifications';
import paymentsMixin from '@/mixins/payments';
// analytics
import * as Analytics from '@/libs/analytics';
export default {
components: {
selectTranslatedArray,
@@ -536,16 +533,6 @@ export default {
this.close();
},
submit () {
if (this.paymentData.group && !this.paymentData.newGroup) {
Analytics.track({
hitType: 'event',
eventName: 'group plan upgrade',
eventAction: 'group plan upgrade',
eventCategory: 'behavior',
demographics: this.upgradedGroup.demographics,
type: this.paymentData.group.type,
}, { trackOnClient: true });
}
this.paymentData = {};
this.$root.$emit('bv::hide::modal', 'payments-success-modal');
},
@@ -37,6 +37,9 @@ export default {
window.location.href = '/';
} else {
window.sessionStorage.setItem('apple-token', response.idToken);
if (response.email) {
window.sessionStorage.setItem('apple-email', response.email);
}
window.location.href = '/username';
}
},
@@ -83,7 +83,7 @@
</div>
</div>
<draggable
v-if="taskList.length > 0"
v-if="taskList.length > 0 && !rerendering"
ref="tasksList"
class="sortable-tasks"
:disabled="activeFilter.label === 'scheduled' || !canBeDragged()"
@@ -432,6 +432,7 @@ export default {
selectedItemToBuy: {},
dragging: false,
rerendering: false,
};
},
computed: {
@@ -548,8 +549,8 @@ export default {
if (this.taskListOverride) originTasks = this.taskListOverride;
// Server
const taskIdToReplace = filteredList[data.newIndex];
const newIndexOnServer = originTasks.findIndex(taskId => taskId === taskIdToReplace);
const taskIdToReplace = filteredList[data.newIndex]._id;
const newIndexOnServer = originTasks.findIndex(task => task._id === taskIdToReplace);
let newOrder;
if (taskToMove.group.id && !this.isUser) {
@@ -568,6 +569,9 @@ export default {
// Client
const deleted = originTasks.splice(data.oldIndex, 1);
originTasks.splice(data.newIndex, 0, deleted[0]);
this.rerendering = true;
await this.$nextTick();
this.rerendering = false;
},
async moveTo (task, where) { // where is 'top' or 'bottom'
const taskIdToMove = task._id;
@@ -55,11 +55,31 @@
</div>
</div>
<div class="form-group">
<lockable-label
:class-override="cssClass('headings')"
:locked="challengeAccessRequired"
:text="`${$t('text')}*`"
/>
<div class="d-flex align-items-center">
<lockable-label
class="mr-auto"
:class-override="cssClass('headings')"
:locked="challengeAccessRequired"
:text="`${$t('text')}*`"
/>
<div
id="spi-alert"
class="d-flex align-items-center"
:class="cssClass('headings')"
>
<div
class="svg svg-icon color icon-16 mr-1"
v-html="icons.alert"
></div>
<small
class="my-1"
>
<a
:class="cssClass('headings')"
>{{ $t('avoidSPI') }}</a>
</small>
</div>
</div>
<input
ref="inputToFocus"
v-model="task.text"
@@ -79,10 +99,20 @@
@keydown.esc="autoCompleteMixinHandleEscape($event)"
>
</div>
<b-popover
:target="'spi-alert'"
triggers="hover"
placement="bottom"
offset="-128"
>
<div
v-html="$t('avoidSPIDetails', spiLinkData)">
</div>
</b-popover>
<div
class="form-group mb-0"
>
<div class="d-flex">
<div class="d-flex align-items-center">
<lockable-label
class="mr-auto"
:class-override="cssClass('headings')"
@@ -963,6 +993,20 @@
box-shadow: 0px 1px 3px 0px rgba(26, 24, 29, 0.12), 0px 1px 2px 0px rgba(26, 24, 29, 0.24);
}
}
.b-popover {
margin-top: -5px;
max-width: 330px;
}
.popover-body {
text-align: left;
a {
color: $gray-500;
text-decoration: underline;
}
}
}
@media only screen and (max-width: 768px) {
@@ -1196,6 +1240,7 @@ import chevronIcon from '@/assets/svg/chevron.svg?raw';
import calendarIcon from '@/assets/svg/calendar.svg?raw';
import gripIcon from '@/assets/svg/grip.svg?raw';
import InformationIcon from '@/components/ui/informationIcon.vue';
import alertIcon from '@/assets/svg/for-css/alert-white.svg?raw';
export default {
components: {
@@ -1231,6 +1276,7 @@ export default {
streak: streakIcon,
calendar: calendarIcon,
grip: gripIcon,
alert: alertIcon,
}),
members: [],
membersNameAndId: [],
@@ -1251,6 +1297,11 @@ export default {
{ key: 'per', label: 'perception', description: 'perTaskText' },
],
calendarHighlights: { dates: [new Date()] },
spiLinkData: {
firstLink: '<a href="/static/privacy#section_1" target="_blank">',
secondLink: '<a href="/static/privacy" target="_blank">',
linkClose: '</a>',
},
};
},
computed: {
@@ -1399,7 +1450,7 @@ export default {
this.task.down = !this.task.down;
},
weekdaysMin (dayNumber) {
return moment.weekdaysMin(dayNumber);
return this.$t(`weekdaysMin${dayNumber}`);
},
formattedDate (date) {
return moment(date).format('MM/DD/YYYY');
+1 -1
View File
@@ -6,7 +6,7 @@ import amplitude from 'amplitude-js';
import Vue from 'vue';
import getStore from '@/store';
const AMPLITUDE_KEY = import.meta.env.AMPLITUDE_KEY;
const { AMPLITUDE_KEY } = import.meta.env;
const REQUIRED_FIELDS = ['eventCategory', 'eventAction'];
let analyticsLoading = false;
-18
View File
@@ -1,28 +1,10 @@
// Vue plugin to globally expose a '$t' method that calls common/i18n.t.
// Can be anywhere inside vue as 'this.$t' or '$t' in templates.
import moment from 'moment';
import i18n from '@/../../common/script/i18n';
function loadLocale (i18nData) {
// Load i18n strings
i18n.strings = i18nData.strings;
// Load Moment.js locale
const { language } = i18nData;
if (language && i18nData.momentLang && language.momentLangCode) {
// Make moment available under `window` so that the locale can be set
window.moment = moment;
// Execute the script and set the locale
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');
script.type = 'text/javascript';
script.text = i18nData.momentLang;
head.appendChild(script);
moment.updateLocale(language.momentLangCode);
}
}
export default {
-2
View File
@@ -11,7 +11,6 @@ import {
NavbarPlugin,
CollapsePlugin,
} from 'bootstrap-vue';
import Fragment from 'vue-fragment';
import AppComponent from './app';
import { setUpLogging } from '@/libs/logging';
import router from './router/index';
@@ -44,7 +43,6 @@ Vue.use(FormRadioPlugin);
Vue.use(TooltipPlugin);
Vue.use(NavbarPlugin);
Vue.use(CollapsePlugin);
Vue.use(Fragment.Plugin);
setUpLogging();
const store = getStore();
+1 -12
View File
@@ -6,9 +6,8 @@ import { mapState } from '@/libs/store';
import encodeParams from '@/libs/encodeParams';
import notificationsMixin from '@/mixins/notifications';
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
import * as Analytics from '@/libs/analytics';
const STRIPE_PUB_KEY = import.meta.env.STRIPE_PUB_KEY;
const { STRIPE_PUB_KEY } = import.meta.env;
let stripeInstance = null;
@@ -207,16 +206,6 @@ export default {
alert(`Error while redirecting to Stripe: ${checkoutSessionResult.error.message}`);
throw checkoutSessionResult.error;
}
if (paymentType === 'groupPlan') {
Analytics.track({
hitType: 'event',
eventName: 'group plan create',
eventAction: 'group plan create',
eventCategory: 'behavior',
demographics: appState.newGroup.demographics,
type: appState.newGroup.type,
}, { trackOnClient: true });
}
} catch (err) {
console.error('Error while redirecting to Stripe', err); // eslint-disable-line
alert(`Error while redirecting to Stripe: ${err.message}`);
-10
View File
@@ -3,7 +3,6 @@ import Vue from 'vue';
import scoreTask from '@/../../common/script/ops/scoreTask';
import notifications from './notifications';
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
export default {
@@ -58,15 +57,6 @@ export default {
const tasksScoredCount = getLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT);
if (!tasksScoredCount || tasksScoredCount < 2) {
Analytics.track({
eventName: 'task scored',
eventAction: 'task scored',
eventCategory: 'behavior',
hitType: 'event',
uuid: user._id,
taskType: task.type,
direction,
}, { trackOnClient: true });
if (!tasksScoredCount) {
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 1);
} else {
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -90,7 +90,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment v-if="allowedToChangeClass">
<div class="d-content" v-if="allowedToChangeClass">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -71,7 +71,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -55,7 +55,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -77,7 +77,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -94,7 +94,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -78,7 +78,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -83,7 +83,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr>
<td class="settings-label">
{{ $t("showHeader") }}
@@ -26,7 +26,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -67,7 +67,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-for="network in SOCIAL_AUTH_NETWORKS"
:key="network.key"
@@ -39,7 +39,7 @@
</a>
</td>
</tr>
</fragment>
</div>
</template>
<script>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -66,7 +66,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -111,7 +111,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -56,7 +56,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -60,7 +60,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -54,7 +54,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -48,7 +48,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -76,7 +76,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
+1 -2
View File
@@ -4,6 +4,7 @@
:class="{
'casting-spell': castingSpell,
}"
@dragover.prevent
>
<!-- <banned-account-modal /> -->
<amazon-payments-modal v-if="!isStaticPage" />
@@ -130,7 +131,6 @@ import PrivacyBanner from '@/components/header/banners/privacy';
import AppFooter from '@/components/appFooter';
import notificationsDisplay from '@/components/notifications';
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import BuyModal from '@/components/shops/buyModal.vue';
import SelectMembersModal from '@/components/selectMembersModal.vue';
import notifications from '@/mixins/notifications';
@@ -276,7 +276,6 @@ export default {
}
}
Analytics.updateUser();
return this.loadAllTranslations();
}).then(() => {
this.$store.state.isUserLoaded = true;
+33 -43
View File
@@ -1,6 +1,5 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import * as Analytics from '@/libs/analytics';
import getStore from '@/store';
import handleRedirect from './handleRedirect';
@@ -11,56 +10,56 @@ import { DEPRECATED_ROUTES } from '@/router/deprecated-routes';
// NOTE: when adding a page make sure to implement the `common:setTitle` action
const Logout = () => import(/* webpackChunkName: "auth" */'@/components/auth/logout');
const Logout = () => import('@/components/auth/logout');
// Hall
const HallPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/index');
const PatronsPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/patrons');
const HeroesPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/heroes');
const HallPage = () => import('@/components/hall/index');
const PatronsPage = () => import('@/components/hall/patrons');
const HeroesPage = () => import('@/components/hall/heroes');
// Admin Pages
const AdminContainerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/container');
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel');
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/user-support');
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/search');
const GroupAdminPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups');
const GroupAdminGroupPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups/group-support');
const BlockerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/blocker');
const AdminContainerPage = () => import('@/components/admin/container');
const AdminPanelPage = () => import('@/components/admin/admin-panel');
const AdminPanelUserPage = () => import('@/components/admin/admin-panel/user-support');
const AdminPanelSearchPage = () => import('@/components/admin/admin-panel/search');
const GroupAdminPage = () => import('@/components/admin/groups');
const GroupAdminGroupPage = () => import('@/components/admin/groups/group-support');
const BlockerPage = () => import('@/components/admin/blocker');
// Tasks
const UserTasks = () => import(/* webpackChunkName: "userTasks" */'@/components/tasks/user');
const UserTasks = () => import('@/components/tasks/user');
// Inventory
const InventoryContainer = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/index');
const ItemsPage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/items/index');
const EquipmentPage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/equipment/index');
const StablePage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/stable/index');
const InventoryContainer = () => import('@/components/inventory/index');
const ItemsPage = () => import('@/components/inventory/items/index');
const EquipmentPage = () => import('@/components/inventory/equipment/index');
const StablePage = () => import('@/components/inventory/stable/index');
// Guilds & Parties
const GroupPage = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/group');
const GroupPlansAppPage = () => import(/* webpackChunkName: "guilds" */ '@/components/static/groupPlans');
const LookingForParty = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/lookingForParty');
const GroupPage = () => import('@/components/groups/group');
const GroupPlansAppPage = () => import('@/components/static/groupPlans');
const LookingForParty = () => import('@/components/groups/lookingForParty');
// Group Plans
const GroupPlanIndex = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/index');
const GroupPlanTaskInformation = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/taskInformation');
const GroupPlanBilling = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/billing');
const GroupPlanIndex = () => import('@/components/group-plans/index');
const GroupPlanTaskInformation = () => import('@/components/group-plans/taskInformation');
const GroupPlanBilling = () => import('@/components/group-plans/billing');
const MessagesIndex = () => import(/* webpackChunkName: "private-messages" */ '@/pages/private-messages/index.vue');
const MessagesIndex = () => import('@/pages/private-messages/index.vue');
// Challenges
const ChallengeIndex = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/index');
const MyChallenges = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/myChallenges');
const FindChallenges = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/findChallenges');
const ChallengeDetail = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/challengeDetail');
const ChallengeIndex = () => import('@/components/challenges/index');
const MyChallenges = () => import('@/components/challenges/myChallenges');
const FindChallenges = () => import('@/components/challenges/findChallenges');
const ChallengeDetail = () => import('@/components/challenges/challengeDetail');
// Shops
const ShopsContainer = () => import(/* webpackChunkName: "shops" */'@/components/shops/index');
const MarketPage = () => import(/* webpackChunkName: "shops-market" */'@/components/shops/market/index');
const QuestsPage = () => import(/* webpackChunkName: "shops-quest" */'@/components/shops/quests/index');
const CustomizationsPage = () => import(/* webpackChunkName: "shops-customizations" */'@/components/shops/customizations/index');
const SeasonalPage = () => import(/* webpackChunkName: "shops-seasonal" */'@/components/shops/seasonal/index');
const TimeTravelersPage = () => import(/* webpackChunkName: "shops-timetravelers" */'@/components/shops/timeTravelers/index');
const ShopsContainer = () => import('@/components/shops/index');
const MarketPage = () => import('@/components/shops/market/index');
const QuestsPage = () => import('@/components/shops/quests/index');
const CustomizationsPage = () => import('@/components/shops/customizations/index');
const SeasonalPage = () => import('@/components/shops/seasonal/index');
const TimeTravelersPage = () => import('@/components/shops/timeTravelers/index');
Vue.use(VueRouter);
@@ -318,15 +317,6 @@ router.beforeEach(async (to, from, next) => {
router.app.$root.$emit('update-party');
}
if (to.name === 'lookingForParty') {
Analytics.track({
hitType: 'event',
eventName: 'View Find Members',
eventAction: 'View Find Members',
eventCategory: 'behavior',
}, { trackOnClient: true });
}
// Redirect old guild urls
if (to.hash.indexOf('#/options/groups/guilds/') !== -1) {
const splits = to.hash.split('/');
+2 -2
View File
@@ -21,8 +21,8 @@ const NewsPage = () => import('@/components/static/newStuff');
const OverviewPage = () => import('@/components/static/overview');
const PressKitPage = () => import('@/components/static/pressKit');
const PrivacyPage = () => import('@/components/static/privacy');
const RegisterLoginReset = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerLoginReset');
const RegisterUsername = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerUsername');
const RegisterLoginReset = () => import('@/components/auth/registerLoginReset');
const RegisterUsername = () => import('@/components/auth/registerUsername');
const SubscriptionBenefitsFaq = () => import('@/components/static/subscriptionBenefitsFaq');
const TermsPage = () => import('@/components/static/terms');
+5 -1
View File
@@ -101,6 +101,7 @@ export async function appleAuth (store, params) {
id_token: params.idToken,
name: params.name,
username: params.username,
email: params.email,
},
});
@@ -109,7 +110,10 @@ export async function appleAuth (store, params) {
}
if (result.data.message && result.data.id_token) {
return { idToken: result.data.id_token };
return {
idToken: result.data.id_token,
email: result.data.email,
};
}
const user = result.data.data;
-8
View File
@@ -1,6 +1,5 @@
import axios from 'axios';
import Vue from 'vue';
import * as Analytics from '@/libs/analytics';
export async function getChat (store, payload) {
const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat?limit=400`);
@@ -17,13 +16,6 @@ export async function postChat (store, payload) {
url += `?previousMsg=${payload.previousMsg}`;
}
if (group.type === 'party') {
Analytics.updateUser({
partyID: group.id,
partySize: group.memberCount,
});
}
const response = await axios.post(url, {
message: payload.message,
});
@@ -1,7 +1,6 @@
import axios from 'axios';
import omit from 'lodash/omit';
import findIndex from 'lodash/findIndex';
import * as Analytics from '@/libs/analytics';
import { loadAsyncResource } from '@/libs/asyncResource';
export async function getPublicGuilds (store, payload) {
@@ -74,7 +73,6 @@ export async function join (store, payload) {
if (invitationI !== -1) invitations.parties.splice(invitationI, 1);
user.party._id = groupId;
Analytics.updateUser({ partyID: groupId });
// load the party members so that they get shown in the header
store.dispatch('party:getMembers');
}
@@ -18,7 +18,6 @@ import * as shops from './shops';
import * as snackbars from './snackbars';
import * as worldState from './worldState';
import * as news from './news';
import * as analytics from './analytics';
import * as faq from './faq';
import * as blockers from './blockers';
@@ -44,7 +43,6 @@ const actions = flattenAndNamespace({
snackbars,
worldState,
news,
analytics,
faq,
blockers,
});
@@ -1,26 +1,6 @@
import axios from 'axios';
import * as Analytics from '@/libs/analytics';
// export async function initQuest (store) {
// }
export async function sendAction (store, payload) { // eslint-disable-line import/prefer-default-export, max-len
// @TODO: Maybe move this to server
let partyData = {};
if (store.state.party && store.state.party.data) {
partyData = {
partyID: store.state.party.data._id,
partySize: store.state.party.data.memberCount,
};
} else {
partyData = {
partyID: store.state.user.data.party._id,
partySize: store.state.partyMembers.data.length,
};
}
Analytics.updateUser(partyData);
const response = await axios.post(`/api/v4/groups/${payload.groupId}/${payload.action}`);
// @TODO: Update user?
+8 -14
View File
@@ -3,7 +3,6 @@ import Vue from 'vue';
import compact from 'lodash/compact';
import omit from 'lodash/omit';
import { loadAsyncResource } from '@/libs/asyncResource';
import * as Analytics from '@/libs/analytics';
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
export function fetchUserTasks (store, options = {}) {
@@ -112,15 +111,6 @@ export async function create (store, createdTask) {
}
const tasksCreatedCount = getLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT);
if (!tasksCreatedCount || tasksCreatedCount < 2) {
const uuid = store.state.user.data._id;
Analytics.track({
eventName: 'task created',
eventAction: 'task created',
eventCategory: 'behavior',
hitType: 'event',
uuid,
taskType: taskRes.type,
}, { trackOnClient: true });
if (!tasksCreatedCount) {
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 1);
} else {
@@ -168,11 +158,15 @@ export async function collapseChecklist (store, task) {
}
export async function destroy (store, task) {
const list = store.state.tasks.data[`${task.type}s`];
const taskIndex = list.findIndex(t => t._id === task._id);
const type = `${task.type}s`;
const listIndex = store.state.tasks.data[type].findIndex(t => t._id === task._id);
const orderIndex = store.state.user.data.tasksOrder[type].indexOf(task._id);
if (taskIndex > -1) {
list.splice(taskIndex, 1);
if (listIndex > -1) {
store.state.tasks.data[type].splice(listIndex, 1);
}
if (orderIndex > -1) {
store.state.user.data.tasksOrder[type].splice(orderIndex, 1);
}
await axios.delete(`/api/v4/tasks/${task._id}`);
-4
View File
@@ -159,10 +159,6 @@ export default defineConfig({
target: DEV_BASE_URL,
changeOrigin: true,
},
'^/analytics': {
target: DEV_BASE_URL,
changeOrigin: true,
},
}
}
})
+3 -1
View File
@@ -162,5 +162,7 @@
"achievementCatsModalText": "Nasbíral jsi všechny kočičí mazlíčky!",
"achievementRoughRiderModalText": "Nasbíral jsi všechny základní barvy nepohodlných mazlíčků a mountů!",
"achievementRodentRulerModalText": "Nasbíral jsi všechny hlodavce!",
"achievementCatsText": "Vylíhly se všechny standardní barvy kočičích mazlíčků: gepard, lev, šavlozubý tygr a tygr!"
"achievementCatsText": "Vylíhly se všechny standardní barvy kočičích mazlíčků: gepard, lev, šavlozubý tygr a tygr!",
"achievementRodentRuler": "Vládce hlodavců",
"achievementCats": "Pasák koček"
}
+14 -3
View File
@@ -4,7 +4,7 @@
"brokenChaLink": "Nefunkční odkaz na výzvu",
"keepIt": "Ponechat",
"removeIt": "Odstranit",
"brokenChallenge": "Nefunkční odkaz na výzvu: tento úkol byl součástí výzvy, ale ta (nebo skupina, která ji vytvořila) byla odstraněna. Co chceš dělat s osiřelými úkoly?",
"brokenChallenge": "Neplatný odkaz na výzvu",
"challengeCompleted": "Výzva byla ukončena a vítězem se stal <span class=\"badge\"><%= user %></span>! Co chceš dělat s osiřelými úkoly?",
"unsubChallenge": "Nefunkční odkaz na výzvu: tento úkol byl součástí výzvy, ze které jsi se odhlásil/a. Co chceš dělat s osiřelými úkoly?",
"challenges": "Výzvy",
@@ -85,7 +85,7 @@
"summaryRequired": "Je požadováno shrnutí",
"summaryTooLong": "Shrnutí je příliš dlouhé",
"descriptionRequired": "Je požadován popis",
"locationRequired": "Je požadováno vybrat lokaci výzvy ('Přidat k')",
"locationRequired": "Je nutné vybrat umístění výzvy (Přidat do“)",
"categoiresRequired": "Musí být vybrána jedna nebo více kategorií",
"viewProgressOf": "Zobrazit pokrok",
"viewProgress": "Zobrazit pokrok",
@@ -94,5 +94,16 @@
"selectParticipant": "Zvol účastníka",
"filters": "Filtry",
"wonChallengeDesc": "Vyhrál/a jsi výzvu <%= challengeName %>! Tvá výhra je zaznamenána ve tvých úspěších.",
"yourReward": "Tvá odměna"
"yourReward": "Tvá odměna",
"brokenTaskDescription": "Tento úkol byl součástí výzvy, ale byl z ní odstraněn. Co chceš udělat?",
"brokenChallengeDescription": "Tento úkol byl součástí výzvy, ale výzva (nebo skupina) byla smazána. Co chceš udělat s osiřelými úkoly?",
"challengeCompletedDescription": "Vítězem je <%= user %>! Co chceš udělat s osiřelými úkoly?",
"messageChallengeFlagAlreadyReported": "Tuto výzvu jsi už nahlásil.",
"flaggedNotHidden": "Výzva byla nahlášena jednou, není skrytá",
"flaggedAndHidden": "Výzva byla nahlášena a je skrytá",
"resetFlagCount": "Resetovat počet nahlášení",
"deleteChallengeRefundDescription": "Pokud tuto výzvu smažeš, bude ti vrácena odměna v drahokamech a úkoly z výzvy zůstanou na nástěnkách úkolů účastníků.",
"messageChallengeFlagOfficial": "Oficiální výzvy nelze nahlásit.",
"brokenTask": "Nefunkční odkaz na výzvu",
"removeTasks": "Odstranit Úkoly"
}
+3 -1
View File
@@ -3495,5 +3495,7 @@
"backMystery202601Notes": "Dieses Zeichen gewährt dem Anwender die Kontrolle über die Elemente der Jahreszeit von Kälte und Frost. Gewährt keinen Attributbonus. Januar 2026 Abonnentengegenstand.",
"backMystery202602Text": "Fünf Schweife der Sakura",
"backMystery202602Notes": "Diese flauschigen Schweife haben die Farbe der Kirschblüte, eine Erinnerung, dass der Frühling auf dem Weg ist. Gewährt keinen Autobusbonus. Februar 2026 Abonnentengegenstand.",
"backArmoireHarpsichordText": "Cembalo"
"backArmoireHarpsichordText": "Cembalo",
"weaponSpecialSpring2026HealerText": "Schneeglöckchen Stab",
"weaponSpecialSpring2026HealerNotes": "Eine Gelegenheit für einen Neuanfang liegt direkt vor dir, und mit diesem prächtigen Stab wirst du bereit sein! Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe Frühlingsausrüstung 2026."
}
+3 -1
View File
@@ -187,5 +187,7 @@
"learnMore": "Learn More",
"translateHabitica": "Translate Habitica",
"whatToCallYou": "What should we call you?",
"acceptPrivacyTOS": "You confirm that you are at least 18 years old, and that you have read and agree to our <a href='/static/terms' target='_blank'>Terms of Service</a> and <a href='/static/privacy' target='_blank'>Privacy Policy</a>"
"acceptPrivacyTOS": "You confirm that you are at least 18 years old, and that you have read and agree to our <a href='/static/terms' target='_blank'>Terms of Service</a> and <a href='/static/privacy' target='_blank'>Privacy Policy</a>",
"emailAddress": "Email address",
"emailRequiredForSupport": "We require an email address for user support. Please enter an email address to continue creating your account."
}
+3 -1
View File
@@ -243,5 +243,7 @@
"playerReportModalBody": "You should only report a player who violates the <%= firstLinkStart %>Community Guidelines<%= linkEnd %> and/or <%= secondLinkStart %>Terms of Service<%= linkEnd %>. Submitting a false report is a violation of Habiticas Community Guidelines.",
"targetUserNotExist": "Target User: '<%= userName %>' does not exist.",
"rememberToBeKind": "Please remember to be kind, respectful, and follow the <a href='/static/community-guidelines' target='_blank'>Community Guidelines</a>.",
"confirmPurchase": "Confirm Purchase"
"confirmPurchase": "Confirm Purchase",
"avoidSPI": "Avoid SPI",
"avoidSPIDetails": "For your privacy, avoid including <%= firstLink %>sensitive personal information<%= linkClose %> (SPI) when using Habitica. Your account data, including tasks, is stored on our servers so you can access it from any device.<br><br>To learn more, review our <%= secondLink %>Privacy Policy<%= linkClose %>."
}
+1 -1
View File
@@ -4,7 +4,7 @@
"brokenChaLink": "Broken Challenge Link",
"keepIt": "Keep It",
"removeIt": "Remove It",
"brokenChallenge": "Broken Challenge Link: this task was part of a challenge, but the challenge (or group) has been deleted. What to do with the orphan tasks?",
"brokenChallenge": "Broken Challenge Link",
"challengeCompleted": "This challenge has been completed, and the winner was <span class=\"badge\"><%= user %></span>! What to do with the orphan tasks?",
"unsubChallenge": "Broken Challenge Link: this task was part of a challenge, but you have unsubscribed from the challenge. What to do with the orphan tasks?",
"challenges": "Challenges",
+4 -4
View File
@@ -114,8 +114,8 @@
"unallocated": "Unallocated Stat Points",
"autoAllocation": "Automatic Allocation",
"autoAllocationPop": "Places Points into Stats according to your preferences, when you level up.",
"evenAllocation": "Distribute Stat Points evenly",
"evenAllocationPop": "Assigns the same number of Points to each Stat.",
"evenAllocation": "Distribute evenly",
"evenAllocationPop": "Assigns the same number of points to each attribute",
"classAllocation": "Distribute based on class",
"classAllocationPop": "Assigns more points to the attributes important to your class",
"taskAllocation": "Distribute based on task activity",
@@ -123,7 +123,7 @@
"distributePoints": "Distribute Unallocated Points",
"distributePointsPop": "Assigns all unallocated Stat Points according to the selected allocation scheme.",
"warriorText": "Warriors score more and better \"critical hits\", which randomly give bonus Gold, Experience, and drop chance for scoring a task. They also deal heavy damage to boss monsters. Play a Warrior if you find motivation from unpredictable jackpot-style rewards, or want to dish out the hurt in boss Quests!",
"wizardText": "Mages learn swiftly, gaining Experience and Levels faster than other classes. They also get a great deal of Mana for using special abilities. Play a Mage if you enjoy the tactical game aspects of Habitica, or if you are strongly motivated by leveling up and unlocking advanced features!",
"wizardText": "Mages learn swiftly, gaining Experience and Levels faster than other classes. They also get a great deal of Mana for using special abilities. Play a Mage if you enjoy the tactical game aspects of Habitica, or if you are strongly motivated by levelling up and unlocking advanced features!",
"mageText": "Mages learn swiftly, gaining Experience and Levels faster than other classes. They also get a great deal of Mana for using special abilities. Play a Mage if you enjoy the tactical game aspects of Habitica, or if you are strongly motivated by levelling up and unlocking advanced features!",
"rogueText": "Rogues love to accumulate wealth, gaining more Gold than anyone else, and are adept at finding random items. Their iconic Stealth ability lets them duck the consequences of missed Dailies. Play a Rogue if you find strong motivation from Rewards and Achievements, striving for loot and badges!",
"healerText": "Healers stand impervious against harm, and extend that protection to others. Missed Dailies and bad Habits don't faze them much, and they have ways to recover Health from failure. Play a Healer if you enjoy assisting others in your Party, or if the idea of cheating Death through hard work inspires you!",
@@ -178,7 +178,7 @@
"mainHand": "Main-Hand",
"offHand": "Off-Hand",
"statPoints": "Stat Points",
"pts": "pts",
"pts": "PTS",
"chatCastSpellUser": "<%= username %> casts <%= spell %> on <%= target %>.",
"chatCastSpellParty": "<%= username %> casts <%= spell %> for the party.",
"purchasePetItemConfirm": "This purchase would exceed the number of items you need to hatch all possible <%= itemText %> pets. Are you sure?",
+2 -2
View File
@@ -134,7 +134,7 @@
"subscriptionDetail48": "Are there any changes to other subscription benefits, like Mystery Gear Sets?",
"subscriptionDetail480": "These changes only affect Mystic Hourglasses and subscriber Gems. All other benefits will remain the same.",
"subscriptionPara2": "If you have any questions not covered by the answers above, you can always contact our team at <%= mailto %>.",
"contentAnswer01": "<strong>Grand Galas are being extended</strong> to be active throughout the whole season, along with all their Class gear, Avatar Customizations, and other goodies.",
"contentAnswer01": "<strong>Grand Galas are being extended</strong> to be active throughout the whole season, along with all their Class gear, Avatar Customisations, and other goodies.",
"contentAnswer10": "Habitica has been around since 2013 (wow!) and over time weve released thousands of items players can collect. This can be overwhelming, especially for new players. We want to be sure that we showcase everything we have to offer, and that excellent items released earlier in our history arent overlooked.",
"contentAnswer11": "When new players join between Grand Galas they are often unaware of these events and miss out on the fun. We want to be sure all new players can join in on our seasonal festivities no matter when they choose to start their journeys.",
"subscriptionDetail00": "All subscribers, including those with gifted subscriptions, will receive 1 Mystic Hourglass at the start of each month they have subscriber benefits.",
@@ -192,7 +192,7 @@
"faqQuestion63": "How do unassigned tasks work?",
"webFaqAnswer63": "Unassigned tasks can be completed by any member. For example, taking out the trash. Whoever takes out the trash can complete the unassigned task and it will show as completed for everyone.",
"faqQuestion64": "How does the synchronised day reset work?",
"webFaqAnswer64": "Shared tasks will reset at the same time for everyone to keep the shared task board in sync. This time is visible on the shared task board and is determined by the Group Plan leaders day start time. Because shared tasks reset automatically, you will not get a chance to complete yesterdays uncompleted shared Dailies when you check in the next morning.\n\nShared Dailies will not do damage if they are missed, however they will degrade in colour to help visualize progress.",
"webFaqAnswer64": "Shared tasks will reset at the same time for everyone to keep the shared task board in sync. This time is visible on the shared task board and is determined by the Group Plan leaders day start time. Because shared tasks reset automatically, you will not get a chance to complete yesterdays uncompleted shared Dailies when you check in the next morning.\n\nShared Dailies will not do damage if they are missed, however they will degrade in colour to help visualise progress.",
"webFaqAnswer65": "While the mobile apps dont fully support all Group Plans functionality yet, you can still complete shared tasks from the iOS and Android apps!\n\nOn Android, you can tap your Display Name at the top of the screen when viewing your tasks to switch to your shared task board. From there you can view members, access your chat, and create, complete, or assign tasks.\n\nYou can also switch on a preference to copy shared tasks to your personal task board so you can complete all your tasks from one place.\n\nTo do this on the mobile apps:\n * Open Settings and switch on “Copy shared tasks”\n\nTo do this on Habiticas website:\n * Navigate to your Group Plan and switch on the “Copy tasks” toggle on the shared task board",
"faqQuestion66": "Whats the difference between a Group Plans shared tasks and Challenge tasks?",
"webFaqAnswer66": "Group Plan shared task boards are more dynamic than Challenges, in that they can constantly be updated and interacted with. Challenges are great if you have one set of tasks to send out to many people.\n\nGroup Plans are also a paid feature, while Challenges are available free to everyone.\n\nYou cannot assign specific tasks in Challenges, and Challenges do not have a shared day reset. In general, Challenges offer less control and direct interaction.",
+2 -2
View File
@@ -132,7 +132,7 @@
"invalidReqParams": "Invalid request parameters.",
"memberIdRequired": "\"member\" must be a valid UUID.",
"heroIdRequired": "\"heroId\" must be a valid UUID.",
"cannotFulfillReq": "Please enter a valid email address. Email admin@habitica.com if this error persists.",
"cannotFulfillReq": "This email address is already in use. You can try logging in or use a different email to register. If you need help, reach out to admin@habitica.com.",
"modelNotFound": "This model does not exist.",
"signUpWithSocial": "Continue with <%= social %>",
"loginWithSocial": "Log in with <%= social %>",
@@ -163,7 +163,7 @@
"schoolAndWork": "School and Work",
"schoolAndWorkDesc": "Whether you're preparing a report for your teacher or your boss, it's easy to keep track of your progress as you tackle your toughest tasks.",
"muchmuchMore": "And much, much more!",
"muchmuchMoreDesc": "Our fully customizable task list means that you can shape Habitica to fit your personal goals. Work on creative projects, emphasise self-care, or pursue a different dream -- it's all up to you.",
"muchmuchMoreDesc": "Our fully customisable task list means that you can shape Habitica to fit your personal goals. Work on creative projects, emphasise self-care, or pursue a different dream -- it's all up to you.",
"levelUpAnywhere": "Level Up Anywhere",
"levelUpAnywhereDesc": "Our mobile apps make it simple to keep track of your tasks on-the-go. Accomplish your goals with a single tap, no matter where you are.",
"joinMany": "Join over <%= userCountInMillions %> million people having fun while accomplishing their goals!",
+25 -12
View File
@@ -169,7 +169,7 @@
"weaponSpecialSummer2015MageNotes": "Hidden power glimmers in the jewels of this staff. Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition 2015 Summer Gear.",
"weaponSpecialSummer2015HealerText": "Wand of the Waves",
"weaponSpecialSummer2015HealerNotes": "Cures seasickness and sea sickness! Increases Intelligence by <%= int %>. Limited Edition 2015 Summer Gear.",
"weaponSpecialFall2015RogueText": "Bat-tle Ax",
"weaponSpecialFall2015RogueText": "Bat-tle Axe",
"weaponSpecialFall2015RogueNotes": "Fearsome To Do's cower before the flapping of this axe. Increases Strength by <%= str %>. Limited Edition 2015 Autumn Gear.",
"weaponSpecialFall2015WarriorText": "Wooden Plank",
"weaponSpecialFall2015WarriorNotes": "Great for elevating things in cornfields and/or smacking tasks. Increases Strength by <%= str %>. Limited Edition 2015 Autumn Gear.",
@@ -246,7 +246,7 @@
"weaponSpecialWinter2018WarriorText": "Holiday Bow Hammer",
"weaponSpecialWinter2018WarriorNotes": "The sparkly appearance of this bright weapon will dazzle your enemies as you swing it! Increases Strength by <%= str %>. Limited Edition 2017-2018 Winter Gear.",
"weaponSpecialWinter2018MageText": "Holiday Confetti",
"weaponSpecialWinter2018MageNotes": "Magic--and glitter--is in the air! Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition 2017-2018 Winter Gear.",
"weaponSpecialWinter2018MageNotes": "Magicand glitteris in the air! Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition 2017-2018 Winter Gear.",
"weaponSpecialWinter2018HealerText": "Mistletoe Wand",
"weaponSpecialWinter2018HealerNotes": "This mistletoe ball is sure to enchant and delight passersby! Increases Intelligence by <%= int %>. Limited Edition 2017-2018 Winter Gear.",
"weaponSpecialSpring2018RogueText": "Buoyant Bullrush",
@@ -326,7 +326,7 @@
"weaponArmoireBasicLongbowText": "Basic Longbow",
"weaponArmoireBasicLongbowNotes": "A serviceable hand-me-down bow. Increases Strength by <%= str %>. Enchanted Armoire: Basic Archer Set (Item 1 of 3).",
"weaponArmoireHabiticanDiplomaText": "Habitican Diploma",
"weaponArmoireHabiticanDiplomaNotes": "A certificate of significant achievement -- well done! Increases Intelligence by <%= int %>. Enchanted Armoire: Graduate Set (Item 1 of 3).",
"weaponArmoireHabiticanDiplomaNotes": "A certificate of significant achievementwell done! Increases Intelligence by <%= int %>. Enchanted Armoire: Graduate Set (Item 1 of 3).",
"weaponArmoireSandySpadeText": "Sandy Spade",
"weaponArmoireSandySpadeNotes": "A tool for digging, as well as flicking sand into the eyes of enemy monsters. Increases Strength by <%= str %>. Enchanted Armoire: Seaside Set (Item 1 of 3).",
"weaponArmoireCannonText": "Cannon",
@@ -806,7 +806,7 @@
"armorArmoireCoverallsOfBookbindingText": "Coveralls of Bookbinding",
"armorArmoireCoverallsOfBookbindingNotes": "Everything you need in a set of coveralls, including pockets for everything. A pair of goggles, loose change, a golden ring... Increases Constitution by <%= con %> and Perception by <%= per %>. Enchanted Armoire: Bookbinder Set (Item 2 of 4).",
"armorArmoireRobeOfSpadesText": "Robe of Spades",
"armorArmoireRobeOfSpadesNotes": "These luxuriant robes conceal hidden pockets for treasures or weapons--your choice! Increases Strength by <%= str %>. Enchanted Armoire: Ace of Spades Set (Item 2 of 3).",
"armorArmoireRobeOfSpadesNotes": "These luxuriant robes conceal hidden pockets for treasures or weaponsyour choice! Increases Strength by <%= str %>. Enchanted Armoire: Ace of Spades Set (Item 2 of 3).",
"armorArmoireSoftBlueSuitText": "Soft Blue Suit",
"armorArmoireSoftBlueSuitNotes": "Blue is a calming colour. So calming, some even wear this soft outfit to sleep... zZz. Increases Intelligence by <%= int %> and Perception by <%= per %>. Enchanted Armoire: Blue Loungewear Set (Item 2 of 3).",
"armorArmoireSoftGreenSuitText": "Soft Green Suit",
@@ -876,7 +876,7 @@
"headSpecialLunarWarriorHelmText": "Lunar Warrior Helm",
"headSpecialLunarWarriorHelmNotes": "The power of the moon will strengthen you in battle! Increases Strength and Intelligence by <%= attrs %> each.",
"headSpecialMammothRiderHelmText": "Mammoth Rider Helm",
"headSpecialMammothRiderHelmNotes": "Don't let its fluffiness fool you--this hat will grant you piercing powers of perception! Increases Perception by <%= per %>.",
"headSpecialMammothRiderHelmNotes": "Don't let its fluffiness fool youthis hat will grant you piercing powers of perception! Increases Perception by <%= per %>.",
"headSpecialPageHelmText": "Page Helm",
"headSpecialPageHelmNotes": "Chainmail: for the stylish AND the practical. Increases Perception by <%= per %>.",
"headSpecialRoguishRainbowMessengerHoodText": "Roguish Rainbow Messenger Hood",
@@ -960,7 +960,7 @@
"headSpecialFall2015RogueText": "Bat-tle Wings",
"headSpecialFall2015RogueNotes": "Echolocate your enemies with this powerful helm! Increases Perception by <%= per %>. Limited Edition 2015 Autumn Gear.",
"headSpecialFall2015WarriorText": "Scarecrow Hat",
"headSpecialFall2015WarriorNotes": "Everyone would want this hat--if they only had a brain. Increases Strength by <%= str %>. Limited Edition 2015 Autumn Gear.",
"headSpecialFall2015WarriorNotes": "Everyone would want this hatif they only had a brain. Increases Strength by <%= str %>. Limited Edition 2015 Autumn Gear.",
"headSpecialFall2015MageText": "Stitched Hat",
"headSpecialFall2015MageNotes": "Every stitch in this hat augments its power. Increases Perception by <%= per %>. Limited Edition 2015 Autumn Gear.",
"headSpecialFall2015HealerText": "Hat of Frog",
@@ -1435,7 +1435,7 @@
"shieldArmoireMushroomDruidShieldText": "Mushroom Druid Shield",
"shieldArmoireMushroomDruidShieldNotes": "Though made from a mushroom, there's nothing mushy about this tough shield! Increases Constitution by <%= con %> and Strength by <%= str %>. Enchanted Armoire: Mushroom Druid Set (Item 3 of 3).",
"shieldArmoireFestivalParasolText": "Festival Parasol",
"shieldArmoireFestivalParasolNotes": "This lightweight parasol will shield you from the glare--whether it's from the sun or from dark red Dailies! Increases Constitution by <%= con %>. Enchanted Armoire: Festival Attire Set (Item 2 of 3).",
"shieldArmoireFestivalParasolNotes": "This lightweight parasol will shield you from the glarewhether it's from the sun or from dark red Dailies! Increases Constitution by <%= con %>. Enchanted Armoire: Festival Attire Set (Item 2 of 3).",
"shieldArmoireVikingShieldText": "Viking Shield",
"shieldArmoireVikingShieldNotes": "This sturdy shield of wood and hide can stand up to the most daunting of foes. Increases Perception by <%= per %> and Intelligence by <%= int %>. Enchanted Armoire: Viking Set (Item 3 of 3).",
"shieldArmoireSwanFeatherFanText": "Swan Feather Fan",
@@ -2042,7 +2042,7 @@
"weaponSpecialSpring2020WarriorText": "Sharpened Wing",
"weaponSpecialSpring2020RogueNotes": "You'll strike so fast it'll look even MORE blue! Increases Strength by <%= str %>. Limited Edition 2020 Spring Gear.",
"weaponSpecialSpring2020RogueText": "Lazurite Blade",
"headAccessoryMystery202004Notes": "They twitch just a bit if the scent of flowers drifts by--use them to find a pretty garden! Confers no benefit. April 2020 Subscriber Item.",
"headAccessoryMystery202004Notes": "They twitch just a bit if the scent of flowers drifts byuse them to find a pretty garden! Confers no benefit. April 2020 Subscriber Item.",
"headAccessoryMystery202004Text": "Mighty Monarch Antennae",
"backMystery202004Notes": "Make a quick flutter to the nearest flowery meadow or migrate across the continent with these beautiful wings! Confers no benefit. April 2020 Subscriber Item.",
"backMystery202004Text": "Mighty Monarch Wings",
@@ -2092,7 +2092,7 @@
"shieldSpecialSummer2020WarriorText": "Huge Trout Scale",
"armorSpecialSummer2020WarriorNotes": "You'll be the bright fish in a dull stream, with these dazzling scales! Increases Constitution by <%= con %>. Limited Edition 2020 Summer Gear.",
"armorSpecialSummer2020WarriorText": "Rainbow Trout Tail",
"armorSpecialSummer2020RogueNotes": "A crocodile makes the perfect Rogue, waiting for the perfect moment to strike. Borrow their skills--and their explosive speed. Increases Perception by <%= per %>. Limited Edition 2020 Summer Gear.",
"armorSpecialSummer2020RogueNotes": "A crocodile makes the perfect Rogue, waiting for the perfect moment to strike. Borrow their skillsand their explosive speed. Increases Perception by <%= per %>. Limited Edition 2020 Summer Gear.",
"armorSpecialSummer2020RogueText": "Crocodile Disguise",
"weaponSpecialSummer2020HealerNotes": "As the currents wear away sharp edges, so shall your magic soften your friends' pain. Increases Intelligence by <%= int %>. Limited Edition 2020 Summer Gear.",
"weaponSpecialSummer2020HealerText": "Frosted Glass Rod",
@@ -2278,7 +2278,7 @@
"weaponSpecialFall2021RogueText": "Dripping Goo",
"weaponSpecialFall2021RogueNotes": "What on Earth did you get into? When people say Rogues have sticky fingers, this is not what they mean! Increases Strength by <%= str %>. Limited Edition 2021 Autumn Gear.",
"weaponSpecialFall2021WarriorText": "Horse Rider's Axe",
"weaponSpecialFall2021WarriorNotes": "This stylized, single-bladed axe is ideal for chopping... pumpkins! Increases Strength by <%= str %>. Limited Edition 2021 Autumn Gear.",
"weaponSpecialFall2021WarriorNotes": "This stylised, single-bladed axe is ideal for chopping... pumpkins! Increases Strength by <%= str %>. Limited Edition 2021 Autumn Gear.",
"weaponSpecialFall2021MageText": "Staff of Pure Thought",
"weaponSpecialSummer2022RogueNotes": "If you're in a pinch, don't hesitate to show these fearsome claws! Increases Strength by <%= str %>. Limited Edition 2022 Summer Gear.",
"weaponSpecialSummer2022RogueText": "Crab Claw",
@@ -2435,7 +2435,7 @@
"armorSpecialSummer2022MageNotes": "When wearing this armour, you will glide easily through your work like the manta ray glides through water. Increases Intelligence by <%= int %>. Limited Edition 2022 Summer Gear.",
"armorSpecialFall2022RogueNotes": "Whether youre swimming, sneaking, or wrestling, you will be safe in this armour. Increases Perception by <%= per %>. Limited Edition 2022 Autumn Gear.",
"armorSpecialSpring2023WarriorText": "Hummingbird Armour",
"weaponArmoireRidingBroomNotes": "Run all your most magical errands on this fine broom--or, just take it for a joyride around the neighbourhood. Whee! Increases Strength by <%= str %> and Intelligence by <%= int %>. Enchanted Armoire: Spooky Sorcery Set (Item 1 of 3)",
"weaponArmoireRidingBroomNotes": "Run all your most magical errands on this fine broomor, just take it for a joyride around the neighbourhood. Whee! Increases Strength by <%= str %> and Intelligence by <%= int %>. Enchanted Armoire: Spooky Sorcery Set (Item 1 of 3)",
"armorSpecialSummer2021WarriorText": "Finny Armour",
"armorSpecialSummer2023WarriorNotes": "Goldfish Warriors actually have excellent memories because they always keep their Dailies and To Do's organised in lists. Increases Constitution by <%= con %>. Limited Edition 2023 Summer Gear.",
"armorSpecialFall2023RogueNotes": "You were lured with the promise of a nice hot soak... Joke's on you! Increases Perception by <%= per %>. Limited Edition 2023 Autumn Gear.",
@@ -2557,5 +2557,18 @@
"weaponMystery202212Text": "Glacial Wand",
"weaponSpecialWinter2024MageNotes": "Thanks to a generous, magical narwhal that sensed your great abilities, you have been gifted a tusk that lets you sense changes happening around you. Increases Intelligence by <%= int %>. Limited Edition Winter 2023-2024 Gear.",
"weaponSpecialSpring2024RogueNotes": "Challenges that are as hard as ice can be sliced into smaller pieces. Increases Strength by <%= str %>. Limited Edition Spring 2024 Gear.",
"backMystery202602Notes": "These fluffy tails are the colour of cherry blossoms, a reminder that spring is on the way! Confers no benefit. February 2026 Subscriber Item."
"backMystery202602Notes": "These fluffy tails are the colour of cherry blossoms, a reminder that spring is on the way! Confers no benefit. February 2026 Subscriber Item.",
"armorSpecialSpring2026RogueText": "Birch Bark Armour",
"armorSpecialSpring2026WarriorText": "Frog Armour",
"armorMystery202512Text": "Cookie Champion Armour",
"shieldArmoireSoftWhitePillowNotes": "The organised warrior packs a pillow for any expedition. Protect yourself from overlooked obligations… even while you nap. Increases Intelligence and Perception by <%= attrs %> each. Enchanted Armoire: White Loungewear Set (Item 3 of 3)",
"headSpecialSummer2025MageNotes": "All will be mesmerised by the way your fins move with the currents. Increases Perception by <%= per %>. Limited Edition Summer 2025 Gear.",
"headSpecialSpring2025HealerNotes": "This flower symbolises birth, love, and new beginnings! It also provides a beautiful scent you can enjoy as you work on your tasks. Increases Intelligence by <%= int %>. Limited Edition Spring 2025 Gear.",
"armorArmoireFancyPirateSuitNotes": "Wear this fine jacket well as you organise your ships library or talk it through as a crew. Increases Constitution and Intelligence by <%= attrs %> each. Enchanted Armoire: Fancy Pirate Set (Item 1 of 3).",
"armorArmoireJewelersApronNotes": "This heavy-duty apron is just the thing to wear when you feel creative. Best of all, there are dozens of small pockets to hold everything you need. Increases Intelligence by <%= int %>. Enchanted Armoire: Jeweller Set (Item 1 of 4).",
"shieldArmoireJewelersPliersText": "Jeweller's Pliers",
"shieldArmoireJewelersPliersNotes": "They cut, twist, pinch, and more. This tool can help you create whatever you can imagine. Increases Strength by <%= str %>. Enchanted Armoire: Jeweller Set (Item 3 of 4).",
"weaponArmoireFinelyCutGemNotes": "What a find! This stunning, precision-cut gem will be the prize of your collection. And it might contain some special magic, just waiting for you to tap into it. Increases Constitution by <%= con %>. Enchanted Armoire: Jeweller Set (Item 4 of 4).",
"eyewearArmoireJewelersEyeLoupeText": "Jeweller's Eye Loupe",
"eyewearArmoireJewelersEyeLoupeNotes": "This eye loupe magnifies what youre working on so you can see absolutely every detail. Increases Perception by <%= per %>. Enchanted Armoire: Jeweller Set (Item 2 of 4)."
}
+16 -16
View File
@@ -263,24 +263,24 @@
"summer2024SeaAnemoneMageSet": "Sea Anemone Set (Mage)",
"summer2024SeaSnailHealerSet": "Sea Snail Set (Healer)",
"summer2024NudibranchRogueSet": "Nudibranch Set (Rogue)",
"winter2025AuroraMageSet": "Aurora Mage Set",
"winter2025SnowRogueSet": "Snow Rogue Set",
"spring2025SunshineWarriorSet": "Sunshine Warrior Set",
"spring2025CrystalPointRogueSet": "Crystal Point Rogue Set",
"spring2025PlumeriaHealerSet": "Plumeria Healer Set",
"spring2025MantisMageSet": "Mantis Mage Set",
"winter2025AuroraMageSet": "Aurora Set (Mage)",
"winter2025SnowRogueSet": "Snow Set (Rogue)",
"spring2025SunshineWarriorSet": "Sunshine Set (Warrior)",
"spring2025CrystalPointRogueSet": "Crystal Point Set (Rogue)",
"spring2025PlumeriaHealerSet": "Plumeria Set (Healer)",
"spring2025MantisMageSet": "Mantis Set (Mage)",
"fall2024FieryImpWarriorSet": "Fiery Imp Set (Warrior)",
"fall2024UnderworldSorcerorMageSet": "Underworld Sorceror Set (Mage)",
"fall2024SpaceInvaderHealerSet": "Space Invader Set (Healer)",
"fall2024BlackCatRogueSet": "Black Cat Set (Rogue)",
"winter2025MooseWarriorSet": "Moose Warrior Set",
"winter2025StringLightsHealerSet": "String Lights Healer Set",
"fall2025SasquatchWarriorSet": "Sasquatch Warrior Set",
"fall2025SkeletonRogueSet": "Skeleton Rogue Set",
"fall2025KoboldHealerSet": "Kobold Healer Set",
"fall2025MaskedGhostMageSet": "Masked Ghost Mage Set",
"summer2025ScallopWarriorSet": "Scallop Warrior Set",
"summer2025SquidRogueSet": "Squid Rogue Set",
"summer2025SeaAngelHealerSet": "Sea Angel Healer Set",
"summer2025FairyWrasseMageSet": "Fairy Wrasse Mage Set"
"winter2025MooseWarriorSet": "Moose Set (Warrior)",
"winter2025StringLightsHealerSet": "String Lights Set (Healer)",
"fall2025SasquatchWarriorSet": "Sasquatch Set (Warrior)",
"fall2025SkeletonRogueSet": "Skeleton Set (Rogue)",
"fall2025KoboldHealerSet": "Kobold Set (Healer)",
"fall2025MaskedGhostMageSet": "Masked Ghost Set (Mage)",
"summer2025ScallopWarriorSet": "Scallop Set (Warrior)",
"summer2025SquidRogueSet": "Squid Set (Rogue)",
"summer2025SeaAngelHealerSet": "Sea Angel Set (Healer)",
"summer2025FairyWrasseMageSet": "Fairy Wrasse Set (Mage)"
}
+28 -28
View File
@@ -189,7 +189,7 @@
"questTRexUndeadBoss": "Skeletal Tyrannosaur",
"questTRexUndeadRageTitle": "Skeleton Healing",
"questTRexUndeadRageDescription": "This bar fills when you don't complete your Dailies. When it is full, the Skeletal Tyrannosaur will heal 30% of its remaining health!",
"questTRexUndeadRageEffect": "`Skeletal Tyrannosaur uses SKELETON HEALING!`\n\nThe monster lets forth an unearthly roar, and some of its damaged bones knit back together!",
"questTRexUndeadRageEffect": "Skeletal Tyrannosaur uses SKELETON HEALING!\n\nThe monster lets forth an unearthly roar, and some of its damaged bones knit back together!",
"questTRexDropTRexEgg": "Tyrannosaur (Egg)",
"questTRexUnlockText": "Unlocks Tyrannosaur Eggs for purchase in the Market",
"questRockText": "Escape the Cave Creature",
@@ -241,7 +241,7 @@
"questDilatoryDistress2Boss": "Water Skull Swarm",
"questDilatoryDistress2RageTitle": "Swarm Respawn",
"questDilatoryDistress2RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Water Skull Swarm will heal 30% of its remaining health!",
"questDilatoryDistress2RageEffect": "`Water Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls pour forth from the crevasse, bolstering the swarm!",
"questDilatoryDistress2RageEffect": "Water Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls pour forth from the crevasse, bolstering the swarm!",
"questDilatoryDistress2DropSkeletonPotion": "Skeleton Hatching Potion",
"questDilatoryDistress2DropCottonCandyBluePotion": "Candyfloss Blue Hatching Potion",
"questDilatoryDistress2DropHeadgear": "Fire Coral Circlet (Headgear)",
@@ -253,7 +253,7 @@
"questDilatoryDistress3DropWeapon": "Trident of Crashing Tides (Weapon)",
"questDilatoryDistress3DropShield": "Moonpearl Shield (Off-Hand Item)",
"questCheetahText": "Such a Cheetah",
"questCheetahNotes": "As you hike across the Sloensteadi Savannah with your friends @PainterProphet, @tivaquinn, @Unruly Hyena, and @Crawford, you're startled to see a Cheetah screeching past with a new Habitican clamped in its jaws. Under the Cheetah's scorching paws, tasks burn away as though complete -- before anyone has the chance to actually finish them! The Habitican sees you and yells, \"Please help me! This Cheetah is making me level too quickly, but I'm not getting anything done. I want to slow down and enjoy the game. Make it stop!\" You fondly remember your own fledgling days, and know that you have to help the newbie by stopping the Cheetah!",
"questCheetahNotes": "As you hike across the Sloensteadi Savannah with your friends @PainterProphet, @tivaquinn, @Unruly Hyena, and @Crawford, you're startled to see a Cheetah screeching past with a new Habitican clamped in its jaws. Under the Cheetah's scorching paws, tasks burn away as though completebefore anyone has the chance to actually finish them! The Habitican sees you and yells, \"Please help me! This Cheetah is making me level too quickly, but I'm not getting anything done. I want to slow down and enjoy the game. Make it stop!\" You fondly remember your own fledgling days, and know that you have to help the newbie by stopping the Cheetah!",
"questCheetahCompletion": "The new Habitican is breathing heavily after the wild ride, but thanks you and your friends for your help. \"I'm glad that Cheetah won't be able to grab anyone else. It did leave some Cheetah eggs for us, so maybe we can raise them into more trustworthy pets!\"",
"questCheetahBoss": "Cheetah",
"questCheetahDropCheetahEgg": "Cheetah (Egg)",
@@ -277,7 +277,7 @@
"questBurnoutBossRageSeasonalShop": "`Burnout uses EXHAUST STRIKE!`\n\nAhh!!! Our incomplete Dailies have fed the flames of Burnout, and now it has enough energy to strike again! It lets loose a gout of spectral flame that sears the Seasonal Shop. You're horrified to see that the cheery Seasonal Sorceress has been transformed into a drooping Exhaust Spirit.\n\nWe have to rescue our NPCs! Hurry, Habiticans, complete your tasks and defeat Burnout before it strikes for a third time!",
"questBurnoutBossRageTavern": "`Burnout uses EXHAUST STRIKE!`\n\nMany Habiticans have been hiding from Burnout in the Tavern, but no longer! With a screeching howl, Burnout rakes the Tavern with its white-hot hands. As the Tavern patrons flee, Daniel is caught in Burnout's grip, and transforms into an Exhaust Spirit right in front of you!\n\nThis hot-headed horror has gone on for too long. Don't give up... we're so close to vanquishing Burnout for once and for all!",
"questFrogText": "Swamp of the Clutter Frog",
"questFrogNotes": "As you and your friends are slogging through the Swamps of Stagnation, @starsystemic points at a large sign. \"Stay on the path -- if you can.\"<br><br>\"Surely that isn't hard!\" @RosemonkeyCT says. \"It's broad and clear.\"<br><br>But as you continue, you notice that path is gradually overtaken by the muck of the swamp, laced with bits of strange blue debris and clutter, until it's impossible to proceed.<br><br>As you look around, wondering how it got this messy, @Jon Arjinborn shouts, \"Look out!\" An angry frog leaps from the sludge, clad in dirty laundry and lit by blue fire. You will have to overcome this poisonous Clutter Frog to progress!",
"questFrogNotes": "As you and your friends are slogging through the Swamps of Stagnation, @starsystemic points at a large sign. \"Stay on the pathif you can.\"<br><br>\"Surely that isn't hard!\" @RosemonkeyCT says. \"It's broad and clear.\"<br><br>But as you continue, you notice that path is gradually overtaken by the muck of the swamp, laced with bits of strange blue debris and clutter, until it's impossible to proceed.<br><br>As you look around, wondering how it got this messy, @Jon Arjinborn shouts, \"Look out!\" An angry frog leaps from the sludge, clad in dirty laundry and lit by blue fire. You will have to overcome this poisonous Clutter Frog to progress!",
"questFrogCompletion": "The frog cowers back into the muck, defeated. As it slinks away, the blue slime fades, leaving the way ahead clear.<br><br>Sitting in the middle of the path are three pristine eggs. \"You can even see the tiny tadpoles through the clear casing!\" @Breadstrings says. \"Here, you should take them.\"",
"questFrogBoss": "Clutter Frog",
"questFrogDropFrogEgg": "Frog (Egg)",
@@ -313,7 +313,7 @@
"questSnailDropSnailEgg": "Snail (Egg)",
"questSnailUnlockText": "Unlocks Snail Eggs for purchase in the Market",
"questBewilderText": "The Be-Wilder",
"questBewilderNotes": "The party begins like any other.<br><br>The appetisers are excellent, the music is swinging, and even the dancing elephants have become routine. Habiticans laugh and frolic amid the overflowing floral centrepieces, happy to have a distraction from their least-favourite tasks, and the April Fool whirls among them, eagerly providing an amusing trick here and a witty twist there.<br><br>As the Mistiflying clock tower strikes midnight, the April Fool leaps onto the stage to give a speech.<br><br>“Friends! Enemies! Tolerant acquaintances! Lend me your ears.” The crowd chuckles as animal ears sprout from their heads, and they pose with their new accessories.<br><br>“As you know,” the Fool continues, “my confusing illusions usually only last a single day. But Im pleased to announce that Ive discovered a shortcut that will guarantee us non-stop fun, without having to deal with the pesky weight of our responsibilities. Charming Habiticans, meet my magical new friend... the Be-Wilder!”<br><br>Lemoness pales suddenly, dropping her hors d'oeuvres. “Wait! Dont trust--”<br><br>But suddenly mists are pouring into the room, glittering and thick, and they swirl around the April Fool, coalescing into cloudy feathers and a stretching neck. The crowd is speechless as a monstrous bird unfolds before them, its wings shimmering with illusions. It lets out a horrible screeching laugh.<br><br>“Oh, it has been ages since a Habitican has been foolish enough to summon me! How wonderful it feels, to have a tangible form at last.”<br><br>Buzzing in terror, the magic bees of Mistiflying flee the floating city, which sags from the sky. One by one, the brilliant spring flowers wither up and wisp away.<br><br>“My dearest friends, why so alarmed?” crows the Be-Wilder, beating its wings. “Theres no need to toil for your rewards any more. Ill just give you all the things that you desire!”<br><br>A rain of coins pours from the sky, hammering into the ground with brutal force, and the crowd screams and flees for cover. “Is this a joke?” Baconsaur shouts, as the gold smashes through windows and shatters roof shingles.<br><br>PainterProphet ducks as lightning bolts crackle overhead, and fog blots out the sun. “No! This time, I dont think it is!”<br><br>Quickly, Habiticans, dont let this World Boss distract us from our goals! Stay focused on the tasks that you need to complete so we can rescue Mistiflying -- and hopefully, ourselves.",
"questBewilderNotes": "The party begins like any other.<br><br>The appetisers are excellent, the music is swinging, and even the dancing elephants have become routine. Habiticans laugh and frolic amid the overflowing floral centrepieces, happy to have a distraction from their least-favourite tasks, and the April Fool whirls among them, eagerly providing an amusing trick here and a witty twist there.<br><br>As the Mistiflying clock tower strikes midnight, the April Fool leaps onto the stage to give a speech.<br><br>“Friends! Enemies! Tolerant acquaintances! Lend me your ears.” The crowd chuckles as animal ears sprout from their heads, and they pose with their new accessories.<br><br>“As you know,” the Fool continues, “my confusing illusions usually only last a single day. But Im pleased to announce that Ive discovered a shortcut that will guarantee us non-stop fun, without having to deal with the pesky weight of our responsibilities. Charming Habiticans, meet my magical new friend... the Be-Wilder!”<br><br>Lemoness pales suddenly, dropping her hors d'oeuvres. “Wait! Dont trust”<br><br>But suddenly mists are pouring into the room, glittering and thick, and they swirl around the April Fool, coalescing into cloudy feathers and a stretching neck. The crowd is speechless as a monstrous bird unfolds before them, its wings shimmering with illusions. It lets out a horrible screeching laugh.<br><br>“Oh, it has been ages since a Habitican has been foolish enough to summon me! How wonderful it feels, to have a tangible form at last.”<br><br>Buzzing in terror, the magic bees of Mistiflying flee the floating city, which sags from the sky. One by one, the brilliant spring flowers wither up and wisp away.<br><br>“My dearest friends, why so alarmed?” crows the Be-Wilder, beating its wings. “Theres no need to toil for your rewards any more. Ill just give you all the things that you desire!”<br><br>A rain of coins pours from the sky, hammering into the ground with brutal force, and the crowd screams and flees for cover. “Is this a joke?” Baconsaur shouts, as the gold smashes through windows and shatters roof shingles.<br><br>PainterProphet ducks as lightning bolts crackle overhead, and fog blots out the sun. “No! This time, I dont think it is!”<br><br>Quickly, Habiticans, dont let this World Boss distract us from our goals! Stay focused on the tasks that you need to complete so we can rescue Mistiflyingand hopefully, ourselves.",
"questBewilderCompletion": "<strong>The Be-Wilder is DEFEATED!</strong><br><br>We've done it! The Be-Wilder lets out a ululating cry as it twists in the air, shedding feathers like falling rain. Slowly, gradually, it coils into a cloud of sparkling mist. As the newly-revealed sun pierces the fog, it burns away, revealing the coughing, mercifully human forms of Bailey, Matt, Alex.... and the April Fool himself.<br><br><strong>Mistiflying is saved!</strong><br><br>The April Fool has enough shame to look a bit sheepish. “Oh, hm,” he says. “Perhaps I got a little…. carried away.”<br><br>The crowd mutters. Sodden flowers wash up on pavements. Somewhere in the distance, a roof collapses with a spectacular splash.<br><br>“Er, yes,” the April Fool says. “That is. What I meant to say was, Im dreadfully sorry.” He heaves a sigh. “I suppose it cant all be fun and games, after all. It might not hurt to focus occasionally. Maybe Ill get a head start on next years pranking.”<br><br>Redphoenix coughs meaningfully.<br><br>“I mean, get a head start on this years spring cleaning!” the April Fool says. “Nothing to fear, Ill have Habit City in spit-shape soon. Luckily nobody is better than I at dual-wielding mops.”<br><br>Encouraged, the marching band starts up.<br><br>It isnt long before all is back to normal in Habit City. Plus, now that the Be-Wilder has evaporated, the magical bees of Mistiflying bustle back to work, and soon the flowers are blooming and the city is floating once more.<br><br>As Habiticans cuddle the magical fuzzy bees, the April Fools eyes light up. “Oho, Ive had a thought! Why dont you all keep some of these fuzzy Bee Pets and Mounts? Its a gift that perfectly symbolises the balance between hard work and sweet rewards, if Im going to get all boring and allegorical on you.” He winks. “Besides, they dont have stingers! Fools honour.”",
"questBewilderCompletionChat": "`The Be-Wilder is DEFEATED!`\n\nWe've done it! The Be-Wilder lets out a ululating cry as it twists in the air, shedding feathers like falling rain. Slowly, gradually, it coils into a cloud of sparkling mist. As the newly-revealed sun pierces the fog, it burns away, revealing the coughing, mercifully human forms of Bailey, Matt, Alex.... and the April Fool himself.\n\n`Mistiflying is saved!`\n\nThe April Fool has enough shame to look a bit sheepish. “Oh, hm,” he says. “Perhaps I got a little…. carried away.”\n\nThe crowd mutters. Sodden flowers wash up on pavements. Somewhere in the distance, a roof collapses with a spectacular splash.\n\n“Er, yes,” the April Fool says. “That is. What I meant to say was, Im dreadfully sorry.” He heaves a sigh. “I suppose it cant all be fun and games, after all. It might not hurt to focus occasionally. Maybe Ill get a head start on next years pranking.”\n\nRedphoenix coughs meaningfully.\n\n“I mean, get a head start on this years spring cleaning!” the April Fool says. “Nothing to fear, Ill have Habit City in spit-shape soon. Luckily nobody is better than I at dual-wielding mops.”\n\nEncouraged, the marching band starts up.\n\nIt isnt long before all is back to normal in Habit City. Plus, now that the Be-Wilder has evaporated, the magical bees of Mistiflying bustle back to work, and soon the flowers are blooming and the city is floating once more.\n\nAs Habiticans cuddle the magical fuzzy bees, the April Fools eyes light up. “Oho, Ive had a thought! Why dont you all keep some of these fuzzy Bee Pets and Mounts? Its a gift that perfectly symbolises the balance between hard work and sweet rewards, if Im going to get all boring and allegorical on you.” He winks. “Besides, they dont have stingers! Fools honour.”",
"questBewilderBossRageTitle": "Beguilement Strike",
@@ -343,7 +343,7 @@
"questAxolotlUnlockText": "Unlocks Axolotl Eggs for purchase in the Market",
"questAxolotlRageTitle": "Axolotl Regeneration",
"questAxolotlRageDescription": "This bar fills when you don't complete your Dailies. When it is full, the Magical Axolotl will heal 30% of its remaining health!",
"questAxolotlRageEffect": "`Magical Axolotl uses AXOLOTL REGENERATION!`\n\n`A curtain of colourful bubbles obscures the monster for a moment, and when it clears, some of its wounds have vanished!`",
"questAxolotlRageEffect": "Magical Axolotl uses AXOLOTL REGENERATION!\n\nA curtain of colourful bubbles obscures the monster for a moment, and when it clears, some of its wounds have vanished!",
"questTurtleText": "Guide the Turtle",
"questTurtleNotes": "Help! This giant sea turtle cannot find her way to her nesting beach. She returns there every year to lay her eggs, but this year Inkomplete Bay is filled with toxic Task Flotsam made of red Dailies and unchecked To Do's. \"She's thrashing in a panic!\" @JessicaChase says.<br><br>@UncommonCriminal nods. \"It's because her guiding senses are fogged and confused.\"<br><br>@Scarabsi grabs your arm. \"Can you help clear the Task Flotsam blocking her path? It may be hazardous, but we have to help her!\"",
"questTurtleCompletion": "Your valiant work has cleared the waters for our sea turtle to find her beach. You, @Bambin, and @JaizakAripaik watch as she buries her brood of eggs deep in the sand so they can grow and hatch into hundreds of little sea turtles. Ever the lady, she gives you three eggs each, asking that you feed and nurture them so one day they become big sea turtles themselves.",
@@ -357,7 +357,7 @@
"questArmadilloDropArmadilloEgg": "Armadillo (Egg)",
"questArmadilloUnlockText": "Unlocks Armadillo Eggs for purchase in the Market",
"questCowText": "The Mootant Cow",
"questCowNotes": "Its been a long, hot day at Sparring Farms, and there is nothing more you want than a long sip of water and some sleep. You're standing there daydreaming when @Soloana suddenly screams, \"Everyone run! The prize cow has mootated!\"<br><br>@eevachu gulps. \"It must be our bad habits that infected it.\"<br><br>\"Quick!\" @Feralem Tau says. \"Lets do something before the udder cows mootate, too.\"<br><br>Youve herd enough. No more daydreaming -- it's time to get those bad habits under control!",
"questCowNotes": "Its been a long, hot day at Sparring Farms, and there is nothing more you want than a long sip of water and some sleep. You're standing there daydreaming when @Soloana suddenly screams, \"Everyone run! The prize cow has mootated!\"<br><br>@eevachu gulps. \"It must be our bad habits that infected it.\"<br><br>\"Quick!\" @Feralem Tau says. \"Lets do something before the udder cows mootate, too.\"<br><br>Youve herd enough. No more daydreamingit's time to get those bad habits under control!",
"questCowCompletion": "You milk your good habits for all they are worth until the cow reverts to its original form. The cow looks over at you with her pretty brown eyes and nudges over three eggs.<br><br>@fuzzytrees laughs and hands you the eggs, \"Maybe it still is mootated if there are baby cows in these eggs. But I trust you to stick to your good habits when you raise them!\"",
"questCowBoss": "Mootant Cow",
"questCowDropCowEgg": "Cow (Egg)",
@@ -375,7 +375,7 @@
"questTaskwoodsTerror1Boss": "Fire Skull Swarm",
"questTaskwoodsTerror1RageTitle": "Swarm Respawn",
"questTaskwoodsTerror1RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Fire Skull Swarm will heal 30% of its remaining health!",
"questTaskwoodsTerror1RageEffect": "`Fire Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls swirl around you in a gout of flame!",
"questTaskwoodsTerror1RageEffect": "Fire Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls swirl around you in a gout of flame!",
"questTaskwoodsTerror1DropSkeletonPotion": "Skeleton Hatching Potion",
"questTaskwoodsTerror1DropRedPotion": "Red Hatching Potion",
"questTaskwoodsTerror1DropHeadgear": "Pyromancer's Turban (Headgear)",
@@ -387,7 +387,7 @@
"questTaskwoodsTerror2CollectDryads": "Dryads",
"questTaskwoodsTerror2DropArmor": "Pyromancer's Robes (Armour)",
"questTaskwoodsTerror3Text": "Terror in the Taskwoods, Part 3: Jacko of the Lantern",
"questTaskwoodsTerror3Notes": "Ready for battle, your group marches to the heart of the forest, where the renegade spirit is trying to destroy an ancient apple tree surrounded by fruitful berry bushes. His pumpkin-like head radiates a terrible light wherever it turns, and in his left hand he holds a long rod, with a lantern hanging from its tip. Instead of fire or flame, however, the lantern contains a dark crystal that chills you to the very bone.<br><br>The Joyful Reaper raises a bony hand to her mouth. \"That's -- that's Jacko, the Lantern Spirit! But he's a helpful harvest ghost who guides our farmers. What could possibly drive the dear soul to act this way?\"<br><br>\"I don't know,\" says @bridgetteempress. \"But it looks like that 'dear soul' is about to attack us!\"",
"questTaskwoodsTerror3Notes": "Ready for battle, your group marches to the heart of the forest, where the renegade spirit is trying to destroy an ancient apple tree surrounded by fruitful berry bushes. His pumpkin-like head radiates a terrible light wherever it turns, and in his left hand he holds a long rod, with a lantern hanging from its tip. Instead of fire or flame, however, the lantern contains a dark crystal that chills you to the very bone.<br><br>The Joyful Reaper raises a bony hand to her mouth. \"That'sthat's Jacko, the Lantern Spirit! But he's a helpful harvest ghost who guides our farmers. What could possibly drive the dear soul to act this way?\"<br><br>\"I don't know,\" says @bridgetteempress. \"But it looks like that 'dear soul' is about to attack us!\"",
"questTaskwoodsTerror3Completion": "After a long battle, you manage to land a well-aimed blow at the lantern that Jacko carries, and the crystal within shatters. Jacko suddenly snaps back to his senses and bursts into glowing tears. \"Oh, my beautiful forest! What have I done?!\" he wails. His tears extinguish the remaining fires, and the apple tree and wild berries are saved.<br><br>After you help him relax, he explains, \"I met this charming lady named Tzina, and she gave me this glowing crystal as a gift. At her urging, I put it in my lantern... but that's the last thing I recall.\" He turns to you with a golden smile. \"Perhaps you should take it for safekeeping while I help the wild orchards to regrow.\"",
"questTaskwoodsTerror3Boss": "Jacko of the Lantern",
"questTaskwoodsTerror3DropStrawberry": "Strawberry (Food)",
@@ -425,24 +425,24 @@
"questSlothDropSlothEgg": "Sloth (Egg)",
"questSlothUnlockText": "Unlocks Sloth Eggs for purchase in the Market",
"questTriceratopsText": "The Trampling Triceratops",
"questTriceratopsNotes": "The snow-capped Stoïkalm Volcanoes are always bustling with hikers and sight-seers. One tourist, @plumilla, calls over a crowd. \"Look! I enchanted the ground to glow so that we can play field games on it for our outdoor activity Dailies!\" Sure enough, the ground is swirling with glowing red patterns. Even some of the prehistoric pets from the area come over to play.<br><br>Suddenly, there's a loud snap -- a curious Triceratops has stepped on @plumilla's wand! It's engulfed in a burst of magic energy, and the ground starts shaking and growing hot. The Triceratops' eyes shine red, and it roars and begins to stampede!<br><br>\"That's not good,\" calls @McCoyly, pointing in the distance. Each magic-fueled stomp is causing the volcanoes to erupt, and the glowing ground is turning to lava beneath the dinosaur's feet! Quickly, you must hold off the Trampling Triceratops until someone can reverse the spell!",
"questTriceratopsNotes": "The snow-capped Stoïkalm Volcanoes are always bustling with hikers and sight-seers. One tourist, @plumilla, calls over a crowd. \"Look! I enchanted the ground to glow so that we can play field games on it for our outdoor activity Dailies!\" Sure enough, the ground is swirling with glowing red patterns. Even some of the prehistoric pets from the area come over to play.<br><br>Suddenly, there's a loud snapa curious Triceratops has stepped on @plumilla's wand! It's engulfed in a burst of magic energy, and the ground starts shaking and growing hot. The Triceratops' eyes shine red, and it roars and begins to stampede!<br><br>\"That's not good,\" calls @McCoyly, pointing in the distance. Each magic-fuelled stomp is causing the volcanoes to erupt, and the glowing ground is turning to lava beneath the dinosaur's feet! Quickly, you must hold off the Trampling Triceratops until someone can reverse the spell!",
"questTriceratopsCompletion": "With quick thinking, you herd the creature towards the soothing Stoïkalm Steppes so that @*~Seraphina~* and @PainterProphet can reverse the lava spell without distraction. The calming aura of the Steppes takes effect, and the Triceratops curls up as the volcanoes go dormant once more. @PainterProphet passes you some eggs that were rescued from the lava. \"Without you, we wouldn't have been able to concentrate to stop the eruptions. Give these pets a good home.\"",
"questTriceratopsBoss": "Trampling Triceratops",
"questTriceratopsDropTriceratopsEgg": "Triceratops (Egg)",
"questTriceratopsUnlockText": "Unlocks Triceratops Eggs for purchase in the Market",
"questGroupStoikalmCalamity": "Stoïkalm Calamity",
"questStoikalmCalamity1Text": "Stoïkalm Calamity, Part 1: Earthen Enemies",
"questStoikalmCalamity1Notes": "A terse missive arrives from @Kiwibot, and the frost-crusted scroll chills your heart as well as your fingertips. \"Visiting Stoïkalm Steppes -- monsters bursting from earth -- send help!\" You gather your party and ride north, but as soon as you venture down from the mountains, the snow beneath your feet explodes and gruesomely grinning skulls surround you!<br><br>Suddenly, a spear sails past, burying itself in a skull that was burrowing through the snow in an attempt to catch you unawares. A tall woman in finely-crafted armour gallops into the fray on the back of a mastodon, her long braid swinging as she yanks the spear unceremoniously from the crushed beast. It's time to fight off these foes with the help of Lady Glaciate, the leader of the Mammoth Riders!",
"questStoikalmCalamity1Notes": "A terse missive arrives from @Kiwibot, and the frost-crusted scroll chills your heart as well as your fingertips. \"Visiting Stoïkalm Steppesmonsters bursting from earthsend help!\" You gather your party and ride north, but as soon as you venture down from the mountains, the snow beneath your feet explodes and gruesomely grinning skulls surround you!<br><br>Suddenly, a spear sails past, burying itself in a skull that was burrowing through the snow in an attempt to catch you unawares. A tall woman in finely-crafted armour gallops into the fray on the back of a mastodon, her long braid swinging as she yanks the spear unceremoniously from the crushed beast. It's time to fight off these foes with the help of Lady Glaciate, the leader of the Mammoth Riders!",
"questStoikalmCalamity1Completion": "As you deliver a final blow to the skulls, they dissipate in a puff of magic. \"The dratted swarm may be gone,\" Lady Glaciate says, \"but we have bigger problems. Follow me.\" She tosses you a cloak to protect you from the chill air, and you ride off after her.",
"questStoikalmCalamity1Boss": "Earth Skull Swarm",
"questStoikalmCalamity1RageTitle": "Swarm Respawn",
"questStoikalmCalamity1RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Earth Skull Swarm will heal 30% of its remaining health!",
"questStoikalmCalamity1RageEffect": "`Earth Skull Swarm uses SWARM RESPAWN!`\n\nMore skulls break free from the ground, their teeth chattering in the cold!",
"questStoikalmCalamity1RageEffect": "Earth Skull Swarm uses SWARM RESPAWN!\n\nMore skulls break free from the ground, their teeth chattering in the cold!",
"questStoikalmCalamity1DropSkeletonPotion": "Skeleton Hatching Potion",
"questStoikalmCalamity1DropDesertPotion": "Desert Hatching Potion",
"questStoikalmCalamity1DropArmor": "Mammoth Rider Armour",
"questStoikalmCalamity2Text": "Stoïkalm Calamity, Part 2: Seek the Icicle Caverns",
"questStoikalmCalamity2Notes": "The stately hall of the Mammoth Riders is an austere masterpiece of architecture, but it is also entirely empty. There's no furniture, the weapons are missing, and even the columns were picked clean of their inlays.<br><br>\"Those skulls scoured the place,\" Lady Glaciate says, and there is a blizzard brewing in her tone. \"Humiliating. Not a soul is to mention this to the April Fool, or I will never hear the end of it.\"<br><br>\"How mysterious!\" says @Beffymaroo. \"But where did they--\"<br><br>\"The icicle drake caverns.\" Lady Glaciate gestures at shining coins spilled in the snow outside. \"Sloppy.\"<br><br>\"But aren't icicle drakes honorable creatures with their own treasure hoards?\" @Beffymaroo asks. \"Why would they possibly--\"<br><br>\"Mind control,\" says Lady Glaciate, utterly unfazed. \"Or something equally melodramatic and inconvenient.\" She begins to stride from the hall. \"Why are you just standing there?\"<br><br>Quickly, go follow the trail of Icicle Coins!",
"questStoikalmCalamity2Notes": "The stately hall of the Mammoth Riders is an austere masterpiece of architecture, but it is also entirely empty. There's no furniture, the weapons are missing, and even the columns were picked clean of their inlays.<br><br>\"Those skulls scoured the place,\" Lady Glaciate says, and there is a blizzard brewing in her tone. \"Humiliating. Not a soul is to mention this to the April Fool, or I will never hear the end of it.\"<br><br>\"How mysterious!\" says @Beffymaroo. \"But where did they\"<br><br>\"The icicle drake caverns.\" Lady Glaciate gestures at shining coins spilled in the snow outside. \"Sloppy.\"<br><br>\"But aren't icicle drakes honourable creatures with their own treasure hoards?\" @Beffymaroo asks. \"Why would they possibly\"<br><br>\"Mind control,\" says Lady Glaciate, utterly unfazed. \"Or something equally melodramatic and inconvenient.\" She begins to stride from the hall. \"Why are you just standing there?\"<br><br>Quickly, go follow the trail of Icicle Coins!",
"questStoikalmCalamity2Completion": "The Icicle Coins lead you straight to the buried entrance of a cleverly hidden cavern. Though the weather outside is calm and lovely, with the sunlight sparkling across the expanse of snow, there is a howling within like a fierce winter wind. Lady Glaciate grimaces and hands you a Mammoth Rider helm. \"Wear this,\" she says. \"You'll need it.\"",
"questStoikalmCalamity2CollectIcicleCoins": "Icicle Coins",
"questStoikalmCalamity2DropHeadgear": "Mammoth Rider Helm (Headgear)",
@@ -454,19 +454,19 @@
"questStoikalmCalamity3DropShield": "Mammoth Rider's Horn (Off-Hand Item)",
"questStoikalmCalamity3DropWeapon": "Mammoth Rider Spear (Weapon)",
"questGuineaPigText": "The Guinea Pig Gang",
"questGuineaPigNotes": "You're casually strolling through Habit City's famous Market when @Pandah waves you down. \"Hey, check these out!\" They're holding up a brown and beige egg you don't recognise.<br><br>Alexander the Merchant frowns at it. \"I don't remember putting that out. I wonder where it came--\" A small paw cuts him off.<br><br>\"Guinea all your gold, merchant!\" squeaks a tiny voice brimming with evil.<br><br>\"Oh no, the egg was a distraction!\" @mewrose exclaims. \"It's the gritty, greedy Guinea Pig Gang! They never do their Dailies, so they constantly steal gold to buy health potions.\"<br><br>\"Robbing the Market?\" says @emmavig. \"Not on our watch!\" Without further prompting, you leap to Alexander's aid.",
"questGuineaPigCompletion": "\"We submit!\" The Guinea Pig Gang Boss waves his paws at you, fluffy head hanging in shame. From underneath his hat falls a list, and @snazzyorange quickly swipes it for evidence. \"Wait a minute,\" you say. \"It's no wonder you've been getting hurt! You've got way too many Dailies. You don't need health potions -- you just need help organising.\"<br><br>\"Really?\" squeaks the Guinea Pig Gang Boss. \"We've robbed so many people because of this! Please take our eggs as an apology for our crooked ways.\"",
"questGuineaPigNotes": "You're casually strolling through Habit City's famous Market when @Pandah waves you down. \"Hey, check these out!\" They're holding up a brown and beige egg you don't recognise.<br><br>Alexander the Merchant frowns at it. \"I don't remember putting that out. I wonder where it came\" A small paw cuts him off.<br><br>\"Guinea all your gold, merchant!\" squeaks a tiny voice brimming with evil.<br><br>\"Oh no, the egg was a distraction!\" @mewrose exclaims. \"It's the gritty, greedy Guinea Pig Gang! They never do their Dailies, so they constantly steal gold to buy health potions.\"<br><br>\"Robbing the Market?\" says @emmavig. \"Not on our watch!\" Without further prompting, you leap to Alexander's aid.",
"questGuineaPigCompletion": "\"We submit!\" The Guinea Pig Gang Boss waves his paws at you, fluffy head hanging in shame. From underneath his hat falls a list, and @snazzyorange quickly swipes it for evidence. \"Wait a minute,\" you say. \"It's no wonder you've been getting hurt! You've got way too many Dailies. You don't need health potionsyou just need help organising.\"<br><br>\"Really?\" squeaks the Guinea Pig Gang Boss. \"We've robbed so many people because of this! Please take our eggs as an apology for our crooked ways.\"",
"questGuineaPigBoss": "Guinea Pig Gang",
"questGuineaPigDropGuineaPigEgg": "Guinea Pig (Egg)",
"questGuineaPigUnlockText": "Unlocks Guinea Pig Eggs for purchase in the Market",
"questPeacockText": "The Push-and-Pull Peacock",
"questPeacockNotes": "You trek through the Taskwoods, wondering which of the enticing new goals you should pick. As you go deeper into the forest, you realise that you're not alone in your indecision. \"I could learn a new language, or go to the gym...\" @Cecily Perez mutters. \"I could sleep more,\" muses @Lilith of Alfheim, \"or spend time with my friends...\" It looks like @PainterProphet, @Pfeffernusse, and @Draayder are equally paralysed by the overwhelming options.<br><br>You realise that these ever-more-demanding feelings aren't really your own... you've stumbled straight into the trap of the pernicious Push-and-Pull Peacock! Before you can run, it leaps from the bushes. With each head pulling you in conflicting directions, you start to feel burnout overcoming you. You can't defeat both foes at once, so you only have one option -- concentrate on the nearest task to fight back!",
"questPeacockNotes": "You trek through the Taskwoods, wondering which of the enticing new goals you should pick. As you go deeper into the forest, you realise that you're not alone in your indecision. \"I could learn a new language, or go to the gym...\" @Cecily Perez mutters. \"I could sleep more,\" muses @Lilith of Alfheim, \"or spend time with my friends...\" It looks like @PainterProphet, @Pfeffernusse, and @Draayder are equally paralysed by the overwhelming options.<br><br>You realise that these ever-more-demanding feelings aren't really your own... you've stumbled straight into the trap of the pernicious Push-and-Pull Peacock! Before you can run, it leaps from the bushes. With each head pulling you in conflicting directions, you start to feel burnout overcoming you. You can't defeat both foes at once, so you only have one optionconcentrate on the nearest task to fight back!",
"questPeacockCompletion": "The Push-and-Pull Peacock is caught off guard by your sudden conviction. Defeated by your single-minded drive, its heads merge back into one, revealing the most beautiful creature you've ever seen. \"Thank you,\" the peacock says. \"Ive spent so long pulling myself in different directions that I lost sight of what I truly wanted. Please accept these eggs as a token of my gratitude.\"",
"questPeacockBoss": "Push-and-Pull Peacock",
"questPeacockDropPeacockEgg": "Peacock (Egg)",
"questPeacockUnlockText": "Unlocks Peacock Eggs for purchase in the Market",
"questButterflyText": "Bye, Bye, Butterfry",
"questButterflyNotes": "Your gardener friend @Megan sends you an invitation: “These warm days are the perfect time to visit Habiticas butterfly garden in the Taskan countryside. Come see the butterflies migrate!” When you arrive, however, the garden is in shambles -- little more than scorched grass and dried-out weeds. Its been so hot that the Habiticans havent come out to water the flowers, and the dark-red Dailies have turned it into a dry, sun-baked, fire-hazard. There's only one butterfly there, and there's something odd about it...<br><br>“Oh no! This is the perfect hatching ground for the Flaming Butterfry,” cries @Leephon.<br><br>“If we dont catch it, itll destroy everything!” gasps @Eevachu.<br><br>Time to say bye, bye to Butterfry!",
"questButterflyNotes": "Your gardener friend @Megan sends you an invitation: “These warm days are the perfect time to visit Habiticas butterfly garden in the Taskan countryside. Come see the butterflies migrate!” When you arrive, however, the garden is in shambleslittle more than scorched grass and dried-out weeds. Its been so hot that the Habiticans havent come out to water the flowers, and the dark-red Dailies have turned it into a dry, sun-baked, fire-hazard. There's only one butterfly there, and there's something odd about it...<br><br>“Oh no! This is the perfect hatching ground for the Flaming Butterfry,” cries @Leephon.<br><br>“If we dont catch it, itll destroy everything!” gasps @Eevachu.<br><br>Time to say bye, bye to Butterfry!",
"questButterflyCompletion": "After a blazing battle, the Flaming Butterfry is captured. “Great job catching that would-be arsonist,” says @Megan with a sigh of relief. “Still, its hard to vilify even the vilest butterfly. Wed better free this Butterfry someplace safe…like the desert.”<br><br>One of the other gardeners, @Beffymaroo, comes up to you, singed but smiling. “Will you help raise these foundling chrysalises we found? Perhaps next year well have a greener garden for them.”",
"questButterflyBoss": "Flaming Butterfry",
"questButterflyDropButterflyEgg": "Caterpillar (Egg)",
@@ -478,7 +478,7 @@
"questMayhemMistiflying1Boss": "Air Skull Swarm",
"questMayhemMistiflying1RageTitle": "Swarm Respawn",
"questMayhemMistiflying1RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Air Skull Swarm will heal 30% of its remaining health!",
"questMayhemMistiflying1RageEffect": "`Air Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls come whirling out of the clouds!",
"questMayhemMistiflying1RageEffect": "Air Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls come whirling out of the clouds!",
"questMayhemMistiflying1DropSkeletonPotion": "Skeleton Hatching Potion",
"questMayhemMistiflying1DropWhitePotion": "White Hatching Potion",
"questMayhemMistiflying1DropArmor": "Roguish Rainbow Messenger Robes (Armour)",
@@ -535,7 +535,7 @@
"questLostMasterclasser3Boss": "Void Skull Swarm",
"questLostMasterclasser3RageTitle": "Swarm Respawn",
"questLostMasterclasser3RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Void Skull Swarm will heal 30% of its remaining health!",
"questLostMasterclasser3RageEffect": "`Void Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls scream down from the heavens, bolstering the swarm!",
"questLostMasterclasser3RageEffect": "Void Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls scream down from the heavens, bolstering the swarm!",
"questLostMasterclasser3DropBodyAccessory": "Aether Amulet (Body Accessory)",
"questLostMasterclasser3DropBasePotion": "Base Hatching Potion",
"questLostMasterclasser3DropGoldenPotion": "Golden Hatching Potion",
@@ -548,7 +548,7 @@
"questLostMasterclasser4Boss": "Anti'zinnya",
"questLostMasterclasser4RageTitle": "Siphoning Void",
"questLostMasterclasser4RageDescription": "Siphoning Void: This bar fills when you don't complete your Dailies. When it is full, Anti'zinnya will remove the party's Mana!",
"questLostMasterclasser4RageEffect": "`Anti'zinnya uses SIPHONING VOID!` In a twisted inversion of the Ethereal Surge spell, you feel your magic drain away into the darkness!",
"questLostMasterclasser4RageEffect": "Anti'zinnya uses SIPHONING VOID! In a twisted inversion of the Ethereal Surge spell, you feel your magic drain away into the darkness!",
"questLostMasterclasser4DropBackAccessory": "Aether Cloak (Back Accessory)",
"questLostMasterclasser4DropWeapon": "Aether Crystals (Two-Handed Weapon)",
"questLostMasterclasser4DropMount": "Invisible Aether Mount",
@@ -612,7 +612,7 @@
"questSeaSerpentDropSeaSerpentEgg": "Sea Serpent (Egg)",
"questSeaSerpentUnlockText": "Unlocks Sea Serpent Eggs for purchase in the Market",
"questKangarooText": "Kangaroo Catastrophe",
"questKangarooNotes": "Maybe you should have finished that last task… you know, the one you keep avoiding, even though it always comes back around? But @Mewrose and @LilithofAlfheim invited you and @stefalupagus to see a rare kangaroo troop hopping through the Sloensteadi Savannah; how could you say no?! As the troop comes into view, something hits you on the back of the head with a mighty <em>whack!</em><br><br>Shaking the stars from your vision, you pick up the responsible object--a dark red boomerang, with the very task you continually push back etched into its surface. A quick glance around confirms the rest of your party met a similar fate. One larger kangaroo looks at you with a smug grin, like shes daring you to face her and that dreaded task once and for all!",
"questKangarooNotes": "Maybe you should have finished that last task… you know, the one you keep avoiding, even though it always comes back around? But @Mewrose and @LilithofAlfheim invited you and @stefalupagus to see a rare kangaroo troop hopping through the Sloensteadi Savannah; how could you say no?! As the troop comes into view, something hits you on the back of the head with a mighty <em>whack!</em><br><br>Shaking the stars from your vision, you pick up the responsible objecta dark red boomerang, with the very task you continually push back etched into its surface. A quick glance around confirms the rest of your party met a similar fate. One larger kangaroo looks at you with a smug grin, like shes daring you to face her and that dreaded task once and for all!",
"questKangarooCompletion": "“NOW!” You signal your party to throw the boomerangs back at the kangaroo. The beast hops further away with each hit until she flees, leaving nothing more than a dark red cloud of dust, a few eggs, and some gold coins.<br><br>@Mewrose walks forward to where the kangaroo once stood. “Hey, where did the boomerangs go?”<br><br>“They probably dissolved into dust, making that dark red cloud, when we finished our respective tasks,” @stefalupagus speculates.<br><br>@LilithofAlfheim squints at the horizon. “Is that another kangaroo troop heading our way?”<br><br>You all break into a run back to Habit City. Better to face your difficult tasks than take another lump to the back of the head!",
"questKangarooBoss": "Catastrophic Kangaroo",
"questKangarooDropKangarooEgg": "Kangaroo (Egg)",
@@ -672,7 +672,7 @@
"delightfulDinosNotes": "Contains Quests to obtain Triceratops, T-Rex, and Pterodactyl Pet eggs: The Trampling Triceratops, The Dinosaur Unearthed, and The Pterror-dactyl.",
"questAmberText": "The Amber Alliance",
"questAmberNotes": "Youre sitting in the Tavern with @beffymaroo and @-Tyr- when @Vikte bursts through the door and excitedly tells you about the rumours of another type of Magic Hatching Potion hidden in the Taskwoods. Having completed your Dailies, the three of you immediately agree to help @Vikte on their search. After all, whats the harm in a little adventure?<br><br>After walking through the Taskwoods for hours, youre beginning to regret joining such a wild chase. Youre about to head home, when you hear a surprised yelp and turn to see a huge lizard with shiny amber scales coiled around a tree, clutching @Vikte in her claws. @beffymaroo reaches for her sword.<br><br>“Wait!” cries @-Tyr-. “Its the Trerezin! Shes not dangerous, just dangerously clingy!”",
"questAmberCompletion": "“Trerezin?” @-Tyr- says calmly. “Could you let @Vikte go? I dont think theyre enjoying being so high up.”<br><br>The Trerezins amber skin blushes crimson and she gently lowers @Vikte to the ground. “My apologies! Its been so long since Ive had any guests that Ive forgotten my manners!” She slithers forward to greet you properly before disappearing into her treehouse, and returning with an armful of Amber Hatching Potions as thank-you gifts!<br><br>“Magic Potions!” @Vikte gasps.<br><br>“Oh, these old things?” The Trerezin's tongue flickers as she thinks. “How about this? Ill give you this whole stack if you promise to visit me every so often...”<br><br>And so you leave the Taskwoods, excited to tell everyone about the new potions--and your new friend!",
"questAmberCompletion": "“Trerezin?” @-Tyr- says calmly. “Could you let @Vikte go? I dont think theyre enjoying being so high up.”<br><br>The Trerezins amber skin blushes crimson and she gently lowers @Vikte to the ground. “My apologies! Its been so long since Ive had any guests that Ive forgotten my manners!” She slithers forward to greet you properly before disappearing into her treehouse, and returning with an armful of Amber Hatching Potions as thank-you gifts!<br><br>“Magic Potions!” @Vikte gasps.<br><br>“Oh, these old things?” The Trerezin's tongue flickers as she thinks. “How about this? Ill give you this whole stack if you promise to visit me every so often...”<br><br>And so you leave the Taskwoods, excited to tell everyone about the new potionsand your new friend!",
"questAmberBoss": "Trerezin",
"questAmberDropAmberPotion": "Amber Hatching Potion",
"questAmberUnlockText": "Unlocks Amber Hatching Potions for purchase in the Market",
@@ -686,12 +686,12 @@
"questRubyText": "Ruby Rapport",
"questWaffleRageTitle": "Maple Mire",
"questWaffleBoss": "Awful Waffle",
"questWaffleCompletion": "Battered and buttered but triumphant, you savour sweet victory as the Awful Waffle collapses into a pool of sticky goo.<br><br>“Wow, you really creamed that monster,” says Lady Glaciate, impressed.<br><br>“A piece of cake!” beams the April Fool.<br><br>“Kind of a shame, though,” says @beffymaroo. “It looked good enough to eat.”<br><br>The Fool takes a set of potion bottles from somewhere in his cape, fills them with the syrupy leavings of the Waffle, and mixes in a pinch of sparkling dust. The liquid swirls with colour--new Hatching Potions! He tosses them into your arms. “All that adventure has given me an appetite. Who wants to join me for breakfast?”",
"questWaffleCompletion": "Battered and buttered but triumphant, you savour sweet victory as the Awful Waffle collapses into a pool of sticky goo.<br><br>“Wow, you really creamed that monster,” says Lady Glaciate, impressed.<br><br>“A piece of cake!” beams the April Fool.<br><br>“Kind of a shame, though,” says @beffymaroo. “It looked good enough to eat.”<br><br>The Fool takes a set of potion bottles from somewhere in his cape, fills them with the syrupy leavings of the Waffle, and mixes in a pinch of sparkling dust. The liquid swirls with colournew Hatching Potions! He tosses them into your arms. “All that adventure has given me an appetite. Who wants to join me for breakfast?”",
"questWaffleNotes": "“April Fool!” storms a flustered Lady Glaciate. “You said your dessert-themed prank was over with and completely cleaned up!”<br><br>“Why, it was and is, my dear,” replies the Fool, puzzled. “And I am the most honest of Fools. What's wrong?”<br><br>“There's a giant sugary monster approaching Habit City!”<br><br>“Hmm,” muses the Fool. “I did raid a few lairs for the mystic reagents for my last event. Maybe I attracted some unwanted attention. Is it the Saccharine Serpent? The Torte-oise? Tiramisu Rex?”<br><br>“No! It's some sort of... Awful Waffle!”<br><br>“Huh. That's a new one! Perhaps it spawned from all the ambient shenanigan energy.” He turns to you and @beffymaroo with a lopsided smile. “I don't suppose you'd be available for some heroics?”",
"questWaffleText": "Waffling with the Fool: Disaster Breakfast!",
"questWaffleUnlockText": "Unlocks Confection Hatching Potions for purchase in the Market",
"questWaffleDropDessertPotion": "Confection Hatching Potion",
"questWaffleRageEffect": "`Awful Waffle uses MAPLE MIRE!` Sticky sappy syrup slows your swings and spells! Pending damage reduced.",
"questWaffleRageEffect": "Awful Waffle uses MAPLE MIRE! Sticky sappy syrup slows your swings and spells! Pending damage reduced.",
"questWaffleRageDescription": "Maple Mire: This bar fills when you don't complete your Dailies. When it is full, the Awful Waffle will set back the party's attack progress!",
"jungleBuddiesNotes": "Contains Quests to obtain Monkey, Treeling, and Sloth Pet eggs: Monstrous Mandrill and the Mischief Monkeys, The Tangle Tree, and The Somnolent Sloth.",
"jungleBuddiesText": "Jungle Buddies Quest Bundle",
@@ -701,7 +701,7 @@
"questFluoriteCompletion": "As you do battle, the crystal creature seems more and more distracted by the light show you are creating. “So shiny…” it mutters.<br><br>“Of course!” @nirbhao exclaims. “It must be a fluorite elemental. All they want is light to let them glow. Lets help it shine.”<br><br>The elemental giggles happily and glows all the brighter as you light up torches and motes of magic. Its so glad to be shining again that it leads you to a rich deposit of fluorite crystals.<br><br>“This is the perfect ingredient for a new hatching potion,” says @nirbhao. “One which will make our pets as bright as our new fluorescent friend.”",
"questFluoriteNotes": "Unusual minerals are in high demand these days, so you and a few friends have trekked deep into the mines of the Meandering Mountains, in search of exciting ores. Its a long and boring expedition, until @-Tyr- stumbles over a large rock, sitting right in the middle of the tunnel.<br><br>“This should help brighten things up,” says @nirbhao, before conjuring up an orb of light.<br><br>A warm brightness fills the tunnel, but something odd starts happening to that large rock. Feeding on the magical light, it begins to glow with fluorescent blues, greens and purples. Then it rears upright into a vaguely humanoid shape, complete with glowing red eyes fixed right on you! You jump into action with flashing spells and shining weapons.",
"questFluoriteText": "A Bright Fluorite Fright",
"questWindupNotes": "Habit City is seldom quiet, but you werent prepared for the cacophony of creaks, squeaks and screams escaping Good Timekeeping, Habiticas finest clockwork emporium. You sigh--you just wanted your watch fixed. The proprietor, known only as “Great and Powerful”, tumbles out the door, pursued by a clanking copper colossus!<br><br>“Ki-! Ki-! Ki!” it clangs, arms smashing up and down. Its gears grind and screech in protest.<br><br>“My robot Clankton has gone mad! Its trying to kill me!” the supposedly Powerful one shrieks.<br><br>Even with a broken watch, you can tell when its time to fight. You leap forward to defend the panicking watchmaker. @Vikte and @a_diamond also step up to help!<br><br>“Ki-! Ki-! Ki-!” Clankton chants with each blow. “Mew!”<br><br>Wait, was that mechanical mewling amidst the murderous monotone?",
"questWindupNotes": "Habit City is seldom quiet, but you werent prepared for the cacophony of creaks, squeaks and screams escaping Good Timekeeping, Habiticas finest clockwork emporium. You sighyou just wanted your watch fixed. The proprietor, known only as “Great and Powerful”, tumbles out the door, pursued by a clanking copper colossus!<br><br>“Ki-! Ki-! Ki!” it clangs, arms smashing up and down. Its gears grind and screech in protest.<br><br>“My robot Clankton has gone mad! Its trying to kill me!” the supposedly Powerful one shrieks.<br><br>Even with a broken watch, you can tell when its time to fight. You leap forward to defend the panicking watchmaker. @Vikte and @a_diamond also step up to help!<br><br>“Ki-! Ki-! Ki-!” Clankton chants with each blow. “Mew!”<br><br>Wait, was that mechanical mewling amidst the murderous monotone?",
"questWindupText": "A Whirl with a Wind-Up Warrior",
"questBlackPearlUnlockText": "Unlocks Black Pearl Hatching Potions for purchase in the Market",
"questBlackPearlDropBlackPearlPotion": "Black Pearl Hatching Potion",
@@ -739,7 +739,7 @@
"questStoneText": "A Maze of Moss",
"questSolarSystemUnlockText": "Unlocks Solar System Hatching Potions for purchase in the Market",
"questSolarSystemText": "A Voyage of Cosmic Concentration",
"questSolarSystemNotes": "Your party is traveling through the cosmos, seeing the sights in a fantastical airship designed by talented space engineer @gawrone. Its meditationite propulsion relies on the calm of your Party to stay on course.<br><br>Up ahead in the clouds of sparkling galaxies, you spot an ominously pulsing star. “Keep your focus,” warns @beffymaroo. “If we get too distracted when were passing that nova, the pull between the star and our ship may veer us off course!”<br><br>As you near the star, pulses of strange energy come toward the ship.<br><br>“Theyre Diversionoids, thought creatures trying to get us lost,” says @SabreCat. “If we can let them flow by without carrying us away, we should be able to stay pointed toward our goal!”",
"questSolarSystemNotes": "Your party is travelling through the cosmos, seeing the sights in a fantastical airship designed by talented space engineer @gawrone. Its meditationite propulsion relies on the calm of your Party to stay on course.<br><br>Up ahead in the clouds of sparkling galaxies, you spot an ominously pulsing star. “Keep your focus,” warns @beffymaroo. “If we get too distracted when were passing that nova, the pull between the star and our ship may veer us off course!”<br><br>As you near the star, pulses of strange energy come toward the ship.<br><br>“Theyre Diversionoids, thought creatures trying to get us lost,” says @SabreCat. “If we can let them flow by without carrying us away, we should be able to stay pointed toward our goal!”",
"questStoneCollectMarsRunes": "Mars Runes",
"questSolarSystemCompletion": "Through careful practice, you and the crew manage to keep the Diversionoids from sweeping you overboard, just by noticing and acknowledging them without letting them take over. As you pass safely by the pulsing star, @gawrone notices a cluster of floating bottles and pulls them aboard. Each appears to contain a tiny solar system!<br><br>“Well, looks like our hard work has brought us some fine rewards!” says @beffymaroo. “Lets see what celestial wonders might appear if we hatch pet eggs with these new potions.”",
"questStoneDropMossyStonePotion": "Mossy Stone Hatching Potion",
@@ -760,7 +760,7 @@
"questVirtualPetCompletion": "Some careful button pushing seems to have fulfilled the virtual pets mysterious needs, and finally it has quieted down and appears content.<br><br>Suddenly in a burst of confetti, the April Fool appears with a basket full of strange potions emitting soft beeps.<br><br>“What timing, April Fool,” @Beffymaroo says with a wry smile. “I suspect this large beeping fellow is an acquaintance of yours.”<br><br>“Uh, yes,” the Fool says, sheepishly. “So sorry about that, and thank you both for taking care of Wotchimon! Take these potions in the way of thanks, they can bring your Virtual pets back anytime you like!”<br><br>Youre not 100% sure youre on board with all the beeping, but theyre sure cute so its worth a shot!",
"questVirtualPetBoss": "Wotchimon",
"questVirtualPetRageTitle": "The Beepening",
"questVirtualPetRageEffect": "`Wotchimon uses Bothersome Beep!` Wotchimon sounds a bothersome beep, and its happiness bar suddenly disappears! Pending damage reduced.",
"questVirtualPetRageEffect": "Wotchimon uses Bothersome Beep! Wotchimon sounds a bothersome beep, and its happiness bar suddenly disappears! Pending damage reduced.",
"questVirtualPetDropVirtualPetPotion": "Virtual Pet Hatching Potion",
"questVirtualPetUnlockText": "Unlocks Virtual Pet Hatching Potion for purchase in the Market",
"questVirtualPetRageDescription": "This bar fills when you don't complete your Dailies. When it is full, the Wotchimon will take away some of your party's pending damage!",
@@ -769,7 +769,7 @@
"questPinkMarbleBoss": "Cupido",
"questPinkMarbleRageTitle": "Pink Punch",
"questPinkMarbleRageDescription": "This bar fills when you don't complete your Dailies. When it is full, Cupido will take away some of your party's pending damage!",
"questPinkMarbleRageEffect": "`Cupido uses Pink Punch!` That wasn't affectionate at all! Your partymates are taken aback. Pending damage reduced.",
"questPinkMarbleRageEffect": "Cupido uses Pink Punch! That wasn't affectionate at all! Your partymates are taken aback. Pending damage reduced.",
"questPinkMarbleDropPinkMarblePotion": "Pink Marble Hatching Potion",
"questPinkMarbleText": "Calm the Corrupted Cupid",
"questGiraffeText": "The Gear-affe",
+2 -2
View File
@@ -1,8 +1,8 @@
{
"rebirthNew": "Rebirth: New Adventure Available!",
"rebirthUnlock": "You've unlocked Rebirth! This special Market item allows you to begin a new game at level 1 while keeping your tasks, achievements, pets, and more. Use it to breathe new life into Habitica if you feel you've achieved it all, or to experience new features with the fresh eyes of a beginning character!",
"rebirthAchievement": "You've begun a new adventure! This is Rebirth <%= number %> for you, and the highest Level you've attained is <%= level %>. To stack this Achievement, begin your next new adventure when you've reached an even higher Level!",
"rebirthAchievement100": "You've begun a new adventure! This is Rebirth <%= number %> for you, and the highest Level you've attained is 100 or higher. To stack this Achievement, begin your next new adventure when you've reached at least 100!",
"rebirthAchievement": "You've used the Orb of Rebirth <strong><%= number %></strong> times, and your highest level reached is <strong><%= level %></strong>.",
"rebirthAchievement100": "You've used the Orb of Rebirth <strong><%= number %></strong> times, and your highest level reached is <strong>100</strong> or higher.",
"rebirthBegan": "Began a New Adventure",
"rebirthText": "Began <%= rebirths %> New Adventures",
"rebirthOrb": "Used an Orb of Rebirth to start over after attaining Level <%= level %>.",
+82 -82
View File
@@ -3,9 +3,9 @@
"onwards": "¡Adelante!",
"levelup": "Al cumplir tus objetivos en la vida real, ¡has subido de nivel y has recuperado toda tu salud!",
"reachedLevel": "Has Alcanzado el Nivel <%= level %>",
"achievementLostMasterclasser": "Completador de Aventuras: Serie Arquimaestra",
"achievementLostMasterclasser": "Completista de Aventuras: Serie Arquimaestra",
"achievementLostMasterclasserText": "¡Ha completado las dieciséis misiones en la Serie Arquimaestra y resuelto el misterio de la Arquimaestra Perdida!",
"achievementLostMasterclasserModalText": "¡Completaste las dieciséis misiones en la Serie Maestro de Clases y resolviste el misterio de la Arquimaestra Perdida!",
"achievementLostMasterclasserModalText": "¡Completaste las dieciséis misiones en la Serie Arquimaestra y resolviste el misterio de la Arquimaestra Perdida!",
"achievementMindOverMatter": "La mente sobre la materia",
"achievementMindOverMatterText": "Ha completado las misiones de la Roca, el Limo y el Hilo.",
"achievementMindOverMatterModalText": "¡Completaste las misiones de mascotas de la Roca, el Limo y el Hilo!",
@@ -24,106 +24,106 @@
"achievementAridAuthorityText": "Por domar todas las Monturas Desérticas.",
"achievementAridAuthority": "Autoridad Árida",
"achievementDustDevilModalText": "¡Conseguiste todas las Mascotas Desérticas!",
"achievementDustDevilText": "Por conseguir todas las Mascotas Desérticas.",
"achievementDustDevilText": "Ha conseguido todas las Mascotas Desérticas.",
"achievementDustDevil": "Demonio de Polvo",
"achievementMonsterMagus": "Monstruo Magus",
"achievementUndeadUndertakerModalText": "¡Has domado todas las Monturas Zombi!",
"achievementUndeadUndertakerText": "Por domar todas las Monturas Zombie.",
"achievementUndeadUndertakerText": "Ha domado todas las Monturas Zombi.",
"achievementUndeadUndertaker": "Sepulturero de Muertos Vivientes",
"achievementMonsterMagusModalText": "¡Has conseguido todas las Mascotas Zombi!",
"achievementMonsterMagusText": "Por conseguir todas las Mascotas Zombi.",
"achievementMonsterMagusText": "Ha conseguido todas las Mascotas Zombi.",
"achievementPartyOn": "¡Tu equipo llegó a 4 miembros!",
"achievementPartyUp": "¡Formaste un equipo con alguien más!",
"achievementPearlyProModalText": "¡Has domado todas las Mascotas Blancas!",
"achievementPearlyProText": "Ha domado Todas las Mascotas Blancas.",
"achievementPearlyProModalText": "¡Has domado todas las Monturas Blancas!",
"achievementPearlyProText": "Ha domado todas las Monturas Blancas.",
"achievementPrimedForPaintingModalText": "¡Has conseguido todas las Mascotas Blancas!",
"achievementPrimedForPaintingText": "Ha conseguido todas las Mascotas Blancas.",
"achievementPrimedForPainting": "Preparado para pintar",
"achievementPrimedForPainting": "Preparado para Pintar",
"hideAchievements": "Ocultar <%= category %>",
"showAllAchievements": "Mostrar todo <%= category %>",
"onboardingCompleteDesc": "Has ganado <strong>5 logros</strong> y <strong class=\"gold-amount\">100 de oro</strong> por completar la lista.",
"showAllAchievements": "Mostrar Todos <%= category %>",
"onboardingCompleteDesc": "Has ganado <strong>5 Logros</strong> y <strong class=\"gold-amount\">100 de Oro</strong> por completar la lista.",
"earnedAchievement": "¡Has conseguido un logro!",
"viewAchievements": "Ver Logros",
"letsGetStarted": "¡Comencemos!",
"onboardingProgress": "<%= percentage %> % de progreso",
"gettingStartedDesc": "¡Completa estas tareas de incorporación y ganarás <strong>5 logros</strong> y <strong class=\"gold-amount\">100 de oro</strong> una vez hayas terminado!",
"gettingStartedDesc": "¡Completa estas tareas de introducción y ganarás <strong>5 Logros</strong> y <strong class=\"gold-amount\">100 de Oro</strong> una vez hayas terminado!",
"achievementCreatedTaskText": "Creó su primera tarea.",
"achievementCreatedTask": "Crea tu primera tarea",
"achievementFedPet": "Alimenta a una mascota",
"achievementFedPetModalText": "Hay muchos tipos diferentes de alimentos, pero las mascotas pueden ser exigentes",
"achievementFedPet": "Alimenta a una Mascota",
"achievementFedPetModalText": "Hay muchos tipos diferentes de alimentos, pero las Mascotas pueden ser selectivas",
"achievementHatchedPetModalText": "Dirígete a tu inventario y prueba a combinar una poción de eclosión y un huevo",
"achievementCompletedTaskText": "Has completado tu primera tarea.",
"achievementPurchasedEquipmentModalText": "El equipamiento es una forma de personalizar tu avatar y mejorar tus estadísticas",
"achievementPurchasedEquipment": "Compra una pieza de equipamiento",
"achievementCompletedTaskModalText": "Marca alguna de tus tareas para conseguir recompensas",
"achievementCompletedTaskText": "Ha completado su primera tarea.",
"achievementPurchasedEquipmentModalText": "El Equipamiento es una forma de personalizar tu avatar y mejorar tus Estadísticas",
"achievementPurchasedEquipment": "Compra una pieza de Equipamiento",
"achievementCompletedTaskModalText": "Marca como completada cualquier tarea para conseguir recompensas",
"achievementCompletedTask": "Completa una tarea",
"achievementCreatedTaskModalText": "Añade una tarea para algo que te gustaría cumplir esta semana",
"achievementFedPetText": "Alimentó a su primera mascota.",
"achievementPurchasedEquipmentText": "Adquirió su primera pieza de equipo.",
"achievementPurchasedEquipmentText": "Adquirió su primera pieza de equipamento.",
"achievementHatchedPetText": "Eclosionó su primera mascota.",
"achievementHatchedPet": "Eclosiona una mascota",
"achievementHatchedPet": "Eclosiona una Mascota",
"achievementPearlyPro": "Coleccionista de Perlas",
"achievementTickledPinkModalText": "¡Has conseguido todas las mascotas de algodón de azúcar rosa!",
"achievementTickledPinkText": "Ha conseguido todas las mascotas de algodón de azúcar rosa.",
"achievementTickledPinkModalText": "¡Has conseguido todas las mascotas de Algodón de Azúcar Rosa!",
"achievementTickledPinkText": "Ha conseguido todas las mascotas de Algodón de Azúcar Rosa.",
"achievementTickledPink": "cosquillas rosa",
"foundNewItemsCTA": "¡Dirígete a tu Inventario e intenta combinar tu nueva poción para incubar y un huevo!",
"foundNewItemsCTA": "¡Dirígete a tu Inventario e intenta combinar tu nueva poción de eclosión y un huevo!",
"foundNewItemsExplanation": "Completar tareas te da la oportunidad de encontrar objetos, como Huevos, Pociones de Eclosión y Alimento para Mascotas.",
"foundNewItems": "¡Encontraste nuevos artículos!",
"onboardingCompleteDescSmall": "¡Si quieres aún más, mira tus Logros y empieza a coleccionarlos!",
"yourProgress": "Tu Progreso",
"onboardingComplete": "¡Has completado tus tareas de incorporación!",
"achievementRosyOutlookModalText": "¡Has domado todas las monturas de algodón de azúcar rosa!",
"achievementRosyOutlookText": "Ha domado todas las monturas de algodón de azúcar rosa.",
"achievementRosyOutlook": "Perspectiva optimista",
"achievementBareNecessities": "Necesidades básicas",
"achievementBugBonanzaModalText": "¡Has completado las misiones de las mascotas escarabajo, mariposa, caracol y araña!",
"achievementBugBonanzaText": "Ha completado las misiones de las mascotas escarabajo, mariposa, caracol y araña.",
"achievementBareNecessitiesModalText": "¡Has completado las misiones de las mascotas mono, perezoso y brote!",
"achievementBareNecessitiesText": "Ha completado las misiones de las mascotas mono, perezoso y brote.",
"onboardingComplete": "¡Has completado tus tareas de introducción!",
"achievementRosyOutlookModalText": "¡Has domado todas las Monturas de Algodón de Azúcar Rosa!",
"achievementRosyOutlookText": "Ha domado todas las Monturas de Algodón de Azúcar Rosa.",
"achievementRosyOutlook": "Perspectiva Optimista",
"achievementBareNecessities": "Necesidades Básicas",
"achievementBugBonanzaModalText": "¡Has completado las misiones de las mascotas Escarabajo, Mariposa, Caracol y Araña!",
"achievementBugBonanzaText": "Ha completado las misiones de las mascotas Escarabajo, Mariposa, Caracol y Araña.",
"achievementBareNecessitiesModalText": "¡Has completado las misiones de las mascotas Mono, Perezoso y Brote!",
"achievementBareNecessitiesText": "Ha completado las misiones de las mascotas Mono, Perezoso y Brote.",
"achievementBugBonanza": "Bonanza insectil",
"achievementFreshwaterFriendsModalText": "¡Has completado las misiones de las mascotas ajolote, rana e hipopótamo!",
"achievementFreshwaterFriendsText": "Ha completado las misiones de las mascotas ajolote, rana, e hipopótamo.",
"achievementFreshwaterFriends": "Amigos de agua dulce",
"achievementFreshwaterFriendsModalText": "¡Has completado las misiones de las mascotas Ajolote, Rana e Hipopótamo!",
"achievementFreshwaterFriendsText": "Ha completado las misiones de las mascotas Ajolote, Rana, e Hipopótamo.",
"achievementFreshwaterFriends": "Amigos de Agua Dulce",
"achievementAllThatGlittersModalText": "¡Has domado todas las Monturas Doradas!",
"achievementAllThatGlittersText": "Ha domado todas las Monturas Doradas.",
"achievementGoodAsGoldModalText": "¡Has conseguido todas las Mascotas Doradas!",
"achievementGoodAsGoldText": "Ha conseguido todas las Mascotas doradas.",
"achievementGoodAsGold": "Más Bueno que el Pan",
"achievementGoodAsGoldText": "Ha conseguido todas las Mascotas Doradas.",
"achievementGoodAsGold": "Corazón de Oro",
"yourRewards": "Tus Recompensas",
"achievementAllThatGlitters": "Todo lo que brilla",
"achievementBoneCollector": "Coleccionista de huesos",
"achievementBoneCollectorText": "Ha conseguido todas las Mascotas esqueléticas.",
"achievementAllThatGlitters": "Todo lo que Brilla",
"achievementBoneCollector": "Coleccionista de Huesos",
"achievementBoneCollectorText": "Ha conseguido todas las Mascotas Esqueleto.",
"achievementSeeingRed": "Rojo de Ira",
"achievementSkeletonCrewModalText": "¡Has domado todas las monturas esqueléticas!",
"achievementSkeletonCrewText": "Ha domado todas las monturas esqueléticas.",
"achievementSkeletonCrew": "Equipo esquelético",
"achievementBoneCollectorModalText": "¡Has conseguido todas las mascotas esqueléticas!",
"achievementSeeingRedText": "Ha conseguido todas las Mascotas rojas.",
"achievementSeeingRedModalText": "¡Has conseguido todas las mascotas rojas!",
"achievementRedLetterDayModalText": "¡Has domado todas las monturas rojas!",
"achievementRedLetterDayText": "Ha domado todas las monturas rojas.",
"achievementSkeletonCrewModalText": "¡Has domado todas las Monturas Esqueleto!",
"achievementSkeletonCrewText": "Ha domado todas las Monturas Esqueleto.",
"achievementSkeletonCrew": "Ejército Huesudo",
"achievementBoneCollectorModalText": "¡Has conseguido todas las Mascotas Esqueleto!",
"achievementSeeingRedText": "Ha conseguido todas las Mascotas Rojas.",
"achievementSeeingRedModalText": "¡Has conseguido todas las Mascotas Rojas!",
"achievementRedLetterDayModalText": "¡Has domado todas las Monturas Rojas!",
"achievementRedLetterDayText": "Ha domado todas las Monturas Rojas.",
"achievementRedLetterDay": "Día señalado",
"achievementLegendaryBestiaryModalText": "¡Has conseguido todas las mascotas míticas!",
"achievementLegendaryBestiaryText": "¡Ha eclosionado todos los colores estándar de todas las mascotas míticas: Dragón, Cerdo Volador, Grifo, Serpiente Marina y Unicornio!",
"achievementLegendaryBestiary": "Bestiario legendario",
"achievementLegendaryBestiary": "Bestiario Legendario",
"achievementSeasonalSpecialistModalText": "¡Completaste todas las misiones estacionales!",
"achievementSeasonalSpecialistText": "Ha completado todas las misiones estacionales de Primavera e Invierno: ¡Búsqueda de Huevos, Santa Trampero, y Encuentra al Cachorro!",
"achievementSeasonalSpecialist": "Especialista Estacional",
"achievementVioletsAreBlue": "Las violetas son azules",
"achievementVioletsAreBlue": "Las Violetas son Azules",
"achievementWildBlueYonderModalText": "¡Has domado todas las monturas de Algodon de Azúcar Azul!",
"achievementWildBlueYonderText": "Ha domado todas las monturas de Algodon de Azúcar Azul.",
"achievementWildBlueYonder": "La salvaje y azul lejanía",
"achievementWildBlueYonder": "La Salvaje y Azul Lejanía",
"achievementVioletsAreBlueModalText": "¡Has conseguido todas las mascotas de Algodon de Azúcar Azul!",
"achievementVioletsAreBlueText": "Ha conseguido todas las mascotas de Algodon de Azúcar Azul.",
"achievementDomesticatedText": "¡Has criado todos los colores de mascotas domésticas: hurón, cobaya, gallo, cerdo volador, rata, conejito, caballo y vaca!",
"achievementDomesticated": "E-I-E-I-O",
"achievementDomesticatedText": "¡Ha eclosionado todos los colores de mascotas domésticas: Hurón, Cobaya, Gallo, Cerdo Volador, Rata, Conejito, Caballo y Vaca!",
"achievementDomesticated": "I-A-I-A-O",
"achievementDomesticatedModalText": "¡Has conseguido todas las mascotas domesticadas!",
"achievementShadyCustomerModalText": "¡Has conseguido todas las mascotas sombrías!",
"achievementShadeOfItAll": "La sombra de todo ello",
"achievementShadeOfItAllText": "Ha domado todas las monturas sombrías.",
"achievementShadeOfItAllModalText": "¡Has domado todas las monturas sombrías!",
"achievementShadyCustomerText": "Ha conseguido todas las mascotas sombrías.",
"achievementShadyCustomer": "Cliente sombrío",
"achievementShadyCustomerModalText": "¡Has conseguido todas las Mascotas Sombrías!",
"achievementShadeOfItAll": "A la Sombra de Todo",
"achievementShadeOfItAllText": "Ha domado todas las Monturas Sombrías.",
"achievementShadeOfItAllModalText": "¡Has domado todas las Monturas Sombrías!",
"achievementShadyCustomerText": "Ha conseguido todas las Mascotas Sombrías.",
"achievementShadyCustomer": "Cliente Sombrío",
"achievementZodiacZookeeper": "Cuidador del Zodiaco",
"achievementZodiacZookeeperText": "¡Ha eclosionado todas las mascotas del zodíaco de color básico: Rata, Vaca, Conejo, Serpiente, Caballo, Oveja, Mono, Gallo, Lobo, Tigre, Cerdo Volador y Dragón!",
"achievementZodiacZookeeperModalText": "¡Has conseguido todas las mascotas del zodíaco!",
@@ -131,39 +131,39 @@
"achievementBirdsOfAFeatherModalText": "¡Has conseguido todas las mascotas voladoras!",
"achievementBirdsOfAFeather": "Aves de Pluma",
"achievementReptacularRumbleModalText": "¡Has coleccionado todas las mascotas reptiles!",
"achievementReptacularRumbleText": "!Has incubado todos los colores estándar de las mascotas reptiles: caimán, pterodáctilo, serpiente, triceratops, tortuga, tiranosaurio rex y velociraptor!",
"achievementGroupsBeta2022": "Probador Beta interactivo",
"achievementGroupsBeta2022Text": "Tú y tu grupo habéis proporcionado comentarios invaluables para ayudar a probar Habitica.",
"achievementReptacularRumbleText": "¡Ha eclosionado todos los colores estándar de las mascotas reptiles: Caimán, Pterodáctilo, Serpiente, Triceratops, Tortuga, Tiranosaurio Rex y Velociraptor!",
"achievementGroupsBeta2022": "Probador Beta Interactivo",
"achievementGroupsBeta2022Text": "Tú y tu grupo proporcionaron comentarios invaluables para ayudar a probar Habitica.",
"achievementGroupsBeta2022ModalText": "¡Usted y sus grupos ayudaron a Habitica probando y proporcionando comentarios!",
"achievementReptacularRumble": "Rumble reptacular",
"achievementWoodlandWizard": "Mago del bosque",
"achievementWoodlandWizardModalText": "¡Has recogido todas las mascotas del bosque!",
"achievementWoodlandWizardText": "Ha incubado todos los colores estándar de las criaturas del bosque: Tejón, Oso, Ciervo, Zorro, Rana, Erizo, Búho, Caracol, Ardilla y Treeling!",
"achievementBoneToPick": "Un hueso duro de roer",
"achievementBoneToPickText": "¡Ha eclosionado todas las mascotas Clásicas y de Misión Esqueléticas!",
"achievementWoodlandWizard": "Mago del Bosque",
"achievementWoodlandWizardModalText": "¡Has conseguido todas las mascotas del bosque!",
"achievementWoodlandWizardText": "¡Ha eclosionado todos los colores estándar de las criaturas del bosque: Tejón, Oso, Ciervo, Zorro, Rana, Erizo, Búho, Caracol, Ardilla y Brote!",
"achievementBoneToPick": "Un Hueso Duro de Roer",
"achievementBoneToPickText": "¡Ha eclosionado todas las Mascotas Esqueleto, Clásicas y de Misión!",
"achievementPolarPro": "Experto Polar",
"achievementPolarProModalText": "¡Has coleccionado todas las mascotas Polares!",
"achievementBoneToPickModalText": "¡Has coleccionado todas las mascotas clásicas y de misiones esqueléticas!",
"achievementPolarProModalText": "¡Has conseguido todas las Mascotas Polares!",
"achievementBoneToPickModalText": "¡Has coleccionado todas las Mascotas Esqueleto, Clásicas y de Misión!",
"achievementPolarProText": "¡Ha eclosionado todos los colores estándar para mascotas Polares: Osos, Zorros, Pinguinos, Ballenas y Lobos!",
"achievementPlantParent": "Cuidador de Plantas",
"achievementPlantParentText": "¡Ha eclosionado todos los colores estándar de mascotas vegetales: Cactus y Árbolito!",
"achievementPlantParentModalText": "¡Has coleccionado todas las Mascotas Planta!",
"achievementPlantParentText": "¡Ha eclosionado todos los colores estándar de mascotas vegetales: Cactus y Brote!",
"achievementPlantParentModalText": "¡Has conseguido todas las Mascotas Planta!",
"achievementDinosaurDynasty": "Dinastía de Dinosaurios",
"achievementDinosaurDynastyModalText": "¡Has recogido todas las mascotas de pájaros y dinosaurios!",
"achievementDinosaurDynastyText": "Ha eclosionado todos los colores estándar de mascotas, de aves y dinosaurios: halcón, búho, loro, pavo real, pingüino, gallo, pterodáctilo, tiranosaurio rex, triceratops y velociraptor!",
"achievementDinosaurDynastyModalText": "¡Has conseguido todas las mascotas ave y dinosaurio!",
"achievementDinosaurDynastyText": "Ha eclosionado todos los colores estándar de mascotas ave y dinosaurio: Halcón, Búho, Loro, Pavo Real, Pingüino, Gallo, Pterodáctilo, Tiranosaurio rex, Triceratops y Velociraptor!",
"achievementRoughRider": "Jinete tosco",
"achievementRoughRiderText": "¡Ha eclosionado todos los colores estándar de mascotas y monturas incómodas: Cactus, Erizo y Piedra!",
"achievementBonelessBossModalText": Tienes todas las mascotas invertebradas en tu colección!",
"achievementBonelessBossText": "Ha eclosionado todos los colores estándar de mascotas invertebradas: escarabajo, mariposa calamar, nudibranquio, pulpo, caracol y araña!",
"achievementBonelessBoss": "Jefe deshuesado",
"achievementDuneBuddyText": "¡Ha eclosionado todos los colores estándar de mascotas de clima desértico: armadillo, cactus, zorro, rana, serpiente y araña!",
"achievementBonelessBossModalText": Has conseguido todas las mascotas invertebradas!",
"achievementBonelessBossText": "¡Ha eclosionado todos los colores estándar de mascotas invertebradas: Escarabajo, Mariposa, Calamar, Nudibranquio, Pulpo, Caracol y Araña!",
"achievementBonelessBoss": "Jefe Deshuesado",
"achievementDuneBuddyText": "¡Ha eclosionado todos los colores estándar de mascotas de clima desértico: Armadillo, Cactus, Zorro, Rana, Serpiente y Araña!",
"achievementDuneBuddy": "Amigo de médano",
"achievementDuneBuddyModalText": "¡Has conseguido todas las mascotas de desierto!",
"achievementRoughRiderModalText": "¡Has conseguido todos los colores básicos de las mascotas y monturas incómodas!",
"achievementRodentRuler": "Gobernante Roedor",
"achievementRodentRulerText": "¡Ha eclosionado todos los colores estándar de las mascotas roedores: Conejillo de Indias, Rata y Ardilla!",
"achievementRodentRulerModalText": "¡Has conseguido todos las roedores mascota!",
"achievementCats": "Pastor de Gatos",
"achievementCatsText": "¡Ha eclosionado todos los colores estándar de las mascotas felinas: Guepardo, León, Tigre Dientes de Sable y Tigre!",
"achievementRoughRiderModalText": "¡Has conseguido todos los colores básicos de mascotas y monturas incómodas!",
"achievementRodentRuler": "Rey Roedor",
"achievementRodentRulerText": "¡Ha eclosionado todos los colores estándar de mascotas roedores: Conejillo de Indias, Rata y Ardilla!",
"achievementRodentRulerModalText": "¡Has conseguido todos las mascotas roedores!",
"achievementCats": "Señora de los Gatos",
"achievementCatsText": "¡Ha eclosionado todos los colores estándar de mascotas felinas: Guepardo, León, Tigre Dientes de Sable y Tigre!",
"achievementCatsModalText": "¡Has conseguido todas las mascotas felinas!"
}
+125 -114
View File
@@ -1,203 +1,203 @@
{
"backgrounds": "Fondos",
"background": "Fondo",
"backgroundShop": "Tienda de fondos",
"noBackground": "Ningún fondo seleccionado",
"backgroundShop": "Tienda de Fondos",
"noBackground": "Ningún Fondo Seleccionado",
"backgrounds062014": "1.ª serie: publicada en junio de 2014",
"backgroundBeachText": "Playa",
"backgroundBeachNotes": "Relájate en una cálida playa.",
"backgroundFairyRingText": "Anillo de hadas",
"backgroundFairyRingText": "Anillo de Hadas",
"backgroundFairyRingNotes": "Baila en un anillo de hadas.",
"backgroundForestText": "Bosque",
"backgroundForestNotes": "Pasea por un bosque estival.",
"backgrounds072014": "2.ª serie: publicada en julio de 2014",
"backgroundCoralReefText": "Arrecife de coral",
"backgroundCoralReefText": "Arrecife de Coral",
"backgroundCoralReefNotes": "Nada en un arrecife de coral.",
"backgroundOpenWatersText": "Aguas abiertas",
"backgroundOpenWatersText": "Aguas Abiertas",
"backgroundOpenWatersNotes": "Disfruta de las aguas abiertas.",
"backgroundSeafarerShipText": "Bajel de marineros",
"backgroundSeafarerShipNotes": "Navega a bordo de un barco marinero.",
"backgroundSeafarerShipText": "Bajel de Marineros",
"backgroundSeafarerShipNotes": "Navega a bordo de un Barco Marinero.",
"backgrounds082014": "3.ª serie: publicada en agosto de 2014",
"backgroundCloudsText": "Nubes",
"backgroundCloudsNotes": "Planea entre las nubes.",
"backgroundDustyCanyonsText": "Cañón polvoriento",
"backgroundDustyCanyonsText": "Cañón Polvoriento",
"backgroundDustyCanyonsNotes": "Pasea por un cañón polvoriento.",
"backgroundVolcanoText": "Volcán",
"backgroundVolcanoNotes": "Entra en calor dentro de un volcán.",
"backgrounds092014": "4.ª serie: publicada en septiembre de 2014",
"backgroundThunderstormText": "Tormenta eléctrica",
"backgroundThunderstormText": "Tormenta Eléctrica",
"backgroundThunderstormNotes": "Conduce rayos en la tormenta eléctrica.",
"backgroundAutumnForestText": "Bosque otoñal",
"backgroundAutumnForestText": "Bosque Otoñal",
"backgroundAutumnForestNotes": "Pasea por un bosque otoñal.",
"backgroundHarvestFieldsText": "Campos de cultivo",
"backgroundHarvestFieldsText": "Campos de Cultivo",
"backgroundHarvestFieldsNotes": "Labra tus campos de cultivo.",
"backgrounds102014": "5.ª serie: publicada en octubre de 2014",
"backgroundGraveyardText": "Cementerio",
"backgroundGraveyardNotes": "Visita un espeluznante cementerio.",
"backgroundHauntedHouseText": "Casa encantada",
"backgroundHauntedHouseText": "Casa Encantada",
"backgroundHauntedHouseNotes": "Entra a hurtadillas en una casa encantada.",
"backgroundPumpkinPatchText": "Terreno de calabazas",
"backgroundPumpkinPatchNotes": "Talla tu calabaza en este terreno.",
"backgroundPumpkinPatchText": "Campo de Calabazas",
"backgroundPumpkinPatchNotes": "Talla tus calabazas en este campo de calabazas.",
"backgrounds112014": "6.ª serie: publicada en noviembre de 2014",
"backgroundHarvestFeastText": "Festín de la cosecha",
"backgroundHarvestFeastNotes": "Disfruta del banquete de la cosecha.",
"backgroundStarrySkiesText": "Cielos estrellados",
"backgroundHarvestFeastText": "Festín de la Cosecha",
"backgroundHarvestFeastNotes": "Disfruta del Banquete de la Cosecha.",
"backgroundStarrySkiesText": "Cielos Estrellados",
"backgroundStarrySkiesNotes": "Contempla los cielos repletos de estrellas.",
"backgroundSunsetMeadowText": "Atardecer en la pradera",
"backgroundSunsetMeadowText": "Atardecer en la Pradera",
"backgroundSunsetMeadowNotes": "Admira un atardecer en la pradera.",
"backgrounds122014": "7.ª serie: publicada en diciembre de 2014",
"backgroundIcebergText": "Iceberg",
"backgroundIcebergNotes": "Flota a la deriva sobre un iceberg.",
"backgroundTwinklyLightsText": "Luces brillantes de invierno",
"backgroundIcebergText": "Témpano de Hielo",
"backgroundIcebergNotes": "Flota a la deriva sobre un Témpano de Hielo.",
"backgroundTwinklyLightsText": "Luces Brillantes de Invierno",
"backgroundTwinklyLightsNotes": "Camina entre árboles engalanados con luces festivas.",
"backgroundSouthPoleText": "Polo Sur",
"backgroundSouthPoleNotes": "Visita el gélido Polo Sur.",
"backgrounds012015": "8.ª serie: publicada en enero de 2015",
"backgroundIceCaveText": "Cueva de hielo",
"backgroundIceCaveNotes": "Desciende y adéntrate en una cueva de hielo.",
"backgroundFrigidPeakText": "Cima glacial",
"backgroundIceCaveText": "Cueva de Hielo",
"backgroundIceCaveNotes": "Desciende y adéntrate en una Cueva de Hielo.",
"backgroundFrigidPeakText": "Cima Glacial",
"backgroundFrigidPeakNotes": "Escala una cima glacial.",
"backgroundSnowyPinesText": "Pinos nevados",
"backgroundSnowyPinesText": "Pinos Nevados",
"backgroundSnowyPinesNotes": "Refúgiate entre pinos nevados.",
"backgrounds022015": "9.ª serie: publicada en febrero de 2015",
"backgroundBlacksmithyText": "Forja",
"backgroundBlacksmithyNotes": "Trabaja en la forja.",
"backgroundCrystalCaveText": "Cueva de cristal",
"backgroundCrystalCaveText": "Cueva de Cristal",
"backgroundCrystalCaveNotes": "Explora una cueva de cristal.",
"backgroundDistantCastleText": "Castillo distante",
"backgroundDistantCastleText": "Castillo Distante",
"backgroundDistantCastleNotes": "Defiende un castillo distante.",
"backgrounds032015": "10.ª serie: publicada en marzo de 2015",
"backgroundSpringRainText": "Lluvia primaveral",
"backgroundSpringRainText": "Lluvia Primaveral",
"backgroundSpringRainNotes": "Baila bajo la lluvia de primavera.",
"backgroundStainedGlassText": "Vidriera",
"backgroundStainedGlassNotes": "Contempla las vidrieras.",
"backgroundRollingHillsText": "Colinas ondulantes",
"backgroundRollingHillsText": "Colinas Ondulantes",
"backgroundRollingHillsNotes": "Corretea por las colinas ondulantes.",
"backgrounds042015": "11.ª serie: publicada en abril de 2015",
"backgroundCherryTreesText": "Cerezos",
"backgroundCherryTreesNotes": "Admira los cerezos en flor.",
"backgroundFloralMeadowText": "Prado floreciente",
"backgroundFloralMeadowText": "Prado Floreciente",
"backgroundFloralMeadowNotes": "Ve de pícnic a un prado floreciente.",
"backgroundGumdropLandText": "País de las Gominolas",
"backgroundGumdropLandNotes": "Mordisquea el paisaje del País de las Gominolas.",
"backgrounds052015": "12.ª serie: publicada en mayo de 2015",
"backgroundMarbleTempleText": "Templo de mármol",
"backgroundMarbleTempleText": "Templo de Mármol",
"backgroundMarbleTempleNotes": "Posa delante de un templo de mármol.",
"backgroundMountainLakeText": "Lago de montaña",
"backgroundMountainLakeText": "Lago de Montaña",
"backgroundMountainLakeNotes": "Atrévete a sumergir la punta del pie en este lago de montaña.",
"backgroundPagodasText": "Pagodas",
"backgroundPagodasNotes": "Sube a lo alto de las pagodas.",
"backgroundPagodasNotes": "Sube a lo alto de las Pagodas.",
"backgrounds062015": "13.ª serie: publicada en junio de 2015",
"backgroundDriftingRaftText": "Balsa a la deriva",
"backgroundDriftingRaftText": "Balsa a la Deriva",
"backgroundDriftingRaftNotes": "Rema sobre una balsa a la deriva.",
"backgroundShimmeryBubblesText": "Burbujas relucientes",
"backgroundShimmeryBubblesText": "Burbujas Relucientes",
"backgroundShimmeryBubblesNotes": "Flota a través de un mar de burbujas relucientes.",
"backgroundIslandWaterfallsText": "Cascadas isleñas",
"backgroundIslandWaterfallsText": "Cascadas Isleñas",
"backgroundIslandWaterfallsNotes": "Haz un pícnic junto a las cascadas de esta isla.",
"backgrounds072015": "14.ª serie: publicada en julio de 2015",
"backgroundDilatoryRuinsText": "Ruinas de Dilatoria",
"backgroundDilatoryRuinsNotes": "Sumérgete en las ruinas de Dilatoria.",
"backgroundGiantWaveText": "Ola gigante",
"backgroundGiantWaveText": "Ola Gigante",
"backgroundGiantWaveNotes": "¡Surfea una ola gigante!",
"backgroundSunkenShipText": "Barco hundido",
"backgroundSunkenShipText": "Barco Hundido",
"backgroundSunkenShipNotes": "Explora un barco hundido.",
"backgrounds082015": "15.ª serie: publicada en agosto de 2015",
"backgroundPyramidsText": "Pirámides",
"backgroundPyramidsNotes": "Admira las pirámides.",
"backgroundSunsetSavannahText": "Ocaso en la sabana",
"backgroundSunsetSavannahText": "Ocaso en la Sabana",
"backgroundSunsetSavannahNotes": "Acecha a tus presas al atardecer en la sabana.",
"backgroundTwinklyPartyLightsText": "Luces parpadeantes de fiesta",
"backgroundTwinklyPartyLightsText": "Luces Parpadeantes de Fiesta",
"backgroundTwinklyPartyLightsNotes": "¡Baila bajo las luces festivas centelleantes!",
"backgrounds092015": "16.ª serie: publicada en septiembre de 2015",
"backgroundMarketText": "Mercado de Habitica",
"backgroundMarketNotes": "Compra en el mercado de Habitica.",
"backgroundMarketNotes": "Compra en el Mercado de Habitica.",
"backgroundStableText": "Establo de Habitica",
"backgroundStableNotes": "Cuida a tus monturas en el establo de Habitica.",
"backgroundStableNotes": "Cuida a tus monturas en el Establo de Habitica.",
"backgroundTavernText": "Taberna de Habitica",
"backgroundTavernNotes": "Visita la Taberna de Habitica.",
"backgrounds102015": "17.ª serie: publicada en octubre de 2015",
"backgroundHarvestMoonText": "Luna de cosecha",
"backgroundHarvestMoonText": "Luna de Cosecha",
"backgroundHarvestMoonNotes": "Ríete a carcajadas bajo la luna de cosecha.",
"backgroundSlimySwampText": "Pantano lodoso",
"backgroundSlimySwampText": "Pantano Lodoso",
"backgroundSlimySwampNotes": "Cruza con esfuerzo el pantano lodoso.",
"backgroundSwarmingDarknessText": "Criaturas de la oscuridad",
"backgroundSwarmingDarknessText": "Criaturas de la Oscuridad",
"backgroundSwarmingDarknessNotes": "Tiembla entre las criaturas de la oscuridad.",
"backgrounds112015": "18.ª serie: publicada en noviembre de 2015",
"backgroundFloatingIslandsText": "Islas flotantes",
"backgroundFloatingIslandsText": "Islas Flotantes",
"backgroundFloatingIslandsNotes": "Salta entre las islas flotantes.",
"backgroundNightDunesText": "Dunas nocturnas",
"backgroundNightDunesText": "Dunas Nocturnas",
"backgroundNightDunesNotes": "Camina tranquilamente por las dunas nocturnas.",
"backgroundSunsetOasisText": "Oasis al atardecer",
"backgroundSunsetOasisText": "Oasis al Atardecer",
"backgroundSunsetOasisNotes": "Disfruta del oasis al atardecer.",
"backgrounds122015": "19.ª serie: publicada en diciembre de 2015",
"backgroundAlpineSlopesText": "Laderas alpinas",
"backgroundAlpineSlopesText": "Laderas Alpinas",
"backgroundAlpineSlopesNotes": "Esquía en las laderas alpinas.",
"backgroundSnowySunriseText": "Amanecer nevado",
"backgroundSnowySunriseText": "Amanecer Nevado",
"backgroundSnowySunriseNotes": "Contempla el amanecer nevado.",
"backgroundWinterTownText": "Pueblo invernal",
"backgroundWinterTownText": "Pueblo Invernal",
"backgroundWinterTownNotes": "Camina deprisa por el pueblo invernal.",
"backgrounds012016": "20.ª serie: publicada en enero de 2016",
"backgroundFrozenLakeText": "Lago congelado",
"backgroundFrozenLakeText": "Lago Congelado",
"backgroundFrozenLakeNotes": "Patina sobre un lago congelado.",
"backgroundSnowmanArmyText": "Ejército de muñecos de nieve",
"backgroundSnowmanArmyText": "Ejército de Muñecos de Nieve",
"backgroundSnowmanArmyNotes": "Lidera un ejército de muñecos de nieve.",
"backgroundWinterNightText": "Noche de invierno",
"backgroundWinterNightText": "Noche de Invierno",
"backgroundWinterNightNotes": "Mira las estrellas de una noche de invierno.",
"backgrounds022016": "21.ª serie: publicada en febrero de 2016",
"backgroundBambooForestText": "Bosque de bambú",
"backgroundBambooForestText": "Bosque de Bambú",
"backgroundBambooForestNotes": "Pasea por el bosque de bambú.",
"backgroundCozyLibraryText": "Biblioteca acogedora",
"backgroundCozyLibraryText": "Biblioteca Acogedora",
"backgroundCozyLibraryNotes": "Lee en esta acogedora biblioteca.",
"backgroundGrandStaircaseText": "Gran escalinata",
"backgroundGrandStaircaseText": "Gran Escalinata",
"backgroundGrandStaircaseNotes": "Desciende por la gran escalinata.",
"backgrounds032016": "22.ª serie: publicada en marzo de 2016",
"backgroundDeepMineText": "Mina profunda",
"backgroundDeepMineText": "Mina Profunda",
"backgroundDeepMineNotes": "Encuentra metales preciosos en esta mina profunda.",
"backgroundRainforestText": "Selva tropical",
"backgroundRainforestText": "Selva Tropical",
"backgroundRainforestNotes": "Adéntrate en la selva tropical.",
"backgroundStoneCircleText": "Crómlech",
"backgroundStoneCircleNotes": "Lanza hechizos en este crómlech.",
"backgrounds042016": "23.ª serie: publicada en abril de 2016",
"backgroundArcheryRangeText": "Campo de tiro con arco",
"backgroundArcheryRangeText": "Campo de Tiro con Arco",
"backgroundArcheryRangeNotes": "Practica en este campo de tiro con arco.",
"backgroundGiantFlowersText": "Flores gigantes",
"backgroundGiantFlowersText": "Flores Gigantes",
"backgroundGiantFlowersNotes": "Diviértete sobre estas gigantescas flores.",
"backgroundRainbowsEndText": "Final del arcoíris",
"backgroundRainbowsEndText": "Final del Arcoíris",
"backgroundRainbowsEndNotes": "Encuentra oro al final del arcoíris.",
"backgrounds052016": "24.ª serie: publicada en mayo de 2016",
"backgroundBeehiveText": "Colmena",
"backgroundBeehiveNotes": "Zumba y baila en una colmena.",
"backgroundGazeboText": "Kiosko",
"backgroundGazeboNotes": "Pelea en un kiosko.",
"backgroundTreeRootsText": "Raíces de árbol",
"backgroundTreeRootsText": "Raíces de Árbol",
"backgroundTreeRootsNotes": "Explora las raíces del árbol.",
"backgrounds062016": "25.ª serie: publicada en junio de 2016",
"backgroundLighthouseShoreText": "Costa con faro",
"backgroundLighthouseShoreText": "Costa con Faro",
"backgroundLighthouseShoreNotes": "Pasea por la costa junto al faro.",
"backgroundLilypadText": "Nenúfar",
"backgroundLilypadNotes": "Salta sobre un nenúfar.",
"backgroundWaterfallRockText": "Roca de cascada",
"backgroundWaterfallRockText": "Roca de Cascada",
"backgroundWaterfallRockNotes": "Chapotea junto a la roca de la cascada.",
"backgrounds072016": "26.ª serie: publicada en julio de 2016",
"backgroundAquariumText": "Acuario",
"backgroundAquariumNotes": "Sube y baja dentro de este acuario.",
"backgroundDeepSeaText": "Profundidades del mar",
"backgroundDeepSeaText": "Mar Profundo",
"backgroundDeepSeaNotes": "Bucea hasta las profundidades del mar.",
"backgroundDilatoryCastleText": "Castillo de Dilatoria",
"backgroundDilatoryCastleNotes": "Nada junto al Castillo de Dilatoria.",
"backgrounds082016": "27.ª serie: publicada en agosto de 2016",
"backgroundIdyllicCabinText": "Cabaña bucólica",
"backgroundIdyllicCabinNotes": "Retírate a una cabaña bucólica.",
"backgroundMountainPyramidText": "Pirámide de montaña",
"backgroundIdyllicCabinText": "Cabaña Idílica",
"backgroundIdyllicCabinNotes": "Retírate a una cabaña idílica.",
"backgroundMountainPyramidText": "Pirámide de Montaña",
"backgroundMountainPyramidNotes": "Sube los incontables peldaños de esta pirámide de montaña.",
"backgroundStormyShipText": "Barco tormentoso",
"backgroundStormyShipText": "Barco Tormentoso",
"backgroundStormyShipNotes": "Agárrate fuerte contra viento y marea a bordo de un barco tormentoso.",
"backgrounds092016": "28.ª serie: publicada en septiembre de 2016",
"backgroundCornfieldsText": "Campos de maíz",
"backgroundCornfieldsText": "Campos de Maíz",
"backgroundCornfieldsNotes": "Disfruta de un bonito día en los campos de maíz.",
"backgroundFarmhouseText": "Casa de granja",
"backgroundFarmhouseText": "Casa de Granja",
"backgroundFarmhouseNotes": "Saluda a los animales de camino a la casa de la granja.",
"backgroundOrchardText": "Huerto de árboles frutales",
"backgroundOrchardText": "Huerto de Árboles Frutales",
"backgroundOrchardNotes": "Coge fruta madura en el huerto.",
"backgrounds102016": "29.ª serie: publicada en octubre de 2016",
"backgroundSpiderWebText": "Telaraña",
@@ -207,14 +207,14 @@
"backgroundRainyCityText": "Ciudad Lluviosa",
"backgroundRainyCityNotes": "Chapotea a través de una Ciudad Lluviosa.",
"backgrounds112016": "30.ª serie: publicada en noviembre de 2016",
"backgroundMidnightCloudsText": "Media noche nublada",
"backgroundMidnightCloudsNotes": "Vuela a través de la Media noche nublada.",
"backgroundStormyRooftopsText": "Tempestuoso Techo",
"backgroundStormyRooftopsNotes": "Deslízate a través del Tempestuoso Techo.",
"backgroundMidnightCloudsText": "Nubes de Medianoche",
"backgroundMidnightCloudsNotes": "Vuela a través de las nubes de medianoche.",
"backgroundStormyRooftopsText": "Techos Tormentosos",
"backgroundStormyRooftopsNotes": "Deslízate a través de los techos tormentosos.",
"backgroundWindyAutumnText": "Otoño Ventoso",
"backgroundWindyAutumnNotes": "Caza las hojas durante el Otoño Ventoso.",
"backgroundWindyAutumnNotes": "Persigue las hojas durante el otoño ventoso.",
"incentiveBackgrounds": "Fondos Estándar",
"backgroundVioletText": "violeta",
"backgroundVioletText": "Violeta",
"backgroundVioletNotes": "Un vibrante fondo violeta.",
"backgroundBlueText": "Azul",
"backgroundBlueNotes": "Un fondo básico azul.",
@@ -228,16 +228,16 @@
"backgroundYellowNotes": "Un agradable fondo amarillo.",
"backgrounds122016": "31.ª serie: publicada en diciembre de 2016",
"backgroundShimmeringIcePrismText": "Prismas de Hielo Relucientes",
"backgroundShimmeringIcePrismNotes": "Baila junto a los Prismas de Hielo Relucientes.",
"backgroundShimmeringIcePrismNotes": "Baila junto a los prismas de hielo relucientes.",
"backgroundWinterFireworksText": "Fuegos Artificiales de Invierno",
"backgroundWinterFireworksNotes": "Lanza los Fuegos Artificiales de Invierno.",
"backgroundWinterStorefrontText": "Tienda Invernal",
"backgroundWinterStorefrontNotes": "Compra regalos en la Tienda Invernal.",
"backgrounds012017": "32.ª serie: publicada en enero de 2017",
"backgroundBlizzardText": "Tormenta de Nieve",
"backgroundBlizzardNotes": "Enfréntate a una terrible Tormenta de Nieve.",
"backgroundBlizzardNotes": "Enfréntate a una terrible tormenta de nieve.",
"backgroundSparklingSnowflakeText": "Copo de Nieve Chispeante",
"backgroundSparklingSnowflakeNotes": "Deslízate en un Copo de Nieve Chispeante.",
"backgroundSparklingSnowflakeNotes": "Deslízate en un copo de nieve chispeante.",
"backgroundStoikalmVolcanoesText": "Volcanes Stoïkalm",
"backgroundStoikalmVolcanoesNotes": "Explora los Volcanes Stoïkalm.",
"backgrounds022017": "33.ª serie: publicada en febrero de 2017",
@@ -246,10 +246,10 @@
"backgroundTreasureRoomText": "Sala del Tesoro",
"backgroundTreasureRoomNotes": "Disfruta de la riqueza de la Sala del Tesoro.",
"backgroundWeddingArchText": "Arco de Boda",
"backgroundWeddingArchNotes": "Posa bajo el Arco de Boda.",
"backgroundWeddingArchNotes": "Posa bajo el arco de boda.",
"backgrounds032017": "34.ª serie: publicada en marzo de 2017",
"backgroundMagicBeanstalkText": "Tallo de Judía Mágico",
"backgroundMagicBeanstalkNotes": "Sube por un Tallo de Judía Mágico.",
"backgroundMagicBeanstalkNotes": "Sube por un tallo de judía mágico.",
"backgroundMeanderingCaveText": "Cuerva Serpenteante",
"backgroundMeanderingCaveNotes": "Explora la Cueva Serpenteante.",
"backgroundMistiflyingCircusText": "Desconcertante Circo Volador",
@@ -260,47 +260,47 @@
"backgroundGiantBirdhouseText": "Casa de Pájaros Gigante",
"backgroundGiantBirdhouseNotes": "Posarse sobre la Casa de Pájaros Gigante.",
"backgroundMistShroudedMountainText": "Montaña Envuelta en Niebla",
"backgroundMistShroudedMountainNotes": "Escala la Montaña Envuelta en Niebla.",
"backgroundMistShroudedMountainNotes": "Escala la montaña envuelta en niebla.",
"backgrounds052017": "36.ª serie: publicada en mayo de 2017",
"backgroundGuardianStatuesText": "Estatuas Guardianes",
"backgroundGuardianStatuesNotes": "Quédate en vigilia frente a Estatuas Guardianes.",
"backgroundHabitCityStreetsText": "Calles de la Ciudad de los Hábitos",
"backgroundHabitCityStreetsNotes": "Explora las Calles de la Ciudad de los Hábitos.",
"backgroundOnATreeBranchText": "Sobre Una Rama de un Árbol",
"backgroundOnATreeBranchNotes": "Pósate sobre una Rama de un Árbol.",
"backgroundOnATreeBranchNotes": "Pósate sobre una rama de un árbol.",
"backgrounds062017": "37.ª serie: publicada en junio de 2017",
"backgroundBuriedTreasureText": "Tesoro Enterrado",
"backgroundBuriedTreasureNotes": "Desentierra el Tesoro Enterrado.",
"backgroundBuriedTreasureNotes": "Desentierra el tesoro enterrado.",
"backgroundOceanSunriseText": "Amanecer Oceánico",
"backgroundOceanSunriseNotes": "Admira el Amanecer Oceánico.",
"backgroundOceanSunriseNotes": "Admira el amanecer oceánico.",
"backgroundSandcastleText": "Castillo de Arena",
"backgroundSandcastleNotes": "Rige sobre el Castillo de Arena.",
"backgroundSandcastleNotes": "Rige sobre el castillo de arena.",
"backgrounds072017": "38.ª serie: publicada en julio de 2017",
"backgroundGiantSeashellText": "Concha Gigante",
"backgroundGiantSeashellNotes": "Reposa en la Concha Gigante.",
"backgroundGiantSeashellNotes": "Reposa en la concha gigante.",
"backgroundKelpForestText": "Bosque de Algas Marinas",
"backgroundKelpForestNotes": "Nada a través del Bosque de Algas Marinas.",
"backgroundKelpForestNotes": "Nada a través del bosque de algas marinas.",
"backgroundMidnightLakeText": "Lago Medianoche",
"backgroundMidnightLakeNotes": "Descansa junto al Lago Medianoche.",
"backgrounds082017": "39.ª serie: publicada en agosto de 2017",
"backgroundBackOfGiantBeastText": "Espalda de una Bestia Gigante",
"backgroundBackOfGiantBeastNotes": "Cabalga en la Espalda de una Bestia Gigante.",
"backgroundBackOfGiantBeastNotes": "Cabalga en la espalda de una bestia gigante.",
"backgroundDesertDunesText": "Dunas del Desierto",
"backgroundDesertDunesNotes": "Explora valientemente las Dunas del Desierto.",
"backgroundDesertDunesNotes": "Explora valientemente las dunas del desierto.",
"backgroundSummerFireworksText": "Fuegos Artificiales de Verano",
"backgroundSummerFireworksNotes": "¡Celebra el Día del Nombramiento de Habitica con Fuegos Artificiales de Verano!",
"backgrounds092017": "40.ª serie: publicada en septiembre de 2017",
"backgroundBesideWellText": "Al lado de un Pozo",
"backgroundBesideWellNotes": "Paseo junto a un Pozo.",
"backgroundBesideWellNotes": "Paseo junto a un pozo.",
"backgroundGardenShedText": "Cobertizo de Jardín",
"backgroundGardenShedNotes": "Trabajo en un Cobertizo de Jardín.",
"backgroundGardenShedNotes": "Trabaja en un cobertizo de jardín.",
"backgroundPixelistsWorkshopText": "Taller de Pixelist",
"backgroundPixelistsWorkshopNotes": "Crea obras maestras en el Taller de Pixelist.",
"backgrounds102017": "41.ª serie: publicada en octubre de 2017",
"backgroundMagicalCandlesText": "Velas Mágicas",
"backgroundMagicalCandlesNotes": "Déjate acariciar por la luz de Velas Mágicas.",
"backgroundMagicalCandlesNotes": "Déjate acariciar por la luz de velas mágicas.",
"backgroundSpookyHotelText": "Hotel Escalofriante",
"backgroundSpookyHotelNotes": "Infíltrate por el vestíbulo de un Hotel Escalofriante.",
"backgroundSpookyHotelNotes": "Infíltrate por el vestíbulo de un hotel escalofriante.",
"backgroundTarPitsText": "Pozos de Alquitrán",
"backgroundTarPitsNotes": "Pasa de puntillas por los Pozos de Alquitrán.",
"backgrounds112017": "42.ª serie: publicada en noviembre de 2017",
@@ -314,16 +314,16 @@
"backgroundCrosscountrySkiTrailText": "Ruta de Ski Todo-Terreno",
"backgroundCrosscountrySkiTrailNotes": "Planea sobre la Ruta de Ski Todo-Terreno.",
"backgroundStarryWinterNightText": "Noche de Invierno Estrellada",
"backgroundStarryWinterNightNotes": "Noche de Invierno Estrellada.",
"backgroundStarryWinterNightNotes": "Admira una noche de invierno estrellada.",
"backgroundToymakersWorkshopText": "Taller del Fabricante de Juguetes",
"backgroundToymakersWorkshopNotes": "Disfrutar las maravillas del Taller del Fabricante de Juguetes.",
"backgrounds012018": "44.ª serie: publicada en enero de 2018",
"backgroundAuroraText": "Aurora",
"backgroundAuroraNotes": "Disfrutar del brillo invernal de la Aurora.",
"backgroundAuroraNotes": "Disfruta del brillo invernal de la aurora.",
"backgroundDrivingASleighText": "Trineo",
"backgroundDrivingASleighNotes": "Conduce un Trineo sobre terreno nevado.",
"backgroundDrivingASleighNotes": "Conduce un trineo sobre terreno nevado.",
"backgroundFlyingOverIcySteppesText": "Estepas heladas",
"backgroundFlyingOverIcySteppesNotes": "Volar sobre las Estepas Heladas.",
"backgroundFlyingOverIcySteppesNotes": "Vuela sobre las estepas heladas.",
"backgrounds022018": "45.ª serie: publicada en febrero de 2018",
"backgroundChessboardLandText": "Tierra de Tablero de Ajedrez",
"backgroundChessboardLandNotes": "Juega una partida en la Tierra de Tablero de Ajedrez.",
@@ -333,16 +333,16 @@
"backgroundRoseGardenNotes": "Pasear por el Jardín de Rosas.",
"backgrounds032018": "45.ª serie: publicada en marzo de 2018",
"backgroundGorgeousGreenhouseText": "Espléndido Invernadero",
"backgroundGorgeousGreenhouseNotes": "Camina entre la flora que se cuida en el Espléndido Invernadero.",
"backgroundGorgeousGreenhouseNotes": "Camina entre la flora que se cuida en un espléndido invernadero.",
"backgroundElegantBalconyText": "Elegante Balcón",
"backgroundElegantBalconyNotes": "Admira el paisaje desde un Elegante Balcón.",
"backgroundElegantBalconyNotes": "Admira el paisaje desde un elegante balcón.",
"backgroundDrivingACoachText": "Conduciendo una Carroza",
"backgroundDrivingACoachNotes": "Disfruta Conducir una Carroza por campos de flores.",
"backgroundDrivingACoachNotes": "Disfruta conducir una carroza por campos de flores.",
"backgrounds042018": "47.ª serie: publicada en abril de 2018",
"backgroundTulipGardenText": "Jardín de Tulipanes",
"backgroundTulipGardenNotes": "Pasa de puntillas por un Jardín de Tulipanes.",
"backgroundTulipGardenNotes": "Pasa de puntillas por un jardín de tulipanes.",
"backgroundFlyingOverWildflowerFieldText": "Campo de Flores silvestres",
"backgroundFlyingOverWildflowerFieldNotes": "Elévate por encima de un Campo de Flores silvestres.",
"backgroundFlyingOverWildflowerFieldNotes": "Elévate por encima de un campo de flores silvestres.",
"backgroundFlyingOverAncientForestText": "Bosque Antiguo",
"backgroundFlyingOverAncientForestNotes": "Vuela por encima de las copas de los árboles de un bosque antiguo.",
"backgrounds052018": "48.ª serie: publicada en mayo de 2018",
@@ -355,19 +355,19 @@
"backgrounds062018": "49.ª serie: publicada en junio de 2018",
"backgroundDocksText": "Muelle",
"backgroundDocksNotes": "Pescado de lo alto del muelle.",
"backgroundRowboatText": "Bote de remos",
"backgroundRowboatText": "Bote de Remos",
"backgroundRowboatNotes": "Canta en un bote de remos.",
"backgroundPirateFlagText": "Bandera pirata",
"backgroundPirateFlagText": "Bandera Pirata",
"backgroundPirateFlagNotes": "Cuelga una temible bandera pirata.",
"backgrounds072018": "50.ª serie: publicada en julio de 2018",
"backgroundDarkDeepText": "Oscuras profundidades",
"backgroundDarkDeepText": "Oscuras Profundidades",
"backgroundDarkDeepNotes": "Nada en las oscuras profundidades entre bichos bioluminiscentes.",
"backgroundDilatoryCityText": "Ciudad de Dilatoria",
"backgroundDilatoryCityNotes": "Deambula a través de la ciudad submarina de Dilatoria.",
"backgroundTidePoolText": "Poza de marea",
"backgroundTidePoolText": "Poza de Marea",
"backgroundTidePoolNotes": "Observa la vida del océano cerca de una poza de marea.",
"backgrounds082018": "51.ª serie: publicada en agosto de 2018",
"backgroundTrainingGroundsText": "Campos de entrenamiento",
"backgroundTrainingGroundsText": "Campos de Entrenamiento",
"backgroundTrainingGroundsNotes": "Practica en los campos de entrenamiento.",
"backgroundFlyingOverRockyCanyonText": "Cañón Rocoso",
"backgroundFlyingOverRockyCanyonNotes": "Contempla un escenario que quita el aliento mientras vuelas sobre un Cañón rocoso.",
@@ -375,9 +375,9 @@
"backgroundBridgeNotes": "Cruza un Puente encantador.",
"backgrounds092018": "52.ª serie: publicada en septiembre de 2018",
"backgroundApplePickingText": "Colecta de Manzanas",
"backgroundApplePickingNotes": "Ve a Recolectar Manzanas y trae a casa unas cuantas.",
"backgroundApplePickingNotes": "Ve a recolectar manzanas y trae a casa unas cuantas.",
"backgroundGiantBookText": "Libro Gigante",
"backgroundGiantBookNotes": "Lee mientras paseas por las páginas de un Libro Gigante.",
"backgroundGiantBookNotes": "Lee mientras paseas por las páginas de un libro gigante.",
"backgroundCozyBarnText": "Granero Confortable",
"backgroundCozyBarnNotes": "Relájate con tus mascotas y monturas en su Confortable Granero.",
"backgrounds102018": "53.ª serie: publicada en octubre de 2018",
@@ -930,5 +930,16 @@
"backgroundWinterDesertWithSaguarosNotes": "Exalta tus sentidos entre los Cactus en Desierto Invernal.",
"backgrounds022026": "CONJUNTO 141: Publicado en Febrero 2026",
"backgroundElegantPalaceText": "Palacio Elegante",
"backgroundElegantPalaceNotes": "Quédate obnubilado por las coloridas salas del Palacio Elegante."
"backgroundElegantPalaceNotes": "Quédate obnubilado por las coloridas salas del Palacio Elegante.",
"backgrounds032026": "142.ª serie: publicada en marzo de 2026",
"backgroundWaterfallWithRainbowText": "Cascada y Arcoíris",
"backgroundWaterfallWithRainbowNotes": "Admira la belleza cautivadora de una cascada con un arcoíris.",
"backgrounds042026": "143.ª serie: publicada en abril de 2026",
"backgroundRidingACometText": "Montando un Cometa",
"backgroundRidingACometNotes": "¡Viaja a través del espacio mientras montas un cometa!",
"backgrounds052026": "144.ª serie: publicada en mayo de 2026",
"backgroundElvenCitadelText": "Ciudadela Élfica",
"backgroundElvenCitadelNotes": "Toma un pintoresto recorrido en una ciudadela élfica.",
"backgroundOnAStrangePlanetText": "En un Extraño Planeta",
"backgroundOnAStrangePlanetNotes": "Aventúrate allá donde ningun Habiticano ha viajado antes: hacia un extraño planeta."
}
+69 -15
View File
@@ -3,14 +3,14 @@
"equipmentType": "Tipo",
"klass": "Clase",
"groupBy": "Agrupar por <%= type %>",
"classBonus": "(Este equipamiento es de tu clase por lo que gana un multiplicador de 1,5 a sus atributos)",
"classArmor": "Armadura de clase",
"featuredset": "Conjunto destacado: <%= name %>",
"mysterySets": "Conjuntos misteriosos",
"classBonus": "(Este equipamiento es de tu clase por lo que gana un multiplicador adicional de 1.5 a sus atributos)",
"classArmor": "Armadura de Clase",
"featuredset": "Conjunto Destacado: <%= name %>",
"mysterySets": "Conjuntos Misteriosos",
"gearNotOwned": "No tienes este objeto.",
"noGearItemsOfType": "No tienes ninguno de estos.",
"classLockedItem": "Este equipamiento solo está disponible para una clase específica. ¡A partir del nivel 10, cambia tu clase pulsando: Icono de Usuario > Ajustes > Creación de personaje!",
"tierLockedItem": "Este objeto solo estará disponible una vez hayas comprado los anteriores de la secuencia. ¡Sigue trabajando duro para llegar hasta él!",
"classLockedItem": "Este equipamiento solo está disponible para una clase específica. ¡A partir del nivel 10, puedes cambiar tu clase pulsando: Icono de Usuario > Ajustes > Creación de personaje!",
"tierLockedItem": "Este objeto solo estará disponible una vez hayas comprado los anteriores en orden. ¡Sigue trabajando duro para llegar hasta él!",
"sortByType": "Tipo",
"sortByPrice": "Precio",
"sortByCon": "CON",
@@ -18,16 +18,16 @@
"sortByStr": "FUE",
"sortByInt": "INT",
"weapon": "arma",
"weaponCapitalized": "Objeto de la mano dominante",
"weaponBase0Text": "Sin arma",
"weaponBase0Notes": "Sin arma.",
"weaponWarrior0Text": "Espada de entrenamiento",
"weaponCapitalized": "Objeto Mano Dominante",
"weaponBase0Text": "Sin Arma",
"weaponBase0Notes": "Sin Arma.",
"weaponWarrior0Text": "Espada de Entrenamiento",
"weaponWarrior0Notes": "Arma de práctica. No otorga ningún beneficio.",
"weaponWarrior1Text": "Espada",
"weaponWarrior1Notes": "Espada común de un soldado. Aumenta la Fuerza en <%= str %>.",
"weaponWarrior2Text": "Hacha",
"weaponWarrior2Notes": "Arma cortante de doble filo. Aumenta la fuerza en <%= str %>.",
"weaponWarrior3Text": "Lucero del alba",
"weaponWarrior3Text": "Lucero del Alba",
"weaponWarrior3Notes": "Maza pesada con brutales espinas. Aumenta la Fuerza en <%= str %>.",
"weaponWarrior4Text": "Espada de Zafiro",
"weaponWarrior4Notes": "Espada cuyos filos cortan como el viento del norte. Aumenta la Fuerza en <%= str %>.",
@@ -3108,9 +3108,9 @@
"headSpecialSummer2024MageNotes": "Este sombrero se mece suavemente en las corrientes del océano a la vez que te asiste para canalizar tu gran sabiduría. Aumenta la percepcion en <%= per %>. Equipamiento de edición limitada Verano 2024.",
"headArmoireCorsairsBandanaNotes": "Este pañuelo badana es esencial ya sea que desees mantener tu cabeza protegida de alguna gaviota que volando sobre ella suelte la carga o por que no quieras que la tripulación vea que estas sudando nerviosamente y decida amotinarse. Eso si, añade una cuenta decorativa cada vez que completes una de tus aventuras. Aumenta la Inteligencia en <%= int %>. Armario Encantado: Conjunto de Corsario (Artículo 2 de 3)",
"shieldSpecialSummer2024WarriorNotes": "Jajaja, para todos aquellos que dicen que no eres capaz de alcanzar tus objetivos, mirad: ¡decídselo a mi mano, quiero decir, jeje, aleta! Aumenta la Constitución en <%= con %>. Equipamiento de edición limitada Verano 2024.",
"gearItemsCompleted": "¡Ya tienes todo el equipamiento de <%= klass %>! Se publicará nuevo equipamiento en las Galas estaciónales.",
"gearItemsCompleted": "¡Ya tienes todo el equipamiento de <%= klass %>! Nuevo equipamiento se publica durante las Galas estaciónales.",
"moreArmoireGearAvailable": "¡Hasta entonces, hay aún <%= armoireCount %> piezas de equipamiento que puedes encontrar en el Armario Encantado!",
"moreArmoireGearComing": También en el Armario Encantado tienes nuevos objetos cada mes!",
"moreArmoireGearComing": El Armario Encantado también obtiene objetos nuevos cada mes!",
"weaponSpecialSummer2024RogueText": "Tridente de Nudibranquio",
"weaponSpecialSummer2024MageText": "Varita de Anémona de Mar",
"weaponSpecialSummer2024MageNotes": "Estos tentáculos terroríficos poseen la habilidad de disipar, desviar y dirigir la magia indistintamente. Aumenta la inteligencia en <%= int %> y la percepción en <%= per %>. Equipamiento de edición limitada Verano 2024.",
@@ -3484,7 +3484,7 @@
"headMystery202512Text": "Yelmo de Galleta Ganadora",
"headMystery202512Notes": "Pan de jengibre forjado con números hechizos arcanos que acabaron con la cordura de sus conjuradores, protección mística ¡siempre y cuando no te lo comas! No otorga ningún beneficio. Artículo de Suscriptor Diciembre 2025.",
"headMystery202602Text": "Orejas de Zorro del Sakura",
"headMystery202602Notes": " Tu percepción de los sonidos será tan aguzada debido a estas orejas, que podrás escuchar no solo una mano aplaudiendo sola, si no también el florecimiento de las hermosas florecillas cuando la primavera se vaya acercando, florecilla. No otorga ningún beneficio. Artículo de Suscriptor Febrero 2026.",
"headMystery202602Notes": "Con estas orejas tu oído se afilará tanto que podrás escuchar el brote de las flores en las ramas de los árboles al acercarse la primavera. No otorga ningún beneficio. Artículo de Suscriptor de febrero 2026.",
"headArmoireLoneCowpokeHatText": "Sombrero de Vaquero Solitario",
"shieldSpecialWinter2026WarriorText": "Escudo Escarcha",
"shieldSpecialWinter2026HealerText": "Explosión Estelar",
@@ -3495,5 +3495,59 @@
"backMystery202601Text": "Símbolo Invernal",
"backMystery202602Text": "Las Cinco Colas de Sakura",
"backArmoireHarpsichordText": "Clavicémbalo",
"backArmoireHarpsichordNotes": "¡Duing! ¡Ding! Reúne a tu equipo ya sea para un almuerzo campestre o un picnic, algo totalmente diferente a lo anterior, y una vez allí deleita sus pabellones auditivos mientras interpretas Highway to Hell en tu clavicémbalo. Aumenta la Percepción y la Inteligencia en <%= attrs %> cada uno.Armario Encantado: Conjunto Instrumentos Musicales 2 (Artículo 1 de 3)"
"backArmoireHarpsichordNotes": "¡Duing! ¡Ding! Reúne a tu equipo ya sea para un almuerzo campestre o un picnic, algo totalmente diferente a lo anterior, y una vez allí deleita sus pabellones auditivos mientras interpretas Highway to Hell en tu clavicémbalo. Aumenta la Percepción y la Inteligencia en <%= attrs %> cada uno.Armario Encantado: Conjunto Instrumentos Musicales 2 (Artículo 1 de 3)",
"weaponSpecialSpring2026WarriorText": "Florete de la Poderosa Rana",
"weaponSpecialSpring2026RogueText": "Rama Primaveral",
"weaponSpecialSpring2026HealerText": "Bastón de Campanilla de Invierno",
"weaponSpecialSpring2026MageText": "Parasol Palo de Mayo",
"weaponSpecialSpring2026MageNotes": "Se acerca una ocasión para celebrar, ¡y con este bonito palo-parasol estarás preparado! Aumenta la Inteligencia en <%= int %> y la Percepción en <%= per %>. Equipamiento de Edición Limitada de Primavera 2026.",
"armorSpecialSpring2026WarriorText": "Armadura Rana",
"armorSpecialSpring2026WarriorNotes": "Salta a la acción tan pronto como la nieve empieza a derretirse. Aumenta la Constitución en <%= con %>. Equipamiento de Edición Limitada de Primavera 2026.",
"armorSpecialSpring2026RogueText": "Armadura de Corteza de Abedul",
"armorSpecialSpring2026RogueNotes": "Resiste las inevitables lluvias primaverales y las brisas ligeras. Aumenta la Percepción en <%= per %>. Equipamiento de Edición Limitada de Primavera 2026.",
"armorSpecialSpring2026HealerText": "Vestido de Campanilla de Invierno",
"armorSpecialSpring2026MageText": "Traje de Bailarín del Palo de Mayo",
"armorSpecialSpring2026MageNotes": "Llega preparado para bailar, hacer un picnic y disfrutar del clima cálido que trae la primavera. Aumenta la Inteligencia en <%= int %>. Equipamiento de Edición Limitada de Primavera 2026.",
"armorMystery202604Text": "Traje Espacial del Audaz Astronauta",
"armorMystery202604Notes": "Un pequeño paso para tus Tareas Pendientes, ¡un gran salto para tu satisfacción personal! No otorga ningún beneficio. Artículo de Suscriptor de abril de 2026.",
"armorArmoireHandstandOutfitText": "Parado de manos",
"armorArmoireHandstandOutfitNotes": "Las cosas definitvamente se ven de otra manera cuando estás boca abajo, no es así? Si te sientes atrapado, ¡es tiempo de una nueva perspectiva! Aumenta la Percepción en <%= per %>. Armario Encantado: Conjunto Parado de Manos (Artículo 1 de 1).",
"headSpecialSpring2026RogueText": "Casco Rama Primaveral",
"headSpecialSpring2026HealerText": "Casco de Campanilla de Invierno",
"headSpecialSpring2026HealerNotes": "Haz una esperanzadora declaración con estos hermosos, resistentes pétalos. Aumenta la Inteligencia en <%= int %>. Equipamiento de Edición Limitada de Primavera 2026.",
"headSpecialSpring2026MageText": "Corona de Flor de Mayo",
"headMystery202603Text": "Sombrero de Mago de las Glicinias",
"headMystery202604Text": "Casco del Audaz Astronauta",
"headArmoireFloppyYellowHatText": "Sombrero Flexible Amarillo",
"headArmoireVerdantArmingCapText": "Cofia Acolchada del Paje Verdoso",
"shieldSpecialSpring2026RogueText": "Rama Primaveral",
"shieldSpecialSpring2026RogueNotes": "Alcanza tus límites con estas ramas. También sirven como rascador de espalda en caso de necesitarlo. Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Primavera 2026.",
"shieldSpecialSpring2026HealerText": "Hoja de Campanilla de Invierno",
"shieldMystery202605Text": "Escudo del Anochecer",
"shieldArmoireSoftYellowPillowText": "Almohada Amarillo Suave",
"shieldArmoireVerdantBannerText": "Estandarte del Paje Verde",
"shieldArmoireVerdantBannerNotes": "¡Ondea alto tu estandarte para indicar a tus amigos que es hora de reunirse! Aumenta la Inteligencia en <%= int %>. Armario Encantado: Conjunto del Paje Verdoso (Artículo 2 de 2).",
"backMystery202605Text": "Nimbus del Anochecer",
"weaponSpecialSpring2026RogueNotes": "Se acerca una gran oportunidad para crecer, ¡y con estas ramas en ciernes estarás preparado! Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Primavera 2026.",
"weaponSpecialSpring2026WarriorNotes": "Puede que se presente una oportunidad para batirse en duelo en cualquier momento, ¡y con este formidable florete estarás preparado! Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Primavera 2026.",
"weaponSpecialSpring2026HealerNotes": "Se presenta una oportunidad para empezar de nuevo con un fresco comienzo, ¡y con este espléndido bastón estarás preparado! Aumenta la Inteligencia en <%= int %>. Equipamiento de Edición Limitada de Primavera 2026.",
"weaponMystery202603Text": "Bastón de Mago de las Glicinias",
"weaponMystery202603Notes": "¡Lanza hechizos para calentar el aire primaveral y favorecer la floración! No otorga ningún beneficio. Artículo de Suscriptor de marzo de 2026.",
"armorSpecialSpring2026HealerNotes": "Deslízate con gracia desde un invierno frío y oscuro hasta una primavera gloriosa. Aumenta la Constitución en <%= con %>. Equipamiento de Edición Limitada de Primavera 2026.",
"armorArmoireSoftYellowSuitText": "Traje Amarillo Suave",
"armorArmoireSoftYellowSuitNotes": "El amarillo es un color energético. Ponte esto a la hora de dormir, y te despertarás con el Sol, listo para enfrentarte a un día atareado. Aumenta la Constitución y la Fuerza en <%= attrs %>. Armario Encantado: Conjunto Ropa de Casa Amarilla (Artículo 2 de 3).",
"headSpecialSpring2026WarriorText": "Casco del Guerrero Rana",
"headSpecialSpring2026WarriorNotes": "Las ranas son famosas por su resistencia a la corrupción. ¡Este casco te otorgará sus nobles cualidades! Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Primavera 2026.",
"headSpecialSpring2026RogueNotes": "Haz una notable declaración con ramitas y brotes creciendo de forma salvaje en todas direcciones. Aumenta la Percepción en <%= per %>. Equipamiento de Edición Limitada de Primavera 2026.",
"headSpecialSpring2026MageNotes": "Haz una alegre declaración con brillantes flores rodeando tu cabeza. Aumenta la Percepción en <%= per %>. Equipamiento de Edición Limitada de Primavera 2026.",
"headMystery202603Notes": "Este coqueto sombrero no solo potencia tus habilidades mágicas, ¡sino que además tiene un adorable aroma primaveral! No otorga ningún beneficio. Artículo de Suscriptor de marzo 2026.",
"headMystery202604Notes": "En el espacio, nadie puede oirte completando tus Tareas Pendientes. ¡Pero la verdadera recompensa es la satisfacción personal! No otorga ningún beneficio. Artículo de Suscriptor de abril 2026.",
"backMystery202605Notes": "Un halo resplandeciente de luz de luna y estrellas para iluminar la noche más oscura. No confiere ningún beneficio. Artículo de Suscriptor de mayo 2026.",
"headArmoireFloppyYellowHatNotes": "Muchos encantamientos han sido cosidos a este simple sombrero, confiriéndole un juvenil color amarillo. Aumenta todas las estadísticas en <%= attrs %>. Armario Encantado: Conjunto Ropa de Casa Amarilla (Artículo 1 de 3).",
"headArmoireVerdantArmingCapNotes": "Esta cómoda, acolchada cofia te prepara para la batalla y te ayuda a resistir cualquier cosa pesada que se te presente. Aumenta la Percepción y la Constitución en <%= attrs %>. Armario Encantado: Conjunto del Paje Verdoso (Artículo 1 de 2).",
"shieldSpecialSpring2026WarriorText": "Candelabro del Guerrero Rana",
"shieldSpecialSpring2026WarriorNotes": "Este candelabro no sólo puede iluminar tu camino; también puedes usarlo para derretir cualquier resto de nieve y hielo. Aumenta la Constitución en <%= con %>. Equipamiento de Edición Limitada de Primavera 2026.",
"shieldSpecialSpring2026HealerNotes": "Crea una brisa suave con este ventilador a medida que suben las temperaturas. También sirve como bolígrafo en caso de necesitarlo. Aumenta la Constitución en <%= con %>. Equipamiento de Edición Limitada de Primavera 2026.",
"shieldMystery202605Notes": "Deja que la brillante luz de luna te proteja de los peligros en la oscuridad. No otorga ningún beneficio. Artículo de Suscriptor de mayo 2026.",
"shieldArmoireSoftYellowPillowNotes": "El guerrero experimentado lleva una almohada en cada expedición. Crece y brilla mientras consolidas todo lo aprendido en aventuras pasadas… incluso mientras duermes la siesta. Aumenta la Inteligencia y la Percepción en <%= attrs %>. Armario Encantado: Conjunto Ropa de Casa Amarilla (Artículo 3 de 3)."
}
+13 -1
View File
@@ -428,5 +428,17 @@
"groupTeacher": "Usándolo para dar clase",
"groupParentChildren": "Usándolo con mi familia",
"groupPlanBillingFYI": "Las suscripciones de Planes de Grupo se renuevan automáticamente a no ser que la canceles al menos 24 horas antes del fin del periodo en curso. Puedes cancelarla desde la pestaña Facturación de Grupo en tu Plan de Grupo. Se te cargará dentro de las 24 horas en las que renueva la suscripción y basado en el número de miembros de tu grupo en el Plan de Grupo en ese mismo momento. Si añades miembros entre períodos de pago, habrá un cargo adicional prorrateado por los beneficios obtenidos en tu siguiente ciclo de facturación.",
"groupPlanBillingFYIShort": "Las suscripciones de Plan de Grupo se renuevan automáticamente a no ser que se cancelen 24 horas antes del fin del periodo actual. Se te cargará dentro de las 24 horas antes de la renovación de la suscripción, basado en el número de miembros de tu Plan de Grupo en ese mismo momento. Si añades miembros entre períodos de pago, se te aplicará un pago adicional prorrateado por los beneficios obtenidos en tu siguiente ciclo de facturación."
"groupPlanBillingFYIShort": "Las suscripciones de Plan de Grupo se renuevan automáticamente a no ser que se cancelen 24 horas antes del fin del periodo actual. Se te cargará dentro de las 24 horas antes de la renovación de la suscripción, basado en el número de miembros de tu Plan de Grupo en ese mismo momento. Si añades miembros entre períodos de pago, se te aplicará un pago adicional prorrateado por los beneficios obtenidos en tu siguiente ciclo de facturación.",
"createNewGroup": "Crear un Nuevo Grupo",
"upgradeExistingGroup": "Mejorar un Grupo Existente",
"chooseAnOption": "Elegir una Opción",
"yourParty": "Tu Equipo",
"previouslyUpgradedGroup": "Grupo previamente mejorado",
"inviteOthersForAdditional": "Invita a otros a tu Grupo por un adicional de",
"perMember": "por miembro",
"oneMember": "1 miembro",
"membersCount": "<%= count %> miembros",
"pendingCount": "(<%= count %> pendiente)",
"upgradeCancelsPendingInvites": "Mejorar tu Equipo cancelará todas las invitaciones pendientes",
"additionalMembersProrated": "Miembros adicionales invitados durante el mes se añadirán al total del siguiente ciclo de facturación como cargo prorrateado."
}
+24 -20
View File
@@ -267,24 +267,28 @@
"fall2024SpaceInvaderHealerSet": "Conjunto de Invasor del Espacio (Sanador)",
"fall2024BlackCatRogueSet": "Conjunto Gato Negro (Pícaro)",
"fall2024FieryImpWarriorSet": "Conjunto de Balrog Menor (Guerrero)",
"winter2025StringLightsHealerSet": "Conjunto de Sanador Tira de Luces",
"winter2025SnowRogueSet": "Conjunto Pícaro Muñeco de Nieve",
"winter2025MooseWarriorSet": "Conjunto de Guerrero Alce",
"winter2025AuroraMageSet": "Conjunto de Mago de la Aurora",
"spring2025CrystalPointRogueSet": "Conjunto Pícaro Puntas de Cristal",
"spring2025PlumeriaHealerSet": "Conjunto Sanador Flor Plumaria",
"spring2025MantisMageSet": "Conjunto Mago Mantis",
"spring2025SunshineWarriorSet": "Conjunto Guerrero Brillo Solar",
"summer2025ScallopWarriorSet": "Conjunto Guerrero Vieira",
"summer2025SquidRogueSet": "Conjunto Pícaro Calamar",
"summer2025SeaAngelHealerSet": "Conjunto Sanador Ángel de Mar",
"summer2025FairyWrasseMageSet": "Conjunto Mago Pez Lábrido Hada",
"fall2025SasquatchWarriorSet": "Conjunto de Guerrero Bigfoot",
"fall2025SkeletonRogueSet": "Conjunto de Esqueleto Pícaro",
"fall2025KoboldHealerSet": "Conjunto de Sanador Kobold",
"fall2025MaskedGhostMageSet": "Conjunto de Mago Fantasma Enmascarado",
"winter2026RimeReaperWarriorSet": "Conjunto de Guerrero Destripador Escarcha",
"winter2026SkiRogueSet": "Conjunto de Esquiador Pícaro",
"winter2026PolarBearHealerSet": "Conjunto de Sanador Oso Polar",
"winter2026MidwinterCandleMageSet": "Conjunto de Mago Vela Invernal"
"winter2025StringLightsHealerSet": "Conjunto Tira de Luces (Sanador)",
"winter2025SnowRogueSet": "Conjunto Muñeco de Nieve (Pícaro)",
"winter2025MooseWarriorSet": "Conjunto Alce (Guerrero)",
"winter2025AuroraMageSet": "Conjunto Aurora (Mago)",
"spring2025CrystalPointRogueSet": "Conjunto Punta de Cristal (Pícaro)",
"spring2025PlumeriaHealerSet": "Conjunto Plumeria (Sanador)",
"spring2025MantisMageSet": "Conjunto Mantis (Mago)",
"spring2025SunshineWarriorSet": "Conjunto Rayo de Sol (Guerrero)",
"summer2025ScallopWarriorSet": "Conjunto Vieira (Guerrero)",
"summer2025SquidRogueSet": "Conjunto Calamar (Pícaro)",
"summer2025SeaAngelHealerSet": "Conjunto Ángel de Mar (Sanador)",
"summer2025FairyWrasseMageSet": "Conjunto Pez Lábrido Hada (Mago)",
"fall2025SasquatchWarriorSet": "Conjunto Pie Grande (Guerrero)",
"fall2025SkeletonRogueSet": "Conjunto Esqueleto (Pícaro)",
"fall2025KoboldHealerSet": "Conjunto Kobold (Sanador)",
"fall2025MaskedGhostMageSet": "Conjunto Fantasma Enmascarado (Mago)",
"winter2026RimeReaperWarriorSet": "Conjunto Destripador Escarcha (Guerrero)",
"winter2026SkiRogueSet": "Conjunto Esquí (Pícaro)",
"winter2026PolarBearHealerSet": "Conjunto Oso Polar (Sanador)",
"winter2026MidwinterCandleMageSet": "Conjunto Vela Invernal (Mago)",
"spring2026FrogWarriorSet": "Conjunto Rana (Guerrero)",
"spring2026BranchRogueSet": "Conjunto Rama Primaveral (Pícaro)",
"spring2026SnowdropHealerSet": "Conjunto Campanilla de Invierno (Sanador)",
"spring2026MaypoleMageSet": "Conjunto Palo de Mayo (Mago)"
}
+18 -9
View File
@@ -189,7 +189,7 @@
"questTRexUndeadBoss": "Tiranosaurio Esqueleto",
"questTRexUndeadRageTitle": "Sanación de Esqueleto",
"questTRexUndeadRageDescription": "Esta barra se llena cuando no completas tus tareas Diarias. Cuando esté completa, ¡el Tiranosaurio Esqueleto va a sanar un 30 por ciento de su salud restante!",
"questTRexUndeadRageEffect": "`¡Tiranosaurio Esqueleto usa SANACIÓN DE ESQUELETO!`\n\n¡El monstruo deja salir un rugido sobrenatural, y algunos de sus huesos dañados vuelven a unirse!",
"questTRexUndeadRageEffect": "¡Tiranosaurio Esqueleto usa SANACIÓN DE ESQUELETO!\n\n¡El monstruo deja salir un rugido sobrenatural, y algunos de sus huesos dañados vuelven a unirse!",
"questTRexDropTRexEgg": "Tiranosaurio (Huevo)",
"questTRexUnlockText": "Desbloquea la compra de huevos de tiranosaurio en el Mercado",
"questRockText": "Escapa de la Cueva Viviente",
@@ -241,7 +241,7 @@
"questDilatoryDistress2Boss": "Enjambre de Carabelas Aquaticas",
"questDilatoryDistress2RageTitle": "Regeneración de Enjambre",
"questDilatoryDistress2RageDescription": "Regeneración de Enjambre: Esta barra se va llenando cuando no completas tus Diarias. Cuando se llene completamente, ¡el Enjambre de Calaveras Acuáticas sanará el 30% de su salud restante!",
"questDilatoryDistress2RageEffect": "`¡El Enjambre de Calaveras Acuáticas usa REGENERACIÓN DE ENJAMBRE!`\n\n¡Incentivadas por sus victorias, más calaveras salen de la grieta, fortaleciendo al enjambre!",
"questDilatoryDistress2RageEffect": "¡El Enjambre de Calaveras Acuáticas usa REGENERACIÓN DE ENJAMBRE!\n\n¡Incentivadas por sus victorias, más calaveras salen de la grieta, fortaleciendo al enjambre!",
"questDilatoryDistress2DropSkeletonPotion": "Poción de Eclosión Esquelética",
"questDilatoryDistress2DropCottonCandyBluePotion": "Poción de Eclosión Algodón de Azúcar Azul",
"questDilatoryDistress2DropHeadgear": "Diadema de coral de fuego (equipo de cabeza)",
@@ -437,7 +437,7 @@
"questStoikalmCalamity1Boss": "Enjambre de Calaveras Terrestres",
"questStoikalmCalamity1RageTitle": "Reaparición del Enjambre",
"questStoikalmCalamity1RageDescription": "Regeneración de Enjambre: Esta barra se va llenando cuando no completas tus Diarias. Cuando se llene completamente, ¡el Enjambre de Calaveras Terrestres sanará el 30% o de su salud restante!",
"questStoikalmCalamity1RageEffect": "`¡El Enjambre de Calaveras de Tierra usa REGENERACIÓN DE ENJAMBRE!`\n\n¡Más calaveras salen de la tierra, chasqueando sus dientes en el frío!",
"questStoikalmCalamity1RageEffect": "¡El Enjambre de Calaveras de Tierra usa REGENERACIÓN DE ENJAMBRE!\n\n¡Más calaveras salen de la tierra, chasqueando sus dientes en el frío!",
"questStoikalmCalamity1DropSkeletonPotion": "Poción de eclosión de esqueleto",
"questStoikalmCalamity1DropDesertPotion": "Poción de eclosión del desierto",
"questStoikalmCalamity1DropArmor": "Armadura de jinete de mamut",
@@ -478,7 +478,7 @@
"questMayhemMistiflying1Boss": "Enjambre de Calaveras Aéreas",
"questMayhemMistiflying1RageTitle": "Reaparición del Enjambre",
"questMayhemMistiflying1RageDescription": "Reaparición del Enjambre: Esta barra se llena cuando no completas tus Tareas Diarias. Cuando está llena, ¡el Enjambre de Calaveras Aéreas recuperará el 30% de su salud restante!",
"questMayhemMistiflying1RageEffect": "`El Enjambre de Calaveras Aéreas utiliza REAPARICIÓN DEL ENJAMBRE`\n\n¡Envalentonadas por sus victorias, más calaveras aparecen girando de entre las nubes!",
"questMayhemMistiflying1RageEffect": "¡El Enjambre de Calaveras Aéreas utiliza REAPARICIÓN DEL ENJAMBRE!\n\n¡Envalentonadas por sus victorias, más calaveras aparecen girando de entre las nubes!",
"questMayhemMistiflying1DropSkeletonPotion": "Poción de Eclosión Esquelética",
"questMayhemMistiflying1DropWhitePotion": "Poción de Eclosión Blanca",
"questMayhemMistiflying1DropArmor": "Túnicas del Mensajero Picaresco Arcoiris (Armadura)",
@@ -548,7 +548,7 @@
"questLostMasterclasser4Boss": "Anti'zinnya",
"questLostMasterclasser4RageTitle": "Vacío Sifónico",
"questLostMasterclasser4RageDescription": "Vacío Sifónico: Esta barra se llena cuando no completas tus Tareas Diarias. ¡Cuando esté llena, Anti'zinnya eliminará el Maná del Equipo!",
"questLostMasterclasser4RageEffect": "'Anti'zinnya usa VACÍO SIFÓNICO! ¡En una inversión del hechizo Oleada Etérea, sientes que tu magia se escurre entre la oscuridad!",
"questLostMasterclasser4RageEffect": "¡'Anti'zinnya usa VACÍO SIFÓNICO! ¡En una inversión del hechizo Oleada Etérea, sientes que tu magia se escurre hacia la oscuridad!",
"questLostMasterclasser4DropBackAccessory": "Capa de Éter (Accesorio en la Espalda)",
"questLostMasterclasser4DropWeapon": "Cristales de Éter (Arma de dos manos)",
"questLostMasterclasser4DropMount": "Montura Invisible de Éter",
@@ -693,7 +693,7 @@
"jungleBuddiesText": "Paquete de misiones Colegas de la Jungla",
"questWaffleUnlockText": "Desbloquea la compra de la poción de eclosión de confitería en el Mercado",
"questWaffleDropDessertPotion": "Poción de eclosión de confitería",
"questWaffleRageEffect": "`¡Tortita Terrible usa CIENAGA DE ARCE!` ¡Un sirope pegajoso de savia ralentiza tus golpes y hechizos! Daño pendiente reducido.",
"questWaffleRageEffect": "¡Tortita Terrible usa CIENAGA DE ARCE! ¡Un sirope pegajoso de savia ralentiza tus golpes y hechizos! Daño pendiente reducido.",
"questWaffleRageDescription": "Cienaga de Arce: Esta barra se llena cuando no completas tus Tareas Diarias. ¡Cuando esté llena, la Tortita Terrible restará del daño pendiente que los miembros del Equipo hayan acumulado!",
"questBlackPearlUnlockText": "Desbloquea Pociones de Eclosión de Perla Negra para comprar en el Mercado",
"questBlackPearlDropBlackPearlPotion": "Poción de Eclosión de Perla Negra",
@@ -749,7 +749,7 @@
"questVirtualPetNotes": "Es una tranquila y agradable mañana de primavera en Habitica, una semana después de un memorable Día de los Inocentes. Tú y @Beffymaroo estáis en los establos atendiendo a vuestras mascotas (¡quienes todavía están un poco confundidas por el tiempo que pasaron virtualmente!).<br><br>A lo lejos escuchas un estruendo y un pitido, suave al principio pero aumentando en volumen como si estuviera cada vez más cerca. Aparece una forma de huevo en el horizonte y, a medida que se acerca, con un pitido cada vez más fuerte, ¡ves que es una mascota virtual gigantesca!<br><br>“Oh, no”, exclama @Beffymaroo, “Creo que Santo Inocente dejó asuntos pendientes con este tipo grande aquí, ¡parece querer atención!”<br><br>La mascota virtual emite un pitido enfadado, lanzando una rabieta virtual y gritando cada vez más cerca.",
"questVirtualPetBoss": "Tamagotchi",
"questVirtualPetRageTitle": "El pitido",
"questVirtualPetRageEffect": "\"¡Wotchimon usa un pitido molesto!\" ¡Wotchimon emite un pitido molesto y su barra de felicidad desaparece repentinamente! Daño pendiente reducido.",
"questVirtualPetRageEffect": "¡Wotchimon usa Pitido Molesto!\" ¡Wotchimon emite un pitido molesto y su barra de felicidad desaparece repentinamente! Daño pendiente reducido.",
"questVirtualPetRageDescription": "Esta barra se llena cuando no completas tus Diarios. ¡Cuando esté lleno, Wotchimon eliminará algunos de los daños causados de tu grupo!",
"questVirtualPetDropVirtualPetPotion": "Poción virtual para incubar mascotas",
"questVirtualPetText": "El Caos Virtual con Santo Inocente: El Pitido",
@@ -760,7 +760,7 @@
"questPinkMarbleBoss": "Cupido",
"questPinkMarbleRageTitle": "Ponche Rosa",
"questPinkMarbleRageDescription": "Esta barra se llena cuando no completas tus Tareas Diarias. ¡Cuando esté llena, Cupido eliminará algunos daños causados por tu grupo!",
"questPinkMarbleRageEffect": "\"¡Cupido usa Ponche Rosa!\" ¡No ha sido nada afectuoso! Tus compañeros de equipo están desconcertados. El daño pendiente se ha reducido.",
"questPinkMarbleRageEffect": "¡Cupido usa Ponche Rosa! ¡No ha sido nada afectuoso! Tus compañeros de equipo están desconcertados. El daño pendiente se ha reducido.",
"questPinkMarbleDropPinkMarblePotion": "Poción de eclosión de mármol rosa",
"questPinkMarbleUnlockText": "Desbloquea Poción de eclosión de mármol rosa para comprarla en el Mercado.",
"questFungiNotes": "Ha sido una primavera lluviosa y la tierra alrededor de los establos está esponjosa y húmeda. Te das cuenta que bastantes setas han crecido cerca de las paredes de madera y en las vallas. Hay una niebla que impide que el sol brille con fuerza y esto crea un paisaje muy desalentador. <br><br>Entre la espesura de la bruma ves la silueta del Bromista de Abril, pero no parece que esté en su habitual estado divertido y saltarín.<br><br>”Esperaba traeros algunas divertidas pociones de eclosión de Setas Mágicas para que pudierais disfrutar de vuestras amigas setas desde mi día especial y para siempre” dice, con una expresión alarmantemente seria. “Pero esta fría niebla me está afectando, me hace sentirme demasiado cansado y triste y hace que mi magia no funcione.”<br><br>”Oh no, siento escuchar eso,” le dices, notando tu también tu propio estado de ánimo muy afectado y apagado. “Esta extraña niebla esta haciendo que el día se vuelva sombrío. Me pregunto cuál será su procedencia...”<br><br>Un estruendo apenas audible resuena por entre los campos y puedes ver una silueta emergiendo de la bruma. La sensación de peligro que notas se hace más intensa al ver una gigantesca criatura parecida a una seta con cara de pocos amigos, y la bruma parece que procede de ella.<br><br>”Vale,” dice el Bromista, “Creo que este colega micológico puede ser el origen de nuestra súbita tristeza. Veamos si una buena trifulca nos ameniza el día mientras le abrimos otra sonrisa al bicho.”",
@@ -860,5 +860,14 @@
"questOpalCollectMercuryRunes": "Runa de Mercurio",
"questOpalCollectOpalGems": "Gema de Ópalo",
"questOpalDropOpalPotion": "Poción de eclosión Ópalo",
"questOpalUnlockText": "Desbloquea pociones de eclosión Ópalo para comprarlas en la Tienda"
"questOpalUnlockText": "Desbloquea pociones de eclosión Ópalo para comprarlas en la Tienda",
"questAlienText": "Invasión de los Ladrones de Motivación",
"questAlienNotes": "Han sido días extraños en Habitica. El gran platillo volador aún flota cerca de los Campos Florecientes. Zumba extrañamente. ¿Por qué sigue ahí? El Día de los Santos Inocentes ha pasado, y el tiempo de protagonismo del Maestro de los Pícaros ha terminado.<br><br>Deambulas hacia la luz de la nave espacial. Podrías echarle un vistazo y adentrarte un poco, de paso.<br><br>Mientras te acercas, ves al Bromista de Abril, con aspecto algo sombrío. Su rostro luce verdoso a la luz del rayo de la nave.<br><br>\"'¡Era mi plan conseguir algunas pociones para todos, un pequeño regalo para que todos puedan disfrutar de sus pequeños amigos extraterrestres de nuevo! Pero simplemente no puedo reunir el valor... Creo que sé por qué\", dice el Bromista, asintiendo hacia el rayo.<br><br>Pequeños símbolos están siendo absorbidos por la nave. ¡Son todas tus tareas marcadas! No me extraña que tu motivación haya estado tan baja.<br><br>”¡Nuestra motivación está siendo abducida!” exclamas. “¡Tenemos que rescatarla antes de que acabe perdida en el espacio profundo!”<br><br>El Bromista sonríe. “¡Concentra tus pensamientos en las tareas que sabes que debes terminar! Yo haré el resto con un poco de magia.”",
"questAlienBoss": "Ladrón de Ánimo, el Extraterrestre",
"questAlienRageTitle": "Impedimento Intergaláctico",
"questAlienRageEffect": "¡Ladrón de Ánimo usa Impedimento Intergaláctico! Has retrocedido a través del hiperespacio. ¡Tu oponente recupera PV!",
"questAlienDropAlienPotion": "Poción de eclosión alien",
"questAlienUnlockText": "Desbloque la Poción de Eclosión Alien para comprar en el Mercado",
"questAlienRageDescription": "Esta barra se llena cuando no completas tus Tareas Diarias. ¡Cuando está llena, el Extraterrestre te desanimará recuperando parte de su Salud!",
"questAlienCompletion": "Has logrado recuperar la motivación robada con tu determinación y el poder mágico del Bromista. Mientras sientes regresar tus fuerzas, el OVNI desciende y una rampa emerge lentamente junto con una criatura grande, verde y de un solo ojo. Aunque de aspecto extraño, no parece amenazante.<br><br>“Parece que fuimos un poco demasiado lejos al intentar obtener un poco de ánimo extra de tu hermosa ciudad”, dice. “Disculpas por eso, y fantástico trabajo al recuperarlo. ¡El aura extra de tus esfuerzos, de hecho, cargó el motor de la nave lo suficiente como para llevarnos a casa! Por favor, toma esto en señal de agradecimiento.”<br><br>“¡Oh, pociones!”, dice el Bromista, “¡qué encantador, y qué conveniente para mí que las tengas todas listas!”"
}
+10 -3
View File
@@ -1,8 +1,8 @@
{
"rebirthNew": "Renacimiento: ¡Nueva aventura disponible!",
"rebirthUnlock": "¡Has desbloqueado el Renacimiento! Este objeto especial del Mercado te permite empezar un juego nuevo en el nivel 1, manteniendo tus tareas, logros, mascotas, y más. Úsalo para comenzar una nueva vida en Habitica por si sientes que ya lo lograste todo, o para experimentar nuevas funciones con la perspectiva de un personaje nuevo.",
"rebirthAchievement": "¡Has comenzado una nueva aventura! Este es tu renacimiento número <%= number %>, y el nivel más alto que has conseguido es <%= level %>. Para apilar este logro, ¡empieza tu siguiente aventura después de haber conseguido un nivel más alto!",
"rebirthAchievement100": "¡Has comenzado una nueva aventura! Este es el Renacimiento <%= number %> para ti, y el Nivel más alto que has conseguido es 100 o superior. Para acumular este Logro, ¡empieza tu aventura siguiente después de haber conseguido al menos el nivel 100!",
"rebirthAchievement": "Has utilizado el Orbe del Renacimiento <strong><%= number %></strong> vez, y el nivel más alto que has conseguido <strong><%= level %></strong>.",
"rebirthAchievement100": "Has utilizado el Orbe del Renacimiento <strong><%= number %></strong> veces, y el nivel más alto que has conseguido es <strong>100</strong> o superior.",
"rebirthBegan": "Comienza una nueva aventura",
"rebirthText": "Has comenzado <%= rebirths %> nuevas aventuras",
"rebirthOrb": "Usó un Orbe de Renacimiento para comenzar de nuevo después de alcanzar el Nivel <%= level %>.",
@@ -11,5 +11,12 @@
"rebirthPop": "Reinicia instantáneamente tu personaje como un Guerrero de Nivel 1, manteniendo logros, colecciones y equipamiento. Tus tareas y su historial se mantendrán, pero volverán a ser amarillas. Tus rachas desaparecerán excepto por tareas pertenecientes a Desafíos y Planes Grupales activos. Tu Oro, Experiencia, Maná y los efectos de todas tus habilidades desaparecerán. Todo esto tendrá efecto inmediato.",
"rebirthName": "Esfera de Renacimiento",
"rebirthComplete": "¡Has vuelto a nacer!",
"nextFreeRebirth": "<strong><%= days %> días</strong> hasta la Orbe de Renacimiento <strong>GRATIS</strong>"
"nextFreeRebirth": "<strong><%= days %> días</strong> hasta la Orbe de Renacimiento <strong>GRATIS</strong>",
"rebirthUnlockedOrb": "¡Una nueva aventura está disponible!",
"rebirthNewAchievement": "Nuevo Logro",
"rebirthNewAdventure": "¡Una nueva aventura comienza!",
"rebirthAchievementPlural": "Has utilizado el Orbe del Renacimiento <strong><%= number %></strong> veces, y el nivel más alto que has conseguido <strong><%= level %></strong>.",
"rebirthStackInfo": "Este logro se acumulará cada vez que uses el Orbe del Renacimiento.",
"rebirthUnlockedNewItem": "Orbe del Renacimiento Desbloqueado",
"rebirthUnlockedDesc": "¡Usa el Orbe del Renacimiento para darle nuevo significado a tu aventura en Habitica cuando sientas que ya has logrado todo! Vuelve a empezar desde el nivel 1 conservando tus tareas, Logros y Mascotas con este objeto especial que encontrarás en el Mercado."
}
+5 -1
View File
@@ -273,5 +273,9 @@
"mysterySet202512": "Conjunto de Galleta Ganadora",
"mysterySet202601": "Conjunto Égida de Invierno",
"mysterySet202602": "Conjunto Zorro del Sakura",
"subscriptionBillingFYI": "Las suscripciones se renuevan automáticamente a no ser que se cancelen con 24 horas de antelación antes de que finalice el periodo en curso. Puedes modificar tu suscripción desde la pestaña Suscripción en los ajustes. Se te cargará a tu cuenta dentro de las 24 horas de la fecha de renovación, al mismo precio que pagaste inicialmente."
"subscriptionBillingFYI": "Las suscripciones se renuevan automáticamente a no ser que se cancelen con 24 horas de antelación antes de que finalice el periodo en curso. Puedes modificar tu suscripción desde la pestaña Suscripción en los ajustes. Se te cargará a tu cuenta dentro de las 24 horas de la fecha de renovación, al mismo precio que pagaste inicialmente.",
"mysterySet202603": "Conjunto de Mago de las Glicinias",
"mysterySet202604": "Conjunto Astronauta Audaz",
"mysterySet202605": "Conjunto Nimbus del Anochecer",
"subscriptionBillingFYIShort": "Las suscripciones se renuevan automáticamente a menos que las canceles al menos 24 horas antes de que finalice el período actual. Se te cobrará el importe correspondiente en las 24 horas siguientes a la fecha de renovación, al mismo precio que pagaste inicialmente."
}
@@ -932,7 +932,7 @@
"backgroundElvenCitadelText": "ciudadela élfica",
"backgroundElvenCitadelNotes": "Tome un viaje escénico a una ciudadela élfica.",
"backgrounds122025": "Lote 139: Lanzamiento en diciembre de 2025",
"backgroundNighttimeStreetWithShopsText": "Calle nocturna con tiendas",
"backgroundNighttimeStreetWithShopsText": "Calle Nocturna con Tiendas",
"backgroundNighttimeStreetWithShopsNotes": "Disfrutre del calido resplandor de una calle nocturna con tiendas.",
"backgrounds012026": "Lote 140: Lanzamiento enero de 2026",
"backgroundWinterDesertWithSaguarosText": "Desierto invernal con saguaros",
+1 -1
View File
@@ -172,7 +172,7 @@
"editProfile": "Editar Perfil",
"challengesWon": "Desafíos Ganados",
"questsCompleted": "Misiones Completadas",
"headAccess": "Accesorío para la Cabeza.",
"headAccess": "Accesorio para la Cabeza.",
"backAccess": "Accesorio para la Espalda.",
"bodyAccess": "Accesorio para el Cuerpo.",
"mainHand": "Mano Principal",

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