Compare commits

..

333 Commits

Author SHA1 Message Date
Phillip Thelen 23059231ce start building out more sophisticated rate limiting 2024-07-22 17:51:12 +02:00
Phillip Thelen 23163043c2 correct math 2024-07-19 11:41:16 +02:00
Phillip Thelen a18b8265a5 fix tests and add new one 2024-07-19 11:18:32 +02:00
Phillip Thelen ce1db6923b make rate limiter config names more consistent 2024-07-19 11:18:15 +02:00
Phillip Thelen 2465189fb1 Improve rate limiting 2024-07-18 18:49:58 +02:00
Sabe Jones 04554c5309 5.26.1 2024-07-03 14:23:37 -05:00
Phillip Thelen 5ef88b5c56 Update releaseDates.js 2024-07-03 18:52:23 +02:00
Weblate 892c4934d5 Translated using Weblate (Japanese)
Currently translated at 99.2% (3080 of 3103 strings)

Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translation: Habitica/Gear
2024-07-01 22:51:54 +02:00
Sabe Jones b90457c04f fix(test): correct hatching potion test 2024-06-28 11:20:21 -05:00
Sabe Jones 379d98a91e fix(test): correct schedule test 2024-06-28 11:09:09 -05:00
Sabe Jones 07352480cd Merge remote-tracking branch 'origin/phillip/memoize-me' into develop 2024-06-28 10:26:38 -05:00
Phillip Thelen 1fb44bbe73 fix naming 2024-06-28 10:20:11 -05:00
Phillip Thelen 5323849f90 fix naming 2024-06-28 17:16:45 +02:00
Sabe Jones 034327f647 5.26.0 2024-06-28 09:55:53 -05:00
Sabe Jones de9aac0988 Squashed commit of the following:
commit 8309686922
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 25 15:41:41 2024 -0400

    melior updates - loading screen & menu bar

commit 53608dd688
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 24 22:12:09 2024 +0200

    fix food

commit eecae86454
Merge: 95c562fdbc 960e262f19
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 24 16:05:04 2024 -0400

    Merge branch '2024-07-content-prebuild' into subs-private

commit 960e262f19
Merge: c5bbadaacd 7ec8b84b01
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 24 16:03:40 2024 -0400

    Merge branch '2024-07-content-prebuild' of https://github.com/HabitRPG/habitica-private into 2024-07-content-prebuild

commit c5bbadaacd
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 24 16:03:23 2024 -0400

    add missing string info to July mystery items

commit 7ec8b84b01
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 24 11:29:17 2024 -0400

    add missing info to mystery item strings

commit 95c562fdbc
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Jun 21 11:12:18 2024 +0200

    Fix serving memoized content

commit 877fe48225
Author: Phillip Thelen <phillip@habitica.com>
Date:   Thu Jun 20 12:23:24 2024 +0200

    correctly memoize conent api

commit e0f6f79c5b
Author: Phillip Thelen <phillip@habitica.com>
Date:   Thu Jun 20 10:11:27 2024 +0200

    don’t build multiple times on heroku

commit f62254d68e
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 19:40:20 2024 +0200

    fix client command

commit d054e6fc16
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 19:36:57 2024 +0200

    correct build call

commit 7231f699c1
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 19:32:32 2024 +0200

    try setting up with heroku buildpack

commit 1dae0793fd
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:50:32 2024 +0200

    call gulp build:prod

commit f18fbe86b6
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:40:53 2024 +0200

    build client

commit 61a61724ca
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:33:18 2024 +0200

    testing

commit 93cf30eb18
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:20:25 2024 +0200

    integration fix

commit cff08adcd0
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:13:20 2024 +0200

    specify dev docker file

commit 4da2ed4a1f
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:10:07 2024 +0200

    initialize stub

commit 11c5b26c59
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:08:45 2024 +0200

    test heroku file

commit ac85bb2e2d
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:03:15 2024 +0200

    fix stub reference

commit 74dfb2710f
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:01:27 2024 +0200

    test fixes

commit 8dbd3c3db1
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:37:04 2024 +0200

    fix canOwn test

commit 74b3b348ff
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:32:31 2024 +0200

    fix buy test

commit 3386d61fde
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:30:37 2024 +0200

    fix debug tests

commit 19da14531c
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:05:25 2024 +0200

    add chameleon to featured quests

commit 254dd80f24
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:05:14 2024 +0200

    fix import

commit 0bc3f16b4b
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:33:22 2024 +0200

    add new content to new release file

commit 5184973bd5
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:33:11 2024 +0200

    fix release date tests

commit b6accca5ca
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:33:06 2024 +0200

    fix armoire tests

commit fec68e6211
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:02:03 2024 +0200

    fix tests

commit fc63c906dd
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 10 14:44:21 2024 +0200

    Improve test coverage

commit 3333f8f0f5
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 10 14:24:59 2024 +0200

    allow hatching potions to have a release date

commit 89a3ac3dde
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 10 14:11:38 2024 +0200

    allow eggs to have a release date

    # Conflicts:
    #	test/content/armoire.test.js

commit 16551ec83f
Merge: f5f4974a73 2645bf6023
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 15:03:12 2024 -0400

    Merge branch '2024-07-content-prebuild' into subs-private

commit 2645bf6023
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 15:02:47 2024 -0400

    update habitica images

commit f5f4974a73
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 14:58:13 2024 -0400

    update habitica-images

commit 162e337d14
Merge: f2506c3231 21a7d36b7b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:46:03 2024 -0400

    Merge branch '2024-07-content-prebuild' into subs-private

commit 21a7d36b7b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:45:09 2024 -0400

    update sprites

commit f2506c3231
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:24:21 2024 -0400

    updated sprites css

commit d47641e25a
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 12:46:59 2024 -0400

    typo fix

commit fb8479ad1e
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 17 13:44:36 2024 -0400

    finish July prebuild

commit 3810cf3ef3
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Jun 14 10:42:47 2024 -0400

    add chameleon quest

commit d05da3722c
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 17:12:43 2024 -0400

    add June background notes

commit b8a3440ef2
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 16:40:04 2024 -0400

    fix mystery item and background description

commit 44d63032d8
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 15:38:23 2024 -0400

    add subscriber gear, enchanted armoire, and background

commit 9d7da91ec6
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 14:44:59 2024 -0400

    add sprites
2024-06-28 09:55:41 -05:00
Sabe Jones f55d836398 Squashed commit of the following:
commit 960e262f19
Merge: c5bbadaacd 7ec8b84b01
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 24 16:03:40 2024 -0400

    Merge branch '2024-07-content-prebuild' of https://github.com/HabitRPG/habitica-private into 2024-07-content-prebuild

commit c5bbadaacd
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 24 16:03:23 2024 -0400

    add missing string info to July mystery items

commit 7ec8b84b01
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 24 11:29:17 2024 -0400

    add missing info to mystery item strings

commit 2645bf6023
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 15:02:47 2024 -0400

    update habitica images

commit 21a7d36b7b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:45:09 2024 -0400

    update sprites

commit d47641e25a
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 12:46:59 2024 -0400

    typo fix

commit fb8479ad1e
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 17 13:44:36 2024 -0400

    finish July prebuild

commit 3810cf3ef3
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Jun 14 10:42:47 2024 -0400

    add chameleon quest

commit d05da3722c
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 17:12:43 2024 -0400

    add June background notes

commit b8a3440ef2
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 16:40:04 2024 -0400

    fix mystery item and background description

commit 44d63032d8
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 15:38:23 2024 -0400

    add subscriber gear, enchanted armoire, and background

commit 9d7da91ec6
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 14:44:59 2024 -0400

    add sprites
2024-06-28 09:50:28 -05:00
Sabe Jones 287014518d Squashed commit of the following:
commit 28193f86fb
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Jun 21 11:12:18 2024 +0200

    Fix serving memoized content

commit 877fe48225
Author: Phillip Thelen <phillip@habitica.com>
Date:   Thu Jun 20 12:23:24 2024 +0200

    correctly memoize conent api

commit e0f6f79c5b
Author: Phillip Thelen <phillip@habitica.com>
Date:   Thu Jun 20 10:11:27 2024 +0200

    don’t build multiple times on heroku

commit f62254d68e
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 19:40:20 2024 +0200

    fix client command

commit d054e6fc16
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 19:36:57 2024 +0200

    correct build call

commit 7231f699c1
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 19:32:32 2024 +0200

    try setting up with heroku buildpack

commit 1dae0793fd
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:50:32 2024 +0200

    call gulp build:prod

commit f18fbe86b6
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:40:53 2024 +0200

    build client

commit 61a61724ca
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:33:18 2024 +0200

    testing

commit 93cf30eb18
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:20:25 2024 +0200

    integration fix

commit cff08adcd0
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:13:20 2024 +0200

    specify dev docker file

commit 4da2ed4a1f
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:10:07 2024 +0200

    initialize stub

commit 11c5b26c59
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:08:45 2024 +0200

    test heroku file

commit ac85bb2e2d
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:03:15 2024 +0200

    fix stub reference

commit 74dfb2710f
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:01:27 2024 +0200

    test fixes

commit 8dbd3c3db1
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:37:04 2024 +0200

    fix canOwn test

commit 74b3b348ff
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:32:31 2024 +0200

    fix buy test

commit 3386d61fde
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:30:37 2024 +0200

    fix debug tests

commit 19da14531c
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:05:25 2024 +0200

    add chameleon to featured quests

commit 254dd80f24
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:05:14 2024 +0200

    fix import

commit 0bc3f16b4b
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:33:22 2024 +0200

    add new content to new release file

commit 5184973bd5
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:33:11 2024 +0200

    fix release date tests

commit b6accca5ca
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:33:06 2024 +0200

    fix armoire tests

commit fec68e6211
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:02:03 2024 +0200

    fix tests

commit fc63c906dd
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 10 14:44:21 2024 +0200

    Improve test coverage

commit 3333f8f0f5
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 10 14:24:59 2024 +0200

    allow hatching potions to have a release date

commit 89a3ac3dde
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 10 14:11:38 2024 +0200

    allow eggs to have a release date

    # Conflicts:
    #	test/content/armoire.test.js

commit 16551ec83f
Merge: f5f4974a73 2645bf6023
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 15:03:12 2024 -0400

    Merge branch '2024-07-content-prebuild' into subs-private

commit 2645bf6023
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 15:02:47 2024 -0400

    update habitica images

commit f5f4974a73
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 14:58:13 2024 -0400

    update habitica-images

commit 162e337d14
Merge: f2506c3231 21a7d36b7b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:46:03 2024 -0400

    Merge branch '2024-07-content-prebuild' into subs-private

commit 21a7d36b7b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:45:09 2024 -0400

    update sprites

commit f2506c3231
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:24:21 2024 -0400

    updated sprites css

commit d47641e25a
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 12:46:59 2024 -0400

    typo fix

commit fb8479ad1e
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 17 13:44:36 2024 -0400

    finish July prebuild

commit 3810cf3ef3
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Jun 14 10:42:47 2024 -0400

    add chameleon quest

commit d05da3722c
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 17:12:43 2024 -0400

    add June background notes

commit b8a3440ef2
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 16:40:04 2024 -0400

    fix mystery item and background description

commit 44d63032d8
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 15:38:23 2024 -0400

    add subscriber gear, enchanted armoire, and background

commit 9d7da91ec6
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 14:44:59 2024 -0400

    add sprites
2024-06-28 09:49:08 -05:00
Weblate b46e2da61b Merge branch 'develop' of github.com:HabitRPG/habitica into develop 2024-06-28 16:41:20 +02:00
Emilia Skoglund ef47d6cf0b Translated using Weblate (Swedish)
Currently translated at 80.0% (619 of 773 strings)

Translation: Habitica/Questscontent
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/sv/
2024-06-28 05:28:46 +02:00
Daniel Faria Gomes 1f5d66cd58 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (773 of 773 strings)

Translation: Habitica/Questscontent
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
2024-06-28 05:28:46 +02:00
Natalie Luhrs a88602a21f Translated using Weblate (Japanese)
Currently translated at 100.0% (773 of 773 strings)

Translation: Habitica/Questscontent
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
2024-06-27 20:24:32 +02:00
Sabe Jones 760c05df5d 5.25.8 2024-06-26 12:36:32 -05:00
Phillip Thelen 26d070f2c3 improve loggly request logging call (#15259) 2024-06-26 12:30:13 -05:00
Petal Forrest bc9577439e Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (286 of 286 strings)

Translation: Habitica/Limited
Translate-URL: https://translate.habitica.com/projects/habitica/limited/en_GB/
2024-06-22 10:40:51 +02:00
Petal Forrest 10cd596f0b Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (427 of 427 strings)

Translation: Habitica/Groups
Translate-URL: https://translate.habitica.com/projects/habitica/groups/en_GB/
2024-06-22 10:40:50 +02:00
Toro Mor d180062ad2 Translated using Weblate (German)
Currently translated at 91.9% (2852 of 3103 strings)

Translation: Habitica/Gear
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
2024-06-22 10:40:50 +02:00
Petal Forrest bfacf4b36e Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (259 of 259 strings)

Translation: Habitica/Settings
Translate-URL: https://translate.habitica.com/projects/habitica/settings/en_GB/
2024-06-22 10:40:50 +02:00
Weblate 2912f31dec Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (233 of 233 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 76.1% (325 of 427 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (378 of 378 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 97.3% (184 of 189 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 97.3% (184 of 189 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 99.8% (772 of 773 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 96.8% (183 of 189 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 79.6% (691 of 868 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (167 of 167 strings)

Co-authored-by: Petal Forrest <adrijanaskar2008backup@gmail.com>
Co-authored-by: Razi H <razi.haj@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/en_GB/
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/communityguidelines/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/content/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/front/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/en_GB/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Contrib
Translation: Habitica/Front
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2024-06-22 01:12:00 +02:00
Sabe Jones c47b287a89 5.25.7 2024-06-21 13:28:34 -05:00
Sabe Jones 3aa626d2ae fix(sprites): remove potion PNG override 2024-06-21 13:28:29 -05:00
Sabe Jones 647ee2a073 5.25.6 2024-06-21 11:56:07 -05:00
Sabe Jones 2080c3f7b8 fix(content): restore Fungi Potions and Snail Armor 2024-06-21 11:56:01 -05:00
Sabe Jones 6f65c72921 5.25.5 2024-06-21 10:14:57 -05:00
Sabe Jones 22bbdd6a28 Merge branch 'develop' into release 2024-06-21 10:14:54 -05:00
Phillip Thelen 1a3d6f6520 fix mage gear being shown as twohanded (#15254) 2024-06-21 10:14:25 -05:00
Sabe Jones aa1e78ac94 5.25.4 2024-06-21 09:59:08 -05:00
Sabe Jones 858caa4582 fix(cShop): price display for animal pieces 2024-06-21 09:58:33 -05:00
Sabe Jones a7e1091f3f 5.25.3 2024-06-21 09:13:32 -05:00
Sabe Jones 78fca804b7 fix(content): repair Rose Gold Potion and Beach Umbrella 2024-06-21 09:12:18 -05:00
Sabe Jones a919ef99fe 5.25.2 2024-06-21 07:26:01 -05:00
Sabe Jones de9ca06607 fix(gear): fill special data for seasonal body accessories 2024-06-21 07:25:49 -05:00
Sabe Jones b5c5990e56 5.25.1 2024-06-21 07:22:23 -05:00
Sabe Jones 756af8aafb fix(gear): fill special data for seasonal eyewear 2024-06-21 07:21:55 -05:00
Sabe Jones 54f84af274 5.25.0 2024-06-21 07:00:58 -05:00
Sabe Jones c151b6e1bc chore(subproj): update habitica-images 2024-06-21 07:00:55 -05:00
Sabe Jones effd729222 Merge branch 'schedule-rc' into develop 2024-06-21 06:52:46 -05:00
Weblate b7cdbc5c94 Translated using Weblate (Indonesian)
Currently translated at 95.7% (223 of 233 strings)

Translated using Weblate (Indonesian)

Currently translated at 98.3% (179 of 182 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (378 of 378 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Indonesian)

Currently translated at 76.0% (197 of 259 strings)

Translated using Weblate (German)

Currently translated at 91.8% (2850 of 3103 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Indonesian)

Currently translated at 74.5% (193 of 259 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (German)

Currently translated at 91.7% (2848 of 3103 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (3080 of 3103 strings)

Translated using Weblate (German)

Currently translated at 91.6% (2844 of 3103 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.8% (3037 of 3103 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.3% (862 of 868 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.2% (164 of 167 strings)

Translated using Weblate (German)

Currently translated at 91.5% (2840 of 3103 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (German)

Currently translated at 91.4% (2838 of 3103 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (378 of 378 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Japanese)

Currently translated at 98.8% (3067 of 3103 strings)

Translated using Weblate (German)

Currently translated at 91.3% (2836 of 3103 strings)

Translated using Weblate (Croatian)

Currently translated at 77.8% (102 of 131 strings)

Translated using Weblate (Korean)

Currently translated at 96.8% (183 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 98.4% (186 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 53.2% (124 of 233 strings)

Translated using Weblate (Korean)

Currently translated at 81.9% (77 of 94 strings)

Translated using Weblate (Croatian)

Currently translated at 67.0% (63 of 94 strings)

Translated using Weblate (Korean)

Currently translated at 51.6% (47 of 91 strings)

Translated using Weblate (Croatian)

Currently translated at 40.6% (37 of 91 strings)

Translated using Weblate (Croatian)

Currently translated at 52.1% (135 of 259 strings)

Translated using Weblate (Slovak)

Currently translated at 51.0% (119 of 233 strings)

Translated using Weblate (Danish)

Currently translated at 63.0% (147 of 233 strings)

Translated using Weblate (Bulgarian)

Currently translated at 68.6% (160 of 233 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Turkish)

Currently translated at 80.9% (106 of 131 strings)

Translated using Weblate (Serbian)

Currently translated at 75.5% (99 of 131 strings)

Translated using Weblate (Slovak)

Currently translated at 75.5% (99 of 131 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Polish)

Currently translated at 90.8% (388 of 427 strings)

Translated using Weblate (Bulgarian)

Currently translated at 55.0% (1709 of 3103 strings)

Translated using Weblate (Spanish)

Currently translated at 84.5% (159 of 188 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Swedish)

Currently translated at 40.6% (37 of 91 strings)

Translated using Weblate (Serbian)

Currently translated at 39.5% (36 of 91 strings)

Translated using Weblate (Slovak)

Currently translated at 39.5% (36 of 91 strings)

Translated using Weblate (Romanian)

Currently translated at 40.6% (37 of 91 strings)

Translated using Weblate (Portuguese)

Currently translated at 52.7% (48 of 91 strings)

Translated using Weblate (Italian)

Currently translated at 49.4% (45 of 91 strings)

Translated using Weblate (Hebrew)

Currently translated at 38.4% (35 of 91 strings)

Translated using Weblate (Czech)

Currently translated at 40.6% (37 of 91 strings)

Translated using Weblate (Bulgarian)

Currently translated at 39.5% (36 of 91 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Serbian)

Currently translated at 86.3% (95 of 110 strings)

Translated using Weblate (Italian)

Currently translated at 92.7% (805 of 868 strings)

Translated using Weblate (Swedish)

Currently translated at 53.6% (139 of 259 strings)

Translated using Weblate (Serbian)

Currently translated at 52.8% (137 of 259 strings)

Translated using Weblate (Slovak)

Currently translated at 51.7% (134 of 259 strings)

Translated using Weblate (Hebrew)

Currently translated at 49.4% (128 of 259 strings)

Co-authored-by: Alberto jor avondet <albertoavondet@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Dragan SIRET <orion.siret0.2@gmail.com>
Co-authored-by: Filip Betko <filipbetko@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: liimee <git.taaa@fedora.email>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/it/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/character/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/bg/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/he/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/it/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/death/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es/
Translate-URL: https://translate.habitica.com/projects/habitica/front/id/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/bg/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/he/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/he/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/id/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/bg/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/da/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/id/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/sk/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Loginincentives
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Quests
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2024-06-21 12:33:19 +02:00
Phillip Thelen 28193f86fb Fix serving memoized content 2024-06-21 11:12:18 +02:00
Phillip Thelen 877fe48225 correctly memoize conent api 2024-06-20 12:23:24 +02:00
Phillip Thelen e0f6f79c5b don’t build multiple times on heroku 2024-06-20 10:11:27 +02:00
Phillip Thelen f62254d68e fix client command 2024-06-19 19:40:20 +02:00
Phillip Thelen d054e6fc16 correct build call 2024-06-19 19:36:57 +02:00
Phillip Thelen 7231f699c1 try setting up with heroku buildpack 2024-06-19 19:32:32 +02:00
Phillip Thelen 1dae0793fd call gulp build:prod 2024-06-19 18:50:32 +02:00
Phillip Thelen f18fbe86b6 build client 2024-06-19 18:40:53 +02:00
Phillip Thelen 61a61724ca testing 2024-06-19 18:33:18 +02:00
Phillip Thelen 93cf30eb18 integration fix 2024-06-19 18:20:43 +02:00
Phillip Thelen 379f41ff04 integration fix 2024-06-19 18:20:25 +02:00
Phillip Thelen cff08adcd0 specify dev docker file 2024-06-19 18:13:20 +02:00
Phillip Thelen 71936c1f0a initialize stub 2024-06-19 18:10:16 +02:00
Phillip Thelen 4da2ed4a1f initialize stub 2024-06-19 18:10:07 +02:00
Phillip Thelen 11c5b26c59 test heroku file 2024-06-19 18:08:45 +02:00
Phillip Thelen 19c79ce510 fix buy test 2024-06-19 18:04:01 +02:00
Phillip Thelen 0ba14c18b1 fix stub reference 2024-06-19 18:03:36 +02:00
Phillip Thelen ac85bb2e2d fix stub reference 2024-06-19 18:03:15 +02:00
Phillip Thelen 74dfb2710f test fixes 2024-06-19 18:02:28 +02:00
Phillip Thelen d2a0ab684a test fixes 2024-06-19 18:01:27 +02:00
Phillip Thelen 12d38fa813 fix content tests 2024-06-19 17:45:01 +02:00
Phillip Thelen 8dbd3c3db1 fix canOwn test 2024-06-19 17:37:04 +02:00
Phillip Thelen 74b3b348ff fix buy test 2024-06-19 17:32:31 +02:00
Phillip Thelen 3386d61fde fix debug tests 2024-06-19 17:30:52 +02:00
Phillip Thelen db41e00990 fix debug tests 2024-06-19 17:30:37 +02:00
Phillip Thelen 5d5275ce70 lint fixes 2024-06-19 17:30:29 +02:00
Phillip Thelen e39c63700e remove duplicate keys 2024-06-19 17:30:16 +02:00
Phillip Thelen 550ac2db9d fix tests 2024-06-19 17:14:32 +02:00
Phillip Thelen 19da14531c add chameleon to featured quests 2024-06-19 17:05:25 +02:00
Phillip Thelen 254dd80f24 fix import 2024-06-19 17:05:14 +02:00
Phillip Thelen 0bc3f16b4b add new content to new release file 2024-06-19 16:33:22 +02:00
Phillip Thelen 5184973bd5 fix release date tests 2024-06-19 16:33:11 +02:00
Phillip Thelen b6accca5ca fix armoire tests 2024-06-19 16:33:06 +02:00
Phillip Thelen fec68e6211 fix tests 2024-06-19 16:02:03 +02:00
Phillip Thelen fc63c906dd Improve test coverage 2024-06-19 15:25:45 +02:00
Phillip Thelen 3333f8f0f5 allow hatching potions to have a release date 2024-06-19 15:25:45 +02:00
Phillip Thelen 89a3ac3dde allow eggs to have a release date
# Conflicts:
#	test/content/armoire.test.js
2024-06-19 15:24:21 +02:00
CuriousMagpie 16551ec83f Merge branch '2024-07-content-prebuild' into subs-private 2024-06-18 15:03:12 -04:00
CuriousMagpie 2645bf6023 update habitica images 2024-06-18 15:02:47 -04:00
CuriousMagpie f5f4974a73 update habitica-images 2024-06-18 14:58:13 -04:00
CuriousMagpie 162e337d14 Merge branch '2024-07-content-prebuild' into subs-private 2024-06-18 13:46:03 -04:00
CuriousMagpie 21a7d36b7b update sprites 2024-06-18 13:45:09 -04:00
CuriousMagpie f2506c3231 updated sprites css 2024-06-18 13:24:21 -04:00
CuriousMagpie d47641e25a typo fix 2024-06-18 12:46:59 -04:00
Sabe Jones 983e01cb3f chore(sprites): update css 2024-06-17 15:02:23 -05:00
Sabe Jones a55ede9175 fix(migration): couple of vet pet issues 2024-06-17 14:45:57 -05:00
CuriousMagpie fb8479ad1e finish July prebuild 2024-06-17 13:44:36 -04:00
Sabe Jones 28491cb01d fix(content): Umbrella is PER not STR 2024-06-17 10:47:22 -05:00
Sabe Jones b49dddeb47 fix(shops): post merge cleanup 2024-06-14 18:12:22 -05:00
Sabe Jones 31e501f65a Merge branch 'release' into schedule-rc 2024-06-14 10:18:16 -05:00
CuriousMagpie 3810cf3ef3 add chameleon quest 2024-06-14 10:42:47 -04:00
Sabe Jones 050c227e6f 5.25.2 2024-06-14 08:54:38 -05:00
Weblate ddb8725052 Merge branch 'origin/develop' into Weblate. 2024-06-14 15:53:32 +02:00
Weblate e11b9ebe26 Translated using Weblate (Japanese)
Currently translated at 98.8% (3066 of 3103 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (German)

Currently translated at 91.3% (2834 of 3103 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3103 of 3103 strings)

Translated using Weblate (Japanese)

Currently translated at 97.2% (3018 of 3103 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (233 of 233 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Japanese)

Currently translated at 98.0% (851 of 868 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Korean)

Currently translated at 71.8% (624 of 868 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (German)

Currently translated at 91.2% (2832 of 3103 strings)

Translated using Weblate (Italian)

Currently translated at 4.2% (8 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Danish)

Currently translated at 87.2% (96 of 110 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (French)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Japanese)

Currently translated at 97.2% (3018 of 3103 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (233 of 233 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (763 of 773 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (763 of 773 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Russian)

Currently translated at 98.2% (853 of 868 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Korean)

Currently translated at 91.4% (128 of 140 strings)

Translated using Weblate (Korean)

Currently translated at 79.8% (617 of 773 strings)

Translated using Weblate (Korean)

Currently translated at 75.0% (6 of 8 strings)

Translated using Weblate (Korean)

Currently translated at 70.7% (302 of 427 strings)

Translated using Weblate (Korean)

Currently translated at 51.6% (47 of 91 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3103 of 3103 strings)

Co-authored-by: Dragan SIRET <orion.siret0.2@gmail.com>
Co-authored-by: Filip Betko <filipbetko@gmail.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: razil <boss.razmarin@gmail.com>
Co-authored-by: taesub park <dungfly75@gmail.com>
Co-authored-by: Юрий Артамонов <zilberstein2211@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/da/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/character/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/death/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/death/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/it/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ko/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Inventory
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2024-06-14 15:53:21 +02:00
Phillip Thelen 4da53f83c9 Add option to log every request start and end to loggly (#15243) 2024-06-14 08:51:10 -05:00
CuriousMagpie d05da3722c add June background notes 2024-06-13 17:12:43 -04:00
CuriousMagpie b8a3440ef2 fix mystery item and background description 2024-06-13 16:40:04 -04:00
CuriousMagpie 44d63032d8 add subscriber gear, enchanted armoire, and background 2024-06-13 15:38:23 -04:00
CuriousMagpie 9d7da91ec6 add sprites 2024-06-13 14:44:59 -04:00
Phillip Thelen 39add61618 Revert "lint"
This reverts commit 544d67e7e5.
2024-06-11 19:10:50 +02:00
Sabe Jones 1c1543f012 fix(layout): tighten up various margins 2024-06-11 12:04:21 -05:00
Phillip Thelen 10a27354bb REVERT ._. 2024-06-11 19:00:34 +02:00
Phillip Thelen a00f199d18 Revert "Revert "allow hatching potions to have a release date""
This reverts commit 485584c144.

# Conflicts:
#	website/common/script/content/hatching-potions.js
2024-06-11 18:54:39 +02:00
Phillip Thelen 6c5bff7843 put armoire test back 2024-06-11 18:20:48 +02:00
Phillip Thelen 388c3d38ed Revert "Improve test coverage"
This reverts commit d4ba96796c.
2024-06-11 18:19:29 +02:00
Phillip Thelen 485584c144 Revert "allow hatching potions to have a release date"
This reverts commit ebdac0b388.
2024-06-11 18:19:21 +02:00
Phillip Thelen b83f62bd82 Revert "allow eggs to have a release date"
This reverts commit 6d13a257dd.
2024-06-11 18:19:11 +02:00
Phillip Thelen 544d67e7e5 lint 2024-06-10 14:53:23 +02:00
Phillip Thelen 1f0a4dad23 fix content caching 2024-06-10 14:46:39 +02:00
Phillip Thelen eb3220c96b fix lint 2024-06-10 14:46:32 +02:00
Phillip Thelen d4ba96796c Improve test coverage 2024-06-10 14:44:21 +02:00
Phillip Thelen ebdac0b388 allow hatching potions to have a release date 2024-06-10 14:24:59 +02:00
Phillip Thelen 6d13a257dd allow eggs to have a release date 2024-06-10 14:11:38 +02:00
Phillip Thelen c2b370f4d3 handle error from invalid input 2024-06-10 11:23:46 +02:00
Phillip Thelen 3313584d60 actually clear out seeking field on user. Even when creating a party 2024-06-07 08:36:08 -05:00
Phillip Thelen b76585cce3 add additional way to get content switchover time for client 2024-06-07 11:10:48 +02:00
Sabe Jones 8d9af82521 fix(import): add missing nconf 2024-06-06 14:55:37 -05:00
Phillip Thelen dcf25c0b4a fix tests 2024-06-06 21:51:07 +02:00
Phillip Thelen 31036ad9e4 return actually correct quests 2024-06-06 21:51:07 +02:00
Phillip Thelen 1ba85b403f clear content api cache each day 2024-06-06 21:51:07 +02:00
Phillip Thelen c26f410cc3 add fallback for seasonal gear 2024-06-06 12:21:34 +02:00
Phillip Thelen 242e64cedc add content time offset to client 2024-06-06 10:56:56 +02:00
Sabe Jones a44418c4fa fix(backgrounds): correct event and tt loading 2024-06-05 08:14:13 -05:00
Phillip Thelen ea66e4e4a1 Add end dates to categories in customizationshop 2024-06-05 10:34:17 +02:00
Phillip Thelen 6889b65123 fix questshop featured items 2024-06-05 10:12:47 +02:00
Sabe Jones fd5bc8f0b9 fix(shops): various cleanup 2024-06-04 11:50:26 -05:00
Sabe Jones f33aff577c fix(build): skip some polyfills 2024-05-31 15:54:06 -05:00
Sabe Jones 9ba986f5e5 fix(layout): Justin positioning and shopdown 2024-05-31 15:37:00 -05:00
Sabe Jones bf46c798a6 fix(migration): log transaction for awarded Gems 2024-05-31 11:54:27 -05:00
Phillip Thelen 54b1afd5b4 make content releases use a given offset to the time. 2024-05-29 19:05:17 +02:00
Sabe Jones b501e06f27 fix(pets): finish Veteran Cactus 2024-05-29 09:03:36 -05:00
Sabe Jones 2062c68877 WIP(shops): better Fennec and bg loading, add migration 2024-05-28 17:57:13 -05:00
Sabe Jones 19521b1894 fix(avatar): render first bg addition and Justin 2024-05-24 17:19:49 -05:00
Sabe Jones 688e7181f0 fix(customizations): responsive updates 2024-05-23 17:50:16 -05:00
Sabe Jones a53a9be4b7 fix(css): flex gap is a thing 2024-05-22 15:53:24 -05:00
Sabe Jones 66c56225a4 fix(shops): border radius and typo 2024-05-22 13:41:49 -05:00
Sabe Jones cabc08c04b fix(lint): remove console log 2024-05-22 09:27:35 -05:00
Sabe Jones 9ae6063f78 fix(npm): move moment-locales to main deps 2024-05-22 09:23:30 -05:00
Sabe Jones 7936677fd8 WIP(shops): empty states and deselect UX 2024-05-22 09:16:13 -05:00
Phillip Thelen 88a0b57335 add end date to seasonal gear in market 2024-05-22 15:47:02 +02:00
Phillip Thelen 2303d5de32 Fix quest premium hatching potions in market 2024-05-22 12:21:51 +02:00
Phillip Thelen 1f5de1ab42 remove sinon and moment locales to shrink client bundle 2024-05-22 11:24:31 +02:00
Phillip Thelen 435047cace strip sinon, nise and bootstrap-vue icons 2024-05-21 20:18:57 +02:00
Sabe Jones c0d6338eba WIP(shops): safer debug mode 2024-05-21 09:11:18 -05:00
Phillip Thelen 36b589e92d make it easier to unequip backgrounds 2024-05-21 15:53:14 +02:00
Phillip Thelen 42cafbeaab make market return unlocked quest potions 2024-05-21 15:39:59 +02:00
Phillip Thelen 5b5d5a39a4 remove date 2024-05-21 14:52:01 +02:00
Sabe Jones 8d479e358d WIP(shops): more fixes 2024-05-17 20:29:14 -05:00
Sabe Jones 3bb1cceed1 chore(testing): DO NOT MERGE THIS 2024-05-17 14:14:04 -05:00
Sabe Jones 0756d36fb3 fix(admin): let panel break item 2024-05-17 14:13:48 -05:00
Phillip Thelen 57deadaa5c Fix purchasing some hatching potions 2024-05-17 15:44:08 +02:00
Phillip Thelen becdf640b5 fix mystery gear 2024-05-17 12:54:21 +02:00
Phillip Thelen 756e99c089 add more purchase related tests 2024-05-17 11:35:22 +02:00
Phillip Thelen 709a14fd51 add some spell related tests 2024-05-17 11:35:14 +02:00
Phillip Thelen 33181c0ac4 Add purchase related test cases 2024-05-17 10:57:33 +02:00
Phillip Thelen ee974dfa19 fix seasonal quest purchasing 2024-05-17 10:30:15 +02:00
Phillip Thelen b697598d75 fix showing seasonal gear in market 2024-05-17 10:03:59 +02:00
Sabe Jones 6e5b13668a WIP(shops): fixes and Hall of Heroes update 2024-05-16 18:49:56 -05:00
Phillip Thelen 4c13f3193e fix backgrounds not showing on time traveler shop 2024-05-16 20:38:58 +02:00
Phillip Thelen 15cea33c4b fix import 2024-05-16 20:29:03 +02:00
Phillip Thelen cac0a84763 Add some tests for time travelers content 2024-05-16 20:19:26 +02:00
Phillip Thelen c56c07a0f8 fix buying transformation items 2024-05-16 20:19:26 +02:00
Phillip Thelen 929778bdad fix code coverage 2024-05-16 20:19:26 +02:00
Phillip Thelen d6dba9767d fix set name 2024-05-16 20:19:26 +02:00
Sabe Jones 7e7ce44c77 fix(shops): lots of layout corrections 2024-05-15 17:28:39 -05:00
Phillip Thelen 4d38880249 Refactor armoire content to be cached by day 2024-05-15 16:51:09 +02:00
Phillip Thelen 46d164ddd1 Fix seasonal npc sprite 2024-05-15 13:18:32 +02:00
Phillip Thelen 31e4c51c3f Add June Subscriber Gear 2024-05-15 12:54:53 +02:00
Phillip Thelen eebfb81bd2 add armoire items for june 2024-05-15 12:51:01 +02:00
Phillip Thelen 06623991b3 lint 2024-05-15 12:17:18 +02:00
Phillip Thelen fe697898ee Fix featured items 2024-05-15 12:05:30 +02:00
Phillip Thelen f3fc14bd53 fix seasonal gear 2024-05-15 11:56:40 +02:00
Phillip Thelen 7f0b0a3909 fix seasonal gold price 2024-05-15 11:35:27 +02:00
Phillip Thelen 0f395bcc3e Fix some sets not showing 2024-05-15 11:35:19 +02:00
Phillip Thelen d366b2cde1 remove old date strings 2024-05-15 11:25:13 +02:00
Phillip Thelen 7d8611bae2 remove availability data from strings 2024-05-15 11:13:23 +02:00
Sabe Jones 17964c0ab7 fix(build): add assert, le sigh 2024-05-14 14:34:53 -05:00
Phillip Thelen 5b6cc23fb7 event code cleanup 2024-05-14 12:41:49 +02:00
Phillip Thelen efe8cff1ad fix seasonal gear filtering 2024-05-14 11:56:04 +02:00
Phillip Thelen 6591f6780c test fixes 2024-05-14 11:07:15 +02:00
Phillip Thelen 592c320d1d remove only 2024-05-14 11:07:05 +02:00
Phillip Thelen df641d0866 remove express.js 2024-05-14 11:06:27 +02:00
Phillip Thelen da4606df5e fix seasonal gear 2024-05-14 11:06:17 +02:00
Phillip Thelen ceb6b93dc1 Add background 2024-05-13 15:24:04 +02:00
Phillip Thelen e006f3f7de Add cactus veteran pet 2024-05-13 14:56:04 +02:00
Phillip Thelen 246cc25b6d add seasonal sets to schedule 2024-05-13 14:51:35 +02:00
Phillip Thelen a1bb61793b add sprite definitions 2024-05-13 14:51:29 +02:00
Phillip Thelen 6523ed08cd Add Koi Hatchingpotion 2024-05-13 14:49:39 +02:00
Phillip Thelen ab34257c03 Add Summer Splash Gear 2024-05-13 14:37:33 +02:00
Phillip Thelen 6afdffae92 fixes 2024-05-13 14:00:44 +02:00
Phillip Thelen c3b17e3db0 Squashed commit of the following:
commit 934b85d716
Author: Sabe Jones <sabe@habitica.com>
Date:   Thu May 9 09:27:28 2024 -0500

    5.24.2

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

    chore(subproj): update habitica-images

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

    Squashed commit of the following:

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

        fix(faq): copy updates

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

        fix(faq): cleaner layout

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

        feat(faq): Content Schedule notes

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

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3089 of 3089 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (15 of 15 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.5% (762 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (378 of 378 strings)

    Translated using Weblate (German)

    Currently translated at 82.6% (214 of 259 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.5% (762 of 773 strings)

    Translated using Weblate (German)

    Currently translated at 97.4% (753 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.5% (762 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (131 of 131 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (2 of 2 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3089 of 3089 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (868 of 868 strings)

    Translated using Weblate (German)

    Currently translated at 75.2% (195 of 259 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (113 of 113 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (239 of 239 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.5% (762 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (189 of 189 strings)

    Translated using Weblate (German)

    Currently translated at 97.1% (751 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (140 of 140 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (113 of 113 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (427 of 427 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (239 of 239 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.5% (762 of 773 strings)

    Translated using Weblate (German)

    Currently translated at 96.2% (744 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (110 of 110 strings)

    Translated using Weblate (German)

    Currently translated at 99.8% (867 of 868 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (German)

    Currently translated at 98.7% (229 of 232 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (378 of 378 strings)

    Translated using Weblate (German)

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (140 of 140 strings)

    Translated using Weblate (German)

    Currently translated at 89.5% (2766 of 3089 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.5% (762 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (868 of 868 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (German)

    Currently translated at 89.3% (2760 of 3089 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (3089 of 3089 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (232 of 232 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (286 of 286 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3089 of 3089 strings)

    Translated using Weblate (French)

    Currently translated at 99.9% (3088 of 3089 strings)

    Translated using Weblate (German)

    Currently translated at 89.1% (2754 of 3089 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (182 of 182 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.4% (761 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (868 of 868 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (868 of 868 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (232 of 232 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3089 of 3089 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.4% (761 of 773 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (378 of 378 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.3% (862 of 868 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (427 of 427 strings)

    Translated using Weblate (German)

    Currently translated at 89.0% (2752 of 3089 strings)

    Translated using Weblate (German)

    Currently translated at 89.0% (2750 of 3089 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (232 of 232 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (3089 of 3089 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (773 of 773 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (773 of 773 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 99.7% (377 of 378 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (378 of 378 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (378 of 378 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (868 of 868 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (167 of 167 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (German)

    Currently translated at 88.9% (2747 of 3089 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (259 of 259 strings)

    Translated using Weblate (German)

    Currently translated at 88.7% (2740 of 3089 strings)

    Translated using Weblate (German)

    Currently translated at 88.8% (2734 of 3077 strings)

    Translated using Weblate (Korean)

    Currently translated at 79.8% (131 of 164 strings)

    Translated using Weblate (Korean)

    Currently translated at 79.8% (131 of 164 strings)

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

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

    remove dempendabot.yml (#15193)

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

    5.24.1

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

    Merge branch 'develop' into release

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

    Merge branch 'origin/develop' into Weblate.

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

    5.24.0

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

    Fix issue with gift sub processing (#15184)

    * Fix issue with gift sub processing

    * Update cron.js

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

    May 2024 Content Prebuild (#15185)

    * 2024-05 css update

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

    * typo correction

    * add May achievement

    * content fixes after local testing

    * canonical date fix

    * fix potion descriptions, add periods to background descriptions

    * fix canonical date

    * updated armoire items

    * fix stat display on item

    * Fixing merge conflicts

    * resolve merge conflicts

    * add leading zero to mp drain for mushroom quest

    * fix timezones

    * proofreading pass

    * fix linting errors

    * date fixes & linter fixes

    * correct armoire expression at end of file

    * fix(autolint): roll back Prettier change

    ---------

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

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

    Translated using Weblate (German)

    Currently translated at 88.7% (2731 of 3077 strings)

    Translated using Weblate (German)

    Currently translated at 88.6% (2729 of 3077 strings)

    Translated using Weblate (German)

    Currently translated at 88.6% (2727 of 3077 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (286 of 286 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (110 of 110 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 99.8% (860 of 861 strings)

    Translated using Weblate (German)

    Currently translated at 88.5% (2726 of 3077 strings)

    Translated using Weblate (Portuguese (Brazil))

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Portuguese (Brazil))

    Currently translated at 100.0% (861 of 861 strings)

    Translated using Weblate (German)

    Currently translated at 88.5% (2724 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (861 of 861 strings)

    Translated using Weblate (German)

    Currently translated at 98.9% (283 of 286 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 97.5% (160 of 164 strings)

    Translated using Weblate (German)

    Currently translated at 89.8% (257 of 286 strings)

    Translated using Weblate (German)

    Currently translated at 87.7% (251 of 286 strings)

    Translated using Weblate (German)

    Currently translated at 99.8% (860 of 861 strings)

    Translated using Weblate (German)

    Currently translated at 97.6% (841 of 861 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 98.8% (256 of 259 strings)

    Translated using Weblate (German)

    Currently translated at 91.8% (392 of 427 strings)

    Translated using Weblate (German)

    Currently translated at 91.1% (389 of 427 strings)

    Translated using Weblate (German)

    Currently translated at 88.7% (379 of 427 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (3077 of 3077 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (231 of 231 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (286 of 286 strings)

    Translated using Weblate (French)

    Currently translated at 99.1% (3050 of 3077 strings)

    Translated using Weblate (German)

    Currently translated at 88.0% (376 of 427 strings)

    Translated using Weblate (German)

    Currently translated at 98.7% (228 of 231 strings)

    Translated using Weblate (Dutch)

    Currently translated at 100.0% (113 of 113 strings)

    Translated using Weblate (Dutch)

    Currently translated at 84.5% (2602 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (164 of 164 strings)

    Translated using Weblate (Romanian)

    Currently translated at 92.8% (130 of 140 strings)

    Translated using Weblate (German)

    Currently translated at 97.8% (226 of 231 strings)

    Translated using Weblate (Romanian)

    Currently translated at 75.0% (6 of 8 strings)

    Translated using Weblate (Romanian)

    Currently translated at 96.6% (58 of 60 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (861 of 861 strings)

    Translated using Weblate (German)

    Currently translated at 95.2% (220 of 231 strings)

    Translated using Weblate (French)

    Currently translated at 98.7% (3040 of 3077 strings)

    Translated using Weblate (French)

    Currently translated at 100.0% (861 of 861 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (94 of 94 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (13 of 13 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (286 of 286 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.6% (761 of 764 strings)

    Translated using Weblate (German)

    Currently translated at 54.9% (50 of 91 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (286 of 286 strings)

    Translated using Weblate (Portuguese (Brazil))

    Currently translated at 98.7% (3037 of 3077 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (3077 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (239 of 239 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.6% (761 of 764 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (189 of 189 strings)

    Translated using Weblate (Portuguese (Brazil))

    Currently translated at 100.0% (861 of 861 strings)

    Translated using Weblate (Portuguese)

    Currently translated at 99.3% (163 of 164 strings)

    Translated using Weblate (German)

    Currently translated at 94.8% (219 of 231 strings)

    Translated using Weblate (German)

    Currently translated at 84.2% (241 of 286 strings)

    Translated using Weblate (German)

    Currently translated at 51.6% (47 of 91 strings)

    Translated using Weblate (Portuguese)

    Currently translated at 98.1% (161 of 164 strings)

    Translated using Weblate (Spanish)

    Currently translated at 99.8% (3072 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (94 of 94 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (286 of 286 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (239 of 239 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.6% (761 of 764 strings)

    Translated using Weblate (German)

    Currently translated at 49.4% (45 of 91 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Spanish (Latin America))

    Currently translated at 89.7% (253 of 282 strings)

    Translated using Weblate (Spanish (Latin America))

    Currently translated at 2.1% (3 of 137 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3077 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (239 of 239 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.6% (761 of 764 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (427 of 427 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.6% (761 of 764 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (231 of 231 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (56 of 56 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3077 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.9% (756 of 764 strings)

    Translated using Weblate (German)

    Currently translated at 48.3% (44 of 91 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (861 of 861 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.0% (749 of 764 strings)

    Translated using Weblate (German)

    Currently translated at 97.3% (744 of 764 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (164 of 164 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (231 of 231 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (113 of 113 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (131 of 131 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (22 of 22 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (8 of 8 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3077 of 3077 strings)

    Translated using Weblate (Spanish)

    Currently translated at 99.8% (3071 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 99.1% (3051 of 3077 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (861 of 861 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (140 of 140 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (230 of 230 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (94 of 94 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (131 of 131 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (427 of 427 strings)

    Translated using Weblate (German)

    Currently translated at 86.6% (370 of 427 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (3035 of 3035 strings)

    Translated using Weblate (Spanish)

    Currently translated at 100.0% (3035 of 3035 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (239 of 239 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (182 of 182 strings)

    Translated using Weblate (Russian)

    Currently translated at 29.9% (41 of 137 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 98.0% (749 of 764 strings)

    Translated using Weblate (Ukrainian)

    Currently translated at 100.0% (764 of 764 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (91 of 91 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (377 of 377 strings)

    Translated using Weblate (Chinese (Simplified))

    Currently translated at 100.0% (110 of 110 strings)

    Translated using Weblate (German)

    Currently translated at 97.8% (836 of 854 strings)

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

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

    Update README.md for better grammar (#15103)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    5.23.0

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

    chore(git): update subproject

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

    Squashed commit of the following:

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

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

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

        fix(quest): revise rage text

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

        fix(quest): correct rage and "guscompletion"

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

        fix(events): correct dates and times

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

        fix(content): a few more unruly Fungus

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

        feat(content): wacky potions 2024 by @CuriousMagpie

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

        feat(event): AF by @CuriousMagpie
2024-05-13 12:29:40 +02:00
Sabe Jones b9e128b387 feat(customizations): more icons 2024-05-10 15:37:07 -05:00
Sabe Jones cac14ab2cc Merge branch 'sabrecat/subs-private' into subs-private 2024-05-10 08:55:16 -05:00
Phillip Thelen c64a6eb66e remove log 2024-05-10 13:11:05 +02:00
Phillip Thelen cd33a539cf fix error in time travelers shop 2024-05-10 13:09:30 +02:00
Phillip Thelen ec76757f93 remove log 2024-05-09 14:38:16 +02:00
Phillip Thelen 03adff80f7 fix seasonal gear not showing as available 2024-05-09 13:55:39 +02:00
Phillip Thelen a73abcca74 don’t return event data anymore for items 2024-05-09 13:55:24 +02:00
Phillip Thelen a8c8fffa7c make sure schedule end dates are set as utc 2024-05-09 13:54:59 +02:00
Phillip Thelen f835cf2761 fix world state 2024-05-09 12:02:23 +02:00
Phillip Thelen 96fed21fbd add gear field to gala events 2024-05-09 10:45:37 +02:00
Phillip Thelen 7d62c87de3 optimize world state api call 2024-05-09 10:45:37 +02:00
Sabe Jones b28251dc9e WIP(shops): more layouts and fixes 2024-05-08 20:27:27 -05:00
Phillip Thelen b713e10c14 lint fixes 2024-05-08 17:48:34 +02:00
Phillip Thelen fce5371fce fix typos 2024-05-08 17:46:40 +02:00
Phillip Thelen a9cefd284a support april fools resale in schedule 2024-05-08 17:42:40 +02:00
Phillip Thelen 6ed422cd28 Improve repeating events handling 2024-05-08 17:42:24 +02:00
Phillip Thelen 02914685dc improve armoire release test cases 2024-05-08 17:05:09 +02:00
Phillip Thelen 856ed24dcb Add tests to check for correct food content populating 2024-05-08 16:46:42 +02:00
Phillip Thelen 4a9ec734c1 validate that schedule keys refer to existing content 2024-05-08 16:06:51 +02:00
Sabe Jones 61d151d2bb chore(testing): enable non prod analytics 2024-05-07 20:15:16 -05:00
Sabe Jones 32cb201b81 WIP(schedule): add June 2024 content 2024-05-07 19:36:39 -05:00
Sabe Jones 44a7006295 fix(backgrounds): correct wrapping 2024-05-07 17:19:14 -05:00
Sabe Jones 21a0bf7d65 WIP(shops): update avatar modal style 2024-05-07 09:06:23 -05:00
Phillip Thelen 0089506165 Fix setting end date 2024-05-07 15:17:22 +02:00
Phillip Thelen fbce5aae32 fix buying animal gear 2024-05-07 14:50:12 +02:00
Sabe Jones a8726eee0b fix(faq): copy updates 2024-05-06 16:23:33 -05:00
Sabe Jones 9efe370d33 fix(faq): cleaner layout 2024-05-03 16:07:35 -05:00
Phillip Thelen 47df62e716 remove logs 2024-05-03 11:14:49 +02:00
Phillip Thelen dac792dd27 add end date to more items/categories 2024-05-03 11:14:23 +02:00
Phillip Thelen eacf6de19a fix warning 2024-05-03 11:13:52 +02:00
Sabe Jones c1ca4e84b8 fix(layouts): May 2 updates 2024-05-03 00:14:33 -05:00
Phillip Thelen 87fc01cb81 improve armoire release process 2024-05-02 14:37:14 +02:00
Phillip Thelen 71f21c643c remove only 2024-05-02 14:37:14 +02:00
Phillip Thelen 1ced4a18d6 add handling and tests for new timetravelers schedule 2024-05-02 14:37:14 +02:00
Phillip Thelen 9dabe79d5e Add handling and tests for new background schedule 2024-05-02 14:37:14 +02:00
Phillip Thelen 4c2cdfe5b8 fix customization shop display on mobile 2024-05-02 14:37:14 +02:00
Sabe Jones f05888b116 fix(time-travelers): add countdowns 2024-04-30 17:26:22 -05:00
Sabe Jones ae8607c0c3 feat(faq): Content Schedule notes 2024-04-30 17:22:59 -05:00
Phillip Thelen d6cabeedb4 fix tests 2024-04-26 13:44:51 +02:00
Phillip Thelen 99a7b90247 Fix linting issues 2024-04-26 13:15:30 +02:00
Phillip Thelen fbdaa50fcf handle schedule end date for galas 2024-04-26 13:03:34 +02:00
Phillip Thelen 30e81297da Add comments to clarify schedule 2024-04-26 12:18:26 +02:00
Phillip Thelen b373eaff39 Fix merge issue 2024-04-26 12:09:04 +02:00
Phillip Thelen 80a212683d Fix assigning end date to content schedule items 2024-04-26 12:09:04 +02:00
Sabe Jones b0a4ed30d4 WIP(shops): dates and fixes 2024-04-26 12:08:30 +02:00
Phillip Thelen c2cb37ffe6 Cleanup pinned items that are no longer for purchase 2024-04-26 12:08:11 +02:00
Sabe Jones 558894fafd WIP(shops): cShop reconciled to schedule backend 2024-04-26 12:08:11 +02:00
Phillip Thelen 4bbdf27f48 add option to reset TT 2024-04-26 12:05:30 +02:00
Phillip Thelen daa296f2af fix some quests scheduling 2024-04-26 12:05:30 +02:00
Phillip Thelen 4c51212315 Cleanup pinned items that are no longer for purchase 2024-04-26 12:05:27 +02:00
Sabe Jones 372763c57c fix(shops): various 2024-04-24 17:58:20 -05:00
Sabe Jones 3bf323032c WIP(shops): first full test version 2024-04-24 00:02:41 -05:00
Sabe Jones 7a50b2d2ff Merge branch 'sabrecat/cshop' into subs-private 2024-04-23 10:52:05 -05:00
Sabe Jones 8df326bf92 WIP(shop): add timings to categories 2024-04-23 10:50:45 -05:00
Phillip Thelen 2d6555fe0f fix merge 2024-04-23 17:24:17 +02:00
Phillip Thelen b0d6f7722b allow non-admins to observe timetravel 2024-04-23 17:20:21 +02:00
Phillip Thelen aedbe7d333 content schedule tweaks 2024-04-23 17:20:21 +02:00
Phillip Thelen 6079dd4af6 feature items according to content schedule 2024-04-23 17:20:21 +02:00
Phillip Thelen 5c448188cf fix potion scheduling 2024-04-23 17:20:21 +02:00
Phillip Thelen d7dc878b1c Cleanup pinned items that are no longer for purchase 2024-04-23 17:20:20 +02:00
Phillip Thelen 7baec4e48e Fix assigning end date to content schedule items 2024-04-23 14:40:10 +02:00
Sabe Jones 5cd58d4119 WIP(shops): dates and fixes 2024-04-22 17:52:07 -05:00
Sabe Jones 4cdfefd92b Merge branch 'sabrecat/cshop' into subs-private 2024-04-19 20:12:35 -05:00
Sabe Jones 28b936e2d1 WIP(shops): cShop reconciled to schedule backend 2024-04-19 20:11:17 -05:00
Sabe Jones e4ec7e3e1e Merge branch 'gsf' into subs-private 2024-04-19 08:42:16 -05:00
Phillip Thelen 3c7ecef6a8 Update cron.js 2024-04-18 18:17:16 +02:00
Sabe Jones e2a5a1ab39 Merge branch 'sabrecat/cshop' into subs-private 2024-04-17 18:03:11 -05:00
Sabe Jones 5f64b2fb25 WIP(customizations): revised backgrounds modal 2024-04-17 18:02:35 -05:00
Sabe Jones f2a2d4cde5 Merge branch 'gsf' into subs-private 2024-04-17 09:05:36 -05:00
Phillip Thelen edc3c58876 Fix issue with gift sub processing 2024-04-05 13:30:15 +02:00
Sabe Jones 4277c08324 fix(scheduling): lint and code review 2024-04-04 22:42:28 -05:00
Sabe Jones 424f29a82b fix(lint): curly space 2024-04-04 22:02:31 -05:00
Sabe Jones e52c7ff9ce WIP(shop): empty states for categories 2024-04-04 22:02:19 -05:00
Sabe Jones fdc709d1c2 fix(lint): remove console log 2024-04-03 10:33:43 -05:00
Sabe Jones ea750571a0 fix(packages): add timers-browserify 2024-04-03 10:30:20 -05:00
Phillip Thelen f8fbea4654 quest shop fix 2024-04-03 16:06:08 +02:00
Phillip Thelen e0f4a4ecb8 merge in shop schedule loading 2024-04-03 16:06:08 +02:00
Phillip Thelen 5ce12d97be fix errors 2024-04-03 16:06:08 +02:00
Sabe Jones 4fdd064cd6 Merge branch 'sabrecat/customizations' into subs-private 2024-04-03 08:36:25 -05:00
Sabe Jones 37731b236a WIP(shop): add item names and unlocking 2024-04-02 17:58:06 -05:00
Sabe Jones b06c708480 Merge branch 'release' into sabrecat/customizations 2024-04-02 15:39:58 -05:00
Phillip Thelen f8c452ae3f Remove duplicate menu entry 2024-04-02 17:12:38 +02:00
Phillip Thelen 9befbec2b0 Update index.vue 2024-04-02 17:11:18 +02:00
Phillip Thelen 7b46f3bc23 Fix method name 2024-04-02 17:07:28 +02:00
Sabe Jones 64a500987c fix(packages): move sinon to main 2024-04-02 09:32:44 -05:00
Sabe Jones 92eaece5eb fix(packages): add assert 2024-04-01 19:13:29 -05:00
Sabe Jones ee73d5b628 fix(packages): add util 2024-04-01 19:10:59 -05:00
Sabe Jones a7eda1355b fix(packages): add sinon to client 2024-04-01 19:08:07 -05:00
Sabe Jones 8b9b79db8e fix(potions): remove EVENT 2024-04-01 19:04:57 -05:00
Sabe Jones 69c0488335 Merge branch 'sabrecat/customizations' into subs-private 2024-04-01 18:57:36 -05:00
Phillip Thelen c18e06f071 changes 2024-04-01 18:38:04 -05:00
Phillip Thelen a50c0eb1e7 fix test 2024-04-01 18:37:57 -05:00
Phillip Thelen fb56f7df20 Fix various tests 2024-04-01 18:37:46 -05:00
Phillip Thelen 3540a274b3 fix issue with seasonal quest scheduling 2024-04-01 18:36:44 -05:00
Phillip Thelen fe2c02679e Fix buying some items 2024-04-01 18:36:33 -05:00
Phillip Thelen bca3e96e9c Implement food seasons 2024-04-01 18:36:24 -05:00
Phillip Thelen 041edb3042 Preen old scheduling code 2024-04-01 18:13:40 -05:00
Phillip Thelen 6e96085f99 add cards to event content cycle 2024-04-01 18:07:41 -05:00
Phillip Thelen 249394b4ad Implement events throughout the year 2024-04-01 18:07:10 -05:00
Phillip Thelen 2a84561e00 fix some date issues with scheduling 2024-04-01 18:06:55 -05:00
Phillip Thelen 962456204e improve updating UI when time traveling 2024-04-01 18:06:00 -05:00
Phillip Thelen 593524905e allow admins to manipulate time on test servers 2024-04-01 18:05:41 -05:00
Phillip Thelen 1b12e9d8b7 proof of concept for time travel 2024-04-01 18:04:38 -05:00
Phillip Thelen 127f105934 typo 2024-04-01 17:56:24 -05:00
Phillip Thelen 2dfe5585eb fix 2024-04-01 17:56:15 -05:00
Phillip Thelen 93011f182f Fix lint and test 2024-04-01 17:56:06 -05:00
Phillip Thelen d11e95ab26 change mobile filter defaults to a list 2024-04-01 17:55:58 -05:00
Phillip Thelen f99ddbe60f display customizations in new shop 2024-04-01 17:54:07 -05:00
Phillip Thelen 982069df36 remove logs 2024-04-01 17:52:46 -05:00
Phillip Thelen db4bec37e3 Implement new schedule system for seasonal shop 2024-04-01 17:52:40 -05:00
Phillip Thelen 736ef16430 simplify schedule matching usage 2024-04-01 17:52:29 -05:00
Phillip Thelen 129cb7627c Implement new content schedule for magic hatching potions 2024-04-01 17:52:21 -05:00
Phillip Thelen f223b5dd2a Implement schedule for quest bundles 2024-04-01 17:52:12 -05:00
Phillip Thelen b3521be629 Implement new content schedule for potion and pet quests 2024-04-01 17:52:02 -05:00
Phillip Thelen 17db6a1772 Implement new release schedule for backgrounds 2024-04-01 17:51:47 -05:00
Phillip Thelen 278d9b74f9 Implement new scheduling for time travelers shop 2024-04-01 17:51:38 -05:00
Phillip Thelen ce796fa1d9 begin building recurring content scheduling 2024-04-01 17:51:24 -05:00
Phillip Thelen ec0275e6f6 Add tests for content filtering 2024-04-01 17:50:33 -05:00
Phillip Thelen 39252c7828 allow clients to filter content api call 2024-04-01 17:50:03 -05:00
Sabe Jones 75a88ab25a WIP(shops): style fixes 2024-03-29 16:14:14 -05:00
Sabe Jones 70434b17cc WIP(shop): show customs in buy modal 2024-03-14 17:29:15 -05:00
Sabe Jones a921a8bc61 Merge branch 'release' into sabrecat/customizations 2024-03-14 14:55:56 -05:00
Sabe Jones 0aa9d4d1d5 WIP(shops): start wiring up buy modals 2024-03-11 16:09:16 -05:00
Sabe Jones 0ead06937b Merge branch 'release' into sabrecat/customizations 2024-03-11 14:54:34 -05:00
Sabe Jones 037fb6737d Merge branch 'release' into sabrecat/customizations 2024-03-04 14:25:53 -06:00
Sabe Jones 4e0d8cba51 Merge branch 'release' into sabrecat/customizations 2024-03-01 14:31:28 -06:00
Sabe Jones ecc8a65d28 WIP(customizations): animal bits 2024-02-29 15:59:31 -06:00
Sabe Jones 28fef8df86 WIP(shops): shirts vs skins 2024-02-27 15:17:10 -06:00
Sabe Jones 33b54a734e WIP(shop): more CSS, add hair styles 2024-02-22 17:03:07 -06:00
Sabe Jones 1f8aa7d778 Merge branch 'release' into sabrecat/customizations 2024-02-22 15:26:01 -06:00
Sabe Jones 09ff3ee865 WIP(customizations): load hair colors 2024-02-20 17:56:56 -06:00
Sabe Jones cbfeb18517 WIP(shop): backgrounds appear 2024-02-16 17:58:11 -06:00
Sabe Jones 63e7ace693 WIP(shops): customization categories skeleton 2024-02-15 17:51:14 -06:00
Sabe Jones 0f9cf48b55 Merge branch 'release' into sabrecat/customizations 2024-02-15 15:26:05 -06:00
Sabe Jones 5a48436eff WIP(shops): customizations route 2024-02-14 17:49:26 -06:00
371 changed files with 13635 additions and 7606 deletions
+2 -1
View File
@@ -8,7 +8,7 @@ i18n_cache
apidoc/html
*.swp
.idea*
config.json
config*.json
npm-debug.log*
lib
newrelic_agent.log
@@ -48,3 +48,4 @@ webpack.webstorm.config
# mongodb replica set for local dev
mongodb-*.tgz
/mongodb-data
/.nyc_output
+5 -2
View File
@@ -3,10 +3,13 @@ FROM node:20
# Install global packages
RUN npm install -g gulp-cli mocha
# Copy package.json and package-lock.json into image, then install
# dependencies.
# Copy package.json and package-lock.json into image
WORKDIR /usr/src/habitica
COPY ["package.json", "package-lock.json", "./"]
# Copy the remaining source files in.
COPY . /usr/src/habitica
# Install dependencies
RUN npm install
RUN npm run postinstall
RUN npm run client:build
RUN gulp build:prod
+5 -1
View File
@@ -32,6 +32,7 @@
"LOGGLY_CLIENT_TOKEN": "token",
"LOGGLY_SUBDOMAIN": "example-subdomain",
"LOGGLY_TOKEN": "example-token",
"LOG_REQUESTS_EXCESSIVE_MODE": "false",
"MAINTENANCE_MODE": "false",
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
@@ -88,5 +89,8 @@
"REDIS_HOST": "aaabbbcccdddeeefff",
"REDIS_PORT": "1234",
"REDIS_PASSWORD": "12345678",
"TRUSTED_DOMAINS": "localhost,https://habitica.com"
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
"TIME_TRAVEL_ENABLED": "false",
"DEBUG_ENABLED": "false",
"CONTENT_SWITCHOVER_TIME_OFFSET": 8
}
+5 -5
View File
@@ -44,8 +44,8 @@ function runInChildProcess (command, options = {}, envVariables = '') {
return done => pipe(exec(testBin(command, envVariables), options, done));
}
function integrationTestCommand (testDir, coverageDir) {
return `istanbul cover --dir coverage/${coverageDir} --report lcovonly node_modules/mocha/bin/_mocha -- ${testDir} --recursive --require ./test/helpers/start-server`;
function integrationTestCommand (testDir) {
return `nyc --silent --no-clean mocha ${testDir} --recursive --require ./test/helpers/start-server`;
}
/* Test task definitions */
@@ -148,7 +148,7 @@ gulp.task('test:content:safe', gulp.series('test:prepare:build', cb => {
gulp.task(
'test:api:unit:run',
runInChildProcess(integrationTestCommand('test/api/unit', 'coverage/api-unit')),
runInChildProcess(integrationTestCommand('test/api/unit')),
);
gulp.task('test:api:unit:watch', () => gulp.watch(['website/server/libs/*', 'test/api/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit:run', done => done())));
@@ -156,7 +156,7 @@ gulp.task('test:api:unit:watch', () => gulp.watch(['website/server/libs/*', 'tes
gulp.task('test:api-v3:integration', gulp.series(
'test:prepare:mongo',
runInChildProcess(
integrationTestCommand('test/api/v3/integration', 'coverage/api-v3-integration'),
integrationTestCommand('test/api/v3/integration'),
LIMIT_MAX_BUFFER_OPTIONS,
),
));
@@ -175,7 +175,7 @@ gulp.task('test:api-v3:integration:separate-server', runInChildProcess(
gulp.task('test:api-v4:integration', gulp.series(
'test:prepare:mongo',
runInChildProcess(
integrationTestCommand('test/api/v4', 'api-v4-integration'),
integrationTestCommand('test/api/v4'),
LIMIT_MAX_BUFFER_OPTIONS,
),
));
@@ -0,0 +1,149 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20240621_veteran_pet_ladder';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {};
let push = { notifications: { $each: [] }};
set.migration = MIGRATION_NAME;
if (user.items.pets['Dragon-Veteran']) {
set['items.pets.Cactus-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_cactus',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Cactus and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Fox-Veteran']) {
set['items.pets.Dragon-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_dragon',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Dragon and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Bear-Veteran']) {
set['items.pets.Fox-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_fox',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Fox and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Lion-Veteran']) {
set['items.pets.Bear-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_bear',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Bear and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Tiger-Veteran']) {
set['items.pets.Lion-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_lion',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Lion and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Wolf-Veteran']) {
set['items.pets.Tiger-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_tiger',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Tiger and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
} else {
set['items.pets.Wolf-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_wolf',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Wolf and 24 Gems!',
destination: '/inventory/stable',
},
seen: false,
});
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
await user.updateBalance(
6,
'admin_update_balance',
'',
'Veteran Ladder award',
);
return await User.updateOne(
{ _id: user._id },
{ $set: set, $push: push, $inc: { balance: 6 } },
).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': { $gt: new Date('2024-05-21') },
};
const fields = {
_id: 1,
items: 1,
migration: 1,
contributor: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};
+857 -565
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.25.1",
"version": "5.26.1",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
@@ -67,6 +67,7 @@
"remove-markdown": "^0.5.0",
"rimraf": "^3.0.2",
"short-uuid": "^4.2.2",
"sinon": "^15.2.0",
"stripe": "^12.18.0",
"superagent": "^8.1.2",
"universal-analytics": "^0.5.3",
@@ -93,11 +94,11 @@
"test:api-v3:integration:separate-server": "NODE_ENV=test gulp test:api-v3:integration:separate-server",
"test:api-v4:integration": "gulp test:api-v4:integration",
"test:api-v4:integration:separate-server": "NODE_ENV=test gulp test:api-v4:integration:separate-server",
"test:sanity": "istanbul cover --dir coverage/sanity --report lcovonly node_modules/mocha/bin/_mocha -- test/sanity --recursive",
"test:common": "istanbul cover --dir coverage/common --report lcovonly node_modules/mocha/bin/_mocha -- test/common --recursive",
"test:content": "istanbul cover --dir coverage/content --report lcovonly node_modules/mocha/bin/_mocha -- test/content --recursive",
"test:sanity": "nyc --silent --no-clean mocha test/sanity --recursive",
"test:common": "nyc --silent --no-clean mocha test/common --recursive",
"test:content": "nyc --silent --no-clean mocha test/content --recursive",
"test:nodemon": "gulp test:nodemon",
"coverage": "COVERAGE=true mocha --require register-handlers.js --reporter html-cov > coverage.html; open coverage.html",
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
"sprites": "gulp sprites:compile",
"client:dev": "cd website/client && npm run serve",
"client:build": "cd website/client && npm run build",
@@ -106,7 +107,8 @@
"debug": "gulp nodemon --inspect",
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
"apidoc": "gulp apidoc"
"apidoc": "gulp apidoc",
"heroku-postbuild": "npm run client:build"
},
"devDependencies": {
"axios": "^1.4.0",
@@ -115,13 +117,11 @@
"chai-moment": "^0.1.0",
"chalk": "^5.3.0",
"cross-spawn": "^7.0.3",
"expect.js": "^0.3.1",
"istanbul": "^1.1.0-alpha.1",
"mocha": "^5.1.1",
"monk": "^7.3.4",
"nyc": "^15.1.0",
"require-again": "^2.0.0",
"run-rs": "^0.7.7",
"sinon": "^15.2.0",
"sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0"
}
+89
View File
@@ -1,5 +1,9 @@
import fs from 'fs';
import * as contentLib from '../../../../website/server/libs/content';
import content from '../../../../website/common/script/content';
import {
generateRes,
} from '../../../helpers/api-unit.helper';
describe('contentLib', () => {
describe('CONTENT_CACHE_PATH', () => {
@@ -13,5 +17,90 @@ describe('contentLib', () => {
contentLib.getLocalizedContentResponse();
expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function');
});
it('removes keys from the content data', () => {
const response = contentLib.localizeContentData(content, 'en', { backgroundsFlat: true, dropHatchingPotions: true });
expect(response.backgroundsFlat).to.not.exist;
expect(response.backgrounds).to.exist;
expect(response.dropHatchingPotions).to.not.exist;
expect(response.hatchingPotions).to.exist;
});
it('removes nested keys from the content data', () => {
const response = contentLib.localizeContentData(content, 'en', { gear: { tree: true } });
expect(response.gear.tree).to.not.exist;
expect(response.gear.flat).to.exist;
});
});
it('generates a hash for a filter', () => {
const hash = contentLib.hashForFilter('backgroundsFlat,gear.flat');
expect(hash).to.equal('-1791877526');
});
it('serves content', () => {
const resSpy = generateRes();
contentLib.serveContent(resSpy, 'en', '', false);
expect(resSpy.send).to.have.been.calledOnce;
});
it('serves filtered content', () => {
const resSpy = generateRes();
contentLib.serveContent(resSpy, 'en', 'backgroundsFlat,gear.flat', false);
expect(resSpy.send).to.have.been.calledOnce;
});
describe('caches content', async () => {
let resSpy;
beforeEach(() => {
resSpy = generateRes();
if (fs.existsSync(contentLib.CONTENT_CACHE_PATH)) {
fs.rmSync(contentLib.CONTENT_CACHE_PATH, { recursive: true });
}
fs.mkdirSync(contentLib.CONTENT_CACHE_PATH);
});
it('does not cache requests in development mode', async () => {
contentLib.serveContent(resSpy, 'en', '', false);
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.false;
});
it('caches unfiltered requests', async () => {
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.false;
contentLib.serveContent(resSpy, 'en', '', true);
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.true;
});
it('serves cached requests', async () => {
fs.writeFileSync(
`${contentLib.CONTENT_CACHE_PATH}en.json`,
'{"success": true, "data": {"all": {}}}',
'utf8',
);
contentLib.serveContent(resSpy, 'en', '', true);
expect(resSpy.sendFile).to.have.been.calledOnce;
expect(resSpy.sendFile).to.have.been.calledWith(`${contentLib.CONTENT_CACHE_PATH}en.json`);
});
it('caches filtered requests', async () => {
const filter = 'backgroundsFlat,gear.flat';
const hash = contentLib.hashForFilter(filter);
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`)).to.be.false;
contentLib.serveContent(resSpy, 'en', filter, true);
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`)).to.be.true;
});
it('serves filtered cached requests', async () => {
const filter = 'backgroundsFlat,gear.flat';
const hash = contentLib.hashForFilter(filter);
fs.writeFileSync(
`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`,
'{"success": true, "data": {}}',
'utf8',
);
contentLib.serveContent(resSpy, 'en', filter, true);
expect(resSpy.sendFile).to.have.been.calledOnce;
expect(resSpy.sendFile).to.have.been.calledWith(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`);
});
});
});
+1 -1
View File
@@ -117,7 +117,7 @@ describe('Items Utils', () => {
it('converts values for owned gear to true/false', () => {
expect(castItemVal('items.gear.owned.shield_warrior_0', 'true')).to.equal(true);
expect(castItemVal('items.gear.owned.invalid', 'false')).to.equal(false);
expect(castItemVal('items.gear.owned.invalid', 'null')).to.equal(false);
expect(castItemVal('items.gear.owned.invalid', 'null')).to.equal(undefined);
expect(castItemVal('items.gear.owned.invalid', 'truthy')).to.equal(true);
expect(castItemVal('items.gear.owned.invalid', 0)).to.equal(false);
});
@@ -0,0 +1,51 @@
/* eslint-disable global-require */
import nconf from 'nconf';
import {
generateRes,
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
import ensureDevelopmentMode from '../../../../website/server/middlewares/ensureDevelopmentMode';
import { NotFound } from '../../../../website/server/libs/errors';
describe('developmentMode middleware', () => {
let res; let req; let next;
let nconfStub;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
nconfStub = sandbox.stub(nconf, 'get');
});
it('returns not found when on production URL', () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
ensureDevelopmentMode(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('returns not found when intentionally disabled', () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
ensureDevelopmentMode(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('passes when enabled and on non-production URL', () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
ensureDevelopmentMode(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
});
});
@@ -1,38 +0,0 @@
/* eslint-disable global-require */
import nconf from 'nconf';
import {
generateRes,
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
import ensureDevelpmentMode from '../../../../website/server/middlewares/ensureDevelpmentMode';
import { NotFound } from '../../../../website/server/libs/errors';
describe('developmentMode middleware', () => {
let res; let req; let
next;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
it('returns not found when in production mode', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
ensureDevelpmentMode(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('passes when not in production', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
ensureDevelpmentMode(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
});
});
@@ -0,0 +1,51 @@
/* eslint-disable global-require */
import nconf from 'nconf';
import {
generateRes,
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
import { NotFound } from '../../../../website/server/libs/errors';
import ensureTimeTravelMode from '../../../../website/server/middlewares/ensureTimeTravelMode';
describe('timetravelMode middleware', () => {
let res; let req; let next;
let nconfStub;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
nconfStub = sandbox.stub(nconf, 'get');
});
it('returns not found when using production URL', () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
ensureTimeTravelMode(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('returns not found when not in time travel mode', () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
ensureTimeTravelMode(req, res, next);
const calledWith = next.getCall(0).args;
expect(calledWith[0] instanceof NotFound).to.equal(true);
});
it('passes when in time travel mode', () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
ensureTimeTravelMode(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
});
});
+75 -1
View File
@@ -10,7 +10,7 @@ import { TooManyRequests } from '../../../../website/server/libs/errors';
import { apiError } from '../../../../website/server/libs/apiError';
import logger from '../../../../website/server/libs/logger';
describe('rateLimiter middleware', () => {
describe.only('rateLimiter middleware', () => {
const pathToRateLimiter = '../../../../website/server/middlewares/rateLimiter';
let res; let req; let next; let nconfGetStub;
@@ -54,6 +54,7 @@ describe('rateLimiter middleware', () => {
it('does not throw when there are available points', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
await attachRateLimiter(req, res, next);
@@ -71,6 +72,7 @@ describe('rateLimiter middleware', () => {
it('does not throw when an unknown error is thrown by the rate limiter', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
sandbox.stub(logger, 'error');
sandbox.stub(RateLimiterMemory.prototype, 'consume')
.returns(Promise.reject(new Error('Unknown error.')));
@@ -104,6 +106,7 @@ describe('rateLimiter middleware', () => {
it('limits when LIVELINESS_PROBE_KEY is incorrect', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.query.liveliness = 'das';
@@ -120,6 +123,7 @@ describe('rateLimiter middleware', () => {
it('limits when LIVELINESS_PROBE_KEY is not set', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns(undefined);
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
await attachRateLimiter(req, res, next);
@@ -135,6 +139,7 @@ describe('rateLimiter middleware', () => {
it('throws when LIVELINESS_PROBE_KEY is blank', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.query.liveliness = '';
@@ -150,6 +155,7 @@ describe('rateLimiter middleware', () => {
it('throws when there are no available points remaining', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
// call for 31 times
@@ -173,6 +179,7 @@ describe('rateLimiter middleware', () => {
it('uses the user id if supplied or the ip address', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.ip = 1;
@@ -199,4 +206,71 @@ describe('rateLimiter middleware', () => {
'X-RateLimit-Reset': sinon.match(Date),
});
});
it('applies increased cost for registration calls with and without user id', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_REGISTRATION_COST').returns(3);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.path = '/api/v4/user/auth/local/register';
req.ip = 1;
await attachRateLimiter(req, res, next);
req.headers['x-api-user'] = 'user-1';
await attachRateLimiter(req, res, next);
await attachRateLimiter(req, res, next);
// user id an ip are counted as separate sources
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 27, // 2 calls with user id
'X-RateLimit-Reset': sinon.match(Date),
});
req.headers['x-api-user'] = undefined;
await attachRateLimiter(req, res, next);
await attachRateLimiter(req, res, next);
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 24, // 3 calls with only ip
'X-RateLimit-Reset': sinon.match(Date),
});
});
it('applies increased cost for unauthenticated API calls', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(10);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.ip = 1;
await attachRateLimiter(req, res, next);
await attachRateLimiter(req, res, next);
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 10,
'X-RateLimit-Reset': sinon.match(Date),
});
});
describe('authentication rate limiting', async () => {
it('applies cost for failed login attempts', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.path = '/api/v4/user/auth/local/login';
req.ip = 1;
await attachRateLimiter(req, res, next);
await attachRateLimiter(req, res, next);
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 28,
'X-RateLimit-Reset': sinon.match(Date),
});
});
});
});
@@ -0,0 +1,37 @@
/* eslint-disable global-require */
import requireAgain from 'require-again';
import {
generateRes,
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
describe('requestLogHandler middleware', () => {
let res; let req; let
next;
const pathToMiddleWare = '../../../../website/server/middlewares/requestLogHandler';
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
it('attaches start time and request ID object to req', () => {
const middleware = requireAgain(pathToMiddleWare);
middleware.logRequestData(req, res, next);
expect(req.requestStartTime).to.exist;
expect(req.requestStartTime).to.be.a('number');
expect(req.requestIdentifier).to.exist;
expect(req.requestIdentifier).to.be.a('string');
});
it('calls next', () => {
const middleware = requireAgain(pathToMiddleWare);
const spy = sinon.spy();
middleware.logRequestData(req, res, spy);
expect(spy.calledOnce).to.be.true;
});
});
@@ -22,4 +22,38 @@ describe('GET /content', () => {
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
});
it('does not filter content for regular requests', async () => {
const res = await requester().get('/content');
expect(res).to.have.nested.property('backgrounds.backgrounds062014');
expect(res).to.have.nested.property('gear.tree');
});
it('filters content automatically for iOS requests', async () => {
const res = await requester(null, { 'x-client': 'habitica-ios' }).get('/content');
expect(res).to.have.nested.property('appearances.background.beach');
expect(res).to.not.have.nested.property('backgrounds.backgrounds062014');
expect(res).to.not.have.property('backgroundsFlat');
expect(res).to.not.have.nested.property('gear.tree');
});
it('filters content automatically for Android requests', async () => {
const res = await requester(null, { 'x-client': 'habitica-android' }).get('/content');
expect(res).to.not.have.nested.property('appearances.background.beach');
expect(res).to.have.nested.property('backgrounds.backgrounds062014');
expect(res).to.not.have.property('backgroundsFlat');
expect(res).to.not.have.nested.property('gear.tree');
});
it('filters content if the request specifies a filter', async () => {
const res = await requester().get('/content?filter=backgroundsFlat,gear.flat');
expect(res).to.not.have.property('backgroundsFlat');
expect(res).to.have.nested.property('gear.tree');
expect(res).to.not.have.nested.property('gear.flat');
});
it('filters content if the request contains invalid filters', async () => {
const res = await requester().get('/content?filter=backgroundsFlat,invalid');
expect(res).to.not.have.property('backgroundsFlat');
});
});
@@ -0,0 +1,46 @@
import nconf from 'nconf';
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('GET /debug/time-travel-time', () => {
let user;
let nconfStub;
before(async () => {
user = await generateUser({ permissions: { fullAccess: true } });
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
});
afterEach(() => {
nconfStub.restore();
});
it('returns the shifted time', async () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
const result = await user.get('/debug/time-travel-time');
expect(result.time).to.exist;
await user.post('/debug/jump-time', { disable: true });
});
it('returns shifted when the user is not an admin', async () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
const regularUser = await generateUser();
const result = await regularUser.get('/debug/time-travel-time');
expect(result.time).to.exist;
});
it('returns error when not in time travel mode', async () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
await expect(user.get('/debug/time-travel-time'))
.eventually.be.rejected.and.to.deep.equal({
code: 404,
error: 'NotFound',
message: 'Not found.',
});
});
});
@@ -5,16 +5,23 @@ import {
describe('POST /debug/add-hourglass', () => {
let userToGetHourGlass;
let nconfStub;
before(async () => {
userToGetHourGlass = await generateUser();
});
after(() => {
nconf.set('IS_PROD', false);
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconfStub.restore();
});
it('adds Hourglass to the current user', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
await userToGetHourGlass.post('/debug/add-hourglass');
const userWithHourGlass = await userToGetHourGlass.get('/user');
@@ -23,7 +30,7 @@ describe('POST /debug/add-hourglass', () => {
});
it('returns error when not in production mode', async () => {
nconf.set('IS_PROD', true);
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
await expect(userToGetHourGlass.post('/debug/add-hourglass'))
.eventually.be.rejected.and.to.deep.equal({
@@ -5,16 +5,23 @@ import {
describe('POST /debug/add-ten-gems', () => {
let userToGainTenGems;
let nconfStub;
before(async () => {
userToGainTenGems = await generateUser();
});
after(() => {
nconf.set('IS_PROD', false);
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconfStub.restore();
});
it('adds ten gems to the current user', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
await userToGainTenGems.post('/debug/add-ten-gems');
const userWithTenGems = await userToGainTenGems.get('/user');
@@ -23,7 +30,7 @@ describe('POST /debug/add-ten-gems', () => {
});
it('returns error when not in production mode', async () => {
nconf.set('IS_PROD', true);
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
await expect(userToGainTenGems.post('/debug/add-ten-gems'))
.eventually.be.rejected.and.to.deep.equal({
@@ -0,0 +1,82 @@
import nconf from 'nconf';
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
describe('POST /debug/jump-time', () => {
let user;
let today;
let nconfStub;
before(async () => {
user = await generateUser({ permissions: { fullAccess: true } });
today = new Date();
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
});
afterEach(() => {
nconfStub.restore();
});
after(async () => {
nconf.set('TIME_TRAVEL_ENABLED', true);
await user.post('/debug/jump-time', { disable: true });
nconf.set('TIME_TRAVEL_ENABLED', false);
});
it('Jumps forward', async () => {
const resultDate = new Date((await user.post('/debug/jump-time', { reset: true })).time);
expect(resultDate.getDate()).to.eql(today.getDate());
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 1 })).time);
expect(newResultDate.getDate()).to.eql(today.getDate() + 1);
expect(newResultDate.getMonth()).to.eql(today.getMonth());
expect(newResultDate.getFullYear()).to.eql(today.getFullYear());
});
it('jumps back', async () => {
const resultDate = new Date((await user.post('/debug/jump-time', { reset: true })).time);
expect(resultDate.getDate()).to.eql(today.getDate());
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: -1 })).time);
expect(newResultDate.getDate()).to.eql(today.getDate() - 1);
expect(newResultDate.getMonth()).to.eql(today.getMonth());
expect(newResultDate.getFullYear()).to.eql(today.getFullYear());
});
it('can jump a lot', async () => {
const resultDate = new Date((await user.post('/debug/jump-time', { reset: true })).time);
expect(resultDate.getDate()).to.eql(today.getDate());
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 355 })).time);
expect(newResultDate.getFullYear()).to.eql(today.getFullYear() + 1);
});
it('returns error when the user is not an admin', async () => {
const regularUser = await generateUser();
await expect(regularUser.post('/debug/jump-time', { offsetDays: 1 }))
.eventually.be.rejected.and.to.deep.equal({
code: 400,
error: 'BadRequest',
message: 'You do not have permission to time travel.',
});
});
it('returns error when not in time travel mode', async () => {
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
await expect(user.post('/debug/jump-time', { offsetDays: 1 }))
.eventually.be.rejected.and.to.deep.equal({
code: 404,
error: 'NotFound',
message: 'Not found.',
});
});
});
@@ -5,16 +5,23 @@ import {
describe('POST /debug/make-admin', () => {
let user;
let nconfStub;
before(async () => {
user = await generateUser();
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconf.set('IS_PROD', false);
nconfStub.restore();
});
it('makes user an admin', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
await user.post('/debug/make-admin');
await user.sync();
@@ -23,7 +30,7 @@ describe('POST /debug/make-admin', () => {
});
it('returns error when not in production mode', async () => {
nconf.set('IS_PROD', true);
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
await expect(user.post('/debug/make-admin'))
.eventually.be.rejected.and.to.deep.equal({
@@ -8,6 +8,7 @@ import {
describe('POST /debug/modify-inventory', () => {
let user; let
originalItems;
let nconfStub;
before(async () => {
originalItems = {
@@ -39,8 +40,14 @@ describe('POST /debug/modify-inventory', () => {
});
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconf.set('IS_PROD', false);
nconfStub.restore();
});
it('sets equipment', async () => {
@@ -149,7 +156,7 @@ describe('POST /debug/modify-inventory', () => {
});
it('returns error when not in production mode', async () => {
nconf.set('IS_PROD', true);
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
await expect(user.post('/debug/modify-inventory'))
.eventually.be.rejected.and.to.deep.equal({
@@ -5,13 +5,20 @@ import {
describe('POST /debug/quest-progress', () => {
let user;
let nconfStub;
beforeEach(async () => {
user = await generateUser();
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconf.set('IS_PROD', false);
nconfStub.restore();
});
it('errors if user is not on a quest', async () => {
@@ -48,7 +55,7 @@ describe('POST /debug/quest-progress', () => {
});
it('returns error when not in production mode', async () => {
nconf.set('IS_PROD', true);
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
await expect(user.post('/debug/quest-progress'))
.eventually.be.rejected.and.to.deep.equal({
@@ -5,13 +5,20 @@ import {
describe('POST /debug/set-cron', () => {
let user;
let nconfStub;
before(async () => {
user = await generateUser();
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconf.set('IS_PROD', false);
nconfStub.restore();
});
it('sets last cron', async () => {
@@ -27,7 +34,7 @@ describe('POST /debug/set-cron', () => {
});
it('returns error when not in production mode', async () => {
nconf.set('IS_PROD', true);
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
await expect(user.post('/debug/set-cron'))
.eventually.be.rejected.and.to.deep.equal({
@@ -17,9 +17,5 @@ describe('GET /shops/backgrounds', () => {
expect(shop.notes).to.eql(t('backgroundShop'));
expect(shop.imageName).to.equal('background_shop');
expect(shop.sets).to.be.an('array');
const sets = shop.sets.map(set => set.identifier);
expect(sets).to.include('incentiveBackgrounds');
expect(sets).to.include('backgrounds062014');
});
});
@@ -5,9 +5,15 @@ import {
describe('GET /shops/time-travelers', () => {
let user;
let clock;
beforeEach(async () => {
user = await generateUser();
clock = sinon.useFakeTimers(new Date('2024-06-08'));
});
afterEach(() => {
clock.restore();
});
it('returns a valid shop object', async () => {
@@ -33,6 +33,20 @@ describe('POST /user/purchase/:type/:key', () => {
expect(user.items[type][key]).to.equal(1);
});
it('purchases animal ears', async () => {
await user.post('/user/purchase/gear/headAccessory_special_tigerEars');
await user.sync();
expect(user.items.gear.owned.headAccessory_special_tigerEars).to.equal(true);
});
it('purchases animal tails', async () => {
await user.post('/user/purchase/gear/back_special_pandaTail');
await user.sync();
expect(user.items.gear.owned.back_special_pandaTail).to.equal(true);
});
it('can convert gold to gems if subscribed', async () => {
const oldBalance = user.balance;
await user.updateOne({
@@ -5,7 +5,7 @@ import {
describe('POST /user/unlock', () => {
let user;
const unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
const unlockPath = 'shirt.convict,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
const unlockCost = 1.25;
const usersStartingGems = 5;
@@ -274,6 +274,14 @@ describe('PUT /user', () => {
expect(get(updatedUser.preferences, type)).to.eql(item);
});
});
it('updates user when background is unequipped', async () => {
expect(get(user.preferences, 'background')).to.not.eql('');
const updatedUser = await user.put('/user', { 'preferences.background': '' });
expect(get(updatedUser.preferences, 'background')).to.eql('');
});
});
context('Improvement Categories', () => {
@@ -11,6 +11,7 @@ const { content } = shared;
describe('POST /user/buy/:key', () => {
let user;
let clock;
beforeEach(async () => {
user = await generateUser({
@@ -18,6 +19,12 @@ describe('POST /user/buy/:key', () => {
});
});
afterEach(() => {
if (clock) {
clock.restore();
}
});
// More tests in common code unit tests
it('returns an error if the item is not found', async () => {
@@ -68,9 +75,9 @@ describe('POST /user/buy/:key', () => {
});
it('buys a special spell', async () => {
clock = sinon.useFakeTimers(new Date('2024-10-31T00:00:00Z'));
const key = 'spookySparkles';
const item = content.special[key];
const stub = sinon.stub(item, 'canOwn').returns(true);
await user.updateOne({ 'stats.gp': 250 });
const res = await user.post(`/user/buy/${key}`);
@@ -83,8 +90,6 @@ describe('POST /user/buy/:key', () => {
expect(res.message).to.equal(t('messageBought', {
itemText: item.text(),
}));
stub.restore();
});
it('allows for bulk purchases', async () => {
@@ -1,5 +1,3 @@
import { TAVERN_ID } from '../../../../../website/server/models/group';
import { updateDocument } from '../../../../helpers/mongo';
import {
requester,
resetHabiticaDB,
@@ -18,7 +16,9 @@ describe('GET /world-state', () => {
});
it('returns Tavern quest data when world boss is active', async () => {
await updateDocument('groups', { _id: TAVERN_ID }, { quest: { active: true, key: 'dysheartener', progress: { hp: 50000, rage: 9999 } } });
sinon.stub(worldState, 'getWorldBoss').returns({
active: true, extra: {}, key: 'dysheartener', progress: { hp: 50000, rage: 9999, collect: {} },
});
const res = await requester().get('/world-state');
expect(res).to.have.nested.property('worldBoss');
@@ -33,15 +33,29 @@ describe('GET /world-state', () => {
rage: 9999,
},
});
worldState.getWorldBoss.restore();
});
it('calls getRepeatingEvents for data', async () => {
const getRepeatingEventsOnDate = sinon.stub(common.content, 'getRepeatingEventsOnDate').returns([]);
const getCurrentGalaEvent = sinon.stub(common.schedule, 'getCurrentGalaEvent').returns({});
await requester().get('/world-state');
expect(getRepeatingEventsOnDate).to.have.been.calledOnce;
expect(getCurrentGalaEvent).to.have.been.calledOnce;
getRepeatingEventsOnDate.restore();
getCurrentGalaEvent.restore();
});
context('no current event', () => {
beforeEach(async () => {
sinon.stub(worldState, 'getCurrentEvent').returns(null);
sinon.stub(worldState, 'getCurrentEventList').returns([]);
});
afterEach(() => {
worldState.getCurrentEvent.restore();
worldState.getCurrentEventList.restore();
});
it('returns null for the current event when there is none active', async () => {
@@ -51,18 +65,18 @@ describe('GET /world-state', () => {
});
});
context('no current event', () => {
context('active event', () => {
const evt = {
...common.content.events.fall2020,
event: 'fall2020',
};
beforeEach(async () => {
sinon.stub(worldState, 'getCurrentEvent').returns(evt);
sinon.stub(worldState, 'getCurrentEventList').returns([evt]);
});
afterEach(() => {
worldState.getCurrentEvent.restore();
worldState.getCurrentEventList.restore();
});
it('returns the current event when there is an active one', async () => {
@@ -71,4 +85,45 @@ describe('GET /world-state', () => {
expect(res.currentEvent).to.eql(evt);
});
});
context('active event with NPC image suffix', () => {
const evt = {
...common.content.events.fall2020,
event: 'fall2020',
npcImageSuffix: 'fall',
};
beforeEach(async () => {
sinon.stub(worldState, 'getCurrentEventList').returns([evt]);
});
afterEach(() => {
worldState.getCurrentEventList.restore();
});
it('returns the NPC image suffix when present', async () => {
const res = await requester().get('/world-state');
expect(res.npcImageSuffix).to.equal('fall');
});
it('returns the NPC image suffix with multiple events present', async () => {
const evt2 = {
...common.content.events.winter2020,
event: 'test',
};
const evt3 = {
...common.content.events.winter2020,
event: 'winter2020',
npcImageSuffix: 'winter',
};
worldState.getCurrentEventList.returns([evt, evt2, evt3]);
const res = await requester().get('/world-state');
expect(res.npcImageSuffix).to.equal('winter');
});
});
});
+67
View File
@@ -0,0 +1,67 @@
/* eslint-disable global-require */
import { expect } from 'chai';
import nconf from 'nconf';
const SWITCHOVER_TIME = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0;
describe('datedMemoize', () => {
it('should return a function that returns a function', () => {
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
const memoized = datedMemoize(() => {});
expect(memoized).to.be.a('function');
});
it('should not call multiple times', () => {
const stub = sandbox.stub().returns({});
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
const memoized = datedMemoize(stub);
memoized(1, 2);
memoized(1, 3);
expect(stub).to.have.been.calledOnce;
});
it('call multiple times for different identifiers', () => {
const stub = sandbox.stub().returns({});
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
const memoized = datedMemoize(stub);
memoized({ identifier: 'a', memoizeConfig: true }, 1, 2);
memoized({ identifier: 'b', memoizeConfig: true }, 1, 2);
expect(stub).to.have.been.calledTwice;
});
it('call once for the same identifier', () => {
const stub = sandbox.stub().returns({});
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
const memoized = datedMemoize(stub);
memoized({ identifier: 'a', memoizeConfig: true }, 1, 2);
memoized({ identifier: 'a', memoizeConfig: true }, 1, 2);
expect(stub).to.have.been.calledOnce;
});
it('call once on the same day', () => {
const stub = sandbox.stub().returns({});
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
const memoized = datedMemoize(stub);
memoized({ date: new Date('2024-01-01'), memoizeConfig: true }, 1, 2);
memoized({ date: new Date('2024-01-01'), memoizeConfig: true }, 1, 2);
expect(stub).to.have.been.calledOnce;
});
it('call multiple times on different days', () => {
const stub = sandbox.stub().returns({});
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
const memoized = datedMemoize(stub);
memoized({ date: new Date('2024-01-01'), memoizeConfig: true }, 1, 2);
memoized({ date: new Date('2024-01-02'), memoizeConfig: true }, 1, 2);
expect(stub).to.have.been.calledTwice;
});
it('respects switchover time', () => {
const stub = sandbox.stub().returns({});
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
const memoized = datedMemoize(stub);
memoized({ date: new Date('2024-01-01T00:00:00.000Z'), memoizeConfig: true }, 1, 2);
memoized({ date: new Date(`2024-01-01T${String(SWITCHOVER_TIME).padStart(2, '0')}`), memoizeConfig: true }, 1, 2);
expect(stub).to.have.been.calledTwice;
});
});
+123
View File
@@ -0,0 +1,123 @@
import {
generateUser,
} from '../../helpers/common.helper';
import cleanupPinnedItems from '../../../website/common/script/libs/cleanupPinnedItems';
describe('cleanupPinnedItems', () => {
let user;
let testPinnedItems;
let clock;
beforeEach(() => {
user = generateUser();
clock = sinon.useFakeTimers(new Date('2024-04-08'));
testPinnedItems = [
{ type: 'armoire', path: 'armoire' },
{ type: 'potion', path: 'potion' },
{ type: 'background', path: 'backgrounds.backgrounds042020.heather_field' },
{ type: 'background', path: 'backgrounds.backgrounds042021.heather_field' },
{ type: 'premiumHatchingPotion', path: 'premiumHatchingPotions.Rainbow' },
{ type: 'premiumHatchingPotion', path: 'premiumHatchingPotions.StainedGlass' },
{ type: 'quests', path: 'quests.rat' },
{ type: 'quests', path: 'quests.spider' },
{ type: 'quests', path: 'quests.moon1' },
{ type: 'quests', path: 'quests.silver' },
{ type: 'marketGear', path: 'gear.flat.head_special_nye2021' },
{ type: 'gear', path: 'gear.flat.armor_special_spring2019Rogue' },
{ type: 'gear', path: 'gear.flat.armor_special_winter2021Rogue' },
{ type: 'mystery_set', path: 'mystery.201804' },
{ type: 'mystery_set', path: 'mystery.201506' },
{ type: 'bundles', path: 'bundles.farmFriends' },
{ type: 'bundles', path: 'bundles.birdBuddies' },
{ type: 'customization', path: 'skin.birdBuddies' },
];
});
afterEach(() => {
clock.restore();
});
it('always keeps armoire and potion', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'armoire')).to.exist;
expect(_.find(result, item => item.path === 'potion')).to.exist;
});
it('removes simple items that are no longer available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'backgrounds.backgrounds042021.heather_field')).to.not.exist;
expect(_.find(result, item => item.path === 'premiumHatchingPotions.Rainbow')).to.not.exist;
});
it('keeps simple items that are still available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'backgrounds.backgrounds042020.heather_field')).to.exist;
expect(_.find(result, item => item.path === 'premiumHatchingPotions.StainedGlass')).to.exist;
});
it('removes gear that is no longer available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'gear.flat.armor_special_winter2021Rogue')).to.not.exist;
});
it('keeps gear that is still available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'gear.flat.armor_special_spring2019Rogue')).to.exist;
});
it('keeps gear that is not seasonal', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'gear.flat.head_special_nye2021')).to.exist;
});
it('removes time traveler gear that is no longer available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'mystery.201506')).to.not.exist;
});
it('keeps time traveler gear that is still available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'mystery.201804')).to.exist;
});
it('removes quests that are no longer available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'quests.rat')).to.not.exist;
expect(_.find(result, item => item.path === 'quests.silver')).to.not.exist;
});
it('keeps quests that are still available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'quests.spider')).to.exist;
});
it('keeps quests that are not seasonal', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'quests.moon1')).to.exist;
});
it('removes bundles that are no longer available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'bundles.farmFriends')).to.not.exist;
});
it('keeps bundles that are still available', () => {
user.pinnedItems = testPinnedItems;
const result = cleanupPinnedItems(user);
expect(_.find(result, item => item.path === 'bundles.birdBuddies')).to.exist;
});
});
-219
View File
@@ -1,219 +0,0 @@
import shared from '../../../website/common';
import {
generateUser,
} from '../../helpers/common.helper';
describe('shops', () => {
const user = generateUser();
describe('market', () => {
const shopCategories = shared.shops.getMarketCategories(user);
it('contains at least the 3 default categories', () => {
expect(shopCategories.length).to.be.greaterThan(2);
});
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'class'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
});
});
it('shows relevant non class gear in special category', () => {
const contributor = generateUser({
contributor: {
level: 7,
critical: true,
},
items: {
gear: {
owned: {
weapon_armoire_basicCrossbow: true, // eslint-disable-line camelcase
},
},
},
});
const gearCategories = shared.shops.getMarketGearCategories(contributor);
const specialCategory = gearCategories.find(o => o.identifier === 'none');
expect(specialCategory.items.find(item => item.key === 'weapon_special_1'));
expect(specialCategory.items.find(item => item.key === 'armor_special_1'));
expect(specialCategory.items.find(item => item.key === 'head_special_1'));
expect(specialCategory.items.find(item => item.key === 'shield_special_1'));
expect(specialCategory.items.find(item => item.key === 'weapon_special_critical'));
expect(specialCategory.items.find(item => item.key === 'weapon_armoire_basicCrossbow'));// eslint-disable-line camelcase
});
it('does not show gear when it is all owned', () => {
const userWithItems = generateUser({
stats: {
class: 'wizard',
},
items: {
gear: {
owned: {
weapon_wizard_0: true, // eslint-disable-line camelcase
weapon_wizard_1: true, // eslint-disable-line camelcase
weapon_wizard_2: true, // eslint-disable-line camelcase
weapon_wizard_3: true, // eslint-disable-line camelcase
weapon_wizard_4: true, // eslint-disable-line camelcase
weapon_wizard_5: true, // eslint-disable-line camelcase
weapon_wizard_6: true, // eslint-disable-line camelcase
armor_wizard_1: true, // eslint-disable-line camelcase
armor_wizard_2: true, // eslint-disable-line camelcase
armor_wizard_3: true, // eslint-disable-line camelcase
armor_wizard_4: true, // eslint-disable-line camelcase
armor_wizard_5: true, // eslint-disable-line camelcase
head_wizard_1: true, // eslint-disable-line camelcase
head_wizard_2: true, // eslint-disable-line camelcase
head_wizard_3: true, // eslint-disable-line camelcase
head_wizard_4: true, // eslint-disable-line camelcase
head_wizard_5: true, // eslint-disable-line camelcase
},
},
},
});
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
expect(shopWizardItems.length).to.eql(0);
});
it('shows available gear not yet purchased and previously owned', () => {
const userWithItems = generateUser({
stats: {
class: 'wizard',
},
items: {
gear: {
owned: {
weapon_wizard_0: true, // eslint-disable-line camelcase
weapon_wizard_1: true, // eslint-disable-line camelcase
weapon_wizard_2: true, // eslint-disable-line camelcase
weapon_wizard_3: true, // eslint-disable-line camelcase
weapon_wizard_4: true, // eslint-disable-line camelcase
armor_wizard_1: true, // eslint-disable-line camelcase
armor_wizard_2: true, // eslint-disable-line camelcase
armor_wizard_3: false, // eslint-disable-line camelcase
armor_wizard_4: false, // eslint-disable-line camelcase
head_wizard_1: true, // eslint-disable-line camelcase
head_wizard_2: false, // eslint-disable-line camelcase
head_wizard_3: true, // eslint-disable-line camelcase
head_wizard_4: false, // eslint-disable-line camelcase
head_wizard_5: true, // eslint-disable-line camelcase
},
},
},
});
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_5').locked).to.eql(false);
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_6').locked).to.eql(true);
expect(shopWizardItems.find(item => item.key === 'armor_wizard_3').locked).to.eql(false);
expect(shopWizardItems.find(item => item.key === 'armor_wizard_4').locked).to.eql(true);
expect(shopWizardItems.find(item => item.key === 'head_wizard_2').locked).to.eql(false);
expect(shopWizardItems.find(item => item.key === 'head_wizard_4').locked).to.eql(true);
});
});
describe('questShop', () => {
const shopCategories = shared.shops.getQuestShopCategories(user);
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
if (category.identifier === 'bundle') {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'purchaseType', 'class'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
} else {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
}
});
});
});
describe('timeTravelers', () => {
const shopCategories = shared.shops.getTimeTravelersCategories(user);
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
_.each(category.items, item => {
_.each(['key', 'text', 'value', 'currency', 'locked', 'purchaseType', 'class', 'notes', 'class'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
});
});
});
describe('seasonalShop', () => {
const shopCategories = shared.shops.getSeasonalShopCategories(user);
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'type'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
});
});
});
});
+430
View File
@@ -0,0 +1,430 @@
import shared from '../../../website/common';
import {
generateUser,
} from '../../helpers/common.helper';
import seasonalConfig from '../../../website/common/script/libs/shops-seasonal.config';
describe('shops', () => {
const user = generateUser();
let clock;
afterEach(() => {
if (clock) {
clock.restore();
}
user.achievements.quests = {};
});
describe('market', () => {
const shopCategories = shared.shops.getMarketCategories(user);
it('contains at least the 3 default categories', () => {
expect(shopCategories.length).to.be.greaterThan(2);
});
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'class'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
});
});
describe('premium hatching potions', () => {
it('contains current scheduled premium hatching potions', async () => {
clock = sinon.useFakeTimers(new Date('2024-04-01'));
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.length).to.eql(2);
});
it('does not contain past scheduled premium hatching potions', async () => {
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.filter(x => x.key === 'Aquatic' || x.key === 'Celestial').length).to.eql(0);
});
it('returns end date for scheduled premium potions', async () => {
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
potions.items.forEach(potion => {
expect(potion.end).to.exist;
});
});
it('contains unlocked quest premium hatching potions', async () => {
user.achievements.quests = {
bronze: 1,
blackPearl: 1,
};
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').length).to.eql(2);
});
it('does not contain locked quest premium hatching potions', async () => {
clock = sinon.useFakeTimers(new Date('2024-04-01'));
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.length).to.eql(2);
expect(potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').length).to.eql(0);
});
it('does not return end date for quest premium potions', async () => {
user.achievements.quests = {
bronze: 1,
blackPearl: 1,
};
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').forEach(potion => {
expect(potion.end).to.not.exist;
});
});
});
it('does not return items with event data', async () => {
shopCategories.forEach(category => {
category.items.forEach(item => {
expect(item.event).to.not.exist;
expect(item.season).to.not.exist;
});
});
});
it('shows relevant non class gear in special category', () => {
const contributor = generateUser({
contributor: {
level: 7,
critical: true,
},
items: {
gear: {
owned: {
weapon_armoire_basicCrossbow: true, // eslint-disable-line camelcase
},
},
},
});
const gearCategories = shared.shops.getMarketGearCategories(contributor);
const specialCategory = gearCategories.find(o => o.identifier === 'none');
expect(specialCategory.items.find(item => item.key === 'weapon_special_1'), 'weapon_special_1');
expect(specialCategory.items.find(item => item.key === 'armor_special_1'), 'armor_special_1');
expect(specialCategory.items.find(item => item.key === 'head_special_1'), 'head_special_1');
expect(specialCategory.items.find(item => item.key === 'shield_special_1'), 'shield_special_1');
expect(specialCategory.items.find(item => item.key === 'weapon_special_critical'), 'weapon_special_critical');
expect(specialCategory.items.find(item => item.key === 'weapon_armoire_basicCrossbow'), 'weapon_armoire_basicCrossbow');// eslint-disable-line camelcase
});
describe('handles seasonal gear', () => {
beforeEach(() => {
clock = sinon.useFakeTimers(new Date('2024-04-01'));
});
it('shows current seasonal gear for warriors', () => {
const warriorItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'warrior').items.filter(x => x.key.indexOf('spring2024') !== -1);
expect(warriorItems.length, 'Warrior seasonal gear').to.eql(4);
});
it('shows current seasonal gear for mages', () => {
const mageItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'wizard').items.filter(x => x.key.indexOf('spring2024') !== -1);
expect(mageItems.length, 'Mage seasonal gear').to.eql(3);
});
it('shows current seasonal gear for healers', () => {
const healerItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'healer').items.filter(x => x.key.indexOf('spring2024') !== -1);
expect(healerItems.length, 'Healer seasonal gear').to.eql(4);
});
it('shows current seasonal gear for rogues', () => {
const rogueItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'rogue').items.filter(x => x.key.indexOf('spring2024') !== -1);
expect(rogueItems.length, 'Rogue seasonal gear').to.eql(4);
});
it('seasonal gear contains end date', () => {
const categories = shared.shops.getMarketGearCategories(user);
categories.forEach(category => {
category.items.filter(x => x.key.indexOf('spring2024') !== -1).forEach(item => {
expect(item.end, item.key).to.exist;
});
});
});
it('only shows gear for the current season', () => {
const categories = shared.shops.getMarketGearCategories(user);
categories.forEach(category => {
const otherSeasons = category.items.filter(item => item.key.indexOf('winter') !== -1 || item.key.indexOf('summer') !== -1 || item.key.indexOf('fall') !== -1);
expect(otherSeasons.length).to.eql(0);
});
});
it('does not show gear from past seasons', () => {
const categories = shared.shops.getMarketGearCategories(user);
categories.forEach(category => {
const otherYears = category.items.filter(item => item.key.indexOf('spring') !== -1 && item.key.indexOf('2024') === -1);
expect(otherYears.length).to.eql(0);
});
});
});
it('does not show gear when it is all owned', () => {
const userWithItems = generateUser({
stats: {
class: 'wizard',
},
items: {
gear: {
owned: {
weapon_wizard_0: true, // eslint-disable-line camelcase
weapon_wizard_1: true, // eslint-disable-line camelcase
weapon_wizard_2: true, // eslint-disable-line camelcase
weapon_wizard_3: true, // eslint-disable-line camelcase
weapon_wizard_4: true, // eslint-disable-line camelcase
weapon_wizard_5: true, // eslint-disable-line camelcase
weapon_wizard_6: true, // eslint-disable-line camelcase
armor_wizard_1: true, // eslint-disable-line camelcase
armor_wizard_2: true, // eslint-disable-line camelcase
armor_wizard_3: true, // eslint-disable-line camelcase
armor_wizard_4: true, // eslint-disable-line camelcase
armor_wizard_5: true, // eslint-disable-line camelcase
head_wizard_1: true, // eslint-disable-line camelcase
head_wizard_2: true, // eslint-disable-line camelcase
head_wizard_3: true, // eslint-disable-line camelcase
head_wizard_4: true, // eslint-disable-line camelcase
head_wizard_5: true, // eslint-disable-line camelcase
},
},
},
});
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
expect(shopWizardItems.length).to.eql(0);
});
it('shows available gear not yet purchased and previously owned', () => {
const userWithItems = generateUser({
stats: {
class: 'wizard',
},
items: {
gear: {
owned: {
weapon_wizard_0: true, // eslint-disable-line camelcase
weapon_wizard_1: true, // eslint-disable-line camelcase
weapon_wizard_2: true, // eslint-disable-line camelcase
weapon_wizard_3: true, // eslint-disable-line camelcase
weapon_wizard_4: true, // eslint-disable-line camelcase
armor_wizard_1: true, // eslint-disable-line camelcase
armor_wizard_2: true, // eslint-disable-line camelcase
armor_wizard_3: false, // eslint-disable-line camelcase
armor_wizard_4: false, // eslint-disable-line camelcase
head_wizard_1: true, // eslint-disable-line camelcase
head_wizard_2: false, // eslint-disable-line camelcase
head_wizard_3: true, // eslint-disable-line camelcase
head_wizard_4: false, // eslint-disable-line camelcase
head_wizard_5: true, // eslint-disable-line camelcase
},
},
},
});
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_5').locked).to.eql(false);
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_6').locked).to.eql(true);
expect(shopWizardItems.find(item => item.key === 'armor_wizard_3').locked).to.eql(false);
expect(shopWizardItems.find(item => item.key === 'armor_wizard_4').locked).to.eql(true);
expect(shopWizardItems.find(item => item.key === 'head_wizard_2').locked).to.eql(false);
expect(shopWizardItems.find(item => item.key === 'head_wizard_4').locked).to.eql(true);
});
});
describe('questShop', () => {
const shopCategories = shared.shops.getQuestShopCategories(user);
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length, category.identifier).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
if (category.identifier === 'bundle') {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'purchaseType', 'class'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
} else {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
}
});
});
it('does not return items with event data', async () => {
shopCategories.forEach(category => {
category.items.forEach(item => {
expect(item.event).to.not.exist;
});
});
});
});
describe('timeTravelers', () => {
const shopCategories = shared.shops.getTimeTravelersCategories(user);
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
_.each(category.items, item => {
_.each(['key', 'text', 'value', 'currency', 'locked', 'purchaseType', 'class', 'notes', 'class'], key => {
expect(_.has(item, key)).to.eql(true);
});
});
});
});
it('does not return items with event data', async () => {
shopCategories.forEach(category => {
category.items.forEach(item => {
expect(item.event).to.not.exist;
});
});
});
it('returns pets', () => {
const pets = shopCategories.find(cat => cat.identifier === 'pets').items;
expect(pets.length).to.be.greaterThan(0);
});
it('returns mounts', () => {
const mounts = shopCategories.find(cat => cat.identifier === 'mounts').items;
expect(mounts.length).to.be.greaterThan(0);
});
it('returns quests', () => {
const quests = shopCategories.find(cat => cat.identifier === 'quests').items;
expect(quests.length).to.be.greaterThan(0);
});
it('returns backgrounds', () => {
const backgrounds = shopCategories.find(cat => cat.identifier === 'backgrounds').items;
expect(backgrounds.length).to.be.greaterThan(0);
});
});
describe('customizationShop', () => {
const shopCategories = shared.shops.getCustomizationsShopCategories(user, null);
it('does not return items with event data', async () => {
shopCategories.forEach(category => {
category.items.forEach(item => {
expect(item.event, item.key).to.not.exist;
});
});
});
it('backgrounds category contains end date', () => {
const backgroundCategory = shopCategories.find(cat => cat.identifier === 'backgrounds');
expect(backgroundCategory.end).to.exist;
expect(backgroundCategory.end).to.be.greaterThan(new Date());
});
it('hair color category contains end date', () => {
const colorCategory = shopCategories.find(cat => cat.identifier === 'color');
expect(colorCategory.end).to.exist;
expect(colorCategory.end).to.be.greaterThan(new Date());
});
it('skin category contains end date', () => {
const colorCategory = shopCategories.find(cat => cat.identifier === 'color');
expect(colorCategory.end).to.exist;
expect(colorCategory.end).to.be.greaterThan(new Date());
});
});
describe('seasonalShop', () => {
const shopCategories = shared.shops.getSeasonalShopCategories(user, null, seasonalConfig());
const today = new Date();
it('does not contain an empty category', () => {
_.each(shopCategories, category => {
expect(category.items.length).to.be.greaterThan(0);
});
});
it('does not duplicate identifiers', () => {
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
expect(identifiers.length).to.eql(shopCategories.length);
});
it('does not return items with event data', async () => {
shopCategories.forEach(category => {
category.items.forEach(item => {
expect(item.event, item.key).to.not.exist;
});
});
});
it('items contain required fields', () => {
_.each(shopCategories, category => {
_.each(category.items, item => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'type'], key => {
expect(_.has(item, key), item.key).to.eql(true);
});
});
});
});
it('items have a valid end date', () => {
shopCategories.forEach(category => {
category.items.forEach(item => {
expect(item.end, item.key).to.be.a('date');
expect(item.end, item.key).to.be.greaterThan(today);
});
});
});
it('items match current season', () => {
const currentSeason = seasonalConfig().currentSeason.toLowerCase();
shopCategories.forEach(category => {
category.items.forEach(item => {
if (item.klass === 'special') {
expect(item.season, item.key).to.eql(currentSeason);
}
});
});
});
});
});
+2 -2
View File
@@ -1,11 +1,11 @@
import * as armoireSet from '../../../website/common/script/content/gear/sets/armoire';
import armoireSet from '../../../website/common/script/content/gear/sets/armoire';
describe('armoireSet items', () => {
it('checks if canOwn has the same id', () => {
Object.keys(armoireSet).forEach(type => {
if (type === 'all') return;
Object.keys(armoireSet[type]).forEach(itemKey => {
const ownedKey = `${type}_armoire_${itemKey}`;
expect(armoireSet[type][itemKey].canOwn({
items: {
gear: {
@@ -49,7 +49,7 @@ describe('shared.ops.buy', () => {
}
});
it('recovers 15 hp', async () => {
it('buys health potion', async () => {
user.stats.hp = 30;
await buy(user, { params: { key: 'potion' } }, analytics);
expect(user.stats.hp).to.eql(45);
@@ -17,9 +17,7 @@ function getFullArmoire () {
_.each(content.gearTypes, type => {
_.each(content.gear.tree[type].armoire, gearObject => {
if (gearObject.released) {
fullArmoire[gearObject.key] = true;
}
fullArmoire[gearObject.key] = true;
});
});
@@ -22,6 +22,7 @@ async function buyGear (user, req, analytics) {
describe('shared.ops.buyMarketGear', () => {
let user;
const analytics = { track () {} };
let clock;
beforeEach(() => {
user = generateUser({
@@ -54,6 +55,10 @@ describe('shared.ops.buyMarketGear', () => {
shared.fns.predictableRandom.restore();
shared.onboarding.checkOnboardingStatus.restore();
analytics.track.restore();
if (clock) {
clock.restore();
}
});
context('Gear', () => {
@@ -184,30 +189,28 @@ describe('shared.ops.buyMarketGear', () => {
});
// TODO after user.ops.equip is done
xit('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', async () => {
it('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', async () => {
user.stats.gp = 100;
user.preferences.autoEquip = true;
await buyGear(user, { params: { key: 'shield_warrior_1' } });
user.ops.equip({ params: { key: 'shield_warrior_1' } });
await buyGear(user, { params: { key: 'weapon_warrior_1' } });
user.ops.equip({ params: { key: 'weapon_warrior_1' } });
user.items.gear.equipped.weapon = 'weapon_warrior_1';
user.items.gear.equipped.shield = 'shield_warrior_1';
user.stats.class = 'wizard';
await buyGear(user, { params: { key: 'weapon_wizard_1' } });
await buyGear(user, { params: { key: 'weapon_wizard_0' } });
expect(user.items.gear.equipped).to.have.property('shield', 'shield_base_0');
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_wizard_1');
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_wizard_0');
});
// TODO after user.ops.equip is done
xit('buyGears two-handed equipment but does not automatically remove sword or shield', async () => {
it('buyGears two-handed equipment but does not automatically remove sword or shield', async () => {
user.stats.gp = 100;
user.preferences.autoEquip = false;
await buyGear(user, { params: { key: 'shield_warrior_1' } });
user.ops.equip({ params: { key: 'shield_warrior_1' } });
await buyGear(user, { params: { key: 'weapon_warrior_1' } });
user.ops.equip({ params: { key: 'weapon_warrior_1' } });
user.items.gear.equipped.weapon = 'weapon_warrior_1';
user.items.gear.equipped.shield = 'shield_warrior_1';
user.stats.class = 'wizard';
await buyGear(user, { params: { key: 'weapon_wizard_1' } });
await buyGear(user, { params: { key: 'weapon_wizard_0' } });
expect(user.items.gear.equipped).to.have.property('shield', 'shield_warrior_1');
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_warrior_1');
@@ -283,5 +286,40 @@ describe('shared.ops.buyMarketGear', () => {
expect(user.items.gear.owned).to.have.property('shield_armoire_ramHornShield', true);
});
it('buys current seasonal gear', async () => {
user.stats.gp = 200;
clock = sinon.useFakeTimers(new Date('2024-01-01'));
await buyGear(user, { params: { key: 'armor_special_winter2024Warrior' } });
expect(user.items.gear.owned).to.have.property('armor_special_winter2024Warrior', true);
});
it('errors when buying past seasonal gear', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-01'));
user.stats.gp = 200;
try {
await buyGear(user, { params: { key: 'armor_special_winter2023Warrior' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notAvailable'));
expect(user.items.gear.owned).to.not.have.property('armor_special_winter2023Warrior');
}
});
it('errors when buying gear from wrong season', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-01'));
user.stats.gp = 200;
try {
await buyGear(user, { params: { key: 'weapon_special_spring2024Warrior' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notAvailable'));
expect(user.items.gear.owned).to.not.have.property('weapon_special_spring2024Warrior');
}
});
});
});
@@ -15,6 +15,7 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buyMysterySet', () => {
let user;
const analytics = { track () {} };
let clock;
beforeEach(() => {
user = generateUser({
@@ -31,6 +32,9 @@ describe('shared.ops.buyMysterySet', () => {
afterEach(() => {
analytics.track.restore();
if (clock) {
clock.restore();
}
});
context('Mystery Sets', () => {
@@ -41,7 +45,7 @@ describe('shared.ops.buyMysterySet', () => {
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.eql(i18n.t('notEnoughHourglasses'));
expect(user.items.gear.owned).to.have.property('weapon_warrior_0', true);
expect(user.items.gear.owned).to.not.have.property('armor_mystery_201501');
}
});
@@ -72,6 +76,18 @@ describe('shared.ops.buyMysterySet', () => {
expect(err.message).to.equal(errorMessage('missingKeyParam'));
}
});
it('returns error if the set is not available', async () => {
user.purchased.plan.consecutive.trinkets = 1;
clock = sinon.useFakeTimers(new Date('2024-01-16'));
try {
await buyMysterySet(user, { params: { key: '201501' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.eql(i18n.t('notAvailable'));
expect(user.items.gear.owned).to.not.have.property('armor_mystery_201501');
}
});
});
context('successful purchases', () => {
@@ -86,6 +102,16 @@ describe('shared.ops.buyMysterySet', () => {
expect(user.items.gear.owned).to.have.property('head_mystery_301404', true);
expect(user.items.gear.owned).to.have.property('eyewear_mystery_301404', true);
});
it('buys mystery set if it is available', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-16'));
user.purchased.plan.consecutive.trinkets = 1;
await buyMysterySet(user, { params: { key: '201601' } }, analytics);
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
expect(user.items.gear.owned).to.have.property('shield_mystery_201601', true);
expect(user.items.gear.owned).to.have.property('head_mystery_201601', true);
});
});
});
});
@@ -10,6 +10,7 @@ import { BuyQuestWithGemOperation } from '../../../../website/common/script/ops/
describe('shared.ops.buyQuestGems', () => {
let user;
let clock;
const goldPoints = 40;
const analytics = { track () {} };
@@ -26,11 +27,13 @@ describe('shared.ops.buyQuestGems', () => {
beforeEach(() => {
sinon.stub(analytics, 'track');
sinon.spy(pinnedGearUtils, 'removeItemByPath');
clock = sinon.useFakeTimers(new Date('2024-01-16'));
});
afterEach(() => {
analytics.track.restore();
pinnedGearUtils.removeItemByPath.restore();
clock.restore();
});
context('single purchase', () => {
@@ -44,7 +47,7 @@ describe('shared.ops.buyQuestGems', () => {
user.pinnedItems.push({ type: 'quests', key: 'gryphon' });
});
it('successfully purchases quest', async () => {
it('successfully purchases pet quest', async () => {
const key = 'gryphon';
await buyQuest(user, { params: { key } });
@@ -52,6 +55,61 @@ describe('shared.ops.buyQuestGems', () => {
expect(user.items.quests[key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('successfully purchases hatching potion quest', async () => {
const key = 'silver';
await buyQuest(user, { params: { key } });
expect(user.items.quests[key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('successfully purchases seasonal quest', async () => {
const key = 'evilsanta';
await buyQuest(user, { params: { key } });
expect(user.items.quests[key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('errors if the pet quest is not available', async () => {
const key = 'sabretooth';
try {
await buyQuest(user, { params: { key } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notAvailable'));
expect(user.items.quests[key]).to.eql(undefined);
}
});
it('errors if the hatching potion quest is not available', async () => {
const key = 'ruby';
try {
await buyQuest(user, { params: { key } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notAvailable'));
expect(user.items.quests[key]).to.eql(undefined);
}
});
it('errors if the seasonal quest is not available', async () => {
const key = 'egg';
try {
await buyQuest(user, { params: { key } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notAvailable'));
expect(user.items.quests[key]).to.eql(undefined);
}
});
it('if a user\'s count of a quest scroll is negative, it will be reset to 0 before incrementing when they buy a new one.', async () => {
const key = 'dustbunnies';
user.items.quests[key] = -1;
@@ -61,6 +119,7 @@ describe('shared.ops.buyQuestGems', () => {
expect(user.items.quests[key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('errors if the user has not completed prerequisite quests', async () => {
const key = 'atom3';
user.achievements.quests.atom1 = 1;
@@ -73,6 +132,7 @@ describe('shared.ops.buyQuestGems', () => {
expect(user.items.quests[key]).to.eql(undefined);
}
});
it('successfully purchases quest if user has completed all prerequisite quests', async () => {
const key = 'atom3';
user.achievements.quests.atom1 = 1;
-89
View File
@@ -1,89 +0,0 @@
import { BuySpellOperation } from '../../../../website/common/script/ops/buy/buySpell';
import {
BadRequest,
NotFound,
NotAuthorized,
} from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n';
import {
generateUser,
} from '../../../helpers/common.helper';
import content from '../../../../website/common/script/content/index';
import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
describe('shared.ops.buySpecialSpell', () => {
let user;
const analytics = { track () {} };
async function buySpecialSpell (_user, _req, _analytics) {
const buyOp = new BuySpellOperation(_user, _req, _analytics);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
it('throws an error if params.key is missing', async () => {
try {
await buySpecialSpell(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(errorMessage('missingKeyParam'));
}
});
it('throws an error if the spell doesn\'t exists', async () => {
try {
await buySpecialSpell(user, {
params: {
key: 'notExisting',
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotFound);
expect(err.message).to.equal(errorMessage('spellNotFound', { spellId: 'notExisting' }));
}
});
it('throws an error if the user doesn\'t have enough gold', async () => {
user.stats.gp = 1;
try {
await buySpecialSpell(user, {
params: {
key: 'thankyou',
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
}
});
it('buys an item', async () => {
user.stats.gp = 11;
const item = content.special.thankyou;
const [data, message] = await buySpecialSpell(user, {
params: {
key: 'thankyou',
},
}, analytics);
expect(user.stats.gp).to.equal(1);
expect(user.items.special.thankyou).to.equal(1);
expect(data).to.eql({
items: user.items,
stats: user.stats,
});
expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(),
}));
expect(analytics.track).to.be.calledOnce;
});
});
+172
View File
@@ -0,0 +1,172 @@
import { BuySpellOperation } from '../../../../website/common/script/ops/buy/buySpell';
import {
BadRequest,
NotFound,
NotAuthorized,
} from '../../../../website/common/script/libs/errors';
import i18n from '../../../../website/common/script/i18n';
import {
generateUser,
} from '../../../helpers/common.helper';
import content from '../../../../website/common/script/content/index';
import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
describe('shared.ops.buySpecialSpell', () => {
let user;
let clock;
const analytics = { track () {} };
async function buySpecialSpell (_user, _req, _analytics) {
const buyOp = new BuySpellOperation(_user, _req, _analytics);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
if (clock) {
clock.restore();
}
});
it('throws an error if params.key is missing', async () => {
try {
await buySpecialSpell(user);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(errorMessage('missingKeyParam'));
}
});
it('throws an error if the item doesn\'t exists', async () => {
try {
await buySpecialSpell(user, {
params: {
key: 'notExisting',
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotFound);
expect(err.message).to.equal(errorMessage('spellNotFound', { spellId: 'notExisting' }));
}
});
it('throws an error if the user doesn\'t have enough gold', async () => {
user.stats.gp = 1;
try {
await buySpecialSpell(user, {
params: {
key: 'thankyou',
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
}
});
describe('buying cards', () => {
it('buys a card that is always available', async () => {
user.stats.gp = 11;
const item = content.special.thankyou;
const [data, message] = await buySpecialSpell(user, {
params: {
key: 'thankyou',
},
}, analytics);
expect(user.stats.gp).to.equal(1);
expect(user.items.special.thankyou).to.equal(1);
expect(data).to.eql({
items: user.items,
stats: user.stats,
});
expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(),
}));
expect(analytics.track).to.be.calledOnce;
});
it('buys a limited card when it is available', async () => {
user.stats.gp = 11;
const item = content.special.nye;
clock = sinon.useFakeTimers(new Date('2024-01-01'));
const [data, message] = await buySpecialSpell(user, {
params: {
key: 'nye',
},
}, analytics);
expect(user.stats.gp).to.equal(1);
expect(user.items.special.nye).to.equal(1);
expect(data).to.eql({
items: user.items,
stats: user.stats,
});
expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(),
}));
expect(analytics.track).to.be.calledOnce;
});
it('throws an error if the card is not currently available', async () => {
user.stats.gp = 11;
clock = sinon.useFakeTimers(new Date('2024-06-01'));
try {
await buySpecialSpell(user, {
params: {
key: 'nye',
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('cannotBuyItem'));
}
});
});
describe('buying spells', () => {
it('buys a spell if it is currently available', async () => {
user.stats.gp = 16;
clock = sinon.useFakeTimers(new Date('2024-06-22'));
const item = content.special.seafoam;
const [data, message] = await buySpecialSpell(user, {
params: {
key: 'seafoam',
},
}, analytics);
expect(user.stats.gp).to.equal(1);
expect(user.items.special.seafoam).to.equal(1);
expect(data).to.eql({
items: user.items,
stats: user.stats,
});
expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(),
}));
expect(analytics.track).to.be.calledOnce;
});
it('throws an error if the spell is not currently available', async () => {
user.stats.gp = 50;
clock = sinon.useFakeTimers(new Date('2024-01-22'));
try {
await buySpecialSpell(user, {
params: {
key: 'seafoam',
},
});
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('cannotBuyItem'));
}
});
});
});
@@ -15,6 +15,7 @@ import {
describe('shared.ops.purchase', () => {
const SEASONAL_FOOD = moment().isBefore('2021-11-02T20:00-04:00') ? 'Candy_Base' : 'Meat';
let user;
let clock;
const goldPoints = 40;
const analytics = { track () {} };
@@ -25,11 +26,13 @@ describe('shared.ops.purchase', () => {
beforeEach(() => {
sinon.stub(analytics, 'track');
sinon.spy(pinnedGearUtils, 'removeItemByPath');
clock = sandbox.useFakeTimers(new Date('2024-01-10'));
});
afterEach(() => {
analytics.track.restore();
pinnedGearUtils.removeItemByPath.restore();
clock.restore();
});
context('failure conditions', () => {
@@ -82,13 +85,77 @@ describe('shared.ops.purchase', () => {
it('returns error when user does not have enough gems to buy an item', async () => {
try {
await purchase(user, { params: { type: 'gear', key: 'headAccessory_special_wolfEars' } });
await purchase(user, { params: { type: 'gear', key: 'shield_special_winter2019Healer' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notEnoughGems'));
}
});
it('returns error when gear is not available', async () => {
try {
await purchase(user, { params: { type: 'gear', key: 'shield_special_spring2019Healer' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
}
});
it('returns an error when purchasing current seasonal gear', async () => {
user.balance = 2;
try {
await purchase(user, { params: { type: 'gear', key: 'shield_special_winter2024Healer' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
}
});
it('returns error when hatching potion is not available', async () => {
try {
await purchase(user, { params: { type: 'hatchingPotions', key: 'PolkaDot' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
}
});
it('returns error when quest for hatching potion was not yet completed', async () => {
try {
await purchase(user, { params: { type: 'hatchingPotions', key: 'BlackPearl' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
}
});
it('returns error when quest for egg was not yet completed', async () => {
try {
await purchase(user, { params: { type: 'eggs', key: 'Octopus' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
}
});
it('returns error when bundle is not available', async () => {
try {
await purchase(user, { params: { type: 'bundles', key: 'forestFriends' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('notAvailable'));
}
});
it('returns error when gear is not gem purchasable', async () => {
try {
await purchase(user, { params: { type: 'gear', key: 'shield_healer_3' } });
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
}
});
it('returns error when item is not found', async () => {
const params = { key: 'notExisting', type: 'food' };
@@ -99,44 +166,6 @@ describe('shared.ops.purchase', () => {
expect(err.message).to.equal(i18n.t('contentKeyNotFound', params));
}
});
it('returns error when user supplies a non-numeric quantity', async () => {
const type = 'eggs';
const key = 'Wolf';
try {
await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
}
});
it('returns error when user supplies a negative quantity', async () => {
const type = 'eggs';
const key = 'Wolf';
user.balance = 10;
try {
await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
}
});
it('returns error when user supplies a decimal quantity', async () => {
const type = 'eggs';
const key = 'Wolf';
user.balance = 10;
try {
await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
}
});
});
context('successful purchase', () => {
@@ -150,7 +179,7 @@ describe('shared.ops.purchase', () => {
user.pinnedItems.push({ type: 'eggs', key: 'Wolf' });
user.pinnedItems.push({ type: 'hatchingPotions', key: 'Base' });
user.pinnedItems.push({ type: 'food', key: SEASONAL_FOOD });
user.pinnedItems.push({ type: 'gear', key: 'headAccessory_special_tigerEars' });
user.pinnedItems.push({ type: 'gear', key: 'shield_special_winter2019Healer' });
user.pinnedItems.push({ type: 'bundles', key: 'featheredFriends' });
});
@@ -185,9 +214,9 @@ describe('shared.ops.purchase', () => {
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('purchases gear', async () => {
it('purchases past seasonal gear', async () => {
const type = 'gear';
const key = 'headAccessory_special_tigerEars';
const key = 'shield_special_winter2019Healer';
await purchase(user, { params: { type, key } });
@@ -195,9 +224,39 @@ describe('shared.ops.purchase', () => {
expect(pinnedGearUtils.removeItemByPath.calledOnce).to.equal(true);
});
it('purchases hatching potion', async () => {
const type = 'hatchingPotions';
const key = 'Peppermint';
await purchase(user, { params: { type, key } });
expect(user.items.hatchingPotions[key]).to.eql(1);
});
it('purchases hatching potion if user completed quest', async () => {
const type = 'hatchingPotions';
const key = 'Bronze';
user.achievements.quests.bronze = 1;
await purchase(user, { params: { type, key } });
expect(user.items.hatchingPotions[key]).to.eql(1);
});
it('purchases egg if user completed quest', async () => {
const type = 'eggs';
const key = 'Deer';
user.achievements.quests.ghost_stag = 1;
await purchase(user, { params: { type, key } });
expect(user.items.eggs[key]).to.eql(1);
});
it('purchases quest bundles', async () => {
const startingBalance = user.balance;
const clock = sandbox.useFakeTimers(moment('2024-03-20').valueOf());
clock.restore();
clock = sandbox.useFakeTimers(moment('2022-03-10').valueOf());
const type = 'bundles';
const key = 'cuddleBuddies';
const price = 1.75;
@@ -216,7 +275,6 @@ describe('shared.ops.purchase', () => {
expect(user.balance).to.equal(startingBalance - price);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
clock.restore();
});
});
@@ -257,5 +315,43 @@ describe('shared.ops.purchase', () => {
expect(user.items[type][key]).to.equal(2);
});
it('returns error when user supplies a non-numeric quantity', async () => {
const type = 'eggs';
const key = 'Wolf';
try {
await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
}
});
it('returns error when user supplies a negative quantity', async () => {
const type = 'eggs';
const key = 'Wolf';
user.balance = 10;
try {
await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
}
});
it('returns error when user supplies a decimal quantity', async () => {
const type = 'eggs';
const key = 'Wolf';
user.balance = 10;
try {
await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
}
});
});
});
@@ -2,14 +2,17 @@ import get from 'lodash/get';
import unlock from '../../../website/common/script/ops/unlock';
import i18n from '../../../website/common/script/i18n';
import { generateUser } from '../../helpers/common.helper';
import { NotAuthorized, BadRequest } from '../../../website/common/script/libs/errors';
import {
NotAuthorized,
BadRequest,
} from '../../../website/common/script/libs/errors';
describe('shared.ops.unlock', () => {
let user;
const unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
let clock;
const unlockPath = 'shirt.convict,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
const backgroundUnlockPath = 'background.giant_florals';
const backgroundSetUnlockPath = 'background.archery_range,background.giant_florals,background.rainbows_end';
const hairUnlockPath = 'hair.color.rainbow,hair.color.yellow,hair.color.green,hair.color.purple,hair.color.blue,hair.color.TRUred';
const facialHairUnlockPath = 'hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3';
const usersStartingGems = 50 / 4;
@@ -17,6 +20,11 @@ describe('shared.ops.unlock', () => {
beforeEach(() => {
user = generateUser();
user.balance = usersStartingGems;
clock = sandbox.useFakeTimers(new Date('2024-04-10'));
});
afterEach(() => {
clock.restore();
});
it('returns an error when path is not provided', async () => {
@@ -31,7 +39,9 @@ describe('shared.ops.unlock', () => {
it('does not unlock lost gear', async () => {
user.items.gear.owned.headAccessory_special_bearEars = false;
await unlock(user, { query: { path: 'items.gear.owned.headAccessory_special_bearEars' } });
await unlock(user, {
query: { path: 'items.gear.owned.headAccessory_special_bearEars' },
});
expect(user.balance).to.equal(usersStartingGems);
});
@@ -95,7 +105,9 @@ describe('shared.ops.unlock', () => {
it('returns an error if gear is not from the animal set', async () => {
try {
await unlock(user, { query: { path: 'items.gear.owned.back_mystery_202004' } });
await unlock(user, {
query: { path: 'items.gear.owned.back_mystery_202004' },
});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
@@ -153,7 +165,6 @@ describe('shared.ops.unlock', () => {
await unlock(user, { query: { path: partialUnlockPaths[4] } });
await unlock(user, { query: { path: partialUnlockPaths[5] } });
await unlock(user, { query: { path: partialUnlockPaths[6] } });
await unlock(user, { query: { path: partialUnlockPaths[7] } });
await unlock(user, { query: { path: unlockPath } });
});
@@ -163,7 +174,9 @@ describe('shared.ops.unlock', () => {
await unlock(user, { query: { path: backgroundUnlockPath } });
const afterBalance = user.balance;
const response = await unlock(user, { query: { path: backgroundUnlockPath } });
const response = await unlock(user, {
query: { path: backgroundUnlockPath },
});
expect(user.balance).to.equal(afterBalance); // do not bill twice
expect(response.message).to.not.exist;
@@ -176,7 +189,9 @@ describe('shared.ops.unlock', () => {
await unlock(user, { query: { path: backgroundUnlockPath } }); // unlock
const afterBalance = user.balance;
await unlock(user, { query: { path: backgroundUnlockPath } }); // equip
const response = await unlock(user, { query: { path: backgroundUnlockPath } });
const response = await unlock(user, {
query: { path: backgroundUnlockPath },
});
expect(user.balance).to.equal(afterBalance); // do not bill twice
expect(response.message).to.not.exist;
@@ -192,8 +207,9 @@ describe('shared.ops.unlock', () => {
individualPaths.forEach(path => {
expect(get(user.purchased, path)).to.be.true;
});
expect(Object.keys(user.purchased.shirt).length)
.to.equal(initialShirts + individualPaths.length);
expect(Object.keys(user.purchased.shirt).length).to.equal(
initialShirts + individualPaths.length,
);
expect(user.balance).to.equal(usersStartingGems - 1.25);
});
@@ -208,8 +224,9 @@ describe('shared.ops.unlock', () => {
individualPaths.forEach(path => {
expect(get(user.purchased, path)).to.be.true;
});
expect(Object.keys(user.purchased.hair.color).length)
.to.equal(initialHairColors + individualPaths.length);
expect(Object.keys(user.purchased.hair.color).length).to.equal(
initialHairColors + individualPaths.length,
);
expect(user.balance).to.equal(usersStartingGems - 1.25);
});
@@ -219,21 +236,28 @@ describe('shared.ops.unlock', () => {
const initialMustache = Object.keys(user.purchased.hair.mustache).length;
const initialBeard = Object.keys(user.purchased.hair.mustache).length;
const [, message] = await unlock(user, { query: { path: facialHairUnlockPath } });
const [, message] = await unlock(user, {
query: { path: facialHairUnlockPath },
});
expect(message).to.equal(i18n.t('unlocked'));
const individualPaths = facialHairUnlockPath.split(',');
individualPaths.forEach(path => {
expect(get(user.purchased, path)).to.be.true;
});
expect(Object.keys(user.purchased.hair.mustache).length + Object.keys(user.purchased.hair.beard).length) // eslint-disable-line max-len
expect(
Object.keys(user.purchased.hair.mustache).length
+ Object.keys(user.purchased.hair.beard).length,
) // eslint-disable-line max-len
.to.equal(initialMustache + initialBeard + individualPaths.length);
expect(user.balance).to.equal(usersStartingGems - 1.25);
});
it('unlocks a full set of gear', async () => {
const initialGear = Object.keys(user.items.gear.owned).length;
const [, message] = await unlock(user, { query: { path: unlockGearSetPath } });
const [, message] = await unlock(user, {
query: { path: unlockGearSetPath },
});
expect(message).to.equal(i18n.t('unlocked'));
@@ -241,32 +265,21 @@ describe('shared.ops.unlock', () => {
individualPaths.forEach(path => {
expect(get(user, path)).to.be.true;
});
expect(Object.keys(user.items.gear.owned).length)
.to.equal(initialGear + individualPaths.length);
expect(Object.keys(user.items.gear.owned).length).to.equal(
initialGear + individualPaths.length,
);
expect(user.balance).to.equal(usersStartingGems - 1.25);
});
it('unlocks a full set of backgrounds', async () => {
const initialBackgrounds = Object.keys(user.purchased.background).length;
const [, message] = await unlock(user, { query: { path: backgroundSetUnlockPath } });
expect(message).to.equal(i18n.t('unlocked'));
const individualPaths = backgroundSetUnlockPath.split(',');
individualPaths.forEach(path => {
expect(get(user.purchased, path)).to.be.true;
});
expect(Object.keys(user.purchased.background).length)
.to.equal(initialBackgrounds + individualPaths.length);
expect(user.balance).to.equal(usersStartingGems - 3.75);
});
it('unlocks an item (appearance)', async () => {
const path = unlockPath.split(',')[0];
const initialShirts = Object.keys(user.purchased.shirt).length;
const [, message] = await unlock(user, { query: { path } });
expect(message).to.equal(i18n.t('unlocked'));
expect(Object.keys(user.purchased.shirt).length).to.equal(initialShirts + 1);
expect(Object.keys(user.purchased.shirt).length).to.equal(
initialShirts + 1,
);
expect(get(user.purchased, path)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 0.5);
});
@@ -279,7 +292,9 @@ describe('shared.ops.unlock', () => {
const [, message] = await unlock(user, { query: { path } });
expect(message).to.equal(i18n.t('unlocked'));
expect(Object.keys(user.purchased.hair.color).length).to.equal(initialColorHair + 1);
expect(Object.keys(user.purchased.hair.color).length).to.equal(
initialColorHair + 1,
);
expect(get(user.purchased, path)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 0.5);
});
@@ -295,8 +310,12 @@ describe('shared.ops.unlock', () => {
expect(message).to.equal(i18n.t('unlocked'));
expect(Object.keys(user.purchased.hair.mustache).length).to.equal(initialMustache + 1);
expect(Object.keys(user.purchased.hair.beard).length).to.equal(initialBeard);
expect(Object.keys(user.purchased.hair.mustache).length).to.equal(
initialMustache + 1,
);
expect(Object.keys(user.purchased.hair.beard).length).to.equal(
initialBeard,
);
expect(get(user.purchased, path)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 0.5);
@@ -315,11 +334,24 @@ describe('shared.ops.unlock', () => {
it('unlocks an item (background)', async () => {
const initialBackgrounds = Object.keys(user.purchased.background).length;
const [, message] = await unlock(user, { query: { path: backgroundUnlockPath } });
const [, message] = await unlock(user, {
query: { path: backgroundUnlockPath },
});
expect(message).to.equal(i18n.t('unlocked'));
expect(Object.keys(user.purchased.background).length).to.equal(initialBackgrounds + 1);
expect(Object.keys(user.purchased.background).length).to.equal(
initialBackgrounds + 1,
);
expect(get(user.purchased, backgroundUnlockPath)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 1.75);
});
it('handles an invalid hair path gracefully', async () => {
try {
await unlock(user, { query: { path: 'hair.invalid' } });
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
}
});
});
-27
View File
@@ -1,27 +0,0 @@
/* eslint-disable prefer-template, no-shadow, func-names, import/no-commonjs */
const expect = require('expect.js');
module.exports.addCustomMatchers = function () {
const { Assertion } = expect;
Assertion.prototype.toHaveGP = function (gp) {
const actual = this.obj.stats.gp;
return this.assert(actual === gp, () => 'expected user to have ' + gp + ' gp, but got ' + actual, () => 'expected user to not have ' + gp + ' gp');
};
Assertion.prototype.toHaveHP = function (hp) {
const actual = this.obj.stats.hp;
return this.assert(actual === hp, () => 'expected user to have ' + hp + ' hp, but got ' + actual, () => 'expected user to not have ' + hp + ' hp');
};
Assertion.prototype.toHaveExp = function (exp) {
const actual = this.obj.stats.exp;
return this.assert(actual === exp, () => 'expected user to have ' + exp + ' experience points, but got ' + actual, () => 'expected user to not have ' + exp + ' experience points');
};
Assertion.prototype.toHaveLevel = function (lvl) {
const actual = this.obj.stats.lvl;
return this.assert(actual === lvl, () => 'expected user to be level ' + lvl + ', but got ' + actual, () => 'expected user to not be level ' + lvl);
};
Assertion.prototype.toHaveMaxMP = function (mp) {
const actual = this.obj._statsComputed.maxMP;
return this.assert(actual === mp, () => 'expected user to have ' + mp + ' max mp, but got ' + actual, () => 'expected user to not have ' + mp + ' max mp');
};
};
+72
View File
@@ -0,0 +1,72 @@
/* eslint-disable global-require */
import forEach from 'lodash/forEach';
import {
expectValidTranslationString,
} from '../helpers/content.helper';
import armoire from '../../website/common/script/content/gear/sets/armoire';
describe('armoire', () => {
let clock;
afterEach(() => {
if (clock) {
clock.restore();
}
});
it('does not return unreleased gear', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-02'));
const items = armoire.all;
expect(items.length).to.equal(377);
expect(items.filter(item => item.set === 'pottersSet' || item.set === 'optimistSet' || item.set === 'schoolUniform')).to.be.an('array').that.is.empty;
});
it('released gear has all required properties', async () => {
clock = sinon.useFakeTimers(new Date('2024-05-08'));
const items = armoire.all;
expect(items.length).to.equal(396);
forEach(items, item => {
if (item.set !== undefined) {
expect(item.set, item.key).to.be.a('string');
expect(item.set, item.key).to.not.be.empty;
}
expectValidTranslationString(item.text);
expect(item.value, item.key).to.be.a('number');
});
});
it('releases gear when appropriate', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-01T00:00:00.000Z'));
const items = armoire.all;
expect(items.length).to.equal(377);
clock.restore();
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
clock = sinon.useFakeTimers(new Date('2024-01-08'));
const januaryItems = armoire.all;
expect(januaryItems.length).to.equal(381);
clock.restore();
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
clock = sinon.useFakeTimers(new Date('2024-02-07'));
const januaryItems2 = armoire.all;
expect(januaryItems2.length).to.equal(381);
clock.restore();
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
clock = sinon.useFakeTimers(new Date('2024-02-07T09:00:00.000Z'));
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 -18
View File
@@ -5,29 +5,51 @@ import {
expectValidTranslationString,
} from '../helpers/content.helper';
import * as eggs from '../../website/common/script/content/eggs';
import eggs from '../../website/common/script/content/eggs';
describe('eggs', () => {
describe('all', () => {
it('is a combination of drop and quest eggs', () => {
const dropNumber = Object.keys(eggs.drops).length;
const questNumber = Object.keys(eggs.quests).length;
const allNumber = Object.keys(eggs.all).length;
let clock;
expect(allNumber).to.be.greaterThan(0);
expect(allNumber).to.equal(dropNumber + questNumber);
});
afterEach(() => {
if (clock) {
clock.restore();
}
});
it('contains basic information about each egg', () => {
each(eggs.all, (egg, key) => {
expectValidTranslationString(egg.text);
expectValidTranslationString(egg.adjective);
expectValidTranslationString(egg.mountText);
expectValidTranslationString(egg.notes);
expect(egg.canBuy).to.be.a('function');
expect(egg.value).to.be.a('number');
expect(egg.key).to.equal(key);
const eggTypes = [
'drops',
'quests',
];
eggTypes.forEach(eggType => {
describe(eggType, () => {
it('contains basic information about each egg', () => {
each(eggs[eggType], (egg, key) => {
expectValidTranslationString(egg.text);
expectValidTranslationString(egg.adjective);
expectValidTranslationString(egg.mountText);
expectValidTranslationString(egg.notes);
expect(egg.canBuy).to.be.a('function');
expect(egg.value).to.be.a('number');
expect(egg.key).to.equal(key);
});
});
});
});
it('does not contain unreleased eggs', () => {
clock = sinon.useFakeTimers(new Date('2024-05-20'));
const questEggs = eggs.quests;
expect(questEggs.Giraffe).to.not.exist;
});
it('Releases eggs when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-05-20'));
const mayEggs = eggs.quests;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const juneEggs = eggs.quests;
expect(juneEggs.Giraffe).to.exist;
expect(Object.keys(mayEggs).length).to.equal(Object.keys(juneEggs).length - 1);
});
});
+40
View File
@@ -0,0 +1,40 @@
import { getRepeatingEvents } from '../../website/common/script/content/constants/events';
describe('events', () => {
let clock;
afterEach(() => {
if (clock) {
clock.restore();
}
});
it('returns empty array when no events are active', () => {
clock = sinon.useFakeTimers(new Date('2024-01-06'));
const events = getRepeatingEvents();
expect(events).to.be.empty;
});
it('returns events when active', () => {
clock = sinon.useFakeTimers(new Date('2024-01-31'));
const events = getRepeatingEvents();
expect(events).to.have.length(1);
expect(events[0].key).to.equal('birthday');
expect(events[0].end).to.be.greaterThan(new Date());
expect(events[0].start).to.be.lessThan(new Date());
});
it('returns nye event at beginning of the year', () => {
clock = sinon.useFakeTimers(new Date('2025-01-01'));
const events = getRepeatingEvents();
expect(events).to.have.length(1);
expect(events[0].key).to.equal('nye');
});
it('returns nye event at end of the year', () => {
clock = sinon.useFakeTimers(new Date('2024-12-30'));
const events = getRepeatingEvents();
expect(events).to.have.length(1);
expect(events[0].key).to.equal('nye');
});
});
+94
View File
@@ -0,0 +1,94 @@
/* eslint-disable global-require */
import {
each,
} from 'lodash';
import {
expectValidTranslationString,
} from '../helpers/content.helper';
import content from '../../website/common/script/content';
describe('food', () => {
let clock;
afterEach(() => {
if (clock) {
clock.restore();
}
delete require.cache[require.resolve('../../website/common/script/content')];
});
describe('all', () => {
it('contains basic information about each food item', () => {
each(content.food, (foodItem, key) => {
if (foodItem.key === 'Saddle') {
expectValidTranslationString(foodItem.sellWarningNote);
} else {
expectValidTranslationString(foodItem.textA);
expectValidTranslationString(foodItem.textThe);
expect(foodItem.target).to.be.a('string');
}
expectValidTranslationString(foodItem.text);
expectValidTranslationString(foodItem.notes);
expect(foodItem.canBuy).to.be.a('function');
expect(foodItem.value).to.be.a('number');
expect(foodItem.key).to.equal(key);
});
});
it('sets canDrop for normal food if there is no food season', () => {
clock = sinon.useFakeTimers(new Date(2024, 5, 8));
const datedContent = require('../../website/common/script/content').default;
each(datedContent.food, foodItem => {
if (foodItem.key.indexOf('Cake') === -1 && foodItem.key.indexOf('Candy_') === -1 && foodItem.key.indexOf('Pie_') === -1 && foodItem.key !== 'Saddle') {
expect(foodItem.canDrop).to.equal(true);
} else {
expect(foodItem.canDrop).to.equal(false);
}
});
});
it('sets canDrop for candy if it is candy season', () => {
clock = sinon.useFakeTimers(new Date(2024, 9, 31));
const datedContent = require('../../website/common/script/content').default;
each(datedContent.food, foodItem => {
if (foodItem.key.indexOf('Candy_') !== -1) {
expect(foodItem.canDrop).to.equal(true);
} else {
expect(foodItem.canDrop).to.equal(false);
}
});
});
it('sets canDrop for cake if it is cake season', () => {
clock = sinon.useFakeTimers(new Date(2024, 0, 31));
const datedContent = require('../../website/common/script/content').default;
each(datedContent.food, foodItem => {
if (foodItem.key.indexOf('Cake_') !== -1) {
expect(foodItem.canDrop).to.equal(true);
} else {
expect(foodItem.canDrop).to.equal(false);
}
});
});
it('sets canDrop for pie if it is pie season', () => {
clock = sinon.useFakeTimers(new Date(2024, 2, 14));
const datedContent = require('../../website/common/script/content').default;
each(datedContent.food, foodItem => {
if (foodItem.key.indexOf('Pie_') !== -1) {
expect(foodItem.canDrop).to.equal(true);
} else {
expect(foodItem.canDrop).to.equal(false);
}
});
});
});
it('sets correct values for saddles', () => {
const saddle = content.food.Saddle;
expect(saddle.canBuy).to.be.a('function');
expect(saddle.value).to.equal(5);
expect(saddle.key).to.equal('Saddle');
expect(saddle.canDrop).to.equal(false);
});
});
@@ -4,6 +4,8 @@ import {
expectValidTranslationString,
} from '../helpers/content.helper';
import { CLASSES } from '../../website/common/script/content/constants';
import gearData from '../../website/common/script/content/gear';
import * as backerGear from '../../website/common/script/content/gear/sets/special/special-backer';
import * as contributorGear from '../../website/common/script/content/gear/sets/special/special-contributor';
@@ -17,35 +19,48 @@ describe('Gear', () => {
context(`${klass} ${gearType}s`, () => {
it('have a value of at least 0 for each stat', () => {
each(items, gear => {
expect(gear.con).to.be.at.least(0);
expect(gear.int).to.be.at.least(0);
expect(gear.per).to.be.at.least(0);
expect(gear.str).to.be.at.least(0);
expect(gear.con, gear.key).to.be.at.least(0);
expect(gear.int, gear.key).to.be.at.least(0);
expect(gear.per, gear.key).to.be.at.least(0);
expect(gear.str, gear.key).to.be.at.least(0);
});
});
it('have a purchase value of at least 0', () => {
each(items, gear => {
expect(gear.value).to.be.at.least(0);
expect(gear.value, gear.key).to.be.at.least(0);
});
});
it('has a canBuy function', () => {
each(items, gear => {
expect(gear.canBuy).to.be.a('function');
expect(gear.canBuy, gear.key).to.be.a('function');
});
});
it('have valid translation strings for text and notes', () => {
each(items, gear => {
expectValidTranslationString(gear.text);
expectValidTranslationString(gear.notes);
expectValidTranslationString(gear.text, gear.key);
expectValidTranslationString(gear.notes, gear.key);
});
});
it('has a set attribue', () => {
each(items, gear => {
expect(gear.set).to.exist;
expect(gear.set, gear.key).to.exist;
});
});
it('has a valid value for klass or specialClass', () => {
const validClassValues = CLASSES + ['base', 'mystery', 'armoire'];
each(items, gear => {
const field = gear.klass === 'special' ? gear.specialClass : gear.klass;
if (gear.klass === 'special' && field === undefined) {
// some special gear doesn't have a klass
return;
}
expect(field, gear.key).to.exist;
expect(validClassValues, gear.key).to.include(field);
});
});
});
@@ -53,6 +68,16 @@ describe('Gear', () => {
});
});
it('only assigns mage weapons twoHanded', () => {
each([allGear.armor.special, allGear.head.special, allGear.shield.special], gearType => {
each(gearType, gear => {
if (gear.specialClass === 'wizard') {
expect(gear.twoHanded, gear.key).to.not.eql(true);
}
});
});
});
describe('backer gear', () => {
let user;
+39 -17
View File
@@ -5,28 +5,50 @@ import {
expectValidTranslationString,
} from '../helpers/content.helper';
import * as hatchingPotions from '../../website/common/script/content/hatching-potions';
import hatchingPotions from '../../website/common/script/content/hatching-potions';
describe('hatchingPotions', () => {
describe('all', () => {
it('is a combination of drop, premium, and wacky potions', () => {
const dropNumber = Object.keys(hatchingPotions.drops).length;
const premiumNumber = Object.keys(hatchingPotions.premium).length;
const wackyNumber = Object.keys(hatchingPotions.wacky).length;
const allNumber = Object.keys(hatchingPotions.all).length;
let clock;
expect(allNumber).to.be.greaterThan(0);
expect(allNumber).to.equal(dropNumber + premiumNumber + wackyNumber);
});
afterEach(() => {
if (clock) {
clock.restore();
}
});
it('contains basic information about each potion', () => {
each(hatchingPotions.all, (potion, key) => {
expectValidTranslationString(potion.text);
expectValidTranslationString(potion.notes);
expect(potion.canBuy).to.be.a('function');
expect(potion.value).to.be.a('number');
expect(potion.key).to.equal(key);
const potionTypes = [
'drops',
'quests',
'premium',
'wacky',
];
potionTypes.forEach(potionType => {
describe(potionType, () => {
it('contains basic information about each potion', () => {
each(hatchingPotions.all, (potion, key) => {
expectValidTranslationString(potion.text);
expectValidTranslationString(potion.notes);
expect(potion.canBuy).to.be.a('function');
expect(potion.value).to.be.a('number');
expect(potion.key).to.equal(key);
});
});
});
});
it('does not contain unreleased potions', () => {
clock = sinon.useFakeTimers(new Date('2024-05-20'));
const premiumPotions = hatchingPotions.premium;
expect(premiumPotions.Koi).to.not.exist;
});
it('Releases potions when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-05-20'));
const mayPotions = hatchingPotions.premium;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const junePotions = hatchingPotions.premium;
expect(junePotions.Koi).to.exist;
expect(Object.keys(mayPotions).length).to.equal(Object.keys(junePotions).length - 1);
});
});
+154
View File
@@ -0,0 +1,154 @@
import content from '../../website/common/script/content';
describe('content index', () => {
let clock;
afterEach(() => {
if (clock) {
clock.restore();
}
});
it('Releases eggs when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const mayEggs = content.eggs;
expect(mayEggs.Chameleon).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-20'));
const juneEggs = content.eggs;
expect(juneEggs.Chameleon).to.exist;
expect(Object.keys(mayEggs).length, '').to.equal(Object.keys(juneEggs).length - 1);
});
it('Releases hatching potions when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-05-20'));
const mayHatchingPotions = content.hatchingPotions;
expect(mayHatchingPotions.Koi).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const juneHatchingPotions = content.hatchingPotions;
expect(juneHatchingPotions.Koi).to.exist;
expect(Object.keys(mayHatchingPotions).length, '').to.equal(Object.keys(juneHatchingPotions).length - 1);
});
it('Releases armoire gear when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const juneGear = content.gear.flat;
expect(juneGear.armor_armoire_corsairsCoatAndCape).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-10'));
const julyGear = content.gear.flat;
expect(julyGear.armor_armoire_corsairsCoatAndCape).to.exist;
expect(Object.keys(juneGear).length, '').to.equal(Object.keys(julyGear).length - 3);
});
it('Releases pets gear when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const junePets = content.petInfo;
expect(junePets['Chameleon-Base']).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-10'));
const julyPets = content.petInfo;
expect(julyPets['Chameleon-Base']).to.exist;
expect(Object.keys(junePets).length, '').to.equal(Object.keys(julyPets).length - 10);
});
it('Releases mounts gear when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const juneMounts = content.mountInfo;
expect(juneMounts['Chameleon-Base']).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-10'));
const julyMounts = content.mountInfo;
expect(julyMounts['Chameleon-Base']).to.exist;
expect(Object.keys(juneMounts).length, '').to.equal(Object.keys(julyMounts).length - 10);
});
it('marks regular food as buyable and droppable without any events', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const { food } = content;
Object.keys(food).forEach(key => {
if (key === 'Saddle') {
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
return;
}
let expected = true;
if (key.startsWith('Cake_')) {
expected = false;
} else if (key.startsWith('Candy_')) {
expected = false;
} else if (key.startsWith('Pie_')) {
expected = false;
}
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
});
});
it('marks candy as buyable and droppable during habitoween', () => {
clock = sinon.useFakeTimers(new Date('2024-10-31'));
const { food } = content;
Object.keys(food).forEach(key => {
if (key === 'Saddle') {
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
return;
}
let expected = false;
if (key.startsWith('Cake_')) {
expected = false;
} else if (key.startsWith('Candy_')) {
expected = true;
} else if (key.startsWith('Pie_')) {
expected = false;
}
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
});
});
it('marks cake as buyable and droppable during birthday', () => {
clock = sinon.useFakeTimers(new Date('2024-01-31'));
const { food } = content;
Object.keys(food).forEach(key => {
if (key === 'Saddle') {
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
return;
}
let expected = false;
if (key.startsWith('Cake_')) {
expected = true;
} else if (key.startsWith('Candy_')) {
expected = false;
} else if (key.startsWith('Pie_')) {
expected = false;
}
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
});
});
it('marks pie as buyable and droppable during pi day', () => {
clock = sinon.useFakeTimers(new Date('2024-03-14'));
const { food } = content;
Object.keys(food).forEach(key => {
if (key === 'Saddle') {
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
return;
}
let expected = false;
if (key.startsWith('Cake_')) {
expected = false;
} else if (key.startsWith('Candy_')) {
expected = false;
} else if (key.startsWith('Pie_')) {
expected = true;
}
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
});
});
});
+82
View File
@@ -0,0 +1,82 @@
import find from 'lodash/find';
import maxBy from 'lodash/maxBy';
import {
ARMOIRE_RELEASE_DATES,
EGGS_RELEASE_DATES,
HATCHING_POTIONS_RELEASE_DATES,
} from '../../website/common/script/content/constants/releaseDates';
import armoire from '../../website/common/script/content/gear/sets/armoire';
import eggs from '../../website/common/script/content/eggs';
import hatchingPotions from '../../website/common/script/content/hatching-potions';
describe('releaseDates', () => {
let clock;
afterEach(() => {
if (clock) {
clock.restore();
}
});
describe('armoire', () => {
it('should only contain valid armoire names', () => {
const lastReleaseDate = maxBy(Object.values(ARMOIRE_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-20`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-20`));
Object.keys(ARMOIRE_RELEASE_DATES).forEach(key => {
expect(find(armoire.all, { set: key }), `${key} is not a valid armoire set`).to.exist;
});
});
it('should contain a valid year and month', () => {
Object.keys(ARMOIRE_RELEASE_DATES).forEach(key => {
const date = ARMOIRE_RELEASE_DATES[key];
expect(date.year, `${key} year is not a valid year`).to.be.a('number');
expect(date.year).to.be.at.least(2023);
expect(date.month, `${key} month is not a valid month`).to.be.a('number');
expect(date.month).to.be.within(1, 12);
expect(date.day).to.not.exist;
});
});
});
describe('eggs', () => {
it('should only contain valid egg names', () => {
const lastReleaseDate = maxBy(Object.values(EGGS_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-${value.day}`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.day}`));
Object.keys(EGGS_RELEASE_DATES).forEach(key => {
expect(eggs.all[key], `${key} is not a valid egg name`).to.exist;
});
});
it('should contain a valid year, month and date', () => {
Object.keys(EGGS_RELEASE_DATES).forEach(key => {
const date = EGGS_RELEASE_DATES[key];
expect(date.year, `${key} year is not a valid year`).to.be.a('number');
expect(date.year).to.be.at.least(2024);
expect(date.month, `${key} month is not a valid month`).to.be.a('number');
expect(date.month).to.be.within(1, 12);
expect(date.day, `${key} day is not a valid day`).to.be.a('number');
});
});
});
describe('hatchingPotions', () => {
it('should only contain valid potion names', () => {
const lastReleaseDate = maxBy(Object.values(HATCHING_POTIONS_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-${value.day}`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.day}`));
Object.keys(HATCHING_POTIONS_RELEASE_DATES).forEach(key => {
expect(hatchingPotions.all[key], `${key} is not a valid potion name`).to.exist;
});
});
it('should contain a valid year, month and date', () => {
Object.keys(HATCHING_POTIONS_RELEASE_DATES).forEach(key => {
const date = HATCHING_POTIONS_RELEASE_DATES[key];
expect(date.year, `${key} year is not a valid year`).to.be.a('number');
expect(date.year).to.be.at.least(2024);
expect(date.month, `${key} month is not a valid month`).to.be.a('number');
expect(date.month).to.be.within(1, 12);
expect(date.day, `${key} day is not a valid day`).to.be.a('number');
});
});
});
});
+271
View File
@@ -0,0 +1,271 @@
// eslint-disable-next-line max-len
import moment from 'moment';
import nconf from 'nconf';
import {
getAllScheduleMatchingGroups, clearCachedMatchers, MONTHLY_SCHEDULE, GALA_SCHEDULE,
} from '../../website/common/script/content/constants/schedule';
import QUEST_PETS from '../../website/common/script/content/quests/pets';
import QUEST_HATCHINGPOTIONS from '../../website/common/script/content/quests/potions';
import QUEST_BUNDLES from '../../website/common/script/content/bundles';
import potions from '../../website/common/script/content/hatching-potions';
import SPELLS from '../../website/common/script/content/spells';
import QUEST_SEASONAL from '../../website/common/script/content/quests/seasonal';
function validateMatcher (matcher, checkedDate) {
expect(matcher.end).to.be.a('date');
expect(matcher.end).to.be.greaterThan(checkedDate);
}
describe('Content Schedule', () => {
let switchoverTime;
beforeEach(() => {
switchoverTime = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0;
clearCachedMatchers();
});
it('assembles scheduled items on january 15th', () => {
const date = new Date('2024-01-15');
const matchers = getAllScheduleMatchingGroups(date);
for (const key in matchers) {
if (matchers[key]) {
validateMatcher(matchers[key], date);
}
}
});
it('assembles scheduled items on january 31th', () => {
const date = new Date('2024-01-31');
const matchers = getAllScheduleMatchingGroups(date);
for (const key in matchers) {
if (matchers[key]) {
validateMatcher(matchers[key], date);
}
}
});
it('assembles scheduled items on march 2nd', () => {
const date = new Date('2024-03-02');
const matchers = getAllScheduleMatchingGroups(date);
for (const key in matchers) {
if (matchers[key]) {
validateMatcher(matchers[key], date);
}
}
});
it('assembles scheduled items on march 22st', () => {
const date = new Date('2024-03-22');
const matchers = getAllScheduleMatchingGroups(date);
for (const key in matchers) {
if (matchers[key]) {
validateMatcher(matchers[key], date);
}
}
});
it('assembles scheduled items on october 7th', () => {
const date = new Date('2024-10-07');
const matchers = getAllScheduleMatchingGroups(date);
for (const key in matchers) {
if (matchers[key]) {
validateMatcher(matchers[key], date);
}
}
});
it('assembles scheduled items on november 1th', () => {
const date = new Date('2024-11-01');
const matchers = getAllScheduleMatchingGroups(date);
for (const key in matchers) {
if (matchers[key]) {
validateMatcher(matchers[key], date);
}
}
});
it('assembles scheduled items on december 20th', () => {
const date = new Date('2024-12-20');
const matchers = getAllScheduleMatchingGroups(date);
for (const key in matchers) {
if (matchers[key]) {
validateMatcher(matchers[key], date);
}
}
});
it('sets the end date if its in the same month', () => {
const date = new Date('2024-04-03');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-04-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date if its in the next day', () => {
const date = new Date('2024-05-06T14:00:00.000Z');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-05-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date if its on the release day', () => {
const date = new Date('2024-05-07T07:00:00.000Z');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-06-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date if its next month', () => {
const date = new Date('2024-05-20T01:00:00.000Z');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-06-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date for a gala', () => {
const date = new Date('2024-05-20');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('contains content for repeating events', () => {
const date = new Date('2024-04-15');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.premiumHatchingPotions).to.exist;
expect(matchers.premiumHatchingPotions.items.length).to.equal(4);
expect(matchers.premiumHatchingPotions.items.indexOf('Garden')).to.not.equal(-1);
expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1);
});
describe('only contains valid keys for', () => {
it('pet quests', () => {
const petKeys = Object.keys(QUEST_PETS);
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
const petQuests = MONTHLY_SCHEDULE[key][14].find(item => item.type === 'petQuests');
for (const petQuest of petQuests.items) {
expect(petQuest).to.be.a('string');
expect(petKeys).to.include(petQuest);
}
});
});
it('hatchingpotion quests', () => {
const potionKeys = Object.keys(QUEST_HATCHINGPOTIONS);
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
const potionQuests = MONTHLY_SCHEDULE[key][14].find(item => item.type === 'hatchingPotionQuests');
for (const potionQuest of potionQuests.items) {
expect(potionQuest).to.be.a('string');
expect(potionKeys).to.include(potionQuest);
}
});
});
it('bundles', () => {
const bundleKeys = Object.keys(QUEST_BUNDLES);
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
const bundles = MONTHLY_SCHEDULE[key][14].find(item => item.type === 'bundles');
for (const bundle of bundles.items) {
expect(bundle).to.be.a('string');
expect(bundleKeys).to.include(bundle);
}
});
});
it('premium hatching potions', () => {
const potionKeys = Object.keys(potions.premium);
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
const monthlyPotions = MONTHLY_SCHEDULE[key][21].find(item => item.type === 'premiumHatchingPotions');
for (const potion of monthlyPotions.items) {
expect(potion).to.be.a('string');
expect(potionKeys).to.include(potion);
}
});
});
it('seasonal quests', () => {
const questKeys = Object.keys(QUEST_SEASONAL);
Object.keys(GALA_SCHEDULE).forEach(key => {
const quests = GALA_SCHEDULE[key].matchers.find(item => item.type === 'seasonalQuests');
for (const quest of quests.items) {
expect(quest).to.be.a('string');
expect(questKeys).to.include(quest);
}
});
});
it('seasonal spells', () => {
const spellKeys = Object.keys(SPELLS.special);
Object.keys(GALA_SCHEDULE).forEach(key => {
const petQuests = GALA_SCHEDULE[key].matchers.find(item => item.type === 'seasonalSpells');
for (const petQuest of petQuests.items) {
expect(petQuest).to.be.a('string');
expect(spellKeys).to.include(petQuest);
}
});
});
});
describe('backgrounds matcher', () => {
it('allows background matching the month for new backgrounds', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey072024')).to.be.true;
});
it('disallows background in the future', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey072025')).to.be.false;
});
it('disallows background for the inverse month for new backgrounds', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey012024')).to.be.false;
});
it('allows background for the inverse month for old backgrounds', () => {
const date = new Date('2024-08-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey022023')).to.be.true;
expect(matcher.match('backgroundkey022021')).to.be.true;
});
it('allows background even yeared backgrounds in first half of year', () => {
const date = new Date('2025-02-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey022024')).to.be.true;
expect(matcher.match('backgroundkey082022')).to.be.true;
});
it('allows background odd yeared backgrounds in second half of year', () => {
const date = new Date('2024-08-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('backgroundkey022023')).to.be.true;
expect(matcher.match('backgroundkey082021')).to.be.true;
});
});
describe('timeTravelers matcher', () => {
it('allows sets matching the month', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202307')).to.be.true;
expect(matcher.match('202207')).to.be.true;
});
it('disallows sets not matching the month', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202306')).to.be.false;
expect(matcher.match('202402')).to.be.false;
});
it('disallows sets from current month', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202407')).to.be.false;
});
it('disallows sets from the future', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('202507')).to.be.false;
});
});
});
+52
View File
@@ -0,0 +1,52 @@
import Sinon from 'sinon';
import featuredItems from '../../website/common/script/content/shop-featuredItems';
describe('Shop Featured Items', () => {
let clock;
afterEach(() => {
if (clock !== undefined) {
clock.restore();
clock = undefined;
}
});
describe('Market', () => {
it('contains armoire', () => {
const items = featuredItems.market();
expect(_.find(items, item => item.path === 'armoire')).to.exist;
});
it('contains the current premium hatching potions', () => {
clock = Sinon.useFakeTimers(new Date('2024-04-08'));
const items = featuredItems.market();
expect(_.find(items, item => item.path === 'premiumHatchingPotions.Porcelain')).to.exist;
});
it('is featuring 4 items', () => {
clock = Sinon.useFakeTimers(new Date('2024-02-08'));
const items = featuredItems.market();
expect(items.length).to.eql(4);
});
});
describe('Quest Shop', () => {
it('contains bundle', () => {
clock = Sinon.useFakeTimers(new Date('2024-03-08'));
const items = featuredItems.quests();
expect(_.find(items, item => item.path === 'quests.pinkMarble')).to.exist;
});
it('contains pet quests', () => {
clock = Sinon.useFakeTimers(new Date('2024-04-08'));
const items = featuredItems.quests();
expect(_.find(items, item => item.path === 'quests.frog')).to.exist;
});
it('is featuring 3 items', () => {
clock = Sinon.useFakeTimers(new Date('2024-02-08'));
const items = featuredItems.quests();
expect(items.length).to.eql(3);
});
});
});
+63
View File
@@ -0,0 +1,63 @@
import {
generateUser,
} from '../helpers/common.helper';
import spells from '../../website/common/script/content/spells';
import {
expectValidTranslationString,
} from '../helpers/content.helper';
import { TRANSFORMATION_DEBUFFS_LIST } from '../../website/common/script/constants';
// TODO complete the test suite...
describe('shared.ops.spells', () => {
let user;
let target;
beforeEach(() => {
user = generateUser();
target = generateUser();
});
it('all spells have required properties', () => {
for (const category of Object.values(spells)) {
for (const spell of Object.values(category)) {
expectValidTranslationString(spell.text, spell.key);
expectValidTranslationString(spell.notes);
expect(spell.target, spell.key).to.be.oneOf(['self', 'party', 'task', 'tasks', 'user']);
}
}
});
it('all special spells have a working cast method', async () => {
for (const s of Object.values(spells.special)) {
user.items.special[s.key] = 1;
s.cast(user, target, { language: 'en' });
}
});
it('all debuff spells cost 5 gold', () => {
for (const s of Object.values(spells.special)) {
if (s.purchaseType === 'debuffPotion') {
user.stats.gp = 5;
s.cast(user);
expect(user.stats.gp).to.equal(0);
}
}
});
it('all debuff spells remove the buff', () => {
const debuffMapping = {};
Object.keys(TRANSFORMATION_DEBUFFS_LIST).forEach(key => {
debuffMapping[TRANSFORMATION_DEBUFFS_LIST[key]] = key;
});
for (const s of Object.values(spells.special)) {
if (s.purchaseType === 'debuffPotion') {
user.stats.gp = 5;
user.stats.buffs[debuffMapping[s.key]] = true;
expect(user.stats.buffs[debuffMapping[s.key]]).to.equal(true);
s.cast(user);
expect(user.stats.buffs[debuffMapping[s.key]]).to.equal(false);
}
}
});
});
+12 -3
View File
@@ -6,12 +6,21 @@ import {
} from '../helpers/content.helper';
import t from '../../website/common/script/content/translation';
import * as stable from '../../website/common/script/content/stable';
import * as eggs from '../../website/common/script/content/eggs';
import * as potions from '../../website/common/script/content/hatching-potions';
import stable from '../../website/common/script/content/stable';
import eggs from '../../website/common/script/content/eggs';
import potions from '../../website/common/script/content/hatching-potions';
describe('stable', () => {
describe('dropPets', () => {
let clock;
beforeEach(() => {
clock = sinon.useFakeTimers(new Date('2020-05-20'));
});
afterEach(() => {
clock.restore();
});
it('contains a pet for each drop potion * each drop egg', () => {
const numberOfDropPotions = Object.keys(potions.drops).length;
const numberOfDropEggs = Object.keys(eggs.drops).length;
+94 -12
View File
@@ -6,23 +6,105 @@ import timeTravelers from '../../website/common/script/content/time-travelers';
describe('time-travelers store', () => {
let user;
let date;
beforeEach(() => {
user = generateUser();
});
it('removes owned sets from the time travelers store', () => {
user.items.gear.owned.head_mystery_201602 = true; // eslint-disable-line camelcase
expect(timeTravelers.timeTravelerStore(user)['201602']).to.not.exist;
expect(timeTravelers.timeTravelerStore(user)['201603']).to.exist;
describe('on january 15th', () => {
beforeEach(() => {
date = new Date('2024-01-15');
});
it('returns the correct gear', () => {
const items = timeTravelers.timeTravelerStore(user, date);
for (const [key] of Object.entries(items)) {
if (key.startsWith('20')) {
expect(key).to.match(/20[0-9]{2}(01|07)/);
}
}
});
it('removes owned sets from the time travelers store', () => {
user.items.gear.owned.head_mystery_201601 = true; // eslint-disable-line camelcase
const items = timeTravelers.timeTravelerStore(user, date);
expect(items['201601']).to.not.exist;
expect(items['201801']).to.exist;
expect(items['202207']).to.exist;
});
it('removes unopened mystery item sets from the time travelers store', () => {
user.purchased = {
plan: {
mysteryItems: ['head_mystery_201601'],
},
};
const items = timeTravelers.timeTravelerStore(user, date);
expect(items['201601']).to.not.exist;
expect(items['201607']).to.exist;
});
});
it('removes unopened mystery item sets from the time travelers store', () => {
user.purchased = {
plan: {
mysteryItems: ['head_mystery_201602'],
},
};
expect(timeTravelers.timeTravelerStore(user)['201602']).to.not.exist;
expect(timeTravelers.timeTravelerStore(user)['201603']).to.exist;
describe('on may 1st', () => {
beforeEach(() => {
date = new Date('2024-05-01');
});
it('returns the correct gear', () => {
const items = timeTravelers.timeTravelerStore(user, date);
for (const [key] of Object.entries(items)) {
if (key.startsWith('20')) {
expect(key).to.match(/20[0-9]{2}(05|11)/);
}
}
});
it('removes owned sets from the time travelers store', () => {
user.items.gear.owned.head_mystery_201705 = true; // eslint-disable-line camelcase
const items = timeTravelers.timeTravelerStore(user, date);
expect(items['201705']).to.not.exist;
expect(items['201805']).to.exist;
expect(items['202211']).to.exist;
});
it('removes unopened mystery item sets from the time travelers store', () => {
user.purchased = {
plan: {
mysteryItems: ['head_mystery_201705'],
},
};
const items = timeTravelers.timeTravelerStore(user, date);
expect(items['201705']).to.not.exist;
expect(items['201611']).to.exist;
});
});
describe('on october 21st', () => {
beforeEach(() => {
date = new Date('2024-10-21');
});
it('returns the correct gear', () => {
const items = timeTravelers.timeTravelerStore(user, date);
for (const [key] of Object.entries(items)) {
if (key.startsWith('20')) {
expect(key).to.match(/20[0-9]{2}(10|04)/);
}
}
});
it('removes owned sets from the time travelers store', () => {
user.items.gear.owned.head_mystery_201810 = true; // eslint-disable-line camelcase
user.items.gear.owned.armor_mystery_201810 = true; // eslint-disable-line camelcase
const items = timeTravelers.timeTravelerStore(user, date);
expect(items['201810']).to.not.exist;
expect(items['201910']).to.exist;
expect(items['202204']).to.exist;
});
it('removes unopened mystery item sets from the time travelers store', () => {
user.purchased = {
plan: {
mysteryItems: ['armor_mystery_201710'],
},
};
const items = timeTravelers.timeTravelerStore(user, date);
expect(items['201710']).to.not.exist;
expect(items['201604']).to.exist;
});
});
});
+11
View File
@@ -40,6 +40,7 @@ export function generateRes (options = {}) {
redirect: sandbox.stub(),
render: sandbox.stub(),
send: sandbox.stub(),
sendFile: sandbox.stub(),
sendStatus: sandbox.stub().returnsThis(),
set: sandbox.stub(),
status: sandbox.stub().returnsThis(),
@@ -59,7 +60,17 @@ export function generateReq (options = {}) {
header (header) {
return this.headers[header];
},
listeners: {},
session: {},
on (key, func) {
if (!this.listeners[key]) {
this.listeners[key] = [];
}
this.listeners[key].push(func);
},
end () {
this.listeners.close.forEach(func => func());
},
};
const req = defaultsDeep(options, defaultReq);
+6 -6
View File
@@ -7,13 +7,13 @@ i18n.translations = translations;
export const STRING_ERROR_MSG = /^Error processing the string ".*". Please see Help > Report a Bug.$/;
export const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
export function expectValidTranslationString (attribute) {
expect(attribute).to.be.a('function');
export function expectValidTranslationString (attribute, contextKey) {
expect(attribute, contextKey).to.be.a('function');
const translatedString = attribute();
expect(translatedString.trim()).to.not.be.empty;
expect(translatedString).to.not.contain('function func(lang)');
expect(translatedString).to.not.eql(STRING_ERROR_MSG);
expect(translatedString).to.not.match(STRING_DOES_NOT_EXIST_MSG);
expect(translatedString.trim(), contextKey).to.not.be.empty;
expect(translatedString, contextKey).to.not.contain('function func(lang)');
expect(translatedString, contextKey).to.not.eql(STRING_ERROR_MSG);
expect(translatedString, contextKey).to.not.match(STRING_DOES_NOT_EXIST_MSG);
}
+363 -66
View File
@@ -16,12 +16,12 @@
"@vue/cli-service": "^5.0.8",
"@vue/test-utils": "1.0.0-beta.29",
"amplitude-js": "^8.21.3",
"assert": "^2.1.0",
"axios": "^0.28.0",
"axios-progress-bar": "^1.2.0",
"babel-eslint": "^10.1.0",
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.23.1",
"chai": "^5.1.0",
"core-js": "^3.33.1",
"dompurify": "^3.0.3",
"eslint": "7.32.0",
@@ -30,16 +30,18 @@
"eslint-plugin-vue": "7.20.0",
"habitica-markdown": "^3.0.0",
"hellojs": "^1.20.0",
"inspectpack": "^4.7.1",
"intro.js": "^7.2.0",
"jquery": "^3.7.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"moment-locales-webpack-plugin": "^1.2.0",
"nconf": "^0.12.1",
"sass": "^1.63.4",
"sass-loader": "^14.1.1",
"sinon": "^17.0.1",
"smartbanner.js": "^1.19.3",
"stopword": "^2.0.8",
"timers-browserify": "^2.0.12",
"uuid": "^9.0.1",
"validator": "^13.9.0",
"vue": "^2.7.10",
@@ -49,11 +51,15 @@
"vue-template-babel-compiler": "^2.0.0",
"vue-template-compiler": "^2.7.10",
"vuedraggable": "^2.24.3",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
"webpack": "^5.89.0"
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.21.0"
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"babel-plugin-lodash": "^3.3.4",
"chai": "^5.1.0",
"inspectpack": "^4.7.1",
"terser-webpack-plugin": "^5.3.10",
"webpack": "^5.89.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -2120,6 +2126,45 @@
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
},
"node_modules/@sinonjs/commons": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
"integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/@sinonjs/fake-timers": {
"version": "11.2.2",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
"integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
"dependencies": {
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@sinonjs/samsam": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz",
"integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==",
"dependencies": {
"@sinonjs/commons": "^2.0.0",
"lodash.get": "^4.4.2",
"type-detect": "^4.0.8"
}
},
"node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/@sinonjs/text-encoding": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
"integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ=="
},
"node_modules/@soda/friendly-errors-webpack-plugin": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz",
@@ -3601,10 +3646,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/assert": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
"integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
"dependencies": {
"call-bind": "^1.0.2",
"is-nan": "^1.3.2",
"object-is": "^1.1.5",
"object.assign": "^4.1.4",
"util": "^0.12.5"
}
},
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
"engines": {
"node": ">=12"
}
@@ -3683,9 +3741,9 @@
}
},
"node_modules/axios": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.28.0.tgz",
"integrity": "sha512-Tu7NYoGY4Yoc7I+Npf9HhUMtEEpV7ZiLH9yndTCoNhcpBH0kwcvFbzYN9/u5QKI5A6uefjsNNWaz5olJVYS62Q==",
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.28.1.tgz",
"integrity": "sha512-iUcGA5a7p0mVb4Gm/sy+FSECNkPFT4y7wt6OM/CDpO/OnNCvSs3PoMG8ibrC9jRoGYU0gUK5pXVC4NPXq6lHRQ==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@@ -3759,6 +3817,19 @@
"object.assign": "^4.1.0"
}
},
"node_modules/babel-plugin-lodash": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/babel-plugin-lodash/-/babel-plugin-lodash-3.3.4.tgz",
"integrity": "sha512-yDZLjK7TCkWl1gpBeBGmuaDIFhZKmkoL+Cu2MUUjv5VxUZx/z7tBGBCBcQs5RI1Bkz5LLmNdjx7paOyQtMovyg==",
"dev": true,
"dependencies": {
"@babel/helper-module-imports": "^7.0.0-beta.49",
"@babel/types": "^7.0.0-beta.49",
"glob": "^7.1.1",
"lodash": "^4.17.10",
"require-package-name": "^2.0.1"
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz",
@@ -4063,13 +4134,18 @@
}
},
"node_modules/call-bind": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.1",
"set-function-length": "^1.1.1"
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -4142,12 +4218,13 @@
}
},
"node_modules/chai": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.0.tgz",
"integrity": "sha512-kDZ7MZyM6Q1DhR9jy7dalKohXQ2yrlXkk59CR52aRKxJrobmlBNqnFQxX9xOX8w+4mz8SYlKJa/7D7ddltFXCw==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
"integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
"dev": true,
"dependencies": {
"assertion-error": "^2.0.1",
"check-error": "^2.0.0",
"check-error": "^2.1.1",
"deep-eql": "^5.0.1",
"loupe": "^3.1.0",
"pathval": "^2.0.0"
@@ -4170,9 +4247,10 @@
}
},
"node_modules/check-error": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.0.0.tgz",
"integrity": "sha512-tjLAOBHKVxtPoHe/SA7kNOMvhCRdCJ3vETdeY0RuAc9popf+hyaSV6ZEg9hr4cpWF7jmo/JSWEnLDrnijS9Tog==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
"dev": true,
"engines": {
"node": ">= 16"
}
@@ -5046,6 +5124,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.1.tgz",
"integrity": "sha512-nwQCf6ne2gez3o1MxWifqkciwt0zhl0LO1/UwVu4uMBuPmflWM4oQ70XMqHqnBJA+nhzncaqL9HVL6KkHJ28lw==",
"dev": true,
"engines": {
"node": ">=6"
}
@@ -5141,16 +5220,19 @@
}
},
"node_modules/define-data-property": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dependencies": {
"get-intrinsic": "^1.2.1",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.0"
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/define-lazy-prop": {
@@ -5521,6 +5603,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
@@ -6929,7 +7030,8 @@
"node_modules/fp-ts": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz",
"integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA=="
"integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==",
"dev": true
},
"node_modules/fraction.js": {
"version": "4.3.7",
@@ -6975,19 +7077,6 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -7046,20 +7135,25 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -7253,11 +7347,11 @@
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dependencies": {
"get-intrinsic": "^1.2.2"
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -7660,6 +7754,7 @@
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/inspectpack/-/inspectpack-4.7.1.tgz",
"integrity": "sha512-XoDJbKSM9I2KA+8+OLFJHm8m4NM2pMEgsDD2hze6swVfynEed9ngCx36mRR+otzOsskwnxIZWXjI23FTW1uHqA==",
"dev": true,
"dependencies": {
"chalk": "^4.1.0",
"fp-ts": "^2.6.1",
@@ -7680,6 +7775,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -7694,6 +7790,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -7709,6 +7806,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -7719,12 +7817,14 @@
"node_modules/inspectpack/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/inspectpack/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -7733,6 +7833,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -7770,6 +7871,7 @@
"version": "2.2.21",
"resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.21.tgz",
"integrity": "sha512-zz2Z69v9ZIC3mMLYWIeoUcwWD6f+O7yP92FMVVaXEOSZH1jnVBmET/urd/uoarD1WGBY4rCj8TAyMPzsGNzMFQ==",
"dev": true,
"peerDependencies": {
"fp-ts": "^2.5.0"
}
@@ -7778,6 +7880,7 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/io-ts-reporters/-/io-ts-reporters-1.2.2.tgz",
"integrity": "sha512-igASwWWkDY757OutNcM6zTtdJf/eTZYkoe2ymsX2qpm5bKZLo74FJYjsCtMQOEdY7dRHLLEulCyFQwdN69GBCg==",
"dev": true,
"peerDependencies": {
"fp-ts": "^2.0.2",
"io-ts": "^2.0.0"
@@ -7791,6 +7894,21 @@
"node": ">= 10"
}
},
"node_modules/is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
"dependencies": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
@@ -7931,6 +8049,20 @@
"node": ">=8"
}
},
"node_modules/is-generator-function": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
"integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
"dependencies": {
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -7950,6 +8082,21 @@
"node": ">=8"
}
},
"node_modules/is-nan": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
"integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
"dependencies": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-negative-zero": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
@@ -8335,6 +8482,11 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/just-extend": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz",
"integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw=="
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -8470,6 +8622,16 @@
"resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz",
"integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA=="
},
"node_modules/lodash.difference": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="
},
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
},
"node_modules/lodash.kebabcase": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
@@ -8682,9 +8844,10 @@
}
},
"node_modules/loupe": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.0.tgz",
"integrity": "sha512-qKl+FrLXUhFuHUoDJG7f8P8gEMHq9NFS0c6ghXG1J0rldmZFQZoNVv/vyirE9qwCIhWZDsvEFd1sbFu3GvRQFg==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
"integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
"dev": true,
"dependencies": {
"get-func-name": "^2.0.1"
}
@@ -9447,6 +9610,18 @@
"node": "*"
}
},
"node_modules/moment-locales-webpack-plugin": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/moment-locales-webpack-plugin/-/moment-locales-webpack-plugin-1.2.0.tgz",
"integrity": "sha512-QAi5v0OlPUP7GXviKMtxnpBAo8WmTHrUNN7iciAhNOEAd9evCOvuN0g1N7ThIg3q11GLCkjY1zQ2saRcf/43nQ==",
"dependencies": {
"lodash.difference": "^4.5.0"
},
"peerDependencies": {
"moment": "^2.8.0",
"webpack": "^1 || ^2 || ^3 || ^4 || ^5"
}
},
"node_modules/mrmime": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
@@ -9530,6 +9705,23 @@
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
},
"node_modules/nise": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz",
"integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==",
"dependencies": {
"@sinonjs/commons": "^3.0.0",
"@sinonjs/fake-timers": "^11.2.2",
"@sinonjs/text-encoding": "^0.7.2",
"just-extend": "^6.2.0",
"path-to-regexp": "^6.2.1"
}
},
"node_modules/nise/node_modules/path-to-regexp": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
},
"node_modules/no-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
@@ -9693,6 +9885,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-is": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
@@ -10135,6 +10342,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
"dev": true,
"engines": {
"node": ">= 14.16"
}
@@ -10159,6 +10367,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
"integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==",
"dev": true,
"engines": {
"node": ">=10"
},
@@ -11275,6 +11484,12 @@
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
},
"node_modules/require-package-name": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz",
"integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==",
"dev": true
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -11434,9 +11649,9 @@
}
},
"node_modules/sass-loader": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz",
"integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==",
"version": "14.2.1",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.2.1.tgz",
"integrity": "sha512-G0VcnMYU18a4N7VoNDegg2OuMjYtxnqzQWARVWCIVSZwJeiL9kg8QMsuIZOplsJgTzZLF6jGxI3AClj8I9nRdQ==",
"dependencies": {
"neo-async": "^2.6.2"
},
@@ -11533,7 +11748,8 @@
"node_modules/semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"dev": true
},
"node_modules/send": {
"version": "0.18.0",
@@ -11674,15 +11890,16 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
"node_modules/set-function-length": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dependencies": {
"define-data-property": "^1.1.1",
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.1"
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -11701,6 +11918,11 @@
"node": ">= 0.4"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -11762,6 +11984,50 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"node_modules/sinon": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
"integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
"dependencies": {
"@sinonjs/commons": "^3.0.0",
"@sinonjs/fake-timers": "^11.2.2",
"@sinonjs/samsam": "^8.0.0",
"diff": "^5.1.0",
"nise": "^5.1.5",
"supports-color": "^7.2.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/sinon"
}
},
"node_modules/sinon/node_modules/diff": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/sinon/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/sinon/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/sirv": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
@@ -12261,15 +12527,15 @@
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.9",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz",
"integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==",
"version": "5.3.10",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
"integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.17",
"@jridgewell/trace-mapping": "^0.3.20",
"jest-worker": "^27.4.5",
"schema-utils": "^3.1.1",
"serialize-javascript": "^6.0.1",
"terser": "^5.16.8"
"terser": "^5.26.0"
},
"engines": {
"node": ">= 10.13.0"
@@ -12404,6 +12670,17 @@
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="
},
"node_modules/timers-browserify": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
"integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==",
"dependencies": {
"setimmediate": "^1.0.4"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -12515,6 +12792,14 @@
"node": ">= 0.8.0"
}
},
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"engines": {
"node": ">=4"
}
},
"node_modules/type-fest": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
@@ -12718,6 +13003,18 @@
"requires-port": "^1.0.0"
}
},
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
"dependencies": {
"inherits": "^2.0.3",
"is-arguments": "^1.0.4",
"is-generator-function": "^1.0.7",
"is-typed-array": "^1.1.3",
"which-typed-array": "^1.1.2"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+11 -5
View File
@@ -18,12 +18,12 @@
"@vue/cli-service": "^5.0.8",
"@vue/test-utils": "1.0.0-beta.29",
"amplitude-js": "^8.21.3",
"assert": "^2.1.0",
"axios": "^0.28.0",
"axios-progress-bar": "^1.2.0",
"babel-eslint": "^10.1.0",
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.23.1",
"chai": "^5.1.0",
"core-js": "^3.33.1",
"dompurify": "^3.0.3",
"eslint": "7.32.0",
@@ -32,16 +32,18 @@
"eslint-plugin-vue": "7.20.0",
"habitica-markdown": "^3.0.0",
"hellojs": "^1.20.0",
"inspectpack": "^4.7.1",
"intro.js": "^7.2.0",
"jquery": "^3.7.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"moment-locales-webpack-plugin": "^1.2.0",
"nconf": "^0.12.1",
"sass": "^1.63.4",
"sass-loader": "^14.1.1",
"sinon": "^17.0.1",
"smartbanner.js": "^1.19.3",
"stopword": "^2.0.8",
"timers-browserify": "^2.0.12",
"uuid": "^9.0.1",
"validator": "^13.9.0",
"vue": "^2.7.10",
@@ -51,10 +53,14 @@
"vue-template-babel-compiler": "^2.0.0",
"vue-template-compiler": "^2.7.10",
"vuedraggable": "^2.24.3",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
"webpack": "^5.89.0"
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.21.0"
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"babel-plugin-lodash": "^3.3.4",
"chai": "^5.1.0",
"inspectpack": "^4.7.1",
"terser-webpack-plugin": "^5.3.10",
"webpack": "^5.89.0"
}
}
+9 -2
View File
@@ -27,8 +27,15 @@
</head>
<body>
<div id="loading-screen">
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.62 28.86c-.32-.706-1.057-1.048-1.538-.706-.48.341-1.147.393-1.78.24-.633-.153-.753-1.604-.616-3.278.136-1.673.363-2.318.506-2.925.162-.61.877-.562.962-.084.086.479.582.479 1.307-.391.724-.87.617-3.409-.218-5.474-.836-2.065.326-1.865.664-1.66.337.205.544-.102.462-1.28-.082-1.178-1.166-2.098-2.039-2.663-.873-.564-1.936-1.186-1.911-2.697.025-1.511 2.08-1.464 2.358-1.439.279.025.815-.093.506-1.663-.31-1.57-1.43-1.869-2.133-1.826-.703.042-1.177.428-2.17.053-.995-.376-1.655-.23-2.58-.023-.926.206-2.138.776-3.646 1.183-.795.219-1.064.274-1.93.288-.532.008-.755.653-.043 1.444.563.643 1.839.814 2.606.707.494-.07.608.258.563.74a8.013 8.013 0 0 0-.01 1.795c.08.6.18 1.62-.103 2.286-.14.326-.545.677-.98.653-.565-.034-1.022-.7-1.414-1.49-.825-1.662-1.793-2.014-5.404-3.535-3.248-1.367-5.007-3.5-6.096-4.874-.969-1.217-1.939-.756-1.85.342.07.852.592 3.604 1.912 5.257 1.623 2.525 4.128 3.67 7.013 3.895.755.06 1.226.208 1.29.553.095.735-.622 1.244-1.959 1.09-1.336-.157-1.907.087-1.641.848.85 1.79 2.809 1.869 3.623 1.942.275.05 1.246 0 1.764.143.605.166.735 1.005-.14 1.459-1.558.76-2.237 1.391-3.025 2.83-.595 1.13-1.108 3.022-.574 5.745.513 2.648-3.337 2.733-5 2.357-.716-.151-1.47-1.512.287-2.65 1.421-.922 1.708-1.49 1.645-2.657-.074-1.36-.824-1.458-.822-2.64v-2.82a.435.435 0 0 0-.435-.435H7.698a.435.435 0 0 1-.435-.434v-1.7a.435.435 0 0 0-.435-.435H5.501a.435.435 0 0 1-.435-.435v-1.524a.435.435 0 0 0-.435-.435H3.015a.435.435 0 0 1-.435-.435v-1.603a.435.435 0 0 0-.435-.434H.435a.435.435 0 0 0-.435.434v1.705c0 .24.195.435.435.435h1.62c.24 0 .435.195.435.435v6.076c0 .241.195.435.435.435h1.71c.241 0 .436.196.436.435v1.988c0 .24.195.434.435.434h2.402c.734-.052.862.934.854 1.286-.016.803-.923 1.06-1.352 1.395-1.145.884-2.031 1.783-1.513 3.512l.013.036c.945 2.007 3.542 1.8 5.183 1.8h10.326c.584 0 1.184.135 1.046-.545-.136-.68-.425-1.61-1.265-1.61-.84 0-.703.467-1.524.228-.821-.238-.822-1.348.411-3.279 1.276-1.649 3.46-1.524 4.781-.358 1.32 1.166.93 3.191.653 4.354-.158.82.218 1.224.669 1.213h5.242c.806-.014.647-.556.185-1.614h.003z" fill="#fff"/>
<svg width="80" height="80" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg">
<path
xmlns="http://www.w3.org/2000/svg"
fill-rule="evenodd"
clip-rule="evenodd"
d="M79.05 72.15c-.8-1.766-2.643-2.62-3.845-1.766-1.201.855-2.867.985-4.448.602-1.584-.385-1.885-4.01-1.543-8.195.342-4.184.909-5.795 1.267-7.314.404-1.524 2.191-1.404 2.405-.209.215 1.196 1.454 1.196 3.266-.979 1.811-2.175 1.543-8.52-.546-13.684-2.088-5.163.817-4.661 1.66-4.149.844.513 1.362-.255 1.156-3.2-.204-2.945-2.916-5.247-5.096-6.657-2.184-1.41-4.842-2.967-4.78-6.745.063-3.777 5.2-3.658 5.897-3.596.697.063 2.037-.233 1.264-4.157-.773-3.924-3.575-4.673-5.332-4.567-1.758.106-2.943 1.071-5.427.133-2.484-.938-4.136-.572-6.45-.057-2.313.515-5.343 1.94-9.112 2.959-1.989.545-2.661.683-4.828.718-1.33.02-1.885 1.633-.106 3.61 1.408 1.608 4.597 2.036 6.515 1.768 1.236-.174 1.521.645 1.407 1.85a20.023 20.023 0 0 0-.024 4.488c.198 1.5.45 4.051-.258 5.713-.35.817-1.361 1.693-2.449 1.633-1.413-.084-2.555-1.75-3.537-3.726-2.06-4.152-4.48-5.033-13.509-8.835-8.12-3.417-12.516-8.749-15.24-12.185-2.421-3.042-4.846-1.89-4.626.855.179 2.128 1.48 9.008 4.781 13.141 4.058 6.314 10.32 9.177 17.534 9.739 1.885.149 3.065.52 3.225 1.383.236 1.835-1.557 3.11-4.898 2.722-3.341-.39-4.768.22-4.103 2.121 2.123 4.477 7.021 4.672 9.058 4.857.686.122 3.114 0 4.41.355 1.51.418 1.836 2.514-.353 3.648-3.892 1.903-5.59 3.479-7.561 7.075-1.486 2.826-2.77 7.555-1.435 14.365 1.283 6.62-8.342 6.83-12.497 5.89-1.793-.377-3.675-3.778.716-6.625 3.553-2.305 4.269-3.724 4.111-6.642-.184-3.4-2.058-3.644-2.053-6.598v-7.05c0-.602-.488-1.088-1.087-1.088h-3.334a1.087 1.087 0 0 1-1.087-1.087v-4.25c0-.602-.488-1.087-1.088-1.087h-3.317a1.087 1.087 0 0 1-1.087-1.088v-3.81c0-.602-.489-1.087-1.088-1.087h-4.04a1.087 1.087 0 0 1-1.089-1.088V26.25c0-.602-.488-1.088-1.087-1.088H1.088C.485 25.161 0 25.65 0 26.25v4.26c0 .602.488 1.087 1.088 1.087h4.049c.602 0 1.087.489 1.087 1.088v15.192c0 .602.489 1.087 1.088 1.087h4.277c.602 0 1.088.489 1.088 1.088v4.968c0 .602.488 1.087 1.087 1.087h6.005c1.836-.13 2.156 2.335 2.137 3.214-.04 2.007-2.308 2.652-3.382 3.487-2.861 2.21-5.077 4.459-3.78 8.781l.032.09c2.362 5.017 8.855 4.499 12.956 4.499h25.817c1.459 0 2.959.339 2.614-1.362-.342-1.7-1.063-4.024-3.162-4.024-2.1 0-1.758 1.166-3.81.57-2.054-.597-2.057-3.371 1.027-8.198 3.19-4.122 8.652-3.81 11.952-.895 3.301 2.915 2.325 7.978 1.633 10.885-.396 2.048.545 3.06 1.67 3.032H78.58c2.015-.035 1.62-1.391.464-4.035h.008z"
fill="#fff"
>
</path>
</svg>
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because it is too large Load Diff
+2 -5
View File
@@ -217,16 +217,13 @@
.btn-show-more {
display: block;
width: 50%;
max-width: 448px;
margin: 0 auto;
margin-top: 12px;
width: 100%;
padding: 8px;
font-size: 14px;
line-height: 1.43;
font-weight: bold;
text-align: center;
background: $gray-600;
background: $gray-500;
color: $gray-200 !important; // Otherwise it gets ignored when used on an A element
box-shadow: none;
+1 -1
View File
@@ -12,7 +12,7 @@
}
&.color {
svg path {
svg path, svg polygon {
fill: currentColor;
}
}
+25 -30
View File
@@ -1,5 +1,11 @@
// TODO move to item component?
.item, .item-wrapper, .item > div > div {
&:focus-visible {
outline: none;
}
}
.items > div {
display: inline-block;
margin-right: 24px;
@@ -9,34 +15,22 @@
position: relative;
display: inline-block;
margin-bottom: 12px;
&:focus {
outline: 2px solid $purple-400;
border-radius: 2px;
}
&:hover {
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
border-radius: 4px;
}
}
.items-one-line .item-wrapper {
margin-bottom: 8px;
}
.item.pet-slot {
// Desktop XL (1440)
@media only screen and (min-width: 1440px){
margin-right: 1.71em;
}
// Desktop L (1280)
@media only screen and (min-width: 1280px) and (max-width: 1439px) {
margin-right: 0.43em;
}
// Desktop M (1024)
@media only screen and (min-width: 1024px) and (max-width: 1279px) {
margin-right: 0.86em;
}
// Tablets and mobile
@media only screen and (max-width: 1023px) {
margin-right: 1.71em;
}
}
.item {
position: relative;
width: 94px;
@@ -56,11 +50,6 @@
background: $purple-500;
}
&:hover {
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
border-color: $purple-400;
}
&.highlight {
box-shadow: 0 0 8px 8px rgba($black, 0.16), 0 5px 10px 0 rgba($black, 0.12) !important;
}
@@ -70,9 +59,15 @@
}
}
.flat .item {
box-shadow: none;
border: none;
.flat {
.item {
box-shadow: none;
border: none;
}
.item-wrapper:hover {
box-shadow: none;
}
}
.bordered-item .item {
+90
View File
@@ -0,0 +1,90 @@
.featured-label {
margin: 24px auto;
}
.group {
display: inline-block;
width: 33%;
margin-bottom: 24px;
.items {
border-radius: 2px;
background-color: #edecee;
display: inline-block;
padding: 8px;
}
.item-wrapper {
margin-bottom: 0;
}
.items > div:not(:last-of-type) {
margin-right: 16px;
}
}
.timeTravelers {
.standard-page {
position: relative;
}
.badge-pin:not(.pinned) {
display: none;
}
.item:hover .badge-pin {
display: block;
}
.avatar {
cursor: default;
margin: 0 auto;
}
.featuredItems {
height: 192px;
.background {
background-repeat: repeat-x;
width: 100%;
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.background-open, .background-closed {
height: 216px;
}
.content {
display: flex;
flex-direction: column;
}
.npc {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 216px;
background-repeat: no-repeat;
&.closed {
background-repeat: no-repeat;
}
.featured-label {
position: absolute;
bottom: -14px;
margin: 0;
left: 79px;
}
}
}
}
@@ -158,7 +158,6 @@ function collateItemData (self) {
if (
// ignore items the user owns because we captured them above:
!(key in ownedItems)
&& allItems[key].price > 0
) {
const item = allItems[key];
itemData.push({
@@ -282,20 +282,16 @@ export default {
item.modified = true;
// for non-integer items, toggle through the allowed values:
if (item.itemType === 'gear') {
// Allowed starting values are true, false, and '' (never owned)
// Allowed values to switch to are true and false
item.value = !item.value;
} else if (item.itemType === 'mounts') {
// Allowed starting values are true, null, and "never owned"
// Allowed values to switch to are true and null
if (item.value === true) {
item.value = null;
if (item.itemType === 'gear' || item.itemType === 'mounts') {
// Allowed starting values are true, false, and undefined (never owned)
if (item.value && item.value !== '') {
item.value = false;
} else if (typeof item.value === 'boolean') {
item.value = '';
} else {
item.value = true;
}
}
// @TODO add a delete option
},
},
};
@@ -28,15 +28,15 @@
<div class="form-group">
<label>About</label>
<div class="row about-row">
<textarea
v-model="hero.profile.blurb"
class="form-control col"
rows="10"
></textarea>
<div
v-markdown="hero.profile.blurb"
class="markdownPreview col"
></div>
<textarea
v-model="hero.profile.blurb"
class="form-control col"
rows="10"
></textarea>
<div
v-markdown="hero.profile.blurb"
class="markdownPreview col"
></div>
</div>
</div>
<input
+76 -4
View File
@@ -291,7 +291,44 @@
</div>
<div
v-if="!IS_PRODUCTION && isUserLoaded"
v-if="TIME_TRAVEL_ENABLED && user.permissions && user.permissions.fullAccess"
:key="lastTimeJump"
>
<a
class="btn btn-secondary mr-1"
@click="jumpTime(-1)"
>-1 Day</a>
<a
class="btn btn-secondary mr-1"
@click="jumpTime(-7)"
>-7 Days</a>
<a
class="btn btn-secondary mr-1"
@click="jumpTime(-30)"
>-30 Days</a>
<div class="my-2">
Time Traveling! It is {{ new Date().toLocaleDateString() }}
<a
class="btn btn-warning mr-1"
@click="resetTime()"
>Reset</a>
</div>
<a
class="btn btn-secondary mr-1"
@click="jumpTime(1)"
>+1 Day</a>
<a
class="btn btn-secondary mr-1"
@click="jumpTime(7)"
>+7 Days</a>
<a
class="btn btn-secondary mr-1"
@click="jumpTime(30)"
>+30 Days</a>
</div>
<div
v-if="DEBUG_ENABLED && isUserLoaded"
class="debug-toggle"
>
<button
@@ -772,6 +809,7 @@ h3 {
// modules
import axios from 'axios';
import moment from 'moment';
import Vue from 'vue';
// images
import melior from '@/assets/svg/melior.svg';
@@ -785,13 +823,24 @@ import heart from '@/assets/svg/heart.svg';
import { mapState } from '@/libs/store';
import buyGemsModal from './payments/buyGemsModal.vue';
import reportBug from '@/mixins/reportBug.js';
import { worldStateMixin } from '@/mixins/worldState';
const DEBUG_ENABLED = process.env.DEBUG_ENABLED === 'true'; // eslint-disable-line no-process-env
const TIME_TRAVEL_ENABLED = process.env.TIME_TRAVEL_ENABLED === 'true'; // eslint-disable-line no-process-env
let sinon;
if (TIME_TRAVEL_ENABLED) {
// eslint-disable-next-line global-require
sinon = await import('sinon');
}
const IS_PRODUCTION = process.env.NODE_ENV === 'production'; // eslint-disable-line no-process-env
export default {
components: {
buyGemsModal,
},
mixins: [reportBug],
mixins: [
reportBug,
worldStateMixin,
],
data () {
return {
icons: Object.freeze({
@@ -803,7 +852,9 @@ export default {
heart,
}),
debugMenuShown: false,
IS_PRODUCTION,
DEBUG_ENABLED,
TIME_TRAVEL_ENABLED,
lastTimeJump: null,
};
},
computed: {
@@ -865,6 +916,27 @@ export default {
'stats.mp': this.user.stats.mp + 10000,
});
},
async jumpTime (amount) {
const response = await axios.post('/api/v4/debug/jump-time', { offsetDays: amount });
if (amount > 0) {
Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000);
} else {
Vue.config.clock.setSystemTime(moment().add(amount, 'days').toDate());
}
this.lastTimeJump = response.data.data.time;
this.triggerGetWorldState(true);
},
async resetTime () {
const response = await axios.post('/api/v4/debug/jump-time', { reset: true });
const time = new Date(response.data.data.time);
Vue.config.clock.restore();
Vue.config.clock = sinon.useFakeTimers({
now: time,
shouldAdvanceTime: true,
});
this.lastTimeJump = response.data.data.time;
this.triggerGetWorldState(true);
},
addExp () {
// @TODO: Name these variables better
let exp = 0;
+23 -4
View File
@@ -35,7 +35,7 @@
<span :class="[skinClass, specialMountClass]"></span>
<!-- eslint-disable max-len-->
<span
:class="[member.preferences.size + '_shirt_' + member.preferences.shirt, specialMountClass]"
:class="[shirtClass, specialMountClass]"
></span>
<!-- eslint-enable max-len-->
<span :class="['head_0', specialMountClass]"></span>
@@ -46,12 +46,10 @@
<template
v-for="type in ['bangs', 'base', 'mustache', 'beard']"
>
<!-- eslint-disable max-len-->
<span
:key="type"
:class="['hair_' + type + '_' + member.preferences.hair[type] + '_' + member.preferences.hair.color, specialMountClass]"
:class="[hairClass(type), specialMountClass]"
></span>
<!-- eslint-enable max-len-->
</template>
<span :class="[getGearClass('body'), specialMountClass]"></span>
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
@@ -233,10 +231,20 @@ export default {
},
skinClass () {
if (!this.member) return '';
if (this.overrideAvatarGear?.skin) {
return `skin_${this.overrideAvatarGear.skin}`;
}
const baseClass = `skin_${this.member.preferences.skin}`;
return `${baseClass}${this.member.preferences.sleep ? '_sleep' : ''}`;
},
shirtClass () {
if (!this.member) return '';
if (this.overrideAvatarGear?.shirt) {
return `${this.member.preferences.size}_shirt_${this.overrideAvatarGear.shirt}`;
}
return `${this.member.preferences.size}_shirt_${this.member.preferences.shirt}`;
},
costumeClass () {
return this.member?.preferences.costume ? 'costume' : 'equipped';
},
@@ -269,6 +277,17 @@ export default {
return result;
},
hairClass (slot) {
if (this.overrideAvatarGear?.hair) {
if (this.overrideAvatarGear.hair[slot]) {
return `hair_${slot}_${this.overrideAvatarGear.hair[slot]}_${this.member.preferences.hair.color}`;
}
if (this.overrideAvatarGear.hair.color) {
return `hair_${slot}_${this.member.preferences.hair[slot]}_${this.overrideAvatarGear.hair.color}`;
}
}
return `hair_${slot}_${this.member.preferences.hair[slot]}_${this.member.preferences.hair.color}`;
},
hideGear (gearType) {
if (!this.member) return true;
if (gearType === 'weapon') {
@@ -1,7 +1,8 @@
<template>
<div
id="body"
class="section customize-section"
class="customize-section d-flex flex-column"
:class="{ 'justify-content-between': editing }"
>
<sub-menu
class="text-center"
@@ -17,17 +18,11 @@
</div>
<div v-if="activeSubPage === 'shirt'">
<customize-options
:items="freeShirts"
:items="userShirts"
:current-value="user.preferences.shirt"
/>
<customize-options
v-if="editing"
:items="specialShirts"
:current-value="user.preferences.shirt"
:full-set="!userOwnsSet('shirt', specialShirtKeys)"
@unlock="unlock(`shirt.${specialShirtKeys.join(',shirt.')}`)"
/>
</div>
<customize-banner v-if="editing" />
</div>
</template>
@@ -35,33 +30,27 @@
import appearance from '@/../../common/script/content/appearance';
import { subPageMixin } from '../../mixins/subPage';
import { userStateMixin } from '../../mixins/userState';
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
import subMenu from './sub-menu';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
import customizeBanner from './customize-banner.vue';
import customizeOptions from './customize-options';
import gem from '@/assets/svg/gem.svg';
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
import subMenu from './sub-menu';
export default {
components: {
subMenu,
customizeBanner,
customizeOptions,
},
mixins: [
subPageMixin,
userStateMixin,
avatarEditorUtilies,
avatarEditorUtilities,
],
props: [
'editing',
],
data () {
return {
specialShirtKeys,
icons: Object.freeze({
gem,
}),
items: [
{
id: 'size',
@@ -78,25 +67,19 @@ export default {
sizes () {
return ['slim', 'broad'].map(s => this.mapKeysToFreeOption(s, 'size'));
},
freeShirts () {
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
},
specialShirts () {
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.specialShirtKeys;
const options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
return options;
userShirts () {
const freeShirts = Object.keys(appearance.shirt)
.filter(k => appearance.shirt[k].price === 0)
.map(s => this.mapKeysToFreeOption(s, 'shirt'));
const ownedShirts = Object.keys(this.user.purchased.shirt)
.filter(k => this.user.purchased.shirt[k])
.map(s => this.mapKeysToFreeOption(s, 'shirt'));
return [...freeShirts, ...ownedShirts];
},
},
mounted () {
this.changeSubPage('size');
},
methods: {
},
};
</script>
<style scoped>
</style>
@@ -0,0 +1,69 @@
<template>
<div class="bottom-banner">
<div class="d-flex justify-content-center align-items-center mt-3">
<span
class="svg svg-icon sparkles"
v-html="icons.sparkles"
></span>
<strong
v-once
class="mx-2"
> {{ $t('lookingForMore') }}
</strong>
<span
v-once
class="svg svg-icon sparkles mirror"
v-html="icons.sparkles"
></span>
</div>
<div
class="check-link"
>
<span>Check out the </span>
<a href="/shops/customizations">Customizations Shop</a>
<span> for even more ways to customize your avatar!</span>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.bottom-banner {
background: linear-gradient(114.26deg, $purple-300 0%, $purple-200 100%);
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
color: $white;
height: 80px;
line-height: 24px;
.check-link, a {
color: $purple-600;
}
a {
text-decoration: underline;
}
}
.sparkles {
width: 32px;
&.mirror {
transform: scaleX(-1);
}
}
</style>
<script>
import sparkles from '@/assets/svg/sparkles-left.svg';
export default {
data () {
return {
icons: Object.freeze({
sparkles,
}),
};
},
};
</script>
@@ -1,14 +1,13 @@
<template>
<div
class="customize-options"
:class="{'background-set': fullSet}"
v-if="items.length > 1"
class="customize-options mb-4"
>
<div
v-for="option in items"
:key="option.key"
class="outer-option-background"
:class="{
locked: option.gemLocked || option.goldLocked,
premium: Boolean(option.gem),
active: option.active || currentValue === option.key,
none: option.none,
@@ -28,38 +27,6 @@
</div>
</div>
</div>
<div
v-if="option.gemLocked"
class="gem-lock"
>
<div
class="svg-icon gem"
v-html="icons.gem"
></div>
<span>{{ option.gem }}</span>
</div>
<div
v-if="option.goldLocked"
class="gold-lock"
>
<div
class="svg-icon gold"
v-html="icons.gold"
></div>
<span>{{ option.gold }}</span>
</div>
</div>
<div
v-if="fullSet"
class="purchase-set"
@click="unlock()"
>
<span class="label">{{ $t('purchaseAll') }}</span>
<div
class="svg-icon gem"
v-html="icons.gem"
></div>
<span class="price">5</span>
</div>
</div>
</template>
@@ -67,13 +34,13 @@
<script>
import gem from '@/assets/svg/gem.svg';
import gold from '@/assets/svg/gold.svg';
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
export default {
mixins: [
avatarEditorUtilies,
avatarEditorUtilities,
],
props: ['items', 'currentValue', 'fullSet'],
props: ['items', 'currentValue'],
data () {
return {
icons: Object.freeze({
@@ -150,7 +117,7 @@ export default {
&:not(.locked):not(.active) {
.option:hover {
background-color: rgba(213, 200, 255, .32);
background-color: rgba($purple-300, .25);
}
}
@@ -216,9 +183,6 @@ export default {
margin-top: 0;
margin-left: 0;
&.color-bangs {
margin-top: 3px;
}
&.skin {
margin-top: -4px;
margin-left: -4px;
@@ -237,14 +201,14 @@ export default {
margin-top: -5px;
}
}
&.color, &.bangs {
margin-top: 4px;
margin-left: -3px;
&.color, &.bangs, &.beard, &.flower, &.mustache {
background-position-x: -6px;
background-position-y: -12px;
}
&.hair.base {
margin-top: 0px;
margin-left: -5px;
background-position-x: -6px;
background-position-y: -4px;
}
&.headAccessory {
@@ -258,89 +222,4 @@ export default {
}
}
}
.text-center {
.gem-lock, .gold-lock {
display: inline-block;
margin: 0 auto 8px;
vertical-align: bottom;
}
}
.gem-lock, .gold-lock {
.svg-icon {
width: 16px;
}
span {
font-weight: bold;
margin-left: .5em;
}
.svg-icon, span {
display: inline-block;
vertical-align: bottom;
}
}
.gem-lock span {
color: $green-10
}
.purchase-set {
background: #fff;
padding: 0.5em;
border-radius: 0 0 2px 2px;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
cursor: pointer;
span {
font-weight: bold;
font-size: 12px;
}
span.price {
color: #24cc8f;
}
.gem, .coin {
width: 16px;
}
&.single {
width: 141px;
}
width: 100%;
span {
font-size: 14px;
}
.gem, .coin {
width: 20px;
margin: 0 .5em;
display: inline-block;
vertical-align: bottom;
}
}
.background-set {
background-color: #edecee;
border-radius: 2px;
padding-top: 12px;
margin-left: 12px;
margin-right: 12px;
margin-bottom: 12px;
width: calc(100% - 24px);
padding-left: 0;
padding-right: 0;
max-width: unset; // disable col12 styling
flex: unset;
}
</style>
@@ -1,7 +1,8 @@
<template>
<div
id="extra"
class="section container customize-section"
class="customize-section d-flex flex-column"
:class="{ 'justify-content-between': !showEmptySection}"
>
<sub-menu
class="text-center"
@@ -20,9 +21,8 @@
id="animal-ears"
>
<customize-options
v-if="animalItems('back').length > 0"
:items="animalItems('headAccessory')"
:full-set="!animalItemsOwned('headAccessory')"
@unlock="unlock(animalItemsUnlockString('headAccessory'))"
/>
</div>
<div
@@ -30,9 +30,8 @@
id="animal-tails"
>
<customize-options
v-if="animalItems('back').length > 0"
:items="animalItems('back')"
:full-set="!animalItemsOwned('back')"
@unlock="unlock(animalItemsUnlockString('back'))"
/>
</div>
<div
@@ -53,6 +52,24 @@
>
<customize-options :items="flowers" />
</div>
<div
v-if="showEmptySection"
class="my-5"
>
<h3
v-once
>
{{ $t('noItemsOwned') }}
</h3>
<p
v-once
class="w-50 mx-auto"
v-html="$t('visitCustomizationsShop')"
></p>
</div>
<customize-banner
v-else-if="editing"
/>
</div>
</template>
@@ -60,23 +77,24 @@
import appearance from '@/../../common/script/content/appearance';
import { subPageMixin } from '../../mixins/subPage';
import { userStateMixin } from '../../mixins/userState';
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
import subMenu from './sub-menu';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
import customizeBanner from './customize-banner';
import customizeOptions from './customize-options';
import gem from '@/assets/svg/gem.svg';
import subMenu from './sub-menu';
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
export default {
components: {
subMenu,
customizeBanner,
customizeOptions,
subMenu,
},
mixins: [
subPageMixin,
userStateMixin,
avatarEditorUtilies,
avatarEditorUtilities,
],
props: [
'editing',
@@ -89,9 +107,6 @@ export default {
},
chairKeys: ['none', 'black', 'blue', 'green', 'pink', 'red', 'yellow', 'handleless_black', 'handleless_blue', 'handleless_green', 'handleless_pink', 'handleless_red', 'handleless_yellow'],
specialShirtKeys,
icons: Object.freeze({
gem,
}),
items: [
{
id: 'size',
@@ -178,7 +193,7 @@ export default {
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
},
specialShirts () {
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.specialShirtKeys;
const options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
return options;
@@ -193,6 +208,11 @@ export default {
for (const key of keys) {
const option = this.createGearItem(key, 'headAccessory', 'special', 'headband');
const newKey = `headAccessory_special_${key}`;
option.click = () => {
const type = this.user.preferences.costume ? 'costume' : 'equipped';
return this.equip(newKey, type);
};
options.push(option);
}
@@ -222,12 +242,22 @@ export default {
option.none = true;
}
option.active = this.user.preferences.hair.flower === key;
option.class = `hair_flower_${key} flower`;
option.class = `icon_hair_flower_${key} flower`;
option.click = () => this.set({ 'preferences.hair.flower': key });
return option;
});
return options;
},
showEmptySection () {
switch (this.activeSubPage) {
case 'ears':
return this.editing && this.animalItems('headAccessory').length === 1;
case 'tails':
return this.editing && this.animalItems('back').length === 1;
default:
return false;
}
},
},
mounted () {
this.changeSubPage(this.extraSubMenuItems[0].id);
@@ -236,7 +266,7 @@ export default {
animalItems (category) {
// @TODO: For some resonse when I use $set on the
// user purchases object, this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.animalItemKeys[category];
const noneOption = this.createGearItem(0, category, 'base', category);
@@ -248,36 +278,22 @@ export default {
for (const key of keys) {
const newKey = `${category}_special_${key}`;
const userPurchased = this.user.items.gear.owned[newKey];
const option = {};
option.key = key;
option.active = this.user.preferences.costume
? this.user.items.gear.costume[category] === newKey
: this.user.items.gear.equipped[category] === newKey;
option.class = `headAccessory_special_${option.key} ${category}`;
if (category === 'back') {
option.class = `icon_back_special_${option.key} back`;
}
option.gemLocked = userPurchased === undefined;
option.goldLocked = userPurchased === false;
if (option.goldLocked) {
option.gold = 20;
}
if (option.gemLocked) {
option.gem = 2;
}
option.locked = option.gemLocked || option.goldLocked;
option.click = () => {
if (option.gemLocked) {
return this.unlock(`items.gear.owned.${newKey}`);
} if (option.goldLocked) {
return this.buy(newKey);
if (userPurchased) {
const option = {};
option.key = key;
option.active = this.user.preferences.costume
? this.user.items.gear.costume[category] === newKey
: this.user.items.gear.equipped[category] === newKey;
option.class = `headAccessory_special_${option.key} ${category}`;
if (category === 'back') {
option.class = `icon_back_special_${option.key} back`;
}
const type = this.user.preferences.costume ? 'costume' : 'equipped';
return this.equip(newKey, type);
};
options.push(option);
option.click = () => {
const type = this.user.preferences.costume ? 'costume' : 'equipped';
return this.equip(newKey, type);
};
options.push(option);
}
}
return options;
@@ -287,17 +303,6 @@ export default {
return keys.join(',');
},
animalItemsOwned (category) {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
let own = true;
this.animalItemKeys[category].forEach(key => {
if (this.user.items.gear.owned[`${category}_special_${key}`] === undefined) own = false;
});
return own;
},
createGearItem (key, gearType, subGearType, additionalClass) {
const newKey = `${gearType}_${subGearType ? `${subGearType}_` : ''}${key}`;
const option = {};
@@ -339,7 +344,3 @@ export default {
},
};
</script>
<style scoped>
</style>
@@ -1,7 +1,8 @@
<template>
<div
id="hair"
class="section customize-section"
class="customize-section d-flex flex-column"
:class="{ 'justify-content-between': editing && !showEmptySection}"
>
<sub-menu
class="text-center"
@@ -14,37 +15,9 @@
id="hair-color"
>
<customize-options
:items="freeHairColors"
:items="userHairColors"
:current-value="user.preferences.hair.color"
/>
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
<div
v-for="set in seasonalHairColors"
v-if="editing && set.key !== 'undefined'"
:key="set.key"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<customize-options
:items="set.options"
:current-value="user.preferences.hair.color"
:full-set="!hideSet(set.key) && !userOwnsSet('hair', set.keys, 'color')"
@unlock="unlock(`hair.color.${set.keys.join(',hair.color.')}`)"
/>
</div>
</div>
<div
v-if="activeSubPage === 'style'"
id="style"
>
<!-- eslint-disable vue/require-v-for-key NO KEY AVAILABLE HERE -->
<div v-for="set in styleSets">
<customize-options
:items="set.options"
:full-set="set.fullSet"
@unlock="set.unlock()"
/>
</div>
<!-- eslint-enable vue/require-v-for-key -->
</div>
<div
v-if="activeSubPage === 'bangs'"
@@ -55,67 +28,73 @@
:current-value="user.preferences.hair.bangs"
/>
</div>
<div
v-if="activeSubPage === 'style'"
id="style"
>
<customize-options
:items="userHairStyles"
:current-value="user.preferences.hair.base"
/>
</div>
<div
v-if="activeSubPage === 'facialhair'"
id="facialhair"
>
<customize-options
v-if="editing"
:items="mustacheList"
v-if="userMustaches.length > 1"
:items="userMustaches"
/>
<!-- eslint-disable max-len -->
<customize-options
v-if="editing"
:items="beardList"
:full-set="isPurchaseAllNeeded('hair', ['baseHair5', 'baseHair6'], ['mustache', 'beard'])"
@unlock="unlock(`hair.mustache.${baseHair5Keys.join(',hair.mustache.')},hair.beard.${baseHair6Keys.join(',hair.beard.')}`)"
v-if="userBeards.length > 1"
:items="userBeards"
/>
<!-- eslint-enable max-len -->
<div
v-if="showEmptySection"
class="my-5"
>
<h3
v-once
>
{{ $t('noItemsOwned') }}
</h3>
<p
v-once
class="w-50 mx-auto"
v-html="$t('visitCustomizationsShop')"
></p>
</div>
</div>
<customize-banner
v-if="editing && !showEmptySection"
/>
</div>
</template>
<script>
import groupBy from 'lodash/groupBy';
import appearance from '@/../../common/script/content/appearance';
import appearanceSets from '@/../../common/script/content/appearance/sets';
import { subPageMixin } from '../../mixins/subPage';
import { userStateMixin } from '../../mixins/userState';
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
import subMenu from './sub-menu';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
import customizeBanner from './customize-banner';
import customizeOptions from './customize-options';
import gem from '@/assets/svg/gem.svg';
const hairColorBySet = groupBy(appearance.hair.color, 'set.key');
const freeHairColorKeys = hairColorBySet[undefined].map(s => s.key);
import subMenu from './sub-menu';
export default {
components: {
subMenu,
customizeBanner,
customizeOptions,
subMenu,
},
mixins: [
subPageMixin,
userStateMixin,
avatarEditorUtilies,
avatarEditorUtilities,
],
props: [
'editing',
],
data () {
return {
freeHairColorKeys,
icons: Object.freeze({
gem,
}),
baseHair1: [1, 3],
baseHair2Keys: [2, 4, 5, 6, 7, 8],
baseHair3Keys: [9, 10, 11, 12, 13, 14],
baseHair4Keys: [15, 16, 17, 18, 19, 20],
baseHair5Keys: [1, 2],
baseHair6Keys: [1, 2, 3],
};
},
computed: {
hairSubMenuItems () {
const items = [
@@ -142,91 +121,46 @@ export default {
return items;
},
freeHairColors () {
return freeHairColorKeys.map(s => this.mapKeysToFreeOption(s, 'hair', 'color'));
userHairColors () {
const freeHairColors = groupBy(appearance.hair.color, 'set.key')[undefined]
.map(s => s.key).map(s => this.mapKeysToFreeOption(s, 'hair', 'color'));
const ownedHairColors = Object.keys(this.user.purchased.hair.color || {})
.filter(k => this.user.purchased.hair.color[k])
.map(h => this.mapKeysToFreeOption(h, 'hair', 'color'));
return [...freeHairColors, ...ownedHairColors];
},
seasonalHairColors () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
userHairStyles () {
const emptyHairStyle = {
...this.mapKeysToFreeOption(0, 'hair', 'base'),
none: true,
};
const freeHairStyles = [1, 3].map(s => this.mapKeysToFreeOption(s, 'hair', 'base'));
const ownedHairStyles = Object.keys(this.user.purchased.hair.base || {})
.filter(k => this.user.purchased.hair.base[k])
.map(h => this.mapKeysToFreeOption(h, 'hair', 'base'));
return [emptyHairStyle, ...freeHairStyles, ...ownedHairStyles];
},
userMustaches () {
const emptyMustache = {
...this.mapKeysToFreeOption(0, 'hair', 'mustache'),
none: true,
};
const ownedMustaches = Object.keys(this.user.purchased.hair.mustache || {})
.filter(k => this.user.purchased.hair.mustache[k])
.map(h => this.mapKeysToFreeOption(h, 'hair', 'mustache'));
const seasonalHairColors = [];
for (const key of Object.keys(hairColorBySet)) {
const set = hairColorBySet[key];
return [emptyMustache, ...ownedMustaches];
},
userBeards () {
const emptyBeard = {
...this.mapKeysToFreeOption(0, 'hair', 'beard'),
none: true,
};
const ownedBeards = Object.keys(this.user.purchased.hair.beard || {})
.filter(k => this.user.purchased.hair.beard[k])
.map(h => this.mapKeysToFreeOption(h, 'hair', 'beard'));
const keys = set.map(item => item.key);
const options = keys.map(optionKey => {
const option = this.mapKeysToOption(optionKey, 'hair', 'color', key);
return option;
});
let text = this.$t(key);
if (appearanceSets[key] && appearanceSets[key].text) {
text = appearanceSets[key].text();
}
const compiledSet = {
key,
options,
keys,
text,
};
seasonalHairColors.push(compiledSet);
}
return seasonalHairColors;
},
premiumHairColors () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.premiumHairColorKeys;
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'color'));
return options;
},
baseHair2 () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.baseHair2Keys;
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'base'));
return options;
},
baseHair3 () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.baseHair3Keys;
const options = keys.map(key => {
const option = this.mapKeysToOption(key, 'hair', 'base');
return option;
});
return options;
},
baseHair4 () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.baseHair4Keys;
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'base'));
return options;
},
baseHair5 () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.baseHair5Keys;
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'mustache'));
return options;
},
baseHair6 () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.baseHair6Keys;
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'beard'));
return options;
return [emptyBeard, ...ownedBeards];
},
hairBangs () {
const none = this.mapKeysToFreeOption(0, 'hair', 'bangs');
@@ -236,136 +170,13 @@ export default {
return [none, ...options];
},
mustacheList () {
const noneOption = this.mapKeysToFreeOption(0, 'hair', 'mustache');
noneOption.none = true;
return [noneOption, ...this.baseHair5];
},
beardList () {
const noneOption = this.mapKeysToFreeOption(0, 'hair', 'beard');
noneOption.none = true;
return [noneOption, ...this.baseHair6];
},
styleSets () {
const sets = [];
const emptyHairBase = {
...this.mapKeysToFreeOption(0, 'hair', 'base'),
none: true,
};
sets.push({
options: [
emptyHairBase,
...this.baseHair1.map(key => this.mapKeysToFreeOption(key, 'hair', 'base')),
],
});
if (this.editing) {
sets.push({
fullSet: !this.userOwnsSet('hair', this.baseHair3Keys, 'base'),
unlock: () => this.unlock(`hair.base.${this.baseHair3Keys.join(',hair.base.')}`),
options: [
...this.baseHair3,
],
});
sets.push({
fullSet: !this.userOwnsSet('hair', this.baseHair4Keys, 'base'),
unlock: () => this.unlock(`hair.base.${this.baseHair4Keys.join(',hair.base.')}`),
options: [
...this.baseHair4,
],
});
}
if (this.editing) {
sets.push({
fullSet: !this.userOwnsSet('hair', this.baseHair2Keys, 'base'),
unlock: () => this.unlock(`hair.base.${this.baseHair2Keys.join(',hair.base.')}`),
options: [
...this.baseHair2,
],
});
}
return sets;
showEmptySection () {
return this.activeSubPage === 'facialhair'
&& this.userMustaches.length === 1 && this.userBeards.length === 1;
},
},
mounted () {
this.changeSubPage('color');
},
methods: {
/**
* Allows you to find out whether you need the "Purchase All" button or not.
* If there are more than 2 unpurchased items, returns true, otherwise returns false.
* @param {string} category - The selected category.
* @param {string[]} keySets - The items keySets.
* @param {string[]} [types] - The items types (subcategories). Optional.
* @returns {boolean} - Determines whether the "Purchase All" button
* is needed (true) or not (false).
*/
isPurchaseAllNeeded (category, keySets, types) {
const purchasedItemsLengths = [];
// If item types are specified, count them
if (types && types.length > 0) {
// Types can be undefined, so we must check them.
types.forEach(type => {
if (this.user.purchased[category][type]) {
purchasedItemsLengths
.push(Object.keys(this.user.purchased[category][type]).length);
}
});
} else {
let purchasedItemsCounter = 0;
// If types are not specified, recursively
// search for purchased items in the category
const findPurchasedItems = item => {
if (typeof item === 'object') {
Object.values(item)
.forEach(innerItem => {
if (typeof innerItem === 'boolean' && innerItem === true) {
purchasedItemsCounter += 1;
}
return findPurchasedItems(innerItem);
});
}
return purchasedItemsCounter;
};
findPurchasedItems(this.user.purchased[category]);
if (purchasedItemsCounter > 0) {
purchasedItemsLengths.push(purchasedItemsCounter);
}
}
// We don't need to count the key sets (below)
// if there are no purchased items at all.
if (purchasedItemsLengths.length === 0) {
return true;
}
const allItemsLengths = [];
// Key sets must be specify correctly.
keySets.forEach(keySet => {
allItemsLengths.push(Object.keys(this[keySet]).length);
});
// Simply sum all the length values and
// write them into variables for the convenience.
const allItems = allItemsLengths.reduce((acc, val) => acc + val);
const purchasedItems = purchasedItemsLengths.reduce((acc, val) => acc + val);
const unpurchasedItems = allItems - purchasedItems;
return unpurchasedItems > 2;
},
},
};
</script>
<style scoped>
</style>
@@ -1,7 +1,8 @@
<template>
<div
id="skin"
class="section customize-section"
class="customize-section d-flex flex-column"
:class="{ 'justify-content-between': editing }"
>
<sub-menu
class="text-center"
@@ -10,63 +11,39 @@
@changeSubPage="changeSubPage($event)"
/>
<customize-options
:items="freeSkins"
:items="userSkins"
:current-value="user.preferences.skin"
/>
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
<div
v-for="set in seasonalSkins"
v-if="editing && set.key !== 'undefined'"
:key="set.key"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<customize-options
:items="set.options"
:current-value="user.preferences.skin"
:full-set="!hideSet(set.key) && !userOwnsSet('skin', set.keys)"
@unlock="unlock(`skin.${set.keys.join(',skin.')}`)"
/>
</div>
<customize-banner v-if="editing" />
</div>
</template>
<script>
import groupBy from 'lodash/groupBy';
import appearance from '@/../../common/script/content/appearance';
import appearanceSets from '@/../../common/script/content/appearance/sets';
import { subPageMixin } from '../../mixins/subPage';
import { userStateMixin } from '../../mixins/userState';
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
import subMenu from './sub-menu';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
import customizeBanner from './customize-banner.vue';
import customizeOptions from './customize-options';
import gem from '@/assets/svg/gem.svg';
const skinsBySet = groupBy(appearance.skin, 'set.key');
const freeSkinKeys = skinsBySet[undefined].map(s => s.key);
// const specialSkinKeys = Object.keys(appearance.shirt)
// .filter(k => appearance.shirt[k].price !== 0);
import subMenu from './sub-menu';
export default {
components: {
subMenu,
customizeBanner,
customizeOptions,
},
mixins: [
subPageMixin,
userStateMixin,
avatarEditorUtilies,
avatarEditorUtilities,
],
props: [
'editing',
],
data () {
return {
freeSkinKeys,
icons: Object.freeze({
gem,
}),
skinSubMenuItems: [
{
id: 'color',
@@ -76,41 +53,13 @@ export default {
};
},
computed: {
freeSkins () {
return freeSkinKeys.map(s => this.mapKeysToFreeOption(s, 'skin'));
},
seasonalSkins () {
// @TODO: For some resonse when I use $set on the user purchases object,
// this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const seasonalSkins = [];
for (const setKey of Object.keys(skinsBySet)) {
const set = skinsBySet[setKey];
const keys = set.map(item => item.key);
const options = keys.map(optionKey => {
const option = this.mapKeysToOption(optionKey, 'skin', '', setKey);
return option;
});
let text = this.$t(setKey);
if (appearanceSets[setKey] && appearanceSets[setKey].text) {
text = appearanceSets[setKey].text();
}
const compiledSet = {
key: setKey,
options,
keys,
text,
};
seasonalSkins.push(compiledSet);
}
return seasonalSkins;
userSkins () {
const freeSkins = groupBy(appearance.skin, 'set.key')[undefined]
.map(s => s.key).map(s => this.mapKeysToFreeOption(s, 'skin'));
const ownedSkins = Object.keys(this.user.purchased.skin)
.filter(k => this.user.purchased.skin[k])
.map(s => this.mapKeysToFreeOption(s, 'skin'));
return [...freeSkins, ...ownedSkins];
},
},
mounted () {
File diff suppressed because it is too large Load Diff
@@ -320,7 +320,7 @@
<script>
import each from 'lodash/each';
import * as quests from '@/../../common/script/content/quests';
import { mountInfo, petInfo } from '@/../../common/script/content/stable';
import stable from '@/../../common/script/content/stable';
import content from '@/../../common/script/content';
import gear from '@/../../common/script/content/gear';
import styleHelper from '@/mixins/styleHelper';
@@ -330,6 +330,8 @@ import userLink from '../userLink';
import PurchaseHistoryTable from '../ui/purchaseHistoryTable.vue';
import { userStateMixin } from '../../mixins/userState';
const { mountInfo, petInfo } = stable;
export default {
components: {
userLink,
+11 -2
View File
@@ -16,10 +16,13 @@
class="brand"
aria-label="Habitica"
>
<div
<router-link to="/">
<div
class="logo svg-icon svg color gryphon"
v-html="icons.melior"
></div>
></div>
<div class="svg-icon"></div>
</router-link>
<div class="svg-icon"></div>
</b-navbar-brand>
<b-navbar-toggle
@@ -134,6 +137,12 @@
>
{{ $t('quests') }}
</router-link>
<router-link
class="topbar-dropdown-item dropdown-item"
:to="{name: 'customizations'}"
>
{{ $t('customizations') }}
</router-link>
<router-link
class="topbar-dropdown-item dropdown-item"
:to="{name: 'seasonal'}"
@@ -35,13 +35,9 @@
/>
</a>
<a
class="topbar-dropdown-item dropdown-item"
class="topbar-dropdown-item dropdown-item dropdown-separated"
@click="showAvatar('body', 'size')"
>{{ $t('editAvatar') }}</a>
<a
class="topbar-dropdown-item dropdown-item dropdown-separated"
@click="showAvatar('backgrounds', '2024')"
>{{ $t('backgrounds') }}</a>
<a
class="topbar-dropdown-item dropdown-item"
@click="showProfile('profile')"
@@ -66,6 +66,7 @@
:right="true"
:hide-icon="false"
:inline-dropdown="false"
:direct-select="true"
@select="groupBy = $event"
>
<template #item="{ item }">
@@ -444,7 +444,7 @@ export default {
const isSearched = !searchText || item.text()
.toLowerCase()
.indexOf(searchText) !== -1;
if (isSearched) {
if (isSearched && item) {
itemsArray.push({
...item,
class: `${group.classPrefix}${item.key}`,
@@ -134,56 +134,57 @@
v-for="(petGroup) in petGroups"
v-if="!anyFilterSelected || viewOptions[petGroup.key].selected"
:key="petGroup.key"
:class="{ hide: viewOptions[petGroup.key].animalCount === 0 }"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<h4 v-if="viewOptions[petGroup.key].animalCount !== 0">
{{ petGroup.label }}
</h4>
<!-- eslint-disable vue/no-use-v-if-with-v-for, max-len -->
<div
v-for="(group, key, index) in pets(petGroup, hideMissing, selectedSortBy, searchTextThrottled)"
v-if="index === 0 || $_openedItemRows_isToggled(petGroup.key)"
:key="key"
class="pet-row d-flex"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<div class="d-inline-flex flex-column">
<div
v-for="item in group"
v-show="show('pet', item)"
:key="item.key"
v-drag.drop.food="item.key"
class="pet-group"
:class="{'last': item.isLastInRow}"
@itemDragOver="onDragOver($event, item)"
@itemDropped="onDrop($event, item)"
@itemDragLeave="onDragLeave()"
v-for="(group, key, index) in pets(petGroup, hideMissing, selectedSortBy, searchTextThrottled)"
v-if="index === 0 || $_openedItemRows_isToggled(petGroup.key)"
:key="key"
class="pet-row d-flex"
>
<petItem
:item="item"
:popover-position="'top'"
:show-popover="currentDraggingFood == null"
:highlight-border="highlightPet == item.key"
@click="petClicked(item)"
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<div
v-for="item in group"
v-show="show('pet', item)"
:key="item.key"
v-drag.drop.food="item.key"
class="pet-group"
@itemDragOver="onDragOver($event, item)"
@itemDropped="onDrop($event, item)"
@itemDragLeave="onDragLeave()"
>
<template
slot="itemBadge"
slot-scope="context"
<petItem
:item="item"
:popover-position="'top'"
:show-popover="currentDraggingFood == null"
:highlight-border="highlightPet == item.key"
@click="petClicked(item)"
>
<equip-badge
:equipped="context.item.key === currentPet"
:show="isOwned('pet', context.item)"
@click="selectPet(context.item)"
/>
</template>
</petItem>
<template
slot="itemBadge"
slot-scope="context"
>
<equip-badge
:equipped="context.item.key === currentPet"
:show="isOwned('pet', context.item)"
@click="selectPet(context.item)"
/>
</template>
</petItem>
</div>
</div>
<show-more-button
v-if="petRowCount[petGroup.key] > 1 && petGroup.key !== 'specialPets' && !(petGroup.key === 'wackyPets' && selectedSortBy !== 'sortByColor')"
:show-all="$_openedItemRows_isToggled(petGroup.key)"
@click="setShowMore(petGroup.key)"
/>
</div>
<show-more-button
v-if="petRowCount[petGroup.key] > 1 && petGroup.key !== 'specialPets' && !(petGroup.key === 'wackyPets' && selectedSortBy !== 'sortByColor')"
:show-all="$_openedItemRows_isToggled(petGroup.key)"
class="show-more-button"
@click="setShowMore(petGroup.key)"
/>
</div>
<h2>
{{ $t('mounts') }}
@@ -196,52 +197,55 @@
v-for="mountGroup in mountGroups"
v-if="!anyFilterSelected || viewOptions[mountGroup.key].selected"
:key="mountGroup.key"
:class="{ hide: viewOptions[mountGroup.key].animalCount === 0 }"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<h4 v-if="viewOptions[mountGroup.key].animalCount != 0">
{{ mountGroup.label }}
</h4>
<!-- eslint-disable vue/no-use-v-if-with-v-for, max-len -->
<div
v-for="(group, key, index) in mounts(mountGroup, hideMissing, selectedSortBy, searchTextThrottled)"
v-if="index === 0 || $_openedItemRows_isToggled(mountGroup.key)"
:key="key"
class="pet-row d-flex"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<div class="d-inline-flex flex-column">
<div
v-for="item in group"
v-show="show('mount', item)"
:key="item.key"
class="pet-group"
v-for="(group, key, index) in mounts(mountGroup, hideMissing, selectedSortBy, searchTextThrottled)"
v-if="index === 0 || $_openedItemRows_isToggled(mountGroup.key)"
:key="key"
class="pet-row d-flex"
>
<mountItem
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<div
v-for="item in group"
v-show="show('mount', item)"
:key="item.key"
:item="item"
:popover-position="'top'"
:show-popover="true"
@click="selectMount(item)"
class="pet-group"
>
<span slot="popoverContent">
<h4 class="popover-content-title">{{ item.name }}</h4>
</span>
<template
slot="itemBadge"
<mountItem
:key="item.key"
:item="item"
:popover-position="'top'"
:show-popover="true"
@click="selectMount(item)"
>
<equip-badge
:equipped="item.key === currentMount"
:show="isOwned('mount', item)"
@click="selectMount(item)"
/>
</template>
</mountItem>
<span slot="popoverContent">
<h4 class="popover-content-title">{{ item.name }}</h4>
</span>
<template
slot="itemBadge"
>
<equip-badge
:equipped="item.key === currentMount"
:show="isOwned('mount', item)"
@click="selectMount(item)"
/>
</template>
</mountItem>
</div>
</div>
<show-more-button
v-if="mountRowCount[mountGroup.key] > 1 && mountGroup.key !== 'specialMounts'"
:show-all="$_openedItemRows_isToggled(mountGroup.key)"
@click="setShowMore(mountGroup.key)"
/>
</div>
<show-more-button
v-if="mountRowCount[mountGroup.key] > 1 && mountGroup.key !== 'specialMounts'"
:show-all="$_openedItemRows_isToggled(mountGroup.key)"
@click="setShowMore(mountGroup.key)"
/>
</div>
<inventoryDrawer>
<template
@@ -310,13 +314,8 @@
overflow: hidden;
}
.pet-row {
max-width: 100%;
flex-wrap: wrap;
.item {
margin-right: .5em;
}
.hide {
height: 0px;
}
</style>
@@ -330,6 +329,14 @@
display: inline-block;
}
.pet-row {
flex-wrap: wrap;
.pet-group:not(:last-of-type) {
margin-right: 24px;
}
}
.GreyedOut {
opacity: 0.3;
}
@@ -343,24 +350,11 @@
}
.stable {
.standard-page {
padding-right:0;
}
.standard-page .clearfix .float-right {
margin-right: 24px;
}
.svg-icon.inline.icon-16 {
vertical-align: bottom;
}
}
.last {
margin-right: 0 !important;
}
.no-focus:focus {
background-color: inherit;
color: inherit;
@@ -80,7 +80,7 @@
</style>
<script>
import { mountInfo } from '@/../../common/script/content/stable';
import stable from '@/../../common/script/content/stable';
import markdownDirective from '@/directives/markdown';
export default {
@@ -105,7 +105,7 @@ export default {
},
methods: {
openDialog (mountKey) {
this.mount = mountInfo[mountKey];
this.mount = stable.mountInfo[mountKey];
this.$root.$emit('bv::show::modal', 'mount-raised-modal');
},
close () {
@@ -1,47 +1,41 @@
<template>
<div class="d-flex justify-content-around">
<span
<div class="d-flex align-items-center">
<div
v-for="currency of currencies"
:key="currency.key"
class="d-flex align-items-center"
>
<div
class="svg-icon ml-1"
class="svg-icon icon-16 ml-1"
v-html="currency.icon"
></div>
<span
<div
:class="{'notEnough': currency.notEnough}"
class="mx-1"
class="currency-value mx-1 my-auto"
>
{{ currency.value | roundBigNumber }}
</span>
</span>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
span {
font-size: 0.75rem;
line-height: 1.33;
color: $gray-100;
margin-bottom: 16px;
margin-top: -4px;
display: inline-block;
}
.svg-icon {
vertical-align: middle;
width: 16px;
height: 16px;
display: inline-block;
}
.currency-value {
font-size: 0.75rem;
line-height: 1.33;
color: $gray-100;
display: inline-block;
}
.notEnough {
color: #f23035 !important;
}
.svg-icon {
margin-top: 1px;
}
</style>
<script>
@@ -89,19 +89,19 @@
v-if="item.value > 0 && !(item.key === 'gem' && gemsLeft < 1)"
class="purchase-amount"
>
<!-- this is where the pretty item cost element lives -->
<div class="item-cost">
<div class="item-cost justify-content-center my-3">
<span
class="cost"
class="cost d-flex mx-auto"
:class="getPriceClass()"
>
<span
class="svg-icon inline icon-24"
class="svg-icon icon-24 my-auto mr-1"
aria-hidden="true"
v-html="icons[getPriceClass()]"
>
</span>
<span
class="my-auto"
:class="getPriceClass()"
>{{ item.value }}</span>
</span>
@@ -181,7 +181,7 @@
</div>
</div>
<countdown-banner
v-if="item.event && item.owned == null"
v-if="item.end && item.owned == null"
:end-date="endDate"
class="limitedTime available"
/>
@@ -218,11 +218,10 @@
</div>
<div
slot="modal-footer"
class="clearfix"
>
<span class="user-balance float-left">{{ $t('yourBalance') }}</span>
<span class="user-balance ml-3 my-auto">{{ $t('yourBalance') }}</span>
<balanceInfo
class="currency-totals"
class="mr-3"
:currency-needed="getPriceClass()"
:amount-needed="item.value"
/>
@@ -250,24 +249,21 @@
border-bottom-left-radius: 8px;
display: block;
margin: 24px 0 0 0;
padding: 16px 24px;
align-content: center;
padding: 0px;
> div {
display: flex;
justify-content: space-between;
align-items: center;
margin: 0px;
height: 100%;
}
.user-balance {
width: 150px;
height: 16px;
font-size: 0.75rem;
font-weight: bold;
line-height: 1.33;
color: $gray-100;
margin-bottom: 16px;
margin-top: -4px;
margin-left: -4px;
}
.currency-totals {
margin-right: -8px;
float: right;
}
}
@@ -452,14 +448,11 @@
}
.item-cost {
display: inline-flex;
margin: 16px 0;
align-items: center;
height: 40px;
}
.cost {
display: inline-block;
width: fit-content;
font-family: sans-serif;
font-size: 1.25rem;
font-weight: bold;
@@ -470,19 +463,16 @@
&.gems {
color: $green-10;
background-color: rgba(36, 204, 143, 0.15);
align-items: center;
}
&.gold {
color: $yellow-5;
background-color: rgba(255, 190, 93, 0.15);
align-items: center;
}
&.hourglasses {
color: $hourglass-color;
background-color: rgba(41, 149, 205, 0.15);
align-items: center;
}
}
@@ -547,10 +537,6 @@
margin: auto -1rem -1rem;
}
// .pt-015 {
// padding-top: 0.15rem;
// }
.gems-left {
height: 32px;
background-color: $green-100;
@@ -600,10 +586,12 @@ import reduce from 'lodash/reduce';
import moment from 'moment';
import planGemLimits from '@/../../common/script/libs/planGemLimits';
import { drops as dropEggs } from '@/../../common/script/content/eggs';
import { drops as dropPotions } from '@/../../common/script/content/hatching-potions';
import spellsMixin from '@/mixins/spells';
import eggs from '@/../../common/script/content/eggs';
import hatchingPotions from '@/../../common/script/content/hatching-potions';
import { avatarEditorUtilities } from '@/mixins/avatarEditUtilities';
import numberInvalid from '@/mixins/numberInvalid';
import spellsMixin from '@/mixins/spells';
import sync from '@/mixins/sync';
import svgClose from '@/assets/svg/close.svg';
import svgGold from '@/assets/svg/gold.svg';
@@ -629,6 +617,9 @@ import EquipmentAttributesGrid from '../inventory/equipment/attributesGrid.vue';
import Item from '@/components/inventory/item';
import Avatar from '@/components/avatar';
const dropEggs = eggs.drops;
const dropPotions = hatchingPotions.drops;
const dropEggKeys = keys(dropEggs);
const amountOfDropEggs = size(dropEggs);
@@ -637,7 +628,7 @@ const amountOfDropPotions = size(dropPotions);
const hideAmountSelectionForPurchaseTypes = [
'gear', 'backgrounds', 'mystery_set', 'card',
'rebirth_orb', 'fortify', 'armoire', 'keys',
'debuffPotion', 'pets', 'mounts',
'debuffPotion', 'pets', 'mounts', 'customization',
];
export default {
@@ -650,7 +641,15 @@ export default {
CountdownBanner,
numberIncrement,
},
mixins: [buyMixin, currencyMixin, notifications, numberInvalid, spellsMixin],
mixins: [
avatarEditorUtilities,
buyMixin,
currencyMixin,
notifications,
numberInvalid,
spellsMixin,
sync,
],
props: {
// eslint-disable-next-line vue/require-default-prop
item: {
@@ -690,7 +689,8 @@ export default {
computed: {
...mapState({ user: 'user.data' }),
showAvatar () {
return ['backgrounds', 'gear', 'mystery_set'].includes(this.item.purchaseType);
return ['backgrounds', 'gear', 'mystery_set', 'customization']
.includes(this.item.purchaseType);
},
preventHealthPotion () {
@@ -741,7 +741,7 @@ export default {
return (!this.user.purchased.plan.customerId && !this.user.purchased.plan.consecutive.trinkets && this.getPriceClass() === 'hourglasses');
},
endDate () {
return moment(this.item.event.end);
return moment(this.item.end);
},
totalOwned () {
return this.user.items[this.item.purchaseType][this.item.key] || 0;
@@ -759,7 +759,7 @@ export default {
this.selectedAmountToBuy = 1;
},
buyItem () {
async buyItem () {
// @TODO: I think we should buying to the items.
// Turn the items into classes, and use polymorphism
if (this.item.buy) {
@@ -824,17 +824,25 @@ export default {
) return;
}
const shouldConfirmPurchase = this.item.currency === 'gems' || this.item.currency === 'hourglasses';
if (
shouldConfirmPurchase
&& !this.confirmPurchase(this.item.currency, this.item.value * this.selectedAmountToBuy)
) {
return;
}
if (this.genericPurchase) {
this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
if (this.item.purchaseType === 'customization') {
const buySuccess = await this.unlock(this.item.path);
if (!buySuccess) return;
this.sync();
this.$root.$emit('playSound', 'Reward');
this.$root.$emit('buyModal::boughtItem', this.item);
this.purchased(this.item.text);
} else {
const shouldConfirmPurchase = this.item.currency === 'gems' || this.item.currency === 'hourglasses';
if (
shouldConfirmPurchase
&& !this.confirmPurchase(this.item.currency, this.item.value * this.selectedAmountToBuy)
) {
return;
}
if (this.genericPurchase) {
this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
this.purchased(this.item.text);
}
}
this.$emit('buyPressed', this.item);
@@ -891,6 +899,27 @@ export default {
return gear;
}
case 'customization': {
if (item.type === 'skin') {
return {
skin: item.key,
};
}
if (item.type === 'shirt') {
return {
shirt: item.key,
armor: 'armor_base_0',
};
}
if (['base', 'beard', 'color', 'mustache'].includes(item.type)) {
return {
hair: {
[item.type]: item.key,
},
head: 'head_base_0',
};
}
}
}
return {};
@@ -0,0 +1,265 @@
<template>
<div class="row market">
<div class="standard-sidebar">
<filter-sidebar>
<div
slot="search"
class="form-group"
>
<input
v-model="searchText"
class="form-control input-search"
type="text"
:placeholder="$t('search')"
>
</div>
<filter-group>
<checkbox
v-for="category in unfilteredCategories"
:id="`category-${category.identifier}`"
:key="category.identifier"
:checked.sync="viewOptions[category.identifier].selected"
:text="category.text"
/>
</filter-group>
</filter-sidebar>
</div>
<div class="standard-page p-0">
<div
class="background"
:style="{'background-image': imageURLs.background}"
>
<div
class="npc"
:style="{'background-image': imageURLs.npc}"
>
<div class="featured-label">
<span class="rectangle"></span><span
v-once
class="text"
>{{ $t('customizationsNPC') }}</span><span class="rectangle"></span>
</div>
</div>
</div>
<div class="p-4">
<h1
v-once
>
{{ $t('customizations') }}
</h1>
<div
v-for="category in categories"
:key="category.identifier"
>
<h2 class="mb-3 mt-4">
{{ category.text }}
</h2>
<item-rows
:items="customizationsItems({category, searchBy: searchTextThrottled})"
:type="category.identifier"
:fold-button="category.identifier === 'background'"
:item-width="94"
:item-margin="24"
:max-items-per-row="8"
:no-items-label="emptyStateString(category.identifier)"
@emptyClick="emptyClick(category.identifier, $event)"
>
<template
slot="item"
slot-scope="ctx"
>
<shop-item
:key="ctx.item.path"
:item="ctx.item"
:price="ctx.item.value"
:price-type="ctx.item.currency"
:empty-item="false"
:show-popover="Boolean(ctx.item.text)"
@click="selectItem(ctx.item)"
/>
</template>
</item-rows>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '~@/assets/scss/shops.scss';
h1 {
line-height: 32px;
color: $purple-200;
}
.background, .npc {
height: 216px;
}
.featured-label {
margin-left: 90px;
margin-top: 200px;
}
.npc {
background-repeat: no-repeat;
}
</style>
<script>
import shops from '@/../../common/script/libs/shops';
import throttle from 'lodash/throttle';
import { mapState } from '@/libs/store';
import Checkbox from '@/components/ui/checkbox';
import FilterGroup from '@/components/ui/filterGroup';
import FilterSidebar from '@/components/ui/filterSidebar';
import ItemRows from '@/components/ui/itemRows';
import ShopItem from '../shopItem';
export default {
components: {
Checkbox,
FilterGroup,
FilterSidebar,
ItemRows,
ShopItem,
},
data () {
return {
searchText: null,
searchTextThrottled: null,
unfilteredCategories: [],
viewOptions: {},
};
},
computed: {
...mapState({
// content: 'content',
user: 'user.data',
currentEventList: 'worldState.data.currentEventList',
}),
anyFilterSelected () {
return Object.values(this.viewOptions).some(g => g.selected);
},
imageURLs () {
return {
background: 'url(/static/npc/normal/customizations_background.png)',
npc: 'url(/static/npc/normal/customizations_npc.png)',
};
},
categories () {
const { unfilteredCategories } = this;
return unfilteredCategories.filter(category => !this.anyFilterSelected
|| this.viewOptions[category.identifier].selected);
},
},
watch: {
// TODO mixin?
searchText: throttle(function throttleSearch () {
this.searchTextThrottled = this.searchText.toLowerCase();
}, 250),
},
mounted () {
this.$store.dispatch('common:setTitle', {
subSection: this.$t('customizations'),
section: this.$t('shops'),
});
this.updateShop();
this.$root.$on('buyModal::boughtItem', () => {
this.updateShop();
});
},
methods: {
customizationsItems (options = {}) {
const { category, searchBy } = options;
return category.items.filter(item => !searchBy
|| item.text.toLowerCase().includes(searchBy));
},
emptyClick (identifier, event) {
if (event.target.tagName !== 'A') return;
this.$store.state.avatarEditorOptions.editingUser = true;
switch (identifier) {
case 'animalEars':
this.$store.state.avatarEditorOptions.startingPage = 'extra';
this.$store.state.avatarEditorOptions.subpage = 'ears';
break;
case 'animalTails':
this.$store.state.avatarEditorOptions.startingPage = 'extra';
this.$store.state.avatarEditorOptions.subpage = 'tails';
break;
case 'backgrounds':
this.$store.state.avatarEditorOptions.startingPage = 'background';
this.$store.state.avatarEditorOptions.subpage = '2024';
break;
case 'facialHair':
this.$store.state.avatarEditorOptions.startingPage = 'hair';
this.$store.state.avatarEditorOptions.subpage = 'beard';
break;
case 'color':
this.$store.state.avatarEditorOptions.startingPage = 'hair';
this.$store.state.avatarEditorOptions.subpage = 'color';
break;
case 'base':
this.$store.state.avatarEditorOptions.startingPage = 'hair';
this.$store.state.avatarEditorOptions.subpage = 'style';
break;
case 'shirt':
this.$store.state.avatarEditorOptions.startingPage = 'body';
this.$store.state.avatarEditorOptions.subpage = 'shirt';
break;
case 'skin':
this.$store.state.avatarEditorOptions.startingPage = 'skin';
this.$store.state.avatarEditorOptions.subpage = 'color';
break;
default:
throw new Error(`Unknown identifier ${identifier}`);
}
this.$root.$emit('bv::show::modal', 'avatar-modal');
},
emptyStateString (identifier) {
const { $t } = this;
switch (identifier) {
case 'animalEars':
return $t('allCustomizationsOwned');
case 'animalTails':
return $t('allCustomizationsOwned');
case 'backgrounds':
return `${$t('allCustomizationsOwned')} ${$t('checkNextMonth')}`;
case 'facialHair':
return $t('allCustomizationsOwned');
case 'color':
return `${$t('allCustomizationsOwned')} ${$t('checkNextSeason')}`;
case 'base':
return $t('allCustomizationsOwned');
case 'shirt':
return $t('allCustomizationsOwned');
case 'skin':
return `${$t('allCustomizationsOwned')} ${$t('checkNextSeason')}`;
default:
return `Unknown identifier ${identifier}`;
}
},
selectItem (item) {
this.$root.$emit('buyModal::showItem', item);
},
updateShop () {
const shop = shops.getCustomizationsShop(this.user);
shop.categories.forEach(category => {
// do not reset the viewOptions if already set once
if (typeof this.viewOptions[category.identifier] === 'undefined') {
this.$set(this.viewOptions, category.identifier, {
selected: false,
});
}
});
this.unfilteredCategories = shop.categories;
},
},
};
</script>
@@ -89,7 +89,7 @@ export default {
<style lang="scss" scoped>
.featuredItems {
height: 216px;
height: 192px;
.background {
width: 100%;
+10 -4
View File
@@ -3,26 +3,32 @@
<secondary-menu class="col-12">
<router-link
class="nav-link"
:to="{name: 'market'}"
:to="{ name: 'market' }"
exact="exact"
>
{{ $t('market') }}
</router-link>
<router-link
class="nav-link"
:to="{name: 'quests'}"
:to="{ name: 'quests' }"
>
{{ $t('quests') }}
</router-link>
<router-link
class="nav-link"
:to="{name: 'seasonal'}"
:to="{ name: 'customizations' }"
>
{{ $t('customizations') }}
</router-link>
<router-link
class="nav-link"
:to="{ name: 'seasonal' }"
>
{{ $t('titleSeasonalShop') }}
</router-link>
<router-link
class="nav-link"
:to="{name: 'time'}"
:to="{ name: 'time' }"
>
{{ $t('titleTimeTravelers') }}
</router-link>
@@ -6,6 +6,7 @@
:initial-item="selectedGearCategory"
:items="marketGearCategories"
:with-icon="true"
:direct-select="true"
@selected="selectedGroupGearByClass = $event.id"
>
<span
@@ -23,6 +24,7 @@
:label="$t('sortBy')"
:initial-item="selectedSortGearBy"
:items="sortGearBy"
:direct-select="true"
@selected="selectedSortGearBy = $event"
>
<span
@@ -40,7 +42,7 @@
:item-width="94"
:item-margin="24"
:type="'gear'"
:no-items-label="$t('noGearItemsOfClass')"
:no-items-label="noItemsLabel"
>
<template
slot="item"
@@ -75,6 +77,7 @@
import _filter from 'lodash/filter';
import _orderBy from 'lodash/orderBy';
import shops from '@/../../common/script/libs/shops';
import { remainingGearInSet } from '@/../../common/script/count';
import { getClassName } from '@/../../common/script/libs/getClassName';
import { mapState } from '@/libs/store';
import LayoutSection from '@/components/ui/layoutSection';
@@ -93,7 +96,7 @@ import pinUtils from '../../../mixins/pinUtils';
const sortGearTypes = [
'sortByType', 'sortByPrice', 'sortByCon',
'sortByPer', 'sortByStr', 'sortByInt',
].map(g => ({ id: g }));
].map(g => ({ id: g, identifier: g }));
const sortGearTypeMap = {
sortByType: 'type',
@@ -134,6 +137,17 @@ export default {
userItems: 'user.data.items',
userStats: 'user.data.stats',
}),
armoireCount () {
return remainingGearInSet(this.userItems.gear.owned, 'armoire');
},
noItemsLabel () {
if (this.armoireCount > 0) {
return `${this.$t('gearItemsCompleted', { klass: this.$t(this.selectedGroupGearByClass) })}
${this.$t('moreArmoireGearAvailable', { armoireCount: this.armoireCount })}`;
}
return `${this.$t('gearItemsCompleted', { klass: this.$t(this.selectedGroupGearByClass) })}
${this.$t('moreArmoireGearComing')}`;
},
marketGearCategories () {
return shops.getMarketGearCategories(this.user).map(c => {
c.id = c.identifier;

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