mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-05-24 02:52:20 -05:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a22038d05 | |||
| 8e1bc6bcd7 | |||
| beb51fb00d | |||
| 7548834442 | |||
| 2a4886b325 | |||
| a1d0403782 | |||
| 9de6f7b3bc | |||
| d6bd10fa25 | |||
| 55f3792c96 | |||
| a390dd82a7 | |||
| d47f30fc10 | |||
| 03744c63f6 | |||
| f2a418e3fa | |||
| 1477326351 | |||
| 38b39b600c | |||
| 1823f658c6 | |||
| 9a3e3c93eb | |||
| 7a94b031e0 | |||
| 22b5a5e6f2 | |||
| 132918419a |
@@ -21,6 +21,7 @@ RUN npm install -g gulp-cli mocha
|
||||
RUN mkdir -p /usr/src/habitrpg
|
||||
WORKDIR /usr/src/habitrpg
|
||||
RUN git clone --branch release --depth 1 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
|
||||
RUN git config --global url."https://".insteadOf git://
|
||||
RUN npm set unsafe-perm true
|
||||
RUN npm install
|
||||
|
||||
|
||||
+1
-1
Submodule habitica-images updated: 05c7c19fa5...08edadc432
Generated
+287
-99
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "4.229.1",
|
||||
"version": "4.230.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz",
|
||||
"integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
|
||||
"integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
|
||||
"requires": {
|
||||
"@jridgewell/trace-mapping": "^0.3.0"
|
||||
"@jridgewell/gen-mapping": "^0.1.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
}
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
@@ -26,20 +27,20 @@
|
||||
"integrity": "sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q=="
|
||||
},
|
||||
"@babel/core": {
|
||||
"version": "7.17.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.9.tgz",
|
||||
"integrity": "sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw==",
|
||||
"version": "7.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.10.tgz",
|
||||
"integrity": "sha512-liKoppandF3ZcBnIYFjfSDHZLKdLHGJRkoWtG8zQyGJBQfIYobpnVGI5+pLBNtS6psFLDzyq8+h5HiVljW9PNA==",
|
||||
"requires": {
|
||||
"@ampproject/remapping": "^2.1.0",
|
||||
"@babel/code-frame": "^7.16.7",
|
||||
"@babel/generator": "^7.17.9",
|
||||
"@babel/helper-compilation-targets": "^7.17.7",
|
||||
"@babel/generator": "^7.17.10",
|
||||
"@babel/helper-compilation-targets": "^7.17.10",
|
||||
"@babel/helper-module-transforms": "^7.17.7",
|
||||
"@babel/helpers": "^7.17.9",
|
||||
"@babel/parser": "^7.17.9",
|
||||
"@babel/parser": "^7.17.10",
|
||||
"@babel/template": "^7.16.7",
|
||||
"@babel/traverse": "^7.17.9",
|
||||
"@babel/types": "^7.17.0",
|
||||
"@babel/traverse": "^7.17.10",
|
||||
"@babel/types": "^7.17.10",
|
||||
"convert-source-map": "^1.7.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@@ -56,28 +57,28 @@
|
||||
}
|
||||
},
|
||||
"@babel/compat-data": {
|
||||
"version": "7.17.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.7.tgz",
|
||||
"integrity": "sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ=="
|
||||
"version": "7.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.10.tgz",
|
||||
"integrity": "sha512-GZt/TCsG70Ms19gfZO1tM4CVnXsPgEPBCpJu+Qz3L0LUDsY5nZqFZglIoPC1kIYOtNBZlrnFT+klg12vFGZXrw=="
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.17.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.9.tgz",
|
||||
"integrity": "sha512-rAdDousTwxbIxbz5I7GEQ3lUip+xVCXooZNbsydCWs3xA7ZsYOv+CFRdzGxRX78BmQHu9B1Eso59AOZQOJDEdQ==",
|
||||
"version": "7.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz",
|
||||
"integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.17.0",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
"@babel/types": "^7.17.10",
|
||||
"@jridgewell/gen-mapping": "^0.1.0",
|
||||
"jsesc": "^2.5.1"
|
||||
}
|
||||
},
|
||||
"@babel/helper-compilation-targets": {
|
||||
"version": "7.17.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz",
|
||||
"integrity": "sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==",
|
||||
"version": "7.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.10.tgz",
|
||||
"integrity": "sha512-gh3RxjWbauw/dFiU/7whjd0qN9K6nPJMqe6+Er7rOavFh0CQUSwhAE3IcTho2rywPJFxej6TUUHDkWcYI6gGqQ==",
|
||||
"requires": {
|
||||
"@babel/compat-data": "^7.17.7",
|
||||
"@babel/compat-data": "^7.17.10",
|
||||
"@babel/helper-validator-option": "^7.16.7",
|
||||
"browserslist": "^4.17.5",
|
||||
"browserslist": "^4.20.2",
|
||||
"semver": "^6.3.0"
|
||||
}
|
||||
},
|
||||
@@ -129,36 +130,53 @@
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.17.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.9.tgz",
|
||||
"integrity": "sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg=="
|
||||
"version": "7.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.10.tgz",
|
||||
"integrity": "sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ=="
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.17.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.9.tgz",
|
||||
"integrity": "sha512-PQO8sDIJ8SIwipTPiR71kJQCKQYB5NGImbOviK8K+kg5xkNSYXLBupuX9QhatFowrsvo9Hj8WgArg3W7ijNAQw==",
|
||||
"version": "7.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz",
|
||||
"integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==",
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.16.7",
|
||||
"@babel/generator": "^7.17.9",
|
||||
"@babel/generator": "^7.17.10",
|
||||
"@babel/helper-environment-visitor": "^7.16.7",
|
||||
"@babel/helper-function-name": "^7.17.9",
|
||||
"@babel/helper-hoist-variables": "^7.16.7",
|
||||
"@babel/helper-split-export-declaration": "^7.16.7",
|
||||
"@babel/parser": "^7.17.9",
|
||||
"@babel/types": "^7.17.0",
|
||||
"@babel/parser": "^7.17.10",
|
||||
"@babel/types": "^7.17.10",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
|
||||
"integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==",
|
||||
"version": "7.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz",
|
||||
"integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.16.7",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"version": "4.20.3",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz",
|
||||
"integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==",
|
||||
"requires": {
|
||||
"caniuse-lite": "^1.0.30001332",
|
||||
"electron-to-chromium": "^1.4.118",
|
||||
"escalade": "^3.1.1",
|
||||
"node-releases": "^2.0.3",
|
||||
"picocolors": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001334",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001334.tgz",
|
||||
"integrity": "sha512-kbaCEBRRVSoeNs74sCuq92MJyGrMtjWVfhltoHUCW4t4pXFvGjUBrfo47weBRViHkiV3eBYyIsfl956NtHGazw=="
|
||||
},
|
||||
"chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
@@ -169,10 +187,10 @@
|
||||
"supports-color": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
|
||||
"node-releases": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.4.tgz",
|
||||
"integrity": "sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
@@ -450,13 +468,13 @@
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.17.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.9.tgz",
|
||||
"integrity": "sha512-rAdDousTwxbIxbz5I7GEQ3lUip+xVCXooZNbsydCWs3xA7ZsYOv+CFRdzGxRX78BmQHu9B1Eso59AOZQOJDEdQ==",
|
||||
"version": "7.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.10.tgz",
|
||||
"integrity": "sha512-46MJZZo9y3o4kmhBVc7zW7i8dtR1oIK/sdO5NcfcZRhTGYi+KKJRtHNgsU6c4VUcJmUNV/LQdebD/9Dlv4K+Tg==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.17.0",
|
||||
"jsesc": "^2.5.1",
|
||||
"source-map": "^0.5.0"
|
||||
"@babel/types": "^7.17.10",
|
||||
"@jridgewell/gen-mapping": "^0.1.0",
|
||||
"jsesc": "^2.5.1"
|
||||
}
|
||||
},
|
||||
"@babel/helper-function-name": {
|
||||
@@ -484,31 +502,31 @@
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.17.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.9.tgz",
|
||||
"integrity": "sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg=="
|
||||
"version": "7.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.10.tgz",
|
||||
"integrity": "sha512-n2Q6i+fnJqzOaq2VkdXxy2TCPCWQZHiCo0XqmrCvDWcZQKRyZzYi4Z0yxlBuN0w+r2ZHmre+Q087DSrw3pbJDQ=="
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.17.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.9.tgz",
|
||||
"integrity": "sha512-PQO8sDIJ8SIwipTPiR71kJQCKQYB5NGImbOviK8K+kg5xkNSYXLBupuX9QhatFowrsvo9Hj8WgArg3W7ijNAQw==",
|
||||
"version": "7.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.10.tgz",
|
||||
"integrity": "sha512-VmbrTHQteIdUUQNTb+zE12SHS/xQVIShmBPhlNP12hD5poF2pbITW1Z4172d03HegaQWhLffdkRJYtAzp0AGcw==",
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.16.7",
|
||||
"@babel/generator": "^7.17.9",
|
||||
"@babel/generator": "^7.17.10",
|
||||
"@babel/helper-environment-visitor": "^7.16.7",
|
||||
"@babel/helper-function-name": "^7.17.9",
|
||||
"@babel/helper-hoist-variables": "^7.16.7",
|
||||
"@babel/helper-split-export-declaration": "^7.16.7",
|
||||
"@babel/parser": "^7.17.9",
|
||||
"@babel/types": "^7.17.0",
|
||||
"@babel/parser": "^7.17.10",
|
||||
"@babel/types": "^7.17.10",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
|
||||
"integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==",
|
||||
"version": "7.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.10.tgz",
|
||||
"integrity": "sha512-9O26jG0mBYfGkUYCYZRnBwbVLd1UZOICEr2Em6InB6jVfsAv1GKgwXHmrSg+WFWDmeKTA6vyTZiN8tCSM5Oo3A==",
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.16.7",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
@@ -1432,10 +1450,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@jridgewell/gen-mapping": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
|
||||
"integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
|
||||
"requires": {
|
||||
"@jridgewell/set-array": "^1.0.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"@jridgewell/resolve-uri": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz",
|
||||
"integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew=="
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.6.tgz",
|
||||
"integrity": "sha512-R7xHtBSNm+9SyvpJkdQl+qrM3Hm2fea3Ef197M3mUug+v+yR+Rhfbs7PBtcBUVnIWJ4JcAdjvij+c8hXS9p5aw=="
|
||||
},
|
||||
"@jridgewell/set-array": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.0.tgz",
|
||||
"integrity": "sha512-SfJxIxNVYLTsKwzB3MoOQ1yxf4w/E6MdkvTgrgAt1bfxjSrLUoHMKrDOykwN14q65waezZIdqDneUIPh4/sKxg=="
|
||||
},
|
||||
"@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.11",
|
||||
@@ -1443,9 +1475,9 @@
|
||||
"integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg=="
|
||||
},
|
||||
"@jridgewell/trace-mapping": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz",
|
||||
"integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==",
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"requires": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
@@ -1675,9 +1707,9 @@
|
||||
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="
|
||||
},
|
||||
"@types/body-parser": {
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz",
|
||||
"integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==",
|
||||
"version": "1.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
|
||||
"integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
|
||||
"requires": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
@@ -1759,9 +1791,9 @@
|
||||
}
|
||||
},
|
||||
"@types/express-serve-static-core": {
|
||||
"version": "4.17.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz",
|
||||
"integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==",
|
||||
"version": "4.17.28",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz",
|
||||
"integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==",
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
@@ -1769,9 +1801,9 @@
|
||||
}
|
||||
},
|
||||
"@types/express-unless": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.2.tgz",
|
||||
"integrity": "sha512-Q74UyYRX/zIgl1HSp9tUX2PlG8glkVm+59r7aK4KGKzC5jqKIOX6rrVLRQrzpZUQ84VukHtRoeAuon2nIssHPQ==",
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.3.tgz",
|
||||
"integrity": "sha512-TyPLQaF6w8UlWdv4gj8i46B+INBVzURBNRahCozCSXfsK2VTlL1wNyTlMKw817VHygBtlcl5jfnPadlydr06Yw==",
|
||||
"requires": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
@@ -2347,6 +2379,19 @@
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"optional": true
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
@@ -5463,6 +5508,11 @@
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.4.129",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.129.tgz",
|
||||
"integrity": "sha512-GgtN6bsDtHdtXJtlMYZWGB/uOyjZWjmRDumXTas7dGBaB9zUyCjzHet1DY2KhyHN8R0GLbzZWqm4efeddqqyRQ=="
|
||||
},
|
||||
"emitter-listener": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz",
|
||||
@@ -6259,6 +6309,22 @@
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"yallist": {
|
||||
@@ -6813,6 +6879,22 @@
|
||||
"requires": {
|
||||
"glob": "^7.0.3",
|
||||
"minimatch": "^3.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
@@ -6928,6 +7010,21 @@
|
||||
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7413,16 +7510,34 @@
|
||||
"dev": true
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.1.tgz",
|
||||
"integrity": "sha512-cF7FYZZ47YzmCu7dDy50xSRRfO3ErRfrXuLZcNIuyiJEco0XSrGtuilG19L5xp3NcwTx7Gn+X6Tv3fmsUPTbow==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"minimatch": "^5.0.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
|
||||
"integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
|
||||
"requires": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
@@ -7450,6 +7565,19 @@
|
||||
"unique-stream": "^2.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
|
||||
@@ -7744,6 +7872,19 @@
|
||||
"slash": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"ignore": {
|
||||
"version": "5.1.8",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
|
||||
@@ -9219,6 +9360,22 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9455,21 +9612,21 @@
|
||||
}
|
||||
},
|
||||
"jwks-rsa": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.0.5.tgz",
|
||||
"integrity": "sha512-fliHfsiBRzEU0nXzSvwnh0hynzGB0WihF+CinKbSRlaqRxbqqKf2xbBPgwc8mzf18/WgwlG8e5eTpfSTBcU4DQ==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.0.tgz",
|
||||
"integrity": "sha512-GKOSDBWWBCiQTzawei6mEdRQvji5gecj8F9JwMt0ZOPnBPSmTjo5CKFvvbhE7jGPkU159Cpi0+OTLuABFcNOQQ==",
|
||||
"requires": {
|
||||
"@types/express-jwt": "0.0.42",
|
||||
"debug": "^4.3.2",
|
||||
"debug": "^4.3.4",
|
||||
"jose": "^2.0.5",
|
||||
"limiter": "^1.1.5",
|
||||
"lru-memoizer": "^2.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
@@ -10534,6 +10691,22 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12138,9 +12311,9 @@
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
||||
},
|
||||
"rate-limiter-flexible": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.3.6.tgz",
|
||||
"integrity": "sha512-8DVFOe89rreyut/vzwBI7vgXJynyYoYnH5XogtAKs0F/neAbCTTglXxSJ7fZeZamcFXZDvMidCBvps4KM+1srw=="
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.3.7.tgz",
|
||||
"integrity": "sha512-dmc+J/IffVBvHlqq5/XClsdLdkOdQV/tjrz00cwneHUbEDYVrf4aUDAyR4Jybcf2+Vpn4NwoVrnnAyt/D0ciWw=="
|
||||
},
|
||||
"raw-body": {
|
||||
"version": "2.5.1",
|
||||
@@ -12708,6 +12881,21 @@
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"run-async": {
|
||||
@@ -13806,21 +13994,21 @@
|
||||
"integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ=="
|
||||
},
|
||||
"superagent": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.2.tgz",
|
||||
"integrity": "sha512-o9/fP6dww7a4xmEF5a484o2rG34UUGo8ztDlv7vbCWuqPhpndMi0f7eXxdlryk5U12Kzy46nh8eNpLAJ93Alsg==",
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-7.1.3.tgz",
|
||||
"integrity": "sha512-WA6et4nAvgBCS73lJvv1D0ssI5uk5Gh+TGN/kNe+B608EtcVs/yzfl+OLXTzDs7tOBDIpvgh/WUs1K2OK1zTeQ==",
|
||||
"requires": {
|
||||
"component-emitter": "^1.3.0",
|
||||
"cookiejar": "^2.1.3",
|
||||
"debug": "^4.3.3",
|
||||
"debug": "^4.3.4",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"form-data": "^4.0.0",
|
||||
"formidable": "^2.0.1",
|
||||
"methods": "^1.1.2",
|
||||
"mime": "^2.5.0",
|
||||
"qs": "^6.10.1",
|
||||
"qs": "^6.10.3",
|
||||
"readable-stream": "^3.6.0",
|
||||
"semver": "^7.3.5"
|
||||
"semver": "^7.3.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
@@ -13840,9 +14028,9 @@
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.229.1",
|
||||
"version": "4.230.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.17.9",
|
||||
"@babel/core": "^7.17.10",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/register": "^7.17.7",
|
||||
"@google-cloud/trace-agent": "^5.1.6",
|
||||
@@ -30,7 +30,7 @@
|
||||
"express": "^4.17.3",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"express-validator": "^5.2.0",
|
||||
"glob": "^7.2.0",
|
||||
"glob": "^8.0.1",
|
||||
"got": "^11.8.3",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^8.0.0",
|
||||
@@ -43,7 +43,7 @@
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"js2xmlparser": "^4.0.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwks-rsa": "^2.0.5",
|
||||
"jwks-rsa": "^2.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"merge-stream": "^2.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
@@ -61,14 +61,14 @@
|
||||
"paypal-rest-sdk": "^1.8.1",
|
||||
"pp-ipn": "^1.1.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"rate-limiter-flexible": "^2.3.6",
|
||||
"rate-limiter-flexible": "^2.3.7",
|
||||
"redis": "^3.1.2",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^4.2.0",
|
||||
"stripe": "^8.219.0",
|
||||
"superagent": "^7.1.2",
|
||||
"superagent": "^7.1.3",
|
||||
"universal-analytics": "^0.5.3",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^8.3.2",
|
||||
|
||||
@@ -99,23 +99,26 @@ describe('Items Utils', () => {
|
||||
expect(castItemVal('items.food.Cake_Invalid', '5')).to.equal(5);
|
||||
});
|
||||
|
||||
it('converts values for mounts paths to numbers', () => {
|
||||
expect(castItemVal('items.mounts.Cactus-Base', 'true')).to.equal(true);
|
||||
expect(castItemVal('items.mounts.Aether-Invisible', 'false')).to.equal(false);
|
||||
expect(castItemVal('items.mounts.Aether-Invalid', 'true')).to.equal(true);
|
||||
expect(castItemVal('items.mounts.Aether-Invalid', 'truish')).to.equal(true);
|
||||
expect(castItemVal('items.mounts.Aether-Invalid', 0)).to.equal(false);
|
||||
});
|
||||
|
||||
it('converts values for quests paths to numbers', () => {
|
||||
expect(castItemVal('items.quests.atom3', '5')).to.equal(5);
|
||||
expect(castItemVal('items.quests.invalid', '5')).to.equal(5);
|
||||
});
|
||||
|
||||
it('converts values for owned gear', () => {
|
||||
it('converts values for mounts paths to true/null', () => {
|
||||
// mounts are never false but can be null (function contains more details)
|
||||
expect(castItemVal('items.mounts.Cactus-Base', 'true')).to.equal(true);
|
||||
expect(castItemVal('items.mounts.Aether-Invisible', 'null')).to.equal(null);
|
||||
expect(castItemVal('items.mounts.Aether-Invisible', 'false')).to.equal(null);
|
||||
expect(castItemVal('items.mounts.Aether-Invalid', 'true')).to.equal(true);
|
||||
expect(castItemVal('items.mounts.Aether-Invalid', 'truthy')).to.equal(true);
|
||||
expect(castItemVal('items.mounts.Aether-Invalid', 0)).to.equal(null);
|
||||
});
|
||||
|
||||
it('converts values for owned gear to true/false', () => {
|
||||
expect(castItemVal('items.gear.owned.shield_warrior_0', 'true')).to.equal(true);
|
||||
expect(castItemVal('items.gear.owned.invalid', 'false')).to.equal(false);
|
||||
expect(castItemVal('items.gear.owned.invalid', 'thruthy')).to.equal(true);
|
||||
expect(castItemVal('items.gear.owned.invalid', 'null')).to.equal(false);
|
||||
expect(castItemVal('items.gear.owned.invalid', 'truthy')).to.equal(true);
|
||||
expect(castItemVal('items.gear.owned.invalid', 0)).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,8 +4,7 @@ import {
|
||||
generateReq,
|
||||
generateNext,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import { ensureAdmin, ensureSudo, ensureNewsPoster } from '../../../../website/server/middlewares/ensureAccessRight';
|
||||
import { ensurePermission } from '../../../../website/server/middlewares/ensureAccessRight';
|
||||
import { NotAuthorized } from '../../../../website/server/libs/errors';
|
||||
import apiError from '../../../../website/server/libs/apiError';
|
||||
|
||||
@@ -20,20 +19,20 @@ describe('ensure access middlewares', () => {
|
||||
});
|
||||
|
||||
context('ensure admin', () => {
|
||||
it('returns not authorized when user is not an admin', () => {
|
||||
res.locals = { user: { contributor: { admin: false } } };
|
||||
it('returns not authorized when user is not in userSupport', () => {
|
||||
res.locals = { user: { permissions: { userSupport: false } } };
|
||||
|
||||
ensureAdmin(req, res, next);
|
||||
ensurePermission('userSupport')(req, res, next);
|
||||
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal(i18n.t('noAdminAccess'));
|
||||
expect(calledWith[0].message).to.equal(apiError('noPrivAccess'));
|
||||
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
|
||||
});
|
||||
|
||||
it('passes when user is an admin', () => {
|
||||
res.locals = { user: { contributor: { admin: true } } };
|
||||
it('passes when user is an userSuppor', () => {
|
||||
res.locals = { user: { permissions: { userSupport: true } } };
|
||||
|
||||
ensureAdmin(req, res, next);
|
||||
ensurePermission('userSupport')(req, res, next);
|
||||
|
||||
expect(next).to.be.calledOnce;
|
||||
expect(next.args[0]).to.be.empty;
|
||||
@@ -42,40 +41,40 @@ describe('ensure access middlewares', () => {
|
||||
|
||||
context('ensure newsPoster', () => {
|
||||
it('returns not authorized when user is not a newsPoster', () => {
|
||||
res.locals = { user: { contributor: { newsPoster: false } } };
|
||||
res.locals = { user: { permissions: { news: false } } };
|
||||
|
||||
ensureNewsPoster(req, res, next);
|
||||
ensurePermission('news')(req, res, next);
|
||||
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal(apiError('noNewsPosterAccess'));
|
||||
expect(calledWith[0].message).to.equal(apiError('noPrivAccess'));
|
||||
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
|
||||
});
|
||||
|
||||
it('passes when user is a newsPoster', () => {
|
||||
res.locals = { user: { contributor: { newsPoster: true } } };
|
||||
res.locals = { user: { permissions: { news: true } } };
|
||||
|
||||
ensureNewsPoster(req, res, next);
|
||||
ensurePermission('news')(req, res, next);
|
||||
|
||||
expect(next).to.be.calledOnce;
|
||||
expect(next.args[0]).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
||||
context('ensure sudo', () => {
|
||||
it('returns not authorized when user is not a sudo user', () => {
|
||||
res.locals = { user: { contributor: { sudo: false } } };
|
||||
context('ensure coupons', () => {
|
||||
it('returns not authorized when user does not have access to coupon calls', () => {
|
||||
res.locals = { user: { permissions: { coupons: false } } };
|
||||
|
||||
ensureSudo(req, res, next);
|
||||
ensurePermission('coupons')(req, res, next);
|
||||
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal(apiError('noSudoAccess'));
|
||||
expect(calledWith[0].message).to.equal(apiError('noPrivAccess'));
|
||||
expect(calledWith[0] instanceof NotAuthorized).to.equal(true);
|
||||
});
|
||||
|
||||
it('passes when user is a sudo user', () => {
|
||||
res.locals = { user: { contributor: { sudo: true } } };
|
||||
it('passes when user has access to coupon calls', () => {
|
||||
res.locals = { user: { permissions: { coupons: true } } };
|
||||
|
||||
ensureSudo(req, res, next);
|
||||
ensurePermission('coupons')(req, res, next);
|
||||
|
||||
expect(next).to.be.calledOnce;
|
||||
expect(next.args[0]).to.be.empty;
|
||||
|
||||
@@ -1029,7 +1029,7 @@ describe('Group Model', () => {
|
||||
expect(toJSON.chat.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('shows messages with >= 2 flag to admins', async () => {
|
||||
it('shows messages with >= 2 flag to moderators', async () => {
|
||||
party.chat = [{
|
||||
flagCount: 3,
|
||||
info: {
|
||||
@@ -1037,12 +1037,12 @@ describe('Group Model', () => {
|
||||
quest: 'basilist',
|
||||
},
|
||||
}];
|
||||
const admin = new User({ 'contributor.admin': true });
|
||||
const admin = new User({ 'permissions.moderator': true });
|
||||
const toJSON = await Group.toJSONCleanChat(party, admin);
|
||||
expect(toJSON.chat.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('doesn\'t show flagged messages to non-admins', async () => {
|
||||
it('doesn\'t show flagged messages to non-moderators', async () => {
|
||||
party.chat = [{
|
||||
flagCount: 3,
|
||||
info: {
|
||||
|
||||
@@ -877,7 +877,7 @@ describe('User Model', () => {
|
||||
|
||||
expect(user.isNewsPoster()).to.equal(false);
|
||||
|
||||
user.contributor.newsPoster = true;
|
||||
user.permissions = { news: true };
|
||||
expect(user.isNewsPoster()).to.equal(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ describe('GET challenges/groups/:groupId', () => {
|
||||
publicGuild = group;
|
||||
|
||||
await user.update({
|
||||
'contributor.admin': true,
|
||||
'permissions.challengeAdmin': true,
|
||||
});
|
||||
|
||||
officialChallenge = await generateChallenge(user, group, {
|
||||
|
||||
@@ -231,7 +231,7 @@ describe('GET challenges/user', () => {
|
||||
publicGuild = group;
|
||||
|
||||
await user.update({
|
||||
'contributor.admin': true,
|
||||
'permissions.challengeAdmin': true,
|
||||
});
|
||||
|
||||
officialChallenge = await generateChallenge(user, group, {
|
||||
|
||||
@@ -203,8 +203,8 @@ describe('POST /challenges', () => {
|
||||
|
||||
it('sets challenge as official if created by admin and official flag is set', async () => {
|
||||
await groupLeader.update({
|
||||
contributor: {
|
||||
admin: true,
|
||||
permissions: {
|
||||
challengeAdmin: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
|
||||
message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
|
||||
message = message.message;
|
||||
userThatDidNotCreateChat = await generateUser();
|
||||
admin = await generateUser({ 'contributor.admin': true });
|
||||
admin = await generateUser({ 'permissions.moderator': true });
|
||||
});
|
||||
|
||||
context('Chat errors', () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('POST /chat/:chatId/flag', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({ balance: 1, 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
|
||||
admin = await generateUser({ balance: 1, 'contributor.admin': true });
|
||||
admin = await generateUser({ balance: 1, 'permissions.moderator': true });
|
||||
anotherUser = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
|
||||
newUser = await generateUser({ 'auth.timestamps.created': moment().subtract(1, 'days').toDate() });
|
||||
sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve());
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
|
||||
groupWithChat = group;
|
||||
author = groupLeader;
|
||||
nonAdmin = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
|
||||
admin = await generateUser({ 'contributor.admin': true });
|
||||
admin = await generateUser({ 'permissions.moderator': true });
|
||||
|
||||
message = await author.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
|
||||
message = message.message;
|
||||
|
||||
@@ -14,18 +14,18 @@ describe('GET /coupons/', () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('returns an error if user has no sudo permission', async () => {
|
||||
it('returns an error if user has no coupons permission', async () => {
|
||||
await user.get('/user'); // needed so the request after this will authenticate with the correct cookie session
|
||||
await expect(user.get('/coupons')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: apiError('noSudoAccess'),
|
||||
message: apiError('noPrivAccess'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the coupons in CSV format ordered by creation date', async () => {
|
||||
await user.update({
|
||||
'contributor.sudo': true,
|
||||
'permissions.coupons': true,
|
||||
});
|
||||
|
||||
const coupons = await user.post('/coupons/generate/wondercon?count=11');
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('POST /coupons/enter/:code', () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
sudoUser = await generateUser({
|
||||
'contributor.sudo': true,
|
||||
'permissions.coupons': true,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -14,19 +14,19 @@ describe('POST /coupons/generate/:event', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'contributor.sudo': true,
|
||||
'permissions.coupons': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if user has no sudo permission', async () => {
|
||||
it('returns an error if user has no coupons permission', async () => {
|
||||
await user.update({
|
||||
'contributor.sudo': false,
|
||||
'permissions.coupons': false,
|
||||
});
|
||||
|
||||
await expect(user.post('/coupons/generate/aaa')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: apiError('noSudoAccess'),
|
||||
message: apiError('noPrivAccess'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('POST /coupons/generate/:event', () => {
|
||||
|
||||
it('should generate coupons', async () => {
|
||||
await user.update({
|
||||
'contributor.sudo': true,
|
||||
'permissions.coupons': true,
|
||||
});
|
||||
|
||||
const coupons = await user.post('/coupons/generate/wondercon?count=2');
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('POST /coupons/validate/:code', () => {
|
||||
|
||||
it('returns true if coupon code is valid', async () => {
|
||||
const sudoUser = await generateUser({
|
||||
'contributor.sudo': true,
|
||||
'permissions.coupons': true,
|
||||
});
|
||||
|
||||
const [coupon] = await sudoUser.post('/coupons/generate/wondercon?count=1');
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /debug/make-admin (pended for v3 prod testing)', () => {
|
||||
describe('POST /debug/make-admin', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
@@ -14,12 +14,12 @@ describe('POST /debug/make-admin (pended for v3 prod testing)', () => {
|
||||
nconf.set('IS_PROD', false);
|
||||
});
|
||||
|
||||
it('makes user an admine', async () => {
|
||||
it('makes user an admin', async () => {
|
||||
await user.post('/debug/make-admin');
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.contributor.admin).to.eql(true);
|
||||
expect(user.permissions.fullAccess).to.eql(true);
|
||||
});
|
||||
|
||||
it('returns error when not in production mode', async () => {
|
||||
|
||||
@@ -219,11 +219,19 @@ describe('GET /groups', () => {
|
||||
|
||||
it('returns 30 guilds per page ordered by number of members', async () => {
|
||||
await user.update({ balance: 9000 });
|
||||
const groups = await Promise.all(_.times(60, i => generateGroup(user, {
|
||||
name: `public guild ${i} - is member`,
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
})));
|
||||
const delay = () => new Promise(resolve => setTimeout(resolve, 40));
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < 60; i += 1) {
|
||||
promises.push(generateGroup(user, {
|
||||
name: `public guild ${i} - is member`,
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
}));
|
||||
await delay(); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
|
||||
const groups = await Promise.all(promises);
|
||||
|
||||
// update group number 32 and not the first to make sure sorting works
|
||||
await groups[32].update({ name: 'guild with most members', memberCount: 199 });
|
||||
|
||||
@@ -315,7 +315,7 @@ describe('GET /groups/:id', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
admin = await generateUser({
|
||||
'contributor.admin': true,
|
||||
'permissions.moderator': true,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
|
||||
describe('POST /group', () => {
|
||||
let user;
|
||||
@@ -203,6 +204,23 @@ describe('POST /group', () => {
|
||||
|
||||
expect(updatedUser.balance).to.eql(user.balance - 1);
|
||||
});
|
||||
|
||||
it('does not deduct the gems from user when guild creation fails', async () => {
|
||||
const stub = sinon.stub(Group.prototype, 'save').rejects();
|
||||
const promise = user.post('/groups', {
|
||||
name: groupName,
|
||||
type: groupType,
|
||||
privacy: groupPrivacy,
|
||||
});
|
||||
|
||||
await expect(promise).to.eventually.be.rejected;
|
||||
|
||||
const updatedUser = await user.get('/user');
|
||||
|
||||
expect(updatedUser.balance).to.eql(user.balance);
|
||||
|
||||
stub.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
|
||||
member = members[0]; // eslint-disable-line prefer-destructuring
|
||||
member2 = members[1]; // eslint-disable-line prefer-destructuring
|
||||
adminUser = await generateUser({ 'contributor.admin': true });
|
||||
adminUser = await generateUser({ 'permissions.moderator': true });
|
||||
});
|
||||
|
||||
context('All Groups', () => {
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('PUT /group', () => {
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
adminUser = await generateUser({ 'contributor.admin': true });
|
||||
adminUser = await generateUser({ 'permissions.moderator': true });
|
||||
groupToUpdate = group;
|
||||
leader = groupLeader;
|
||||
nonLeader = members[0]; // eslint-disable-line prefer-destructuring
|
||||
@@ -104,11 +104,11 @@ describe('PUT /group', () => {
|
||||
// Update the bannedWordsAllowed property for the group
|
||||
const response = await groupLeader.put(`/groups/${group._id}`, updateGroupDetails);
|
||||
|
||||
expect(groupLeader.contributor.admin).to.eql(true);
|
||||
expect(groupLeader.permissions.fullAccess).to.eql(true);
|
||||
expect(response.bannedWordsAllowed).to.eql(true);
|
||||
});
|
||||
|
||||
it('does not allow for a non-admin to update the bannedWordsAllow property for an existing guild', async () => {
|
||||
it('does not allow for a non-moderator to update the bannedWordsAllow property for an existing guild', async () => {
|
||||
const { group, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'public guild',
|
||||
@@ -128,7 +128,6 @@ describe('PUT /group', () => {
|
||||
// Update the bannedWordsAllowed property for the group
|
||||
const response = await groupLeader.put(`/groups/${group._id}`, updateGroupDetails);
|
||||
|
||||
expect(groupLeader.contributor.admin).to.eql(undefined);
|
||||
expect(response.bannedWordsAllowed).to.eql(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,9 +7,14 @@ import {
|
||||
describe('GET /heroes/:heroId', () => {
|
||||
let user;
|
||||
|
||||
const heroFields = [
|
||||
'_id', 'id', 'auth', 'balance', 'contributor', 'flags', 'items',
|
||||
'lastCron', 'party', 'preferences', 'profile', 'purchased', 'secret',
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser({
|
||||
contributor: { admin: true },
|
||||
permissions: { userSupport: true },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +24,7 @@ describe('GET /heroes/:heroId', () => {
|
||||
await expect(nonAdmin.get(`/hall/heroes/${user._id}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('noAdminAccess'),
|
||||
message: t('noPrivAccess'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,10 +54,7 @@ describe('GET /heroes/:heroId', () => {
|
||||
});
|
||||
const heroRes = await user.get(`/hall/heroes/${hero._id}`);
|
||||
|
||||
expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'id', 'balance', 'profile', 'purchased',
|
||||
'contributor', 'auth', 'items', 'secret',
|
||||
]);
|
||||
expect(heroRes).to.have.all.keys(heroFields); // works as: object has all and only these keys
|
||||
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
expect(heroRes.secret.text).to.be.eq('Super Hero');
|
||||
@@ -64,10 +66,7 @@ describe('GET /heroes/:heroId', () => {
|
||||
});
|
||||
const heroRes = await user.get(`/hall/heroes/${hero.auth.local.username}`);
|
||||
|
||||
expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'id', 'balance', 'profile', 'purchased',
|
||||
'contributor', 'auth', 'items', 'secret',
|
||||
]);
|
||||
expect(heroRes).to.have.all.keys(heroFields);
|
||||
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import apiError from '../../../../../website/server/libs/apiError';
|
||||
|
||||
describe('GET /heroes/party/:groupId', () => {
|
||||
let user; // admin user
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser({
|
||||
'permissions.userSupport': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('requires the caller to be an admin', async () => {
|
||||
const nonAdmin = await generateUser();
|
||||
const party = await generateGroup(nonAdmin, { type: 'party', privacy: 'private' });
|
||||
await expect(nonAdmin.get(`/hall/heroes/party/${party._id}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: apiError('noPrivAccess'),
|
||||
});
|
||||
});
|
||||
|
||||
it('validates req.params.groupId', async () => {
|
||||
await expect(user.get('/hall/heroes/party/invalidUUID')).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('handles non-existing party', async () => {
|
||||
const dummyId = generateUUID();
|
||||
await expect(user.get(`/hall/heroes/party/${dummyId}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: apiError('groupWithIDNotFound', { groupId: dummyId }),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns only necessary party data given group id', async () => {
|
||||
const nonAdmin = await generateUser();
|
||||
const party = await generateGroup(nonAdmin, { type: 'party', privacy: 'private' });
|
||||
|
||||
const partyRes = await user.get(`/hall/heroes/party/${party._id}`);
|
||||
|
||||
expect(partyRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'id', 'balance', 'challengeCount', 'leader', 'leaderOnly', 'memberCount',
|
||||
'purchased', 'quest', 'summary',
|
||||
]);
|
||||
expect(partyRes.summary).to.eq(' ');
|
||||
// NB: 'summary' is NOT a field that the API route retrieves!
|
||||
// It must not be retrieved for privacy reasons.
|
||||
// However the group model automatically adds a summary for reasons given here:
|
||||
// https://github.com/HabitRPG/habitica/blob/8da36bf27c62ba0397a6af260c20d35a17f3d911/website/server/models/group.js#L161-L170
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
@@ -8,15 +9,12 @@ describe('PUT /heroes/:heroId', () => {
|
||||
let user;
|
||||
|
||||
const heroFields = [
|
||||
'_id', 'balance', 'profile', 'purchased',
|
||||
'contributor', 'auth', 'items', 'flags',
|
||||
'secret',
|
||||
'_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron',
|
||||
'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions',
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser({
|
||||
contributor: { admin: true },
|
||||
});
|
||||
user = await generateUser({ 'permissions.userSupport': true });
|
||||
});
|
||||
|
||||
it('requires the caller to be an admin', async () => {
|
||||
@@ -25,7 +23,7 @@ describe('PUT /heroes/:heroId', () => {
|
||||
await expect(nonAdmin.put(`/hall/heroes/${user._id}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('noAdminAccess'),
|
||||
message: t('noPrivAccess'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,8 +55,7 @@ describe('PUT /heroes/:heroId', () => {
|
||||
});
|
||||
|
||||
// test response
|
||||
// works as: object has all and only these keys
|
||||
expect(heroRes).to.have.all.keys(heroFields);
|
||||
expect(heroRes).to.have.all.keys(heroFields); // works as: object has all and only these keys
|
||||
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
|
||||
@@ -134,7 +131,6 @@ describe('PUT /heroes/:heroId', () => {
|
||||
});
|
||||
|
||||
// test response
|
||||
// works as: object has all and only these keys
|
||||
expect(heroRes).to.have.all.keys(heroFields);
|
||||
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
@@ -159,7 +155,6 @@ describe('PUT /heroes/:heroId', () => {
|
||||
});
|
||||
|
||||
// test response
|
||||
// works as: object has all and only these keys
|
||||
expect(heroRes).to.have.all.keys(heroFields);
|
||||
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
@@ -215,7 +210,6 @@ describe('PUT /heroes/:heroId', () => {
|
||||
});
|
||||
|
||||
// test response
|
||||
// works as: object has all and only these keys
|
||||
expect(heroRes).to.have.all.keys(heroFields);
|
||||
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
@@ -226,4 +220,35 @@ describe('PUT /heroes/:heroId', () => {
|
||||
await hero.sync();
|
||||
expect(hero.items.special.snowball).to.equal(5);
|
||||
});
|
||||
|
||||
it('does not accidentally update API Token', async () => {
|
||||
// This test has been included because hall.js will contain code to produce
|
||||
// a truncated version of the API Token, and we want to be sure that
|
||||
// the real Token is not modified by bugs in that code.
|
||||
const hero = await generateUser();
|
||||
const originalToken = hero.apiToken;
|
||||
|
||||
// make any change to the user except the Token
|
||||
await user.put(`/hall/heroes/${hero._id}`, {
|
||||
contributor: { text: 'Astronaut' },
|
||||
});
|
||||
|
||||
const updatedHero = await User.findById(hero._id).exec();
|
||||
expect(updatedHero.apiToken).to.equal(originalToken);
|
||||
expect(updatedHero.apiTokenObscured).to.not.exist;
|
||||
});
|
||||
|
||||
it('does update API Token when admin changes it', async () => {
|
||||
const hero = await generateUser();
|
||||
const originalToken = hero.apiToken;
|
||||
|
||||
// change the user's API Token
|
||||
await user.put(`/hall/heroes/${hero._id}`, {
|
||||
changeApiToken: true,
|
||||
});
|
||||
|
||||
const updatedHero = await User.findById(hero._id).exec();
|
||||
expect(updatedHero.apiToken).to.not.equal(originalToken);
|
||||
expect(updatedHero.apiTokenObscured).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -176,7 +176,7 @@ describe('POST /members/send-private-message', () => {
|
||||
|
||||
it('allows admin to send when sender has blocked the admin', async () => {
|
||||
userToSendMessage = await generateUser({
|
||||
'contributor.admin': 1,
|
||||
'permissions.moderator': true,
|
||||
});
|
||||
const receiver = await generateUser({ 'inbox.blocks': [userToSendMessage._id] });
|
||||
|
||||
@@ -204,7 +204,7 @@ describe('POST /members/send-private-message', () => {
|
||||
|
||||
it('allows admin to send when to user has opted out of messaging', async () => {
|
||||
userToSendMessage = await generateUser({
|
||||
'contributor.admin': 1,
|
||||
'permissions.moderator': true,
|
||||
});
|
||||
const receiver = await generateUser({ 'inbox.optOut': true });
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ describe('GET /tasks/:id', () => {
|
||||
|
||||
it('can get challenge task if admin', async () => {
|
||||
const admin = await generateUser({
|
||||
'contributor.admin': true,
|
||||
'permissions.challengeAdmin': true,
|
||||
});
|
||||
|
||||
const getTask = await admin.get(`/tasks/${task._id}`);
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('POST /tasks/challenge/:challengeId', () => {
|
||||
});
|
||||
|
||||
it('allows non-leader admin to add tasks to a challenge when not a member', async () => {
|
||||
const admin = await generateUser({ 'contributor.admin': true });
|
||||
const admin = await generateUser({ 'permissions.challengeAdmin': true });
|
||||
const task = await admin.post(`/tasks/challenge/${challenge._id}`, {
|
||||
text: 'test habit from admin',
|
||||
type: 'habit',
|
||||
|
||||
@@ -120,7 +120,7 @@ describe('POST /user/reset', () => {
|
||||
|
||||
it('does not delete secret', async () => {
|
||||
const admin = await generateUser({
|
||||
contributor: { admin: true },
|
||||
permissions: { userSupport: true },
|
||||
});
|
||||
|
||||
const hero = await generateUser({
|
||||
|
||||
@@ -135,6 +135,7 @@ describe('PUT /user', () => {
|
||||
'gem balance': { balance: 100 },
|
||||
auth: { 'auth.blocked': true, 'auth.timestamps.created': new Date() },
|
||||
contributor: { 'contributor.level': 9, 'contributor.admin': true, 'contributor.text': 'some text' },
|
||||
permissions: { 'permissions.fullAccess': true, 'permissions.news': true, 'permissions.moderator': 'some text' },
|
||||
backer: { 'backer.tier': 10, 'backer.npc': 'Bilbo' },
|
||||
subscriptions: { 'purchased.plan.extraMonths': 500, 'purchased.plan.consecutive.trinkets': 1000 },
|
||||
'customization gem purchases': { 'purchased.background.tavern': true, 'purchased.skin.bear': true },
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('POST /coupons/enter/:code', () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
sudoUser = await generateUser({
|
||||
'contributor.sudo': true,
|
||||
'permissions.coupons': true,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ describe('GET /members/:memberId/purchase-history', () => {
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser({
|
||||
contributor: { admin: true },
|
||||
permissions: { userSupport: true },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('GET /members/:memberId/purchase-history', () => {
|
||||
await expect(nonAdmin.get(`/members/${member._id}/purchase-history`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('noAdminAccess'),
|
||||
message: t('noPrivAccess'),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -15,16 +15,16 @@ describe('DELETE /news/:newsID', () => {
|
||||
};
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'contributor.newsPoster': true,
|
||||
'permissions.news': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('disallows access to non-newsPosters', async () => {
|
||||
const nonAdminUser = await generateUser({ 'contributor.newsPoster': false });
|
||||
const nonAdminUser = await generateUser({ 'permissions.news': false });
|
||||
await expect(nonAdminUser.del(`/news/${v4()}`)).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: 'You don\'t have news poster access.',
|
||||
message: t('noPrivAccess'),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('GET /news', () => {
|
||||
before(async () => {
|
||||
api = requester();
|
||||
const user = await generateUser({
|
||||
'contributor.newsPoster': true,
|
||||
'permissions.news': true,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('GET /news/:newsID', () => {
|
||||
};
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'contributor.newsPoster': true,
|
||||
'permissions.news': true,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -16,16 +16,16 @@ describe('POST /news', () => {
|
||||
};
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'contributor.newsPoster': true,
|
||||
'permissions.news': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('disallows access to non-admins', async () => {
|
||||
const nonAdminUser = await generateUser({ 'contributor.newsPoster': false });
|
||||
const nonAdminUser = await generateUser({ 'permissions.news': false });
|
||||
await expect(nonAdminUser.post('/news')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: 'You don\'t have news poster access.',
|
||||
message: 'You don\'t have the required privileges.',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -17,16 +17,16 @@ describe('PUT /news/:newsID', () => {
|
||||
};
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
'contributor.newsPoster': true,
|
||||
'permissions.news': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('disallows access to non-admins', async () => {
|
||||
const nonAdminUser = await generateUser({ 'contributor.newsPoster': false });
|
||||
const nonAdminUser = await generateUser({ 'permissions.news': false });
|
||||
await expect(nonAdminUser.put('/news/1234')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: 'You don\'t have news poster access.',
|
||||
message: 'You don\'t have the required privileges.',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ describe('POST /user/reset', () => {
|
||||
|
||||
it('does not delete secret', async () => {
|
||||
const admin = await generateUser({
|
||||
contributor: { admin: true },
|
||||
permissions: { userSupport: true },
|
||||
});
|
||||
|
||||
const hero = await generateUser({
|
||||
|
||||
@@ -84,6 +84,7 @@ describe('PUT /user', () => {
|
||||
'gem balance': { balance: 100 },
|
||||
auth: { 'auth.blocked': true, 'auth.timestamps.created': new Date() },
|
||||
contributor: { 'contributor.level': 9, 'contributor.admin': true, 'contributor.text': 'some text' },
|
||||
permissions: { 'permissions.fullAccess': true, 'permissions.news': true, 'permissions.moderator': 'some text' },
|
||||
backer: { 'backer.tier': 10, 'backer.npc': 'Bilbo' },
|
||||
subscriptions: { 'purchased.plan.extraMonths': 500, 'purchased.plan.consecutive.trinkets': 1000 },
|
||||
'customization gem purchases': { 'purchased.background.tavern': true, 'purchased.skin.bear': true },
|
||||
|
||||
@@ -675,6 +675,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_castle_gate {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_castle_gate.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_champions_colosseum {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_champions_colosseum.png');
|
||||
width: 141px;
|
||||
@@ -860,6 +865,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_enchanted_music_room {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_enchanted_music_room.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_fairy_ring {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_fairy_ring.png');
|
||||
width: 141px;
|
||||
@@ -1389,6 +1399,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_on_a_castle_wall {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_a_castle_wall.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_on_tree_branch {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_tree_branch.png');
|
||||
width: 141px;
|
||||
@@ -2171,6 +2186,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_castle_gate {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_castle_gate.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_champions_colosseum {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_champions_colosseum.png');
|
||||
width: 68px;
|
||||
@@ -2361,6 +2381,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_enchanted_music_room {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_enchanted_music_room.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_fairy_ring {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_fairy_ring.png');
|
||||
width: 68px;
|
||||
@@ -2890,6 +2915,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_on_a_castle_wall {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_on_a_castle_wall.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.icon_background_on_tree_branch {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_on_tree_branch.png');
|
||||
width: 68px;
|
||||
@@ -19055,6 +19085,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_snareDrum {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_snareDrum.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_softBlackPillow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_softBlackPillow.png');
|
||||
width: 114px;
|
||||
@@ -19085,6 +19120,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_spanishGuitar {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_spanishGuitar.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_strawberryFood {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_strawberryFood.png');
|
||||
width: 90px;
|
||||
@@ -20240,6 +20280,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_shield_armoire_snareDrum {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_snareDrum.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_shield_armoire_softBlackPillow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_softBlackPillow.png');
|
||||
width: 68px;
|
||||
@@ -20270,6 +20315,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_shield_armoire_spanishGuitar {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_spanishGuitar.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_shield_armoire_strawberryFood {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_strawberryFood.png');
|
||||
width: 68px;
|
||||
@@ -20485,6 +20535,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_weapon_armoire_huntingHorn {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_huntingHorn.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_weapon_armoire_ironCrook {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_ironCrook.png');
|
||||
width: 68px;
|
||||
@@ -21315,6 +21370,11 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_huntingHorn {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_huntingHorn.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_ironCrook {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_ironCrook.png');
|
||||
width: 90px;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import moment from 'moment';
|
||||
|
||||
export default function formatDate (inputDate) {
|
||||
if (!inputDate) return '';
|
||||
const date = moment(inputDate).utcOffset(0).format('YYYY-MM-DD HH:mm');
|
||||
return `${date} UTC`;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="row standard-page">
|
||||
<div class="well col-12">
|
||||
<h1>Admin Panel</h1>
|
||||
|
||||
<div>
|
||||
<form
|
||||
class="form-inline"
|
||||
@submit.prevent="loadHero(userIdentifier)"
|
||||
>
|
||||
<input
|
||||
v-model="userIdentifier"
|
||||
class="form-control uidField"
|
||||
type="text"
|
||||
:placeholder="'User ID or Username; blank for your account'"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Load User"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<router-view @changeUserIdentifier="changeUserIdentifier" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.uidField {
|
||||
min-width: 45ch;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import VueRouter from 'vue-router';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
userIdentifier: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: 'Admin Panel',
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
changeUserIdentifier (newId) {
|
||||
// If we've accessed the admin panel from a URL that had a user identifier in it,
|
||||
// this method will insert that identifier into the "Load User" form field
|
||||
// (useful if we want to re-fetch the user after making changes).
|
||||
this.userIdentifier = newId;
|
||||
},
|
||||
async loadHero (userIdentifier) {
|
||||
const id = userIdentifier || this.user._id;
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,132 @@
|
||||
import content from '@/../../common/script/content';
|
||||
|
||||
function _getGearSetName (key) {
|
||||
let set = 'NO SET [probably an omission in the API data]';
|
||||
if (content.gear.flat[key].set) {
|
||||
set = `${content.gear.flat[key].set}`;
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
function _getGearSetDescription (key) {
|
||||
let setName = _getGearSetName(key);
|
||||
if (setName === 'special-takeThis') {
|
||||
// no point displaying set details for gear where it's obvious
|
||||
return '';
|
||||
}
|
||||
const klassNames = {
|
||||
healer: 'Healer',
|
||||
rogue: 'Rogue',
|
||||
warrior: 'Warrior',
|
||||
wizard: 'Mage',
|
||||
};
|
||||
const lunarBattleQuestGear = ['armor_special_lunarWarriorArmor', 'head_special_lunarWarriorHelm', 'weapon_special_lunarScythe'];
|
||||
|
||||
const loginIncentivesGear = ['armor_special_bardRobes', 'armor_special_dandySuit', 'armor_special_lunarWarriorArmor', 'armor_special_nomadsCuirass', 'armor_special_pageArmor', 'armor_special_samuraiArmor', 'armor_special_sneakthiefRobes', 'armor_special_snowSovereignRobes', 'back_special_snowdriftVeil', 'head_special_bardHat', 'head_special_clandestineCowl', 'head_special_dandyHat', 'head_special_kabuto', 'head_special_lunarWarriorHelm', 'head_special_pageHelm', 'head_special_snowSovereignCrown', 'head_special_spikedHelm', 'shield_special_diamondStave', 'shield_special_lootBag', 'shield_special_wakizashi', 'shield_special_wintryMirror', 'weapon_special_bardInstrument', 'weapon_special_fencingFoil', 'weapon_special_lunarScythe', 'weapon_special_nomadsScimitar', 'weapon_special_pageBanner', 'weapon_special_skeletonKey', 'weapon_special_tachi'];
|
||||
|
||||
const goldQuestsGear = ['armor_special_finnedOceanicArmor', 'head_special_fireCoralCirclet', 'weapon_special_tridentOfCrashingTides', 'shield_special_moonpearlShield', 'head_special_pyromancersTurban', 'armor_special_pyromancersRobes', 'weapon_special_taskwoodsLantern', 'armor_special_mammothRiderArmor', 'head_special_mammothRiderHelm', 'weapon_special_mammothRiderSpear', 'shield_special_mammothRiderHorn', 'armor_special_roguishRainbowMessengerRobes', 'head_special_roguishRainbowMessengerHood', 'weapon_special_roguishRainbowMessage', 'shield_special_roguishRainbowMessage', 'eyewear_special_aetherMask', 'body_special_aetherAmulet', 'back_special_aetherCloak', 'weapon_special_aetherCrystals'];
|
||||
|
||||
const animalGear = ['back_special_bearTail', 'back_special_cactusTail', 'back_special_foxTail', 'back_special_lionTail', 'back_special_pandaTail', 'back_special_pigTail', 'back_special_tigerTail', 'back_special_wolfTail', 'headAccessory_special_bearEars', 'headAccessory_special_cactusEars', 'headAccessory_special_foxEars', 'headAccessory_special_lionEars', 'headAccessory_special_pandaEars', 'headAccessory_special_pigEars', 'headAccessory_special_tigerEars', 'headAccessory_special_wolfEars'];
|
||||
|
||||
let wantSetName = true; // some set names are useful, others aren't
|
||||
let setType = '[cannot determine set type]';
|
||||
if (setName === 'base-0') {
|
||||
setType = 'empty slot';
|
||||
wantSetName = false;
|
||||
} else if (setName.includes('special-turkey')) {
|
||||
setType = '<a href="https://habitica.fandom.com/wiki/Turkey_Day">Turkey Day</a>';
|
||||
wantSetName = false;
|
||||
} else if (setName.includes('special-nye')) {
|
||||
setType = '<a href="https://habitica.fandom.com/wiki/Event_Item_Sequences">New Year\'s Eve</a>';
|
||||
wantSetName = false;
|
||||
} else if (setName.includes('special-birthday')) {
|
||||
setType = '<a href="https://habitica.fandom.com/wiki/Habitica_Birthday_Bash">Habitica Birthday Bash</a>';
|
||||
wantSetName = false;
|
||||
} else if (setName.includes('special-0') || key === 'weapon_special_3') {
|
||||
setType = '<a href="https://habitica.fandom.com/wiki/Kickstarter">Kickstarter 2013</a>';
|
||||
wantSetName = false;
|
||||
} else if (setName.includes('special-1')) {
|
||||
setType = 'Contributor gear';
|
||||
wantSetName = false;
|
||||
} else if (setName.includes('special-2') || key === 'shield_special_goldenknight') {
|
||||
setType = '<a href="https://habitica.fandom.com/wiki/Legendary_Equipment">Legendary Equipment</a>';
|
||||
wantSetName = false;
|
||||
} else if (setName.includes('special-wondercon')) {
|
||||
setType = '<a href="https://habitica.fandom.com/wiki/Unconventional_Armor">Unconventional Armor</a>';
|
||||
wantSetName = false;
|
||||
} else if (lunarBattleQuestGear.includes(key)) {
|
||||
setType = '<a href="https://habitica.fandom.com/wiki/Quest_Lines#Lunar_Battle_Quest_Line">Lunar Battle Quest Line</a>';
|
||||
wantSetName = false;
|
||||
} else if (loginIncentivesGear.includes(key)) {
|
||||
setType = '<a href="https://habitica.fandom.com/wiki/Daily_Check-In_Incentives">Check-In Incentive</a>';
|
||||
wantSetName = false;
|
||||
} else if (goldQuestsGear.includes(key)) {
|
||||
setType = 'from <a href="https://habitica.fandom.com/wiki/Quest_Lines#Gold_Purchasable_Quest_Lines">Gold-Purchasable Quest Lines</a>';
|
||||
wantSetName = false;
|
||||
} else if (animalGear.includes(key)) {
|
||||
setType = '<a href="https://habitica.fandom.com/wiki/Avatar_Customizations">Animal Avatar Accessory Customisations</a>';
|
||||
wantSetName = false;
|
||||
} else if (!content.gear.flat[key].klass) {
|
||||
setType = 'NO "klass" [omission in API data]';
|
||||
} else if (content.gear.flat[key].klass === 'armoire') {
|
||||
setType = 'Armoire set';
|
||||
} else if (content.gear.flat[key].klass === 'mystery') {
|
||||
setType = 'Mystery Items';
|
||||
setName = setName.replace(/mystery-(....)(..)/, '$1-$2');
|
||||
} else if (content.gear.flat[key].klass === 'special') {
|
||||
const specialClass = content.gear.flat[key].specialClass || '';
|
||||
if (specialClass && Object.keys(klassNames).includes(specialClass)) {
|
||||
setType = `Grand Gala ${klassNames[specialClass]} set`;
|
||||
} else if (key.includes('special_gaymerx')) {
|
||||
setType = 'GaymerX';
|
||||
wantSetName = false;
|
||||
} else if (key.includes('special_ks2019')) {
|
||||
setType = '<a href="https://habitica.fandom.com/wiki/Kickstarter">Kickstarter 2019</a>';
|
||||
wantSetName = false;
|
||||
} else {
|
||||
setType = '[unknown set]';
|
||||
wantSetName = false;
|
||||
}
|
||||
} else if (Object.keys(klassNames).includes(content.gear.flat[key].klass)) {
|
||||
// e.g., base class gear such as weapon_warrior_6 (Golden Sword)
|
||||
setType = `base ${klassNames[content.gear.flat[key].klass]} gear`;
|
||||
wantSetName = false;
|
||||
}
|
||||
return (wantSetName) ? `${setType}: ${setName}` : setType;
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
content,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getItemDescription (itemType, key) {
|
||||
// Returns item name. Also returns other info for equipment.
|
||||
|
||||
const simpleItemTypes = ['eggs', 'hatchingPotions', 'food', 'quests', 'special'];
|
||||
if (simpleItemTypes.includes(itemType) && content[itemType][key]) {
|
||||
return content[itemType][key].text();
|
||||
}
|
||||
|
||||
if (itemType === 'mounts' && content.mountInfo[key]) {
|
||||
return content.mountInfo[key].text();
|
||||
}
|
||||
|
||||
if (itemType === 'pets' && content.petInfo[key]) {
|
||||
return content.petInfo[key].text();
|
||||
}
|
||||
|
||||
if (itemType === 'gear' && content.gear.flat[key]) {
|
||||
const name = content.gear.flat[key].text();
|
||||
const description = _getGearSetDescription(key);
|
||||
if (description) return `${name} -- ${description}`;
|
||||
return name;
|
||||
}
|
||||
|
||||
return 'NO NAME - invalid item?';
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
export default {
|
||||
methods: {
|
||||
async saveHero ({ hero, msg = 'User', clearData }) {
|
||||
await this.$store.dispatch('hall:updateHero', { heroDetails: hero });
|
||||
await this.$store.dispatch('snackbars:add', {
|
||||
title: '',
|
||||
text: `${msg} updated`,
|
||||
type: 'info',
|
||||
});
|
||||
|
||||
if (clearData) {
|
||||
// Use clearData when the saved changes may affect data in other components
|
||||
// (e.g., adding a contributor tier will increase the Gem balance)
|
||||
// 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' });
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Current Avatar Appearance, Drop Count Today
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<div>Drops Today: {{ items.lastDrop.count }}</div>
|
||||
<div>Most Recent Drop: {{ items.lastDrop.date | formatDate }}</div>
|
||||
<div>Use Costume: {{ preferences.costume ? 'on' : 'off' }}</div>
|
||||
<div class="subsection-start">
|
||||
Equipped Gear:
|
||||
<ul v-html="formatEquipment(items.gear.equipped)"></ul>
|
||||
</div>
|
||||
<div>
|
||||
Costume:
|
||||
<ul v-html="formatEquipment(items.gear.costume)"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import formatDate from '../filters/formatDate';
|
||||
import getItemDescription from '../mixins/getItemDescription';
|
||||
|
||||
export default {
|
||||
filters: {
|
||||
formatDate,
|
||||
},
|
||||
mixins: [
|
||||
getItemDescription,
|
||||
],
|
||||
props: {
|
||||
items: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
preferences: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formatEquipment (gearWorn) {
|
||||
const gearTypes = ['head', 'armor', 'weapon', 'shield', 'headAccessory', 'eyewear',
|
||||
'body', 'back'];
|
||||
let equipmentList = '';
|
||||
gearTypes.forEach(gearType => {
|
||||
const key = gearWorn[gearType] || '';
|
||||
const description = (key)
|
||||
? `<strong>${key}</strong> : ${this.getItemDescription('gear', gearWorn[gearType])}`
|
||||
: 'none';
|
||||
equipmentList += `<li>${gearType} : ${description}</li>\n`;
|
||||
});
|
||||
return equipmentList;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>@{{ auth.local.username }} / {{ profile.name }}</h2>
|
||||
{{ userId }}
|
||||
<router-link :to="{'name': 'userProfile', 'params': {'userId': userId}}">
|
||||
profile link
|
||||
</router-link>
|
||||
<br>
|
||||
language: {{ preferences.language }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
auth: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
preferences: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
profile: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Contributor Details
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<form @submit.prevent="saveHero({hero, msg: 'Contributor details', clearData: true})">
|
||||
<div>
|
||||
<label>Permissions</label>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="hero.permissions.fullAccess"
|
||||
:disabled="!hasPermission(user, 'fullAccess')"
|
||||
type="checkbox"
|
||||
>
|
||||
Full Admin Access (Allows access to everything. EVERYTHING)
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="hero.permissions.userSupport"
|
||||
:disabled="!hasPermission(user, 'fullAccess')"
|
||||
type="checkbox"
|
||||
>
|
||||
User Support (Access this form, access purchase history)
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="hero.permissions.news"
|
||||
:disabled="!hasPermission(user, 'fullAccess')"
|
||||
type="checkbox"
|
||||
>
|
||||
News poster (Bailey CMS)
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="hero.permissions.moderator"
|
||||
:disabled="!hasPermission(user, 'fullAccess')"
|
||||
type="checkbox"
|
||||
>
|
||||
Community Moderator (ban and mute users, access chat flags, manage social spaces)
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="hero.permissions.challengeAdmin"
|
||||
:disabled="!hasPermission(user, 'fullAccess')"
|
||||
type="checkbox"
|
||||
>
|
||||
Challenge Admin (can create official habitica challenges and admin all challenges)
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="hero.permissions.coupons"
|
||||
:disabled="!hasPermission(user, 'fullAccess')"
|
||||
type="checkbox"
|
||||
>
|
||||
Coupon Creator (can manage coupon codes)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Title</label>
|
||||
<input
|
||||
v-model="hero.contributor.text"
|
||||
class="form-control textField"
|
||||
type="text"
|
||||
>
|
||||
<small>
|
||||
Common titles:
|
||||
<strong>Ambassador, Artisan, Bard, Blacksmith, Challenger, Comrade, Fletcher,
|
||||
Linguist, Linguistic Scribe, Scribe, Socialite, Storyteller</strong>.
|
||||
<br>
|
||||
Rare titles:
|
||||
Advisor, Chamberlain, Designer, Mathematician, Shirtster, Spokesperson,
|
||||
Statistician, Tinker, Transcriber, Troubadour.
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group form-inline">
|
||||
<label>Tier</label>
|
||||
<input
|
||||
v-model="hero.contributor.level"
|
||||
class="form-control levelField"
|
||||
type="number"
|
||||
>
|
||||
<small>
|
||||
1-7 for normal contributors, 8 for moderators, 9 for staff.
|
||||
This determines which items, pets, mounts are available, and name-tag coloring.
|
||||
Tiers 8 and 9 are automatically given admin status.
|
||||
</small>
|
||||
</div>
|
||||
<div
|
||||
v-if="hero.secret.text"
|
||||
class="form-group"
|
||||
>
|
||||
<label>Moderation Notes</label>
|
||||
<div
|
||||
v-markdown="hero.secret.text"
|
||||
class="markdownPreview"
|
||||
></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Contributions</label>
|
||||
<textarea
|
||||
v-model="hero.contributor.contributions"
|
||||
class="form-control"
|
||||
cols="5"
|
||||
rows="5"
|
||||
></textarea>
|
||||
<div
|
||||
v-markdown="hero.contributor.contributions"
|
||||
class="markdownPreview"
|
||||
></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Edit Moderation Notes</label>
|
||||
<textarea
|
||||
v-model="hero.secret.text"
|
||||
class="form-control"
|
||||
cols="5"
|
||||
rows="3"
|
||||
></textarea>
|
||||
<div
|
||||
v-markdown="hero.secret.text"
|
||||
class="markdownPreview"
|
||||
></div>
|
||||
</div>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save and Clear Data"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.levelField {
|
||||
min-width: 10ch;
|
||||
}
|
||||
.textField {
|
||||
min-width: 50ch;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
import { mapState } from '@/libs/store';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
|
||||
function resetData (self) {
|
||||
self.expand = self.hero.contributor.level;
|
||||
}
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [
|
||||
userStateMixin,
|
||||
saveHero,
|
||||
],
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
props: {
|
||||
resetCounter: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
hero: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
resetData(this);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
resetData(this);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Timestamps, Time Zone, Authentication, Email Address
|
||||
<span
|
||||
v-if="errorsOrWarningsExist"
|
||||
>- ERRORS / WARNINGS EXIST</span>
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<p
|
||||
v-if="errorsOrWarningsExist"
|
||||
class="errorMessage"
|
||||
>
|
||||
See error(s) below.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
Account created:
|
||||
<strong>{{ hero.auth.timestamps.created | formatDate }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Most recent cron:
|
||||
<strong>{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
|
||||
("auth.timestamps.loggedin")
|
||||
</div>
|
||||
<div v-if="cronError">
|
||||
"lastCron" value:
|
||||
<strong>{{ hero.lastCron | formatDate }}</strong>
|
||||
<br>
|
||||
<span class="errorMessage">
|
||||
ERROR: cron probably crashed before finishing
|
||||
("auth.timestamps.loggedin" and "lastCron" dates are different).
|
||||
</span>
|
||||
</div>
|
||||
<div class="subsection-start">
|
||||
Time zone:
|
||||
<strong>{{ hero.preferences.timezoneOffset | formatTimeZone }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Custom Day Start time (CDS):
|
||||
<strong>{{ hero.preferences.dayStart }}</strong>
|
||||
</div>
|
||||
<div v-if="timezoneDiffError || timezoneMissingError">
|
||||
Time zone at previous cron:
|
||||
<strong>{{ hero.preferences.timezoneOffsetAtLastCron | formatTimeZone }}</strong>
|
||||
|
||||
<div class="errorMessage">
|
||||
<div v-if="timezoneDiffError">
|
||||
ERROR: the player's current time zone is different than their time zone when
|
||||
their previous cron ran. This can be because:
|
||||
<ul>
|
||||
<li>daylight savings started or stopped <sup>*</sup></li>
|
||||
<li>the player changed zones due to travel <sup>*</sup></li>
|
||||
<li>the player has devices set to different zones <sup>**</sup></li>
|
||||
<li>the player uses a VPN with varying zones <sup>**</sup></li>
|
||||
<li>something similarly unpleasant is happening. <sup>**</sup></li>
|
||||
</ul>
|
||||
<p>
|
||||
<em>* The problem should fix itself in about a day.</em><br>
|
||||
<em>** One of these causes is probably happening if the time zones stay
|
||||
different for more than a day.</em>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="timezoneMissingError">
|
||||
ERROR: One of the player's time zones is missing.
|
||||
This is expected and okay if it's the "Time zone at previous cron"
|
||||
AND if it's their first day in Habitica.
|
||||
Otherwise an error has occurred.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subsection-start form-inline">
|
||||
API Token:
|
||||
<form @submit.prevent="changeApiToken()">
|
||||
<input
|
||||
type="submit"
|
||||
value="Change API Token"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
</form>
|
||||
<div
|
||||
v-if="tokenModified"
|
||||
class="form-inline"
|
||||
>
|
||||
<strong>API Token has been changed. Tell the player something like this:</strong>
|
||||
<br>
|
||||
I've given you a new API Token.
|
||||
You'll need to log out of the website and mobile app then log back in
|
||||
otherwise they won't work correctly.
|
||||
If you have trouble logging out, for the website go to
|
||||
https://habitica.com/static/clear-browser-data and click the red button there,
|
||||
and for the Android app, clear its data.
|
||||
For the iOS app, if you can't log out you might need to uninstall it,
|
||||
reboot your phone, then reinstall it.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subsection-start">
|
||||
Local authentication:
|
||||
<span v-if="hero.auth.local.email">Yes,
|
||||
<strong>{{ hero.auth.local.email }}</strong></span>
|
||||
<span v-else><strong>None</strong></span>
|
||||
</div>
|
||||
<div>
|
||||
Google authentication:
|
||||
<pre v-if="authMethodExists('google')">{{ hero.auth.google }}</pre>
|
||||
<span v-else><strong>None</strong></span>
|
||||
</div>
|
||||
<div>
|
||||
Facebook authentication:
|
||||
<pre v-if="authMethodExists('facebook')">{{ hero.auth.facebook }}</pre>
|
||||
<span v-else><strong>None</strong></span>
|
||||
</div>
|
||||
<div>
|
||||
Apple ID authentication:
|
||||
<pre v-if="authMethodExists('apple')">{{ hero.auth.apple }}</pre>
|
||||
<span v-else><strong>None</strong></span>
|
||||
</div>
|
||||
<div class="subsection-start">
|
||||
Full "auth" object for checking above is correct:
|
||||
<pre>{{ hero.auth }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import formatDate from '../filters/formatDate';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
function resetData (self) {
|
||||
self.cronError = false;
|
||||
self.timezoneDiffError = false;
|
||||
self.timezoneMissingError = false;
|
||||
self.errorsOrWarningsExist = false;
|
||||
self.expand = false;
|
||||
|
||||
const cronDate1 = moment(self.hero.auth.timestamps.loggedin);
|
||||
const cronDate2 = moment(self.hero.lastCron);
|
||||
const maxAllowableSecondsDifference = 60; // expect cron to take less than this many seconds
|
||||
if (Math.abs(cronDate1.diff(cronDate2, 'seconds')) > maxAllowableSecondsDifference) {
|
||||
self.cronError = true;
|
||||
self.errorsOrWarningsExist = true;
|
||||
}
|
||||
|
||||
// compare the user's time zones to see if they're different
|
||||
const newTimezone = self.hero.preferences.timezoneOffset;
|
||||
const oldTimezone = self.hero.preferences.timezoneOffsetAtLastCron;
|
||||
if ((newTimezone === undefined || oldTimezone === undefined)
|
||||
&& (self.cronError || self.hero.flags.cronCount > 0)) {
|
||||
self.timezoneMissingError = true;
|
||||
self.errorsOrWarningsExist = true;
|
||||
} else if (newTimezone !== oldTimezone) {
|
||||
self.timezoneDiffError = true;
|
||||
self.errorsOrWarningsExist = true;
|
||||
}
|
||||
self.expand = self.errorsOrWarningsExist;
|
||||
}
|
||||
|
||||
export default {
|
||||
filters: {
|
||||
formatDate,
|
||||
formatTimeZone (timezoneOffset) {
|
||||
if (timezoneOffset === undefined) return 'No value recorded.';
|
||||
// convert reverse offset to time zone in "+/-H:MM UTC" format
|
||||
const sign = (timezoneOffset < 0) ? '+' : '-'; // reverse the sign
|
||||
const timezoneHours = Math.floor(Math.abs(timezoneOffset) / 60);
|
||||
const timezoneMinutes = Math.floor((Math.abs(timezoneOffset) / 60 - timezoneHours) * 60);
|
||||
const timezoneMinutesDisplay = (timezoneMinutes) ? `:${timezoneMinutes}` : ''; // don't display :00
|
||||
return `${sign}${timezoneHours}${timezoneMinutesDisplay} UTC`;
|
||||
},
|
||||
},
|
||||
mixins: [
|
||||
saveHero,
|
||||
],
|
||||
props: {
|
||||
resetCounter: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
hero: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
cronError: false,
|
||||
timezoneDiffError: false,
|
||||
timezoneMissingError: false,
|
||||
tokenModified: false,
|
||||
errorsOrWarningsExist: false,
|
||||
expand: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
resetData(this);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
resetData(this);
|
||||
},
|
||||
methods: {
|
||||
authMethodExists (authMethod) {
|
||||
if (this.hero.auth[authMethod] && this.hero.auth[authMethod].length !== 0) return true;
|
||||
return false;
|
||||
},
|
||||
async changeApiToken () {
|
||||
this.hero.changeApiToken = true;
|
||||
await this.saveHero({ hero: this.hero, msg: 'API Token' });
|
||||
this.tokenModified = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div v-if="hasPermission(user, 'userSupport')">
|
||||
<div
|
||||
v-if="hero && hero.profile"
|
||||
class="row"
|
||||
>
|
||||
<div class="form col-12">
|
||||
<basic-details
|
||||
:user-id="hero._id"
|
||||
:auth="hero.auth"
|
||||
:preferences="hero.preferences"
|
||||
:profile="hero.profile"
|
||||
/>
|
||||
|
||||
<privileges-and-gems
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<cron-and-auth
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<party-and-quest
|
||||
v-if="adminHasPrivForParty"
|
||||
:user-id="hero._id"
|
||||
:username="hero.auth.local.username"
|
||||
:user-has-party="hasParty"
|
||||
:party-not-exist-error="partyNotExistError"
|
||||
:user-party-data="hero.party"
|
||||
:group-party-data="party"
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<avatar-and-drops
|
||||
:items="hero.items"
|
||||
:preferences="hero.preferences"
|
||||
/>
|
||||
|
||||
<items-owned
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
/>
|
||||
|
||||
<transactions
|
||||
:hero="hero"
|
||||
/>
|
||||
|
||||
<contributor-details
|
||||
:hero="hero"
|
||||
:reset-counter="resetCounter"
|
||||
@clear-data="clearData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .accordion-group .accordion-group {
|
||||
margin-left: 1em;
|
||||
}
|
||||
::v-deep h3 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
::v-deep h4 {
|
||||
margin-top: 1em;
|
||||
}
|
||||
::v-deep .expand-toggle::after {
|
||||
margin-left: 5px;
|
||||
}
|
||||
::v-deep .subsection-start {
|
||||
margin-top: 1em;
|
||||
}
|
||||
::v-deep .form-inline {
|
||||
margin-bottom: 1em;
|
||||
input, span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
::v-deep .errorMessage {
|
||||
font-weight: bold;
|
||||
}
|
||||
::v-deep .markdownPreview {
|
||||
margin-left: 3em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import BasicDetails from './basicDetails';
|
||||
import ItemsOwned from './itemsOwned';
|
||||
import CronAndAuth from './cronAndAuth';
|
||||
import PartyAndQuest from './partyAndQuest';
|
||||
import AvatarAndDrops from './avatarAndDrops';
|
||||
import PrivilegesAndGems from './privilegesAndGems';
|
||||
import ContributorDetails from './contributorDetails';
|
||||
import Transactions from './transactions';
|
||||
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BasicDetails,
|
||||
ItemsOwned,
|
||||
CronAndAuth,
|
||||
PartyAndQuest,
|
||||
AvatarAndDrops,
|
||||
PrivilegesAndGems,
|
||||
ContributorDetails,
|
||||
Transactions,
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
data () {
|
||||
return {
|
||||
userIdentifier: '',
|
||||
resetCounter: 0,
|
||||
hero: {},
|
||||
party: {},
|
||||
hasParty: false,
|
||||
partyNotExistError: false,
|
||||
adminHasPrivForParty: true,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
userIdentifier () {
|
||||
// close modal if the page is opened in an existing tab from the modal
|
||||
this.$root.$emit('bv::hide::modal', 'profile');
|
||||
|
||||
this.loadHero(this.userIdentifier);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.userIdentifier = this.$route.params.userIdentifier;
|
||||
},
|
||||
methods: {
|
||||
clearData () {
|
||||
this.hero = {};
|
||||
},
|
||||
|
||||
async loadHero (userIdentifier) {
|
||||
const id = userIdentifier.replace(/@/, ''); // allow "@name" to be entered
|
||||
this.$emit('changeUserIdentifier', id); // change user identifier in Admin Panel's form
|
||||
|
||||
this.hero = await this.$store.dispatch('hall:getHero', { uuid: id });
|
||||
|
||||
if (!this.hero.flags) {
|
||||
this.hero.flags = {
|
||||
chatRevoked: false,
|
||||
chatShadowMuted: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.hero.permissions) {
|
||||
this.hero.permissions = {};
|
||||
}
|
||||
|
||||
this.hasParty = false;
|
||||
this.partyNotExistError = false;
|
||||
this.adminHasPrivForParty = true;
|
||||
if (this.hero.party && this.hero.party._id) {
|
||||
try {
|
||||
this.party = await this.$store.dispatch('hall:getHeroParty', { groupId: this.hero.party._id });
|
||||
this.hasParty = true;
|
||||
} catch (e) {
|
||||
if (e.message.includes('status code 401')) {
|
||||
// @TODO is there a better way to recognise NotAuthorized error?
|
||||
this.adminHasPrivForParty = false;
|
||||
} else {
|
||||
// the API's error message isn't worth reporting ("Request failed with status code 404")
|
||||
this.partyNotExistError = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.resetCounter += 1; // tell child components to reinstantiate from scratch
|
||||
},
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
this.userIdentifier = to.params.userIdentifier;
|
||||
next();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Items
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<div>
|
||||
The sections below display each item's key (bolded if the player has ever owned it),
|
||||
followed by the item's English name.
|
||||
<ul>
|
||||
<li>
|
||||
Click on an item's key or value to change it
|
||||
(hovering shows an underline to show where you can click).
|
||||
</li>
|
||||
<li>For Mounts and Gear, clicking toggles between the allowed values.</li>
|
||||
<li>For other item types, clicking gives you a form field to enter a new value.</li>
|
||||
<li>Click Save when the correct value is displayed.</li>
|
||||
<li>
|
||||
You must Save for each item individually but you do not need to reload the user
|
||||
between each Save.
|
||||
</li>
|
||||
<li>If you adjust an item and do not click Save for it, the change will be lost.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="itemType in itemTypes"
|
||||
:key="itemType"
|
||||
>
|
||||
<div class="accordion-group">
|
||||
<h4
|
||||
class="expand-toggle"
|
||||
:class="{'open': expandItemType[itemType]}"
|
||||
@click="expandItemType[itemType] = !expandItemType[itemType]"
|
||||
>
|
||||
{{ itemType }}
|
||||
</h4>
|
||||
|
||||
<div v-if="expandItemType[itemType]">
|
||||
<p v-if="itemType === 'pets'">
|
||||
A value of -1 means they owned the Pet but Released it
|
||||
and have not yet rehatched it.
|
||||
</p>
|
||||
<p v-if="itemType === 'mounts'">
|
||||
A value of "null" means they owned the Mount but Released it
|
||||
and have not yet retamed it.
|
||||
</p>
|
||||
<p v-if="itemType === 'special'">
|
||||
When there are 0 of these items, we can't tell if
|
||||
they had been owned and were all used, or have never been owned.
|
||||
</p>
|
||||
<p v-if="itemType === 'gear'">
|
||||
A value of true means they own the item now and can wear it.
|
||||
A value of false means they used to own it but lost it from Death
|
||||
(or an old Rebirth).
|
||||
</p>
|
||||
<ul>
|
||||
<li
|
||||
v-for="item in collatedItemData[itemType]"
|
||||
:key="item.path"
|
||||
>
|
||||
<form @submit.prevent="saveItem(item)">
|
||||
<span
|
||||
class="enableValueChange"
|
||||
@click="enableValueChange(item)"
|
||||
>
|
||||
{{ item | displayValue }}
|
||||
:
|
||||
<span :class="{ ownedItem: !item.neverOwned }">{{ item.key }} : </span>
|
||||
</span>
|
||||
<span v-html="item.name"></span>
|
||||
|
||||
<div
|
||||
v-if="item.modified"
|
||||
class="form-inline"
|
||||
>
|
||||
<input
|
||||
v-if="item.valueIsInteger"
|
||||
v-model="item.value"
|
||||
class="form-control valueField"
|
||||
type="number"
|
||||
>
|
||||
<input
|
||||
v-if="item.modified"
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ownedItem {
|
||||
font-weight: bold;
|
||||
}
|
||||
.enableValueChange:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.valueField {
|
||||
min-width: 10ch;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import content from '@/../../common/script/content';
|
||||
import getItemDescription from '../mixins/getItemDescription';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
function collateItemData (self) {
|
||||
const collatedItemData = {};
|
||||
self.itemTypes.forEach(itemType => {
|
||||
// itemTypes are pets, food, gear, etc
|
||||
|
||||
// Set up some basic data for this itemType:
|
||||
let basePath = `items.${itemType}`;
|
||||
let ownedItems = self.hero.items[itemType] || {};
|
||||
let allItems = content[itemType];
|
||||
if (itemType === 'gear') {
|
||||
basePath = 'items.gear.owned';
|
||||
ownedItems = self.hero.items.gear.owned || {};
|
||||
allItems = content.gear.flat;
|
||||
} else if (itemType === 'pets' || itemType === 'mounts') {
|
||||
// add the non-Standard pets and mounts
|
||||
const ucItemType = (itemType === 'pets') ? 'Pets' : 'Mounts';
|
||||
self.petMountSubTypes.forEach(subType => {
|
||||
allItems = { ...allItems, ...content[subType + ucItemType] };
|
||||
});
|
||||
}
|
||||
|
||||
const itemData = []; // all items for this itemType
|
||||
|
||||
// Collate data for items that the user owns or used to own:
|
||||
for (const key of Object.keys(ownedItems)) {
|
||||
// Do not sort keys. The order in the items object gives hints about order received.
|
||||
if (itemType !== 'special' || self.specialItems.includes(key)) {
|
||||
const valueIsInteger = !self.nonIntegerTypes.includes(itemType);
|
||||
itemData.push({
|
||||
neverOwned: false,
|
||||
itemType,
|
||||
key,
|
||||
modified: false,
|
||||
name: self.getItemDescription(itemType, key),
|
||||
path: `${basePath}.${key}`,
|
||||
value: ownedItems[key],
|
||||
valueIsInteger,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Collate data for items that the user never owned:
|
||||
for (const key of Object.keys(allItems).sort()) {
|
||||
if (
|
||||
// ignore items the user owns because we captured them above:
|
||||
!(key in ownedItems)
|
||||
|
||||
// ignore gear items that indicate empty equipped slots (e.g., head_base_0):
|
||||
&& !(itemType === 'gear' && content.gear.flat[key].set
|
||||
&& content.gear.flat[key].set === 'base-0')
|
||||
|
||||
// ignore "special" items that aren't Snowballs, Seafoam, etc:
|
||||
&& (itemType !== 'special' || self.specialItems.includes(key))
|
||||
) {
|
||||
const valueIsInteger = !self.nonIntegerTypes.includes(itemType);
|
||||
const value = (valueIsInteger) ? 0 : '';
|
||||
itemData.push({
|
||||
neverOwned: true,
|
||||
itemType,
|
||||
key,
|
||||
modified: false,
|
||||
name: self.getItemDescription(itemType, key),
|
||||
path: `${basePath}.${key}`,
|
||||
value,
|
||||
valueIsInteger,
|
||||
});
|
||||
}
|
||||
}
|
||||
collatedItemData[itemType] = itemData;
|
||||
});
|
||||
return collatedItemData;
|
||||
}
|
||||
|
||||
function resetData (self) {
|
||||
self.collatedItemData = collateItemData(self);
|
||||
self.itemTypes.forEach(itemType => { self.expandItemType[itemType] = false; });
|
||||
}
|
||||
|
||||
export default {
|
||||
filters: {
|
||||
displayValue (item) {
|
||||
if (item.value === '') return 'never owned';
|
||||
if (item.value === 0 && item.neverOwned) return '0 (never owned)';
|
||||
if (item.value === null) return 'null'; // we need visible text
|
||||
return item.value; // true or false or an integer
|
||||
},
|
||||
},
|
||||
mixins: [
|
||||
getItemDescription,
|
||||
saveHero,
|
||||
],
|
||||
props: {
|
||||
resetCounter: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
hero: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
expandItemType: {
|
||||
eggs: false,
|
||||
hatchingPotions: false,
|
||||
food: false,
|
||||
pets: false,
|
||||
mounts: false,
|
||||
quests: false,
|
||||
gear: false,
|
||||
special: false,
|
||||
},
|
||||
itemTypes: ['eggs', 'hatchingPotions', 'food', 'pets', 'mounts', 'quests', 'gear', 'special'],
|
||||
nonIntegerTypes: ['mounts', 'gear'],
|
||||
petMountSubTypes: ['premium', 'quest', 'special', 'wacky'], // e.g., 'premiumPets'
|
||||
// items.special includes many things but we are interested in these only:
|
||||
specialItems: ['snowball', 'spookySparkles', 'shinySeed', 'seafoam'],
|
||||
collatedItemData: {},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
resetData(this);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
resetData(this);
|
||||
},
|
||||
methods: {
|
||||
async saveItem (item) {
|
||||
// prepare the item's new value and path for being saved
|
||||
this.hero.itemPath = item.path;
|
||||
if (item.value === null) {
|
||||
this.hero.itemVal = 'null';
|
||||
} else if (item.value === false) {
|
||||
this.hero.itemVal = 'false';
|
||||
} else {
|
||||
this.hero.itemVal = item.value;
|
||||
}
|
||||
|
||||
await this.saveHero({ hero: this.hero, msg: item.key });
|
||||
item.neverOwned = false;
|
||||
item.modified = false;
|
||||
},
|
||||
enableValueChange (item) {
|
||||
// allow form field(s) to be shown:
|
||||
item.modified = true;
|
||||
|
||||
// for non-integer items, toggle through the allowed values:
|
||||
if (item.itemType === 'gear') {
|
||||
// Allowed starting values are true, false, and '' (never owned)
|
||||
// Allowed values to switch to are true and false
|
||||
item.value = !item.value;
|
||||
} else if (item.itemType === 'mounts') {
|
||||
// Allowed starting values are true, null, and "never owned"
|
||||
// Allowed values to switch to are true and null
|
||||
if (item.value === true) {
|
||||
item.value = null;
|
||||
} else {
|
||||
item.value = true;
|
||||
}
|
||||
}
|
||||
// @TODO add a delete option
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,317 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Party, Quest
|
||||
<span
|
||||
v-if="errorsOrWarningsExist"
|
||||
>- ERRORS / WARNINGS EXIST</span>
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<div
|
||||
v-if="errorsOrWarningsExist"
|
||||
class="errorMessage"
|
||||
>
|
||||
<p v-if="partyNotExistError">
|
||||
ERROR: User has a Party ID but that Party does not exist.
|
||||
If you are seeing a red error notification on screen now
|
||||
("<strong>Group with id ... not found</strong>"), it's refering to this issue.
|
||||
<br>Ask a database admin to delete the user's Party ID ({{ userPartyData._id }}).
|
||||
</p>
|
||||
<p
|
||||
v-if="questErrors"
|
||||
v-html="questErrors"
|
||||
></p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</span>
|
||||
<span v-else>no</span>
|
||||
</div>
|
||||
<div class="subsection-start">
|
||||
<p v-html="questStatus"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as quests from '@/../../common/script/content/quests';
|
||||
|
||||
function determineQuestStatus (self) {
|
||||
// Quest data is in the user doc and party doc. They can be out of sync.
|
||||
// Here we collate data from both sources, showing error messages if needed.
|
||||
|
||||
// First get data from the party's document.
|
||||
const groupQuestData = self.groupPartyData.quest;
|
||||
let questExists = false; // true if quest is active or in invitation stage
|
||||
let questIsActive = false; // true if quest's invitation stage is over
|
||||
let inviteStatusForUser = '';
|
||||
let expectedRsvpStatusForUser = false;
|
||||
let countOfQuestMembers = 0;
|
||||
if (self.userHasParty && groupQuestData) {
|
||||
questIsActive = groupQuestData.active;
|
||||
if (groupQuestData.members) countOfQuestMembers = Object.keys(groupQuestData.members).length;
|
||||
if (groupQuestData.key) {
|
||||
questExists = true;
|
||||
if (!countOfQuestMembers) {
|
||||
self.questErrors = 'ERROR: Quest is running or in invitation stage but has no participants.';
|
||||
} else if (groupQuestData.members[self.userId] === null) {
|
||||
inviteStatusForUser = 'pending';
|
||||
if (questIsActive) {
|
||||
self.questErrors = 'ERROR: Quest is running but user\'s invitation is still pending ("null") in quest object.';
|
||||
} else {
|
||||
expectedRsvpStatusForUser = true;
|
||||
}
|
||||
} else if (groupQuestData.members[self.userId] === false) {
|
||||
inviteStatusForUser = 'rejected';
|
||||
if (questIsActive) {
|
||||
self.questErrors = 'ERROR: Quest is running and user\'s invitation was rejected BUT '
|
||||
+ 'it wasn\'t cleared properly from the quest\'s data ("false"). '
|
||||
+ 'That shouldn\'t cause any problems though.';
|
||||
}
|
||||
} else if (groupQuestData.members[self.userId] === true) {
|
||||
inviteStatusForUser = 'accepted';
|
||||
} else if (questIsActive) {
|
||||
inviteStatusForUser = 'rejected OR not accepted before quest start OR user joined party after quest started';
|
||||
} else {
|
||||
inviteStatusForUser = 'missing';
|
||||
self.questErrors = 'ERROR: Quest is in invitation stage but user doesn\'t have an invitation '
|
||||
+ 'in the party\'s data ("quest.members" needs to be fixed).';
|
||||
}
|
||||
} else if (questIsActive) {
|
||||
self.questErrors = 'ERROR: Quest is running but there is no "key" to say which quest it is. '
|
||||
+ 'This means the other data and errors in this section are unreliable, '
|
||||
+ 'and there may be more errors not shown here.'
|
||||
+ 'Other errors here may tell you which key to add.'
|
||||
+ 'After fixing, check for more errors.';
|
||||
// @TODO display a similar message for when it happens during invitation stage
|
||||
}
|
||||
}
|
||||
if (self.questErrors) self.questErrors += '<br>';
|
||||
// from this point on, further quest errors need to be appended to that
|
||||
|
||||
let questStatus = '<p>';
|
||||
if (questExists) {
|
||||
questStatus = 'Quest exists and is ';
|
||||
if (questIsActive) {
|
||||
questStatus += 'running.<br>User is ';
|
||||
if (inviteStatusForUser !== 'accepted') questStatus += 'not ';
|
||||
questStatus += 'a participant.';
|
||||
} else {
|
||||
questStatus += 'in invitation stage.<br>'
|
||||
+ `User's invitation is ${inviteStatusForUser}.`;
|
||||
}
|
||||
questStatus += '<br>';
|
||||
if (!groupQuestData.leader) {
|
||||
self.questErrors += 'ERROR: quest does not have its owner specified '
|
||||
+ '(party needs value for "quest.leader").<br>';
|
||||
} else if (groupQuestData.leader === self.userId) {
|
||||
questStatus += 'User is the quest owner.';
|
||||
} else {
|
||||
questStatus += `Quest owner is ${groupQuestData.leader}`;
|
||||
}
|
||||
} else {
|
||||
questStatus = 'No quest.';
|
||||
}
|
||||
questStatus += '</p>';
|
||||
|
||||
// Assess quest participants.
|
||||
if (questExists && countOfQuestMembers) {
|
||||
const participants = (questIsActive) ? 'participants' : 'invitees';
|
||||
questStatus += `<p>Quest has ${countOfQuestMembers} ${participants}:<ul>`;
|
||||
for (const [memberId, inviteStatus] of Object.entries(groupQuestData.members)) {
|
||||
questStatus += '<li>';
|
||||
questStatus += (memberId === self.userId)
|
||||
? `@${self.username}`
|
||||
: memberId;
|
||||
let invitationDescription = '';
|
||||
const errMsg = ' - MINOR ERROR: this data should have been deleted when quest started';
|
||||
if (inviteStatus === true) {
|
||||
if (!questIsActive) invitationDescription = ' - invitation accepted';
|
||||
// we don't display anything if quest is running - obvious that participant accepted
|
||||
} else if (inviteStatus === false) {
|
||||
invitationDescription += ' - invitation rejected';
|
||||
if (questIsActive) invitationDescription += errMsg;
|
||||
} else {
|
||||
invitationDescription += ' - invitation pending';
|
||||
if (questIsActive) invitationDescription += errMsg;
|
||||
}
|
||||
questStatus += invitationDescription;
|
||||
questStatus += '</li>';
|
||||
}
|
||||
questStatus += '</ul></p>';
|
||||
// @TODO: show error if all invitations accepted but quest not active
|
||||
}
|
||||
|
||||
// Now get data from the user's document.
|
||||
if (!self.userPartyData.quest) self.userPartyData.quest = {};
|
||||
if (self.userPartyData.quest.RSVPNeeded !== expectedRsvpStatusForUser) {
|
||||
self.questErrors
|
||||
+= `ERROR: User's quest invitation ("party.quest.RSVPNeeded") should be "${expectedRsvpStatusForUser}" but isn't.<br>`;
|
||||
}
|
||||
|
||||
if (inviteStatusForUser === 'pending' || inviteStatusForUser === 'accepted') {
|
||||
if (!self.userPartyData.quest.key) {
|
||||
self.questErrors += 'ERROR: User has accepted quest invitation or invitation is '
|
||||
+ 'still pending but their account has no "key" for the quest.<br>';
|
||||
} else if (self.userPartyData.quest.key !== groupQuestData.key) {
|
||||
self.questErrors += 'ERROR: User has accepted quest invitation or invitation is '
|
||||
+ `still pending but the "key" in their account (${self.userPartyData.quest.key}) `
|
||||
+ `is different than the quest's "key" (${groupQuestData.key}).<br>`;
|
||||
}
|
||||
} else if (self.userPartyData.quest.key) {
|
||||
self.questErrors += `ERROR: User has a "key" for the quest (${self.userPartyData.quest.key})`
|
||||
+ 'but perhaps should not have (no quest exists, or user not participating, '
|
||||
+ 'or quest is in erroneous state).<br>';
|
||||
}
|
||||
|
||||
// Display details of quest (name, type, progress, etc).
|
||||
if (questExists) {
|
||||
const questContent = quests.quests[groupQuestData.key];
|
||||
if (questContent) {
|
||||
let questContentData = `<strong>Quest Details</strong>:<br>Quest name: ${questContent.text()}<br>Quest "key": ${questContent.key}`;
|
||||
let questProgress = '<strong>Quest Progress:</strong>';
|
||||
if (!questIsActive) questProgress += ' none (quest is in invitation stage)';
|
||||
let userProgressToday;
|
||||
let userMadeZeroProgress = false;
|
||||
if (questContent.boss) {
|
||||
// NB Data rounding below is done in the same way as on the user's party page.
|
||||
questContentData += `<br>Boss name: ${questContent.boss.name()}`
|
||||
+ `<br>Boss's starting HP: ${questContent.boss.hp}`
|
||||
+ `<br>Boss's Strength: ${questContent.boss.str}`;
|
||||
let bossHasRage;
|
||||
if (questContent.boss.rage && questContent.boss.rage.value) {
|
||||
bossHasRage = true;
|
||||
questContentData += `<br>Boss's rage name for this quest: ${questContent.boss.rage.title()}`;
|
||||
questContentData += `<br>Boss's rage limit: ${questContent.boss.rage.value}`;
|
||||
}
|
||||
if (questIsActive) {
|
||||
if (!groupQuestData.progress || groupQuestData.progress.hp === undefined) {
|
||||
self.questErrors += 'ERROR: Party\'s quest is missing some or all of the "progress" data.<br>';
|
||||
} else {
|
||||
questProgress += `<br>Current Boss HP: ${Math.ceil(groupQuestData.progress.hp * 100) / 100}`;
|
||||
}
|
||||
if (bossHasRage) {
|
||||
questProgress += `<br>Current Rage: ${Math.floor(groupQuestData.progress.rage * 100) / 100}`;
|
||||
}
|
||||
}
|
||||
userProgressToday = `Player's pending damage to Boss: ${Math.floor(self.userPartyData.quest.progress.up * 10) / 10}`;
|
||||
if (!self.userPartyData.quest.progress.up) userMadeZeroProgress = true;
|
||||
} else {
|
||||
questContentData += '<br>Need to collect:<ul>';
|
||||
if (questIsActive) questProgress += '<br>Current found items: <ul>';
|
||||
for (const [key, obj] of Object.entries(questContent.collect)) {
|
||||
questContentData += `<li>${obj.text()}: ${obj.count} ("key": ${key})</li>`;
|
||||
if (questIsActive) {
|
||||
if (!groupQuestData.progress || !groupQuestData.progress.collect) {
|
||||
self.questErrors += 'ERROR: Party\'s quest is missing some or all of the "progress" data.<br>';
|
||||
} else if (groupQuestData.progress.collect[key] !== undefined) {
|
||||
questProgress += `<li>${obj.text()}: ${groupQuestData.progress.collect[key]}</li>`;
|
||||
} else {
|
||||
self.questErrors += `ERROR: Party's quest has no entry for "${key}" `
|
||||
+ '("quest.progress.collect" needs to be fixed).<br>';
|
||||
}
|
||||
}
|
||||
}
|
||||
questContentData += '</ul>';
|
||||
if (questIsActive) questProgress += '</ul>';
|
||||
userProgressToday = `Player's pending collected items: ${self.userPartyData.quest.progress.collectedItems}`;
|
||||
if (!self.userPartyData.quest.progress.collectedItems) userMadeZeroProgress = true;
|
||||
}
|
||||
if (userMadeZeroProgress) userProgressToday += '<br>NB: Zero pending quest progress may be from an error in which the user\'s database document is missing the pending progress fields. That error can\'t be identified here because the API will apply default data. If the user claims to have made pending progress but none is showing for them, a database admin has to check that.';
|
||||
questStatus += `<p>${questContentData}</p>`
|
||||
+ `<p>${questProgress}</p>`
|
||||
+ `<p>${userProgressToday}</p>`;
|
||||
questStatus += `<p><strong>Raw Quest Data:</strong></p><pre>party: ${JSON.stringify(groupQuestData, null, ' ')}`
|
||||
+ `\nuser: ${JSON.stringify(self.userPartyData.quest, null, ' ')}</pre>`;
|
||||
} else {
|
||||
self.questErrors += `ERROR: quest "key" ${groupQuestData.key} does not match a known quest.`;
|
||||
}
|
||||
}
|
||||
return questStatus;
|
||||
}
|
||||
|
||||
function resetData (self) {
|
||||
self.questStatus = '';
|
||||
self.questErrors = '';
|
||||
self.errorsOrWarningsExist = false;
|
||||
self.expand = false;
|
||||
|
||||
if (self.partyNotExistError) {
|
||||
self.errorsOrWarningsExist = true;
|
||||
} else {
|
||||
self.userIsPartyLeader = self.groupPartyData.leader === self.userId;
|
||||
}
|
||||
|
||||
// check for quest errors even if party doesn't exist (user can have old quest data)
|
||||
self.questStatus = determineQuestStatus(self);
|
||||
if (self.questErrors) self.errorsOrWarningsExist = true;
|
||||
|
||||
self.expand = self.errorsOrWarningsExist;
|
||||
}
|
||||
|
||||
export default {
|
||||
props: {
|
||||
resetCounter: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
userHasParty: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
partyNotExistError: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
userPartyData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
groupPartyData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
userIsPartyLeader: false,
|
||||
questStatus: '',
|
||||
questErrors: '',
|
||||
errorsOrWarningsExist: false,
|
||||
expand: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
resetData(this);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
resetData(this);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Privileges, Gem Balance
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<p
|
||||
v-if="errorsOrWarningsExist"
|
||||
class="errorMessage"
|
||||
>
|
||||
Player has had privileges removed or has moderation notes.
|
||||
</p>
|
||||
|
||||
<form @submit.prevent="saveHero({hero, msg: 'Privileges or Gems or Moderation Notes'})">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-if="hero.flags"
|
||||
v-model="hero.flags.chatShadowMuted"
|
||||
type="checkbox"
|
||||
> Shadow Mute
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-if="hero.flags"
|
||||
v-model="hero.flags.chatRevoked"
|
||||
type="checkbox"
|
||||
> Mute (Revoke Chat Privileges)
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="hero.auth.blocked"
|
||||
type="checkbox"
|
||||
> Ban / Block
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
Balance
|
||||
<input
|
||||
v-model="hero.balance"
|
||||
class="form-control balanceField"
|
||||
type="number"
|
||||
step="0.25"
|
||||
>
|
||||
</label>
|
||||
<span>
|
||||
<small>
|
||||
Balance is in USD, not in Gems.
|
||||
E.g., if this number is 1, it means 4 Gems.
|
||||
Arrows change Balance by 0.25 (i.e., 1 Gem per click).
|
||||
Do not use when awarding tiers; tier gems are automatic.
|
||||
</small>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Moderation Notes</label>
|
||||
<textarea
|
||||
v-model="hero.secret.text"
|
||||
class="form-control"
|
||||
cols="5"
|
||||
rows="5"
|
||||
></textarea>
|
||||
<div
|
||||
v-markdown="hero.secret.text"
|
||||
class="markdownPreview"
|
||||
></div>
|
||||
</div>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.balanceField {
|
||||
min-width: 15ch;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import saveHero from '../mixins/saveHero';
|
||||
|
||||
function resetData (self) {
|
||||
self.errorsOrWarningsExist = false;
|
||||
self.expand = false;
|
||||
if (self.hero.flags.chatRevoked || self.hero.flags.chatShadowMuted || self.hero.auth.blocked
|
||||
|| (self.hero.secret.text && !self.hero.contributor.level)) {
|
||||
// We automatically expand this section if the user has had privileges removed.
|
||||
// We also expand if they have secret.text UNLESS they have a contributor tier because
|
||||
// in that case the notes are probably about their contributions and can be seen in the
|
||||
// Contributor Details section (which will be automatically expanded because of their tier).
|
||||
self.errorsOrWarningsExist = true;
|
||||
self.expand = true;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [
|
||||
saveHero,
|
||||
],
|
||||
props: {
|
||||
resetCounter: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
hero: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
errorsOrWarningsExist: false,
|
||||
expand: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
resetCounter () {
|
||||
resetData(this);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
resetData(this);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="toggleTransactionsOpen"
|
||||
>
|
||||
Transactions
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<purchase-history-table
|
||||
:gem-transactions="gemTransactions"
|
||||
:hourglass-transactions="hourglassTransactions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PurchaseHistoryTable from '../../ui/purchaseHistoryTable.vue';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PurchaseHistoryTable,
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
props: {
|
||||
hero: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
gemTransactions: [],
|
||||
hourglassTransactions: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async toggleTransactionsOpen () {
|
||||
this.expand = !this.expand;
|
||||
if (this.expand) {
|
||||
const transactions = await this.$store.dispatch('members:getPurchaseHistory', { memberId: this.hero._id });
|
||||
this.gemTransactions = transactions.filter(transaction => transaction.currency === 'gems');
|
||||
this.hourglassTransactions = transactions.filter(transaction => transaction.currency === 'hourglasses');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -589,7 +589,7 @@ export default {
|
||||
async makeAdmin () {
|
||||
await axios.post('/api/v4/debug/make-admin');
|
||||
// @TODO: Notification.text('You are now an admin!
|
||||
// Go to the Hall of Heroes to change your contributor level.');
|
||||
// Reload the website then go to Help > Admin Panel to set contributor level, etc.');
|
||||
// @TODO: sync()
|
||||
},
|
||||
openModifyInventoryModal () {
|
||||
|
||||
@@ -321,7 +321,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
import omit from 'lodash/omit';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { mapState } from '@/libs/store';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import memberSearchDropdown from '@/components/members/memberSearchDropdown';
|
||||
import closeChallengeModal from './closeChallengeModal';
|
||||
import Column from '../tasks/column';
|
||||
@@ -358,7 +358,7 @@ export default {
|
||||
userLink,
|
||||
groupLink,
|
||||
},
|
||||
mixins: [challengeMemberSearchMixin],
|
||||
mixins: [challengeMemberSearchMixin, userStateMixin],
|
||||
props: ['challengeId'],
|
||||
data () {
|
||||
return {
|
||||
@@ -387,7 +387,6 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
isMember () {
|
||||
return this.user.challenges.indexOf(this.challenge._id) !== -1;
|
||||
},
|
||||
@@ -396,7 +395,7 @@ export default {
|
||||
return this.user._id === this.challenge.leader._id;
|
||||
},
|
||||
isAdmin () {
|
||||
return Boolean(this.user.contributor.admin);
|
||||
return this.hasPermission(this.user, 'challengeAdmin');
|
||||
},
|
||||
canJoin () {
|
||||
return !this.isMember;
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
||||
<div
|
||||
v-for="group in categoryOptions"
|
||||
v-if="group.key !== 'habitica_official' || user.contributor.admin"
|
||||
v-if="group.key !== 'habitica_official' || hasPermission(user, 'challengeAdmin')"
|
||||
:key="group.key"
|
||||
class="form-check"
|
||||
>
|
||||
@@ -277,14 +277,15 @@ import clone from 'lodash/clone';
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
|
||||
import { TAVERN_ID, MIN_SHORTNAME_SIZE_FOR_CHALLENGES, MAX_SUMMARY_SIZE_FOR_CHALLENGES } from '@/../../common/script/constants';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
props: ['groupId'],
|
||||
data () {
|
||||
const categoryOptions = [
|
||||
@@ -378,7 +379,6 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
creating () {
|
||||
return !this.workingChallenge.id;
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class="mentioned-icon"
|
||||
></div>
|
||||
<div
|
||||
v-if="user.contributor.admin && msg.flagCount"
|
||||
v-if="hasPermission(user, 'moderator') && msg.flagCount"
|
||||
class="message-hidden"
|
||||
>
|
||||
{{ flagCountDescription }}
|
||||
@@ -54,7 +54,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="(user.flags.communityGuidelinesAccepted && msg.uuid !== 'system')
|
||||
&& (!isMessageReported || user.contributor.admin)"
|
||||
&& (!isMessageReported || hasPermission(user, 'moderator'))"
|
||||
class="action d-flex align-items-center"
|
||||
@click="report(msg)"
|
||||
>
|
||||
@@ -68,7 +68,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="msg.uuid === user._id || user.contributor.admin"
|
||||
v-if="msg.uuid === user._id || hasPermission(user, 'moderator')"
|
||||
class="action d-flex align-items-center"
|
||||
@click="remove()"
|
||||
>
|
||||
@@ -202,7 +202,7 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
|
||||
import renderWithMentions from '@/libs/renderWithMentions';
|
||||
import { mapState } from '@/libs/store';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import userLink from '../userLink';
|
||||
|
||||
import deleteIcon from '@/assets/svg/delete.svg';
|
||||
@@ -223,6 +223,7 @@ export default {
|
||||
return moment(value).toDate().toString();
|
||||
},
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
props: {
|
||||
msg: {},
|
||||
groupId: {},
|
||||
@@ -240,7 +241,6 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
isUserMentioned () {
|
||||
const message = this.msg;
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ import moment from 'moment';
|
||||
import axios from 'axios';
|
||||
import debounce from 'lodash/debounce';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import { mapState } from '@/libs/store';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
|
||||
import Avatar from '../avatar';
|
||||
import copyAsTodoModal from './copyAsTodoModal';
|
||||
@@ -161,6 +161,7 @@ export default {
|
||||
chatCard,
|
||||
Avatar,
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
props: {
|
||||
chat: {},
|
||||
groupType: {},
|
||||
@@ -182,7 +183,6 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
// @TODO: We need a different lazy load mechnism.
|
||||
// But honestly, adding a paging route to chat would solve this
|
||||
messages () {
|
||||
@@ -214,7 +214,7 @@ export default {
|
||||
canViewFlag (message) {
|
||||
if (message.uuid === this.user._id) return true;
|
||||
if (!message.flagCount || message.flagCount < 2) return true;
|
||||
return this.user.contributor.admin;
|
||||
return this.hasPermission(this.user, 'moderator');
|
||||
},
|
||||
loadProfileCache: debounce(function loadProfileCache (screenPosition) {
|
||||
this._loadProfileCache(screenPosition);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<div class="footer text-center">
|
||||
<button
|
||||
v-if="user.contributor.admin"
|
||||
v-if="hasPermission(user, 'moderator')"
|
||||
class="pull-left btn btn-danger"
|
||||
@click="clearFlagCount()"
|
||||
>
|
||||
@@ -88,15 +88,15 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [notifications],
|
||||
mixins: [notifications, userStateMixin],
|
||||
data () {
|
||||
const abuseFlagModalBody = {
|
||||
firstLinkStart: '<a href="/static/community-guidelines" target="_blank">',
|
||||
@@ -111,9 +111,6 @@ export default {
|
||||
reportComment: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica::report-chat', this.handleReport);
|
||||
},
|
||||
|
||||
@@ -288,7 +288,7 @@
|
||||
import extend from 'lodash/extend';
|
||||
import groupUtilities from '@/mixins/groupsUtilities';
|
||||
import styleHelper from '@/mixins/styleHelper';
|
||||
import { mapState, mapGetters } from '@/libs/store';
|
||||
import { mapGetters } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import participantListModal from './participantListModal';
|
||||
import groupFormModal from './groupFormModal';
|
||||
@@ -312,6 +312,7 @@ import QuestDetailModal from './questDetailModal';
|
||||
import RightSidebar from '@/components/groups/rightSidebar';
|
||||
import InvitationListModal from './invitationListModal';
|
||||
import { PAGES } from '@/libs/consts';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -327,7 +328,7 @@ export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [groupUtilities, styleHelper],
|
||||
mixins: [groupUtilities, styleHelper, userStateMixin],
|
||||
props: ['groupId'],
|
||||
data () {
|
||||
return {
|
||||
@@ -356,9 +357,6 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
}),
|
||||
...mapGetters({
|
||||
partyMembers: 'party:members',
|
||||
}),
|
||||
@@ -372,7 +370,7 @@ export default {
|
||||
return this.user._id === this.group.leader._id;
|
||||
},
|
||||
isAdmin () {
|
||||
return Boolean(this.user.contributor.admin);
|
||||
return Boolean(this.hasPermission(this.user, 'moderator'));
|
||||
},
|
||||
isMember () {
|
||||
return this.isMemberOfGroup(this.user, this.group);
|
||||
|
||||
@@ -213,7 +213,7 @@ label.custom-control-label(v-once) {{ $t('allowGuildInvitationsFromNonMembers')
|
||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
||||
<div
|
||||
v-for="group in categoryOptions"
|
||||
v-if="group.key !== 'habitica_official' || user.contributor.admin"
|
||||
v-if="group.key !== 'habitica_official' || hasPermission(user, 'challengeAdmin')"
|
||||
:key="group.key"
|
||||
class="form-check"
|
||||
>
|
||||
@@ -372,13 +372,13 @@ label.custom-control-label(v-once) {{ $t('allowGuildInvitationsFromNonMembers')
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import toggleSwitch from '@/components/ui/toggleSwitch';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import gemIcon from '@/assets/svg/gem.svg';
|
||||
import informationIcon from '@/assets/svg/information.svg';
|
||||
|
||||
import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '@/../../common/script/constants';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
|
||||
// @TODO: Not sure the best way to pass party creating status
|
||||
// Since we need the modal in the header, passing props doesn't work
|
||||
@@ -393,6 +393,7 @@ export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
data () {
|
||||
const data = {
|
||||
workingGroup: {
|
||||
@@ -491,7 +492,6 @@ export default {
|
||||
return data;
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
editingGroup () {
|
||||
return this.$store.state.editingGroup;
|
||||
},
|
||||
@@ -512,7 +512,7 @@ export default {
|
||||
return this.workingGroup.type === 'party';
|
||||
},
|
||||
isAdmin () {
|
||||
return Boolean(this.user.contributor.admin);
|
||||
return Boolean(this.hasPermission(this.user, 'moderator'));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -379,7 +379,6 @@
|
||||
<script>
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
import removeMemberModal from '@/components/members/removeMemberModal';
|
||||
import loadingGryphon from '@/components/ui/loadingGryphon';
|
||||
@@ -390,6 +389,7 @@ import starIcon from '@/assets/members/star.svg';
|
||||
import dots from '@/assets/svg/dots.svg';
|
||||
import SelectList from '@/components/ui/selectList';
|
||||
import { PAGES } from '@/libs/consts';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -398,6 +398,7 @@ export default {
|
||||
removeMemberModal,
|
||||
loadingGryphon,
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
props: ['hideBadge'],
|
||||
data () {
|
||||
return {
|
||||
@@ -462,13 +463,12 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
isLeader () {
|
||||
if (!this.group || !this.group.leader) return false;
|
||||
return this.user._id === this.group.leader || this.user._id === this.group.leader._id;
|
||||
},
|
||||
isAdmin () {
|
||||
return Boolean(this.user.contributor.admin);
|
||||
return Boolean(this.hasPermission(this.user, 'moderator'));
|
||||
},
|
||||
isLoadMoreAvailable () {
|
||||
// Only available if the current length of `members` is less than the
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<div class="row standard-page">
|
||||
<div>
|
||||
<div v-if="user.contributor.admin">
|
||||
<div v-if="hasPermission(user, 'userSupport')">
|
||||
<h2>Reward User</h2>
|
||||
<div
|
||||
v-if="!hero.profile"
|
||||
@@ -247,9 +247,6 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('name') }}</th>
|
||||
<th v-if="user.contributor && user.contributor.admin">
|
||||
{{ $t('userId') }}
|
||||
</th>
|
||||
<th>{{ $t('contribLevel') }}</th>
|
||||
<th>{{ $t('title') }}</th>
|
||||
<th>{{ $t('contributions') }}</th>
|
||||
@@ -257,12 +254,12 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(hero, index) in heroes"
|
||||
v-for="hero in heroes"
|
||||
:key="hero._id"
|
||||
>
|
||||
<td>
|
||||
<user-link
|
||||
v-if="hero.contributor && hero.contributor.admin"
|
||||
v-if="hasPermission(hero, 'userSupport')"
|
||||
:user="hero"
|
||||
:popover="$t('gamemaster')"
|
||||
popover-trigger="mouseenter"
|
||||
@@ -272,13 +269,17 @@
|
||||
v-else
|
||||
:user="hero"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
v-if="user.contributor.admin"
|
||||
class="btn-link"
|
||||
@click="populateContributorInput(hero._id, index)"
|
||||
>
|
||||
{{ hero._id }}
|
||||
<span v-if="hasPermission(user, 'userSupport')">
|
||||
<br>
|
||||
{{ hero._id }}
|
||||
<br>
|
||||
<router-link
|
||||
:to="{ name: 'adminPanelUser',
|
||||
params: { userIdentifier: hero._id } }"
|
||||
>
|
||||
admin panel
|
||||
</router-link>
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ hero.contributor.level }}</td>
|
||||
<td>{{ hero.contributor.text }}</td>
|
||||
@@ -305,10 +306,8 @@
|
||||
|
||||
<script>
|
||||
import each from 'lodash/each';
|
||||
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import styleHelper from '@/mixins/styleHelper';
|
||||
import { mapState } from '@/libs/store';
|
||||
import * as quests from '@/../../common/script/content/quests';
|
||||
import { mountInfo, petInfo } from '@/../../common/script/content/stable';
|
||||
import content from '@/../../common/script/content';
|
||||
@@ -316,6 +315,7 @@ import gear from '@/../../common/script/content/gear';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import userLink from '../userLink';
|
||||
import PurchaseHistoryTable from '../ui/purchaseHistoryTable.vue';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -325,7 +325,7 @@ export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [notifications, styleHelper],
|
||||
mixins: [notifications, styleHelper, userStateMixin],
|
||||
data () {
|
||||
return {
|
||||
heroes: [],
|
||||
@@ -347,9 +347,6 @@ export default {
|
||||
expandTransactions: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
async mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('hallContributors'),
|
||||
@@ -392,11 +389,9 @@ export default {
|
||||
},
|
||||
getFormattedItemReference (pathPrefix, itemKeys, values) {
|
||||
let finishedString = '\n'.concat('path: ', pathPrefix, ', ', 'value: {', values, '}\n');
|
||||
|
||||
each(itemKeys, key => {
|
||||
finishedString = finishedString.concat('\t', pathPrefix, '.', key, '\n');
|
||||
});
|
||||
|
||||
return finishedString;
|
||||
},
|
||||
async loadHero (uuid, heroIndex) {
|
||||
@@ -413,7 +408,6 @@ export default {
|
||||
this.expandAuth = false;
|
||||
},
|
||||
async saveHero () {
|
||||
this.hero.contributor.admin = this.hero.contributor.level > 7;
|
||||
const heroUpdated = await this.$store.dispatch('hall:updateHero', { heroDetails: this.hero });
|
||||
this.text('User updated');
|
||||
this.hero = {};
|
||||
@@ -426,11 +420,6 @@ export default {
|
||||
this.heroID = -1;
|
||||
this.currentHeroIndex = -1;
|
||||
},
|
||||
populateContributorInput (id, index) {
|
||||
this.heroID = id;
|
||||
window.scrollTo(0, 200);
|
||||
this.loadHero(id, index);
|
||||
},
|
||||
async toggleTransactionsOpen () {
|
||||
this.expandTransactions = !this.expandTransactions;
|
||||
if (this.expandTransactions) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('name') }}</th>
|
||||
<th v-if="user.contributor.admin">
|
||||
<th v-if="hasPermission(user, 'userSupport')">
|
||||
{{ $t('userId') }}
|
||||
</th>
|
||||
<th>{{ $t('backerTier') }}</th>
|
||||
@@ -28,7 +28,7 @@
|
||||
></a>
|
||||
{{ patron.profile.name }}
|
||||
</td>
|
||||
<td v-if="user.contributor.admin">
|
||||
<td v-if="hasPermission(user, 'userSupport')">
|
||||
{{ patron._id }}
|
||||
</td>
|
||||
<td>{{ patron.backer.tier }}</td>
|
||||
@@ -40,19 +40,16 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import styleHelper from '@/mixins/styleHelper';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
|
||||
export default {
|
||||
mixins: [styleHelper],
|
||||
mixins: [styleHelper, userStateMixin],
|
||||
data () {
|
||||
return {
|
||||
patrons: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
async mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('hallPatrons'),
|
||||
|
||||
@@ -297,6 +297,14 @@
|
||||
{{ $t('help') }}
|
||||
</router-link>
|
||||
<div class="topbar-dropdown">
|
||||
<router-link
|
||||
v-if="user.permissions.fullAccess ||
|
||||
user.permissions.userSupport || user.permissions.newsPoster"
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
:to="{name: 'adminPanel'}"
|
||||
>
|
||||
Admin Panel
|
||||
</router-link>
|
||||
<router-link
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
:to="{name: 'faq'}"
|
||||
|
||||
@@ -118,6 +118,7 @@ import { MAX_LEVEL_HARD_CAP } from '@/../../common/script/constants';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import guide from '@/mixins/guide';
|
||||
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
import yesterdailyModal from './tasks/yesterdailyModal';
|
||||
import newStuff from './news/modal';
|
||||
@@ -841,11 +842,21 @@ export default {
|
||||
},
|
||||
async runCronAction () {
|
||||
// Run Cron
|
||||
await axios.post('/api/v4/cron');
|
||||
|
||||
// Reset daily analytics actions
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 0);
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 0);
|
||||
const response = await axios.post('/api/v4/cron');
|
||||
if (response.status === 200) {
|
||||
// Reset daily analytics actions
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 0);
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 0);
|
||||
} else {
|
||||
// Note a failed cron event, for our records and investigation
|
||||
Analytics.track({
|
||||
eventName: 'cron failed',
|
||||
eventAction: 'cron failed',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
responseCode: response.status,
|
||||
}, { trackOnClient: true });
|
||||
}
|
||||
|
||||
// Sync
|
||||
await Promise.all([
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
{{ $t('subscription') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="user.contributor.admin"
|
||||
v-if="hasPermission(user, 'userSupport')"
|
||||
class="nav-link"
|
||||
:to="{name: 'transactions'}"
|
||||
:class="{'active': $route.name === 'transactions'}"
|
||||
@@ -123,11 +123,13 @@ import find from 'lodash/find';
|
||||
import { mapState } from '@/libs/store';
|
||||
import SecondaryMenu from '@/components/secondaryMenu';
|
||||
import gifts from '@/assets/svg/gifts-vertical.svg';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SecondaryMenu,
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
@@ -138,7 +140,6 @@ export default {
|
||||
computed: {
|
||||
...mapState({
|
||||
currentEventList: 'worldState.data.currentEventList',
|
||||
user: 'user.data',
|
||||
}),
|
||||
currentEvent () {
|
||||
return find(this.currentEventList, event => Boolean(event.promo));
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div>
|
||||
<small>{{ $t('couponText') }}</small>
|
||||
</div>
|
||||
<div v-if="user.contributor.sudo">
|
||||
<div v-if="user.permissions.coupons">
|
||||
<hr>
|
||||
<h4>{{ $t('generateCodes') }}</h4>
|
||||
<div
|
||||
|
||||
@@ -143,12 +143,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.validateInputs();
|
||||
this.$root.$emit('bv::hide::modal', 'restore');
|
||||
},
|
||||
restore () {
|
||||
if (this.restoreValues.stats.lvl < 1) {
|
||||
// @TODO:
|
||||
// Notification.error(env.t('invalidLevel'), true);
|
||||
if (!this.validateInputs()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -175,6 +174,35 @@ export default {
|
||||
this.$store.dispatch('user:set', settings);
|
||||
this.$root.$emit('bv::hide::modal', 'restore');
|
||||
},
|
||||
validateInputs () {
|
||||
const canRestore = ['hp', 'exp', 'gp', 'mp'];
|
||||
let valid = true;
|
||||
|
||||
for (const stat of canRestore) {
|
||||
if (this.restoreValues.stats[stat] === '') {
|
||||
this.restoreValues.stats[stat] = this.user.stats[stat];
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
const inputLevel = Number(this.restoreValues.stats.lvl);
|
||||
if (this.restoreValues.stats.lvl === ''
|
||||
|| !Number.isInteger(inputLevel)
|
||||
|| inputLevel < 1) {
|
||||
this.restoreValues.stats.lvl = this.user.stats.lvl;
|
||||
valid = false;
|
||||
}
|
||||
|
||||
const inputStreak = Number(this.restoreValues.achievements.streak);
|
||||
if (this.restoreValues.achievements.streak === ''
|
||||
|| !Number.isInteger(inputStreak)
|
||||
|| inputStreak < 0) {
|
||||
this.restoreValues.achievements.streak = this.user.achievements.streak;
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return valid;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
></div>
|
||||
</button>
|
||||
<button
|
||||
v-if="userLoggedIn.contributor.admin"
|
||||
v-if="hasPermission(userLoggedIn, 'moderator')"
|
||||
v-b-tooltip.hover.right="'Admin - Toggle Tools'"
|
||||
class="btn btn-secondary positive-icon d-flex justify-content-center align-items-center"
|
||||
@click="toggleAdminTools()"
|
||||
@@ -71,7 +71,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="userLoggedIn.contributor.admin && adminToolsLoaded"
|
||||
v-if="hasPermission(userLoggedIn, 'moderator') && adminToolsLoaded"
|
||||
class="row admin-profile-actions"
|
||||
>
|
||||
<div class="col-12 text-right">
|
||||
@@ -111,6 +111,12 @@
|
||||
class="admin-action"
|
||||
@click="adminUnblockUser()"
|
||||
>un-ban</span>
|
||||
<router-link
|
||||
:to="{ name: 'adminPanelUser', params: { userIdentifier: userId } }"
|
||||
replace
|
||||
>
|
||||
Admin Panel
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -730,6 +736,7 @@ import challenge from '@/assets/svg/challenge.svg';
|
||||
import member from '@/assets/svg/member-icon.svg';
|
||||
import staff from '@/assets/svg/tier-staff.svg';
|
||||
import error404 from '../404';
|
||||
import { userCustomStateMixin } from '../../mixins/userState';
|
||||
// @TODO: EMAILS.COMMUNITY_MANAGER_EMAIL
|
||||
const COMMUNITY_MANAGER_EMAIL = 'admin@habitica.com';
|
||||
|
||||
@@ -742,6 +749,7 @@ export default {
|
||||
profileStats,
|
||||
error404,
|
||||
},
|
||||
mixins: [userCustomStateMixin('userLoggedIn')],
|
||||
props: ['userId', 'startingPage'],
|
||||
data () {
|
||||
return {
|
||||
@@ -780,7 +788,6 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
userLoggedIn: 'user.data',
|
||||
flatGear: 'content.gear.flat',
|
||||
}),
|
||||
userJoinedDate () {
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export const userStateMixin = { // eslint-disable-line import/prefer-default-export
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
export const userCustomStateMixin = fieldname => {
|
||||
const map = { };
|
||||
map[fieldname] = 'user.data';
|
||||
return { // eslint-disable-line import/prefer-default-export
|
||||
computed: {
|
||||
...mapState(map),
|
||||
},
|
||||
methods: {
|
||||
hasPermission (user, permission) {
|
||||
return Boolean((user.permissions
|
||||
&& (user.permissions[permission] || user.permissions.fullAccess))
|
||||
|| (user.contributor && user.contributor.admin));
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const userStateMixin = userCustomStateMixin('user');
|
||||
|
||||
@@ -800,7 +800,7 @@ export default {
|
||||
|
||||
await this.reload();
|
||||
|
||||
// close members modal if the Private Messages page is opened in an existing tab
|
||||
// close modal if the Private Messages page is opened in an existing tab
|
||||
this.$root.$emit('bv::hide::modal', 'profile');
|
||||
this.$root.$emit('bv::hide::modal', 'members-modal');
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import handleRedirect from './handleRedirect';
|
||||
import ParentPage from '@/components/parentPage';
|
||||
import { PAGES } from '@/libs/consts';
|
||||
|
||||
// NOTE: when adding a page make sure to implement setTitle
|
||||
// NOTE: when adding a page make sure to implement the `common:setTitle` action
|
||||
|
||||
// Static Pages
|
||||
const StaticWrapper = () => import(/* webpackChunkName: "entry" */'@/components/static/staticWrapper');
|
||||
@@ -53,6 +53,10 @@ const HallPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/i
|
||||
const PatronsPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/patrons');
|
||||
const HeroesPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/heroes');
|
||||
|
||||
// Admin Panel
|
||||
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel');
|
||||
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel/user-support');
|
||||
|
||||
// Except for tasks that are always loaded all the other main level
|
||||
// All the main level
|
||||
// components are loaded in separate webpack chunks.
|
||||
@@ -109,7 +113,7 @@ const router = new VueRouter({
|
||||
scrollBehavior () {
|
||||
return { x: 0, y: 0 };
|
||||
},
|
||||
// requiresLogin is true by default, isStatic false
|
||||
// meta defaults: requiresLogin true, privilegeNeeded empty
|
||||
// NOTE: when adding a new route entry make sure to implement the `common:setTitle` action
|
||||
// in the route component to set a specific subtitle for the page.
|
||||
routes: [
|
||||
@@ -348,6 +352,31 @@ const router = new VueRouter({
|
||||
{ name: 'contributors', path: 'contributors', component: HeroesPage },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'adminPanel',
|
||||
path: '/admin-panel',
|
||||
component: AdminPanelPage,
|
||||
meta: {
|
||||
privilegeNeeded: [ // any one of these is enough to give access
|
||||
'userSupport',
|
||||
'newsPoster',
|
||||
],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'adminPanelUser',
|
||||
path: ':userIdentifier', // User ID or Username
|
||||
component: AdminPanelUserPage,
|
||||
meta: {
|
||||
privilegeNeeded: [
|
||||
'userSupport',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Only used to handle some redirects
|
||||
// See router.beforeEach
|
||||
{ path: '/redirect/:redirect', name: 'redirect' },
|
||||
@@ -357,9 +386,10 @@ const router = new VueRouter({
|
||||
|
||||
const store = getStore();
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const { isUserLoggedIn } = store.state;
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const { isUserLoggedIn, isUserLoaded } = store.state;
|
||||
const routeRequiresLogin = to.meta.requiresLogin !== false;
|
||||
const routePrivilegeNeeded = to.meta.privilegeNeeded;
|
||||
|
||||
if (to.name === 'redirect') return handleRedirect(to, from, next);
|
||||
|
||||
@@ -392,6 +422,17 @@ router.beforeEach((to, from, next) => {
|
||||
return next({ name: 'tasks' });
|
||||
}
|
||||
|
||||
if (routePrivilegeNeeded) {
|
||||
// Redirect non-admin users when trying to access a page.
|
||||
if (!isUserLoaded) await store.dispatch('user:fetch');
|
||||
if (!store.state.user.data.permissions.fullAccess) {
|
||||
const userHasPriv = routePrivilegeNeeded.some(
|
||||
privName => store.state.user.data.permissions[privName],
|
||||
);
|
||||
if (!userHasPriv) return next({ name: 'tasks' });
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect old guild urls
|
||||
if (to.hash.indexOf('#/options/groups/guilds/') !== -1) {
|
||||
const splits = to.hash.split('/');
|
||||
|
||||
@@ -26,3 +26,9 @@ export async function getPatrons (store, payload) {
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function getHeroParty (store, payload) {
|
||||
const url = `/api/v4/hall/heroes/party/${payload.groupId}`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export function canDelete (store) {
|
||||
const user = store.state.user.data;
|
||||
const userId = user.id || user._id;
|
||||
|
||||
const isUserAdmin = user.contributor && !!user.contributor.admin;
|
||||
const isUserAdmin = user.permissions && user.permissions.challengeAdmin;
|
||||
const isUserGroupLeader = group && (group.leader
|
||||
&& group.leader._id === userId);
|
||||
const isUserGroupManager = group && (group.managers
|
||||
@@ -84,7 +84,7 @@ export function canEdit (store) {
|
||||
const user = store.state.user.data;
|
||||
const userId = user.id || user._id;
|
||||
|
||||
const isUserAdmin = user.contributor && !!user.contributor.admin;
|
||||
const isUserAdmin = user.permissions && user.permissions.challengeAdmin;
|
||||
const isUserGroupLeader = group && (group.leader
|
||||
&& group.leader._id === userId);
|
||||
const isUserGroupManager = group && (group.managers
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('canDelete getter', () => {
|
||||
});
|
||||
|
||||
it('can Delete any challenge task as admin', () => {
|
||||
store.state.user.data.contributor.admin = true;
|
||||
store.state.user.data.permissions = { challengeAdmin: true };
|
||||
|
||||
expect(store.getters['tasks:canDelete'](task, 'challenge', true, null, challenge)).to.equal(true);
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('canEdit getter', () => {
|
||||
});
|
||||
|
||||
it('can Edit any challenge task if admin', () => {
|
||||
store.state.user.data.contributor.admin = true;
|
||||
store.state.user.data.permissions = { challengeAdmin: true };
|
||||
|
||||
expect(store.getters['tasks:canEdit'](task, 'challenge', true, null, challenge)).to.equal(true);
|
||||
expect(store.getters['tasks:canEdit'](task, 'challenge', false, null, challenge)).to.equal(true);
|
||||
|
||||
@@ -690,5 +690,7 @@
|
||||
"backgrounds042022": "SET 95: Veröffentlicht im April 2022",
|
||||
"backgroundBlossomingTreesNotes": "Verweile unter blühenden Bäumen.",
|
||||
"backgroundFlowerShopText": "Blumenladen",
|
||||
"backgroundFlowerShopNotes": "Genieße den süßen Duft eines Blumenladens."
|
||||
"backgroundFlowerShopNotes": "Genieße den süßen Duft eines Blumenladens.",
|
||||
"backgroundSpringtimeLakeText": "Frühlingssee",
|
||||
"backgroundSpringtimeLakeNotes": "Genieße die Aussicht an den Ufern eines Frühlingssees."
|
||||
}
|
||||
|
||||
@@ -370,5 +370,6 @@
|
||||
"hatchingPotionSunset": "Sonnenuntergang",
|
||||
"hatchingPotionSolarSystem": "Sonnensystem",
|
||||
"hatchingPotionMoonglow": "Mondschein",
|
||||
"hatchingPotionOnyx": "Onyx"
|
||||
"hatchingPotionOnyx": "Onyx",
|
||||
"hatchingPotionVirtualPet": "Virtuelles Haustier"
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"companyDonate": "Spenden",
|
||||
"forgotPassword": "Passwort vergessen?",
|
||||
"emailNewPass": "Einen Link per E-Mail senden, um das Passwort zurückzusetzen",
|
||||
"forgotPasswordSteps": "Trage deinen Benutzernamen oder die E-Mail-Adresse ein, mit der Du Deinen Habitica-Account aktiviert hast.",
|
||||
"forgotPasswordSteps": "Trage Deinen Benutzernamen oder die E-Mail-Adresse ein, mit der Du Deinen Habitica-Account aktiviert hast.",
|
||||
"sendLink": "Link senden",
|
||||
"featuredIn": "Vorgestellt in",
|
||||
"footerDevs": "Entwickler",
|
||||
|
||||
@@ -393,7 +393,7 @@
|
||||
"questTaskwoodsTerror3DropStrawberry": "Erdbeere (Futter)",
|
||||
"questTaskwoodsTerror3DropWeapon": "Laterne des Aufgabenwaldes (Zweihändige Waffe)",
|
||||
"questFerretText": "Das ruchlose Frettchen",
|
||||
"questFerretNotes": "Während Du durch Habit City spazierst, siehst Du wie eine unzufriedene Menge sich um ein rot-gekleidetes Frettchen schart. \"Der Produktivitäts-Trank ist nutzlos!\" beschwert sich @Beffymaroo. \"Ich habe gestern abend drei Stunden ferngesehen, anstatt meine Aufgaben zu erledigen.\"\"Genau!\" schreit @Pandah. \"Und heute habe ich eine Stunde damit verbracht meine Bücher zu lesen, anstatt sie zu lesen!\" <br>Das ruchlose Frettchen hebt unschuldig die Hände. \"Das ist mehr fernsehen und Bücher sortieren als Du normalerweise machen würdest, oder nicht?\" <br><br>Die Menge bricht in Zorn aus. <br> \"Kein Rückgeld!\" krächtst das ruchlose Frettchen. Er schleudert einen magischen Blitz in die Menge und bereitet sich darauf vor in dem Rauch zu verschwinden. <br><br>\"Bitte, Habiticaner!\" sagt @Faye, während sie Dich am Arm fässt. \"Besiege das Frettchen und zwinge es dazu das Geld aus seinen unehrlichen Machenschaften zurück zu geben!\"",
|
||||
"questFerretNotes": "Während Du durch Habit City spazierst, siehst Du wie eine unzufriedene Menge sich um ein rot-gekleidetes Frettchen schart. \"Der Produktivitäts-Trank ist nutzlos!\" beschwert sich @Beffymaroo. \"Ich habe gestern abend drei Stunden ferngesehen, anstatt meine Aufgaben zu erledigen.\"\"Genau!\" schreit @Pandah. \"Und heute habe ich eine Stunde damit verbracht meine Bücher zu lesen, anstatt sie zu lesen!\" <br>Das ruchlose Frettchen hebt unschuldig die Hände. \"Das ist mehr fernsehen und Bücher sortieren als Du normalerweise machen würdest, oder nicht?\" <br><br>Die Menge bricht in Zorn aus. <br> \"Kein Rückgeld!\" krächtst das ruchlose Frettchen. Er schleudert einen magischen Blitz in die Menge und bereitet sich darauf vor in dem Rauch zu verschwinden. <br><br>\"Bitte, Habiticaner!\" sagt @Faye, während sie Dich am Arm fässt. \"Besiege das Frettchen und zwinge es dazu das Geld aus seinen unehrlichen Machenschaften zurück zu geben!\"",
|
||||
"questFerretCompletion": "Du schlägst den weichpelzigen Betrüger in die Flucht und @UncommonCriminal erstattet der Menge die Rückzahlung. Es ist sogar etwas Gold für Dich übrig und es sieht so aus, als hätte das Ruchlose Frettchen in der Eile zu entkommen einige Eier fallen lassen!",
|
||||
"questFerretBoss": "Ruchloses Frettchen",
|
||||
"questFerretDropFerretEgg": "Frettchen (Ei)",
|
||||
@@ -433,7 +433,7 @@
|
||||
"questGroupStoikalmCalamity": "Stoïstilles Unglück",
|
||||
"questStoikalmCalamity1Text": "Stoïstilles Unglück, Teil 1: Erdgegner",
|
||||
"questStoikalmCalamity1Notes": "Ein knappes Schreiben von @Kiwibot trifft ein; nicht nur ist die frostbedeckte Schriftrolle eiskalt, sondern sie lässt Dir auch kalte Schauer den Rücken runterlaufen. \"Bin in Stoïstillen Steppen – Monster platzen aus Boden – brauche Hilfe!\" Du versammelst Deine Gruppe und reitest gen Norden, doch gerade, als Ihr Euch den Berg hinabbewegt, explodiert der Schnee unter Euren Füßen und grausig grinsende Schädel umzingeln Euch! <br><br>Plötzlich fliegt ein Speer an Euch vorbei und gräbt sich in einen Schädel, der Dich, sich durch den Schnee buddelnd, unbemerkt angreifen wollte. Eine große Frau in fein geschmiedeter Rüstung galoppiert auf dem Rücken eines Mastodons in die Schlacht und zieht mit wehendem Zopf rabiat den Speer wieder aus dem zerquetschten Biest. Zeit, die Feinde mit der Hilfe von Lady Glaciate, der Anführerin der Mammutreiter, zu bekämpfen!",
|
||||
"questStoikalmCalamity1Completion": "Als Du den letzten Schädeln den Gnadenstoß versetzt, lösen sie sich in einen Hauch Magie auf. \"Der verflixte Schwarm mag zwar verschwunden sein\", sagt Lady Glaciate, \"aber wir haben größere Probleme. Folge mir.\" Sie wirft Dir zum Schutz vor der eisigen Luft einen Mantel zu und Du reitest ihr nach.",
|
||||
"questStoikalmCalamity1Completion": "Als Du den letzten Schädeln den Gnadenstoß versetzt, lösen sie sich in einen Hauch Magie auf. \"Der verflixte Schwarm mag zwar verschwunden sein\", sagt Lady Glaciate, \"aber wir haben größere Probleme. Folge mir.\" Sie wirft Dir zum Schutz vor der eisigen Luft einen Mantel zu und Du reitest ihr nach.",
|
||||
"questStoikalmCalamity1Boss": "Erdschädelschwarm",
|
||||
"questStoikalmCalamity1RageTitle": "Schwarmnachwuchs",
|
||||
"questStoikalmCalamity1RageDescription": "Schwarmnachwuchs: Dieser Balken füllt sich, wenn Du Deine Tagesaufgaben nicht erfüllst. Wenn er voll ist, heilt sich der Erdschädelschwarm um 30% seiner verbleibenden Lebenspunkte!",
|
||||
@@ -525,7 +525,7 @@
|
||||
"questLostMasterclasser1CollectForbiddenTomes": "Verbotene Bücher",
|
||||
"questLostMasterclasser1CollectHiddenTomes": "Versteckte Bücher",
|
||||
"questLostMasterclasser2Text": "Das Geheimnis der Klassenmeister, Teil 2: Beschwörung des v'Schwinders",
|
||||
"questLostMasterclasser2Notes": "Der Fröhliche Reaper trommelt mit ihren knochigen Fingern auf den Büchern, die ihr mitgebracht habt. “Ach je”, sagt der Meister der Heiler. “Da ist eine bösartige Lebensessenz am Werk. Ich hätte es mir denken können, wenn man die Angriffe der wiederbelebten Schädel während der Vorfälle berücksichtigt.” Ihre rechte Hand @tricksy.fox bringt eine Truhe herein, und Du bist überrascht zu sehen, was beffymaroo daraus hervorholt: es sind genau die Gegenstände, die einst von der mysteriösen Tzina benutzt wurden, um anderen ihren Willen aufzuzwingen.<br><br>“Ich werde mit resonierender Heilmagie versuchen, die Kreatur zu manifestieren”, sagt der Fröhliche Reaper, und erinnert Dich daran, dass das Skelett ein eher unkonventioneller Heiler ist. “Du musst die enthüllten Informationen schnell lesen, für den Fall dass sie freikommt.” <br><br>Als sie sich konzentriert, fließt wirbelnder Nebel aus den Büchern und windet sich um die Gegenstände. Du blätterst schnell durch die Seiten, in dem Versuch, die neuen Textzeilen zu lesen, die wabernd wieder sichtbar werden. Du kannst nur ein paar Bruchstücke erfassen: “Sand der Zeitwüste” — “die Große Katastrophe” —“in vier Teile gespalten”— “für immer verdorben”— bevor Dir ein einzelner Name ins Auge springt: Zinnya. <br><br> Schlagartig befreien sich die Seiten aus Deinen Händen und zerfallen in der Luft in tausend Schnipsel, als mit einer Explosion eine heulende Kreatur erscheint und sich mit den Gegenständen verbindet. <br><br>“Das ist ein v'Schwinder!” ruft der Fröhliche Reaper und wirft einen Schutzzauber über euch. “Das sind alte Kreaturen der Verwirrung und Verschleierung. Wenn diese Tzina so einen kontrollieren kann, muss sie eine beängstigende Macht über Lebensmagie haben. Schnell, greift ihn an, bevor er wieder in die Bücher flüchtet!”<br><br>",
|
||||
"questLostMasterclasser2Notes": "Der Fröhliche Reaper trommelt mit ihren knochigen Fingern auf den Büchern, die ihr mitgebracht habt. “Ach je”, sagt der Meister der Heiler. “Da ist eine bösartige Lebensessenz am Werk. Ich hätte es mir denken können, wenn man die Angriffe der wiederbelebten Schädel während der Vorfälle berücksichtigt.” Ihre rechte Hand @tricksy.fox bringt eine Truhe herein, und Du bist überrascht zu sehen, was beffymaroo daraus hervorholt: es sind genau die Gegenstände, die einst von der mysteriösen Tzina benutzt wurden, um anderen ihren Willen aufzuzwingen.<br><br>“Ich werde mit resonierender Heilmagie versuchen, die Kreatur zu manifestieren”, sagt der Fröhliche Reaper, und erinnert Dich daran, dass das Skelett ein eher unkonventioneller Heiler ist. “Du musst die enthüllten Informationen schnell lesen, für den Fall dass sie freikommt.” <br><br>Als sie sich konzentriert, fließt wirbelnder Nebel aus den Büchern und windet sich um die Gegenstände. Du blätterst schnell durch die Seiten, in dem Versuch, die neuen Textzeilen zu lesen, die wabernd wieder sichtbar werden. Du kannst nur ein paar Bruchstücke erfassen: “Sand der Zeitwüste” — “die Große Katastrophe” —“in vier Teile gespalten”— “für immer verdorben”— bevor Dir ein einzelner Name ins Auge springt: Zinnya. <br><br> Schlagartig befreien sich die Seiten aus Deinen Händen und zerfallen in der Luft in tausend Schnipsel, als mit einer Explosion eine heulende Kreatur erscheint und sich mit den Gegenständen verbindet. <br><br>“Das ist ein v'Schwinder!” ruft der Fröhliche Reaper und wirft einen Schutzzauber über euch. “Das sind alte Kreaturen der Verwirrung und Verschleierung. Wenn diese Tzina so einen kontrollieren kann, muss sie eine beängstigende Macht über Lebensmagie haben. Schnell, greift ihn an, bevor er wieder in die Bücher flüchtet!”<br><br>",
|
||||
"questLostMasterclasser2Completion": "Der v'Schwinder unterliegt endlich, und Du liest die Schnipsel vor.<br><br>“Keine dieser Referenzen klingt vertraut, auch nicht für jemanden, der so alt ist wie ich”, sagt der Fröhliche Reaper. “Außer.... die Zeitwüste ist eine entfernte Wüste am unwirtlichsten Rand von Habitica. Portale versagen oft in der Nähe, aber schnelle Reittiere könnten Dich im Handumdrehen dorthin bringen. Lady Glaciate wird gerne helfen.” Ihre Stimme wird immer amüsierter. \"Das bedeutet, dass der verliebte Meister der Schurken zweifellos mitkommen wird.\" Sie gibt dir die schimmernde Maske. \"Vielleicht solltest du versuchen, die verbleibende Magie in diesen Gegenständen bis zur Quelle zu verfolgen. Ich werde etwas Nahrung für Deine Reise sammeln.\"",
|
||||
"questLostMasterclasser2Boss": "Der v'Schwinder",
|
||||
"questLostMasterclasser2DropEyewear": "Äthermaske (Brille)",
|
||||
@@ -574,7 +574,7 @@
|
||||
"questBadgerUnlockText": "Schaltet den Kauf von Dachseiern auf dem Marktplatz frei",
|
||||
"questDysheartenerText": "Der Entmutiger",
|
||||
"questDysheartenerNotes": "Es ist Valentinstag, die Sonne geht gerade auf, als plötzlich ein erschütternder Krach die Luft zerreißt. Ein kränkliches rosa Licht durchdringt die Gebäude, und Ziegel zerbrechen, als sich ein tiefer Riss auf Habit City's Hauptstraße auftut. Ein überirdisches Kreischen ertönt durch die Luft und lässt die Fenster zerspringen, während sich eine ungeschlachte Form aus der klaffenden Erde erhebt.<br><br>Mandibeln schnappen, der Panzer glitzert; Bein um Bein entfaltet sich in der Luft. Die Menge beginnt zu schreien, als die insektoide Kreatur aufsteht und sich als schrecklichste aller Kreaturen zu erkennen gibt: der furchterregende Entmutiger höchstselbst. Er heult erwartungsvoll und stürzt vor, um an den Hoffnungen hart arbeitender Habiticaner zu nagen. Mit jedem scharrenden Kratzen seiner stacheligen Vorderbeine fühlst Du, wie sich Dein Herz in der Brust vor Verzweiflung weiter zusammenzieht.<br><br>“Fasst Euch alle ein Herz!” ruft Lemoness. “Er denkt vielleicht, dass wir leichte Ziele sind, weil so viele von uns entmutigende Neujahrsvorsätze haben, aber er wird feststellen, dass Habiticaner wissen, wie man an seinen Zielen festhält!”<br><br>AnnDeLune hebt ihren Stab. “Lasst uns unsere Aufgaben angehen und dieses Monster erledigen!”",
|
||||
"questDysheartenerCompletion": "<strong>Der Entmutiger wurde BESIEGT!</strong><br><br>Gemeinsam holen alle in Habitica zu einem letzten Schlag mit ihren Aufgaben aus, und der Entmutiger weicht mit einem bestürzten Kreischen zurück. “Stimmt etwas nicht, Entmutiger?” ruft AnnDeLune mit funkelnden Augen. “Fühlst Du Dich entmutigt?”<br><br>Glühend pinke Risse zeigen sich auf dem Panzer des Entmutigers, und er zerbricht mit einer verpuffenden rosa Rauchwolke. Eine Flut von köstlichen Süßigkeiten regnet auf alle hernieder, während sich im ganzen Land ein erneuertes Gefühl von Kraft und Entschlossenheit ausbreitet.<br><br>Die Menge jubelt wild, Umarmungen werden ausgetauscht, und die Haustiere kauen glücklich auf ihren verspäteten Valentinsleckerli. Plötzlich liegt Gesang in der Luft, ein fröhlicher Chor ertönt, und funkelnde Silhouetten ziehen über den Himmel.<br><br>Unser neu-gewonnener Optimismus hat eine Herde Hippogreifen angelockt! Die anmutigen Kreaturen setzen auf dem Boden auf, sträuben interessiert ihre Federn und tänzeln auf der Stelle. “Wie es aussieht, haben wir neue Freunde gefunden, die uns helfen, nicht zu verzagen, selbst wenn unsere Aufgaben noch so beängstigend sind”, sagt Lemoness. <br><br>Beffymaroo hat bereits ihre Arme voll mit gefiederten Plüschbällen. “Vielleicht helfen sie uns, die zerstörten Gebiete Habitica's wieder aufzubauen!”<br><br>Summend und singend ziehen die Hippogreifen voran, während alle Habiticaner zusammenarbeiten, um unsere geliebte Heimat wieder aufzubauen.",
|
||||
"questDysheartenerCompletion": "<strong>Der Entmutiger wurde BESIEGT!</strong><br><br>Gemeinsam holen alle in Habitica zu einem letzten Schlag mit ihren Aufgaben aus, und der Entmutiger weicht mit einem bestürzten Kreischen zurück. “Stimmt etwas nicht, Entmutiger?” ruft AnnDeLune mit funkelnden Augen. “Fühlst Du Dich entmutigt?”<br><br>Glühend pinke Risse zeigen sich auf dem Panzer des Entmutigers, und er zerbricht mit einer verpuffenden rosa Rauchwolke. Eine Flut von köstlichen Süßigkeiten regnet auf alle hernieder, während sich im ganzen Land ein erneuertes Gefühl von Kraft und Entschlossenheit ausbreitet.<br><br>Die Menge jubelt wild, Umarmungen werden ausgetauscht, und die Haustiere kauen glücklich auf ihren verspäteten Valentinsleckerli. Plötzlich liegt Gesang in der Luft, ein fröhlicher Chor ertönt, und funkelnde Silhouetten ziehen über den Himmel.<br><br>Unser neu-gewonnener Optimismus hat eine Herde Hippogreifen angelockt! Die anmutigen Kreaturen setzen auf dem Boden auf, sträuben interessiert ihre Federn und tänzeln auf der Stelle. “Wie es aussieht, haben wir neue Freunde gefunden, die uns helfen, nicht zu verzagen, selbst wenn unsere Aufgaben noch so beängstigend sind”, sagt Lemoness. <br><br>Beffymaroo hat bereits ihre Arme voll mit gefiederten Plüschbällen. “Vielleicht helfen sie uns, die zerstörten Gebiete Habitica's wieder aufzubauen!”<br><br>Summend und singend ziehen die Hippogreifen voran, während alle Habiticaner zusammenarbeiten, um unsere geliebte Heimat wieder aufzubauen.",
|
||||
"questDysheartenerCompletionChat": "`Der Entmutiger wurde BESIEGT!'`\n\nGemeinsam holen alle in Habitica zu einem letzten Schlag mit ihren Aufgaben aus, und der Entmutiger weicht mit einem bestürzten Kreischen zurück. “Stimmt etwas nicht, Entmutiger?” ruft AnnDeLune mit funkelnden Augen. “Fühlst Du Dich entmutigt?”\n\nGlühend pinke Risse zeigen sich auf dem Panzer des Entmutigers, und er zerbricht mit einer verpuffenden rosa Rauchwolke. Eine Flut von köstlichen Süßigkeiten regnet auf alle hernieder, während sich im ganzen Land ein erneuertes Gefühl von Kraft und Entschlossenheit ausbreitet.\n\nDie Menge jubelt wild, Umarmungen werden ausgetauscht, und die Haustiere kauen glücklich auf ihren verspäteten Valentinsleckerli. Plötzlich liegt Gesang in der Luft, ein fröhlicher Chor ertönt, und funkelnde Silhouetten ziehen über den Himmel.\n\nUnser neu-gewonnener Optimismus hat eine Herde Hippogreifen angelockt! Die anmutigen Kreaturen setzen auf dem Boden auf, sträuben interessiert ihre Federn und tänzeln auf der Stelle. “Wie es aussieht, haben wir neue Freunde gefunden, die uns helfen, nicht zu verzagen, selbst wenn unsere Aufgaben noch so beängstigend sind”, sagt Lemoness.\n\nBeffymaroo hat bereits ihre Arme voll mit gefiederten Plüschbällen. “Vielleicht helfen sie uns, die zerstörten Gebiete Habitica's wieder aufzubauen!”\n\nSummend und singend ziehen die Hippogreifen voran, während alle Habiticaner zusammenarbeiten, um unsere geliebte Heimat wieder aufzubauen.",
|
||||
"questDysheartenerBossRageTitle": "Niederschmetternder Herzschmerz",
|
||||
"questDysheartenerBossRageDescription": "Die Anzeige für den Raserei-Angriff füllt sich, wenn Habiticaner ihre Tagesaufgaben nicht abhaken. Sobald sie gefüllt ist, wird der Entmutiger seine Niederschmetternde Herzschmerz-Attacke über einem von Habitica's Ladenbesitzern entfesseln, also strengt Euch an und erledigt Eure Aufgaben!",
|
||||
@@ -744,5 +744,14 @@
|
||||
"questOnyxCollectLeoRunes": "Leo Runen",
|
||||
"questOnyxCollectOnyxStones": "Onyx Steine",
|
||||
"questOnyxDropOnyxPotion": "Onyx Schlüpfelixier",
|
||||
"questOnyxUnlockText": "Schaltet das Onyx Schlüpfelixier zum Kauf auf dem Marktplatz frei"
|
||||
"questOnyxUnlockText": "Schaltet das Onyx Schlüpfelixier zum Kauf auf dem Marktplatz frei",
|
||||
"questVirtualPetBoss": "Wotchimon",
|
||||
"questVirtualPetRageTitle": "Das Piepen",
|
||||
"questVirtualPetRageDescription": "Dieser Balken füllt sich, wenn Du Deine Tagesaufgaben nicht abschließt. Ist er vollständig gefüllt, wird Wotchimon sich um 30% seiner verbleibenden Gesundheit heilen!",
|
||||
"questVirtualPetRageEffect": "`Wotchimon setzt lästiges Piepen ein!` Wotchimon lässt ein lästiges Piepen ertönen, und seine Zufriedenheitsanzeige verschwindet plötzlich! Ausstehender Schaden reduziert.",
|
||||
"questVirtualPetDropVirtualPetPotion": "Virtuelles Haustier Schlüpfelixier",
|
||||
"questVirtualPetUnlockText": "Schaltet das Virtuelles Haustier Schlüpfelixier zum Kauf auf dem Marktplatz frei",
|
||||
"questVirtualPetText": "Virtuelles Chaos mit dem April-Scherzkeks: Das Piepen",
|
||||
"questVirtualPetCompletion": "Vorsichtiges Betätigen von Knöpfen scheint die mysteriösen Bedürfnisse des virtuellen Haustiers erfüllt zu haben, und es hat sich endlich beruhigt und wirkt zufrieden.<br><br>Plötzlich erscheint in einem Konfettiregen der April-Scherzkeks mit einem Korb voller seltsamer Elixiere, die leise vor sich hin piepen.<br><br>\"Gutes Timing, April-Scherzkeks,\" sagt @Beffymaroo mit einem schiefen lächeln. \"Ich vermute, dieser große Kerl ist ein Bekannter von Dir.\"<br><br>\"Hm, ja,\" sagt der April-Scherzkeks verlegen. \"Es tut mir sehr leid, und ich danke euch beiden dafür, dass ihr euch um Wotchimon gekümmert habt! Nehmt diese Elixiere als Dank, sie können eure virtuellen Haustiere jederzeit zurückbringen!\"<br><br>Du bist dir noch nicht zu 100% sicher, ob Du mit dem vielen Piepen einverstanden bist, aber sie sind so süß, dass es einen Versuch wert ist!",
|
||||
"questVirtualPetNotes": "Es ist ein schöner, ruhiger Frühlingsmorgen in Habitica, eine Woche nach einem erinnerungswürdigen ersten April. Du und @Beffymaroo seid in den Ställen und kümmert euch um eure Haustiere (welche immer noch ein wenig verwirrt sind von der Zeit, die sie als virtuelle Haustiere verbracht haben!).<br><br>In der Ferne hört ihr ein Grollen und ein Piepen, zunächst leise, aber schnell an Lautstärke gewinnend, als käme es näher. Eine Ei-Form erscheint am Horizont und während sie sich nähert und noch lauter piept erkennt ihr, dass es ein gigantisches virtuelles Haustier ist!<br><br>\"Oh nein\" ruft @Beffymaroo, \"Ich fürchte der April-Scherzkeks hat mit diesem großen Kerl noch ein paar unerledigte Angelegenheiten, er scheint Aufmerksamkeit zu wollen!\"<br><br>Das virtuelle Haustier piept wütend, bekommt einen virtuellen Wutanfall und nähert sich immer weiter."
|
||||
}
|
||||
|
||||
@@ -779,6 +779,14 @@
|
||||
"backgroundSpringtimeLakeText": "Springtime Lake",
|
||||
"backgroundSpringtimeLakeNotes": "Take in the sights along the shores of a Springtime Lake.",
|
||||
|
||||
"backgrounds052022": "SET 96: Released May 2022",
|
||||
"backgroundOnACastleWallText": "On A Castle Wall",
|
||||
"backgroundOnACastleWallNotes": "Look out from On a Castle Wall.",
|
||||
"backgroundCastleGateText": "Castle Gate",
|
||||
"backgroundCastleGateNotes": "Stand guard at the Castle Gate.",
|
||||
"backgroundEnchantedMusicRoomText": "Enchanted Music Room",
|
||||
"backgroundEnchantedMusicRoomNotes": "Play in an Enchanted Music Room.",
|
||||
|
||||
"timeTravelBackgrounds": "Steampunk Backgrounds",
|
||||
"backgroundAirshipText": "Airship",
|
||||
"backgroundAirshipNotes": "Become a sky sailor on board your very own Airship.",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"hallContributors": "Hall of Contributors",
|
||||
"hallPatrons": "Hall of Patrons",
|
||||
"noAdminAccess": "You don't have admin access.",
|
||||
"noPrivAccess": "You don't have the required privileges.",
|
||||
"userNotFound": "User not found.",
|
||||
"invalidUUID": "UUID must be valid",
|
||||
"title": "Title",
|
||||
|
||||
@@ -628,6 +628,8 @@
|
||||
"weaponArmoirePinkLongbowNotes": "Be a cupid-in-training, mastering both archery and matters of the heart with this beautiful bow. Increases Perception by <%= per %> and Strength by <%= str %>. Enchanted Armoire: Independent Item.",
|
||||
"weaponArmoireGardenersWateringCanText": "Watering Can",
|
||||
"weaponArmoireGardenersWateringCanNotes": "You can’t get far without water! Have an infinite supply on hand with this magic, refilling watering can. Increases Intelligence by <%= int %>. Enchanted Armoire: Gardener Set (Item 4 of 4).",
|
||||
"weaponArmoireHuntingHornText": "Hunting Horn",
|
||||
"weaponArmoireHuntingHornNotes": "Twooooo! Twoo! Twoo! Gather your party for an adventure or quest by playing this horn. Increases Strength by <%= str %> and Intelligence by <%= int %>. Enchanted Armoire: Musical Instrument Set 1 (Item 1 of 3)",
|
||||
|
||||
"armor": "armor",
|
||||
"armorCapitalized": "Armor",
|
||||
@@ -2426,6 +2428,10 @@
|
||||
"shieldArmoireSoftVioletPillowNotes": "The clever warrior packs a pillow for any expedition. Protect yourself from procrastination-induced panic... even while you nap. Increases Intelligence by <%= int %>. Enchanted Armoire: Violet Loungewear Set (Item 3 of 3).",
|
||||
"shieldArmoireGardenersSpadeText": "Gardener's Spade",
|
||||
"shieldArmoireGardenersSpadeNotes": "Whether you’re digging in the garden, searching for buried treasure, or creating a secret tunnel, this trusty spade is at your side. Increases Strength by <%= str %>. Enchanted Armoire: Gardener Set (Item 3 of 4).",
|
||||
"shieldArmoireSpanishGuitarText": "Spanish Guitar",
|
||||
"shieldArmoireSpanishGuitarNotes": "Tink! Tink! Thrummm! Gather your party for a concert or celebration by playing this guitar. Increases Perception by <%= per %> and Intelligence by <%= int %>. Enchanted Armoire: Musical Instrument Set 1 (Item 2 of 3)",
|
||||
"shieldArmoireSnareDrumText": "Snare Drum",
|
||||
"shieldArmoireSnareDrumNotes": "Rat-a-tat-tat! Gather your party for a parade or march into battle by playing this drum. Increases Constitution by <%= con %> and Intelligence by <%= int %>. Enchanted Armoire: Musical Instrument Set 1 (Item 3 of 3)",
|
||||
|
||||
"back": "Back Accessory",
|
||||
"backBase0Text": "No Back Accessory",
|
||||
|
||||
@@ -672,8 +672,8 @@
|
||||
"backgroundWinterWaterfallNotes": "Maravíllate en la catarata invernal.",
|
||||
"backgroundIridescentCloudsText": "Nubes iridiscentes",
|
||||
"backgroundIridescentCloudsNotes": "Flota entre nubes iridiscentes.",
|
||||
"backgroundOrangeGroveText": "Campo de naranjos",
|
||||
"backgroundOrangeGroveNotes": "Pasea por un fragante campo de naranjos.",
|
||||
"backgroundOrangeGroveText": "Naranjal",
|
||||
"backgroundOrangeGroveNotes": "Deambula por un naranjal fragante.",
|
||||
"backgrounds022022": "93.ª serie: publicada en febrero de 2022",
|
||||
"backgrounds032022": "94.ª serie: publiccada en marzo de 2022",
|
||||
"backgroundBrickWallWithIvyText": "Pared de Ladrillo con Hiedra",
|
||||
@@ -688,5 +688,6 @@
|
||||
"backgroundFloweringPrairieNotes": "Brinca por una pradera floreciente.",
|
||||
"backgroundFloweringPrairieText": "Pradera floreciente",
|
||||
"backgroundSpringtimeLakeText": "Lago de Primavera",
|
||||
"backgroundSpringtimeLakeNotes": "Disfruta las vistas a orillas de un Lago de Primavera."
|
||||
"backgroundSpringtimeLakeNotes": "Disfruta las vistas a orillas de un Lago de Primavera.",
|
||||
"hideLockedBackgrounds": "Esconde fondos cerrados"
|
||||
}
|
||||
|
||||
@@ -186,5 +186,6 @@
|
||||
"communityInstagram": "Instagram",
|
||||
"minPasswordLength": "La contraseña debe contener 8 caracteres o más.",
|
||||
"enterHabitica": "Adéntrate en Habitica",
|
||||
"emailUsernamePlaceholder": "p.e., habitrabbit o gryphon@example.com"
|
||||
"emailUsernamePlaceholder": "p.e., habitrabbit o gryphon@example.com",
|
||||
"socialAlreadyExists": "Esta identificación social ya está vinculado a una cuenta Habitica existente."
|
||||
}
|
||||
|
||||
@@ -164,14 +164,14 @@
|
||||
"summer2019ConchHealerSet": "Caracola (Sanador)",
|
||||
"summer2019WaterLilyMageSet": "Nenúfar (Mago)",
|
||||
"summer2019SeaTurtleWarriorSet": "Tortuga Marina (Guerrero)",
|
||||
"augustYYYY": "Agosto del <%= year %>",
|
||||
"augustYYYY": "Agosto <%= year %>",
|
||||
"decemberYYYY": "Diciembre <%= year %>",
|
||||
"winter2020LanternSet": "Linterna (Pícaro)",
|
||||
"winter2020WinterSpiceSet": "Especia de Invierno (Sanador)",
|
||||
"winter2020CarolOfTheMageSet": "Villancico del Mago",
|
||||
"winter2020EvergreenSet": "Siempre Joven (Guerrero)",
|
||||
"mayYYYY": "Mayo del <%= year %>",
|
||||
"marchYYYY": "Marzo del <%= year %>",
|
||||
"mayYYYY": "Mayo <%= year %>",
|
||||
"marchYYYY": "Marzo <%= year %>",
|
||||
"summer2020CrocodileRogueSet": "Cocodrilo (Pícaro)",
|
||||
"summer2020SeaGlassHealerSet": "Cristal Marino (Sanador)",
|
||||
"summer2020OarfishMageSet": "Regaleco (Mago)",
|
||||
@@ -220,5 +220,6 @@
|
||||
"spring2022MagpieRogueSet": "Urraca (Pícaro)",
|
||||
"spring2022RainstormWarriorSet": "Tempestad (Guerrero)",
|
||||
"spring2022ForsythiaMageSet": "Forsitia (Mago)",
|
||||
"spring2022PeridotHealerSet": "Peridoto (Sanador)"
|
||||
"spring2022PeridotHealerSet": "Peridoto (Sanador)",
|
||||
"aprilYYYY": "Abril <%= year %>"
|
||||
}
|
||||
|
||||
@@ -662,5 +662,31 @@
|
||||
"backgroundWinterCanyonText": "Cañón Invernal",
|
||||
"backgroundIcePalaceText": "Palacio de Hielo",
|
||||
"backgroundIcePalaceNotes": "Reina desde el Palacio de Hielo.",
|
||||
"backgrounds012022": "CONJUNTO 92: Lanzado en Enero 2022"
|
||||
"backgrounds012022": "CONJUNTO 92: Lanzado en Enero 2022",
|
||||
"backgrounds022022": "CONJUNTO 93: Lanzado en Febrero 2022",
|
||||
"backgroundMeteorShowerText": "Lluvia de meteoritos",
|
||||
"backgroundMeteorShowerNotes": "Observa el espectáculo nocturno deslumbrante de una lluvia de meteoritos.",
|
||||
"backgroundPalmTreeWithFairyLightsText": "Palmera con una guirnalda luminosa",
|
||||
"backgroundSnowyFarmText": "Granja Nevada",
|
||||
"backgroundSnowyFarmNotes": "Comprueba que todo el mundo es calentito y a gusto en tu Granja Nevada.",
|
||||
"backgroundPalmTreeWithFairyLightsNotes": "Posa junto a una palmera decorada con una guirnalda luminosa.",
|
||||
"backgroundWinterWaterfallText": "Catarata invernal",
|
||||
"backgroundWinterWaterfallNotes": "Maravíllate en la catarata invernal.",
|
||||
"backgroundOrangeGroveText": "Naranjal",
|
||||
"backgroundOrangeGroveNotes": "Deambula por un naranjal fragante.",
|
||||
"backgroundIridescentCloudsText": "Nubes iridiscentes",
|
||||
"backgroundIridescentCloudsNotes": "Flota entre nubes iridiscentes.",
|
||||
"backgrounds032022": "CONJUNTO 94: Lanzado en Marzo 2022",
|
||||
"backgroundAnimalsDenText": "Cubil de una Criatura del Bosque",
|
||||
"backgroundAnimalsDenNotes": "Ponte cómodo en el Cubil de una Criatura del Bosque.",
|
||||
"backgroundBrickWallWithIvyText": "Pared de Ladrillo con Hiedra",
|
||||
"backgroundBrickWallWithIvyNotes": "Admira una Pared de Ladrillo con Hiedra.",
|
||||
"backgroundFloweringPrairieNotes": "Brinca por una pradera floreciente.",
|
||||
"backgrounds042022": "CONJUNTO 95: Lanzado en Abril 2022",
|
||||
"backgroundBlossomingTreesText": "Árboles Florecidos",
|
||||
"backgroundBlossomingTreesNotes": "Entretente bajo Árboles Florecidos.",
|
||||
"backgroundFlowerShopText": "Tienda de Flores",
|
||||
"backgroundFlowerShopNotes": "Disfruta el aroma suave de una Tienda de Flores.",
|
||||
"backgroundSpringtimeLakeText": "Lago de Primavera",
|
||||
"backgroundSpringtimeLakeNotes": "Disfruta las vistas a orillas de un Lago de Primavera."
|
||||
}
|
||||
|
||||
@@ -2459,5 +2459,15 @@
|
||||
"weaponArmoirePotionBaseNotes": "Las mascotas que eclosionas con estas pociones serán muchas cosas, ¡pero no básicas! Aumenta la Fuerza, Inteligencia, Constitución y Percepción en <%= attrs %>. Armario Encantado: Conjunto de Pociones (Artículo 1 de 10)",
|
||||
"weaponMystery202201Text": "Cañón de Confeti de Medianoche",
|
||||
"weaponMystery202201Notes": "Libera una nube de dorada y plateada brillantina cuando el reloj toque la medianoche. ¡Feliz año nuevo! Y ¿quién va a limpiar esto? No otorga ningún beneficio. Artículo de suscriptor de Enero de 2022.",
|
||||
"weaponMystery202111Notes": "Da forma al flujo temporal con este bastón misterioso y poderoso. No otorga ningún beneficio. Artículo de suscriptor de Noviembre 2021."
|
||||
"weaponMystery202111Notes": "Da forma al flujo temporal con este bastón misterioso y poderoso. No otorga ningún beneficio. Artículo de suscriptor de Noviembre 2021.",
|
||||
"weaponArmoirePotionWhiteText": "Poción Blanca Decorativa",
|
||||
"weaponArmoirePotionWhiteNotes": "¡Las mascotas eclosionadas usando esta poción podrían perderse en la nieve! Aumenta la Constitución en <%= con %> y la Percepción en <%= per %>. Armario Encantado: Conjunto de Pociones (Artículo 2 de 10)",
|
||||
"weaponArmoirePotionDesertText": "Poción de Desierto Decorativa",
|
||||
"weaponArmoirePotionDesertNotes": "¡Con esta poción no te hará falta estar en una isla desierta para encontrar a una mascota color desierto con la que disfrutarla! Aumenta la Fuerza en <%= str %> y la Constitución en <%= con %>. Armario Encantado: Conjunto de Pociones (Artículo 3 de 10)",
|
||||
"weaponArmoirePotionRedText": "Poción Roja Decorativa",
|
||||
"weaponArmoirePotionSkeletonText": "Poción de Esqueleto Decorativa",
|
||||
"weaponArmoirePotionRedNotes": "¡No te pongas colorado hoy, porque esta poción de eclosión no te dejará en números rojos! Aumenta la Fuerza y la Constitución en <%= attrs %>. Armario Encantado: Conjunto de Pociones (Artículo 4 de 10)",
|
||||
"weaponArmoirePotionShadeText": "Poción de Sombra Decorativa",
|
||||
"weaponArmoirePotionShadeNotes": "Como dice el refrán, a la sombra del favor, crecen vicios. ¡Y, a la sombra de esta poción, una mascota (a)sombrosa! Aumenta la Inteligencia en <%= int %> y la Percepción en <%= per %>. Armario Encantado: Conjunto de Pociones (Artículo 5 de 10)",
|
||||
"weaponArmoirePotionSkeletonNotes": "¿Te sientes productivo hoy? ¡Pues a mover el esqueleto! ¡No te olvides de llevarte esta poción de eclosión de esqueleto contigo! Aumenta la Fuerza en <%= str %> y la Inteligencia en <%= int %>. Armario Encantado: Conjunto de Pociones (Artículo 6 de 10)"
|
||||
}
|
||||
|
||||
@@ -217,6 +217,6 @@
|
||||
"spring2022RainstormWarriorSet": "Tempestad (Guerrero)",
|
||||
"spring2022ForsythiaMageSet": "Forsitia (Mago)",
|
||||
"spring2022PeridotHealerSet": "Peridoto (Sanador)",
|
||||
"januaryYYYY": "Enero <%= year %>",
|
||||
"aprilYYYY": "Abril <%= year %>"
|
||||
"januaryYYYY": "Enero, <%= year %>",
|
||||
"aprilYYYY": "Abril, <%= year %>"
|
||||
}
|
||||
|
||||
@@ -114,5 +114,7 @@
|
||||
"achievementPartyOn": "Lumaki ang iyong partido sa 4 na miyembro!",
|
||||
"achievementKickstarter2019Text": "Sinuportahan ang 2019 Pin Kickstarter Project",
|
||||
"achievementKickstarter2019": "Pin Kickstarter Backer",
|
||||
"achievementAridAuthorityModalText": "Napaamo mo ang lahat ng Desert Mounts!"
|
||||
"achievementAridAuthorityModalText": "Napaamo mo ang lahat ng Desert Mounts!",
|
||||
"achievementDomesticated": "E-I-E-I-O",
|
||||
"achievementDomesticatedText": "Napisâ ang lahát ng karaniwang kulay ng mga alagang hayop: Ferret, Guinea Pig, Rooster, Flying Pig, Daga, Bunny, Kabayo, at Baka!"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"potionText": "Health Potion",
|
||||
"potionText": "Mahiwagang Langís na Pámpalusóg",
|
||||
"potionNotes": "Mag-recover ng 15 Health (Instant Use)",
|
||||
"armoireText": "Enchanted Armoire",
|
||||
"armoireNotesFull": "Open the Armoire to randomly receive special Equipment, Experience, or food! Equipment pieces remaining:",
|
||||
@@ -183,35 +183,35 @@
|
||||
"questEggVelociraptorMountText": "Velociraptor",
|
||||
"questEggVelociraptorAdjective": "a clever",
|
||||
"eggNotes": "Find a hatching potion to pour on this egg, and it will hatch into <%= eggAdjective(locale) %> <%= eggText(locale) %>.",
|
||||
"hatchingPotionBase": "Base",
|
||||
"hatchingPotionWhite": "White",
|
||||
"hatchingPotionDesert": "Desert",
|
||||
"hatchingPotionRed": "Red",
|
||||
"hatchingPotionShade": "Shade",
|
||||
"hatchingPotionSkeleton": "Skeleton",
|
||||
"hatchingPotionZombie": "Zombie",
|
||||
"hatchingPotionCottonCandyPink": "Cotton Candy Pink",
|
||||
"hatchingPotionCottonCandyBlue": "Cotton Candy Blue",
|
||||
"hatchingPotionGolden": "Golden",
|
||||
"hatchingPotionSpooky": "Spooky",
|
||||
"hatchingPotionPeppermint": "Peppermint",
|
||||
"hatchingPotionFloral": "Floral",
|
||||
"hatchingPotionAquatic": "Aquatic",
|
||||
"hatchingPotionEmber": "Ember",
|
||||
"hatchingPotionThunderstorm": "Thunderstorm",
|
||||
"hatchingPotionGhost": "Ghost",
|
||||
"hatchingPotionRoyalPurple": "Royal Purple",
|
||||
"hatchingPotionHolly": "Holly",
|
||||
"hatchingPotionCupid": "Cupid",
|
||||
"hatchingPotionShimmer": "Shimmer",
|
||||
"hatchingPotionFairy": "Fairy",
|
||||
"hatchingPotionStarryNight": "Starry Night",
|
||||
"hatchingPotionRainbow": "Rainbow",
|
||||
"hatchingPotionGlass": "Glass",
|
||||
"hatchingPotionGlow": "Glow-in-the-Dark",
|
||||
"hatchingPotionFrost": "Frost",
|
||||
"hatchingPotionIcySnow": "Icy Snow",
|
||||
"hatchingPotionNotes": "Pour this on an egg, and it will hatch as a <%= potText(locale) %> pet.",
|
||||
"hatchingPotionBase": "Batayán",
|
||||
"hatchingPotionWhite": "Putí",
|
||||
"hatchingPotionDesert": "Desyerto",
|
||||
"hatchingPotionRed": "Pulá",
|
||||
"hatchingPotionShade": "Lilim",
|
||||
"hatchingPotionSkeleton": "Kalansáy",
|
||||
"hatchingPotionZombie": "Buháy na Bangkáy",
|
||||
"hatchingPotionCottonCandyPink": "Kalimbahíng Minatamís na Bulak",
|
||||
"hatchingPotionCottonCandyBlue": "Bugháw na Minatamís na Bulak",
|
||||
"hatchingPotionGolden": "Ginintuán",
|
||||
"hatchingPotionSpooky": "Nakakakilabot",
|
||||
"hatchingPotionPeppermint": "Yerba Buwena",
|
||||
"hatchingPotionFloral": "Mabulaklák",
|
||||
"hatchingPotionAquatic": "Pantubig",
|
||||
"hatchingPotionEmber": "Baga",
|
||||
"hatchingPotionThunderstorm": "Bagyó",
|
||||
"hatchingPotionGhost": "Multó",
|
||||
"hatchingPotionRoyalPurple": "Maharlikáng Ube",
|
||||
"hatchingPotionHolly": "Asebo",
|
||||
"hatchingPotionCupid": "Kúpidó",
|
||||
"hatchingPotionShimmer": "Makináng",
|
||||
"hatchingPotionFairy": "Maladiwatà",
|
||||
"hatchingPotionStarryNight": "Mabituin na Gabí",
|
||||
"hatchingPotionRainbow": "Bahaghari",
|
||||
"hatchingPotionGlass": "Kristál",
|
||||
"hatchingPotionGlow": "Naglíliwanag-sa-Dilím",
|
||||
"hatchingPotionFrost": "Tigás-Lamíg",
|
||||
"hatchingPotionIcySnow": "Mayelong Niyebe",
|
||||
"hatchingPotionNotes": "Ibuhos mo itó sa isáng itlog, at mapípisâ itó bilang isang <%= potText(locale) %> na alagà.",
|
||||
"premiumPotionAddlNotes": "Not usable on quest pet eggs.",
|
||||
"foodMeat": "Meat",
|
||||
"foodMeatThe": "the Meat",
|
||||
@@ -303,9 +303,9 @@
|
||||
"foodCandyRed": "Cinnamon Candy",
|
||||
"foodCandyRedThe": "the Cinnamon Candy",
|
||||
"foodCandyRedA": "Cinnamon Candy",
|
||||
"foodSaddleText": "Saddle",
|
||||
"foodSaddleNotes": "Instantly raises one of your pets into a mount.",
|
||||
"foodSaddleSellWarningNote": "Hey! This is a pretty useful item! Are you familiar with how to use a Saddle with your Pets?",
|
||||
"foodSaddleText": "Siya",
|
||||
"foodSaddleNotes": "Agád na pinapalakí ang isa sa iyong mga alaga.",
|
||||
"foodSaddleSellWarningNote": "Uy! Medyo kapaki-pakinabang! Pamilyar ka ba kung papaano gumamit ng Siya sa iyong mga Alagà?",
|
||||
"foodNotes": "Feed this to a pet and it may grow into a sturdy steed.",
|
||||
"foodPieRedA": "isang hiwa ng Pulang Cherry Pie",
|
||||
"foodPieRedThe": "ang Pulang Cherry Pie",
|
||||
@@ -338,18 +338,18 @@
|
||||
"foodPieSkeletonThe": "ang Bone Marrow Pot Pie",
|
||||
"foodPieSkeleton": "Bone Marrow Pot Pie",
|
||||
"premiumPotionUnlimitedNotes": "Hindi magagamit sa quest pet eggs.",
|
||||
"hatchingPotionMossyStone": "Malumot na Bato",
|
||||
"hatchingPotionMossyStone": "Malumot na Bató",
|
||||
"hatchingPotionPolkaDot": "Polka Dot",
|
||||
"hatchingPotionStainedGlass": "Stained Glass",
|
||||
"hatchingPotionBlackPearl": "Itim na Perlas",
|
||||
"hatchingPotionAutumnLeaf": "Dahon ng Taglagas",
|
||||
"hatchingPotionVampire": "Bampira",
|
||||
"hatchingPotionBlackPearl": "Itím na Perlas",
|
||||
"hatchingPotionAutumnLeaf": "Dahon ng Taglagás",
|
||||
"hatchingPotionVampire": "Danag",
|
||||
"hatchingPotionTurquoise": "Turkesa",
|
||||
"hatchingPotionWindup": "Wind-Up",
|
||||
"hatchingPotionWindup": "Susián",
|
||||
"hatchingPotionSandSculpture": "Iskulturang Buhangin",
|
||||
"hatchingPotionFluorite": "Fluorite",
|
||||
"hatchingPotionDessert": "Confection",
|
||||
"hatchingPotionBirchBark": "Birch Bark",
|
||||
"hatchingPotionFluorite": "Fluorita",
|
||||
"hatchingPotionDessert": "Minatamís",
|
||||
"hatchingPotionBirchBark": "Balakbák ng Birch",
|
||||
"hatchingPotionRuby": "Rubi",
|
||||
"hatchingPotionAurora": "Aurora",
|
||||
"hatchingPotionAmber": "Amber",
|
||||
@@ -358,14 +358,15 @@
|
||||
"hatchingPotionWatery": "Matubig",
|
||||
"hatchingPotionBronze": "Tanso",
|
||||
"hatchingPotionSunshine": "Sikat ng Araw",
|
||||
"hatchingPotionVeggie": "Hardin",
|
||||
"hatchingPotionCelestial": "Celestial",
|
||||
"hatchingPotionRoseQuartz": "Rosas na Kuwarts",
|
||||
"hatchingPotionVeggie": "Hálamanán",
|
||||
"hatchingPotionCelestial": "Panlangit",
|
||||
"hatchingPotionRoseQuartz": "Rosas na Kwarts",
|
||||
"questEggRobotAdjective": "isang futuristic",
|
||||
"questEggRobotMountText": "Robot",
|
||||
"questEggRobotText": "Robot",
|
||||
"questEggDolphinAdjective": "isang chipper",
|
||||
"questEggDolphinMountText": "Dolphin",
|
||||
"questEggDolphinText": "Dolphin",
|
||||
"hatchingPotionSunset": "Paglubog ng Araw"
|
||||
"hatchingPotionSunset": "Paglubóg ng Araw",
|
||||
"hatchingPotionOnyx": "Onix"
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"pets": "Mga Alaga",
|
||||
"pets": "Mga Alagà",
|
||||
"stable": "Kuwadra",
|
||||
"magicMounts": "Mga Kinabalang Sakay-Alaga",
|
||||
"questMounts": "Lakbayang Sakay-Alaga",
|
||||
"mountsTamed": "Mga Naamong Sakay-alaga",
|
||||
"noActiveMount": "Walang Sinasakyang Alaga",
|
||||
"activeMount": "Sinasakyang Alaga",
|
||||
"mounts": "Mga Sakay Alaga",
|
||||
"questPets": "Mga Alaga mula sa Quest",
|
||||
"questMounts": "Lakbayang Sakáy-Alaga",
|
||||
"mountsTamed": "Mga Naamong Sakáy-Alagà",
|
||||
"noActiveMount": "Waláng Sinásakyang Alagà",
|
||||
"activeMount": "Sinásakyang Alagà",
|
||||
"mounts": "Mga Sakáy-Alagà",
|
||||
"questPets": "Mga Alagang Hangò sa Pakikipagsápalarán",
|
||||
"magicPets": "Mga Alagang Kinabalan",
|
||||
"petsFound": "Mga Alagang Natagpuan",
|
||||
"noActivePet": "Walang Aktibong Alaga",
|
||||
"activePet": "Aktibong Alaga",
|
||||
"petsFound": "Mga Alagang Natagpuán",
|
||||
"noActivePet": "Waláng Aktibong Alagà",
|
||||
"activePet": "Aktibong Alagà",
|
||||
"food": "Pagkaing Pang-Alaga at Saddles",
|
||||
"quickInventory": "Mabilisang Imbentaryo",
|
||||
"haveHatchablePet": "Mayroon kang <%= potion %> hatching potion at <%= egg %> itlog upang ma-hatch ang alagang ito! <b>Pindutin</b> upang ma-hatch!",
|
||||
@@ -28,11 +28,11 @@
|
||||
"hopefulHippogriffMount": "Umaasang Hippogriff",
|
||||
"hopefulHippogriffPet": "Umaasang Hippogriff",
|
||||
"magicalBee": "Mahiwagang Bubuyog",
|
||||
"phoenix": "Phoenix",
|
||||
"royalPurpleGryphon": "Royal Purple Gryphon",
|
||||
"phoenix": "Fenix",
|
||||
"royalPurpleGryphon": "Kulay Ubeng Maharliká na Griffin",
|
||||
"orca": "Orca",
|
||||
"mammoth": "Mabalahibong Mammoth",
|
||||
"mantisShrimp": "Tatampal",
|
||||
"mantisShrimp": "Tatampál",
|
||||
"hydra": "Hydra",
|
||||
"cerberusPup": "Tutang Cerberus",
|
||||
"veteranFox": "Beteranong Soro",
|
||||
@@ -41,12 +41,12 @@
|
||||
"veteranTiger": "Beteranong Tigre",
|
||||
"veteranWolf": "Beteranong Lobo",
|
||||
"etherealLion": "Ethereal na Leon",
|
||||
"wackyPets": "Mga Wacky na Alaga",
|
||||
"wackyPets": "Mga Alagang Katawá-tawá",
|
||||
"beastMasterText2": " at pinakawalan ang kanilang mga alaga ng <%= count %> beses",
|
||||
"beastMasterText": "Nahanap ang lahat ng 90 na alaga (napakahirap, batiin ang user na ito!)",
|
||||
"beastMasterName": "Beast Master",
|
||||
"beastMasterName": "Amo ng mga Halimaw",
|
||||
"beastAchievement": "Nakamit mo ang \"Beast Master\" na Achievement sa pagkolekta ng lahat ng mga alaga!",
|
||||
"beastMasterProgress": "Beast Master Progress",
|
||||
"beastMasterProgress": "Ulat sa Pagiging Amo ng mga Halimaw",
|
||||
"premiumPotionNoDropExplanation": "Ang mga Mahiwagang Hatching Potion ay hindi magagamit sa mga itlog na nakuha mula sa mga Quest. Ang nag-iisang paraan upang makakuha ng mga Mahiwagang Hatching Potion ay sa pagbili nito sa ibaba, hindi mula sa random drops.",
|
||||
"dropsExplanationEggs": "Magwaldas ng mga Hiyas upang mas mabilis na makakuha ng mga itlog, kung ayaw mong hintaying ma-drop ang mga standard na itlog, o ulitin ang mga Quest upang makakuha ng Quest eggs. <a href=\"http://habitica.fandom.com/wiki/Drops\">Matuto pa tungkol sa drop system.</a>",
|
||||
"dropsExplanation": "Mas mabilis na makakuha ng ganitong mga gamit sa pamamagitan ng mga Hiyas kung ayaw mong hintayin ang pag-drop pagtapos ng kada gawain. <a href=\"http://habitica.fandom.com/wiki/Drops\">Matuto pa tungkol sa drop system.</a>",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"quests": "Quests",
|
||||
"quests": "Pakikipagsápalarán",
|
||||
"quest": "quest",
|
||||
"petQuests": "Pang-alaga at Mount Quests",
|
||||
"unlockableQuests": "Unlockable Quests",
|
||||
|
||||
@@ -17,33 +17,33 @@
|
||||
"yellowred": "Weak",
|
||||
"greenblue": "Strong",
|
||||
"edit": "Edit",
|
||||
"save": "Save",
|
||||
"addChecklist": "Add Checklist",
|
||||
"checklist": "Checklist",
|
||||
"newChecklistItem": "New checklist item",
|
||||
"expandChecklist": "Expand Checklist",
|
||||
"collapseChecklist": "Collapse Checklist",
|
||||
"text": "Title",
|
||||
"save": "Iimbák",
|
||||
"addChecklist": "Magdagdág ng Talaán ng mga Hakbáng",
|
||||
"checklist": "Talaán ng mga Hakbáng",
|
||||
"newChecklistItem": "Bagong Talâ",
|
||||
"expandChecklist": "Ibukás ang Talaán ng mga Hakbáng",
|
||||
"collapseChecklist": "Itiklóp ang Talaán ng mga Hakbáng",
|
||||
"text": "Pamagát",
|
||||
"notes": "Notes",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"difficulty": "Difficulty",
|
||||
"difficulty": "Gaano Kahirap",
|
||||
"difficultyHelp": "Inilalarawan ng pagkahirap kung ano ang antas ng isang Gawi, Daily, o To Do upang makumpleto mo. Ang mas mataas na pagkahirap ay nagreresulta ng mas malaking gantimpala pagkatapos ng isang Gawain, pero mas malaki rin ang pagbawas sa buhay tuwing may nalalampasang Daily o may masamang Gawi na napipindot.",
|
||||
"trivial": "Trivial",
|
||||
"easy": "Easy",
|
||||
"trivial": "Napakasisiw",
|
||||
"easy": "Madalíng Isagawâ",
|
||||
"medium": "Medium",
|
||||
"hard": "Hard",
|
||||
"hard": "Mahirap Isagawâ",
|
||||
"attributes": "Stats",
|
||||
"progress": "Progress",
|
||||
"daily": "Daily",
|
||||
"dailies": "Dailies",
|
||||
"dailysDesc": "Dailies repeat on a regular basis. Choose the schedule that works best for you!",
|
||||
"daily": "Pang-Araw-Araw",
|
||||
"dailies": "Mga Pang-Araw-Araw",
|
||||
"dailysDesc": "Madalás umuulit ang mga Pang-Araw-Araw. Piliin ang talatakdaán na pinakamainam para sa iyo!",
|
||||
"streakCounter": "Streak Counter",
|
||||
"repeat": "Repeat",
|
||||
"repeats": "Repeats",
|
||||
"repeatEvery": "Repeat Every",
|
||||
"repeatOn": "Repeat On",
|
||||
"repeat": "Ulitin",
|
||||
"repeats": "Inuulit",
|
||||
"repeatEvery": "Ulitin Bawat",
|
||||
"repeatOn": "Ulitin Sa",
|
||||
"day": "Day",
|
||||
"days": "Days",
|
||||
"days": "Mga Araw",
|
||||
"restoreStreak": "Adjust Streak",
|
||||
"resetStreak": "Reset Streak",
|
||||
"todo": "To Do",
|
||||
@@ -51,23 +51,23 @@
|
||||
"todosDesc": "Kailangang kumpletuhin nang isang beses ang mga To Do. Magdagdag ng mga listahan sa iyong mga To Do upang tumaas ang kanilang halaga.",
|
||||
"dueDate": "Due Date",
|
||||
"remaining": "Active",
|
||||
"complete": "Done",
|
||||
"complete": "Natapos",
|
||||
"complete2": "Complete",
|
||||
"today": "Today",
|
||||
"today": "Ngayóng Araw na Itó",
|
||||
"dueIn": "Due <%= dueIn %>",
|
||||
"due": "Due",
|
||||
"notDue": "Not Due",
|
||||
"grey": "Grey",
|
||||
"grey": "Kulay Abó",
|
||||
"score": "Score",
|
||||
"reward": "Reward",
|
||||
"rewards": "Rewards",
|
||||
"reward": "Pabuyà",
|
||||
"rewards": "Mga Pabuyà",
|
||||
"rewardsDesc": "Rewards are a great way to use Habitica and complete your tasks. Try adding a few today!",
|
||||
"gold": "Gold",
|
||||
"silver": "Silver (100 silver = 1 gold)",
|
||||
"price": "Price",
|
||||
"price": "Halagá",
|
||||
"tags": "Tags",
|
||||
"editTags": "Edit",
|
||||
"newTag": "New Tag",
|
||||
"newTag": "Bagong Taták",
|
||||
"editTags2": "Edit Tags",
|
||||
"toRequired": "You must supply a \"to\" property",
|
||||
"startDate": "Start Date",
|
||||
@@ -76,58 +76,58 @@
|
||||
"streakText": "Has performed <%= count %> 21-day streaks on Dailies",
|
||||
"streakSingular": "Streaker",
|
||||
"streakSingularText": "Has performed a 21-day streak on a Daily",
|
||||
"perfectName": "<%= count %> Perfect Days",
|
||||
"perfectName": "Mga Pagpayag",
|
||||
"perfectText": "Completed all active Dailies on <%= count %> days. With this achievement you get a +level/2 buff to all Stats for the next day. Levels greater than 100 don't have any additional effects on buffs.",
|
||||
"perfectSingular": "Perfect Day",
|
||||
"perfectSingular": "Waláng Kápaltós-Paltós na Araw",
|
||||
"perfectSingularText": "Completed all active Dailies in one day. With this achievement you get a +level/2 buff to all Stats for the next day. Levels greater than 100 don't have any additional effects on buffs.",
|
||||
"fortifyName": "Fortify Potion",
|
||||
"fortifyPop": "Return all tasks to neutral value (yellow color), and restore all lost Health.",
|
||||
"fortify": "Fortify",
|
||||
"fortifyPop": "Ibalík ang lahat ng gawain sa gitnáng halaga (kulay diláw), at ibalik ang lahat ng nawawalang Kalusugan.",
|
||||
"fortify": "Pagtibayin",
|
||||
"fortifyComplete": "Fortify complete!",
|
||||
"deleteTask": "Delete this Task",
|
||||
"sureDelete": "Are you sure you want to delete this task?",
|
||||
"streakCoins": "Streak Bonus!",
|
||||
"taskToTop": "To top",
|
||||
"taskToBottom": "To bottom",
|
||||
"taskToBottom": "Ipailalim",
|
||||
"taskAliasAlreadyUsed": "Task alias already used on another task.",
|
||||
"taskNotFound": "Task not found.",
|
||||
"invalidTaskType": "Task type must be one of \"habit\", \"daily\", \"todo\", \"reward\".",
|
||||
"invalidTasksType": "Task type must be one of \"habits\", \"dailys\", \"todos\", \"rewards\".",
|
||||
"invalidTasksTypeExtra": "Task type must be one of \"habits\", \"dailys\", \"todos\", \"rewards\", \"completedTodos\".",
|
||||
"cantDeleteChallengeTasks": "A task belonging to a challenge can't be deleted.",
|
||||
"cantDeleteChallengeTasks": "Hindi maaaring ilipat ang iisáng gawaing pagmamay-arì ng isang hamon.",
|
||||
"checklistOnlyDailyTodo": "Suportado lamang ang mga listahan sa Dailies at mga To Do",
|
||||
"checklistItemNotFound": "No checklist item was found with given id.",
|
||||
"itemIdRequired": "\"itemId\" must be a valid UUID.",
|
||||
"tagNotFound": "No tag item was found with given id.",
|
||||
"tagIdRequired": "\"tagId\" must be a valid UUID corresponding to a tag belonging to the user.",
|
||||
"tagIdRequired": "Ang \"tagId\" ay dapat wastó na UUID na buhat sa taták na pagmamay-ari ng tagagamit.",
|
||||
"positionRequired": "\"position\" is required and must be a number.",
|
||||
"cantMoveCompletedTodo": "Can't move a completed todo.",
|
||||
"cantMoveCompletedTodo": "Hindi maaaring ilipat ang mga natapos na na dapat gawín.",
|
||||
"directionUpDown": "\"direction\" is required and must be 'up' or 'down'.",
|
||||
"alreadyTagged": "The task is already tagged with given tag.",
|
||||
"taskRequiresApproval": "This task must be approved before you can complete it. Approval has already been requested",
|
||||
"taskApprovalHasBeenRequested": "Approval has been requested",
|
||||
"taskApprovalWasNotRequested": "Only a task waiting for approval can be marked as needing more work",
|
||||
"approvals": "Approvals",
|
||||
"approvalRequired": "Needs Approval",
|
||||
"weekly": "Weekly",
|
||||
"monthly": "Monthly",
|
||||
"yearly": "Yearly",
|
||||
"summary": "Summary",
|
||||
"repeatsOn": "Repeats On",
|
||||
"dayOfWeek": "Day of the Week",
|
||||
"dayOfMonth": "Day of the Month",
|
||||
"month": "Month",
|
||||
"months": "Months",
|
||||
"week": "Week",
|
||||
"weeks": "Weeks",
|
||||
"year": "Year",
|
||||
"years": "Years",
|
||||
"resets": "Resets",
|
||||
"nextDue": "Next Due Dates",
|
||||
"checkOffYesterDailies": "Check off any Dailies you did yesterday:",
|
||||
"yesterDailiesCallToAction": "Start My New Day!",
|
||||
"sessionOutdated": "Your session is outdated. Please refresh or sync.",
|
||||
"errorTemporaryItem": "This item is temporary and cannot be pinned.",
|
||||
"alreadyTagged": "Nabigyán na ng urì na ang gawain gamit ang ibinigay na taták.",
|
||||
"taskRequiresApproval": "Dapat mapayagan muna ang gawaing itó bago mo itó matapos. Ipinakiusap na ang pagpapapayag",
|
||||
"taskApprovalHasBeenRequested": "Ipinakiusap ang pagpapapayag",
|
||||
"taskApprovalWasNotRequested": "Hindí ipinakiusap ang pagpapapayag ukol sa gawaing itó.",
|
||||
"approvals": "Mga Pagpayag",
|
||||
"approvalRequired": "Nangangailangan ng Pagpayag",
|
||||
"weekly": "Linggó-Linggó",
|
||||
"monthly": "Buwán-Buwán",
|
||||
"yearly": "Taón-Taón",
|
||||
"summary": "Buód",
|
||||
"repeatsOn": "Inuulit Tuwíng",
|
||||
"dayOfWeek": "Araw ng Linggó",
|
||||
"dayOfMonth": "Araw ng Buwán",
|
||||
"month": "Buwán",
|
||||
"months": "Mga Buwán",
|
||||
"week": "Linggó",
|
||||
"weeks": "Mga Linggó",
|
||||
"year": "Taón",
|
||||
"years": "Mga Taón",
|
||||
"resets": "Mga Pagsasaayos Mulí",
|
||||
"nextDue": "Mga Susunod na Nakatakdáng Petsa",
|
||||
"checkOffYesterDailies": "Markahán ang ano mang Pang-Araw-Araw na ginawá mo kahapon:",
|
||||
"yesterDailiesCallToAction": "Simulán ang Bagong Araw Ko!",
|
||||
"sessionOutdated": "Lumà na ang iyóng <i>session</i>. Mangyari lamang na mag-<i>refresh</i> mag-<i>sync</i>.",
|
||||
"errorTemporaryItem": "Pánsamantalá lamang ang gamit na itó at hindi maaaring idikít.",
|
||||
"pressEnterToAddTag": "Pindutin ang Enter upang magdagdag ng tag: '<%= tagName %>'",
|
||||
"enterTag": "Maglagay ng tag",
|
||||
"addTags": "Magdagdag ng tags...",
|
||||
|
||||
@@ -685,5 +685,13 @@
|
||||
"backgroundFloweringPrairieText": "Prairie fleurie",
|
||||
"backgroundFloweringPrairieNotes": "Gambadez dans une prairie fleurie.",
|
||||
"backgroundBrickWallWithIvyText": "Mur de brique avec Lierre",
|
||||
"backgroundAnimalsDenText": "Tanière de créature des bois"
|
||||
"backgroundAnimalsDenText": "Tanière de créature des bois",
|
||||
"backgrounds042022": "Ensemble 95 : sorti en avril 2022",
|
||||
"backgroundBlossomingTreesText": "Arbres en fleurs",
|
||||
"hideLockedBackgrounds": "Cacher les arrière-plans verrouillés",
|
||||
"backgroundBlossomingTreesNotes": "Promenez-vous au milieu des arbres en fleurs.",
|
||||
"backgroundFlowerShopText": "Boutique de fleurs",
|
||||
"backgroundSpringtimeLakeNotes": "Profitez de la vue sur les rives d'un lac printanier.",
|
||||
"backgroundFlowerShopNotes": "Profitez de la douce senteur d'une boutique de fleurs.",
|
||||
"backgroundSpringtimeLakeText": "Lac printanier"
|
||||
}
|
||||
|
||||
@@ -2614,5 +2614,9 @@
|
||||
"eyewearMystery202204BText": "Visage virtuel",
|
||||
"eyewearMystery202204BNotes": "Quelle est votre humeur du jour ? Exprimez vous avec ces écrans amusants. Ne confère aucun bonus. Équipement d'abonnement d'avril 2022.",
|
||||
"eyewearMystery202204AText": "Visage virtuel",
|
||||
"eyewearMystery202204ANotes": "Quelle est votre humeur du jour ? Exprimez vous avec ces écrans amusants. Ne confère aucun bonus. Équipement d'abonnement d'avril 2022."
|
||||
"eyewearMystery202204ANotes": "Quelle est votre humeur du jour ? Exprimez vous avec ces écrans amusants. Ne confère aucun bonus. Équipement d'abonnement d'avril 2022.",
|
||||
"armorArmoireStrawRaincoatText": "Ciret de paille",
|
||||
"armorArmoireStrawRaincoatNotes": "Cette cape en paille tressée vous gardera au sec et évitera à votre armure de rouiller pendant les quêtes. Mais ne vous aventurez pas trop près des bougies ! Augmente la constitution de <%= con %>. Armoire enchantée : ensemble de pluie en paille (objet 1 de 2).",
|
||||
"headArmoireStrawRainHatText": "Chapeau de pluie en paille",
|
||||
"headArmoireStrawRainHatNotes": "Vous pourrez détecter tous les obstacles sur votre chemin en portant ce chapeau imperméable et canonique. Augmente la perception de <%= per %>. Armoire enchantée : ensemble de pluie en paille (objet 2 de 2)."
|
||||
}
|
||||
|
||||
@@ -220,5 +220,6 @@
|
||||
"spring2022MagpieRogueSet": "Pie (Voleur)",
|
||||
"spring2022RainstormWarriorSet": "Flot diluvien (Guerrier)",
|
||||
"spring2022ForsythiaMageSet": "Forsythia (Mage)",
|
||||
"spring2022PeridotHealerSet": "Péridot (Guérisseur)"
|
||||
"spring2022PeridotHealerSet": "Péridot (Guérisseur)",
|
||||
"aprilYYYY": "Avril <%= year %>"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user