mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 05:07:22 +01:00
Compare commits
6 Commits
v5.35.2
...
phillip/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
192d649ffa | ||
|
|
e7aae55eca | ||
|
|
36b03613e1 | ||
|
|
2de9a16a2c | ||
|
|
895241b7fa | ||
|
|
2535fd7095 |
@@ -7,14 +7,5 @@ module.exports = {
|
||||
rules: {
|
||||
'prefer-regex-literals': 'warn',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'require-await': 'error',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['migrations/**', 'gulp/**'], // Or *.test.js
|
||||
rules: {
|
||||
'require-await': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
@@ -1,13 +1,6 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'phillip/**'
|
||||
- 'sabrecat/**'
|
||||
- 'kalista/**'
|
||||
- 'natalie/**'
|
||||
pull_request:
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
This webpage includes the documentation for version 3 of the [Habitica](https://habitica.com) API.
|
||||
|
||||
If you're developing a 3rd party tool that uses the Habitica API, read the [API Usage Guidelines](https://github.com/HabitRPG/habitica/wiki/API-Usage-Guidelines), which describe how to be a responsible user of our server resources!
|
||||
If you're developing a 3rd party tool that uses the Habitica API you should read the [Guidance for Comrades](https://habitica.fandom.com/wiki/Guidance_for_Comrades) and in particular the section called [Rules for Third-Party Tools](https://habitica.fandom.com/wiki/Guidance_for_Comrades#Rules_for_Third-Party_Tools) which includes suggestions on how to best use the API and the rules to follow when interacting with it.
|
||||
|
||||
@@ -93,6 +93,5 @@
|
||||
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
|
||||
"TIME_TRAVEL_ENABLED": "false",
|
||||
"DEBUG_ENABLED": "false",
|
||||
"CONTENT_SWITCHOVER_TIME_OFFSET": 8,
|
||||
"SLOW_REQUEST_THRESHOLD": 1000
|
||||
"CONTENT_SWITCHOVER_TIME_OFFSET": 8
|
||||
}
|
||||
|
||||
@@ -64,15 +64,6 @@ function filterFile (file) {
|
||||
if (file.relative.indexOf('icon_background') === 0) {
|
||||
return false;
|
||||
}
|
||||
if (file.relative.indexOf('notif_') === 0) {
|
||||
return false;
|
||||
}
|
||||
if (file.relative.indexOf('quest_') === 0) {
|
||||
return false;
|
||||
}
|
||||
if (file.relative.indexOf('inventory_quest_') === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Submodule habitica-images updated: 1c6f7d65d7...dfb04339a4
331
package-lock.json
generated
331
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "5.35.2",
|
||||
"version": "5.32.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "habitica",
|
||||
"version": "5.35.2",
|
||||
"version": "5.32.5",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -56,7 +56,7 @@
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^8.9.5",
|
||||
"mongoose": "^7.8.3",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.12.1",
|
||||
"node-gcm": "^1.0.5",
|
||||
@@ -3047,7 +3047,7 @@
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz",
|
||||
"integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
@@ -3677,15 +3677,14 @@
|
||||
"node_modules/@types/webidl-conversions": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
|
||||
},
|
||||
"node_modules/@types/whatwg-url": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
|
||||
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
|
||||
"license": "MIT",
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
|
||||
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/webidl-conversions": "*"
|
||||
}
|
||||
},
|
||||
@@ -6402,10 +6401,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bson": {
|
||||
"version": "6.10.2",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-6.10.2.tgz",
|
||||
"integrity": "sha512-5afhLTjqDSA3akH56E+/2J6kTDuSIlBxyXPdQslj9hcIgOUE378xdOfZvC/9q3LifJNI6KR/juZ+d0NRNYBwXg==",
|
||||
"license": "Apache-2.0",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-6.3.0.tgz",
|
||||
"integrity": "sha512-balJfqwwTBddxfnidJZagCBPP/f48zj9Sdp3OJswREOgsJzHiQSaOIAtApSgDQFYgHqAvFkp53AFSqjMDZoTFw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
}
|
||||
@@ -13361,6 +13360,28 @@
|
||||
"resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz",
|
||||
"integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA=="
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address/node_modules/jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
|
||||
},
|
||||
"node_modules/ip-address/node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -14275,10 +14296,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/kareem": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
|
||||
"integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
|
||||
"license": "Apache-2.0",
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz",
|
||||
"integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
@@ -14287,7 +14307,7 @@
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/kerberos/-/kerberos-1.1.7.tgz",
|
||||
"integrity": "sha512-1zXg4rARjsh/VMz2jjZeTfRHbJTVNR6f2DYHbLvtUSOW1satj33Fvc7vOJ0YVWB9+/9ITJWd1QKp4w217SsiFA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
@@ -14945,7 +14965,8 @@
|
||||
"node_modules/memory-pager": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
|
||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/meow": {
|
||||
"version": "3.7.0",
|
||||
@@ -15454,47 +15475,43 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
|
||||
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
|
||||
"license": "Apache-2.0",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz",
|
||||
"integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==",
|
||||
"dependencies": {
|
||||
"@types/whatwg-url": "^11.0.2",
|
||||
"whatwg-url": "^14.1.0 || ^13.0.0"
|
||||
"@types/whatwg-url": "^8.2.1",
|
||||
"whatwg-url": "^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
|
||||
"integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
|
||||
"license": "MIT",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
|
||||
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
"punycode": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz",
|
||||
"integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==",
|
||||
"license": "MIT",
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
|
||||
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
|
||||
"dependencies": {
|
||||
"tr46": "^5.0.0",
|
||||
"tr46": "^3.0.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-core": {
|
||||
@@ -15579,64 +15596,55 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "8.9.7",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.7.tgz",
|
||||
"integrity": "sha512-mvNXmU0V8qZzMR/qoK2mjT4Ti2ALdtfS0teK+twxhlGkwzOD76V02/zWajTu2MJ7QyEmZe9OWvnJsIY0iAuX3Q==",
|
||||
"license": "MIT",
|
||||
"version": "7.8.3",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.8.3.tgz",
|
||||
"integrity": "sha512-eFnbkKgyVrICoHB6tVJ4uLanS7d5AIo/xHkEbQeOv6g2sD7gh/1biRwvFifsmbtkIddQVNr3ROqHik6gkknN3g==",
|
||||
"dependencies": {
|
||||
"bson": "^6.10.1",
|
||||
"kareem": "2.6.3",
|
||||
"mongodb": "~6.12.0",
|
||||
"bson": "^5.5.0",
|
||||
"kareem": "2.5.1",
|
||||
"mongodb": "5.9.2",
|
||||
"mpath": "0.9.0",
|
||||
"mquery": "5.0.0",
|
||||
"ms": "2.1.3",
|
||||
"sift": "17.1.3"
|
||||
"sift": "16.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
"node": ">=14.20.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mongoose"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/kerberos": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/kerberos/-/kerberos-2.2.1.tgz",
|
||||
"integrity": "sha512-Vlyv1tjAPb0y2VIJ03dKkUjsneGIBuTkH24uGRx6/DrKpFlVuGPmct3m5aEotljVUlw7PAGWABwR5aNeW7y8Zw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^6.1.0",
|
||||
"prebuild-install": "^7.1.2"
|
||||
},
|
||||
"node_modules/mongoose/node_modules/bson": {
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz",
|
||||
"integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==",
|
||||
"engines": {
|
||||
"node": ">=12.9.0"
|
||||
"node": ">=14.20.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/mongodb": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz",
|
||||
"integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==",
|
||||
"license": "Apache-2.0",
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz",
|
||||
"integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==",
|
||||
"dependencies": {
|
||||
"@mongodb-js/saslprep": "^1.1.9",
|
||||
"bson": "^6.10.1",
|
||||
"mongodb-connection-string-url": "^3.0.0"
|
||||
"bson": "^5.5.0",
|
||||
"mongodb-connection-string-url": "^2.6.0",
|
||||
"socks": "^2.7.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
"node": ">=14.20.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@mongodb-js/saslprep": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/credential-providers": "^3.188.0",
|
||||
"@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
|
||||
"gcp-metadata": "^5.2.0",
|
||||
"kerberos": "^2.0.1",
|
||||
"mongodb-client-encryption": ">=6.0.0 <7",
|
||||
"snappy": "^7.2.2",
|
||||
"socks": "^2.7.1"
|
||||
"@mongodb-js/zstd": "^1.0.0",
|
||||
"kerberos": "^1.0.0 || ^2.0.0",
|
||||
"mongodb-client-encryption": ">=2.3.0 <3",
|
||||
"snappy": "^7.2.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@aws-sdk/credential-providers": {
|
||||
@@ -15645,9 +15653,6 @@
|
||||
"@mongodb-js/zstd": {
|
||||
"optional": true
|
||||
},
|
||||
"gcp-metadata": {
|
||||
"optional": true
|
||||
},
|
||||
"kerberos": {
|
||||
"optional": true
|
||||
},
|
||||
@@ -15656,9 +15661,6 @@
|
||||
},
|
||||
"snappy": {
|
||||
"optional": true
|
||||
},
|
||||
"socks": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -15667,105 +15669,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/mongoose/node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/mongoose/node_modules/node-abi": {
|
||||
"version": "3.74.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz",
|
||||
"integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/node-addon-api": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
|
||||
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/mongoose/node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^2.0.0",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/monk": {
|
||||
"version": "7.3.4",
|
||||
"resolved": "https://registry.npmjs.org/monk/-/monk-7.3.4.tgz",
|
||||
@@ -16179,7 +16082,7 @@
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz",
|
||||
"integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"semver": "^5.4.1"
|
||||
}
|
||||
@@ -16188,7 +16091,7 @@
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
@@ -16378,7 +16281,7 @@
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz",
|
||||
"integrity": "sha512-6kM8CLXvuW5crTxsAtva2YLrRrDaiTIkIePWs9moLHqbFWT94WpNFjwS/5dfLfECg5i/lkmw3aoqVidxt23TEQ==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "1.0.10",
|
||||
@@ -17993,7 +17896,7 @@
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.2.tgz",
|
||||
"integrity": "sha512-PzYWIKZeP+967WuKYXlTOhYBgGOvTRSfaKI89XnfJ0ansRAH7hDU45X+K+FZeI1Wb/7p/NnuctPH3g0IqKUuSQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"expand-template": "^2.0.3",
|
||||
@@ -18021,7 +17924,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
|
||||
"integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -18030,13 +17933,13 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/are-we-there-yet": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz",
|
||||
"integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^2.0.6"
|
||||
@@ -18046,7 +17949,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
@@ -18058,7 +17961,7 @@
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||
"integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"aproba": "^1.0.3",
|
||||
"console-control-strings": "^1.0.0",
|
||||
@@ -18074,7 +17977,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||
"integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
},
|
||||
@@ -18086,13 +17989,13 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"dev": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/npmlog": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
|
||||
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"are-we-there-yet": "~1.1.2",
|
||||
"console-control-strings": "~1.1.0",
|
||||
@@ -18104,7 +18007,7 @@
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
@@ -18119,7 +18022,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
@@ -18128,7 +18031,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||
"integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -18142,7 +18045,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
|
||||
"integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
},
|
||||
@@ -19673,10 +19576,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sift": {
|
||||
"version": "17.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
|
||||
"integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
|
||||
"license": "MIT"
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz",
|
||||
"integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ=="
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
@@ -19706,7 +19608,7 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz",
|
||||
"integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"decompress-response": "^4.2.0",
|
||||
"once": "^1.3.1",
|
||||
@@ -19717,7 +19619,7 @@
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
|
||||
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"mimic-response": "^2.0.0"
|
||||
},
|
||||
@@ -19729,7 +19631,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
|
||||
"integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
@@ -19870,6 +19772,15 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/snapdragon": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
|
||||
@@ -19989,6 +19900,19 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
|
||||
"integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
|
||||
"dependencies": {
|
||||
"ip-address": "^9.0.5",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sort-keys": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
|
||||
@@ -20066,6 +19990,7 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"memory-pager": "^1.0.2"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.35.2",
|
||||
"version": "5.32.5",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -51,7 +51,7 @@
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^8.9.5",
|
||||
"mongoose": "^7.8.3",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.12.1",
|
||||
"node-gcm": "^1.0.5",
|
||||
|
||||
@@ -2,22 +2,13 @@
|
||||
import moment from 'moment';
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateTodo,
|
||||
generateDaily,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import { cron, cronWrapper } from '../../../../website/server/libs/cron';
|
||||
import { recoverCron, cron } from '../../../../website/server/libs/cron';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
import common from '../../../../website/common';
|
||||
import * as analytics from '../../../../website/server/libs/analyticsService';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
|
||||
const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime();
|
||||
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
|
||||
// const scoreTask = common.ops.scoreTask;
|
||||
|
||||
const pathToCronLib = '../../../../website/server/libs/cron';
|
||||
|
||||
@@ -1209,7 +1200,7 @@ describe('cron', async () => {
|
||||
it('increments perfect day achievement if all (at least 1) due dailies were completed', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
@@ -1221,7 +1212,7 @@ describe('cron', async () => {
|
||||
it('does not increment perfect day achievement if no due dailies', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].isDue = false;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).add({ days: 1 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
@@ -1233,7 +1224,7 @@ describe('cron', async () => {
|
||||
it('gives perfect day buff if all (at least 1) due dailies were completed', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
@@ -1251,7 +1242,7 @@ describe('cron', async () => {
|
||||
user.preferences.sleep = true;
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
@@ -1268,7 +1259,7 @@ describe('cron', async () => {
|
||||
it('clears buffs if user does not have a perfect day (no due dailys)', async () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].isDue = false;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).add({ days: 1 });
|
||||
|
||||
user.stats.buffs = {
|
||||
str: 1,
|
||||
@@ -1497,6 +1488,78 @@ describe('cron', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('notifications', async () => {
|
||||
it('adds a user notification', async () => {
|
||||
const mpBefore = user.stats.mp;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
|
||||
const statsComputedRes = common.statsComputed(user);
|
||||
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
|
||||
daysMissed = 1;
|
||||
const hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.notifications.length).to.be.greaterThan(0);
|
||||
expect(user.notifications[1].type).to.equal('CRON');
|
||||
expect(user.notifications[1].data).to.eql({
|
||||
hp: user.stats.hp - hpBefore,
|
||||
mp: user.stats.mp - mpBefore,
|
||||
});
|
||||
|
||||
common.statsComputed.restore();
|
||||
});
|
||||
|
||||
it('condenses multiple notifications into one', async () => {
|
||||
const mpBefore1 = user.stats.mp;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
|
||||
const statsComputedRes = common.statsComputed(user);
|
||||
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
|
||||
daysMissed = 1;
|
||||
const hpBefore1 = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.notifications.length).to.be.greaterThan(0);
|
||||
expect(user.notifications[1].type).to.equal('CRON');
|
||||
expect(user.notifications[1].data).to.eql({
|
||||
hp: user.stats.hp - hpBefore1,
|
||||
mp: user.stats.mp - mpBefore1,
|
||||
});
|
||||
|
||||
const notifsBefore2 = user.notifications.length;
|
||||
const hpBefore2 = user.stats.hp;
|
||||
const mpBefore2 = user.stats.mp;
|
||||
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.notifications.length - notifsBefore2).to.equal(0);
|
||||
expect(user.notifications[0].type).to.not.equal('CRON');
|
||||
expect(user.notifications[1].type).to.equal('CRON');
|
||||
expect(user.notifications[1].data).to.eql({
|
||||
hp: user.stats.hp - hpBefore2 - (hpBefore2 - hpBefore1),
|
||||
mp: user.stats.mp - mpBefore2 - (mpBefore2 - mpBefore1),
|
||||
});
|
||||
expect(user.notifications[0].type).to.not.equal('CRON');
|
||||
common.statsComputed.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('private messages', async () => {
|
||||
let lastMessageId;
|
||||
|
||||
@@ -1543,7 +1606,7 @@ describe('cron', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.notifications.length).to.eql(1);
|
||||
expect(user.notifications.length).to.be.greaterThan(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
});
|
||||
|
||||
@@ -1757,258 +1820,64 @@ describe('cron', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('cron wrapper', () => {
|
||||
let res; let
|
||||
req;
|
||||
let user;
|
||||
describe('recoverCron', async () => {
|
||||
let locals; let status; let
|
||||
execStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
user = await res.locals.user.save();
|
||||
res.analytics = analytics;
|
||||
execStub = sandbox.stub();
|
||||
sandbox.stub(User, 'findOne').returns({ exec: execStub });
|
||||
|
||||
status = { times: 0 };
|
||||
locals = {
|
||||
user: new User({
|
||||
auth: {
|
||||
local: {
|
||||
username: 'username',
|
||||
lowerCaseUsername: 'username',
|
||||
email: 'email@example.com',
|
||||
salt: 'salt',
|
||||
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('calls next when user is not attached', async () => {
|
||||
res.locals.user = null;
|
||||
await cronWrapper(req, res);
|
||||
});
|
||||
|
||||
it('calls next when days have not been missed', async () => {
|
||||
await cronWrapper(req, res);
|
||||
});
|
||||
|
||||
it('should clear todos older than 30 days for free users', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
||||
expect(taskRes).to.not.exist;
|
||||
});
|
||||
|
||||
it('should not clear todos older than 30 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await Promise.all([task.save(), user.save()]);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
||||
expect(taskRes).to.exist;
|
||||
});
|
||||
|
||||
it('should clear todos older than 90 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 91 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
||||
expect(taskRes).to.not.exist;
|
||||
});
|
||||
|
||||
it('should call next if user was not modified after cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
expect(hpBefore).to.equal(user.stats.hp);
|
||||
});
|
||||
|
||||
it('runs cron if previous cron was incomplete', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
||||
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
});
|
||||
|
||||
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
});
|
||||
|
||||
it('does damage for missing dailies', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const updatedUser = await User.findOne({ _id: user._id });
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
});
|
||||
|
||||
it('updates tasks', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
await Promise.all([todo.save(), user.save()]);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
||||
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
||||
});
|
||||
|
||||
it('updates large number of tasks', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
const start = new Date();
|
||||
const saves = [todo.save(), user.save()];
|
||||
for (let i = 0; i < 200; i += 1) {
|
||||
const newTodo = generateTodo(user);
|
||||
newTodo.value = i;
|
||||
saves.push(newTodo.save());
|
||||
}
|
||||
await Promise.all(saves);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const duration = new Date() - start;
|
||||
expect(duration).to.be.lessThan(1000);
|
||||
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
||||
expect(moment(start).isSame(user.lastCron, 'day'));
|
||||
expect(moment(start).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
||||
});
|
||||
|
||||
it('fails entire cron if one task is failing', async () => {
|
||||
const lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
user.lastCron = lastCron;
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
const badTodo = generateTodo(user);
|
||||
badTodo.text = 'bad todo';
|
||||
badTodo.attribute = 'bad';
|
||||
await Promise.all([badTodo.save({ validateBeforeSave: false }), todo.save(), user.save()]);
|
||||
it('throws an error if user cannot be found', async () => {
|
||||
execStub.returns(Promise.resolve(null));
|
||||
|
||||
try {
|
||||
await cronWrapper(req, res);
|
||||
await recoverCron(status, locals);
|
||||
throw new Error('no exception when user cannot be found');
|
||||
} catch (err) {
|
||||
expect(err).to.exist;
|
||||
expect(err.message).to.eql(`User ${locals.user._id} not found while recovering.`);
|
||||
}
|
||||
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
||||
expect(moment(lastCron).isSame(user.lastCron, 'day'));
|
||||
expect(todoFound.value).to.be.equal(todoValueBefore);
|
||||
});
|
||||
|
||||
it('applies quest progress', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
it('increases status.times count and reruns up to 4 times', async () => {
|
||||
execStub.returns(Promise.resolve({ _cronSignature: 'RUNNING_CRON' }));
|
||||
execStub.onCall(4).returns(Promise.resolve({ _cronSignature: 'NOT_RUNNING' }));
|
||||
|
||||
const questKey = 'dilatory';
|
||||
user.party.quest.key = questKey;
|
||||
await recoverCron(status, locals);
|
||||
|
||||
const party = new Group({
|
||||
type: 'party',
|
||||
name: generateUUID(),
|
||||
leader: user._id,
|
||||
});
|
||||
party.quest.members[user._id] = true;
|
||||
party.quest.key = questKey;
|
||||
await party.save();
|
||||
|
||||
user.party._id = party._id;
|
||||
await user.save();
|
||||
|
||||
party.startQuest(user);
|
||||
|
||||
await cronWrapper(req, res);
|
||||
const updatedUser = await User.findOne({ _id: user._id });
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
expect(status.times).to.eql(4);
|
||||
expect(locals.user).to.eql({ _cronSignature: 'NOT_RUNNING' });
|
||||
});
|
||||
|
||||
it('cronSignature less than 5 minutes ago should error', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
it('throws an error if recoverCron runs 5 times', async () => {
|
||||
execStub.returns(Promise.resolve({ _cronSignature: 'RUNNING_CRON' }));
|
||||
|
||||
try {
|
||||
await cronWrapper(req, res);
|
||||
await recoverCron(status, locals);
|
||||
throw new Error('no exception when recoverCron runs 5 times');
|
||||
} catch (err) {
|
||||
expect(err).to.exist;
|
||||
expect(status.times).to.eql(5);
|
||||
expect(err.message).to.eql(`Impossible to recover from cron for user ${locals.user._id}.`);
|
||||
}
|
||||
});
|
||||
|
||||
it('cronSignature longer than an hour ago should allow cron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
|
||||
await cronWrapper(req, res);
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
|
||||
});
|
||||
|
||||
it('cron should not run more than once', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
const result = await Promise.allSettled([
|
||||
cronWrapper(req, res),
|
||||
cronWrapper(req, res),
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const runResult = await cronWrapper(req, res);
|
||||
if (runResult !== null) {
|
||||
reject(new Error('cron ran more than once'));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}, 200);
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(result.filter(r => r.status === 'fulfilled')).to.have.lengthOf(2);
|
||||
expect(result.filter(r => r.status === 'rejected')).to.have.lengthOf(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -171,23 +171,23 @@ describe('emails', () => {
|
||||
expect(got.post).not.to.be.called;
|
||||
});
|
||||
|
||||
it('throws error when mail target is only a string', async () => {
|
||||
it('throws error when mail target is only a string', () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = 'my email';
|
||||
|
||||
await expect(sendTxn(mailingInfo, emailType)).to.be.rejectedWith('Argument Error mailingInfoArray: does not contain email or _id');
|
||||
expect(sendTxn(mailingInfo, emailType)).to.throw;
|
||||
});
|
||||
|
||||
it('throws error when mail target has no _id or email', async () => {
|
||||
it('throws error when mail target has no _id or email', () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = {
|
||||
|
||||
};
|
||||
|
||||
await expect(sendTxn(mailingInfo, emailType)).to.be.rejectedWith('Argument Error mailingInfoArray: does not contain email or _id');
|
||||
expect(sendTxn(mailingInfo, emailType)).to.throw;
|
||||
});
|
||||
|
||||
it('throws error when variables not an array', async () => {
|
||||
it('throws error when variables not an array', () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = {
|
||||
name: 'my name',
|
||||
@@ -195,10 +195,9 @@ describe('emails', () => {
|
||||
};
|
||||
const variables = {};
|
||||
|
||||
await expect(sendTxn(mailingInfo, emailType, variables)).to.be.rejectedWith('Argument Error variables: is not an array');
|
||||
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
|
||||
});
|
||||
|
||||
it('throws error when variables array not contain name/content', async () => {
|
||||
it('throws error when variables array not contain name/content', () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = {
|
||||
name: 'my name',
|
||||
@@ -210,9 +209,8 @@ describe('emails', () => {
|
||||
},
|
||||
];
|
||||
|
||||
await expect(sendTxn(mailingInfo, emailType, variables)).to.be.rejectedWith('Argument Error variables: does not contain name or content');
|
||||
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
|
||||
});
|
||||
|
||||
it('throws no error when variables array contain name but no content', () => {
|
||||
const emailType = 'an email type';
|
||||
const mailingInfo = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os from 'os';
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
|
||||
const pathToMongoLib = '../../../../website/server/libs/mongodb';
|
||||
@@ -28,4 +29,22 @@ describe('mongodb', () => {
|
||||
expect(string).to.equal('mongodb://hostname:3030');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultConnectionOptions', () => {
|
||||
it('returns development config when IS_PROD is false', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
||||
|
||||
const options = mongoLibOverride.getDefaultConnectionOptions();
|
||||
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
|
||||
});
|
||||
|
||||
it('returns production config when IS_PROD is true', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
||||
|
||||
const options = mongoLibOverride.getDefaultConnectionOptions();
|
||||
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
332
test/api/unit/middlewares/cronMiddleware.js
Normal file
332
test/api/unit/middlewares/cronMiddleware.js
Normal file
@@ -0,0 +1,332 @@
|
||||
import moment from 'moment';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateTodo,
|
||||
generateDaily,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import cronMiddleware from '../../../../website/server/middlewares/cron';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
import * as cronLib from '../../../../website/server/libs/cron';
|
||||
|
||||
const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime();
|
||||
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
|
||||
|
||||
describe('cron middleware', () => {
|
||||
let res; let
|
||||
req;
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
user = await res.locals.user.save();
|
||||
res.analytics = analyticsService;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('calls next when user is not attached', done => {
|
||||
res.locals.user = null;
|
||||
cronMiddleware(req, res, done);
|
||||
});
|
||||
|
||||
it('calls next when days have not been missed', done => {
|
||||
cronMiddleware(req, res, done);
|
||||
});
|
||||
|
||||
it('should clear todos older than 30 days for free users', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
|
||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
||||
expect(foundTask).to.not.exist;
|
||||
resolve();
|
||||
});
|
||||
|
||||
return null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not clear todos older than 30 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
||||
expect(foundTask).to.exist;
|
||||
return resolve();
|
||||
});
|
||||
return null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear todos older than 90 days for subscribed users', async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
|
||||
const task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({ days: 91 });
|
||||
task.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
||||
expect(foundTask).to.not.exist;
|
||||
return resolve();
|
||||
});
|
||||
return null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should call next if user was not modified after cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(hpBefore).to.equal(user.stats.hp);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('runs cron if previous cron was incomplete', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
||||
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does damage for missing dailies', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return User.findOne({ _id: user._id }).then(updatedUser => {
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('updates tasks', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const todo = generateTodo(user);
|
||||
const todoValueBefore = todo.value;
|
||||
await Promise.all([todo.save(), user.save()]);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return Tasks.Task.findOne({ _id: todo._id }).then(todoFound => {
|
||||
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('applies quest progress', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||
await daily.save();
|
||||
|
||||
const questKey = 'dilatory';
|
||||
user.party.quest.key = questKey;
|
||||
|
||||
const party = new Group({
|
||||
type: 'party',
|
||||
name: generateUUID(),
|
||||
leader: user._id,
|
||||
});
|
||||
party.quest.members[user._id] = true;
|
||||
party.quest.key = questKey;
|
||||
await party.save();
|
||||
|
||||
user.party._id = party._id;
|
||||
await user.save();
|
||||
|
||||
party.startQuest(user);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return User.findOne({ _id: user._id }).then(updatedUser => {
|
||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('recovers from failed cron and does not error when user is already cronning', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
const updatedUser = user.toObject();
|
||||
updatedUser.matchedCount = 0;
|
||||
|
||||
sandbox.spy(cronLib, 'recoverCron');
|
||||
|
||||
sandbox.stub(User, 'updateOne')
|
||||
.withArgs({
|
||||
_id: user._id,
|
||||
$or: [
|
||||
{ _cronSignature: 'NOT_RUNNING' },
|
||||
{ _cronSignature: { $lt: sinon.match.number } },
|
||||
],
|
||||
})
|
||||
.returns({
|
||||
exec () {
|
||||
return Promise.resolve(updatedUser);
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(cronLib.recoverCron).to.be.calledOnce;
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cronSignature less than an hour ago should error', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
const expectedErrMessage = `Impossible to recover from cron for user ${user._id}.`;
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (!err) return reject(new Error('Cron should have failed.'));
|
||||
expect(err.message).to.be.equal(expectedErrMessage);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cronSignature longer than an hour ago should allow cron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
const now = new Date();
|
||||
await User.updateOne({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cron should not run more than once', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
|
||||
sandbox.spy(cronLib, 'cron');
|
||||
|
||||
await Promise.all([new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
}), new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
}), new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
cronMiddleware(req, res, err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
}, 400);
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(cronLib.cron).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,6 @@ describe('GET /heroes/:heroId', () => {
|
||||
const heroFields = [
|
||||
'_id', 'id', 'auth', 'balance', 'contributor', 'flags', 'items',
|
||||
'lastCron', 'party', 'preferences', 'profile', 'purchased', 'secret', 'achievements',
|
||||
'stats',
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
|
||||
@@ -11,7 +11,6 @@ describe('PUT /heroes/:heroId', () => {
|
||||
const heroFields = [
|
||||
'_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron',
|
||||
'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions', 'achievements',
|
||||
'stats',
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('GET /members/username/:username', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a member\'s public data only', async () => {
|
||||
it('returns a member public data only', async () => {
|
||||
// make sure user has all the fields that can be returned by the getMember call
|
||||
const member = await generateUser({
|
||||
contributor: { level: 1 },
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('POST /inbox/like-private-message/:messageId', () => {
|
||||
userToSendMessage = await generateUser();
|
||||
});
|
||||
|
||||
it('returns an error when private message is not found', async () => {
|
||||
it('Returns an error when private message is not found', async () => {
|
||||
await expect(userToSendMessage.post(getLikeUrl('some-unknown-id')))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
@@ -35,7 +35,7 @@ describe('POST /inbox/like-private-message/:messageId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('likes a message', async () => {
|
||||
it('Likes a message', async () => {
|
||||
const receiver = await generateUser();
|
||||
|
||||
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||
@@ -57,7 +57,7 @@ describe('POST /inbox/like-private-message/:messageId', () => {
|
||||
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, receiver._id, true);
|
||||
});
|
||||
|
||||
it('allows a user to like their own private message', async () => {
|
||||
it('Allows to likes their own private message', async () => {
|
||||
const receiver = await generateUser();
|
||||
|
||||
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||
@@ -78,7 +78,7 @@ describe('POST /inbox/like-private-message/:messageId', () => {
|
||||
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, userToSendMessage._id, true);
|
||||
});
|
||||
|
||||
it('unlikes a message', async () => {
|
||||
it('Unlikes a message', async () => {
|
||||
const receiver = await generateUser();
|
||||
|
||||
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||
|
||||
@@ -10,7 +10,7 @@ describe('events', () => {
|
||||
});
|
||||
|
||||
it('returns empty array when no events are active', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-11'));
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-08'));
|
||||
const events = getRepeatingEvents();
|
||||
expect(events).to.be.empty;
|
||||
});
|
||||
|
||||
@@ -190,7 +190,7 @@ describe('Content Schedule', () => {
|
||||
const date = new Date('2024-04-15');
|
||||
const matchers = getAllScheduleMatchingGroups(date);
|
||||
expect(matchers.premiumHatchingPotions).to.exist;
|
||||
expect(matchers.premiumHatchingPotions.items.length).to.equal(6);
|
||||
expect(matchers.premiumHatchingPotions.items.length).to.equal(5);
|
||||
expect(matchers.premiumHatchingPotions.items.indexOf('Veggie')).to.not.equal(-1);
|
||||
expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1);
|
||||
});
|
||||
|
||||
@@ -74,10 +74,15 @@ export async function getDocument (collectionName, doc) {
|
||||
}
|
||||
|
||||
before(done => {
|
||||
mongoose.connection.once('open', async err => {
|
||||
if (err) throw err;
|
||||
await resetHabiticaDB();
|
||||
done();
|
||||
mongoose.connection.on('open', err => {
|
||||
if (err) return done(err);
|
||||
return resetHabiticaDB()
|
||||
.then(() => {
|
||||
done();
|
||||
})
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
|
||||
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid {
|
||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup, .Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
@@ -48,10 +47,6 @@
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Cryptid {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Gems {
|
||||
display:inline-block;
|
||||
margin-right:5px;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@
|
||||
top: -16px !important;
|
||||
}
|
||||
|
||||
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi, Cryptid;
|
||||
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi;
|
||||
|
||||
@each $foolPet in $foolPets {
|
||||
.Pet.Pet-FlyingPig-#{$foolPet} {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4,0C1.79,0,0,1.79,0,4v16c0,2.21,1.79,4,4,4h16c2.21,0,4-1.79,4-4V4c0-2.21-1.79-4-4-4H4ZM12,11.57c-.72-1.49-2.7-4.26-4.53-5.63-1.32-.99-3.47-1.75-3.47.68,0,.49.28,4.08.44,4.66.57,2.03,2.65,2.55,4.5,2.23-3.24.55-4.06,2.36-2.28,4.17,3.38,3.44,4.85-.86,5.23-1.97h0s0,0,0,0c.07-.2.1-.29.1-.21,0-.08.03.01.1.22h0c.38,1.1,1.85,5.41,5.23,1.97,1.78-1.81.95-3.63-2.28-4.17,1.85.31,3.93-.2,4.5-2.23.16-.58.44-4.18.44-4.66,0-2.43-2.14-1.67-3.47-.68-1.83,1.37-3.81,4.14-4.53,5.63Z" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 572 B |
3
website/client/src/assets/svg/twitter.svg
Normal file
3
website/client/src/assets/svg/twitter.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M20,0H4A4,4,0,0,0,0,4V20a4,4,0,0,0,4,4H20a4,4,0,0,0,4-4V4A4,4,0,0,0,20,0ZM18.36,8.74c0,.14,0,.29,0,.43A9.34,9.34,0,0,1,4,17a6.85,6.85,0,0,0,.79,0,6.57,6.57,0,0,0,4.07-1.4A3.29,3.29,0,0,1,5.8,13.39a4.1,4.1,0,0,0,.62,0,3.49,3.49,0,0,0,.86-.11,3.28,3.28,0,0,1-2.63-3.22v0a3.35,3.35,0,0,0,1.48.42A3.29,3.29,0,0,1,4.67,7.76,3.22,3.22,0,0,1,5.12,6.1a9.3,9.3,0,0,0,6.76,3.43,3.67,3.67,0,0,1-.08-.75,3.28,3.28,0,0,1,5.67-2.24,6.54,6.54,0,0,0,2.08-.79,3.22,3.22,0,0,1-1.44,1.8A6.67,6.67,0,0,0,20,7.05,7.31,7.31,0,0,1,18.36,8.74Z" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 622 B |
@@ -25,9 +25,9 @@
|
||||
<router-link to="/">
|
||||
Homepage
|
||||
</router-link>or
|
||||
<a href="mailto:admin@habitica.com">
|
||||
<router-link :to="contactUsLink">
|
||||
Contact Us
|
||||
</a>about the issue.
|
||||
</router-link>about the issue.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,6 +40,12 @@ import { mapState } from '@/libs/store';
|
||||
export default {
|
||||
computed: {
|
||||
...mapState(['isUserLoggedIn']),
|
||||
contactUsLink () {
|
||||
if (this.isUserLoggedIn) {
|
||||
return { name: 'guild', params: { groupId: 'a29da26b-37de-4a71-b0c6-48e72a900dac' } };
|
||||
}
|
||||
return { name: 'contact' };
|
||||
},
|
||||
retiredChatPage () {
|
||||
return this.$route.fullPath.indexOf('/groups') !== -1;
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-6 offset-3">
|
||||
<Sprite image-name="shop_armoire" />
|
||||
<div class="shop_armoire"></div>
|
||||
<p>{{ $t('armoireLastItem') }}</p>
|
||||
<p>{{ $t('armoireNotesEmpty') }}</p>
|
||||
</div>
|
||||
@@ -34,12 +34,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Sprite from '@/components/ui/sprite';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Sprite,
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'armoire-empty');
|
||||
|
||||
@@ -95,11 +95,7 @@
|
||||
@click="clickDisableClasses(); close();"
|
||||
>{{ $t('optOutOfClasses') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="opt-out-description"
|
||||
v-html="$t('optOutOfClassesText')"
|
||||
></div>
|
||||
<span class="opt-out-description">{{ $t('optOutOfClassesText') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<Sprite :image-name="questClass" />
|
||||
<div :class="questClass"></div>
|
||||
</section>
|
||||
<!-- @TODO: Keep this? .checkboxinput(type='checkbox', v-model=
|
||||
'user.preferences.suppressModals.levelUp', @change='changeLevelupSuppress()')
|
||||
@@ -150,12 +150,15 @@ label(style='display:inline-block') {{ $t('dontShowAgain') }}
|
||||
section.greyed {
|
||||
padding-bottom: 17px
|
||||
}
|
||||
|
||||
.scroll {
|
||||
margin: -11px auto 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Avatar from '../avatar';
|
||||
import Sprite from '@/components/ui/sprite';
|
||||
import { mapState } from '@/libs/store';
|
||||
import starGroup from '@/assets/svg/star-group.svg';
|
||||
import sparkles from '@/assets/svg/sparkles-left.svg';
|
||||
@@ -170,7 +173,6 @@ const levelQuests = {
|
||||
export default {
|
||||
components: {
|
||||
Avatar,
|
||||
Sprite,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -189,9 +191,7 @@ export default {
|
||||
return this.user.stats.lvl in levelQuests;
|
||||
},
|
||||
questClass () {
|
||||
const questKey = levelQuests[this.user.stats.lvl];
|
||||
if (questKey) return `inventory_quest_scroll_${questKey}`;
|
||||
return '';
|
||||
return `scroll inventory_quest_scroll_${levelQuests[this.user.stats.lvl]}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<p v-html="$t('moreGearAchievements')"></p>
|
||||
<br>
|
||||
</div>
|
||||
<Sprite image-name="shop_armoire" />
|
||||
<div class="shop_armoire"></div>
|
||||
<p v-html="$t('armoireUnlocked')"></p>
|
||||
<br>
|
||||
<button
|
||||
@@ -87,13 +87,11 @@
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
import { mapState } from '@/libs/store';
|
||||
import Sprite from '@/components/ui/sprite.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
Sprite,
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
|
||||
@@ -92,6 +92,8 @@ export default {
|
||||
params: { userIdentifier },
|
||||
}).catch(failure => {
|
||||
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||
// the admin has requested that the same user be displayed again so reload the page
|
||||
// (e.g., if they changed their mind about changes they were making)
|
||||
this.$router.go();
|
||||
}
|
||||
});
|
||||
@@ -99,16 +101,14 @@ export default {
|
||||
|
||||
async loadUser (userIdentifier) {
|
||||
const id = userIdentifier || this.user._id;
|
||||
if (this.$router.currentRoute.name === 'adminPanelUser') {
|
||||
await this.$router.push({
|
||||
name: 'adminPanel',
|
||||
});
|
||||
}
|
||||
await this.$router.push({
|
||||
|
||||
this.$router.push({
|
||||
name: 'adminPanelUser',
|
||||
params: { userIdentifier: id },
|
||||
}).catch(failure => {
|
||||
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||
// the admin has requested that the same user be displayed again so reload the page
|
||||
// (e.g., if they changed their mind about changes they were making)
|
||||
this.$router.go();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import VueRouter from 'vue-router';
|
||||
|
||||
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
async saveHero ({
|
||||
hero,
|
||||
msg = 'User',
|
||||
clearData,
|
||||
reloadData,
|
||||
}) {
|
||||
async saveHero ({ hero, msg = 'User', clearData }) {
|
||||
await this.$store.dispatch('hall:updateHero', { heroDetails: hero });
|
||||
await this.$store.dispatch('snackbars:add', {
|
||||
title: '',
|
||||
@@ -23,20 +14,6 @@ export default {
|
||||
// The admin should re-fetch the data if they need to keep working on that user.
|
||||
this.$emit('clear-data');
|
||||
this.$router.push({ name: 'adminPanel' });
|
||||
} else if (reloadData) {
|
||||
if (this.$router.currentRoute.name === 'adminPanelUser') {
|
||||
await this.$router.push({
|
||||
name: 'adminPanel',
|
||||
});
|
||||
}
|
||||
await this.$router.push({
|
||||
name: 'adminPanelUser',
|
||||
params: { userIdentifier: hero._id },
|
||||
}).catch(failure => {
|
||||
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||
this.$router.go();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,11 +7,7 @@
|
||||
>
|
||||
Could not find any matching users.
|
||||
</div>
|
||||
<loading-spinner
|
||||
v-if="isSearching"
|
||||
class="mx-auto mb-2"
|
||||
dark-color="true"
|
||||
/>
|
||||
<loading-spinner class="mx-auto mb-2" dark-color="true" v-if="isSearching" />
|
||||
<div
|
||||
v-if="users.length > 0"
|
||||
class="list-group"
|
||||
@@ -63,10 +59,6 @@ export default {
|
||||
components: {
|
||||
LoadingSpinner,
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
this.userIdentifier = to.params.userIdentifier;
|
||||
next();
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
userIdentifier: '',
|
||||
@@ -78,6 +70,10 @@ export default {
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
this.userIdentifier = to.params.userIdentifier;
|
||||
next();
|
||||
},
|
||||
watch: {
|
||||
userIdentifier () {
|
||||
this.isSearching = true;
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<li
|
||||
v-for="item in achievements"
|
||||
:key="item.path"
|
||||
v-b-tooltip.hover="item.notes"
|
||||
>
|
||||
<form @submit.prevent="saveItem(item)">
|
||||
<span
|
||||
@@ -28,7 +27,7 @@
|
||||
{{ item.value }}
|
||||
</span>
|
||||
:
|
||||
{{ item.text || item.key }} - <i> {{ item.key }} </i>
|
||||
{{ item.text || item.key }}
|
||||
</span>
|
||||
|
||||
<div
|
||||
@@ -69,7 +68,6 @@
|
||||
<li
|
||||
v-for="item in nestedAchievements[achievementType]"
|
||||
:key="item.path"
|
||||
v-b-tooltip.hover="item.notes"
|
||||
>
|
||||
<form @submit.prevent="saveItem(item)">
|
||||
<span
|
||||
@@ -80,7 +78,7 @@
|
||||
{{ item.value }}
|
||||
</span>
|
||||
:
|
||||
{{ item.text || item.key }} - <i> {{ item.key }} </i>
|
||||
{{ item.text || item.key }}
|
||||
</span>
|
||||
|
||||
<div
|
||||
@@ -145,28 +143,79 @@ function getText (achievementItem) {
|
||||
}
|
||||
const { titleKey } = achievementItem;
|
||||
if (titleKey !== undefined) {
|
||||
return i18n.t(titleKey);
|
||||
return i18n.t(titleKey, 'en');
|
||||
}
|
||||
const { singularTitleKey } = achievementItem;
|
||||
if (singularTitleKey !== undefined) {
|
||||
return i18n.t(singularTitleKey);
|
||||
return i18n.t(singularTitleKey, 'en');
|
||||
}
|
||||
return achievementItem.key;
|
||||
}
|
||||
|
||||
function getNotes (achievementItem, count) {
|
||||
if (achievementItem === undefined) {
|
||||
return '';
|
||||
function collateItemData (self) {
|
||||
const achievements = [];
|
||||
const nestedAchievements = {};
|
||||
const basePath = 'achievements';
|
||||
const ownedAchievements = self.hero.achievements;
|
||||
const allAchievements = content.achievements;
|
||||
|
||||
for (const key of Object.keys(ownedAchievements)) {
|
||||
const value = ownedAchievements[key];
|
||||
if (typeof value === 'object') {
|
||||
nestedAchievements[key] = [];
|
||||
for (const nestedKey of Object.keys(value)) {
|
||||
const valueIsInteger = self.integerTypes.includes(key);
|
||||
let text = nestedKey;
|
||||
if (allAchievements[key] && allAchievements[key][nestedKey]) {
|
||||
text = getText(allAchievements[key][nestedKey]);
|
||||
}
|
||||
nestedAchievements[key].push({
|
||||
key: nestedKey,
|
||||
text,
|
||||
achievementType: key,
|
||||
modified: false,
|
||||
path: `${basePath}.${key}.${nestedKey}`,
|
||||
value: value[nestedKey],
|
||||
valueIsInteger,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const valueIsInteger = self.integerTypes.includes(key);
|
||||
achievements.push({
|
||||
key,
|
||||
text: getText(allAchievements[key]),
|
||||
modified: false,
|
||||
path: `${basePath}.${key}`,
|
||||
value: ownedAchievements[key],
|
||||
valueIsInteger,
|
||||
});
|
||||
}
|
||||
}
|
||||
const { textKey } = achievementItem;
|
||||
if (textKey !== undefined) {
|
||||
return i18n.t(textKey, { count });
|
||||
|
||||
for (const key of Object.keys(allAchievements)) {
|
||||
if (key !== '' && !key.endsWith('UltimateGear') && !key.endsWith('Quest')) {
|
||||
if (ownedAchievements[key] === undefined) {
|
||||
const valueIsInteger = self.integerTypes.includes(key);
|
||||
achievements.push({
|
||||
key,
|
||||
text: getText(allAchievements[key]),
|
||||
modified: false,
|
||||
path: `${basePath}.${key}`,
|
||||
value: valueIsInteger ? 0 : false,
|
||||
valueIsInteger,
|
||||
neverOwned: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const { singularTextKey } = achievementItem;
|
||||
if (singularTextKey !== undefined) {
|
||||
return i18n.t(singularTextKey, { count });
|
||||
}
|
||||
return '';
|
||||
|
||||
self.achievements = achievements;
|
||||
self.nestedAchievements = nestedAchievements;
|
||||
}
|
||||
|
||||
function resetData (self) {
|
||||
collateItemData(self);
|
||||
self.nestedAchievementKeys.forEach(itemType => { self.expandItemType[itemType] = false; });
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -192,34 +241,26 @@ export default {
|
||||
},
|
||||
nestedAchievementKeys: ['quests', 'ultimateGearSets'],
|
||||
integerTypes: ['streak', 'perfect', 'birthday', 'habiticaDays', 'habitSurveys', 'habitBirthdays',
|
||||
'valentine', 'congrats', 'shinySeed', 'goodluck', 'thankyou', 'seafoam', 'snowball', 'quests',
|
||||
'rebirths', 'rebirthLevel', 'greeting', 'spookySparkles', 'nye', 'costumeContests', 'congrats',
|
||||
'getwell', 'beastMasterCount', 'mountMasterCount', 'triadBingoCount',
|
||||
],
|
||||
cardTypes: ['greeting', 'birthday', 'valentine', 'goodluck', 'thankyou', 'greeting', 'nye',
|
||||
'congrats', 'getwell'],
|
||||
'valentine', 'congrats', 'shinySeed', 'goodluck', 'thankyou', 'seafoam', 'snowball', 'quests'],
|
||||
achievements: [],
|
||||
nestedAchievements: {},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
this.resetData();
|
||||
resetData(this);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.resetData();
|
||||
resetData(this);
|
||||
},
|
||||
methods: {
|
||||
async saveItem (item) {
|
||||
await this.saveHero({
|
||||
hero: {
|
||||
_id: this.hero._id,
|
||||
achievementPath: item.path,
|
||||
achievementVal: item.value,
|
||||
},
|
||||
msg: item.path,
|
||||
});
|
||||
// prepare the item's new value and path for being saved
|
||||
this.hero.achievementPath = item.path;
|
||||
this.hero.achievementVal = item.value;
|
||||
|
||||
await this.saveHero({ hero: this.hero, msg: item.path });
|
||||
item.modified = false;
|
||||
},
|
||||
enableValueChange (item) {
|
||||
@@ -229,85 +270,6 @@ export default {
|
||||
item.value = !item.value;
|
||||
}
|
||||
},
|
||||
resetData () {
|
||||
this.collateItemData();
|
||||
this.nestedAchievementKeys.forEach(itemType => { this.expandItemType[itemType] = false; });
|
||||
},
|
||||
collateItemData () {
|
||||
const achievements = [];
|
||||
const nestedAchievements = {};
|
||||
const basePath = 'achievements';
|
||||
const ownedAchievements = this.hero.achievements;
|
||||
const allAchievements = content.achievements;
|
||||
|
||||
const ownedKeys = Object.keys(ownedAchievements).sort();
|
||||
for (const key of ownedKeys) {
|
||||
const value = ownedAchievements[key];
|
||||
let contentKey = key;
|
||||
if (this.cardTypes.indexOf(key) !== -1) {
|
||||
contentKey += 'Cards';
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
nestedAchievements[key] = [];
|
||||
for (const nestedKey of Object.keys(value)) {
|
||||
const valueIsInteger = this.integerTypes.includes(key);
|
||||
let text = nestedKey;
|
||||
if (allAchievements[key] && allAchievements[key][contentKey]) {
|
||||
text = getText(allAchievements[key][contentKey]);
|
||||
}
|
||||
let notes = '';
|
||||
if (allAchievements[key] && allAchievements[key][contentKey]) {
|
||||
notes = getNotes(allAchievements[key][contentKey], ownedAchievements[key]);
|
||||
}
|
||||
nestedAchievements[key].push({
|
||||
key: nestedKey,
|
||||
text,
|
||||
notes,
|
||||
achievementType: key,
|
||||
modified: false,
|
||||
path: `${basePath}.${key}.${nestedKey}`,
|
||||
value: value[nestedKey],
|
||||
valueIsInteger,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const valueIsInteger = this.integerTypes.includes(key);
|
||||
achievements.push({
|
||||
key,
|
||||
text: getText(allAchievements[contentKey]),
|
||||
notes: getNotes(allAchievements[contentKey], ownedAchievements[key]),
|
||||
modified: false,
|
||||
path: `${basePath}.${key}`,
|
||||
value: ownedAchievements[key],
|
||||
valueIsInteger,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const allKeys = Object.keys(allAchievements).sort();
|
||||
|
||||
for (const key of allKeys) {
|
||||
if (key !== '' && !key.endsWith('UltimateGear') && !key.endsWith('Quest')) {
|
||||
const ownedKey = key.replace('Cards', '');
|
||||
if (ownedAchievements[ownedKey] === undefined) {
|
||||
const valueIsInteger = this.integerTypes.includes(ownedKey);
|
||||
achievements.push({
|
||||
key: ownedKey,
|
||||
text: getText(allAchievements[key]),
|
||||
notes: getNotes(allAchievements[key], 0),
|
||||
modified: false,
|
||||
path: `${basePath}.${ownedKey}`,
|
||||
value: valueIsInteger ? 0 : false,
|
||||
valueIsInteger,
|
||||
neverOwned: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.achievements = achievements;
|
||||
this.nestedAchievements = nestedAchievements;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
<template>
|
||||
<form
|
||||
@submit.prevent="saveHero({ hero: {
|
||||
_id: hero._id,
|
||||
contributor: hero.contributor,
|
||||
secret: hero.secret,
|
||||
permissions: hero.permissions,
|
||||
}, msg: 'Contributor details', clearData: true })"
|
||||
>
|
||||
<form @submit.prevent="saveHero({ hero, msg: 'Contributor details', clearData: true })">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
@@ -15,12 +8,6 @@
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Contributor Details
|
||||
<b
|
||||
v-if="hasUnsavedChanges && !expand"
|
||||
class="text-warning float-right"
|
||||
>
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
@@ -117,16 +104,13 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer d-flex align-items-center justify-content-between"
|
||||
class="card-footer"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -206,10 +190,6 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<form
|
||||
@submit.prevent="saveHero({ hero: {
|
||||
_id: hero._id,
|
||||
auth: hero.auth,
|
||||
preferences: hero.preferences,
|
||||
}, msg: 'Authentication' })"
|
||||
>
|
||||
<form @submit.prevent="saveHero({ hero, msg: 'Authentication' })">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
@@ -44,10 +38,7 @@
|
||||
<strong v-else>No</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="cronError"
|
||||
class="form-group row"
|
||||
>
|
||||
<div v-if="cronError" class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">lastCron value:</label>
|
||||
<strong>{{ hero.lastCron | formatDate }}</strong>
|
||||
<br>
|
||||
@@ -62,12 +53,12 @@
|
||||
<div class="col-sm-9 col-form-label">
|
||||
<strong>
|
||||
{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
|
||||
<a
|
||||
<button
|
||||
class="btn btn-warning btn-sm ml-4"
|
||||
@click="resetCron()"
|
||||
>
|
||||
Reset Cron to Yesterday
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
@@ -119,14 +110,13 @@
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">API Token</label>
|
||||
<div class="col-sm-9">
|
||||
<a
|
||||
href="#"
|
||||
<button
|
||||
value="Change API Token"
|
||||
class="btn btn-danger"
|
||||
@click="changeApiToken()"
|
||||
>
|
||||
Change API Token
|
||||
</a>
|
||||
</button>
|
||||
<div
|
||||
v-if="tokenModified"
|
||||
>
|
||||
@@ -278,24 +268,13 @@ export default {
|
||||
return false;
|
||||
},
|
||||
async changeApiToken () {
|
||||
await this.saveHero({
|
||||
hero: {
|
||||
_id: this.hero._id,
|
||||
changeApiToken: true,
|
||||
},
|
||||
msg: 'API Token',
|
||||
});
|
||||
this.hero.changeApiToken = true;
|
||||
await this.saveHero({ hero: this.hero, msg: 'API Token' });
|
||||
this.tokenModified = true;
|
||||
},
|
||||
resetCron () {
|
||||
this.saveHero({
|
||||
hero: {
|
||||
_id: this.hero._id,
|
||||
resetCron: true,
|
||||
},
|
||||
msg: 'Last Cron',
|
||||
clearData: true,
|
||||
});
|
||||
this.hero.resetCron = true;
|
||||
this.saveHero({ hero: this.hero, msg: 'Last Cron', clearData: true });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
:
|
||||
<span :class="{ ownedItem: !item.neverOwned }">{{ item.text }}</span>
|
||||
</span>
|
||||
- {{ itemType }}.{{item.key}} - <i> {{ item.set }}</i>
|
||||
{{ item.set }}
|
||||
|
||||
<div
|
||||
v-if="item.modified"
|
||||
@@ -232,14 +232,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
async saveItem (item) {
|
||||
await this.saveHero({
|
||||
hero: {
|
||||
_id: this.hero._id,
|
||||
purchasedPath: item.path,
|
||||
purchasedVal: item.value,
|
||||
},
|
||||
msg: item.path,
|
||||
});
|
||||
// prepare the item's new value and path for being saved
|
||||
this.hero.purchasedPath = item.path;
|
||||
this.hero.purchasedVal = item.value;
|
||||
|
||||
await this.saveHero({ hero: this.hero, msg: item.path });
|
||||
item.modified = false;
|
||||
},
|
||||
enableValueChange (item) {
|
||||
|
||||
@@ -15,17 +15,10 @@
|
||||
<privileges-and-gems
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
:has-unsaved-changes="hasUnsavedChanges([hero.flags, unModifiedHero.flags],
|
||||
[hero.auth, unModifiedHero.auth],
|
||||
[hero.balance, unModifiedHero.balance],
|
||||
[hero.secret, unModifiedHero.secret])"
|
||||
/>
|
||||
|
||||
<subscription-and-perks
|
||||
:hero="hero"
|
||||
:group-plans="groupPlans"
|
||||
:has-unsaved-changes="hasUnsavedChanges([hero.purchased.plan,
|
||||
unModifiedHero.purchased.plan])"
|
||||
/>
|
||||
|
||||
<cron-and-auth
|
||||
@@ -36,7 +29,6 @@
|
||||
<user-profile
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
:has-unsaved-changes="hasUnsavedChanges([hero.profile, unModifiedHero.profile])"
|
||||
/>
|
||||
|
||||
<party-and-quest
|
||||
@@ -55,12 +47,6 @@
|
||||
:preferences="hero.preferences"
|
||||
/>
|
||||
|
||||
<stats
|
||||
:hero="hero"
|
||||
:has-unsaved-changes="hasUnsavedChanges([hero.stats, unModifiedHero.stats])"
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<items-owned
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
@@ -81,18 +67,8 @@
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<user-history
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<contributor-details
|
||||
:hero="hero"
|
||||
:hasUnsavedChanges="hasUnsavedChanges(
|
||||
[hero.contributor, unModifiedHero.contributor],
|
||||
[hero.permissions, unModifiedHero.permissions],
|
||||
[hero.secret, unModifiedHero.secret],
|
||||
)"
|
||||
:reset-counter="resetCounter"
|
||||
@clear-data="clearData"
|
||||
/>
|
||||
@@ -133,7 +109,6 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import isEqualWith from 'lodash/isEqualWith';
|
||||
import BasicDetails from './basicDetails';
|
||||
import ItemsOwned from './itemsOwned';
|
||||
import CronAndAuth from './cronAndAuth';
|
||||
@@ -146,8 +121,6 @@ import Transactions from './transactions';
|
||||
import SubscriptionAndPerks from './subscriptionAndPerks';
|
||||
import CustomizationsOwned from './customizationsOwned.vue';
|
||||
import Achievements from './achievements.vue';
|
||||
import UserHistory from './userHistory.vue';
|
||||
import Stats from './stats.vue';
|
||||
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
|
||||
@@ -162,8 +135,6 @@ export default {
|
||||
PrivilegesAndGems,
|
||||
ContributorDetails,
|
||||
Transactions,
|
||||
UserHistory,
|
||||
Stats,
|
||||
SubscriptionAndPerks,
|
||||
UserProfile,
|
||||
Achievements,
|
||||
@@ -177,10 +148,8 @@ export default {
|
||||
return {
|
||||
userIdentifier: '',
|
||||
resetCounter: 0,
|
||||
unModifiedHero: {},
|
||||
hero: {},
|
||||
party: {},
|
||||
groupPlans: [],
|
||||
hasParty: false,
|
||||
partyNotExistError: false,
|
||||
adminHasPrivForParty: true,
|
||||
@@ -199,7 +168,6 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
clearData () {
|
||||
this.unModifiedHero = {};
|
||||
this.hero = {};
|
||||
},
|
||||
|
||||
@@ -208,7 +176,6 @@ export default {
|
||||
this.$emit('changeUserIdentifier', id); // change user identifier in Admin Panel's form
|
||||
|
||||
this.hero = await this.$store.dispatch('hall:getHero', { uuid: id });
|
||||
this.unModifiedHero = JSON.parse(JSON.stringify(this.hero));
|
||||
|
||||
if (!this.hero.flags) {
|
||||
this.hero.flags = {
|
||||
@@ -239,38 +206,8 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hero.purchased.plan.planId === 'group_plan_auto') {
|
||||
try {
|
||||
this.groupPlans = await this.$store.dispatch('hall:getHeroGroupPlans', { heroId: this.hero._id });
|
||||
} catch (e) {
|
||||
this.groupPlans = [];
|
||||
}
|
||||
}
|
||||
|
||||
this.resetCounter += 1; // tell child components to reinstantiate from scratch
|
||||
},
|
||||
hasUnsavedChanges (...comparisons) {
|
||||
for (const index in comparisons) {
|
||||
if (index && comparisons[index]) {
|
||||
const objs = comparisons[index];
|
||||
const obj1 = objs[0];
|
||||
const obj2 = objs[1];
|
||||
if (!isEqualWith(obj1, obj2, (x, y) => {
|
||||
if (typeof x === 'object' && typeof y === 'object') {
|
||||
return undefined;
|
||||
}
|
||||
if (x === false && y === undefined) {
|
||||
// Special case for checkboxes
|
||||
return true;
|
||||
}
|
||||
return x == y; // eslint-disable-line eqeqeq
|
||||
})) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -269,19 +269,16 @@ export default {
|
||||
methods: {
|
||||
async saveItem (item) {
|
||||
// prepare the item's new value and path for being saved
|
||||
const toSave = {
|
||||
_id: this.hero._id,
|
||||
};
|
||||
toSave.itemPath = item.path;
|
||||
this.hero.itemPath = item.path;
|
||||
if (item.value === null) {
|
||||
toSave.itemVal = 'null';
|
||||
this.hero.itemVal = 'null';
|
||||
} else if (item.value === false) {
|
||||
toSave.itemVal = 'false';
|
||||
this.hero.itemVal = 'false';
|
||||
} else {
|
||||
toSave.itemVal = item.value;
|
||||
this.hero.itemVal = item.value;
|
||||
}
|
||||
|
||||
await this.saveHero({ hero: toSave, msg: item.key });
|
||||
await this.saveHero({ hero: this.hero, msg: item.key });
|
||||
item.neverOwned = false;
|
||||
item.modified = false;
|
||||
},
|
||||
|
||||
@@ -31,41 +31,22 @@
|
||||
v-html="questErrors"
|
||||
></p>
|
||||
</div>
|
||||
<div v-if="userHasParty">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Party ID
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
{{ groupPartyData._id }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Estimated Member Count
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
{{ groupPartyData.memberCount }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Leader
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
|
||||
<div>
|
||||
Party:
|
||||
<span v-if="userHasParty">
|
||||
yes: party ID {{ groupPartyData._id }},
|
||||
member count {{ groupPartyData.memberCount }} (may be wrong)
|
||||
<br>
|
||||
<span v-if="userIsPartyLeader">User is the party leader</span>
|
||||
<span v-else>Party leader is
|
||||
<router-link :to="{'name': 'userProfile', 'params': {'userId': groupPartyData.leader}}">
|
||||
{{ groupPartyData.leader }}
|
||||
</router-link>
|
||||
</span>
|
||||
</strong>
|
||||
</span>
|
||||
<span v-else>no</span>
|
||||
</div>
|
||||
<div
|
||||
class="btn btn-danger"
|
||||
@click="removeFromParty()">Remove from Party</div>
|
||||
</div>
|
||||
<strong v-else>User is not in a party.</strong>
|
||||
<div class="subsection-start">
|
||||
<p v-html="questStatus"></p>
|
||||
</div>
|
||||
@@ -75,7 +56,6 @@
|
||||
|
||||
<script>
|
||||
import * as quests from '@/../../common/script/content/quests';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
function determineQuestStatus (self) {
|
||||
// Quest data is in the user doc and party doc. They can be out of sync.
|
||||
@@ -291,7 +271,6 @@ function resetData (self) {
|
||||
}
|
||||
|
||||
export default {
|
||||
mixins: [saveHero],
|
||||
props: {
|
||||
resetCounter: {
|
||||
type: Number,
|
||||
@@ -339,14 +318,5 @@ export default {
|
||||
mounted () {
|
||||
resetData(this);
|
||||
},
|
||||
methods: {
|
||||
removeFromParty () {
|
||||
this.saveHero({
|
||||
hero: { _id: this.userId, removeFromParty: true },
|
||||
msg: 'Removed from party',
|
||||
reloadData: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<form @submit.prevent="saveHero({hero: {
|
||||
_id: hero._id,
|
||||
flags: hero.flags,
|
||||
balance: hero.balance,
|
||||
auth: hero.auth,
|
||||
secret: hero.secret,
|
||||
}, msg: 'Privileges or Gems or Moderation Notes'})">
|
||||
<form @submit.prevent="saveHero({hero, msg: 'Privileges or Gems or Moderation Notes'})">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
@@ -14,9 +8,6 @@
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Privileges, Gem Balance
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
@@ -126,16 +117,13 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer d-flex align-items-center justify-content-between"
|
||||
class="card-footer"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -181,10 +169,6 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
<template>
|
||||
<div class="form-group row">
|
||||
<label
|
||||
class="col-sm-3 col-form-label"
|
||||
:class="color"
|
||||
>{{ label }}</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
:value="value"
|
||||
class="form-control"
|
||||
type="number"
|
||||
:step="step"
|
||||
:max="max"
|
||||
:min="min"
|
||||
@input="$emit('input', parseInt($event.target.value, 10))"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.about-row {
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.red-label {
|
||||
color: $red_100;
|
||||
}
|
||||
.blue-label {
|
||||
color: $blue_100;
|
||||
}
|
||||
.purple-label {
|
||||
color: $purple_300;
|
||||
}
|
||||
.yellow-label {
|
||||
color: $yellow_50;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'input',
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'text-label',
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
step: {
|
||||
type: String,
|
||||
default: 'any',
|
||||
},
|
||||
min: {
|
||||
},
|
||||
max: {
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,286 +0,0 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitClicked()">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Stats
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
<stats-row
|
||||
label="Health"
|
||||
color="red-label"
|
||||
:max="maxHealth"
|
||||
v-model="hero.stats.hp" />
|
||||
<stats-row
|
||||
label="Experience"
|
||||
color="yellow-label"
|
||||
min="0"
|
||||
:max="maxFieldHardCap"
|
||||
v-model="hero.stats.exp" />
|
||||
<stats-row
|
||||
label="Mana"
|
||||
color="blue-label"
|
||||
min="0"
|
||||
:max="maxFieldHardCap"
|
||||
v-model="hero.stats.mp" />
|
||||
<stats-row
|
||||
label="Level"
|
||||
step="1"
|
||||
min="0"
|
||||
:max="maxLevelHardCap"
|
||||
v-model="hero.stats.lvl" />
|
||||
<stats-row
|
||||
label="Gold"
|
||||
min="0"
|
||||
:max="maxFieldHardCap"
|
||||
v-model="hero.stats.gp" />
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Selected Class</label>
|
||||
<div class="col-sm-9">
|
||||
<select
|
||||
id="selectedClass"
|
||||
v-model="hero.stats.class"
|
||||
class="form-control"
|
||||
:disabled="hero.stats.lvl < 10"
|
||||
>
|
||||
<option value="warrior">Warrior</option>
|
||||
<option value="wizard">Mage</option>
|
||||
<option value="healer">Healer</option>
|
||||
<option value="rogue">Rogue</option>
|
||||
</select>
|
||||
<small>
|
||||
When changing class, players usually need stat points deallocated as well.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Stat Points</h3>
|
||||
<stats-row
|
||||
label="Unallocated"
|
||||
min="0"
|
||||
step="1"
|
||||
:max="maxStatPoints"
|
||||
v-model="hero.stats.points" />
|
||||
<stats-row
|
||||
label="Strength"
|
||||
color="red-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.str" />
|
||||
<stats-row
|
||||
label="Intelligence"
|
||||
color="blue-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.int" />
|
||||
<stats-row
|
||||
label="Perception"
|
||||
color="purple-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.per" />
|
||||
<stats-row
|
||||
label="Constitution"
|
||||
color="yellow-label"
|
||||
min="0"
|
||||
:max="maxStatPoints"
|
||||
step="1"
|
||||
v-model="hero.stats.con" />
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-warning btn-sm"
|
||||
@click="deallocateStatPoints">
|
||||
Deallocate all stat points
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row" v-if="statPointsIncorrect">
|
||||
<div class="offset-sm-3 col-sm-9 text-danger">
|
||||
Error: Sum of stat points should equal the users level
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Buffs</h3>
|
||||
<stats-row
|
||||
label="Strength"
|
||||
color="red-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.str" />
|
||||
<stats-row
|
||||
label="Intelligence"
|
||||
color="blue-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.int" />
|
||||
<stats-row
|
||||
label="Perception"
|
||||
color="purple-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.per" />
|
||||
<stats-row
|
||||
label="Constitution"
|
||||
color="yellow-label"
|
||||
min="0"
|
||||
step="1"
|
||||
v-model="hero.stats.buffs.con" />
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-warning btn-sm"
|
||||
@click="resetBuffs">
|
||||
Reset Buffs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer d-flex align-items-center justify-content-between"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.about-row {
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import {
|
||||
MAX_HEALTH,
|
||||
MAX_STAT_POINTS,
|
||||
MAX_LEVEL_HARD_CAP,
|
||||
MAX_FIELD_HARD_CAP,
|
||||
} from '@/../../common/script/constants';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
import { mapState } from '@/libs/store';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
|
||||
import StatsRow from './stats-row';
|
||||
|
||||
function resetData (self) {
|
||||
self.expand = false;
|
||||
}
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
components: {
|
||||
StatsRow,
|
||||
},
|
||||
mixins: [
|
||||
userStateMixin,
|
||||
saveHero,
|
||||
],
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
statPointsIncorrect () {
|
||||
if (this.hero.stats.lvl >= 10) {
|
||||
return (parseInt(this.hero.stats.points, 10)
|
||||
+ parseInt(this.hero.stats.str, 10)
|
||||
+ parseInt(this.hero.stats.int, 10)
|
||||
+ parseInt(this.hero.stats.per, 10)
|
||||
+ parseInt(this.hero.stats.con, 10)
|
||||
) !== this.hero.stats.lvl;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
props: {
|
||||
resetCounter: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
hero: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
maxHealth: MAX_HEALTH,
|
||||
maxStatPoints: MAX_STAT_POINTS,
|
||||
maxLevelHardCap: MAX_LEVEL_HARD_CAP,
|
||||
maxFieldHardCap: MAX_FIELD_HARD_CAP,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
resetData(this);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
resetData(this);
|
||||
},
|
||||
methods: {
|
||||
submitClicked () {
|
||||
if (this.statPointsIncorrect) {
|
||||
return;
|
||||
}
|
||||
this.saveHero({
|
||||
hero: {
|
||||
_id: this.hero._id,
|
||||
stats: this.hero.stats,
|
||||
},
|
||||
msg: 'Stats',
|
||||
});
|
||||
},
|
||||
resetBuffs () {
|
||||
this.hero.stats.buffs = {
|
||||
str: 0,
|
||||
int: 0,
|
||||
per: 0,
|
||||
con: 0,
|
||||
};
|
||||
},
|
||||
deallocateStatPoints () {
|
||||
this.hero.stats.points = this.hero.stats.lvl;
|
||||
this.hero.stats.str = 0;
|
||||
this.hero.stats.int = 0;
|
||||
this.hero.stats.per = 0;
|
||||
this.hero.stats.con = 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,135 +1,30 @@
|
||||
<template>
|
||||
<form
|
||||
@submit.prevent="saveHero({ hero: {
|
||||
_id: hero._id,
|
||||
purchased: hero.purchased
|
||||
}, msg: 'Subscription Perks' })"
|
||||
>
|
||||
<form @submit.prevent="saveHero({ hero, msg: 'Subscription Perks' })">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header"
|
||||
@click="expand = !expand">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{ 'open': expand }"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Subscription, Monthly Perks
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
<div
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Payment method:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input v-model="hero.purchased.plan.paymentMethod"
|
||||
class="form-control"
|
||||
type="text"
|
||||
v-if="!isRegularPaymentMethod"
|
||||
>
|
||||
<select
|
||||
v-else
|
||||
v-model="hero.purchased.plan.paymentMethod"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<option value="groupPlan">Group Plan</option>
|
||||
<option value="Stripe">Stripe</option>
|
||||
<option value="Apple">Apple</option>
|
||||
<option value="Google">Google</option>
|
||||
<option value="Amazon Payments">Amazon</option>
|
||||
<option value="PayPal">PayPal</option>
|
||||
<option value="Gift">Gift</option>
|
||||
<option value="">Clear out</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="hero.purchased.plan.paymentMethod">
|
||||
Payment method:
|
||||
<strong>{{ hero.purchased.plan.paymentMethod }}</strong>
|
||||
</div>
|
||||
<div
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Payment schedule:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input v-model="hero.purchased.plan.planId"
|
||||
class="form-control"
|
||||
type="text"
|
||||
v-if="!isRegularPlanId"
|
||||
>
|
||||
<select
|
||||
v-else
|
||||
v-model="hero.purchased.plan.planId"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<option value="basic_earned">Monthly recurring</option>
|
||||
<option value="basic_3mo">3 Months recurring</option>
|
||||
<option value="basic_6mo">6 Months recurring</option>
|
||||
<option value="basic_12mo">12 Months recurring</option>
|
||||
<option value="group_monthly">Group Plan (legacy)</option>
|
||||
<option value="group_plan_auto">Group Plan (auto)</option>
|
||||
<option value="">Clear out</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="hero.purchased.plan.planId">
|
||||
Payment schedule ("basic-earned" is monthly):
|
||||
<strong>{{ hero.purchased.plan.planId }}</strong>
|
||||
</div>
|
||||
<div
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Customer ID:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.purchased.plan.customerId"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row"
|
||||
v-if="hero.purchased.plan.planId === 'group_plan_auto'">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Group Plan Memberships:
|
||||
</label>
|
||||
<div class="col-sm-9 col-form-label">
|
||||
<loading-spinner
|
||||
v-if="!groupPlans"
|
||||
dark-color=true
|
||||
/>
|
||||
<b
|
||||
v-else-if="groupPlans.length === 0"
|
||||
class="text-danger col-form-label"
|
||||
>User is not part of an active group plan!</b>
|
||||
<div
|
||||
v-else
|
||||
v-for="group in groupPlans"
|
||||
:key="group._id"
|
||||
class="card mb-2">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{{ group.name }}
|
||||
<small class="float-right">{{ group._id }}</small>
|
||||
</h6>
|
||||
<p class="card-text">
|
||||
<strong>Leader: </strong>
|
||||
<a
|
||||
v-if="group.leader !== hero._id"
|
||||
@click="switchUser(group.leader)"
|
||||
>{{ group.leader }}</a>
|
||||
<strong v-else class="text-success">This user</strong>
|
||||
</p>
|
||||
<p class="card-text">
|
||||
<strong>Members: </strong> {{ group.memberCount }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hero.purchased.plan.planId == 'group_plan_auto'">
|
||||
Group plan ID:
|
||||
<strong>{{ hero.purchased.plan.owner }}</strong>
|
||||
</div>
|
||||
<div
|
||||
v-if="hero.purchased.plan.dateCreated"
|
||||
@@ -190,18 +85,8 @@
|
||||
<strong class="input-group-text">
|
||||
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
|
||||
</strong>
|
||||
<a class="btn btn-danger"
|
||||
href="#"
|
||||
v-b-modal.sub_termination_modal
|
||||
v-if="!hero.purchased.plan.dateTerminated && hero.purchased.plan.planId">
|
||||
Terminate
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<small v-if="!hero.purchased.plan.dateTerminated
|
||||
&& hero.purchased.plan.planId" class="text-success">
|
||||
The subscription does not have a termination date and is active.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
@@ -216,35 +101,6 @@
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
<small class="text-secondary">
|
||||
Cumulative subscribed months across subscription periods.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Extra months:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="hero.purchased.plan.extraMonths"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
step="any"
|
||||
>
|
||||
<div class="input-group-append">
|
||||
<a class="btn btn-warning"
|
||||
@click="applyExtraMonths"
|
||||
v-if="hero.purchased.plan.dateTerminated && hero.purchased.plan.extraMonths > 0">
|
||||
Apply Credit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-secondary">
|
||||
Additional credit that is applied if a subscription is cancelled.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
@@ -318,6 +174,10 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hero.purchased.plan.extraMonths > 0">
|
||||
Additional credit (applied upon cancellation):
|
||||
<strong>{{ hero.purchased.plan.extraMonths }}</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Mystery Items:
|
||||
@@ -339,64 +199,18 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row"
|
||||
v-if="!isConvertingToGroupPlan && hero.purchased.plan.planId !== 'group_plan_auto'">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
@click="beginGroupPlanConvert">
|
||||
Begin converting to group plan subscription
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row"
|
||||
v-if="isConvertingToGroupPlan">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Group Plan group ID:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="groupPlanID"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer d-flex align-items-center justify-content-between"
|
||||
class="card-footer"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
@click="saveClicked"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
<b-modal id="sub_termination_modal" title="Set Termination Date">
|
||||
<p>
|
||||
You can set the sub benefit termination date to today or to the last
|
||||
day of the current billing cycle. Any extra subscription credit will
|
||||
then be processed and automatically added onto the selected date.
|
||||
</p>
|
||||
<template #modal-footer>
|
||||
<div class="mt-3 btn btn-secondary" @click="$bvModal.hide('sub_termination_modal')">
|
||||
Close
|
||||
</div>
|
||||
<div class="mt-3 btn btn-danger" @click="terminateSubscription()">
|
||||
Set to Today
|
||||
</div>
|
||||
<div class="mt-3 btn btn-danger" @click="terminateSubscription(todayWithRemainingCycle)">
|
||||
Set to {{ todayWithRemainingCycle.utc().format('MM/DD/YYYY') }}
|
||||
</div>
|
||||
</template>
|
||||
</b-modal>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -417,38 +231,21 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import isUUID from 'validator/es/lib/isUUID';
|
||||
import moment from 'moment';
|
||||
import { getPlanContext } from '@/../../common/script/cron';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
import subscriptionBlocks from '../../../../../common/script/content/subscriptionBlocks';
|
||||
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
||||
|
||||
export default {
|
||||
mixins: [saveHero],
|
||||
components: {
|
||||
LoadingSpinner,
|
||||
},
|
||||
props: {
|
||||
hero: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
groupPlans: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
isConvertingToGroupPlan: false,
|
||||
groupPlanID: '',
|
||||
subscriptionBlocks,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -458,30 +255,6 @@ export default {
|
||||
if (!currentPlanContext.nextHourglassDate) return 'N/A';
|
||||
return currentPlanContext.nextHourglassDate.format('MMMM YYYY');
|
||||
},
|
||||
isRegularPlanId () {
|
||||
return this.subscriptionBlocks[this.hero.purchased.plan.planId] !== undefined;
|
||||
},
|
||||
isRegularPaymentMethod () {
|
||||
return [
|
||||
'groupPlan',
|
||||
'Group Plan',
|
||||
'Stripe',
|
||||
'Apple',
|
||||
'Google',
|
||||
'Amazon Payments',
|
||||
'PayPal',
|
||||
'Gift',
|
||||
].includes(this.hero.purchased.plan.paymentMethod);
|
||||
},
|
||||
todayWithRemainingCycle () {
|
||||
const now = moment();
|
||||
const monthCount = subscriptionBlocks[this.hero.purchased.plan.planId].months;
|
||||
const terminationDate = moment(this.hero.purchased.plan.dateCurrentTypeCreated || new Date());
|
||||
while (terminationDate.isBefore(now)) {
|
||||
terminationDate.add(monthCount, 'months');
|
||||
}
|
||||
return terminationDate;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dateFormat (date) {
|
||||
@@ -490,46 +263,6 @@ export default {
|
||||
}
|
||||
return moment(date).format('YYYY/MM/DD');
|
||||
},
|
||||
terminateSubscription (terminationDate) {
|
||||
if (terminationDate) {
|
||||
this.hero.purchased.plan.dateTerminated = terminationDate.utc().format();
|
||||
} else {
|
||||
this.hero.purchased.plan.dateTerminated = moment(new Date()).utc().format();
|
||||
}
|
||||
this.applyExtraMonths();
|
||||
this.saveHero({ hero: this.hero, msg: 'Subscription Termination', reloadData: true });
|
||||
},
|
||||
applyExtraMonths () {
|
||||
if (this.hero.purchased.plan.extraMonths > 0 || this.hero.purchased.plan.extraMonths !== '0') {
|
||||
const date = moment(this.hero.purchased.plan.dateTerminated || new Date());
|
||||
const extraMonths = Math.max(this.hero.purchased.plan.extraMonths, 0);
|
||||
const extraDays = Math.ceil(30.5 * extraMonths);
|
||||
this.hero.purchased.plan.dateTerminated = date.add(extraDays, 'days').utc().format();
|
||||
this.hero.purchased.plan.extraMonths = 0;
|
||||
}
|
||||
},
|
||||
beginGroupPlanConvert () {
|
||||
this.isConvertingToGroupPlan = true;
|
||||
this.hero.purchased.plan.owner = '';
|
||||
},
|
||||
saveClicked (e) {
|
||||
e.preventDefault();
|
||||
if (this.isConvertingToGroupPlan) {
|
||||
if (!isUUID(this.groupPlanID)) {
|
||||
alert('Invalid group ID');
|
||||
return;
|
||||
}
|
||||
this.hero.purchased.plan.convertToGroupPlan = this.groupPlanID;
|
||||
this.saveHero({ hero: this.hero, msg: 'Group Plan Subscription', reloadData: true });
|
||||
} else {
|
||||
this.saveHero({ hero: this.hero, msg: 'Subscription Perks', reloadData: true });
|
||||
}
|
||||
},
|
||||
switchUser (id) {
|
||||
if (window.confirm('Switch to this user?')) {
|
||||
this.$emit('changeUserIdentifier', id);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
<template>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="toggleHistoryOpen"
|
||||
>
|
||||
User History
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
<div>
|
||||
<div class="clearfix">
|
||||
<div class="mb-4 float-left">
|
||||
<button
|
||||
class="page-header btn-flat tab-button textCondensed"
|
||||
:class="{'active': selectedTab === 'armoire'}"
|
||||
@click="selectTab('armoire')"
|
||||
>
|
||||
Armoire
|
||||
</button>
|
||||
<button
|
||||
class="page-header btn-flat tab-button textCondensed"
|
||||
:class="{'active': selectedTab === 'questInvites'}"
|
||||
@click="selectTab('questInvites')"
|
||||
>
|
||||
Quest Invitations
|
||||
</button>
|
||||
<button
|
||||
class="page-header btn-flat tab-button textCondensed"
|
||||
:class="{'active': selectedTab === 'cron'}"
|
||||
@click="selectTab('cron')"
|
||||
>
|
||||
Cron
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div
|
||||
v-if="selectedTab === 'armoire'"
|
||||
class="col-12"
|
||||
>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th
|
||||
v-once
|
||||
>
|
||||
{{ $t('timestamp') }}
|
||||
</th>
|
||||
<th v-once>
|
||||
Client
|
||||
</th>
|
||||
<th
|
||||
v-once
|
||||
>
|
||||
Received
|
||||
</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="entry in armoire"
|
||||
:key="entry.timestamp"
|
||||
>
|
||||
<td>
|
||||
<span
|
||||
v-b-tooltip.hover="entry.timestamp"
|
||||
>{{ entry.timestamp | timeAgo }}</span>
|
||||
</td>
|
||||
<td>{{ entry.client }}</td>
|
||||
<td>{{ entry.reward }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedTab === 'questInvites'"
|
||||
class="col-12"
|
||||
>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th
|
||||
v-once
|
||||
>
|
||||
{{ $t('timestamp') }}
|
||||
</th>
|
||||
<th v-once>
|
||||
Client
|
||||
</th>
|
||||
<th v-once>
|
||||
Quest Key
|
||||
</th>
|
||||
<th v-once>
|
||||
Response
|
||||
</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="entry in questInviteResponses"
|
||||
:key="entry.timestamp"
|
||||
>
|
||||
<td>
|
||||
<span
|
||||
v-b-tooltip.hover="entry.timestamp"
|
||||
>{{ entry.timestamp | timeAgo }}</span>
|
||||
</td>
|
||||
<td>{{ entry.client }}</td>
|
||||
<td>{{ entry.quest }}</td>
|
||||
<td>{{ questInviteResponseText(entry.response) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedTab === 'cron'"
|
||||
class="col-12"
|
||||
>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th
|
||||
v-once
|
||||
>
|
||||
{{ $t('timestamp') }}
|
||||
</th>
|
||||
<th v-once>
|
||||
Client
|
||||
</th>
|
||||
<th v-once>
|
||||
Checkin Count
|
||||
</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="entry in cron"
|
||||
:key="entry.timestamp"
|
||||
>
|
||||
<td>
|
||||
<span
|
||||
v-b-tooltip.hover="entry.timestamp"
|
||||
>{{ entry.timestamp | timeAgo }}</span>
|
||||
</td>
|
||||
<td>{{ entry.client }}</td>
|
||||
<td>{{ entry.checkinCount }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.page-header.btn-flat {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
height: 2rem;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
font-stretch: condensed;
|
||||
line-height: 1.33;
|
||||
letter-spacing: normal;
|
||||
color: $gray-10;
|
||||
|
||||
margin-right: 1.125rem;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-bottom: 2.5rem;
|
||||
|
||||
&.active, &:hover {
|
||||
color: $purple-300;
|
||||
box-shadow: 0px -0.25rem 0px $purple-300 inset;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
|
||||
export default {
|
||||
filters: {
|
||||
timeAgo (value) {
|
||||
return moment(value).fromNow();
|
||||
},
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
props: {
|
||||
hero: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
resetCounter: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
selectedTab: 'armoire',
|
||||
armoire: [],
|
||||
questInviteResponses: [],
|
||||
cron: [],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
if (this.expand) {
|
||||
this.retrieveUserHistory();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectTab (type) {
|
||||
this.selectedTab = type;
|
||||
},
|
||||
async toggleHistoryOpen () {
|
||||
this.expand = !this.expand;
|
||||
if (this.expand) {
|
||||
this.retrieveUserHistory();
|
||||
}
|
||||
},
|
||||
async retrieveUserHistory () {
|
||||
const history = await this.$store.dispatch('adminPanel:getUserHistory', { userIdentifier: this.hero._id });
|
||||
this.armoire = history.armoire;
|
||||
this.questInviteResponses = history.questInviteResponses;
|
||||
this.cron = history.cron;
|
||||
},
|
||||
questInviteResponseText (response) {
|
||||
if (response === 'accept') {
|
||||
return 'Accepted';
|
||||
}
|
||||
if (response === 'reject') {
|
||||
return 'Rejected';
|
||||
}
|
||||
if (response === 'leave') {
|
||||
return 'Left active quest';
|
||||
}
|
||||
if (response === 'invite') {
|
||||
return 'Accepted as owner';
|
||||
}
|
||||
if (response === 'abort') {
|
||||
return 'Aborted by owner';
|
||||
}
|
||||
if (response === 'abortByLeader') {
|
||||
return 'Aborted by party leader';
|
||||
}
|
||||
if (response === 'cancel') {
|
||||
return 'Cancelled before start';
|
||||
}
|
||||
if (response === 'cancelByLeader') {
|
||||
return 'Cancelled before start by party leader';
|
||||
}
|
||||
return response;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,10 +1,5 @@
|
||||
<template>
|
||||
<form
|
||||
@submit.prevent="saveHero({hero: {
|
||||
_id: hero._id,
|
||||
profile: hero.profile
|
||||
}, msg: 'Users Profile'})"
|
||||
>
|
||||
<form @submit.prevent="saveHero({hero, msg: 'Users Profile'})">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
@@ -13,9 +8,6 @@
|
||||
@click="expand = !expand"
|
||||
>
|
||||
User Profile
|
||||
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
@@ -59,16 +51,13 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer d-flex align-items-center justify-content-between"
|
||||
class="card-footer"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||
Unsaved changes
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -112,10 +101,6 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasUnsavedChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
<h3>{{ $t('footerCompany') }}</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="mailto:admin@habitica.com">
|
||||
<router-link to="/static/contact">
|
||||
{{ $t('contactUs') }}
|
||||
</a>
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/static/press-kit">
|
||||
@@ -55,9 +55,9 @@
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
@click="showBailey()"
|
||||
>
|
||||
{{ $t('oldNews') }}
|
||||
href="https://habitica.fandom.com/wiki/Whats_New"
|
||||
target="_blank"
|
||||
>{{ $t('oldNews') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -80,7 +80,7 @@
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica"
|
||||
href="https://habitica.fandom.com/wiki/Contributing_to_Habitica"
|
||||
target="_blank"
|
||||
>{{ $t('companyContribute') }}
|
||||
</a>
|
||||
@@ -158,6 +158,13 @@
|
||||
>{{ $t('guidanceForBlacksmiths') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://habitica.fandom.com/wiki/Extensions,_Add-Ons,_and_Customizations"
|
||||
target="_blank"
|
||||
>{{ $t('communityExtensions') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -198,12 +205,12 @@
|
||||
</a>
|
||||
<a
|
||||
class="social-circle"
|
||||
href="https://bsky.app/profile/habitica.com"
|
||||
href="https://twitter.com/habitica/"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
class="social-icon svg-icon bluesky"
|
||||
v-html="icons.bluesky"
|
||||
class="social-icon svg-icon twitter"
|
||||
v-html="icons.twitter"
|
||||
></div>
|
||||
</a>
|
||||
<a
|
||||
@@ -511,7 +518,7 @@ footer {
|
||||
background-color: $gray-500;
|
||||
color: $gray-50;
|
||||
padding: 32px 142px 40px;
|
||||
a, a:not([href]) {
|
||||
a {
|
||||
color: $gray-50;
|
||||
}
|
||||
a:hover {
|
||||
@@ -800,7 +807,7 @@ h3 {
|
||||
}
|
||||
}
|
||||
|
||||
.bluesky svg {
|
||||
.twitter svg {
|
||||
background-color: #e1e0e3;
|
||||
fill: #878190;
|
||||
height: 24px;
|
||||
@@ -839,7 +846,7 @@ import Vue from 'vue';
|
||||
|
||||
// images
|
||||
import melior from '@/assets/svg/melior.svg';
|
||||
import bluesky from '@/assets/svg/bluesky.svg';
|
||||
import twitter from '@/assets/svg/twitter.svg';
|
||||
import facebook from '@/assets/svg/facebook.svg';
|
||||
import instagram from '@/assets/svg/instagram.svg';
|
||||
import tumblr from '@/assets/svg/tumblr.svg';
|
||||
@@ -871,7 +878,7 @@ export default {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
melior,
|
||||
bluesky,
|
||||
twitter,
|
||||
facebook,
|
||||
instagram,
|
||||
tumblr,
|
||||
@@ -944,28 +951,24 @@ export default {
|
||||
},
|
||||
async jumpTime (amount) {
|
||||
const response = await axios.post('/api/v4/debug/jump-time', { offsetDays: amount });
|
||||
setTimeout(() => {
|
||||
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);
|
||||
}, 1000);
|
||||
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);
|
||||
setTimeout(() => {
|
||||
Vue.config.clock.restore();
|
||||
Vue.config.clock = sinon.useFakeTimers({
|
||||
now: time,
|
||||
shouldAdvanceTime: true,
|
||||
});
|
||||
this.lastTimeJump = response.data.data.time;
|
||||
this.triggerGetWorldState(true);
|
||||
}, 1000);
|
||||
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
|
||||
@@ -993,6 +996,7 @@ export default {
|
||||
async bossRage () {
|
||||
await axios.post('/api/v4/debug/boss-rage');
|
||||
},
|
||||
|
||||
async makeAdmin () {
|
||||
await axios.post('/api/v4/debug/make-admin');
|
||||
// @TODO: Notification.text('You are now an admin!
|
||||
@@ -1002,9 +1006,6 @@ export default {
|
||||
donate () {
|
||||
this.$root.$emit('bv::show::modal', 'buy-gems', { alreadyTracked: true });
|
||||
},
|
||||
showBailey () {
|
||||
this.$root.$emit('bv::show::modal', 'new-stuff');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,98 +1,100 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="member.preferences"
|
||||
class="avatar"
|
||||
:style="{width, height, paddingTop}"
|
||||
:class="topLevelClassList"
|
||||
@click.prevent="castEnd()"
|
||||
>
|
||||
<div class="avatar-wrapper">
|
||||
<div
|
||||
class="character-sprites"
|
||||
:style="{margin: spritesMargin}"
|
||||
v-if="member.preferences"
|
||||
class="avatar"
|
||||
:style="{width, height, paddingTop}"
|
||||
:class="topLevelClassList"
|
||||
@click.prevent="castEnd()"
|
||||
>
|
||||
<template v-if="!avatarOnly">
|
||||
<!-- Mount Body-->
|
||||
<span
|
||||
v-if="member.items.currentMount"
|
||||
:class="'Mount_Body_' + member.items.currentMount"
|
||||
></span>
|
||||
</template>
|
||||
<!-- Buffs that cause visual changes to avatar: Snowman, Ghost, Flower, etc-->
|
||||
<template v-for="(klass, item) in visualBuffs">
|
||||
<span
|
||||
v-if="member.stats.buffs[item] && showVisualBuffs"
|
||||
:key="item"
|
||||
:class="klass"
|
||||
></span>
|
||||
</template>
|
||||
<!-- Show flower ALL THE TIME!!!-->
|
||||
<!-- See https://github.com/HabitRPG/habitica/issues/7133-->
|
||||
<span :class="'hair_flower_' + member.preferences.hair.flower"></span>
|
||||
<!-- Show avatar only if not currently affected by visual buff-->
|
||||
<template v-if="showAvatar()">
|
||||
<span :class="['chair_' + member.preferences.chair, specialMountClass]"></span>
|
||||
<span :class="[getGearClass('back'), specialMountClass]"></span>
|
||||
<span :class="[skinClass, specialMountClass]"></span>
|
||||
<!-- eslint-disable max-len-->
|
||||
<span
|
||||
:class="[shirtClass, specialMountClass]"
|
||||
></span>
|
||||
<!-- eslint-enable max-len-->
|
||||
<span :class="['head_0', specialMountClass]"></span>
|
||||
<!-- eslint-disable max-len-->
|
||||
<span :class="[member.preferences.size + '_' + getGearClass('armor'), specialMountClass]"></span>
|
||||
<!-- eslint-enable max-len-->
|
||||
<span :class="[getGearClass('back_collar'), specialMountClass]"></span>
|
||||
<template
|
||||
v-for="type in ['bangs', 'base', 'mustache', 'beard']"
|
||||
>
|
||||
<div
|
||||
class="character-sprites"
|
||||
:style="{margin: spritesMargin}"
|
||||
>
|
||||
<template v-if="!avatarOnly">
|
||||
<!-- Mount Body-->
|
||||
<span
|
||||
:key="type"
|
||||
:class="[hairClass(type), specialMountClass]"
|
||||
v-if="member.items.currentMount"
|
||||
:class="'Mount_Body_' + member.items.currentMount"
|
||||
></span>
|
||||
</template>
|
||||
<span :class="[getGearClass('body'), specialMountClass]"></span>
|
||||
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
|
||||
<span :class="[getGearClass('head'), specialMountClass]"></span>
|
||||
<span :class="[getGearClass('headAccessory'), specialMountClass]"></span>
|
||||
<!-- Buffs that cause visual changes to avatar: Snowman, Ghost, Flower, etc-->
|
||||
<template v-for="(klass, item) in visualBuffs">
|
||||
<span
|
||||
v-if="member.stats.buffs[item] && showVisualBuffs"
|
||||
:key="item"
|
||||
:class="klass"
|
||||
></span>
|
||||
</template>
|
||||
<!-- Show flower ALL THE TIME!!!-->
|
||||
<!-- See https://github.com/HabitRPG/habitica/issues/7133-->
|
||||
<span :class="'hair_flower_' + member.preferences.hair.flower"></span>
|
||||
<!-- Show avatar only if not currently affected by visual buff-->
|
||||
<template v-if="showAvatar()">
|
||||
<span :class="['chair_' + member.preferences.chair, specialMountClass]"></span>
|
||||
<span :class="[getGearClass('back'), specialMountClass]"></span>
|
||||
<span :class="[skinClass, specialMountClass]"></span>
|
||||
<!-- eslint-disable max-len-->
|
||||
<span
|
||||
:class="[shirtClass, specialMountClass]"
|
||||
></span>
|
||||
<!-- eslint-enable max-len-->
|
||||
<span :class="['head_0', specialMountClass]"></span>
|
||||
<!-- eslint-disable max-len-->
|
||||
<span :class="[member.preferences.size + '_' + getGearClass('armor'), specialMountClass]"></span>
|
||||
<!-- eslint-enable max-len-->
|
||||
<span :class="[getGearClass('back_collar'), specialMountClass]"></span>
|
||||
<template
|
||||
v-for="type in ['bangs', 'base', 'mustache', 'beard']"
|
||||
>
|
||||
<span
|
||||
:key="type"
|
||||
:class="[hairClass(type), specialMountClass]"
|
||||
></span>
|
||||
</template>
|
||||
<span :class="[getGearClass('body'), specialMountClass]"></span>
|
||||
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
|
||||
<span :class="[getGearClass('head'), specialMountClass]"></span>
|
||||
<span :class="[getGearClass('headAccessory'), specialMountClass]"></span>
|
||||
<span
|
||||
:class="[
|
||||
'hair_flower_' + member.preferences.hair.flower, specialMountClass
|
||||
]"
|
||||
></span>
|
||||
<span
|
||||
v-if="!hideGear('shield')"
|
||||
:class="[getGearClass('shield'), specialMountClass]"
|
||||
></span>
|
||||
<span
|
||||
v-if="!hideGear('weapon')"
|
||||
:class="[getGearClass('weapon'), specialMountClass]"
|
||||
class="weapon"
|
||||
></span>
|
||||
</template>
|
||||
<!-- Resting-->
|
||||
<span
|
||||
:class="[
|
||||
'hair_flower_' + member.preferences.hair.flower, specialMountClass
|
||||
]"
|
||||
v-if="member.preferences.sleep"
|
||||
class="zzz"
|
||||
></span>
|
||||
<span
|
||||
v-if="!hideGear('shield')"
|
||||
:class="[getGearClass('shield'), specialMountClass]"
|
||||
></span>
|
||||
<span
|
||||
v-if="!hideGear('weapon')"
|
||||
:class="[getGearClass('weapon'), specialMountClass]"
|
||||
class="weapon"
|
||||
></span>
|
||||
</template>
|
||||
<!-- Resting-->
|
||||
<span
|
||||
v-if="member.preferences.sleep"
|
||||
class="zzz"
|
||||
></span>
|
||||
<template v-if="!avatarOnly">
|
||||
<!-- Mount Head-->
|
||||
<span
|
||||
v-if="member.items.currentMount"
|
||||
:class="'Mount_Head_' + member.items.currentMount"
|
||||
></span>
|
||||
<!-- Pet-->
|
||||
<span
|
||||
class="current-pet"
|
||||
:class="petClass"
|
||||
></span>
|
||||
</template>
|
||||
<template v-if="!avatarOnly">
|
||||
<!-- Mount Head-->
|
||||
<span
|
||||
v-if="member.items.currentMount"
|
||||
:class="'Mount_Head_' + member.items.currentMount"
|
||||
></span>
|
||||
<!-- Pet-->
|
||||
<span
|
||||
class="current-pet"
|
||||
:class="petClass"
|
||||
></span>
|
||||
</template>
|
||||
</div>
|
||||
<class-badge
|
||||
v-if="hasClass && !hideClassBadge"
|
||||
class="under-avatar"
|
||||
:member-class="member.stats.class"
|
||||
/>
|
||||
</div>
|
||||
<class-badge
|
||||
v-if="hasClass && !hideClassBadge"
|
||||
class="under-avatar"
|
||||
:member-class="member.stats.class"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -137,6 +139,11 @@
|
||||
filter: invert(100%);
|
||||
}
|
||||
|
||||
.weapon {
|
||||
// the only one that is relative so that it fits into the parent div
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.debug {
|
||||
border: 1px solid red;
|
||||
|
||||
@@ -155,6 +162,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import some from 'lodash/some';
|
||||
import moment from 'moment';
|
||||
import { mapState } from '@/libs/store';
|
||||
import foolPet from '../mixins/foolPet';
|
||||
@@ -202,11 +210,11 @@ export default {
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '141px',
|
||||
default: '140px',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '147px',
|
||||
default: undefined,
|
||||
},
|
||||
centerAvatar: {
|
||||
type: Boolean,
|
||||
@@ -321,10 +329,11 @@ export default {
|
||||
return null;
|
||||
},
|
||||
petClass () {
|
||||
const foolEvent = this.currentEventList?.find(event => moment()
|
||||
.isBetween(event.start, event.end) && event.aprilFools);
|
||||
if (foolEvent) {
|
||||
return this.foolPet(this.member.items.currentPet, foolEvent.aprilFools);
|
||||
if (some(
|
||||
this.currentEventList,
|
||||
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'Fungi',
|
||||
)) {
|
||||
return this.foolPet(this.member.items.currentPet);
|
||||
}
|
||||
if (this.member?.items.currentPet) return `Pet-${this.member.items.currentPet}`;
|
||||
return '';
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
>
|
||||
<div
|
||||
v-for="option in items"
|
||||
:id="option.imageName"
|
||||
:key="option.key"
|
||||
:id="option.imageName"
|
||||
class="outer-option-background"
|
||||
:class="{
|
||||
premium: Boolean(option.gem),
|
||||
@@ -28,14 +28,15 @@
|
||||
v-if="!option.none"
|
||||
class="sprite"
|
||||
:prefix="option.isGear ? 'shop' : 'icon'"
|
||||
:imageName="option.imageName"
|
||||
:image-name="option.imageName"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="redline-outer"
|
||||
>
|
||||
<div class="redline"></div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="redline-outer"
|
||||
>
|
||||
<div class="redline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
<avatar
|
||||
v-if="user._id !== msg.uuid && msg.uuid !== 'system'"
|
||||
class="avatar-left"
|
||||
:height="null"
|
||||
:class="{ invisible: avatarUnavailable(msg) }"
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid] || {}"
|
||||
:avatar-only="true"
|
||||
@@ -51,7 +50,6 @@
|
||||
v-if="user._id === msg.uuid"
|
||||
:class="{ invisible: avatarUnavailable(msg) }"
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid] || {}"
|
||||
:height="null"
|
||||
:avatar-only="true"
|
||||
:hide-class-badge="true"
|
||||
:override-top-padding="'14px'"
|
||||
@@ -98,6 +96,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-left {
|
||||
margin-left: -1.5rem;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.hr {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
|
||||
@@ -225,9 +225,10 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="quest-icon">
|
||||
<Sprite
|
||||
<div
|
||||
class="quest"
|
||||
:image-name="`inventory_quest_scroll_${questData.key}`" />
|
||||
:class="`inventory_quest_scroll_${questData.key}`"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -297,7 +297,7 @@
|
||||
<div class="topbar-dropdown">
|
||||
<router-link
|
||||
v-if="user.permissions.fullAccess ||
|
||||
user.permissions.userSupport"
|
||||
user.permissions.userSupport || user.permissions.newsPoster"
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
:to="{name: 'adminPanel'}"
|
||||
>
|
||||
|
||||
@@ -12,21 +12,20 @@
|
||||
<strong> {{ notification.data.title }} </strong>
|
||||
<span> {{ notification.data.text }} </span>
|
||||
</div>
|
||||
<Sprite
|
||||
<div
|
||||
slot="icon"
|
||||
class="mt-3"
|
||||
:image-name="notification.data.icon" />
|
||||
:class="notification.data.icon"
|
||||
></div>
|
||||
</base-notification>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
import Sprite from '@/components/ui/sprite.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseNotification,
|
||||
Sprite,
|
||||
},
|
||||
props: {
|
||||
notification: {
|
||||
|
||||
@@ -10,21 +10,20 @@
|
||||
slot="content"
|
||||
v-html="$t('newSubscriberItem')"
|
||||
></div>
|
||||
<Sprite
|
||||
<div
|
||||
slot="icon"
|
||||
:image-name="mysteryClass" />
|
||||
:class="mysteryClass"
|
||||
></div>
|
||||
</base-notification>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import BaseNotification from './base';
|
||||
import Sprite from '@/components/ui/sprite.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseNotification,
|
||||
Sprite,
|
||||
},
|
||||
props: ['notification', 'canRemove'],
|
||||
computed: {
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import some from 'lodash/some';
|
||||
import moment from 'moment';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { mapState } from '@/libs/store';
|
||||
@@ -182,12 +183,13 @@ export default {
|
||||
return 'GreyedOut';
|
||||
},
|
||||
imageName () {
|
||||
const foolEvent = this.currentEventList?.find(event => moment()
|
||||
.isBetween(event.start, event.end) && event.aprilFools);
|
||||
if (this.isOwned() && foolEvent) {
|
||||
if (this.isSpecial()) return `stable_${this.foolPet(this.item.key, foolEvent.aprilFools)}`;
|
||||
if (this.isOwned() && some(
|
||||
this.currentEventList,
|
||||
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'Fungi',
|
||||
)) {
|
||||
if (this.isSpecial()) return `stable_${this.foolPet(this.item.key)}`;
|
||||
const petString = `${this.item.eggKey}-${this.item.key}`;
|
||||
return `stable_${this.foolPet(petString, foolEvent.aprilFools)}`;
|
||||
return `stable_${this.foolPet(petString)}`;
|
||||
}
|
||||
|
||||
if (this.isOwned() || (this.mountOwned() && this.isHatchable())) {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div
|
||||
class="svg-icon mr-1"
|
||||
:title="$t('liked')"
|
||||
v-html="likedIcon"
|
||||
v-html="icons.liked"
|
||||
></div>
|
||||
+{{ likeCount }}
|
||||
</div>
|
||||
@@ -47,7 +47,7 @@
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
&.isLiked.currentUserLiked {
|
||||
&.isLiked {
|
||||
color: $purple-200;
|
||||
font-weight: bold;
|
||||
|
||||
@@ -95,11 +95,7 @@ export default {
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
likedIcon () {
|
||||
return this.likedByCurrentUser ? this.icons.liked : this.icons.like;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async like () {
|
||||
this.$emit('toggle-like');
|
||||
|
||||
@@ -39,9 +39,8 @@
|
||||
class="avatar-left"
|
||||
:member="conversationOpponentUser"
|
||||
:avatar-only="true"
|
||||
:show-weapon="true"
|
||||
:show-weapon="false"
|
||||
:debug-mode="false"
|
||||
:height="null"
|
||||
:override-top-padding="'0'"
|
||||
:hide-class-badge="true"
|
||||
@click.native="showMemberModal(msg.uuid)"
|
||||
@@ -60,9 +59,8 @@
|
||||
v-if="user && user._id === msg.uuid"
|
||||
class="avatar-right"
|
||||
:member="user"
|
||||
:height="null"
|
||||
:avatar-only="true"
|
||||
:show-weapon="true"
|
||||
:show-weapon="false"
|
||||
:debug-mode="false"
|
||||
:hide-class-badge="true"
|
||||
:override-top-padding="'0'"
|
||||
@@ -91,6 +89,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-left {
|
||||
margin-right: 1.5rem;
|
||||
}
|
||||
|
||||
.avatar-right {
|
||||
overflow: clip;
|
||||
margin-left: 1.5rem;
|
||||
|
||||
::v-deep .character-sprites {
|
||||
margin-right: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 0px;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@@ -692,7 +692,7 @@
|
||||
<div class="form-inline clearfix">
|
||||
<Sprite
|
||||
class="pull-left"
|
||||
:image-name="'inventory_quest_scroll_' + item.key"
|
||||
:class="'inventory_quest_scroll_' + item.key"
|
||||
style="margin-right: 10px"
|
||||
/>
|
||||
<p>{{ item.text() }}</p>
|
||||
|
||||
@@ -107,7 +107,7 @@ export default {
|
||||
if (lastPublishedPost) this.posts.push(lastPublishedPost);
|
||||
|
||||
// If the user is authorized, show any draft
|
||||
if (this.user && (this.user.permissions.news || this.user.permissions.fullAccess)) {
|
||||
if (this.user && this.user.contributor.newsPoster) {
|
||||
this.posts.unshift(
|
||||
...postsFromServer
|
||||
.filter(p => !p.published || moment().isBefore(p.publishDate)),
|
||||
|
||||
@@ -843,6 +843,7 @@ export default {
|
||||
purchasedPlanIdInfo () {
|
||||
if (!this.subscriptionBlocks[this.user.purchased.plan.planId]) {
|
||||
// @TODO: find which subs are in the common
|
||||
// console.log(this.subscriptionBlocks
|
||||
// [this.user.purchased.plan.planId]); // eslint-disable-line
|
||||
return {
|
||||
price: 0,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<div
|
||||
v-for="currency of currencies"
|
||||
:key="currency.key"
|
||||
:needed-currency-only="neededCurrencyOnly"
|
||||
class="d-flex align-items-center"
|
||||
>
|
||||
<div
|
||||
@@ -55,9 +54,6 @@ export default {
|
||||
amountNeeded: {
|
||||
type: Number,
|
||||
},
|
||||
neededCurrencyOnly: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -70,34 +66,34 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
currencies () {
|
||||
const currencies = [{
|
||||
const currencies = [];
|
||||
currencies.push({
|
||||
type: 'hourglasses',
|
||||
icon: this.icons.hourglasses,
|
||||
value: this.userHourglasses,
|
||||
},
|
||||
});
|
||||
|
||||
{
|
||||
currencies.push({
|
||||
type: 'gems',
|
||||
icon: this.icons.gem,
|
||||
value: this.userGems,
|
||||
},
|
||||
});
|
||||
|
||||
{
|
||||
currencies.push({
|
||||
type: 'gold',
|
||||
icon: this.icons.gold,
|
||||
value: this.userGold,
|
||||
}];
|
||||
});
|
||||
|
||||
for (const currency of currencies) {
|
||||
if (currency.type === this.currencyNeeded
|
||||
&& !this.enoughCurrency(this.currencyNeeded, this.amountNeeded)
|
||||
if (
|
||||
currency.type === this.currencyNeeded
|
||||
&& !this.enoughCurrency(this.currencyNeeded, this.amountNeeded)
|
||||
) {
|
||||
currency.notEnough = true;
|
||||
}
|
||||
}
|
||||
if (this.neededCurrencyOnly) {
|
||||
return currencies.filter(curr => curr.type === this.currencyNeeded);
|
||||
}
|
||||
|
||||
return currencies;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -31,6 +31,13 @@
|
||||
:
|
||||
<a href="mailto:admin@habitica.com">admin@habitica.com</a>
|
||||
<br>
|
||||
{{ $t('generalQuestionsSite') }}
|
||||
:
|
||||
<a
|
||||
target="_blank"
|
||||
@click.prevent="openBugReportModal(true)"
|
||||
> {{ $t('askQuestion') }}</a>
|
||||
<br>
|
||||
{{ $t('businessInquiries') }}
|
||||
:
|
||||
<a href="mailto:admin@habitica.com">admin@habitica.com</a>
|
||||
@@ -47,8 +54,10 @@
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import { goToModForm } from '@/libs/modform';
|
||||
import reportBug from '@/mixins/reportBug.js';
|
||||
|
||||
export default {
|
||||
mixins: [reportBug],
|
||||
computed: {
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
|
||||
@@ -66,13 +66,16 @@
|
||||
class="nav-link"
|
||||
>{{ $t('presskit') }}</a>
|
||||
</router-link>
|
||||
<li class="nav-item">
|
||||
<router-link
|
||||
class="nav-item"
|
||||
tag="li"
|
||||
to="/static/contact"
|
||||
>
|
||||
<a
|
||||
v-once
|
||||
class="nav-link"
|
||||
href="mailto:admin@habitica.com"
|
||||
>{{ $t('contactUs') }}</a>
|
||||
</li>
|
||||
</router-link>
|
||||
</ul>
|
||||
<ul
|
||||
v-else
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bluesky svg {
|
||||
.twitter svg {
|
||||
background-color: $purple-50;
|
||||
fill: $purple-500;
|
||||
&:hover {
|
||||
|
||||
@@ -2,55 +2,54 @@ import includes from 'lodash/includes';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
foolPet (pet, prank) {
|
||||
foolPet (pet) {
|
||||
const SPECIAL_PETS = [
|
||||
'Bear-Veteran',
|
||||
'BearCub-Polar',
|
||||
'Cactus-Veteran',
|
||||
'Dragon-Hydra',
|
||||
'Dragon-Veteran',
|
||||
'Fox-Veteran',
|
||||
'Gryphatrice-Jubilant',
|
||||
'Gryphon-Gryphatrice',
|
||||
'Gryphon-RoyalPurple',
|
||||
'Hippogriff-Hopeful',
|
||||
'Jackalope-RoyalPurple',
|
||||
'JackOLantern-Base',
|
||||
'JackOLantern-Ghost',
|
||||
'JackOLantern-Glow',
|
||||
'JackOLantern-RoyalPurple',
|
||||
'Lion-Veteran',
|
||||
'MagicalBee-Base',
|
||||
'Mammoth-Base',
|
||||
'MantisShrimp-Base',
|
||||
'Orca-Base',
|
||||
'Phoenix-Base',
|
||||
'Tiger-Veteran',
|
||||
'Turkey-Base',
|
||||
'Turkey-Gilded',
|
||||
'Wolf-Cerberus',
|
||||
'Wolf-Veteran',
|
||||
'Wolf-Cerberus',
|
||||
'Dragon-Hydra',
|
||||
'Turkey-Base',
|
||||
'BearCub-Polar',
|
||||
'MantisShrimp-Base',
|
||||
'JackOLantern-Base',
|
||||
'Mammoth-Base',
|
||||
'Tiger-Veteran',
|
||||
'Phoenix-Base',
|
||||
'Turkey-Gilded',
|
||||
'MagicalBee-Base',
|
||||
'Lion-Veteran',
|
||||
'Gryphon-RoyalPurple',
|
||||
'JackOLantern-Ghost',
|
||||
'Jackalope-RoyalPurple',
|
||||
'Orca-Base',
|
||||
'Bear-Veteran',
|
||||
'Hippogriff-Hopeful',
|
||||
'Fox-Veteran',
|
||||
'JackOLantern-Glow',
|
||||
'Gryphon-Gryphatrice',
|
||||
'Gryphatrice-Jubilant',
|
||||
'JackOLantern-RoyalPurple',
|
||||
'Cactus-Veteran',
|
||||
];
|
||||
const BASE_PETS = [
|
||||
'BearCub',
|
||||
'Cactus',
|
||||
'Dragon',
|
||||
'FlyingPig',
|
||||
'Fox',
|
||||
'LionCub',
|
||||
'PandaCub',
|
||||
'TigerCub',
|
||||
'Wolf',
|
||||
'TigerCub',
|
||||
'PandaCub',
|
||||
'LionCub',
|
||||
'Fox',
|
||||
'FlyingPig',
|
||||
'BearCub',
|
||||
'Dragon',
|
||||
'Cactus',
|
||||
];
|
||||
if (!pet) return `Pet-TigerCub-${prank}`;
|
||||
if (!pet) return 'Pet-TigerCub-Fungi';
|
||||
if (SPECIAL_PETS.indexOf(pet) !== -1) {
|
||||
return `Pet-Dragon-${prank}`;
|
||||
return 'Pet-Dragon-Fungi';
|
||||
}
|
||||
const species = pet.slice(0, pet.indexOf('-'));
|
||||
if (includes(BASE_PETS, species)) {
|
||||
return `Pet-${species}-${prank}`;
|
||||
return `Pet-${species}-Fungi`;
|
||||
}
|
||||
return `Pet-BearCub-${prank}`;
|
||||
return 'Pet-BearCub-Fungi';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -212,11 +212,7 @@ $pmHeaderHeight: 56px;
|
||||
}
|
||||
|
||||
.toggle-switch-outer {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
float: right !important;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1027,58 +1023,19 @@ export default defineComponent({
|
||||
this.scrollToBottom();
|
||||
}, 150);
|
||||
},
|
||||
/**
|
||||
* This method does a couple of things:
|
||||
* - first round:
|
||||
* - tries to scroll down
|
||||
* - in the next tick it triggers it again
|
||||
* (during testing it seemed that the first trigger still had some space left to scroll)
|
||||
* - 2nd round:
|
||||
* - tries to scroll down
|
||||
* - in the next tick it checks if the scrollTop is to most it can scroll down,
|
||||
* if it is, it stops from doing that again
|
||||
* if not, it goes into the next round
|
||||
* - if we reach round 6 it stops completely,
|
||||
* no need to have a endless loop of just scrolling down
|
||||
*/
|
||||
scrollToBottom (callCount = 0) {
|
||||
if (callCount > 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollToBottom () {
|
||||
if (!this.$refs.chatscroll) {
|
||||
// if the message list component not loaded yet, but scrollToBottom was called
|
||||
// just try again at a later time
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom(callCount + 1);
|
||||
}, 125);
|
||||
return;
|
||||
}
|
||||
|
||||
const chatscrollEl = this.$refs.chatscroll.$el;
|
||||
// chatscrollBeforeTick.scrollTop = chatscrollBeforeTick.scrollHeight;
|
||||
chatscrollEl.scrollTo(0, chatscrollEl.scrollHeight);
|
||||
const chatscrollBeforeTick = this.$refs.chatscroll.$el;
|
||||
chatscrollBeforeTick.scrollTop = chatscrollBeforeTick.scrollHeight;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
if (!this.$refs.chatscroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldRetrigger = true;
|
||||
|
||||
if (callCount > 1) {
|
||||
const maxPossibleScrollPos = chatscrollEl.scrollHeight - chatscrollEl.clientHeight;
|
||||
|
||||
if (chatscrollEl.scrollTop === maxPossibleScrollPos) {
|
||||
shouldRetrigger = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRetrigger) {
|
||||
setTimeout(() => {
|
||||
this.scrollToBottom(callCount + 1);
|
||||
}, 125);
|
||||
}
|
||||
const chatscroll = this.$refs.chatscroll.$el;
|
||||
chatscroll.scrollTop = chatscroll.scrollHeight;
|
||||
});
|
||||
},
|
||||
infiniteScrollTrigger () {
|
||||
|
||||
@@ -73,11 +73,13 @@ input {
|
||||
}
|
||||
|
||||
.input-group {
|
||||
&:focus, &:active, &:focus-within {
|
||||
border: solid 2px $purple-400;
|
||||
&:focus, &:active, &:focus-within {
|
||||
border: solid 2px $purple-400;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
class="balance-info"
|
||||
:currency-needed="currencyNeeded"
|
||||
:amount-needed="amountNeeded"
|
||||
:neededCurrencyOnly="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
<your-balance
|
||||
:amount-needed="amountNeeded"
|
||||
currency-needed="gems"
|
||||
class="d-flex align-items-center"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -190,6 +190,7 @@ const router = new VueRouter({
|
||||
meta: {
|
||||
privilegeNeeded: [ // any one of these is enough to give access
|
||||
'userSupport',
|
||||
'newsPoster',
|
||||
],
|
||||
},
|
||||
children: [
|
||||
@@ -218,7 +219,7 @@ const router = new VueRouter({
|
||||
|
||||
// Only used to handle some redirects
|
||||
// See router.beforeEach
|
||||
{ path: '/static/tavern-and-guilds', redirect: '/static/faq/tavern-and-guilds' },
|
||||
{ path: '/static/faq/tavern-and-guilds', redirect: '/static/tavern-and-guilds' },
|
||||
{ path: '/redirect/:redirect', name: 'redirect' },
|
||||
{ path: '*', redirect: { name: 'notFound' } },
|
||||
],
|
||||
|
||||
@@ -5,9 +5,3 @@ export async function searchUsers (store, payload) {
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function getUserHistory (store, payload) {
|
||||
const url = `/api/v4/admin/user/${payload.userIdentifier}/history`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@@ -32,9 +32,3 @@ export async function getHeroParty (store, payload) {
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function getHeroGroupPlans (store, payload) {
|
||||
const url = `/api/v4/hall/heroes/${payload.heroId}/group-plans`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@@ -72,11 +72,7 @@ describe('LevelUp', () => {
|
||||
|
||||
it('generates the right test class for level 15', () => {
|
||||
const questClass = testFunction('questClass', 15);
|
||||
expect(questClass()).to.equal('inventory_quest_scroll_atom1');
|
||||
});
|
||||
|
||||
it('generates empty test class for level 14', () => {
|
||||
const questClass = testFunction('questClass', 14);
|
||||
expect(questClass()).to.equal('');
|
||||
expect(questClass()).to.equal('scroll inventory_quest_scroll_atom1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,7 +43,6 @@ envVars
|
||||
});
|
||||
|
||||
const webpackPlugins = [
|
||||
new webpack.ProvidePlugin({ 'window.jQuery': 'jquery' }),
|
||||
new webpack.DefinePlugin(envObject),
|
||||
new MomentLocalesPlugin({
|
||||
localesToKeep: ['bg',
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"conText": "Якостта намалява щетите от отрицателни навици и пропуснати ежедневни задачи.",
|
||||
"perception": "Усет",
|
||||
"perText": "Усетът увеличава спечеленото злато, а след отключването на пазара увеличава вероятността за намиране на предмети след приключване на задачи.",
|
||||
"intelligence": "Интелект",
|
||||
"intelligence": "Интелигентност",
|
||||
"intText": "Интелигентността увеличава спечеления опит, а след отключването на класовете определя максималната мана за използване за класовите умения.",
|
||||
"levelBonus": "Бонус за ниво",
|
||||
"allocatedPoints": "Разпределени точки",
|
||||
|
||||
@@ -95,6 +95,9 @@
|
||||
"whyReportingPostPlaceholder": "Моля, помогнете на модераторите, като ни кажете защо докладвате тази публикация за нарушение, например: защото е нежелана, включва ругатни, клетви, фанатизъм, обиди, теми за възрастни, насилие.",
|
||||
"optional": "Незадължително",
|
||||
"needsTextPlaceholder": "Въведете съобщението си тук.",
|
||||
"copyMessageAsToDo": "Копиране на съобщението като задача",
|
||||
"copyAsTodo": "Копиране като задача за изпълнение",
|
||||
"messageAddedAsToDo": "Съобщението беше копирано като задача.",
|
||||
"leaderOnlyChallenges": "Само водачът на групата може да създава предизвикателства",
|
||||
"sendGift": "Изпращане на подарък",
|
||||
"inviteFriends": "Поканете приятели",
|
||||
|
||||
@@ -46,8 +46,10 @@
|
||||
"messageNotAbleToBuyInBulk": "Не може да се закупи повече от един брой от този предмет.",
|
||||
"notificationsRequired": "Идентификаторите на известията са задължителни.",
|
||||
"unallocatedStatsPoints": "Имате <span class=\"notification-bold-blue\"><%= points %> неразпределени показателни точки</span>",
|
||||
"beginningOfConversation": "Това е началото на разговора Ви с <%= userName %>.",
|
||||
"messageDeletedUser": "Съжаляваме, но този потребител е изтрил профила си.",
|
||||
"messageMissingDisplayName": "Липсва екранно име.",
|
||||
"canDeleteNow": "Вече може да изтриете съобщението, ако желаете.",
|
||||
"reportedMessage": "Вие докладвахте това съобщние на модераторите."
|
||||
"reportedMessage": "Вие докладвахте това съобщние на модераторите.",
|
||||
"beginningOfConversationReminder": "Не забравяйте да бъдете мили, уважителни и да следвате Обществените Правила!"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"quests": "Мисии",
|
||||
"quest": "мисия",
|
||||
"petQuests": "Мисии за домашни любимци и ездитни животни",
|
||||
"petQuests": "Мисии за любимци и превози",
|
||||
"unlockableQuests": "Мисии, които могат да бъдат отключени",
|
||||
"goldQuests": "Последователности от мисии на класовите повелители",
|
||||
"questDetails": "Подробности за мисията",
|
||||
@@ -29,7 +29,7 @@
|
||||
"collected": "Събрани",
|
||||
"abort": "Прекратяване",
|
||||
"leaveQuest": "Напускане на мисията",
|
||||
"sureLeave": "Сигурни ли сте, че искате да се откажете от мисията? Ще загубите целия си напредък.",
|
||||
"sureLeave": "Наистина ли искате да се откажете от текущата мисия? Ще загубите целия си напредък.",
|
||||
"mustComplete": "Трябва първо да завършите <%= quest %>.",
|
||||
"mustLvlQuest": "Трябва да бъдете ниво <%= level %>, за да купите тази мисия!",
|
||||
"unlockByQuesting": "За да отключите тази мисия, първо завършете <%= title %>.",
|
||||
@@ -78,8 +78,5 @@
|
||||
"questAlreadyStartedFriendly": "Мисията вече е започнала, но винаги може да хванете следващата!",
|
||||
"questAlreadyStarted": "Мисията вече е започнала.",
|
||||
"questInvitationNotificationInfo": "Получихте покана за присъединяване към мисия",
|
||||
"hatchingPotionQuests": "Мисии за Магическа Излюпваща Отвара",
|
||||
"bossDamage": "Нанесохте вреда на главатаря!",
|
||||
"questItemsPending": "<%= amount %> предмета ще бъдат събрани",
|
||||
"sureLeaveInactive": "Сигурни ли сте, че искате да се откажете от мисията? Няма да можете да участвате в нея."
|
||||
"hatchingPotionQuests": "Мисии за Магическа Излюпваща Отвара"
|
||||
}
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
"foundNewItemsCTA": "Podívej se do tvého Inventáře a zkus zkombinovat tvůj nový líhnoucí lektvar a vajíčko!",
|
||||
"foundNewItemsExplanation": "Splnění úkolů ti dá šanci najít předměty jako vajíčka, líhnoucí lektvary a jídlo pro mazlíčky.",
|
||||
"foundNewItems": "Nové předměty nalezeny!",
|
||||
"hideAchievements": "Schovat <%= category %>",
|
||||
"hideAchievements": "Schovat <%= kategorie %>",
|
||||
"onboardingCompleteDesc": "Získáváš <strong>5 úspěchů</strong> a <strong class=\"gold-amount\">100 zlaťáků</strong> za dokončení seznamu.",
|
||||
"onboardingProgress": "<%= percentage %>% postup",
|
||||
"gettingStartedDesc": "Splň tyto základní úkoly a získej <strong>5 úspěchů</strong> a <strong class=\"gold-amount\">100 zlaťáků</strong>, jakmile budeš hotový/á!",
|
||||
"showAllAchievements": "Zobrazit všechny <%= category %>",
|
||||
"showAllAchievements": "Zobrazit všechny <%= kategorie %>",
|
||||
"yourProgress": "Tvůj postup",
|
||||
"achievementBareNecessitiesModalText": "Splnil/a jsi výpravy za opicí, lenochodem a stromečkem!",
|
||||
"achievementBareNecessitiesText": "Splnil/a výpravy za opicí, lenochodem a stromečkem.",
|
||||
|
||||
@@ -95,6 +95,9 @@
|
||||
"whyReportingPostPlaceholder": "Prosím pomož našim moderatorům a vysvětli, proč ohlašuješ tento příspěvek kvůli porušení pravidel, tedy zda je to spam, sprostá slova, náboženské přísahy, netolerance, urážky, témata nevhodná pro mladistvé, násilí.",
|
||||
"optional": "Možný",
|
||||
"needsTextPlaceholder": "Napiš svou zprávu sem.",
|
||||
"copyMessageAsToDo": "Zkopírovat zprávu jako úkol",
|
||||
"copyAsTodo": "Zkopírovat jako úkol",
|
||||
"messageAddedAsToDo": "Zpráva zkopírována jako úkol.",
|
||||
"leaderOnlyChallenges": "Pouze velitel družiny může vytvářet Výzvy",
|
||||
"sendGift": "Poslat dárek",
|
||||
"inviteFriends": "Pozvat přátele",
|
||||
|
||||
@@ -46,10 +46,12 @@
|
||||
"messageNotAbleToBuyInBulk": "Tento předmět nelze nakoupit v množství větším, než je 1.",
|
||||
"notificationsRequired": "Id upozornění je potřeba.",
|
||||
"unallocatedStatsPoints": "Máš <span class=\"notification-bold-blue\"><%= points %> nepřidělený(ch) vlastnostní(ch) bod(ů)</span>",
|
||||
"beginningOfConversation": "Toto je začátek tvé konverzace s uživatelem <%= userName %>.",
|
||||
"messageDeletedUser": "Omlouváme se, ale tento uživatel smazal svůj účet.",
|
||||
"messageMissingDisplayName": "Chybí zobrazované jméno.",
|
||||
"canDeleteNow": "Nyní můžete zprávu smazat.",
|
||||
"reportedMessage": "Tuto zprávu jste nahlásili moderátorům.",
|
||||
"beginningOfConversationReminder": "Nezapomeňte být milí, taktní a respektujte Zásady komunity!",
|
||||
"messageAllUnEquipped": "Vše odloženo.",
|
||||
"messageBackgroundUnEquipped": "Pozadí odloženo.",
|
||||
"messagePetMountUnEquipped": "Mazlíček a zvíře odloženi.",
|
||||
|
||||
@@ -95,6 +95,9 @@
|
||||
"whyReportingPostPlaceholder": "Du kan hjælpe vores moderatorer ved at lade os vide, hvorfor du anmelder denne besked som en overtrædelse - fx spam, banden, religiøse kraftudtryk, fordomme, nedladende skældsord, emner for aldersgruppen +18 eller vold.",
|
||||
"optional": "Valgfri",
|
||||
"needsTextPlaceholder": "Skriv din besked her.",
|
||||
"copyMessageAsToDo": "Kopier besked som To-Do",
|
||||
"copyAsTodo": "Kopier som To-Do",
|
||||
"messageAddedAsToDo": "Besked kopieret som To-Do.",
|
||||
"leaderOnlyChallenges": "Kun gruppelederen kan oprette udfordringer",
|
||||
"sendGift": "Send gave",
|
||||
"inviteFriends": "Invitér venner",
|
||||
|
||||
@@ -46,11 +46,13 @@
|
||||
"messageNotAbleToBuyInBulk": "Denne genstand kan ikke købes i antal større end 1.",
|
||||
"notificationsRequired": "Notafikation ID'er er krævet.",
|
||||
"unallocatedStatsPoints": "Du har <span class=\"notification-bold-blue\"><%= points %> ufordelte Egenskabspoint</span>",
|
||||
"beginningOfConversation": "Dette er begyndelsen på din samtale med <%= userName %>.",
|
||||
"messageDeletedUser": "Sorry, this user has deleted their account.",
|
||||
"messageMissingDisplayName": "Missing display name.",
|
||||
"newsPostNotFound": "News Post er ikke fundet eller du har ikke adgang.",
|
||||
"canDeleteNow": "Du kan nu slette beskeden, hvis du ønsker det.",
|
||||
"reportedMessage": "Du har indrapporteret denne besked til moderatorerne.",
|
||||
"beginningOfConversationReminder": "Husk at være venlig, respektful og følge Retningslinjerne for Fællesskabet!",
|
||||
"messageAllUnEquipped": "Alt fjernet.",
|
||||
"messageBackgroundUnEquipped": "Baggrund fjernet.",
|
||||
"messageCostumeUnEquipped": "Kostume fjernet.",
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
"achievementDinosaurDynastyModalText": "Du hast alle Vogel- und Dinosaurier-Haustiere gesammelt!",
|
||||
"achievementDinosaurDynasty": "Dinosaurier Dynastie",
|
||||
"achievementBonelessBoss": "Knochenloser Boss",
|
||||
"achievementBonelessBossText": "Hat alle wirbellosen Tiere ausgebrütet: Käfer, Schmetterling, Tintenfisch, Nacktschnecke, Oktopus, Schnecke und Spinne!",
|
||||
"achievementBonelessBossText": "Hat alle wirbellosen Tiere ausgebrütet: Käfer, Schmetterling, Tintenfisch, Nacktschnecke, Oktopus, Schnecke und Spinnen!",
|
||||
"achievementBonelessBossModalText": "Du hast alle wirbellosen Tiere gesammelt!",
|
||||
"achievementDuneBuddyText": "Hat alle Standardfarben der Wüstenbewohnern ausgebrütet: Gürteltier, Kaktus, Fuchs, Frosch, Schlange und Spinne!",
|
||||
"achievementRoughRider": "Harter Reiter",
|
||||
|
||||
@@ -789,7 +789,7 @@
|
||||
"backgroundBirthdayBashNotes": "Habitica feiert eine Geburtstagsparty und alle sind eingeladen!",
|
||||
"eventBackgrounds": "Ereignis-Hintergründe",
|
||||
"backgroundBirthdayBashText": "Geburtstagsparty",
|
||||
"backgroundInsideACrystalNotes": "Schau aus dem Inneren eines Kristalls hinaus.",
|
||||
"backgroundInsideACrystalNotes": "Schau aus dem Inneren eines Kristalls heraus.",
|
||||
"backgrounds072023": "SET 110: Veröffentlicht im Juli 2023",
|
||||
"backgroundOnAPaddlewheelBoatText": "Auf einem Schaufelradboot",
|
||||
"backgroundOnAPaddlewheelBoatNotes": "Fahre mit einem Schaufelradboot.",
|
||||
@@ -891,14 +891,5 @@
|
||||
"backgroundFirstSnowForestNotes": "Tritt in den ersten Schnee im Wald.",
|
||||
"backgrounds012025": "Set 128: Veröffentlicht im Januar 2025",
|
||||
"backgroundWinterLandscapeWithCabinText": "Winterlandschaft mit Hütte",
|
||||
"backgroundWinterLandscapeWithCabinNotes": "Macht es dir in einer Winterlandschaft mit einer Hütte gemütlich.",
|
||||
"backgroundOldFashionedTeaShopText": "Altmodischer Teeladen",
|
||||
"backgroundOldFashionedTeaShopNotes": "Genieße ein Getränk in einem Altmodischen Teeladen.",
|
||||
"backgrounds022025": "Set 129: Veröffentlicht im Februar 2025",
|
||||
"backgrounds032025": "SET 130: Veröffentlicht im März 2025",
|
||||
"backgroundMountainSceneWithBlossomsText": "Bergszene mit Blüten",
|
||||
"backgroundMountainSceneWithBlossomsNotes": "Erlebe den entzückenden Anblick und Geruch einer Bergszene mit Blüten.",
|
||||
"backgroundGardenWithFlowerBedsNotes": "Genieße das Blühen des Frühlings in einem Garten mit Blumenbeeten.",
|
||||
"backgrounds0420205": "SET 131: Veröffentlicht im April 2025",
|
||||
"backgroundGardenWithFlowerBedsText": "Garten mit Blumenbeeten"
|
||||
"backgroundWinterLandscapeWithCabinNotes": "Macht es dir in einer Winterlandschaft mit einer Hütte gemütlich."
|
||||
}
|
||||
|
||||
@@ -392,19 +392,5 @@
|
||||
"questEggDogText": "Welpe",
|
||||
"questEggDogMountText": "Hund",
|
||||
"questEggDogAdjective": "ein freundlicher",
|
||||
"hatchingPotionGingerbread": "Lebkuchen",
|
||||
"questEggCatText": "Kätzchen",
|
||||
"questEggCatMountText": "Katze",
|
||||
"questEggCatAdjective": "ein schelmisches",
|
||||
"questEggOtterText": "Otter",
|
||||
"questEggOtterMountText": "Otter",
|
||||
"hatchingPotionJade": "Jade",
|
||||
"questEggOtterAdjective": "Ein perfider",
|
||||
"questEggAlpacaText": "Alpaka",
|
||||
"questEggAlpacaMountText": "Alpaka",
|
||||
"questEggAlpacaAdjective": "ein überladenes",
|
||||
"hatchingPotionBalloon": "Ballon",
|
||||
"wackyPotionAddlNotes": "Kann nicht zum Reittier großgezogen oder für Quest-Haustier Eier benutzt werden.",
|
||||
"hatchingPotionCryptid": "Kryptisch",
|
||||
"wackyPotionNotes": "Schütte dies über ein Ei und es wird als Durchgeknalltes <%= potText(locale) %> Haustier schlüpfen."
|
||||
"hatchingPotionGingerbread": "Lebkuchen"
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"webFaqStillNeedHelp": "Wenn Du eine Frage hast, die hier oder im [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ) nicht beantwortet wurde, verwende das Stell eine Frage Formular [LINK NEEDED]! Wir helfen Dir gerne.",
|
||||
"parties": "Partys",
|
||||
"webFaqAnswer25": "Habitica verwendet drei verschiedene Aufgabentypen, um deinen Bedürfnissen gerecht zu werden: Gewohnheiten, tägliche Aufgaben und To-Dos.\n\nGewohnheiten können positiv oder negativ sein und stellen etwas dar, das Sie vielleicht mehrmals am Tag oder nach einem nicht festgelegten Zeitplan verfolgen möchten. Positive Gewohnheiten bringen euch Belohnungen wie Gold und Erfahrung (Exp), während ihr bei negativen Gewohnheiten Lebenspunkte (HP) verliert.\n\nDailies sind wiederkehrende Aufgaben, die du nach einem strukturierten Zeitplan erledigen möchtest. Zum Beispiel einmal am Tag, dreimal in der Woche oder viermal im Monat. Wenn du Dailies verpasst, verlierst du HP, aber je schwieriger sie sind, desto besser ist die Belohnung!\n\nTo-Dos sind einmalige Aufgaben, für deren Erledigung es Belohnungen gibt. To-Dos können ein Fälligkeitsdatum haben, aber du verlierst keine HP, wenn du es verpasst.\n\nWähle die Aufgabenart, die am besten zu dem passt, was du erreichen willst!",
|
||||
"commonQuestions": "Häufige Fragen",
|
||||
"commonQuestions": "Häufige Fragenj",
|
||||
"faqQuestion25": "Welche Aufgabentypen gibt es?",
|
||||
"faqQuestion26": "Was sind einige Beispielaufgaben?",
|
||||
"webFaqAnswer31": "Wenn du eine Aufgabe erfüllst und HP verlierst, obwohl du das nicht hättest tun sollen, kam es zu einer Verzögerung, während der Server die auf anderen Plattformen vorgenommenen Änderungen synchronisiert hat. Wenn du zum Beispiel Gold oder Mana verwendest oder HP in der mobilen App verlierst und dann eine Aufgabe auf der Website erledigst, bestätigt der Server lediglich, dass alles synchronisiert ist.",
|
||||
"webFaqAnswer49": "Wenn Du Habitica mit anderen erleben möchtest, aber keine anderen Spieler kennst, ist die Suche nach einer Party die beste Option! Wenn Du bereits andere Spieler kennst, die eine Party haben, kannst Du deinen @Benutzernamen mit ihnen teilen, um eingeladen zu werden. Alternativ kannst Du auch eine neue Gruppe erstellen und sie mit ihrem @Nutzernamen oder ihrer E-Mail-Adresse einladen.\n\nUm eine Party zu erstellen oder zu suchen, wähle \"Party\" im Navigationsmenü und wähle dann die Option, die Dir am besten gefällt.",
|
||||
"webFaqAnswer62": "Gruppenpläne bieten dir die einzigartige Möglichkeit, anderen Mitgliedern deines Gruppenplans gemeinsame Aufgaben zuzuweisen. Wenn eine gemeinsame Aufgabe einem Mitglied zugewiesen wird, können andere Mitglieder sie nicht mehr erledigen.\n\nDu kannst eine Aufgabe auch mehreren Mitgliedern zuweisen. Wenn sich zum Beispiel alle Mitglieder die Zähne putzen müssen, erstellst du eine Aufgabe und weist sie jedem Mitglied zu. Jedes Mitglied kann die Aufgabe erledigen und sich seine individuelle Belohnung verdienen. Die Hauptaufgabe wird als erledigt angezeigt, sobald alle Mitglieder sie erledigt haben.",
|
||||
"webFaqAnswer32": "Alle Spieler beginnen in der Klasse des Kriegers, bis sie Stufe 10 erreicht haben. Sobald du Stufe 10 erreichst, hast du die Wahl, eine neue Klasse zu wählen oder als Krieger weiterzuspielen.\n\nJede Klasse verfügt über unterschiedliche Ausrüstungen und Fertigkeiten. Wenn du dich nicht für eine Klasse entscheiden möchtest, kannst du \"Abbrechen\" wählen. Wenn du dich später doch entscheidest, kannst du das Klassensystem in den Einstellungen wieder aktivieren.\n\nWenn Du Deine Klasse nach Level 10 noch einmal ändern möchtest, kannst Du die Sphäre der Wiedergeburt hierfür nutzen. Die Sphäre der Wiedergeburt ist mit Level 50 auf demMarktplatz für 6 Edelsteine verfügbar und mit Level 100 bekommst Du sie umsonst.\n\nAlternativ kannst Du Deine Klasse jederzeit in den Einstellungen für 3 Edelsteine ändern. Dies wird Dein Level nicht wie die Sphäre der Wiedergeburt zurücksetzen, aber es erlaubt Dir, die Fertigkeits-Punkte, die Du beim Leveln gesammelt hast, Deiner neuen Klasse zuzuordnen.",
|
||||
"webFaqAnswer32": "In Habitica gibt es vier Klassen: Krieger, Magier, Schurke und Heiler. Alle Spieler beginnen in der Klasse des Kriegers, bis du Stufe 10 erreicht hast. Sobald du Stufe 10 erreichst, hast du die Wahl, eine neue Klasse zu wählen oder als Krieger weiterzuspielen.\n\nJede Klasse verfügt über unterschiedliche Ausrüstungen und Fertigkeiten. Wenn du dich nicht für eine Klasse entscheiden möchtest, kannst du \"Aussteigen\" wählen. Wenn du dich dafür entscheidest, kannst du das Klassensystem später in den Einstellungen wieder aktivieren.",
|
||||
"sunsetFaqPara14": "<strong>Linguists</strong><br />Wir freuen uns auch weiterhin über Hilfe bei der Übersetzung der Apps und der Website und werden für qualifizierte Beiträge nach wie vor Beitragsstufen vergeben. Die Methode, mit der wir Übersetzungen annehmen, wird sich jedoch ändern. Wir möchten unsere Ressourcen auf die Unterstützung einer bestimmten Auswahl von Sprachen für alle Plattformen konzentrieren. Um dies zu erreichen, werden wir die Anzahl der für Übersetzungen verfügbaren Sprachen reduzieren. Zuvor nicht fertiggestellte Sprachen werden in Github archiviert. Wir hoffen, dass diese Änderung das plattformübergreifende Habitica-Erlebnis konsistenter macht. Sie können unsere aktuellsten Richtlinien für das Übersetzungsverfahren auf unserer Website lesen <a href='https://translate.habitica.com/projects/habitica/#information'>Übersetzungswebsite</a>.",
|
||||
"webFaqAnswer34": "Haustiere mögen Futter, das zu ihrer Farbe passt. Basis-Tiere sind die Ausnahme, aber alle Basis-Tiere mögen den gleichen Gegenstand. Im Folgenden siehst du, welche Nahrungsmittel jedes Haustier mag:\n\n * Basistiere mögen Fleisch\n * Weiße Haustiere mögen Milch\n * Wüstenhaustiere mögen Kartoffeln\n * Rote Haustiere mögen Erdbeeren\n * Schattentiere mögen Schokolade\n * Skelett-Tiere mögen Fisch\n * Zombie-Tiere mögen verdorbenes Fleisch\n * Zuckerwatte rosa Haustiere mögen rosa Zuckerwatte\n * Zuckerwatte blaue Haustiere mögen blaue Zuckerwatte\n * Goldene Haustiere mögen Honig",
|
||||
"webFaqAnswer35": "Sobald du dein Haustier genug gefüttert hast, um es zu einem Reittier zu machen, musst du diese Art von Haustier erneut ausbrüten, um es in deinem Stall zu haben.\n\nUm Reittiere in den mobilen Apps zu sehen:\n\n * Wähle im Menü \"Haustiere & Reittiere\" und wechseln zur Registerkarte \"Reittiere\".\n\nSo zeigst du Reittiere auf der Website an:\n\n * Wähle im Menü \"Inventar\" die Option \"Haustiere und Reittiere\" und scrollen nach unten zum Abschnitt \"Reittiere\"",
|
||||
@@ -36,7 +36,7 @@
|
||||
"faqQuestion30": "Was passiert, wenn ich keine HP mehr habe?",
|
||||
"webFaqAnswer30": "Wenn deine HP Null erreichen, verlierst du eine Stufe, dein gesamtes Gold und ein Ausrüstungsstück, das du wieder kaufen kannst.",
|
||||
"faqQuestion31": "Warum habe ich HP verloren, wenn ich mit einer nicht-negativen Aufgabe interagiere?",
|
||||
"faqQuestion32": "Wie kann ich eine Klasse wählen?",
|
||||
"faqQuestion32": "Wann kann ich eine Klasse wählen?",
|
||||
"faqQuestion33": "Was ist der blaue Balken, der nach Level 10 erscheint?",
|
||||
"webFaqAnswer33": "Nachdem du das Klassensystem freigeschaltet hast, schaltest du auch Fertigkeiten frei, die Mana benötigen, um genutzt werden zu können. Mana wird durch deinen INT-Wert bestimmt und kann durch Fertigkeiten und Ausrüstung angepasst werden.",
|
||||
"faqQuestion34": "Welches Futter mag mein Haustier?",
|
||||
@@ -241,7 +241,5 @@
|
||||
"subscriptionDetail430": "Die Kündigung eines aktiven Abonnements wird ein Enddatum für deine Vorteile festsetzen, bis zu dem du vollen Zugang zu allen Abo-Vorteilen hast. Das bedeutet, dass du weiterhin am Start jedes Monats Mystische Sanduhren und Erhöhungen der Edelsteinobergrenze erhältst, solange du Zugang zu diesen Vorteilen hast.",
|
||||
"subscriptionDetail440": "Am Tag, an dem diese Änderungen in Kraft treten, erhalten aktive Abonnenten mit einer ungeraden Anzahl an Edelsteinen pro Monat folgende Anpassungen ihrer Edelsteinobergrenze:",
|
||||
"subscriptionDetail470": "Gruppenabonnentenvorteile verhalten sich genauso wie die eines wiederkehrenden 1-Monats-Abonnements. Du erhältst eine Mystische Sanduhr am Anfang jedes Monats und die Anzahl an Edelsteinen, die du jeden Monat auf dem Marktplatz kaufen kannst, wird sich erhöhen bis zu einem Limit von 50.",
|
||||
"subscriptionPara3": "Wir hoffen, dass dieser neue Rhythmus besser vorhersagbar ist, mehr Zugang zur fantastischen Gegenstandauswahl im Laden des Zeitreisenden ermöglicht und noch mehr Motivation bietet, jeden Monat Fortschritte an deinen Aufgaben zu machen!",
|
||||
"faqQuestion67": "Was sind die Klassen in Habitica?",
|
||||
"webFaqAnswer67": "Klassen sind verschiedene Rollen, die dein Charakter spielen kann. Jede Klasse bietet ihre eigene Reihe von einzigartigen Vorteilen und Fähigkeiten beim Aufsteigen auf höhere Level. Diese Fähigkeiten können das Bearbeiten deiner Aufgaben ergänzen oder dabei helfen, deine Party beim Abschließen von Quests zu unterstützen.\n\nDeine Klasse bestimmt auch, welche Ausrüstung für dich in den Belohnungen, im Marktplatz und im Jahreszeitenmarkt zum Kauf erhältlich ist.\n\nHier ist eine Zusammenfassung jeder Klasse, um dir dabei zu helfen, diejenige zu wählen, welche am besten zu deinem Spielstil passt:\n#### **Krieger**\n* Krieger verursachen hohen Schaden bei Bossen und haben eine hohe Chance für kritische Treffer beim Abschließen von Aufgaben, was dich mit extra Erfahrung und Gold belohnt.\n* Stärke ist ihr primäres Attribut, welches den Schaden erhöht, den sie verursachen.\n* Ausdauer ist ihr sekundäres Attribut, welches den Schaden verringert, den sie erhalten.\n* Die Fähigkeiten der Krieger erhöhen die Ausdauer und Stärke der Party Kameraden.\n* Erwäge, einen Krieger zu spielen, wenn du es liebst, Bosse zu bekämpfen und auch ein wenig Schutz möchtest, wenn du gelegentlich Aufgaben versäumst.\n#### **Heiler**\n* Heiler haben eine starke Verteidigung und können sich selbst, sowie die Party Kameraden, heilen.\n* Ausdauer ist ihr primäres Attribut, welches ihre Heilungen verstärkt und den Schaden, den sie erhalten, verringert.\n* Intelligenz ist ihr sekundäres Attribut, welches ihr Mana und ihre Erfahrung erhöht.\n* Die Fähigkeiten der Heiler bewirken, dass ihre Aufgaben weniger rot werden und erhöhen die Ausdauer der Party Kameraden.\n* Erwäge, einen Heiler zu spielen, wenn du oft Aufgaben versäumst, und die Fähigkeit benötigst, dich selbst und deine Party Kameraden zu heilen. Heiler erreichen schnell neue Level.\n#### **Magier**\n* Magier gewinnen schnell neue Level und viel Mana, und verursachen Schaden bei Bossen in Quests.\n* Intelligenz ist ihr primäres Attribut, welches ihr Mana und ihre Erfahrung erhöht.\n* Wahrnehmung ist ihr sekundäres Attribut, welches ihr gefundenes Gold und ihre gefundenen Gegenstände vermehrt.\n* Die Fähigkeiten der Magier bewirken, dass ihre Aufgaben Strähnen eingefroren werden, stellen das Mana ihrer Party Kameraden wieder her, und erhöhen ihre Intelligenz.\n* Erwäge, einen Magier zu spielen, wenn du durch das schnelle Erreichen neuer Level und das Beisteuern von Schaden in Boss Quests motiviert wirst.\n#### **Schurke**\n* Schurken bekommen die meisten erbeuteten Gegenstände und das meiste Gold beim Erledigen von Aufgaben, und haben eine höhere Chance, kritische Treffer zu erzielen, was ihnen noch mehr Erfahrung und Gold beschert.\n* Wahrnehmung ist ihr primäres Attribut, welches ihr gefundenes Gold und ihre gefundenen Gegenstände vermehrt.\n* Stärke ist ihr sekundäres Attribut, welches den Schaden erhöht, den sie verursachen.\n* Die Fähigkeiten der Schurken helfen ihnen, versäumten Tagesaufgaben auszuweichen, Gold zu klauen, und die Wahrnehmung ihrer Party Kameraden zu erhöhen.\n* Erwäge, einen Schurken zu spielen, wenn du durch Belohnungen sehr motiviert wirst."
|
||||
"subscriptionPara3": "Wir hoffen, dass dieser neue Rhythmus besser vorhersagbar ist, mehr Zugang zur fantastischen Gegenstandauswahl im Laden des Zeitreisenden ermöglicht und noch mehr Motivation bietet, jeden Monat Fortschritte an deinen Aufgaben zu machen!"
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
"joinMany": "Schließe Dich über <%= userCountInMillions %> Millionen Leuten an und habe Spaß, während Du Deine Aufgaben erfüllst!",
|
||||
"joinToday": "Tritt Habitica heute bei",
|
||||
"signup": "Registrieren",
|
||||
"getStarted": "Auf geht's",
|
||||
"getStarted": "Auf gehts",
|
||||
"mobileApps": "Mobile Apps",
|
||||
"learnMore": "Mehr erfahren",
|
||||
"communityInstagram": "Instagram",
|
||||
|
||||
@@ -2800,12 +2800,12 @@
|
||||
"armorMystery202406Text": "Phantom-Seeräuber Kleidung",
|
||||
"headMystery202406Text": "Phantom-Seeräuber Hut",
|
||||
"eyewearMystery202406Text": "Phantom-Seeräuber Maske",
|
||||
"weaponArmoirePaintbrushNotes": "Ein Ruck purer Inspiration durchdringt dich, wenn du diesen Frabpinsel aufhebst, und ermöglicht dir, alles zu malen, was du dir vorstellen kannst. Erhöht Intelligenz um <%= int %>.Verzauberter Schrank: Malerset (Gegenstand 3 von 4).",
|
||||
"weaponArmoirePaintbrushNotes": "Ein Ruck purer Inspiration durchdringt dich, wenn du diesen Frabpinsel aufhebst, und ermöglicht dir, alles zu malen, was du dir vorstellen kannst. Erhöht Intelligenz um <%= int %>.Verzauberter Schrank: Maler Set (Gegenstand 3 von 4).",
|
||||
"weaponArmoirePaintbrushText": "Farbpinsel",
|
||||
"weaponArmoireMopText": "Mopp",
|
||||
"weaponArmoireCleaningClothText": "Putzlappen",
|
||||
"weaponArmoireMopNotes": "Schritt 1: Tauche den Mopp in einen Eimer mit Wasser und Schaum. Schritt 2: Ziehe den Mopp über den Boden. Schritt 3: Tu so, als wäre das Ende des Mopp Stiels ein Mikrofon und singe mit voller Inbrunst. Schritt 4: Wiederhole Schritte 1-3, bis der Boden sauber ist. Erhöht Ausdauer und Wahrnehmung um jeweils <%= attrs %>. Reinigungs-Set Zwei (Gegenstand 2 von 3)",
|
||||
"weaponArmoireCleaningClothNotes": "Nimm dieses Putzwerkzeug auf deine Abenteuer mit und sei immer bereit, eine hübsche Gedenktafel zu polieren oder eine hölzerne Fensterbank zu wischen. Erhöht Stärke und Ausdauer um jeweils <%= attrs %>. Verzauberter Schrank: Reinigungs-Set Zwei (Gegenstand 3 von 3)",
|
||||
"weaponArmoireMopNotes": "Schritt 1: Tauche den Mopp in einen Eimer mit Wasser und Schaum. Schritt 2: Ziehe den Mopp über den Boden. Schritt 3: Tu so, als wäre das Ende des Mopp Stiels ein Mikrofon und singe mit voller Inbrunst. Schritt 4: Wiederhole Schritte 1-3, bis der Boden sauber ist. Erhöht Ausdauer und Wahrnehmung um jeweils <%= attrs %>. Putzausrüstungs-Set Zwei (Gegenstand 2 von 3)",
|
||||
"weaponArmoireCleaningClothNotes": "Nimm dieses Putzwerkzeug auf deine Abenteuer mit und sei immer bereit, eine hübsche Gedenktafel zu polieren oder eine hölzerne Fensterbank zu wischen. Erhöht Stärke und Ausdauer um jeweils <%= attrs %>. Verzauberter Schrank: Putzausrüstung Set Zwei (Gegenstand 3 von 3)",
|
||||
"weaponArmoireRidingBroomText": "Reitbesen",
|
||||
"weaponArmoireRidingBroomNotes": "Reite auf diesem feinen Besen zu all deinen magischsten Besorgungen--oder nimm ihn für eine Spritztour durch die Nachbarschaft. Wuui! Erhöht Stärke um <%= str %> und Intelligenz um <%= int %>. Verzauberter Schrank: Spukhaftes Zauberer Set (Gegenstand 1 von 3)",
|
||||
"weaponArmoireHattersShearsText": "Scharfe Scheren",
|
||||
@@ -2969,7 +2969,7 @@
|
||||
"armorArmoireBasketballUniformNotes": "Fragst du dich, was auf dem Rücken dieser Uniform aufgedruckt ist? Deine Glückszahl, natürlich! Erhöht Wahrnehmung um <%= per %>.Verzauberter Schrank: Altertümliches Basketballset (Gegenstand 1 von 2).",
|
||||
"armorArmoireShawlCollarCoatNotes": "Ein weiser Zauberer sagte einst, dass nichts besser ist als es sowohl gemütlich zu haben als auch produktiv zu sein! Trage diesen warmen und stylischen Mantel, wenn du die diesjährigen Herausforderungen meisterst. Erhöht Ausdauer um <%= con %>.",
|
||||
"armorArmoirePaintersApronText": "Schürze des Malers",
|
||||
"armorArmoirePaintersApronNotes": "Diese Schürze kann deine Kleidung vor Farbe und deine kreativen Projekte vor harschen Kritiken schützen. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Malerset (Gegenstand 1 von 4).",
|
||||
"armorArmoirePaintersApronNotes": "Diese Schürze kann deine Kleidung vor Farbe und deine kreativen Projekte vor harschen Kritiken schützen. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Maler-Set (Gegenstand 1 von 4).",
|
||||
"weaponSpecialWinter2025WarriorText": "Axt des Elchkriegers",
|
||||
"weaponSpecialWinter2025WarriorNotes": "Eine mächtige Axt für einen mächtigen Elch! Du wirst unaufhaltbar sein! Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2024-2025 Winterausrüstung.",
|
||||
"weaponSpecialWinter2025RogueText": "Schneeflockenausbruch",
|
||||
@@ -3124,7 +3124,7 @@
|
||||
"headArmoireWhiteFloppyHatNotes": "Viele Zaubersprüche wurden in diesen einfachen Hut eingenäht und verleihen ihm eine wundersame weiße Farbe. Erhöht Stärke, Intelligenz und Ausdauer um jeweils <%= attrs %>. Verzauberter Schrank: Weißes Loungewear -Set (Gegenstand 1 von 3).",
|
||||
"headArmoireCorsairsBandanaNotes": "Egal, ob du deinen Kopf bedecken willst, falls eine Möwe über dich hinwegfliegt, oder ob du sicherstellen willst, dass deine Feinde dich nicht schwitzen sehen, dieses Tuch ist unverzichtbar. Füge einfach eine Zierperle für jedes Abenteuer hinzu, das du bestehst. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Korsaren-Set (Gegenstand 2 von 3)",
|
||||
"headArmoireFunnyFoolCapNotes": "Die Glöckchen an diesem Hut könnten deine Gegner zum Kichern bringen, aber dir helfen sie nur, dich zu konzentrieren. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Lustiges Narren-Set (Gegenstand 1 von 3)",
|
||||
"headArmoireDragonKnightsHelmNotes": "Mit den feurigen Elementen auf diesem Helm könnten dich Drachen für einen der ihren halten. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Drachenritter-Set (Gegenstand 1 von 3)",
|
||||
"headArmoireDragonKnightsHelmNotes": "Mit den feurigen Elementen auf diesem Helm könnten Drachen dich für einen der ihren halten. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Drachenritter-Set (Gegenstand 1 von 3)",
|
||||
"headArmoireStormKnightHelmText": "Sturmritterhelm",
|
||||
"headArmoireGreenTrapperHatText": "Grüne Trappermütze",
|
||||
"headArmoireGreenTrapperHatNotes": "Alle sagen, dass deine Mütze so warm aussieht! Und das ist sie tatsächlich. Achte nur darauf, dass du die Klappen von deinen Ohren ziehst, wenn die Leute mit dir reden, sonst hört sich das Ganze eher nach „dne ütze sht ss wrrm ss!“ an. Erhöht Ausdauer und Wahrnehmung um jeweils <%= attrs %> . Verzauberter Schrank: Trappermützen-Set (Gegenstand 1 von 2).",
|
||||
@@ -3189,129 +3189,5 @@
|
||||
"shieldSpecialFall2024WarriorNotes": "Komplikationen bei Aufgaben werden von deinem Schild absorbiert und machen dich entschlossener. Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe Herbstausrüstung 2024.",
|
||||
"shieldSpecialFallRogue2024Notes": "Die Aufgaben selbst werden von den Wirbeln und Spiralen dieser hypnotischen Waffe überwältigt. Erhöht Stärke um <%= str %>. Limitierte Ausgabe Herbstausrüstung 2024.",
|
||||
"headAccessoryMystery202212Notes": "Mit dieser verschnörkelten goldenen Tiara werden deine Herzlichkeit und Freundschaft neue Höhen erreichen. Gewährt keinen Attributbonus. Dezember 2022 Abonnentengegenstand.",
|
||||
"headAccessoryMystery202212Text": "Eis-Tiara",
|
||||
"shieldMystery202408Notes": "Die magischen Lichter beleuchten das Innere deines Seifenblasenverstecks oder jeden anderen Ort, an dem du ein wenig Licht brauchst! Gewährt keinen Attributbonus. August 2024 Abonnentengegenstand.",
|
||||
"shieldArmoireJewelersPliersText": "Juwelierzange",
|
||||
"shieldArmoireJewelersPliersNotes": "Sie schneidet, biegt, presst und vieles mehr. Mit diesem Werkzeug kannst du alles machen, was du dir vorstellen kannst. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Juwelierset (Gegenstand 3 von 4).",
|
||||
"shieldArmoireTeaKettleNotes": "In diesem Kessel kannst du all deine geschmackvollen Lieblingstees aufbrühen. Hast du Lust auf schwarzen Tee, grünen Tee, Oolong oder vielleicht einen Kräutertee? Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Teekränzchen Set (Gegenstand 3 von 3).",
|
||||
"shieldArmoireBasketballNotes": "Zisch! Wann immer du diesen magischen Basketball abschießt, wird es nichts als Treffer geben. Erhöht Ausdauer und Stärke um jeweils <%= attrs %> . Verzauberter Schrank: Altmodisches Basketballset (Gegenstand 2 von 2).",
|
||||
"shieldArmoirePaintersPaletteNotes": "Farben in allen Facetten des Regenbogens stehen dir zur Verfügung. Ist es Magie, die sie so lebendig macht, wenn du sie benutzt, oder ist es dein Talent? Erhöht Stärke um <%= str %>. Verzauberter Schrank: Malerset (Gegenstand 4 von 4).",
|
||||
"shieldArmoireBucketNotes": "Obwohl dieser Eimer für eine Mischung aus Wasser und Reinigungslösung gedacht ist, kannst du ihn auch zum Sammeln, Tragen und Transportieren von allem verwenden, was hineinpasst! Erhöht Stärke und Intelligenz um jeweils <%= attrs %>. Verzauberter Schrank: Reinigungs-Set 2 (Gegenstand 1 von 3)",
|
||||
"backMystery202402Text": "Paradiesische Pinke Herzen",
|
||||
"shieldArmoireSaucepanNotes": "Schau in diesen dampfenden Kochtopf und finde die Antwort auf das bestgehütete Geheimnis des Lebens! (Suppe. Die Antwort ist immer Suppe.) ErhöhtWahrnehmung um <%= per %>. Verzauberter Schrank: Küchenwerkzeugset 2 (Gegenstand 1 von 2).",
|
||||
"shieldArmoireBuoyantBeachBallNotes": "Hast du schon zu viele Bälle in der Luft? Hier ist einer, den du sicher absetzen, rollen, hüpfen und hüpfen und hüpfen lassen kannst... Erhöht Stärke um <%= str %>. Verzauberter Schrank: Strand-Set (Gegenstand 4 von 4).",
|
||||
"shieldArmoireTrustyPencilNotes": "Du weißt, was man sagt: Der Bleistift ist mächtiger als der Schwertstift. Moment... das klingt nicht ganz richtig... Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Schuluniformset (Gegenstand 4 von 4).",
|
||||
"backMystery202401Notes": "Beschwöre sanftes Schneegestöber herauf oder rufe einen mächtigen Schneesturm herbei. Du hast die Wahl! Gewährt keinen Attributbonus. Jänner 2024 Abonnentengegenstand.",
|
||||
"backMystery202402Notes": "Lass dich von einer Aura liebevoller Energie umgeben, wohin du auch gehst! Gewährt keinen Attributbonus. Februar 2024 Abonnentengegenstand.",
|
||||
"backMystery202302Text": "Betrügerischer Tabby-Schweif",
|
||||
"backMystery202301Notes": "Diese flauschigen Schweife enthalten ätherische Kräfte und auch ein hohes Maß an Charme! Gewährt keinen Attributbonus. Jänner 2023 Abonnentengegenstand.",
|
||||
"backMystery202302Notes": "Wann immer du diesen Schweif trägst, wird es ein toller Tag werden! Callooh! Callay! Gewährt keinen Attributbonus. Februar 2023 Abonnentengegenstand.",
|
||||
"backMystery202305Text": "Abendliche Flügel",
|
||||
"backMystery202305Notes": "Fang das Funkeln des Abendsterns ein und schwebe auf diesen Flügeln in fremde Gefilde. Gewährt keinen Attributbonus. Mai 2023 Abonnentengegenstand.",
|
||||
"backMystery202309Text": "Kolossale Kometenmottenflügel",
|
||||
"backMystery202309Notes": "Flattere durch Wälder, gleite über Berge und schwebe über Ozeane auf diesen hellen und schönen Flügeln. Gewährt keinen Attributbonus. September 2023 Abonnentengegenstand.",
|
||||
"backSpecialAnniversaryText": "Habitica Helden Cape",
|
||||
"backSpecialAnniversaryNotes": "Lass dieses stolze Cape im Wind flattern und erzähle jedem, dass du ein Habitica Held bist. Gewährt keinen Attributbonus. Gegenstands-Sonderausgabe zur 10. Geburtstagsfeier.",
|
||||
"backSpecialHeroicAureoleText": "Heroische Aureole",
|
||||
"backSpecialHeroicAureoleNotes": "Die Edelsteine auf dieser Aureole schimmern, wenn du deine ruhmvollen Geschichten erzählst. Erhöht alle Eigenschaften um <%= attrs %>.",
|
||||
"bodySpecialAnniversaryText": "Habiticas Heldenkragen",
|
||||
"bodySpecialAnniversaryNotes": "Ergänzt dein königspurpurnes Kostüm perfekt! Gewährt keinen Attributbonus. Sonderausgaben-Gegenstand zur 10. Geburtstagsfeier.",
|
||||
"eyewearMystery202312Text": "Winterliche blaue Augen",
|
||||
"bodyArmoireKarateBrownBeltNotes": "Dieser Gürtel ist für diejenigen, deren Techniken und Fähigkeiten ausgereift sind. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Karateset (Gegenstand 9 von 10).",
|
||||
"bodyArmoireKarateBrownBeltText": "Brauner Gürtel",
|
||||
"headAccessoryMystery202302Text": "Trickbetrüger Tabby-Ohren",
|
||||
"headAccessoryMystery202307Text": "Krakenkrone",
|
||||
"bodyArmoireKarateOrangeBeltNotes": "Dieser Gürtel ist für diejenigen, die sich gesteigert und das Einsteigerlevel gemeistert haben. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Karateset (Gegenstand 4 von 10).",
|
||||
"bodyArmoireKarateGreenBeltText": "Grüner Gürtel",
|
||||
"bodyArmoireKarateBlackBeltText": "Schwarzer Gürtel",
|
||||
"bodyArmoireKarateYellowBeltNotes": "Dieser Gürtel ist für Einsteiger, welche die Grundlagen gelernt haben. Erhöht Wahrnehmung um <%= per %>. Verzauberter Schrank: Karateset (Gegenstand 3 von 10).",
|
||||
"eyewearMystery202312Notes": "Kein Grund zur Sorge, diese eisigen Blautöne helfen dir, hinter der kalten und dunklen Jahreszeit die Wärme der nachfolgenden Monate zu erspähen. Gewährt keinen Attributbonus. Dezemeber 2023 Abonnentengegenstand.",
|
||||
"eyewearMystery202406Notes": "Versuch zu vermeiden, dass dies von einer Bande aufdringlicher Kinder und ihrem sprechenden Hund abgezogen wird. Gewährt keinen Attributbonus. Juni 2024 Abonnentengegenstand.",
|
||||
"bodyArmoireKarateOrangeBeltText": "Orangener Gürtel",
|
||||
"headAccessoryMystery202305Text": "Abendzeitliche Hörner",
|
||||
"eyewearMystery202303Notes": "Vermittle deinen Feinden durch deinen lässigen Gesichtsausdruck ein falsches Gefühl der Sicherheit. Gewährt keinen Attributbonus. März 2023 Abonnentengegenstand.",
|
||||
"eyewearMystery202308Notes": "Bist du schläfrig oder ruhst du deine Augen nur in Erwartung deines nächsten tollen Kampfes aus? Gewährt keinen Attributbonus. August 2023 Abonnentengegenstand.",
|
||||
"bodyArmoireKarateWhiteBeltText": "Weißer Gürtel",
|
||||
"bodyArmoireKarateWhiteBeltNotes": "Dieser niedrigste Gürtel ist für jene, die ihre Reise gerade erst beginnen. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Karateset (Gegenstand 2 von 10).",
|
||||
"bodyArmoireKarateGreenBeltNotes": "Dieser Gürtel ist für diejenigen gedacht, die auf fortgeschrittenem Niveau lernen, ihre Fähigkeiten zu verbessern. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Karateset (Gegenstand 5 von 10).",
|
||||
"bodyArmoireKarateBlueBeltNotes": "Dieser Gürtel ist für diejenigen, die mehr lernen und ihren Geist und Körper entwickeln wollen. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Karateset (Gegenstand 6 von 10).",
|
||||
"headAccessoryMystery202302Notes": "Das schnurrige Accessoire, das dein bezauberndes Grinsen unterstreicht. Gewährt keinen Attributbonus. Februar 2023 Abonnentengegenstand.",
|
||||
"headAccessoryMystery202307Notes": "Dieser mächtige Stirnreif beschwört Wirbelstürme und stürmisches Wetter herauf! Gewährt keinen Attributbonus. Juli 2023 Abonnentengegenstand.",
|
||||
"headAccessoryMystery202305Notes": "Diese Hörner leuchten durch das reflektierte Mondlicht. Gewährt keinen Attributbonus. Mai 2023 Abonnentengegenstand.",
|
||||
"bodyArmoireKarateYellowBeltText": "Gelber Gürtel",
|
||||
"bodyArmoireKaratePurpleBeltNotes": "Dieser Gürtel ist für diejenigen, die für anspruchsvolle Übungen bereit sind. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Karateset (Gegenstand 7 von 10).",
|
||||
"bodyArmoireKarateRedBeltText": "Roter Gürtel",
|
||||
"bodyArmoireKarateRedBeltNotes": "Dieser Gürtel ist für diejenigen, die gelernt haben, bei ihren Übungen vorsichtig vorzugehen. Erhöht Wahrnehmung um <%= per %>. Verzauberter Schrank: Karateset (Gegenstand 8 von 10).",
|
||||
"bodyArmoireKarateBlackBeltNotes": "Dieser höchste Gürtelgrad ist für diejenigen, die ein tieferes Verständnis anstreben und ihr Wissen an andere weitergeben dürfen. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Karateset (Gegenstand 10 von 10).",
|
||||
"headAccessorySpecialHeroicCircletText": "Heldenhafter Stirnreif",
|
||||
"headAccessorySpecialHeroicCircletNotes": "Schwer ist das Haupt, das die Krone trägt, aber dieser Stirnreif ist so leicht wie dein großzügiger Geist. Erhöht alle Werte um <%= attrs %>.",
|
||||
"headAccessoryMystery202309Text": "Kolossale Kometenmotten-Antennen",
|
||||
"headAccessoryMystery202309Notes": "Diese Antennen sind modisch und gefiedert, helfen aber auch bei der Navigation! Gewährt keinen Attributbonus. September 2023 Abonnentengegenstand.",
|
||||
"headAccessoryMystery202310Text": "Geisterlicht-Krone",
|
||||
"headAccessoryMystery202310Notes": "Wie ein Irrlicht können diese unheimlichen Lichter neugierige Seelen in ihr Verderben locken. Gewährt keinen Attributbonus. Oktober 2023 Abonnentengegenstand.",
|
||||
"eyewearSpecialAnniversaryNotes": "Schau durch die Augen eines Habitica-Helden - durch deine! Gewährt keinen Attributbonus. Sonderausgaben-Gegenstand zur 10. Geburtstagsfeier.",
|
||||
"bodyArmoireKarateBlueBeltText": "Blauer Gürtel",
|
||||
"bodyArmoireKaratePurpleBeltText": "Violetter Gürtel",
|
||||
"eyewearMystery202303Text": "Verträumte Augen",
|
||||
"eyewearSpecialAnniversaryText": "Habiticas Heldenmaske",
|
||||
"shieldArmoireFancyFloralFanNotes": "Beende deinen bezaubernden Look mit diesem erstklassigen Fächer aus besonders blumiger Baumwolle. Erhöht Wahrnehmung um <%= per %>. Verzauberter Schrank: Bezauberndes Blumenset (Gegenstand 2 von 2).",
|
||||
"armorMystery202502Notes": "Du bist voller gutmütiger Witze und Scherze, von deinem zerrupften Kragen bis zu deinen gigantischen Schuhen! Gewährt keinen Attributbonus. Februar 2025 Abonnentengegenstand.",
|
||||
"armorMystery202502Text": "Herzvoller Harlekin Anzug",
|
||||
"headMystery202502Text": "Herzvoller Harlekin Hut",
|
||||
"headMystery202502Notes": "Dieses fröhliche Hütchen wird bei jedem, der dich sieht, für Freude sorgen! Gewährt keinen Attributbonus. Februar 2025 Abonnentengegenstand.",
|
||||
"headArmoireFancyFloralHatText": "Bezaubernder Blumenhut",
|
||||
"shieldMystery202502Text": "Herzvolle Harlekin Ballons",
|
||||
"shieldMystery202502Notes": "Möge dein Herz an diesen Valentinstag, und auch jeden anderen Tag, so leicht sein wie diese beschwingten Luftballons. Gewährt keinen Attributbonus. Februar 2025 Abonnentengegenstand.",
|
||||
"shieldArmoireFancyFloralFanText": "Bezaubernder Blumenfächer",
|
||||
"headArmoireFancyFloralHatNotes": "Bestaune diesen bezaubernden Hut voller hinreißender Blumen und verschnörkelten Schnallen. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Bezauberndes Blumen Zubehör Set (Gegenstand 1 von 2).",
|
||||
"weaponSpecialSpring2025WarriorNotes": "Mit einem Schnitt kannst du durch Blumenstängel säbeln, um ein Bukett zu machen, oder direkt durch Hindernisse, um deine Aufgaben zu erfüllen. Erhöht Stärke um <%= str %>. Limitierte Ausgabe Frühlingsausrüstung 2025.",
|
||||
"weaponSpecialSpring2025WarriorText": "Sonnenschein-Säbel",
|
||||
"weaponSpecialSpring2025RogueText": "Kristallspitzen-Morgenstern",
|
||||
"weaponSpecialSpring2025RogueNotes": "Mit einem Schwinger kannst du jedes Hindernis eliminieren, das deinen Zielen im Weg steht. Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"weaponSpecialSpring2025MageNotes": "Mit einem Schwinger kannst du Elementarmagie nutzen, um deine Umgebung zu kontrollieren. Nutze den Vorteil und spring vorwärts! Erhöht Intelligenz um <%= int %> und Wahrnehmung um <%= per %>. Limitierte Ausgabe Frühlingsausrüstung 2025.",
|
||||
"weaponSpecialSpring2025MageText": "Fangschrecken Stab",
|
||||
"armorSpecialSpring2025WarriorText": "Sonnenschein Rüstung",
|
||||
"armorSpecialSpring2025WarriorNotes": "Diese atemberaubende Rüstung hat Farben, die du inmitten eines sonnigen Frühlingstages am Himmel sehen kannst. Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe Frühlingsausrüstung 2025.",
|
||||
"armorSpecialSpring2025HealerText": "Plumeria Robe",
|
||||
"armorSpecialSpring2025HealerNotes": "Diese atemberaubende Robe enthält Plumeria Blütenblätter, die weich und raschelig sind. Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"weaponSpecialSpring2025HealerText": "Plumeria Hirtenstab",
|
||||
"weaponSpecialSpring2025HealerNotes": "Mit einem Schwinger kannst du Bestäuber an deine Seite rufen, um dir bei deinen Abenteuern zu helfen. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"armorSpecialSpring2025RogueText": "Kristallnadel-Umhang",
|
||||
"armorSpecialSpring2025RogueNotes": "Dieser atemberaubende Umhang enthält extra Kristalle mit speziellen, geheimen Kräften, von denen nur du weißt. Erhöht Wahrnehmung um <%= per %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"armorSpecialSpring2025MageText": "Fangschrecken Uniform",
|
||||
"armorSpecialSpring2025MageNotes": "Diese atemberaubende Uniform enthält auffällige Farben, lässt dich aber trotzdem heimlich an deine schwierigsten Aufgaben anpirschen. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"headSpecialSpring2025WarriorText": "Sonnenschein Helm",
|
||||
"headSpecialSpring2025WarriorNotes": "Der Kamm auf diesem Helm erinnert an den Lauf der Sonne, sieht aber auch aus wie eine Bürste, die du für den Frühjahrsputz nutzen könntest. Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"headSpecialSpring2025RogueText": "Kristallnadel-Hut",
|
||||
"headSpecialSpring2025RogueNotes": "Die Kristalle in diesem Hut ermuntern zu produktiver Energie und leuchten außerdem hell, damit du zu jeder Tages- und Nachtzeit arbeiten kannst. Erhöht Wahrnehmung um <%= per %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"headSpecialSpring2025HealerText": "Plumeria Kopfschmuck",
|
||||
"headSpecialSpring2025HealerNotes": "Diese Blume symbolisiert Geburt, Liebe und Neubeginn! Sie verbreitet auch einen schönen Duft, den du genießen kannst, während du an deinen Aufgaben arbeitest. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"headSpecialSpring2025MageText": "Fangschrecken Maske",
|
||||
"headSpecialSpring2025MageNotes": "Die Fangschrecke ist bekannt dafür, sich zu tarnen oder sich langsam zu bewegen. Wähle eine Taktik zu Hilfe für jedes einzelne Ziel, sei aber gewiss, daß du Taktiken jederzeit ändern kannst, wenn du musst. Erhöht Wahrnehmung um <%= per %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"headMystery202503Text": "Jade Juggernaut Haar",
|
||||
"headMystery202503Notes": "Diese grüne Frisur passt perfekt zu einem tapferen Krieger und Verteidiger des Planeten. Gewährt keinen Attributbonus. März 2025 Abonnentengegenstand.",
|
||||
"armorArmoireSpringPetalYukataText": "Frühlingsblüten Yukata",
|
||||
"armorArmoireSpringPetalYukataNotes": "Diesen Yukata kann man perfekt zum Feiern des kommenden Frühlings anziehen. Stell sicher, daß du für ein Foto neben Kirschblüten posierst. Erhöht Ausdauer und Stärke um jeweils <%= attrs %> . Verzauberter Schrank: Frühlingsblüten Set (Gegenstand 1 von 2).",
|
||||
"shieldSpecialSpring2025RogueText": "Kristallspitzen-Flegel",
|
||||
"shieldSpecialSpring2025RogueNotes": "Du kannst den Kristall nutzen, um eine produktive Zukunft für dich weiszusagen. Nutze die Gelegenheit und spring vorwärts! Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"shieldSpecialSpring2025HealerText": "Plumeria Schild",
|
||||
"shieldSpecialSpring2025HealerNotes": "Du kannst dieses spezielle Blütenblatt verwenden, um Güte zu sammeln oder um negative Gedanken wegzuschnipsen. Nutze die Gelegenheit und spring vorwärts! Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2025 Ausrüstung.",
|
||||
"shieldSpecialSpring2025WarriorText": "Sonnenstrahl Schild",
|
||||
"shieldSpecialSpring2025WarriorNotes": "Du kannst deine Gegner für den Moment blenden, wenn die Sonne diesen Schild genau richtig trifft. Nutze den Vorteil und spring vorwärts! Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2025 Frühlingsausrüstung.",
|
||||
"shieldArmoireSpringPetalUchiwaNotes": "Dieser tragbare Fächer mit schönem Blütenmuster bewirkt eine leichte Brise nur für dich, wenn das Wetter wärmer wird. Erhöht Intelligenz und Wahrnehmung um jeweils <%= attrs %>. Verzauberter Schrank: Frühlingsblüten Set (Gegenstand 2 von 2).",
|
||||
"shieldArmoireSpringPetalUchiwaText": "Frühlingsblütenfächer",
|
||||
"eyewearMystery202503Text": "Jade Juggernaut Augen",
|
||||
"eyewearMystery202503Notes": "Dieser stechende Blick wird jeden Kämpfer, der es wagt, dich herauszufordern, in Panik versetzen! Gewährt keinen Attributbonus. März 2025 Abonnentengegenstand.",
|
||||
"armorMystery202504Text": "Scheues Yeti Rüstung",
|
||||
"armorMystery202504Notes": "Abscheulich? Eher anbetungswürdig! Gewährt keinen Attributbonus. April 2025 Abonnentengegenstand.",
|
||||
"armorArmoireSillyOrangeTuxedoText": "Alberner Orangen-Smoking",
|
||||
"armorArmoireSillyOrangeTuxedoNotes": "Dein eigener persönlicher Anzug des Tages. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Lächerlicher Smoking Set (Gegenstand 1 von 2).",
|
||||
"armorArmoireSillierBlueTuxedoNotes": "Verbreite Stimmung in diesem einzigartigen Outfit. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Noch Lächerlicherer Smoking Set (Gegenstand 1 von 2).",
|
||||
"armorArmoireSillierBlueTuxedoText": "Noch Lächerlicherer Blauer Smoking",
|
||||
"headMystery202504Text": "Scheues Yeti Kutte",
|
||||
"headArmoireSillyOrangeTophatText": "Alberner Orangen-Zylinder",
|
||||
"headArmoireSillyOrangeTophatNotes": "Passt gut zu einem Kürbiskuchen-Haarschnitt. Erhöht Stärke und Ausdauer um jeweils <%= attrs %>. Verzauberter Schrank: Lächerlicher Smoking Set (Gegenstand 2 von 2).",
|
||||
"headArmoireSillierBlueTophatText": "Noch Lächerlicherer Blauer Zylinder",
|
||||
"headMystery202504Notes": "Trage diese mysteriöse Visage, um unentdeckt unter den oskursten Fabelwesen der Welt zu verweilen. Gewährt keinen Attributbonus. April 2025 Abonnentengegenstand.",
|
||||
"headArmoireSillierBlueTophatNotes": "Etwas Klasse, etwas Raffinesse. Erhöht Stärke und Ausdauer um jeweils <%= attrs %>. Verzauberter Schrank: Noch Lächerlicherer Smoking Set (Gegenstand 2 von 2)."
|
||||
"headAccessoryMystery202212Text": "Eis-Tiara"
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"user": "Benutzer",
|
||||
"market": "Marktplatz",
|
||||
"newSubscriberItem": "Du hast einen neuen <span class=\"notification-bold-blue\">Überraschungsgegenstand</span>",
|
||||
"subscriberItemText": "Abonnenten bekommen jeden Monatsanfang ein neues Überraschungsausrüstungsset!",
|
||||
"subscriberItemText": "Abonnenten bekommen jeden Monat einen Überraschungsgegenstand. Er wird Anfang des Monats verfügbar. Schaue auf der 'Überraschungsgegenstand'-Seite des Wikis für mehr Informationen nach.",
|
||||
"all": "Alle",
|
||||
"none": "Keine",
|
||||
"more": "<%= count %> mehr",
|
||||
@@ -238,8 +238,5 @@
|
||||
"mutePlayer": "Stumm",
|
||||
"skipExternalLinkModal": "Halte STRG (Windows) oder Command (Mac) beim Anklicken eines Links, um dieses Modal zu überspringen.",
|
||||
"shadowMute": "Unsichtbare Stummschaltung",
|
||||
"titleCustomizations": "Individualisierungen",
|
||||
"targetUserNotExist": "Zielbenutzer: '<%= userName %>' existiert nicht.",
|
||||
"newMessage": "Neue Nachricht",
|
||||
"rememberToBeKind": "Bitte sei freundlich, respektvoll, und folge den <a href='/static/community-guidelines' target='_blank'>Community-Richtlinien</a>."
|
||||
"titleCustomizations": "Individualisierungen"
|
||||
}
|
||||
|
||||
@@ -95,6 +95,9 @@
|
||||
"whyReportingPostPlaceholder": "Grund für die Beschwerde",
|
||||
"optional": "Optional",
|
||||
"needsTextPlaceholder": "Gib Deine Nachricht hier ein.",
|
||||
"copyMessageAsToDo": "Nachricht als To-Do übernehmen",
|
||||
"copyAsTodo": "Als To-Do kopieren",
|
||||
"messageAddedAsToDo": "Nachricht als To-Do übernommen.",
|
||||
"leaderOnlyChallenges": "Nur die Gruppenleitung kann Herausforderungen erstellen",
|
||||
"sendGift": "Ein Geschenk schicken",
|
||||
"inviteFriends": "Lade Freunde ein",
|
||||
@@ -242,7 +245,7 @@
|
||||
"guildSummaryPlaceholder": "Schreibe eine Kurzbeschreibung über deine Gruppe.. Was ist der Hauptzweck der Gruppe und was werden die Gruppenmitglieder tun?",
|
||||
"groupDescription": "Beschreibung",
|
||||
"guildDescriptionPlaceholder": "Nutze diesen Abschnitt um alles, was Mitglieder über Deine Gruppe wissen sollten, ausführlicher darzustellen. Nützliche Tipps, hilfreiche Links und ermutigende Worte gehören hier hin!",
|
||||
"markdownFormattingHelp": "[Markdown Formatierungshilfe](https://github.com/HabitRPG/habitica/wiki/Markdown-in-Habitica)",
|
||||
"markdownFormattingHelp": "[Markdown Formatierungshilfe](https://habitica.fandom.com/wiki/Markdown_Cheat_Sheet)",
|
||||
"partyDescriptionPlaceholder": "Das ist unsere Partybeschreibung. Sie beschreibt, was wir in unserer Party so tun. Wenn Du mehr darüber wissen willst, was wir in unserer Party so machen, lies die Beschreibung. Party on!",
|
||||
"guildGemCostInfo": "Eine Edelstein-Gebühr fördert die Qualität der Gilden und wird der Gildenbank gutgeschrieben.",
|
||||
"noGuildsTitle": "Du bist nicht Mitglied einer Gilde.",
|
||||
@@ -427,6 +430,5 @@
|
||||
"createGroupTitle": "Erstelle Gruppe",
|
||||
"readyToUpgrade": "Bereit zum Aufrüsten?",
|
||||
"interestedLearningMore": "Willst du mehr erfahren?",
|
||||
"checkGroupPlanFAQ": "Schau in die <a href='/static/faq#what-is-group-plan'>Gruppenpläne FAQ</a> um herauszufinden, wie du deine gemeinsamen Aufgaben optimal nutzen kannst.",
|
||||
"messageCopiedToClipboard": "Nachricht in Zwischenablage kopiert."
|
||||
"checkGroupPlanFAQ": "Schau in die <a href='/static/faq#what-is-group-plan'>Gruppenpläne FAQ</a> um herauszufinden, wie du deine gemeinsamen Aufgaben optimal nutzen kannst."
|
||||
}
|
||||
|
||||
@@ -270,9 +270,5 @@
|
||||
"winter2025StringLightsHealerSet": "Lichterketten Heiler Set",
|
||||
"winter2025SnowRogueSet": "Schneeschurken Set",
|
||||
"winter2025MooseWarriorSet": "Elchkrieger Set",
|
||||
"winter2025AuroraMageSet": "Aurora Magier Set",
|
||||
"spring2025PlumeriaHealerSet": "Plumeria Heiler Set",
|
||||
"spring2025MantisMageSet": "Fangschrecken Magier Set",
|
||||
"spring2025SunshineWarriorSet": "Sonnenschein Krieger Set",
|
||||
"spring2025CrystalPointRogueSet": "Kristallspitzen Schurken Set"
|
||||
"winter2025AuroraMageSet": "Aurora Magier Set"
|
||||
}
|
||||
|
||||
@@ -46,10 +46,12 @@
|
||||
"messageNotAbleToBuyInBulk": "Dieser Gegenstand kann nicht in größeren Mengen als 1 gekauft werden.",
|
||||
"notificationsRequired": "Mitteilungs-IDs werden benötigt.",
|
||||
"unallocatedStatsPoints": "Du kannst <span class=\"notification-bold-blue\"><%= points %> Attributpunkt(e)</span> verteilen",
|
||||
"beginningOfConversation": "Dies ist der Anfang Deiner Unterhaltung mit <%= userName %>.",
|
||||
"messageDeletedUser": "Tut uns leid, dieser Benutzer hat sein Konto gelöscht.",
|
||||
"messageMissingDisplayName": "Fehlender Anzeigename.",
|
||||
"reportedMessage": "Du hast diese Nachricht den Moderatoren gemeldet.",
|
||||
"canDeleteNow": "Du kannst diese Nachricht nun löschen, wenn Du willst.",
|
||||
"beginningOfConversationReminder": "Denke an einen freundlichen und respektvollen Umgang und halte Dich an die Community-Richtlinien!",
|
||||
"newsPostNotFound": "News-Eintrag nicht gefunden, oder Du hast keinen Zugriff.",
|
||||
"messagePetMountUnEquipped": "Haus- und Reittier in die Stallungen gebracht.",
|
||||
"messageCostumeUnEquipped": "Kostüm abgelegt.",
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
"limitedAvailabilityHours": "Für t <%= hours %>std und <%= minutes %>min verfügbar",
|
||||
"limitedAvailabilityDays": "Für <%= days %>t <%= hours %>std und <%= minutes %>min verfügbar",
|
||||
"amountExp": "<%= amount %> Exp",
|
||||
"helpSupportHabitica": "Hilf dabei, Habitica zu unterstützen",
|
||||
"helpSupportHabitica": "Hilf Habitica zu unterstützen",
|
||||
"groupsPaymentSubBilling": "Dein nächstes Rechnungsdatum ist <strong><%= renewalDate %></strong>.",
|
||||
"groupsPaymentAutoRenew": "Dieses Abonnement läuft automatisch weiter, bis es gekündigt wird. Du kannst es im Gruppen-Abrechnungs-Tab kündigen.",
|
||||
"sellItems": "Items verkaufen",
|
||||
|
||||
@@ -786,7 +786,7 @@
|
||||
"questChameleonNotes": "Es ist ein schöner Tag in einer warmen, regnerischen Ecke der Aufgabenwälder. Du bist auf der Jagd nach Neuzugängen für deine Blattsammlung, als ein Ast vor dir ohne Vorwarnung seine Farbe ändert! Und dann bewegt er sich!<br><br>Rückwärts stolpernd realisierst du, dass dies überhaupt kein Ast ist, sondern ein großes Chamäleon! Jeder Teil seines Körpers wechselt andauernd seine Farbe, während seine Augen in unterschiedliche Richtungen zucken.<br><br>“Geht es dir gut?“ fragst du das Chamäleon.<br><br>“Ahhh, na ja,“ sagt es und wirkt ein wenig durcheinander. „Ich habe versucht, mich anzupassen… aber es ist so überwältigend… die Farben kommen und gehen ständig! Es ist schwer, sich auf nur eine zu konzentrieren….“<br><br>“Aha,“ sagst du, „Ich glaube, ich kann helfen. Wir schärfen deine Konzentration mit einer kleinen Herausforderung! Halte deine Farben bereit!“<br><br>“Die Wette gilt!“ erwidert das Chamäleon.",
|
||||
"questGiraffeBoss": "Gear-affe",
|
||||
"questGiraffeCompletion": "Nachdem du der Gear-affe mit ein bisschen grundlegender Organisation ihres Stapels geholfen hast, fühlt ihr euch beide energiegeladener und motivierter!<br><br>Sie nimmt ihre Gitarre und ein Heft mit Anfängerübungen und spielt ein paar Noten. \"Es fühlt sich gut an, einen Schritt in die richtige Richtung zu machen, selbst wenn es nur ein kleiner ist. Vielen Dank, dass du mir geholfen hast! Nimm diese hier, ich habe gehört, du hast einige Haustiere und diese Kameraden könnten eine nette Ergänzung sein!\"",
|
||||
"questCrabDropCrabEgg": "Krabbe (Ei)",
|
||||
"questCrabDropCrabEgg": "Kabbe (Ei)",
|
||||
"questCrabUnlockText": "Schaltet Krabbeneier zum Kauf auf dem Marktplatz frei.",
|
||||
"questChameleonCompletion": "Nach ein paar lebhaften Drehungen durchlief das Chamäleon alle Farben des Regenbogens und traf perfekt alle Farben, die du verlangt hattest.<br><br>\"Wow,\" sagt es, \"zusammenzuarbeiten, und es zu einem Spiel zu machen, hat mir wirklich geholfen, mich zu konzentrieren! Bitte nimm diese als Belohnung, du hast sie verdient! Bring diesen kleinen Jungen bei, wie man in alle Regenbogenfarben wechselt, wenn sie schlüpfen.\"",
|
||||
"questCrabNotes": "Es ist ein warmer, sonniger Morgen, und Du genießt einen Besuch am Strand, um ein paar Bücher von Deiner Sommerleseliste zu lesen. Du schreckst auf, als du fast auf einen glänzenden Kristall in der Nähe eines flachen Lochs im Sand trittst.<br><br>„Ey, pass auf, wo du hingehst! Ich baue hier eine Wohnhöhle!“, sagt eine Stimme. Eine überraschend große Krabbe mit einem dekorativen Panzer buddelt sich vor Deinen Zehen aus dem Loch und schnappt mit ihrer Schere, während sie spricht.<br><br>„Hm, ist das eine Höhle?“, fragst Du und betrachtest die flache Vertiefung. Es sind Muscheln und Kristalle um sie herum angeordnet, aber es deutet nicht viel auf einen Rückzugsort hin.<br><br>Die Krabbe stottert. „Ey, das ist eine vorurteilsfreie Zone! Ich komme schon noch dazu, ich komme schon noch dazu... Ich bin gerade beim Dekorieren hängen geblieben. Manchmal muss eine Krabbe eben ein wenig Zeit vertrödeln“, sagt sie und rückt eine Schale zurecht.<br><br>„Warum hilfst Du nicht mit, wenn Du schon so großartige Vorstellungen davon hast, wie eine Höhle aussehen soll?“",
|
||||
@@ -810,32 +810,5 @@
|
||||
"questDogDropDogEgg": "Hund (Ei)",
|
||||
"questDogUnlockText": "Schaltet den Kauf von Hundeeiern auf dem Marktplatz frei.",
|
||||
"questDogNotes": "Du wurdest für eine Expedition ausgewählt, um die unterirdischen Höhlensysteme von Habitica zu kartieren! Forscher in Habit City vermuten, dass es in diesen Tiefen neue Werkzeuge für die Bewältigung von Aufgaben oder sogar unentdeckte Kreaturen geben könnte.<br><br>Während du felsige Tunnel in der Nähe der Ausläufer des Wandernden Gebirges erkundest, bemerkst du ein Leuchten, das von einem zerklüfteten Eingang vor dir ausgeht. Als du näher kommst, siehst du... Spielzeug? Plüschtiere und Gummibälle liegen auf dem Höhlenboden verstreut. Hörst du da ein Bellen?<br><br>Ein riesiger, dreiköpfiger Hund springt heraus und stürzt sich auf das Spielzeug, das du gerade aufheben wolltest! Du erstarrst und verlierst dabei fast deinen Arm! Aber... die Mäuler des Hundes scheinen zu sehr mit Spielzeug beschäftigt zu sein, um anzugreifen?<br><br>„Wuff!“, bellt eines der Hundemäuler und lässt einen zerrissenen Spielzeugwaschbären fallen. „Bist du hier, um mir beim Aufräumen zu helfen? Ich muss wirklich aufräumen, aber jedes Mal, wenn ich ein Spielzeug in die Hand nehme, spiele ich nur damit... Hier, denk schnell!!!“<br><br> Der Hund wirft dir einen Ball zu, und dann noch einen und noch einen. Diese zusätzlichen Köpfe machen das Ausweichen zu einem echten Training!",
|
||||
"questDogCompletion": "Nachdem du alle Spielzeuge eingesammelt hast, denen du (zum Glück) ausgewichen bist, gibst du Shiberus einen sanften Klaps auf seinen mittleren Kopf.<br><br>„Es ist schön, sich auf eine große Aufgabe zu freuen, aber es könnte hilfreich sein, mit einem Plan vorzugehen. Vielleicht solltest du das nächste Mal am Eingang anfangen und dich rückwärts vorarbeiten? Oder 30 Minuten am Stück arbeiten und dann eine kurze Spielpause einschieben.\"<br><br>„Das ist eine gute Idee“, meldet sich der linke Kopf des Hundes zu Wort. Der rechte Kopf legt ein paar Gegenstände in deine Nähe, darunter auch etwas, das aussieht wie Eier... „Ich habe ein paar Dinge gefunden, die dir gefallen könnten, während wir gespielt haben. Danke für deine Hilfe!“",
|
||||
"questCatNotes": "An diesem schönen Tag befindest du dich in der Werkstatt des im Effizienz Emporium von Habit City. Du hast eine schwierige Aufgabe: Du sollst neue magische Motivationszaubersprüche entwickeln, um allen Habiticans das Erreichen ihrer Ziele zu vereinfachen.<br><br>Auf dem Tisch vor dir liegt eine Vielzahl magischer Objekte. In all den Büchern stand, dass sie zusammen mit produktiver Energie harmonisieren sollen... Aber bisher ist da noch nicht mal einen Funken Motivation.<br><br>Das Knarren der Tür macht dich auf einen neuen Gast aufmerksam, der die Werkstatt betritt. Herumtollende Füße und ein Ball aus Flausch schnellen auf den Tisch. Eine... Katze? Bevor du überhaupt die Chance hast, ihr ein Kompliment zu machen, wie flauschig sie ist, hebt sie eine Pfote in Richtung der Kristalle, die du aufgestellt hast und... wirft einen vom Tisch!<br><br>\"Hey!\" rufst du, \"Du bist sehr süß, aber ich versuche hier zu arbeiten...\"<br><br>Sie schaut dich mit ihren schönen blauen Augen an, kippt ihren Kopf, und legt ein Bündel Kräuter auf den Tisch. \"Ich helfe doch!\" schnurrt sie.<br><br>Du siehst ihre Pfote, die sie nach den anderen Gegenständen, die du gesammelt hast, ausstreckt und springst zu Boden, um den nächsten Gegenstand aufzufangen!",
|
||||
"questCatText": "Ein verwirrendes Dilemma",
|
||||
"questCatDropCatEgg": "Katze (Ei)",
|
||||
"questCatUnlockText": "Schaltet Katzeneier zum Kauf im Marktplatz frei.",
|
||||
"questCatBoss": "Der schnurrende Verwirrer",
|
||||
"questCatRageTitle": "Wütendes Klopfen",
|
||||
"questCatRageDescription": "Diese Leiste füllt sich, wenn du deine Tagesaufgaben nicht erledigst. Wenn sie voll ist, nimmt der Schnurrende Verwirrer einen Teil der MP deiner Party weg!",
|
||||
"questCatRageEffect": "Der Schnurrende Verwirrer stößt die magischen Gegenstände, die du gesammelt hast, vom Tisch! Die MP der Party werden reduziert!",
|
||||
"questCatCompletion": "Zum Glück hast du alles aufgefangen, was die zudringliche Katze vom Tisch geworfen hat. Als du auf dem Boden sitzt, bemerkst du ein helles Leuchten, das von den Gegenständen vor dir ausgeht. Wenn du nach oben schaust, reagieren die Gegenstände auf dem Tisch auch! Sie auf verschiedene Höhen zu stellen, könnte ein Durchbruch in deinen Forschungen sein! <br><br>„Weißt du, am Ende hast du mir doch geholfen. Ich glaube, ich habe einfach einen neuen Blick auf meine Aufgabe gebraucht, um mich aus der Patsche zu befreien. Ich wünschte allerdings, du hättest mich ein bisschen vorgewarnt, bevor du anfängst, die Dinge herumzuschieben“, sagst du zu der Katze und streichelst sie sanft. <br><br>„Das ist ein sehr vernünftiges Anliegen, bitte nimm das als meine Entschuldigung“, schnurrt sie und schiebt dir ein paar lustig aussehende Eier zu. „Es freut mich, dass ich dir helfen konnte, die Dinge aus einer anderen Schnurrspektive zu sehen.“",
|
||||
"questOtterText": "Der Perfide Verschwörer!",
|
||||
"questOtterDropOtterEgg": "Otter (Ei)",
|
||||
"questOtterUnlockText": "Schält Otter Eier zum Kauf im Marktplatz frei",
|
||||
"questOtterBoss": "Der Verschwörer",
|
||||
"questOtterRageEffect": "Der Verschwörer wirft Teile deiner To-Do Liste in die Luft! Der Boss erhält 30% seiner Lebenspunkte zurück!",
|
||||
"questOtterRageDescription": "Dieser Balken füllt sich, wenn du deine Tagesaufgaben nicht erledigst. Wenn er voll ist, erhält Der Verschwörer einige Lebenspunkte zurück!",
|
||||
"questJadeText": "Ein Matter Unglücksbringer",
|
||||
"questJadeBoss": "Matter Unglücksbringer",
|
||||
"questJadeDropJadePotion": "Jade Schlüpfelixier",
|
||||
"questJadeUnlockText": "Schält Jade Schlüpfelixier zum Kauf im Marktplatz frei.",
|
||||
"questOtterRageTitle": "To-Do Abriß!",
|
||||
"questJadeNotes": "Du bist zu Hause und starrst auf den Stapel dreckigen Geschirrs in der Spüle. Auf den Haufen schmutziger Wäsche in einer wahllosen Ecke des Raums. Auf die leeren Tassen und Snackverpackungen um deinen Tisch herum...<br><br>Du seufzt. „Warum ist da immer noch mehr dreckiges Geschirr... Die Sauerei hört nie auf.“ Es ist so demotivierend. Du findest dich auf der Couch wieder, endlos durch die neuesten Trends scrollend. Wer weiß, wie lang du da warst...<br><br>Als du von deinem Telefon hochsiehst, ist alles grün. Dies ist nicht dein Wohnzimmer. Als du aufstehst, findest du dich an der Seite eines leuchtend grünen Berges wieder.<br><br>Bewegung in der Ferne erregt deine Aufmerksamkeit. Eine steinerne grüne Gestalt grunzt, während sie einen Fels das steinige Terrain hoch wälzt. Er macht einige Fortschritte, aber ein kleiner Ausrutscher seines Fußes lässt den glänzenden Fels zurück nach unten rollen, direkt auf dich zu!<br><br>Er entdeckt dich, als er zu dem Brocken Jade rennt, der auf dich zu poltert! „Du denkst also, der Abwasch ist übel?“ schreit die Gestalt, „Versuch das!“",
|
||||
"questAlpacaText": "Das Überladene Alpaka",
|
||||
"questAlpacaBoss": "Das Überladene Alpaka",
|
||||
"questAlpacaRageEffect": "Das Überladene Alpaka schleudert Gepäck nach dir! Der Boss erhält 30% seiner Gesundheit zurück!",
|
||||
"questAlpacaDropAlpacaEgg": "Alpaka (Ei)",
|
||||
"questAlpacaUnlockText": "Schaltet den Kauf von Alpaka Eiern auf dem Marktplatz frei",
|
||||
"questAlpacaRageDescription": "Dieser Balken füllt sich, wenn du deine Tagesaufgaben nicht erledigst. Wenn er voll ist, erhält Das Überladene Alpaka einen Teil seiner Gesundheit zurück!"
|
||||
"questDogCompletion": "Nachdem du alle Spielzeuge eingesammelt hast, denen du (zum Glück) ausgewichen bist, gibst du Shiberus einen sanften Klaps auf seinen mittleren Kopf.<br><br>„Es ist schön, sich auf eine große Aufgabe zu freuen, aber es könnte hilfreich sein, mit einem Plan vorzugehen. Vielleicht solltest du das nächste Mal am Eingang anfangen und dich rückwärts vorarbeiten? Oder 30 Minuten am Stück arbeiten und dann eine kurze Spielpause einschieben.\"<br><br>„Das ist eine gute Idee“, meldet sich der linke Kopf des Hundes zu Wort. Der rechte Kopf legt ein paar Gegenstände in deine Nähe, darunter auch etwas, das aussieht wie Eier... „Ich habe ein paar Dinge gefunden, die dir gefallen könnten, während wir gespielt haben. Danke für deine Hilfe!“"
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"confirmPass": "Neues Passwort bestätigen",
|
||||
"newUsername": "Neuer Benutzername",
|
||||
"dangerZone": "Gefahrenzone",
|
||||
"resetText1": "<b>Sei vorsichtig!</b> Es werden große Teile Deines Accounts zurückgesetzt. Wir raten dringend davon ab. Jedoch finden einige Spieler diese Funktion sinnvoll, um nach einem anfänglichen Testen der Seite neu beginnen zu können.",
|
||||
"resetText1": "Sei vorsichtig! Es werden große Teile Deines Accounts zurückgesetzt. Wir raten dringend davon ab. Jedoch finden einige Spieler diese Funktion sinnvoll, um nach einem anfänglichen Testen der Seite neu beginnen zu können.",
|
||||
"resetText2": "Eine andere Möglichkeit ist die Verwendung einer <b>Sphäre der Wiedergeburt</b>, die alles andere zurücksetzt, während deine Aufgaben und Ausrüstung erhalten bleiben.",
|
||||
"deleteLocalAccountText": "<b>Bist Du sicher?</b> Dies wird Dein Konto für immer löschen und es kann nicht wiederhergestellt werden! Wenn Du Habitica wieder verwenden möchtest, musst Du ein neues Konto registrieren. Gesparte oder verbrauchte Edelsteine werden nicht ersetzt. Wenn Du absolut sicher bist, dann tippe Dein Passwort in das Textfeld unten ein.",
|
||||
"deleteSocialAccountText": "<b>Bist Du sicher?</b> Dies wird Dein Konto für immer löschen und es kann nicht wiederhergestellt werden! Wenn Du Habitica wieder verwenden möchtest, musst Du ein neues Konto registrieren. Gesparte oder verbrauchte Edelsteine werden nicht ersetzt. Wenn Du absolut sicher bist, dann tippe <b>\"<%= magicWord %>\"</b> in das Textfeld unten ein.",
|
||||
@@ -188,7 +188,7 @@
|
||||
"transaction_release_pets": "Haustiere freigelassen",
|
||||
"transaction_release_mounts": "Reittiere freigelassen",
|
||||
"addPasswordAuth": "Passwort hinzufügen",
|
||||
"nextHourglassDescription": "Abonnierende erhalten eine Mystische Sanduhr, ein Mystisches Ausrüstungsset und Edelsteine, die innerhalb der ersten zwei Tage des Monats auf dem Markt wieder aufgefüllt werden",
|
||||
"nextHourglassDescription": "Abonnierende erhalten eine Mystische Sanduhr, ein Mystisches Ausrüstungsset und Edelsteine, die innerhalb der ersten zwei Tage des Monats auf dem Markt wieder aufgefüllt werden.",
|
||||
"gemCap": "Edelsteinobergrenze",
|
||||
"nextHourglass": "Nächste Lieferung einer Mystischen Sanduhr",
|
||||
"adjustment": "Änderung",
|
||||
|
||||
@@ -259,8 +259,5 @@
|
||||
"earn2Gems": "Verdiene <strong>+2 Edelsteine</strong> für jeden Monat, in dem du abonniert hast",
|
||||
"subscribeAgainContinueHourglasses": "Erneuere Dein Abonnement, um weiterhin Mystische Sanduhren zu erhalten",
|
||||
"mysterySet202411": "Borstenkämpfer Set",
|
||||
"mysterySet202501": "Frostbinder-Set",
|
||||
"mysterySet202502": "Herzliches Harlekin-Set",
|
||||
"mysterySet202503": "Jade Juggernaut Set",
|
||||
"mysterySet202504": "Scheues Yeti Set"
|
||||
"mysterySet202501": "Frostbinder-Set"
|
||||
}
|
||||
|
||||
@@ -1011,18 +1011,6 @@
|
||||
"backgroundWinterLandscapeWithCabinText": "Winter Landscape with Cabin",
|
||||
"backgroundWinterLandscapeWithCabinNotes": "Stay cozy in a Winter Landscape with a Cabin.",
|
||||
|
||||
"backgrounds022025": "SET 129: Released February 2025",
|
||||
"backgroundOldFashionedTeaShopText": "Old Fashioned Tea Shop",
|
||||
"backgroundOldFashionedTeaShopNotes": "Enjoy a cozy beverage in an Old Fashioned Tea Shop.",
|
||||
|
||||
"backgrounds032025": "SET 130: Released March 2025",
|
||||
"backgroundMountainSceneWithBlossomsText": "Mountain Scene with Blossoms",
|
||||
"backgroundMountainSceneWithBlossomsNotes": "Take in the lovely sights and scents of a Mountain Scene with Blossoms.",
|
||||
|
||||
"backgrounds0420205": "SET 131: Released April 2025",
|
||||
"backgroundGardenWithFlowerBedsText": "Garden with Flower Beds",
|
||||
"backgroundGardenWithFlowerBedsNotes": "Enjoy the blooms of spring in a Garden with Flower Beds.",
|
||||
|
||||
"timeTravelBackgrounds": "Steampunk Backgrounds",
|
||||
"backgroundAirshipText": "Airship",
|
||||
"backgroundAirshipNotes": "Become a sky sailor on board your very own Airship.",
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
"allocatePerPop": "Add a Point to Perception",
|
||||
"allocateInt": "Points allocated to Intelligence:",
|
||||
"allocateIntPop": "Add a Point to Intelligence",
|
||||
"noMoreAllocate": "Now that you've hit level 100, you won't gain any more Stat Points. You can continue leveling up, or start a new adventure at level 1 by using the <a href='/shops/market'>Orb of Rebirth</a>!",
|
||||
"noMoreAllocate": "Now that you've hit level 100, you won't gain any more Stat Points. You can continue leveling up, or start a new adventure at level 1 by using the <a href='https://habitica.fandom.com/wiki/Orb_of_Rebirth' target='_blank'>Orb of Rebirth</a>!",
|
||||
"stats": "Stats",
|
||||
"strength": "Strength",
|
||||
"strText": "Strength increases the chance of random \"critical hits\" and the Gold, Experience, and drop chance boost from them. It also helps deal damage to boss monsters.",
|
||||
@@ -133,8 +133,8 @@
|
||||
"healerText": "Healers stand impervious against harm, and extend that protection to others. Missed Dailies and bad Habits don't faze them much, and they have ways to recover Health from failure. Play a Healer if you enjoy assisting others in your Party, or if the idea of cheating Death through hard work inspires you!",
|
||||
"optOutOfClasses": "Opt Out",
|
||||
"chooseClass": "Choose your Class",
|
||||
"chooseClassLearnMarkdown": "[Learn more about Habitica's class system](/static/faq#what-classes)",
|
||||
"optOutOfClassesText": "Not ready to choose? There's no rush! If you opt out, you can read about each Class in <a href='/static/faq#what-classes' target='_blank'>our FAQ</a> and visit Settings to enable the Class System when you're ready.",
|
||||
"chooseClassLearnMarkdown": "[Learn more about Habitica's class system](https://habitica.fandom.com/wiki/Class_System)",
|
||||
"optOutOfClassesText": "Can't be bothered with classes? Want to choose later? Opt out - you'll be a warrior with no special abilities. You can read about the class system later on the wiki and enable classes at any time under User Icon > Settings.",
|
||||
"selectClass": "Select <%= heroClass %>",
|
||||
"select": "Select",
|
||||
"stealth": "Stealth",
|
||||
|
||||
@@ -269,19 +269,7 @@
|
||||
|
||||
"questEggDogText": "Puppy",
|
||||
"questEggDogMountText": "Dog",
|
||||
"questEggDogAdjective": "a friendly",
|
||||
|
||||
"questEggCatText": "Kitten",
|
||||
"questEggCatMountText": "Cat",
|
||||
"questEggCatAdjective": "a mischievous",
|
||||
|
||||
"questEggOtterText": "Otter",
|
||||
"questEggOtterMountText": "Otter",
|
||||
"questEggOtterAdjective": "a perfidious",
|
||||
|
||||
"questEggAlpacaText": "Alpaca",
|
||||
"questEggAlpacaMountText": "Alpaca",
|
||||
"questEggAlpacaAdjective": "an overpacked",
|
||||
"questEggDogAdjective": "a friendly",
|
||||
|
||||
"eggNotes": "Find a hatching potion to pour on this egg, and it will hatch into <%= eggAdjective(locale) %> <%= eggText(locale) %>.",
|
||||
|
||||
@@ -348,14 +336,9 @@
|
||||
"hatchingPotionFungi": "Fungi",
|
||||
"hatchingPotionKoi": "Koi",
|
||||
"hatchingPotionGingerbread": "Gingerbread",
|
||||
"hatchingPotionJade": "Jade",
|
||||
"hatchingPotionBalloon": "Balloon",
|
||||
"hatchingPotionCryptid": "Cryptid",
|
||||
|
||||
"hatchingPotionNotes": "Pour this on an egg, and it will hatch as a <%= potText(locale) %> Pet.",
|
||||
"premiumPotionUnlimitedNotes": "Not usable on Quest Pet eggs.",
|
||||
"wackyPotionNotes": "Pour this on an egg, and it will hatch as a Wacky <%= potText(locale) %> Pet.",
|
||||
"wackyPotionAddlNotes": "Cannot be raised to Mounts or used on Quest Pet eggs.",
|
||||
|
||||
"foodMeat": "Meat",
|
||||
"foodMeatThe": "the Meat",
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
"backerTier": "Backer Tier",
|
||||
"playerTiers": "Player Tiers",
|
||||
"tier": "Tier",
|
||||
"conRewardsURL": "https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica#contributor-tier-rewards",
|
||||
"conRewardsURL": "https://habitica.fandom.com/wiki/Contributor_Rewards",
|
||||
"surveysSingle": "Helped Habitica grow, either by filling out a survey or helping with a major testing effort. Thank you!",
|
||||
"surveysMultiple": "Helped Habitica grow on <%= count %> occasions, either by filling out a survey or helping with a major testing effort. Thank you!",
|
||||
"blurbHallPatrons": "This is the Hall of Patrons, where we honor the noble adventurers who backed Habitica's original Kickstarter. We thank them for helping us bring Habitica to life!",
|
||||
"blurbHallContributors": "This is the Hall of Contributors, where open-source contributors to Habitica are honored. Whether through code, art, music, writing, or even just helpfulness, they have earned <a href='https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica#contributor-tier-rewards' target='_blank'>Gems, exclusive Equipment</a>, and <a href='https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica#contributor-tiers' target='_blank'>prestigious titles</a>. You can contribute to Habitica, too! <a href='https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica' target='_blank'>Find out more here.</a>"
|
||||
"blurbHallContributors": "This is the Hall of Contributors, where open-source contributors to Habitica are honored. Whether through code, art, music, writing, or even just helpfulness, they have earned <a href='https://habitica.fandom.com/wiki/Contributor_Rewards' target='_blank'> gems, exclusive equipment</a>, and <a href='https://habitica.fandom.com/wiki/Contributor_Titles' target='_blank'>prestigious titles</a>. You can contribute to Habitica, too! <a href='https://habitica.fandom.com/wiki/Contributing_to_Habitica' target='_blank'> Find out more here. </a>"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user