Compare commits

...

22 Commits

Author SHA1 Message Date
Hafiz d08ea6677d Refactor(tasks): Centralize daily task start date normalization 2026-05-05 12:21:49 -05:00
Kalista Payne 819ed2b355 Task dropdown and keyboard navigation fixes (#15648)
* fix(css): kebab z, focus highlights

* fix(nav): better tab behavior
2026-04-30 17:00:21 -05:00
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
183 changed files with 1641 additions and 3561 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.47.1",
"version": "5.47.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "habitica",
"version": "5.47.1",
"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.47.1",
"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"
}
}
+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;
+5 -17
View File
@@ -13,6 +13,8 @@
}, `type_${task.type}`
]"
@click="castEnd($event, task)"
tabindex="0"
@keypress.enter="$emit('editTask', task)"
>
<div
class="d-flex"
@@ -98,9 +100,7 @@
<div
class="task-clickable-area pt-1 pl-75 pb-0"
:class="{ 'cursor-auto': !teamManagerAccess }"
tabindex="0"
@click="edit($event, task)"
@keypress.enter="edit($event, task)"
>
<div class="d-flex justify-content-between">
<h3
@@ -432,10 +432,6 @@
outline: none;
transition: none;
border: $purple-400 solid 1px;
:not(task-best-control-inner-habit) { // round icon
border-radius: 4px;
}
}
.control-bottom-box {
@@ -462,16 +458,13 @@
&:hover:not(.task-not-editable.task-not-scoreable),
&:focus-within:not(.task-not-editable.task-not-scoreable) {
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
z-index: 11;
}
}
.task:not(.groupTask) {
&:hover,
&:focus-within {
.left-control, .right-control, .task-content {
border-color: $purple-400;
}
&:hover, &:focus {
border: none;
outline: 1px solid $purple-400;
}
}
@@ -522,11 +515,6 @@
&-user {
padding-right: 0px;
}
&:focus {
border-radius: 4px;
border: $purple-400 solid 1px;
}
}
.task-title + .task-dropdown ::v-deep .dropdown-menu {
@@ -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
@@ -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 %>.",
+2 -1
View File
@@ -410,5 +410,6 @@
"questEggPlatypusText": "Ornithorynque",
"questEggPlatypusMountText": "Ornithorynque",
"questEggPlatypusAdjective": "organisé",
"hatchingPotionOpal": "Opal"
"hatchingPotionOpal": "Opal",
"hatchingPotionAlien": "Alien"
}
+3 -1
View File
@@ -187,5 +187,7 @@
"minPasswordLengthLogin": "Votre mot de passe contient au moins 8 caractères.",
"enterValidEmail": "Merci d'entrer une adresse email valide.",
"whatToCallYou": "Comment souhaitez-vous que nous vous appelions ?",
"acceptPrivacyTOS": "Vous confirmez avoir au moins 18 ans, et avoir lu et approuvé nos <a href='/static/terms' target='_blank'>Conditions de Service</a> ainsi que notre <a href='/static/privacy' target='_blank'>Politique de confidentialité</a>"
"acceptPrivacyTOS": "Vous confirmez avoir au moins 18 ans, et avoir lu et approuvé nos <a href='/static/terms' target='_blank'>Conditions de Service</a> ainsi que notre <a href='/static/privacy' target='_blank'>Politique de confidentialité</a>",
"emailAddress": "Adresse mail",
"emailRequiredForSupport": "Une adresse mail est nécessaire pour le support utilisat·eur·rice. Veuillez entrer une adresse mail pour poursuivre la création de votre compte."
}
+12 -1
View File
@@ -930,5 +930,16 @@
"backgroundElegantPalaceNotes": "Csodáld meg az elegáns palota színes folyosóit.",
"backgrounds122025": "139. KÉSZLET: Megjelent 2025 december",
"backgrounds012026": "140. KÉSZLET: Megjelent 2026 január",
"backgrounds022026": "141. KÉSZLET: Megjelent 2026 február"
"backgrounds022026": "141. KÉSZLET: Megjelent 2026 február",
"backgrounds032026": "KÉSZLET 142: Megjelent 2026. március",
"backgroundWaterfallWithRainbowText": "Vízesés szivárvánnyal",
"backgroundWaterfallWithRainbowNotes": "Gyönyörködj a vízesés és a szivárvány lélegzetelállító szépségében.",
"backgrounds042026": "KÉSZLET 143: Megjelent 2026. április",
"backgroundRidingACometText": "Üstököslovaglás",
"backgroundRidingACometNotes": "Utazz az űrben üstökösön lovagolva!",
"backgrounds052026": "KÉSZLET 144: Megjelent 2026. május",
"backgroundElvenCitadelText": "Tündék citadellája",
"backgroundElvenCitadelNotes": "Tegyél egy festői utazást a tündék citadellájához.",
"backgroundOnAStrangePlanetText": "Idegen bolygón",
"backgroundOnAStrangePlanetNotes": "Merészkedj oda, ahol még egyetlen habiticai sem járt: egy idegen bolygóra."
}
+2 -1
View File
@@ -410,5 +410,6 @@
"questEggPlatypusText": "Kacsacsőrű emlős",
"questEggPlatypusAdjective": "egy maximalista",
"questEggPlatypusMountText": "Kacsacsőrű emlős",
"hatchingPotionOpal": "Opál"
"hatchingPotionOpal": "Opál",
"hatchingPotionAlien": "Földönkívüli"
}
+56 -2
View File
@@ -3429,7 +3429,7 @@
"armorSpecialWinter2026HealerNotes": "Mint egy természetes fényshow, lenyűgöző leszel, miközben a napi teendőid felé tartasz. Növeli a szívósságodat <%= con %> ponttal. Limitált kiadás 2025-2026 téli felszerelés.",
"armorMystery202512Notes": "Készen állsz a harcra ebben az édes és erős páncélban. Nem jár semmilyen előnnyel. 2025. december előfizetői tárgy.",
"headSpecialWinter2026RogueNotes": "Tartsd fenn a fókuszodat és a látásodat, miközben nagyobb célokra összpontosítasz ebben a szezonban. Növeli az észlelésedet <%= per %> ponttal. Limitált kiadás 2025-2026 téli felszerelés.",
"headMystery202602Notes": " Ezek a fülek annyira kiélesítik a hallásodat, hogy hallani fogod a virágbimbók növését a faágakon, ahogy közeledik a tavasz. Nem jár semmilyen előnnyel. 2026. február előfizetői tárgy.",
"headMystery202602Notes": "Ezek a fülek annyira kiélesítik a hallásodat, hogy hallani fogod a virágbimbók növését a faágakon, ahogy közeledik a tavasz. Nem jár semmilyen előnnyel. 2026. február előfizetői tárgy.",
"shieldArmoirePrettyPinkGiftBoxNotes": "Ez az ajándék egy kedves barátodtól van? Egy gondoskodó rokonodtól? Az igazitól? Egy titkos imádótól? Bárki küldte is, tudja, hogy örülni fogsz a benne lévőknek. Növeli az összes tulajdonságodat egyaránt <%= attrs %> ponttal. Megbűvölt komód: Rózsaszín szépség szett (2/2. tárgy)",
"backArmoireHarpsichordNotes": "Pting! Ptiiing! Gyűjtsd össze a csapatodat egy vacsorára vagy piknikre, és hallgasd a fémesen csengő dallamot ezen a csembalón. Növeli az észlelésedet és az intelligenciádat egyaránt <%= attrs %> ponttal. Megbűvölt komód: Hangszerkészlet 2 (1. tárgy a 3-ból)",
"weaponSpecialWinter2026MageNotes": "A kandeláberek úgy segítenek, hogy egyszerre több gyertyát tartanak - kövesd a példáját, amikor legközelebb több feladatot kell egyszerre kezelned. Növeli az intelligenciádat <%= int %> ponttal és az észlelésedet <%= per %> ponttal. Limitált kiadás 2025-2026 téli felszerelés.",
@@ -3495,5 +3495,59 @@
"backMystery202601Notes": "Ez a jel megadja a használójának az irányítást a hideg és a fagy szezonjának elemei felett. Nem jár semmilyen előnnyel. 2026. január előfizetői tárgy.",
"backMystery202602Text": "Sakura öt farka",
"backMystery202602Notes": "Ezek a bolyhos farkak cseresznyevirág színűek, emlékeztetve arra, hogy a tavasz közeleg! Nem jár semmilyen előnnyel. 2026. február előfizetői tárgy.",
"backArmoireHarpsichordText": "Csembaló"
"backArmoireHarpsichordText": "Csembaló",
"armorArmoireSoftYellowSuitNotes": "A sárga egy energikus szín. Viselj ezt lefekvéskor, és a nappal együtt fogsz felébredni másnap reggel, készen egy feladatokkal teli napra. Növeli a szívósságodat és az erődet egyaránt <%= attrs %> ponttal. Megbűvölt komód: Sárga pihenőruha szett (2/3 tárgy).",
"weaponSpecialSpring2026WarriorText": "Hatalmas békatőr",
"weaponSpecialSpring2026WarriorNotes": "Egy párbajlehetőség bármikor adódhat, és ezzel a félelmetes tőrrel készen fogsz állni! Növeli az erődet <%= str %> ponttal. Limitált kiadás 2026. tavaszi felszerelés.",
"weaponSpecialSpring2026RogueText": "Tavaszi ág",
"weaponSpecialSpring2026RogueNotes": "Egy fejlődési lehetőség már közeleg, és ezekkel a rügyező ágakkal készen fogsz állni! Növeli az erődet <%= str %> ponttal. Limitált kiadás 2026. tavaszi felszerelés.",
"weaponSpecialSpring2026HealerText": "Hóvirág bot",
"weaponSpecialSpring2026HealerNotes": "Egy lehetőség, hogy friss kezdettel újra kezdhesd, már épp előtted van, és ezzel a pompás bottal készen fogsz állni! Növeli az intelligenciádat <%= int %> ponttal. Limitált kiadás 2026. tavaszi felszerelés.",
"weaponSpecialSpring2026MageText": "Májusfa napernyő",
"weaponSpecialSpring2026MageNotes": "Egy ünneplési lehetőség közeledik, és ezzel a csinos napernyő rúddal készen fogsz állni! Növeli az intelligenciádat <%= int %> ponttal és az észlelésedet <%= per %> ponttal. Limitált kiadás 2026. tavaszi felszerelés.",
"weaponMystery202603Text": "Lilaakác varázsló bot",
"weaponMystery202603Notes": "Varázslj, hogy felmelegítsd a tavaszi levegőt és bátorítsd a virágokat a rügyezésre! Nem jár semmilyen előnnyel. 2026. március előfizetői tárgy.",
"armorSpecialSpring2026WarriorText": "Béka páncél",
"armorSpecialSpring2026WarriorNotes": "Ugorj akcióba, amint a hó elkezdett olvadni. Növeli a szívósságodat <%= con %> ponttal. Limitált kiadás 2026. tavaszi felszerelés.",
"armorSpecialSpring2026RogueText": "Nyírfakéreg páncél",
"armorSpecialSpring2026RogueNotes": "Állj ellen az elkerülhetetlen tavaszi esőknek és a lágy szellőknek is. Növeli az észlelésedet <%= per %> ponttal. Limitált kiadás 2026. tavaszi felszerelés.",
"armorSpecialSpring2026HealerText": "Hóvirág ruha",
"armorSpecialSpring2026HealerNotes": "Siklj kecsesen át a hideg, sötét télből a dicsőséges tavaszba. Növeli a szívósságodat <%= con %> ponttal. Limitált kiadás 2026. tavaszi felszerelés.",
"armorSpecialSpring2026MageText": "Májusfa táncos ruha",
"armorSpecialSpring2026MageNotes": "Érkezz készen táncolni, piknikezni és élvezni a meleg időt, amit a tavasz hoz. Növeli az intelligenciádat <%= int %> ponttal. Limitált kiadás 2026. tavaszi felszerelés.",
"armorMystery202604Text": "Merész űrhajós szkafander",
"armorMystery202604Notes": "Egy kis lépés a feladatlistádnak, egy óriási ugrás a teljesítmény érzésednek! Nem jár semmilyen előnnyel. 2026. április előfizetői tárgy.",
"armorArmoireSoftYellowSuitText": "Puha sárga öltöny",
"armorArmoireHandstandOutfitText": "Kézállás",
"armorArmoireHandstandOutfitNotes": "A dolgok bizony másként néznek ki, amikor fejjel lefelé vagy, nem igaz? Ha úgy érzed, hogy elakadtál, itt az ideje egy friss nézőpontnak! Növeli az észlelésedet <%= per %> ponttal. Megbűvölt komód: Kézállás szett (1/1 tárgy).",
"headSpecialSpring2026WarriorText": "Béka harcos sisak",
"headSpecialSpring2026WarriorNotes": "A békák jól ismertek a korrupcióval szembeni ellenállásukról. Ez a sisak megadja neked a nemes tulajdonságaikat! Növeli az erődet <%= str %> ponttal. Limitált kiadás 2026. tavaszi felszerelés.",
"headSpecialSpring2026RogueText": "Tavaszi ág sisak",
"headSpecialSpring2026RogueNotes": "Tegyél feltűnő kijelentést gallyakkal és rügyekkel, amelyek vadul növnek minden irányba. Növeli az észlelésedet <%= per %> ponttal. Limitált kiadás 2026. tavaszi felszerelés.",
"headSpecialSpring2026HealerText": "Hóvirág sisak",
"headSpecialSpring2026HealerNotes": "Tegyél reményteljes kijelentést ezekkel a gyönyörű, ellenálló szirmokkal. Növeli az intelligenciádat <%= int %> ponttal. Limitált kiadás 2026 tavaszi felszerelés.",
"headSpecialSpring2026MageText": "Májusvirág korona",
"headSpecialSpring2026MageNotes": "Tegyél vidám kijelentést a fejedet körbeölelő élénk virágokkal. Növeli az észlelésedet <%= per %> ponttal. Limitált kiadás 2026 tavaszi felszerelés.",
"headMystery202603Text": "Glicíniavarázs kalap",
"headMystery202603Notes": "Ez a vidám kalap nemcsak fokozza a mágikus képességedet, hanem kellemes tavaszi illattal is rendelkezik! Nem jár semmilyen előnnyel. 2026. március előfizetői tárgy.",
"headMystery202604Text": "Merész űrhajós sisak",
"headMystery202604Notes": "Az űrben senki sem hallja, ahogy kipipálod a feladataidat. De az igazi jutalom a személyes elégedettség érzése! Nem jár semmilyen előnnyel. 2026. április előfizetői tárgy.",
"headArmoireFloppyYellowHatText": "Sárga lógó kalap",
"headArmoireFloppyYellowHatNotes": "Sok varázslat van bevarrva ebbe az egyszerű kalapba, ami ifjúságos sárga színt kölcsönöz neki. Növeli az összes tulajdonságodat egyaránt <%= attrs %> ponttal. Megbűvölt komód: Sárga pihenőruha szett (1. tárgy a 3-ból).",
"headArmoireVerdantArmingCapText": "Zöldellő apródok páncélsipkája",
"headArmoireVerdantArmingCapNotes": "Ez a kényelmes, párnázott fejvédő harcra készen tart, és segít elviselni bármilyen nehezet, ami az utadba kerülhet. Növeli az észlelésedet és a szívósságodat egyaránt <%= attrs %> ponttal. Megbűvölt komód: Zöldellő apród szett (1. tárgy a 2-ből).",
"shieldSpecialSpring2026WarriorText": "Békaharcos gyertyatartó",
"shieldSpecialSpring2026WarriorNotes": "Ez a gyertyatartó nemcsak megvilágítja az utadat, hanem felolvaszthatod vele a megmaradt havat és jeget is. Növeli a szívósságodat <%= con %> ponttal. Limitált kiadás 2026 tavaszi felszerelés.",
"shieldSpecialSpring2026RogueText": "Tavaszi ág",
"shieldSpecialSpring2026RogueNotes": "Nyúlj ki és nyúlj magasra ezekkel az ágakkal. Szükség esetén hátvakaróként is használhatod őket. Növeli az erődet <%= str %> ponttal. Limitált kiadás 2026 tavaszi felszerelés.",
"shieldSpecialSpring2026HealerText": "Hóvirág levél",
"shieldSpecialSpring2026HealerNotes": "Teremts enyhe szellőt ezzel a legyezővel, ahogy a napok melegebbek lesznek. Szükség esetén írószárként is használhatod. Növeli a szívósságodat <%= con %> ponttal. Limitált kiadás 2026 tavaszi felszerelés.",
"shieldMystery202605Text": "Alkonyat pajzs",
"shieldMystery202605Notes": "Hagyd, hogy a hold ragyogó fénye megvédjen a sötétségben leselkedő veszélyektől. Nem jár semmilyen előnnyel. 2026. május előfizetői tárgy.",
"shieldArmoireSoftYellowPillowText": "Puha sárga párna",
"shieldArmoireSoftYellowPillowNotes": "A tapasztalt harcos párnát pakol minden expedícióhoz. Fejlődj és ragyogj, miközben megszilárdítod mindazt, amit a korábbi kalandok során megtanultál... még alvás közben is. Növeli az intelligenciádat és az észlelésedet egyaránt <%= attrs %> ponttal. Megbűvölt komód: Sárga pihenőruha szett (3. tárgy a 3-ból).",
"shieldArmoireVerdantBannerText": "Zöldellő apród zászló",
"shieldArmoireVerdantBannerNotes": "Lobogtatd magasan a zászlódat, hogy jelezd a barátaidnak: itt az ideje összegyűlni! Növeli az intelligenciádat <%= int %> ponttal. Megbűvölt komód: Zöldellő apród szett (2. tárgy a 2-ből).",
"backMystery202605Text": "Alkonyat nimbus",
"backMystery202605Notes": "Egy ragyogó fénykör holdfényből és csillagfényből, hogy megvilágítsd a legsötétebb éjszakát. Nem jár semmilyen előnnyel. 2026. május előfizetői tárgy."
}
+13 -1
View File
@@ -428,5 +428,17 @@
"groupParentChildren": "Használat a háztartásommal",
"groupFriends": "Használat barátokkal",
"groupManager": "Használat munkában",
"groupTeacher": "Használat oktatásban"
"groupTeacher": "Használat oktatásban",
"previouslyUpgradedGroup": "Korábban fejlesztett csoport",
"additionalMembersProrated": "A hónap során meghívott további tagok arányosan kerülnek hozzáadásra a következő számlázási ciklus összegéhez.",
"upgradeCancelsPendingInvites": "A csapatod fejlesztése törli az összes függőben lévő meghívót",
"chooseAnOption": "Válassz egy lehetőséget",
"upgradeExistingGroup": "Meglévő csoport fejlesztése",
"createNewGroup": "Új csoport létrehozása",
"yourParty": "A csapatod",
"inviteOthersForAdditional": "Hívj meg másokat a csoportodba további",
"perMember": "tagonként",
"oneMember": "1 tag",
"membersCount": "<%= count %> tag",
"pendingCount": "(<%= count %> függőben)"
}
+24 -20
View File
@@ -170,8 +170,8 @@
"summer2024SeaSnailHealerSet": "Tengeri csiga szett (Gyógyító)",
"summer2024NudibranchRogueSet": "Csupaszkopoltyús szett (Tolvaj)",
"anniversaryGryphatriceText": "A ritka Ünnepi griffatrice is csatlakozik a születésnapi ünnepléshez! Ne hagyd ki a lehetőséget, hogy megszerezd ezt az exkluzív animált kisállatot.",
"winter2025AuroraMageSet": "Auróra mágus szett",
"winter2025SnowRogueSet": "Hó tolvaj szett",
"winter2025AuroraMageSet": "Sarki fény szett (Mágus)",
"winter2025SnowRogueSet": "Hó szett (Tolvaj)",
"celebrateBirthday": "Ünnepeld Habitica 10. születésnapját ajándékokkal és exkluzív tárgyakkal!",
"jubilantGryphatricePromo": "Animált Ünnepi griffatrice kisállat",
"winter2024PeppermintBarkWarriorSet": "Borsmentás kéreg szett (Harcos)",
@@ -185,10 +185,10 @@
"winter2023FairyLightsMageSet": "Tündérfények (Mágus)",
"winter2023CardinalHealerSet": "Kardinális (Gyógyító)",
"winter2024SnowyOwlRogueSet": "Havas bagoly (Tolvaj)",
"winter2025StringLightsHealerSet": "Fényfüzéres gyógyító szett",
"winter2025StringLightsHealerSet": "Fényfüzér szett (Gyógyító)",
"winter2024FrozenHealerSet": "Fagyott (Gyógyító)",
"gemSaleLimitationsText": "Ez az akció kizárólag az esemény ideje alatt érvényes. Az esemény <%= eventStartMonth %> <%= eventStartOrdinal %>-án <%= eventStartTime %> <%= timeZone %>-kor kezdődik, és <%= eventEndMonth %> <%= eventEndOrdinal %>-án <%= eventEndTime %> <%= timeZone %>-kor ér véget. A promóciós ajánlat csak akkor érhető el, ha saját magadnak vásárolsz gyémántokat.",
"winter2025MooseWarriorSet": "Jávorszarvas harcos szett",
"winter2025MooseWarriorSet": "Jávorszarvas szett (Harcos)",
"celebrateAnniversary": "Ünnepeld Habitica 10. születésnapját ajándékokkal és exkluzív tárgyakkal!",
"wantToPayWithGemsText": "Szeretnél gyémánttal fizetni?",
"spring2023CaterpillarRogueSet": "Hernyó (Tolvaj)",
@@ -271,20 +271,24 @@
"fourForFreeText": "Hogy tovább folytassuk az ünneplést, ajándékba adunk Parti köpenyt, 20 gyémántot, valamint egy limitált kiadású születésnapi hátteret és tárgyszettet, amely tartalmaz egy köpenyt, vállvérteket és egy szemmaszkot.",
"twentyGems": "20 gyémánt",
"jubilantSuccess": "Sikeresen megvásároltad a <strong>Ünnepi griffatrice!</strong>",
"spring2025SunshineWarriorSet": "Napsugár harcos szett",
"spring2025CrystalPointRogueSet": "Kristálycsúcs tolvaj szett",
"spring2025PlumeriaHealerSet": "Plumeria gyógyító szett",
"spring2025MantisMageSet": "Sáska mágus szett",
"summer2025ScallopWarriorSet": "Kagyló harcos szett",
"summer2025SquidRogueSet": "Tintahal tolvaj szett",
"summer2025SeaAngelHealerSet": "Tengeri angyal gyógyító szett",
"summer2025FairyWrasseMageSet": "Tündérajkú mágus szett",
"fall2025SasquatchWarriorSet": "Nagyláb harcos szett",
"fall2025SkeletonRogueSet": "Csontváz tolvaj szett",
"fall2025KoboldHealerSet": "Kobold gyógyító szett",
"fall2025MaskedGhostMageSet": "Álarcos szellem mágus szett",
"winter2026RimeReaperWarriorSet": "Dérarcú harcos szett",
"winter2026SkiRogueSet": "Síelő tolvaj szett",
"winter2026PolarBearHealerSet": "Jegesmedve gyógyító szett",
"winter2026MidwinterCandleMageSet": "Téli napforduló gyertyamágus szett"
"spring2025SunshineWarriorSet": "Napsugár szett (Harcos)",
"spring2025CrystalPointRogueSet": "Kristálycsúcs szett (Tolvaj)",
"spring2025PlumeriaHealerSet": "Plumeria szett (Gyógyító)",
"spring2025MantisMageSet": "Imádkozósáska szett (Mágus)",
"summer2025ScallopWarriorSet": "Fésűkagyló szett (Harcos)",
"summer2025SquidRogueSet": "Tintahal szett (Tolvaj)",
"summer2025SeaAngelHealerSet": "Tengeri angyal szett (Gyógyító)",
"summer2025FairyWrasseMageSet": "Tündérhal szett (Mágus)",
"fall2025SasquatchWarriorSet": "Nagyláb szett (Harcos)",
"fall2025SkeletonRogueSet": "Csontváz szett (Tolvaj)",
"fall2025KoboldHealerSet": "Kobold szett (Gyógyító)",
"fall2025MaskedGhostMageSet": "Álarcos szellem szett (Mágus)",
"winter2026RimeReaperWarriorSet": "Dérarcú szett (Harcos)",
"winter2026SkiRogueSet": "Síelő szett (Tolvaj)",
"winter2026PolarBearHealerSet": "Jegesmedve szett (Gyógyító)",
"winter2026MidwinterCandleMageSet": "Téli napforduló gyertya szett (Mágus)",
"spring2026FrogWarriorSet": "Béka szett (Harcos)",
"spring2026BranchRogueSet": "Tavaszi ág szett (Tolvaj)",
"spring2026SnowdropHealerSet": "Hóvirág szett (Gyógyító)",
"spring2026MaypoleMageSet": "Májusfa szett (Mágus)"
}
+21 -12
View File
@@ -189,7 +189,7 @@
"questTRexUndeadBoss": "Csontváz tirannoszaurusz",
"questTRexUndeadRageTitle": "Csontváz gyógyulás",
"questTRexUndeadRageDescription": "Ez a sáv megtelik, ha nem teljesíted a napi teendőidet. Ha teljesen feltöltődik, a Csontváz tirannoszaurusz a megmaradt életerejének 30%-át visszanyeri!",
"questTRexUndeadRageEffect": "`Csontváz tirannoszaurusz használja a CSONTVÁZ GYÓGYULÁST!`\n\nA szörnyeteg földöntúli ordítást hallat és néhány sérült csontja újra összeforr!",
"questTRexUndeadRageEffect": "Csontváz tirannoszaurusz használja a CSONTVÁZ GYÓGYULÁST!\n\nA szörnyeteg földöntúli ordítást hallat és néhány sérült csontja újra összeforr!",
"questTRexDropTRexEgg": "Tirannoszaurusz (Tojás)",
"questTRexUnlockText": "Elérhetővé teszi a tirannoszaurusz tojások vásárlását a piacon",
"questRockText": "Menekülés a barlangi lény elől",
@@ -241,7 +241,7 @@
"questDilatoryDistress2Boss": "Vízikoponya raj",
"questDilatoryDistress2RageTitle": "A raj újjászületése",
"questDilatoryDistress2RageDescription": "A raj újjászületése: Ez a sáv akkor telik meg, ha nem teljesíted a napi teendőidet. Ha betelik, a vízikoponya raj visszanyeri megmaradt életerejének 30%-át!",
"questDilatoryDistress2RageEffect": "`A vízikoponya raj alkalmazza A RAJ ÚJJÁSZÜLETÉSÉT!`\n\nA győzelmeiktől felbátorodva még több koponya tör elő a hasadékból, megerősítve a rajt!",
"questDilatoryDistress2RageEffect": "A vízikoponya raj alkalmazza A RAJ ÚJJÁSZÜLETÉSÉT!\n\nA győzelmeiktől felbátorodva még több koponya tör elő a hasadékból, megerősítve a rajt!",
"questDilatoryDistress2DropSkeletonPotion": "Csontváz keltetőfőzet",
"questDilatoryDistress2DropCottonCandyBluePotion": "Kék vattacukor keltetőfőzet",
"questDilatoryDistress2DropHeadgear": "Tűzkorall diadém (Fejfedő)",
@@ -343,7 +343,7 @@
"questAxolotlUnlockText": "Elérhetővé teszi az axolotl tojások vásárlását a piacon",
"questAxolotlRageTitle": "Axolotl regeneráció",
"questAxolotlRageDescription": "Ha nem végzed el a napi teendőidet, ez a sáv megtelik. Ha teljesen feltöltődik, a varázslatos axolotl visszanyeri megmaradt életereje 30%-át!",
"questAxolotlRageEffect": "`A varázslatos axolotl beveti az AXOLOTL REGENERÁCIÓT!`\n\n`Egy színes buborékfüggöny egy pillanatra eltakarja a szörnyet, és mire eloszlik, néhány sebe eltűnik!`",
"questAxolotlRageEffect": "A varázslatos axolotl beveti az AXOLOTL REGENERÁCIÓT!\n\nEgy színes buborékfüggöny egy pillanatra eltakarja a szörnyet, és mire eloszlik, néhány sebe eltűnik!",
"questTurtleText": "Vezesd a teknőst",
"questTurtleNotes": "Segítség! Ez a hatalmas tengeri teknős nem talál vissza a fészkelőhelyére. Minden évben ide tér vissza, hogy lerakja a tojásait, de idén az Elintézetlenség-öböl tele van mérgező feladat-szeméttel, vörös napi teendőkkel és kipipálatlan tennivalókkal. „Pánikban csapkod!” mondja @JessicaChase.<br><br>@UncommonCriminal bólint. „Azért, mert az érzékei elhomályosultak és összezavarodtak.”<br><br>@Scarabsi megragadja a karod. „Segítenél kitakarítani a feladat-szemetet az útjából? Lehet, hogy veszélyes, de segítenünk kell neki!”",
"questTurtleCompletion": "Hősies munkáddal sikerült megtisztítani a vizet, és a tengeri teknősünk végre megtalálta a partját. Te, @Bambin és @JaizakAripaik figyelitek, ahogy mélyre ássa a tojásait a homokban, hogy azok kikelhessenek, és több száz kis teknőssé váljanak. Igazi hölgyként mindannyiótoknak három tojást ad, és arra kér, hogy gondoskodjatok róluk, hogy egyszer majd nagy teknősökké nőhessenek.",
@@ -375,7 +375,7 @@
"questTaskwoodsTerror1Boss": "Tűzkoponya raj",
"questTaskwoodsTerror1RageTitle": "Raj újjászületés",
"questTaskwoodsTerror1RageDescription": "Raj újjászületés: Ez a sáv akkor töltődik fel, ha nem teljesíted a napi teendőidet. Ha megtelik, a Tűzkoponya raj visszanyeri megmaradt életerejének 30%-át!",
"questTaskwoodsTerror1RageEffect": "`A Tűzkoponya raj használja a RAJ ÚJJÁSZÜLETÉST!`\n\nMegbátorodva győzelmeiktől, még több koponya kavarog körülötted lángoló örvényben!",
"questTaskwoodsTerror1RageEffect": "A Tűzkoponya raj használja a RAJ ÚJJÁSZÜLETÉST!\n\nMegbátorodva győzelmeiktől, még több koponya kavarog körülötted lángoló örvényben!",
"questTaskwoodsTerror1DropSkeletonPotion": "Csontváz keltetőfőzet",
"questTaskwoodsTerror1DropRedPotion": "Vörös keltetőfőzet",
"questTaskwoodsTerror1DropHeadgear": "Pirokinetikus turbán (fejfedő)",
@@ -437,7 +437,7 @@
"questStoikalmCalamity1Boss": "Föld koponyaraj",
"questStoikalmCalamity1RageTitle": "Raj újraszületés",
"questStoikalmCalamity1RageDescription": "Raj újraszületés: Ez a sáv akkor töltődik, ha nem végzed el a napi teendőidet. Ha megtelik, a Föld koponyaraj 30%-ét visszanyeri a megmaradt életerejének!",
"questStoikalmCalamity1RageEffect": "`Föld Koponyaraj használja a RAJ ÚJRASZÜLETÉST!`\n\nMég több koponya tör elő a földből, foguk vacog a hidegben!",
"questStoikalmCalamity1RageEffect": "Föld Koponyaraj használja a RAJ ÚJRASZÜLETÉST!\n\nMég több koponya tör elő a földből, foguk vacog a hidegben!",
"questStoikalmCalamity1DropSkeletonPotion": "Csontváz keltetőfőzet",
"questStoikalmCalamity1DropDesertPotion": "Sivatagi keltetőfőzet",
"questStoikalmCalamity1DropArmor": "Mamutlovas páncél",
@@ -478,7 +478,7 @@
"questMayhemMistiflying1Boss": "Repülő koponyaraj",
"questMayhemMistiflying1RageTitle": "Raj újraszületés",
"questMayhemMistiflying1RageDescription": "Raj újraszületés: Ez a sáv akkor töltődik, ha nem végzed el a napi teendőidet. Ha megtelik, a Repülő koponyaraj visszanyeri megmaradt életereje 30%-át!",
"questMayhemMistiflying1RageEffect": "`A Repülő koponyaraj beveti a RAJ ÚJRASZÜLETÉST!`\n\nA győzelmektől felbátorodva még több koponya pörög elő a felhők közül!",
"questMayhemMistiflying1RageEffect": "A Repülő koponyaraj beveti a RAJ ÚJRASZÜLETÉST!\n\nA győzelmektől felbátorodva még több koponya pörög elő a felhők közül!",
"questMayhemMistiflying1DropSkeletonPotion": "Csontváz keltetőfőzet",
"questMayhemMistiflying1DropWhitePotion": "Fehér keltetőfőzet",
"questMayhemMistiflying1DropArmor": "Szivárványszínű küldöncköpeny (Páncél)",
@@ -535,7 +535,7 @@
"questLostMasterclasser3Boss": "Vákuumkoponya raj",
"questLostMasterclasser3RageTitle": "A raj újjászületése",
"questLostMasterclasser3RageDescription": "A raj újjászületése: Ez a sáv akkor töltődik, ha nem teljesíted a napi teendőidet. Ha megtelik, a vákuumkoponya raj visszanyeri megmaradt életerejének 30%-át!",
"questLostMasterclasser3RageEffect": "`A Vákuumkoponya raj alkalmazza A RAJ ÚJJÁSZÜLETÉSÉT!`\n\nA győzelmeiktől felbátorodva még több koponya zúdul alá az égből, megerősítve a rajt!",
"questLostMasterclasser3RageEffect": "A Vákuumkoponya raj alkalmazza A RAJ ÚJJÁSZÜLETÉSÉT!\n\nA győzelmeiktől felbátorodva még több koponya zúdul alá az égből, megerősítve a rajt!",
"questLostMasterclasser3DropBodyAccessory": "Éter amulett (Testkiegészítő)",
"questLostMasterclasser3DropBasePotion": "Alap keltetőfőzet",
"questLostMasterclasser3DropGoldenPotion": "Arany keltetőfőzet",
@@ -548,7 +548,7 @@
"questLostMasterclasser4Boss": "Anti'Zinnya",
"questLostMasterclasser4RageTitle": "Üresség kifacsarása",
"questLostMasterclasser4RageDescription": "Üresség kifacsarása: Ez a sáv akkor telik meg, ha nem teljesíted a napi teendőidet. Ha betelik, Anti'Zinnya kiszívja a csapatod manáját!",
"questLostMasterclasser4RageEffect": "`Anti'Zinnya alkalmazza az ÜRESSÉG KIFACSARÁSÁT!` Az Éteri kitörés varázslat egy kifacsart változataként érzed, ahogy a mágiád elszivárog a sötétségbe!",
"questLostMasterclasser4RageEffect": "Anti'Zinnya alkalmazza az ÜRESSÉG KIFACSARÁSÁT! Az Éteri kitörés varázslat egy kifacsart változataként érzed, ahogy a mágiád elszivárog a sötétségbe!",
"questLostMasterclasser4DropBackAccessory": "Éterköpeny (Hátkiegészítő)",
"questLostMasterclasser4DropWeapon": "Éterkristályok (Kétkezes fegyver)",
"questLostMasterclasser4DropMount": "Láthatatlan éter hátas",
@@ -704,7 +704,7 @@
"questSilverNotes": "A nemrég felfedezett bronz keltetőfőzet a habiticaiak között beszédtéma lett. Lehetséges, hogy még fényesebb fémekből is léteznek főzetek? @QuartzFox és @starsystemic társaságában a Habit város központi könyvtárába mész, és hatalmas halom alkímiáról szóló könyvet gyűjtötök össze tanulmányozásra.<br><br>Órákig tartó szemfárasztó kutatás után @QuartzFox egy nem éppen könyvtárba illő diadalittas kiáltást hallat. „Aha! Megtaláltam!” Odasietsz, hogy megnézd. „Ezüst keltetőfőzetet lehet készíteni a rák csillagjegy rúnáival, amelyeket tiszta ezüstben kell feloldani, miután holdrúnák erejével átitatott láng fölött megolvasztottuk.”<br><br>„Sok hozzávalóra lesz szükségünk” töpreng @starsystemic. „Hátha elrontunk egy-két próbát.”<br><br>„Csak egy hely van, ahol ekkora mennyiségben találhatunk ilyen különös alkímiai anyagokat” szólal meg @Edge, aki karba tett kézzel áll a könyvespolcok árnyékában. Vajon végig ott volt? „A robotolás kazamatái. Induljunk!”",
"questAmberBoss": "Trerezin",
"questRubyNotes": "A Stoïkalm-vulkánok általában nyüzsgő csúcsai most csendesen pihennek a hó alatt. „Gondolom, a túrázók és a látogatók téli álmot alszanak” mondja @gully neked és @Aspiring_Advocate-nek. „Ez megkönnyíti a keresésünket.”<br><br>Ahogy eléritek a hegytetőt, a jeges szél összeolvad a kráterből gomolygó gőzzel. „Ott!” kiált fel @Aspiring_Advocate, egy forró forrás felé mutatva. „Hol máshol találhatnánk meg a hűvös vízöntő rúnákat és a szenvedélyes vénusz rúnákat, mint ott, ahol a jég és a tűz találkozik?”<br><br>Hárman sietve indultok a forró forráshoz. „A kutatásaim szerint” mondja @Aspiring_Advocate , „ha ezeket a rúnákat szív alakú rubinokkal kombináljuk, egy olyan keltetőfőzetet hozhatunk létre, amely elősegíti a barátságot és a szeretetet!”<br><br>Izgatottan a felfedezés lehetőségétől mindannyian elmosolyodtok. „Rendben” mondja @gully , „kezdjük el a keresést!”",
"questWaffleRageEffect": "`Borzalmas Gofri használja a JUHARSZIRUP CSAPDÁT!` A ragacsos szirup lelassítja a csapásaidat és varázslataidat! A felhalmozott sebzés csökken.",
"questWaffleRageEffect": "Borzalmas Gofri használja a JUHARSZIRUP CSAPDÁT! A ragacsos szirup lelassítja a csapásaidat és varázslataidat! A felhalmozott sebzés csökken.",
"questRubyUnlockText": "Elérhetővé teszi a rubin keltetőfőzetek vásárlását a piacon",
"questDolphinUnlockText": "Elérhetővé teszi a delfin tojások vásárlását a piacon",
"questFluoriteCompletion": "Ahogy harcoltok, a kristálylény egyre jobban elmerül a fények játékában, amelyet teremtetek. „Olyan… csillogó…” mormolja elbűvölten.<br><br>„Persze!” kiált fel @nirbhao. „Ez egy fluorit elementál! Csak fényre van szüksége, hogy igazán ragyogjon. Segítsünk neki tündökölni!”<br><br>Az elementál boldogan kuncog és egyre fényesebben izzik, miközben még több fáklyát és mágikus fényforrást gyújtotok. Olyan hálás, hogy újra ragyoghat, hogy cserébe elvezet titeket egy gazdag fluorit-lelőhelyhez.<br><br>„Ez tökéletes alapanyag egy új keltetőfőzethez” mondja @nirbhao. „Egy olyan főzethez, amely kisállatainkat éppoly fénylővé teszi, mint az új fluoreszkáló barátunk.”",
@@ -810,14 +810,14 @@
"questVirtualPetBoss": "Wotchimon",
"questVirtualPetRageTitle": "A csipogás veszélye",
"questVirtualPetRageDescription": "Ez a sáv akkor töltődik, ha nem teljesíted a napi teendőidet. Ha betelik, Wotchimon csökkenti a csapat által felhalmozott sebzést!",
"questVirtualPetRageEffect": "`Wotchimon használja a zavaró csipogást!` Wotchimon éles csipogással jelez, és boldogságsávja hirtelen eltűnik! A felhalmozott sebzés csökken.",
"questVirtualPetRageEffect": "Wotchimon használja a zavaró csipogást! Wotchimon éles csipogással jelez, és boldogságsávja hirtelen eltűnik! A felhalmozott sebzés csökken.",
"questVirtualPetDropVirtualPetPotion": "Virtuális kisállat keltetőfőzet",
"questVirtualPetUnlockText": "Elérhetővé teszi a virtuális kisállat keltetőfőzetek vásárlását a piacon",
"questPinkMarbleText": "Nyugtasd meg a megtébolyult Ámort",
"questPinkMarbleNotes": "Az a hír járja, hogy a Kanyargó-hegységben van egy barlang, amelyből rózsaszín por és kődarabok lövellnek ki. A csapatoddal elindultok, hogy kiderítsétek az igazságot. Ahogy közeledtek, valóban hatalmas rózsaszín porfelhőt láttok és furcsa módon egy apró hang harci kiáltása csendül fel, amit kőzúzás hangja követ.<br><br>@Empress42 véletlenül belélegzi a port, és hirtelen álmodozóvá és kevésbé produktívvá válik. „Nálam is ugyanez!” mondja @QuartzFox. „Egyszer csak azon kaptam magam, hogy egy alig ismert személyről fantáziálok!”<br><br>@a_diamond bekukkant a barlangba, és egy apró lényt lát cikázni, amint rózsaszín márványköveket zúz porrá. „Fedezékbe! Ez az Ámor megtébolyult és a varázserejét arra használja, hogy megszállott rajongást és hamis illúziókat keltsen! Le kell állítanunk!”",
"questPinkMarbleBoss": "Ámor",
"questPinkMarbleRageDescription": "Ez a sáv akkor töltődik, ha nem teljesíted a napi teendőidet. Ha betelik, Ámor csökkenti a csapat által felhalmozott sebzést!",
"questPinkMarbleRageEffect": "`Ámor használja a RÓZSASZÍN ÖKLÖST!` Hát, ez nem volt valami gyengéd! A csapattársaid hátrahőkölnek. A felhalmozott sebzés csökken.",
"questPinkMarbleRageEffect": "Ámor használja a RÓZSASZÍN ÖKLÖST! Hát, ez nem volt valami gyengéd! A csapattársaid hátrahőkölnek. A felhalmozott sebzés csökken.",
"questPinkMarbleDropPinkMarblePotion": "Rózsaszín márvány keltetőfőzet",
"questPinkMarbleRageTitle": "Rózsaszín öklös",
"questOtterText": "A ravasz Tervmester!",
@@ -860,5 +860,14 @@
"questOpalCollectMercuryRunes": "Merkúr rúna",
"questOpalCollectLibraRunes": "Mérleg rúna",
"questOpalDropOpalPotion": "Opál keltetőfőzet",
"questOpalUnlockText": "Elérhetővé teszi az opál keltetőfőzetek megvásárlását a piacon"
"questOpalUnlockText": "Elérhetővé teszi az opál keltetőfőzetek megvásárlását a piacon",
"questAlienRageDescription": "Ez a sáv akkor töltődik fel, amikor nem teljesíted a napi teendőidet. Amikor tele van, a földönkívüli elkedvetlenít azzal, hogy visszanyeri egy részét az életerejének!",
"questAlienNotes": "Furcsa napok voltak ezek a Habiticában. A nagy repülő csészealj még mindig ott lebeg a Virágzó Mezők közelében. Furcsán zümmög. Miért időzik ott? Az április bolondja már elmúlt, és a tolvajok mesterének ideje a reflektorfényben is véget ért.<br><br>Az űrhajó fénye felé vándorolsz. Megnézheted, és közben még egy pár lépést is tehetsz.<br><br>Ahogy közelebb érsz, meglátod az Április bolondját, aki kissé komor képet vág. Az arca zöldesen csillog a hajó fénysugarában.<br><br>\"Az volt a tervem, hogy szerezek egy kis főzetet mindenkinek, egy kis ajándékot, hogy újra élvezhessenek a kis földönkívüli cimborákkal! De egyszerűen nem tudom felszedni a bátorságot... Azt hiszem, tudom, miért\" - mondja a Bolond, a fénysugár felé bólintva.<br><br>Kis szimbólumokat szívnak fel a hajóba. Ezek mind a teljesített feladataid! Nem csoda, hogy lanyhult a motivációd.<br><br>\"A motivációnkat rabolják el!\" - kiáltod fel. \"Meg kell mentenünk, mielőtt valahol a világűr mélyén köt ki!\"<br><br>A Bolond elmosolyodik. \"Összpontosítsd a gondolataidat azokra a feladatokra, amiket tudod, hogy be kell fejezned! A többit intézem egy kis mágiával.\"",
"questAlienText": "A motivációrablók inváziója",
"questAlienCompletion": "Sikerült visszaszerezned az ellopott motivációt az elszántságoddal és a Bolond varázserejével. Ahogy érzed, hogy visszatér a lendületed, az UFO leereszkedik, és egy rámpa lassan kijön belőle, vele együtt egy nagy, zöld, egyszemű lény. Bár furcsán néz ki, nem tűnik fenyegetőnek.<br><br>\"Úgy tűnik, egy kicsit túlzásba vittük, amikor megpróbáltunk egy kis extra bátorítást aratni a szép városotokból\" - mondja. \"Elnézést ezért, és fantasztikus munka volt visszaszerezni. A erőfeszítéseitek extra aurája tulajdonképpen annyira feltöltötte a hajó hajtóművét, hogy hazajuthatunk! Kérlek, fogadjátok el ezeket a köszönetünkkel.\"<br><br>\"Óh, főzetek\" - mondja a Bolond -, \"milyen bájos, és milyen kényelmes nekem, hogy már készen állnak!\"",
"questAlienBoss": "Bátorításrabló, a földönkívüli",
"questAlienRageTitle": "Galaktikus akadály",
"questAlienRageEffect": "A Bátorításrabló használja a Galaktikus akadályt! Egyenesen a hipertéren keresztül csúsztál vissza. Az ellenfeled visszanyeri az ÉP-jét!",
"questAlienDropAlienPotion": "Földönkívüli kikeltetőfőzet",
"questAlienUnlockText": "Feloldja a földönkívüli kikeltetőfőzetet megvásárlásra a piacon"
}
+10 -3
View File
@@ -1,8 +1,8 @@
{
"rebirthNew": "Újjászületés: új kaland érhető el!",
"rebirthUnlock": "Feloldottad az újjászületést! Ez a különleges bolti tárgy lehetővé teszi, hogy új játékot kezdj az 1. szinttől, megtartva a feladataidat, kitüntetéseidet, háziállataidat és még sok mást. Használd arra, hogy új életet lehelj a Habitica-ba, ha úgy érzed, hogy már mindent elértél, vagy hogy új funkciókat tapasztalj meg egy kezdő karakter friss szemeivel!",
"rebirthAchievement": "Új kalandot kezdtél! Ez a <%= number %>. újjászületésed és a <%= level %>. szint a legmagasabb, amit elértél. Hogy halmozd ezt a kitüntetést kezdj új kalandot amikor elértél egy még magasabb szintet!",
"rebirthAchievement100": "Új kalandot kezdtél! Ez a <%= number %>. újjászületésed, és a legmagasabb szint amit elértél 100, vagy annál több. Hogy halmozd ezt a kitüntetést kezdj új kalandot, amikor 100. vagy magasabb szinten vagy!",
"rebirthAchievement": "<strong><%= number %>.</strong> alkalommal használtad az Újjászületés Gömbjét, és a legmagasabb elért szinted <strong><%= level %></strong>.",
"rebirthAchievement100": "<strong><%= number %>.</strong> alkalommal használtad az Újjászületés Gömbjét, és a legmagasabb elért szinted <strong>100</strong> vagy magasabb.",
"rebirthBegan": "Új kalandot kezdtél",
"rebirthText": "Eddig <%= rebirths %> új kalandot kezdtél",
"rebirthOrb": "Használtad az újjászületés gömbjét hogy új kalandot kezdj miután elérted a <%= level %>. szintet.",
@@ -11,5 +11,12 @@
"rebirthPop": "Azonnal újrakezdheted a karaktered 1. szintű Harcosként, miközben megtartod az elért kitüntetéseidet, gyűjteményeidet és felszerelésedet. A feladataid és azok előzményei megmaradnak, de mind sárgára lesznek visszaállítva. A szériáid törlődnek, kivéve az aktív kihívásokhoz és Csoportos csomagokhoz tartozó feladatok esetében. Az aranyad, tapasztalatod, manád és az összes képesség hatása eltűnik. Mindez azonnal életbe lép.",
"rebirthName": "Újjászületés gömbje",
"rebirthComplete": "Újjászülettél!",
"nextFreeRebirth": "<strong><%= days %> nap </strong> amíg <strong> INGYENES </strong> újjászületés gömbjét szerzel"
"nextFreeRebirth": "<strong><%= days %> nap </strong> amíg <strong> INGYENES </strong> újjászületés gömbjét szerzel",
"rebirthAchievementPlural": "<strong><%= number %>.</strong> alkalommal használtad az Újjászületés Gömbjét, és a legmagasabb elért szinted <strong><%= level %></strong>.",
"rebirthUnlockedNewItem": "Újjászületés Gömbje feloldva",
"rebirthUnlockedOrb": "Új kaland vár rád!",
"rebirthUnlockedDesc": "Használd az Újjászületés Gömbjét, hogy új életet lehelj Habitica kalandodba, amikor úgy érzed, mindent elértél! Kezdd újra az 1. szinten, miközben megtartod feladataidat, kitüntetéseidet és kisállataidat ezzel a különleges tárggyal, amit a Piactéren találsz.",
"rebirthNewAchievement": "Új kitüntetés",
"rebirthNewAdventure": "Új kaland kezdődik most!",
"rebirthStackInfo": "Ez a kitüntetés minden alkalommal halmozódik, amikor használod az Újjászületés Gömbjét."
}
+4 -1
View File
@@ -274,5 +274,8 @@
"subscriptionBillingFYI": "Az előfizetések automatikusan megújulnak, hacsak nem mondod le legalább 24 órával az aktuális időszak vége előtt. Az előfizetésedet a beállítások Előfizetés fülén kezelhetsz. A fiókodat 24 órán belül terhelik a megújítási dátumtól számítva, ugyanazon az áron, amit eredetileg fizettél.",
"mysterySet202512": "Sütemény bajnok szett",
"mysterySet202601": "Tél védőpajzsa szett",
"mysterySet202602": "Cseresznyevirág róka szett"
"mysterySet202602": "Cseresznyevirág róka szett",
"mysterySet202603": "Lilaakác varázsló szett",
"mysterySet202604": "Merész űrhajós szett",
"mysterySet202605": "Alkonyati felhőfátyol szett"
}
+14 -3
View File
@@ -860,11 +860,11 @@
"backgroundRainyRainforestNotes": "雨降る熱帯雨林で爽やかな豪雨を楽しみましょう。",
"backgrounds042024": "セット119 2024年4月リリース",
"backgroundContainerGardenNotes": "鉢植え園芸で手を汚しましょう。",
"backgroundMaypoleNotes": "五月祭の柱の周りで踊りましょう。",
"backgroundMaypoleNotes": "メイポールの周りで踊りましょう。",
"backgrounds052024": "セット1202024年5月リリース",
"backgroundDragonsBackText": "ドラゴンの背中",
"backgroundDragonsBackNotes": "ドラゴンの背中で空を飛びましょう。",
"backgroundMaypoleText": "五月祭の柱",
"backgroundMaypoleText": "メイポール",
"backgroundPottersStudioText": "陶芸家のスタジオ",
"backgroundPottersStudioNotes": "陶芸家のスタジオで作品を作りましょう。",
"backgrounds062024": "セット1212024年6月リリース",
@@ -930,5 +930,16 @@
"backgroundWinterDesertWithSaguarosNotes": "冬の砂漠に並ぶサボテンの間で、冷たい空気を味わいましょう。",
"backgrounds022026": "セット1412026年2月リリース",
"backgroundElegantPalaceText": "立派なパレス",
"backgroundElegantPalaceNotes": "立派なパレスの、カラフルな広間を歩きましょう。"
"backgroundElegantPalaceNotes": "立派なパレスの、カラフルな広間を歩きましょう。",
"backgroundRidingACometNotes": "彗星を乗りながら宇宙を旅しましょう!",
"backgrounds032026": "セット1422026年3月リリース",
"backgroundWaterfallWithRainbowText": "滝にかかる虹",
"backgroundWaterfallWithRainbowNotes": "滝にかかる虹の圧倒的な美しさを眺めましょう。",
"backgrounds042026": "セット1432026年4月リリース",
"backgroundRidingACometText": "彗星に着陸",
"backgrounds052026": "セット1442026年5月リリース",
"backgroundElvenCitadelText": "エルフの城塞",
"backgroundElvenCitadelNotes": "エルフの城塞まで、景色のいい道を歩きましょう。",
"backgroundOnAStrangePlanetText": "不思議な惑星",
"backgroundOnAStrangePlanetNotes": "Habiticaの住民の行ったことない、不思議な惑星を冒険しましょう。"
}
+2 -1
View File
@@ -410,5 +410,6 @@
"questEggPlatypusMountText": "カモノハシ",
"questEggPlatypusAdjective": "完璧主義者",
"hatchingPotionBalloon": "風船の",
"hatchingPotionOpal": "オパールの"
"hatchingPotionOpal": "オパールの",
"hatchingPotionAlien": "エイリアン"
}
+1 -1
View File
@@ -202,7 +202,7 @@
"subscriptionDetail002": "有料会員は、定期支払いの翌月まで神秘の砂時計を待つ必要がなくなります。",
"subscriptionDetail01": "新しく12か月の有料会員購入時一回、12個追加で神秘の砂時計を手に入れることができます。",
"subscriptionDetail010": "これはすべての有料会員が毎月1個ずつもらえる神秘の砂時計に追加してもらえます。",
"webFaqAnswer67": "「クラス」とは、あなたが選べるキャラクタータイプであり、レベルアップすると、それぞれ固有の能力やスキルが使えるようになります。このスキルはタスクをするときや、パーティーでクエストをするときに役に立ちます。\n\nクラスによって、ごほうび、市場と季節の店で買える装備も違います。\n\n自分のプレイスタイルに合うクラスが選べるように、ここで各クラスの概要を説明します:\n#### **戦士**\n* 戦士はボスに大きなダメージを与え、クリティカルヒットのチャンスが高いので、タスクを完了するときに経験値やゴールドがたくさん獲得できます。\n* メインの属性は力で、ボスに与えられるダメージを高めます。\n* 二番目の属性は体質で、ボスから受けるダメージを減らします。\n* 戦士のスキルは、パーティーメンバーの力と体質を向上させます。\n* このクラスはボスと戦うことが好きだが、タスクをやり忘れてしまった時のために保護が欲しい人におすすめです。\n#### **治療師**\n* 治療師は守りの力が強く、自分またはパーティーメンバーを治療することができます。\n* メインの属性は体質で、治療を強めて、ボスから受けるダメージを減らします。\n* 二番目の属性は知能で、マナと経験値を増やします。\n* 治療師のスキルは、タスクの色を青くすることと、パーティーメンバーの体質を向上させることです。\n* このクラスはよくタスクをやり忘れて、自分やパーティーメンバーの体力を回復する能力が必要な人におすすめです。\n#### **魔士**\n* 魔士は速くレベルアップし、マナをたくさん獲得して、クエストのボスにダメージを与えます。\n* メインの属性は知能で、マナと経験値を増やします。\n* 二番目の属性は知覚で、ゴールドとアイテムがたくさん獲得できます。\n* 魔士のスキルは、タスクの連続実行回数を保留したり、パーティーメンバーのマナや知能を向上させたりすることです。\n* このクラスはレベルアップやる気が出て、ボスにダメージを与えたい人におすすめです。\n#### **盗賊**\n* 盗賊はタスクを完了するときに一番多くアイテムとゴールドが獲得でき、クリティカルヒットのチャンスが高いので、更に多くの経験値とゴールドが獲得できます。\n* メインの属性は知覚で、獲得できるゴールドとアイテムを増やします。\n* 二番目の属性は力で、ボスに与えられるダメージを増やします。\n* 盗賊のスキルは、完了しなかった日課から受けるダメージを避けること、ゴールドをたくさん取ること、パーティーメンバーの知覚を向上させることです。\n* このクラスはごほうびがモチベーションになる人におすすめです。",
"webFaqAnswer67": "「クラス」とは、あなたが選べるキャラクタータイプであり、レベルアップすると、それぞれ固有の能力やスキルが使えるようになります。このスキルはタスクをするときや、パーティーでクエストをするときに役に立ちます。\n\nクラスによって、ごほうび、市場と季節の店で買える装備も違います。\n\n自分のプレイスタイルに合うクラスが選べるように、ここで各クラスの概要を説明します:\n#### **戦士**\n* 戦士はボスに大きなダメージを与え、クリティカルヒットのチャンスが高いので、タスクを完了するときに経験値やゴールドがたくさん獲得できます。\n* メインの属性は力で、ボスに与えられるダメージを高めます。\n* 二番目の属性は体質で、ボスから受けるダメージを減らします。\n* 戦士のスキルは、パーティーメンバーの力と体質を向上させます。\n* このクラスはボスと戦うことが好きだが、タスクをやり忘れてしまった時のために保護が欲しい人におすすめです。\n#### **治療師**\n* 治療師は守りの力が強く、自分またはパーティーメンバーを治療することができます。\n* メインの属性は体質で、治療を強めて、ボスから受けるダメージを減らします。\n* 二番目の属性は知能で、マナと経験値を増やします。\n* 治療師のスキルは、タスクの色を青くすることと、パーティーメンバーの体質を向上させることです。\n* このクラスはよくタスクをやり忘れて、自分やパーティーメンバーの体力を回復する能力が必要な人におすすめです。\n#### **魔士**\n* 魔士は速くレベルアップし、マナをたくさん獲得して、クエストのボスにダメージを与えます。\n* メインの属性は知能で、マナと経験値を増やします。\n* 二番目の属性は知覚で、ゴールドとアイテムがたくさん獲得できます。\n* 魔士のスキルは、タスクの連続実行回数を保留したり、パーティーメンバーのマナや知能を向上させたりすることです。\n* このクラスはレベルアップからやる気が出て、ボスにダメージを与えたい人におすすめです。\n#### **盗賊**\n* 盗賊はタスクを完了するときに一番多くアイテムとゴールドが獲得でき、クリティカルヒットのチャンスが高いので、更に多くの経験値とゴールドが獲得できます。\n* メインの属性は知覚で、獲得できるゴールドとアイテムを増やします。\n* 二番目の属性は力で、ボスに与えられるダメージを増やします。\n* 盗賊のスキルは、完了しなかった日課から受けるダメージを避けること、ゴールドをたくさん取ること、パーティーメンバーの知覚を向上させることです。\n* このクラスはごほうびがモチベーションになる人におすすめです。",
"subscriptionDetail011": "現在、12ヶ月の継続有料プランに加入しているプレイヤーは、この変更が実施される日にボーナスを受け取ることができます。",
"subscriptionDetail012": "このボーナスは、プレゼントされた有料プランには適用されません。",
"subscriptionHeading1": "有料会員のジェムへの変更",

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