diff --git a/README.md b/README.md index 6c21d56..6a6c88f 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,5 @@ * src/fabula-points.svg: https://game-icons.net/1x1/lorc/star-swirl.html * src/ultima-points.svg: https://game-icons.net/1x1/lorc/evil-moon.html * src/default-portrait.svg: https://pixabay.com/vectors/woman-profile-silhouette-people-5786062/ -* src/default-background.jpg: https://www.wallpaperflare.com/dark-insubstantial-spotlight-art-lighting-equipment-no-people-wallpaper-geayr/download \ No newline at end of file +* src/default-background.jpg: https://www.wallpaperflare.com/dark-insubstantial-spotlight-art-lighting-equipment-no-people-wallpaper-geayr/download +* src/blood-points.svg: https://game-icons.net/1x1/lorc/rose.html \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5dd69d9..c2a3d44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,9 +18,13 @@ "@types/react-dom": "^18.0.11", "bootstrap": "^5.2.3", "csstype": "^3.1.2", + "format-duration": "^3.0.2", "react": "^18.2.0", "react-bootstrap": "^2.7.2", "react-dom": "^18.2.0", + "react-helmet": "^6.1.0", + "react-markdown": "^8.0.7", + "react-minimal-pie-chart": "^8.4.0", "react-scripts": "5.0.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4" @@ -3929,6 +3933,14 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz", + "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/eslint": { "version": "8.37.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", @@ -3981,6 +3993,14 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", + "integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -4034,11 +4054,24 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/mdast": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.11.tgz", + "integrity": "sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==" }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, "node_modules/@types/node": { "version": "16.18.23", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.23.tgz", @@ -4153,6 +4186,11 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==" }, + "node_modules/@types/svg-path-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/svg-path-parser/-/svg-path-parser-1.1.3.tgz", + "integrity": "sha512-F1Y6lQIto5b2sKCseVUsFfY5J+8PIhhX4jrDVxpth4m7hwM2OdySh3iTLeR35lEhl/K4ZMEF+GDAwTl7yJcO5Q==" + }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.5", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz", @@ -4166,6 +4204,11 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==" }, + "node_modules/@types/unist": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", + "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" + }, "node_modules/@types/warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", @@ -5269,6 +5312,15 @@ "babel-plugin-transform-react-remove-prop-types": "^0.4.24" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5594,6 +5646,15 @@ "node": ">=10" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/check-types": { "version": "11.2.2", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.2.tgz", @@ -5758,6 +5819,15 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -6386,6 +6456,18 @@ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -6545,6 +6627,14 @@ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, + "node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", @@ -7805,6 +7895,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -8223,6 +8318,11 @@ "node": ">= 6" } }, + "node_modules/format-duration": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/format-duration/-/format-duration-3.0.2.tgz", + "integrity": "sha512-pKzJDSRgK2lqAiPW3uizDaIJaJnataZclsahz25UMwfdryBGDa+1HlbXGjzpMvX/2kMh4O0sNevFXKaEfCjHsA==" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -8615,6 +8715,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -8980,6 +9089,11 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" + }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -9079,6 +9193,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -11829,6 +11965,74 @@ "tmpl": "1.0.5" } }, + "node_modules/mdast-util-definitions": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", + "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", + "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-definitions": "^5.0.0", + "micromark-util-sanitize-uri": "^1.1.0", + "trim-lines": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -11879,6 +12083,427 @@ "node": ">= 0.6" } }, + "node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -12039,6 +12664,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -13991,6 +14624,15 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/property-information": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz", + "integrity": "sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -14329,6 +14971,25 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -14339,6 +15000,53 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-markdown": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz", + "integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/prop-types": "^15.0.0", + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "prop-types": "^15.0.0", + "property-information": "^6.0.0", + "react-is": "^18.0.0", + "remark-parse": "^10.0.0", + "remark-rehype": "^10.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.4.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/react-markdown/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/react-minimal-pie-chart": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/react-minimal-pie-chart/-/react-minimal-pie-chart-8.4.0.tgz", + "integrity": "sha512-A0wG+6mRjboyMxMDrzQNWp+2+GSn2ai4ERzRFHLp/OCC45PwIR1DpDVjwedawO+5AtFpzBRQKSFm3ZUxrqIEzA==", + "dependencies": { + "@types/svg-path-parser": "^1.1.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18", + "react-dom": "^16.8.0 || ^17.0.0 || ^18" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -14419,6 +15127,14 @@ } } }, + "node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -14582,6 +15298,35 @@ "node": ">= 0.10" } }, + "node_modules/remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", + "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-to-hast": "^12.1.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -14836,6 +15581,17 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -15272,6 +16028,15 @@ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "deprecated": "Please use @jridgewell/sourcemap-codec instead" }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", @@ -15542,6 +16307,14 @@ "webpack": "^5.0.0" } }, + "node_modules/style-to-object": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.1.tgz", + "integrity": "sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, "node_modules/stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -16019,6 +16792,24 @@ "node": ">=8" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", + "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/tryer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", @@ -16222,6 +17013,35 @@ "node": ">=4" } }, + "node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -16233,6 +17053,78 @@ "node": ">=8" } }, + "node_modules/unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -16345,6 +17237,31 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uvu/node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", @@ -16366,6 +17283,34 @@ "node": ">= 0.8" } }, + "node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/package.json b/package.json index a3fab57..60e80a4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,6 @@ "name": "fabula-ultima-react", "version": "0.1.0", "private": true, - "main": "src/index.tsx", "dependencies": { "@react-spring/web": "^9.7.2", "@testing-library/jest-dom": "^5.16.5", @@ -14,9 +13,13 @@ "@types/react-dom": "^18.0.11", "bootstrap": "^5.2.3", "csstype": "^3.1.2", + "format-duration": "^3.0.2", "react": "^18.2.0", "react-bootstrap": "^2.7.2", "react-dom": "^18.2.0", + "react-helmet": "^6.1.0", + "react-markdown": "^8.0.7", + "react-minimal-pie-chart": "^8.4.0", "react-scripts": "5.0.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4" diff --git a/public/api/current.json b/public/api/current.json index f8bab27..b1e44a9 100644 --- a/public/api/current.json +++ b/public/api/current.json @@ -1,21 +1,20 @@ { "session": { - "fpUsed": 0, - "upUsed": 0 + "usedSp": {"Fabula": 5, "Ultima": 2} }, "conflict": { "round": 0, - "activeSideIsAllies": true, + "activeSide": "ally", "activeCharacterId": null }, "clocks": [], "characters": [ { "id": "aelica", - "ally": true, + "side": "ally", "name": "Aelica", "level": 32, - "hp": 0, + "hp": 70, "maxHp": 82, "mp": 77, "maxMp": 77, @@ -23,6 +22,10 @@ "maxIp": 6, "sp": 3, "spBank": 2, + "bp": 2, + "maxBp": 4, + "zp": 4, + "maxZp": 6, "spType": "Fabula", "portraitUrl": "/portraits/aelica.png", "turnsTotal": 1, @@ -30,15 +33,17 @@ }, { "id": "athetyz", - "ally": true, + "side": "ally", "name": "Athetyz", "level": 32, - "hp": 0, + "hp": 63, "maxHp": 63, - "mp": 45, + "mp": 55, "maxMp": 55, "ip": 9, "maxIp": 10, + "zp": 3, + "maxZp": 6, "sp": 3, "spBank": 1, "spType": "Fabula", @@ -48,15 +53,17 @@ }, { "id": "echo", - "ally": true, + "side": "ally", "name": "Echo", "level": 32, - "hp": 0, + "hp": 67, "maxHp": 67, "mp": 62, "maxMp": 62, "ip": 4, "maxIp": 12, + "zp": 5, + "maxZp": 6, "sp": 3, "spBank": 4, "spType": "Fabula", @@ -66,33 +73,37 @@ }, { "id": "gravitas", - "ally": true, + "side": "ally", "name": "Gravitas", "level": 32, - "hp": 0, + "hp": 72, "maxHp": 72, "mp": 43, "maxMp": 117, "ip": 5, "maxIp": 8, + "zp": 0, + "maxZp": 6, "sp": 3, "spBank": 6, "spType": "Fabula", "portraitUrl": "/portraits/gravitas.png", "turnsTotal": 1, - "turnsLeft": 0 + "turnsLeft": 1 }, { "id": "linnet", - "ally": true, + "side": "ally", "name": "Linnet", "level": 32, - "hp": 0, + "hp": 117, "maxHp": 117, "mp": 73, "maxMp": 76, "ip": 6, "maxIp": 6, + "zp": 1, + "maxZp": 6, "sp": 3, "spBank": 4, "spType": "Fabula", @@ -102,21 +113,95 @@ }, { "id": "prandia", - "ally": true, + "side": "ally", "name": "Prandia", "level": 32, - "hp": 0, + "hp": 90, "maxHp": 90, + "koText": "rip lmao", "mp": 0, "maxMp": 70, "ip": 8, "maxIp": 8, + "zp": 2, + "maxZp": 6, "sp": 3, "spBank": 4, "spType": "Fabula", "portraitUrl": "/portraits/prandia.png", "turnsTotal": 1, - "turnsLeft": 0 + "turnsLeft": 1, + "statuses": [ + { + "id": "digested", + "name": "Digested", + "count": 3, + "description": "Is currently being melted down for sustenance." + }, + { + "id": "digested2", + "name": "Digested", + "count": 3, + "description": "Is currently being melted down for sustenance." + }, + { + "id": "digested3", + "name": "Digested", + "count": 3, + "description": "Is currently being melted down for sustenance." + }, + { + "id": "digested4", + "name": "Digested", + "count": 3, + "description": "Is currently being melted down for sustenance." + }, + { + "id": "digested5", + "name": "Digested", + "count": 3, + "description": "Is currently being melted down for sustenance." + }, + { + "id": "digested6", + "name": "Digested", + "count": 3, + "description": "Is currently being melted down for sustenance." + }, + { + "id": "digested7", + "name": "Digested", + "count": 3, + "description": "Is currently being melted down for sustenance." + }, + { + "id": "digested8", + "name": "Digested", + "count": 3, + "description": "Is currently being melted down for sustenance." + }, + { + "id": "digested9", + "name": "Digested", + "count": 3, + "description": "Is currently being melted down for sustenance." + }, + { + "id": "digested10", + "name": "Digested", + "count": 3, + "description": "Is currently being melted down for sustenance." + }] + }, + { + "id": "werespider", + "side": "enemy", + "name": "Werespider", + "health": "KO", + "spType": "Ultima", + "sp": 5, + "turnsLeft": 3, + "turnsTotal": 3 } ] } \ No newline at end of file diff --git a/src/model/Character.ts b/src/model/Character.ts index f22dd7f..12e8199 100644 --- a/src/model/Character.ts +++ b/src/model/Character.ts @@ -179,20 +179,28 @@ export interface StatusEffect { readonly id: StatusId readonly name: string readonly count?: number - readonly iconUrl: string + readonly iconUrl?: string readonly description?: string } export type CharacterId = string +export enum CharacterSide { + Ally = "ally", + Enemy = "enemy" +} + export interface Character { readonly id: CharacterId + readonly side: CharacterSide readonly portraitUrl?: string readonly name?: string + readonly altName?: string readonly level?: number readonly hp?: number readonly maxHp?: number readonly health?: CharacterHealth + readonly koText?: string readonly mp?: number readonly maxMp?: number readonly ip?: number @@ -200,19 +208,25 @@ export interface Character { readonly sp?: number readonly spBank?: number readonly spType?: SPType + readonly zp?: number + readonly maxZp?: number + readonly bp?: number + readonly maxBp?: number readonly turnsLeft?: number readonly turnsTotal?: number readonly canAct?: boolean readonly statuses?: readonly StatusEffect[] + readonly privacy?: CharacterPrivacy } -interface CharacterPrivacySettings { +interface CharacterPrivacySetting { readonly showCharacter: boolean readonly showHp: boolean readonly showHealth: boolean readonly showMp: boolean readonly showIp: boolean readonly showSp: boolean + readonly showZp: boolean readonly showName: boolean readonly showPortrait: boolean readonly showTurns: boolean @@ -222,6 +236,7 @@ interface CharacterPrivacySettings { export enum CharacterPrivacy { Friend = "friend", + FullyScannedEnemy = "fully scanned enemy", ScannedEnemy = "scanned enemy", LightlyScannedEnemy = "lightly scanned enemy", UnscannedEnemy = "unscanned enemy", @@ -237,19 +252,35 @@ export const CharacterPrivacySettings = { showMp: true, showIp: true, showSp: true, + showZp: true, showName: true, showPortrait: true, showTurns: true, showStatuses: true, showLevel: true, }, - [CharacterPrivacy.ScannedEnemy]: { + [CharacterPrivacy.FullyScannedEnemy]: { showCharacter: true, showHp: true, showHealth: true, showMp: true, showIp: false, showSp: true, + showZp: true, + showName: true, + showPortrait: true, + showTurns: true, + showStatuses: true, + showLevel: true, + }, + [CharacterPrivacy.ScannedEnemy]: { + showCharacter: true, + showHp: true, + showHealth: true, + showMp: false, + showIp: false, + showSp: true, + showZp: true, showName: true, showPortrait: true, showTurns: true, @@ -263,6 +294,7 @@ export const CharacterPrivacySettings = { showMp: false, showIp: false, showSp: true, + showZp: true, showName: true, showPortrait: true, showTurns: true, @@ -276,6 +308,7 @@ export const CharacterPrivacySettings = { showMp: false, showIp: false, showSp: true, + showZp: true, showName: true, showPortrait: true, showTurns: true, @@ -289,8 +322,9 @@ export const CharacterPrivacySettings = { showMp: false, showIp: false, showSp: false, + showZp: false, showName: false, - showPortrait: true, + showPortrait: false, showTurns: true, showStatuses: true, showLevel: false, @@ -302,20 +336,22 @@ export const CharacterPrivacySettings = { showMp: false, showIp: false, showSp: false, + showZp: false, showName: false, showPortrait: false, showTurns: false, showStatuses: false, showLevel: false, } -} as const satisfies {readonly [value in CharacterPrivacy]: CharacterPrivacySettings} +} as const satisfies {readonly [value in CharacterPrivacy]: CharacterPrivacySetting} -export function applyCharacterPrivacy(character: Character, privacy: CharacterPrivacy): Character|null { - const privacySettings = CharacterPrivacySettings[privacy ?? CharacterPrivacy.Hidden] +export function applyCharacterPrivacy(character: Character): Character|null { + const privacySettings = CharacterPrivacySettings[character.privacy ?? CharacterPrivacy.Hidden] if (!privacySettings.showCharacter) { return null } const out: {-readonly [Field in keyof Character]: Character[Field]} = Object.assign({}, character) + delete out.privacy if (!privacySettings.showHp) { delete out.hp delete out.maxHp @@ -336,8 +372,19 @@ export function applyCharacterPrivacy(character: Character, privacy: CharacterPr delete out.spBank delete out.spType } + if (!privacySettings.showZp) { + delete out.zp + delete out.maxZp + delete out.bp + delete out.maxBp + } if (!privacySettings.showName) { - delete out.name + if (isDefined(out.altName)) { + out.name = out.altName + delete out.altName + } else { + delete out.name + } } if (!privacySettings.showPortrait) { delete out.portraitUrl diff --git a/src/model/Doable.ts b/src/model/Doable.ts deleted file mode 100644 index 5373793..0000000 --- a/src/model/Doable.ts +++ /dev/null @@ -1,372 +0,0 @@ -import {GameState, getCharacterById} from "./GameState"; -import {CharacterId} from "./Character"; - -export interface BaseDoable { - readonly type: string -} - -export type LogEntry = { - readonly markdown: string - readonly children: readonly LogEntry[] -} - -export interface DoableResults { - readonly resultState: GameState - readonly logEntry: LogEntry|null -} - -export interface DoablesResults { - readonly resultState: GameState - readonly logEntries: readonly LogEntry[] -} - -export interface DoableEvaluator { - readonly type: DataType["type"] - evaluate(data: DataType, state: GameState, direction: DoableDirection): DoableResults -} - -export interface GenericAction extends BaseDoable { - readonly type: "generic", - readonly text: string - readonly user: CharacterId|null - readonly target: CharacterId|null - readonly effects: readonly Doable[] -} - -export const GenericActionEvaluator = { - type: "generic", - evaluate(data: GenericAction, state: GameState, direction: DoableDirection): DoableResults { - function runEffects(currentState: GameState): DoablesResults { - return evaluateDoables(data.effects, state, direction) - } - function logSelf(currentState: GameState): string { - return data.text.replaceAll(/@[TU]/g, (substring: string): string => { - switch (substring) { - case "@T": - // TODO: make "character links" a function, likely with identifier characters - return data.target !== null ? `[${getCharacterById(currentState, data.target)?.name ?? "???"}](#character/${data.target})` : "@T" - case "@U": - return data.user !== null ? `[${getCharacterById(currentState, data.user)?.name ?? "???"}](#character/${data.user})` : "@U" - default: - return substring - } - }) - } - switch (direction) { - case DoableDirection.Do: { - const markdown = logSelf(state) - const {resultState, logEntries} = runEffects(state) - return { - resultState, - logEntry: { - markdown, - children: logEntries - } - } - } - case DoableDirection.Undo: { - const {resultState, logEntries} = runEffects(state) - const markdown = logSelf(resultState) - return { - resultState, - logEntry: { - markdown, - children: logEntries - } - } - } - } - }, -} as const satisfies DoableEvaluator -/** - * @dataclass(**JsonableDataclassArgs) - * class AbilityAction(Doable): - * name: str - * user: Target | None - * costs: tuple[DamageEffect, ...] - * effects: tuple[Effect, ...] - * - * - * @dataclass(**JsonableDataclassArgs) - * class ModifyCharacterEffect(Doable): - * index: int - * old_character_data: Character | None - * new_character_data: Character | None - * - * def __post_init__(self): - * if self.old_character_data is None and self.new_character_data is None: - * raise ValueError("At least one of old_character_data or new_character_data must be non-None") - * - * @dataclass(**JsonableDataclassArgs) - * class ModifyClockEffect(Doable): - * index: int - * old_clock_data: Clock | None - * new_clock_data: Clock | None - * - * def __post_init__(self): - * if self.old_clock_data is None and self.new_clock_data is None: - * raise ValueError("At least one of old_clock_data or new_clock_data must be non-None") - * - * - * @dataclass(**JsonableDataclassArgs) - * class EndTurnAction(Doable): - * turn_ending_index: int - * activating_side: CombatSide - * - * - * @dataclass(**JsonableDataclassArgs) - * class StartTurnAction(Doable): - * turn_starting_index: int - * old_active_side: CombatSide - * - * - * @dataclass(**JsonableDataclassArgs) - * class StartRoundAction(Doable): - * last_round: int - * old_active_side: CombatSide - * old_turns_remaining: tuple[int, ...] - * next_round: int - * activating_side: CombatSide - * - * - * @dataclass(**JsonableDataclassArgs) - * class StartBattleAction(Doable): - * starting_side: CombatSide - * starting_round: int - * - * - * @dataclass(**JsonableDataclassArgs) - * class EndBattleAction(Doable): - * old_round_number: int - * old_active_side: CombatSide - * old_starting_side: CombatSide - * old_turns_remaining: tuple[int, ...] - * - * @dataclass(**JsonableDataclassArgs) - * class OpportunityEffect(LogOnly, Effect): - * target: Target | None - * opportunity_text: str - * - * def log_message(self, user: Target | None = None) -> str: - * return f'**Opportunity!!** {log_substitute(self.opportunity_text, user=user, target=self.target)}' - * - * @dataclass(**JsonableDataclassArgs) - * class DamageEffect(Effect): - * target: Target - * target_type: CharacterType - * attribute: Counter - * damage: int - * old_value: int - * new_value: int - * max_value: int | None - * element: Element - * affinity: Affinity - * piercing: bool - * - * def do(self, combat: "CombatStatus", source: Union[Action, "Effect", None] = None) -> "CombatStatus": - * return combat.alter_character(self.target, lambda c: c.set_counter_current(self.attribute, self.new_value)) - * - * def undo(self, combat: "CombatStatus", source: Union[Action, "Effect", None] = None) -> "CombatStatus": - * return combat.alter_character(self.target, lambda c: c.set_counter_current(self.attribute, self.old_value)) - * - * def log_message(self, user: Target | None = None) -> str: - * state_change = None - * if self.attribute == Counter.HP: - * if self.old_value > self.new_value == 0: - * state_change = ['+ **KO**'] - * elif self.new_value > self.old_value == 0: - * if self.new_value * 2 > self.max_value: - * state_change = ['- **KO**'] - * else: - * state_change = ['- **KO**', '+ **Crisis**'] - * elif self.old_value * 2 > self.max_value >= self.new_value * 2: - * state_change = '+ **Crisis**' - * elif self.new_value * 2 > self.max_value >= self.old_value * 2: - * state_change = '- **Crisis**' - * affinity = '' - * if self.affinity == Affinity.Absorb: - * affinity = "?" - * elif self.affinity == Affinity.Immune: - * affinity = " ✖" - * elif self.affinity == Affinity.Resistant: - * affinity = "…" - * elif self.affinity == Affinity.Vulnerable: - * affinity = "‼" - * attribute = (f'{self.element.value}{"!" if self.piercing else ""}' - * if self.attribute == Counter.HP - * else f'{self.attribute.ctr_name_abbr(self.target_type)}') - * sign = '-' if self.damage >= 0 else '+' - * return '\n'.join( - * [log_substitute( - * f'@T: [{sign}{abs(self.damage)}{affinity}] {attribute}', - * user=user, target=self.target)] + - * [log_substitute(f'@T: [{s}]') for s in state_change]) - * - * - * @dataclass(**JsonableDataclassArgs) - * class StatusEffect(Effect): - * target: Target - * old_status: str | None - * new_status: str | None - * - * @staticmethod - * def alter_status(c: Character, old_status: str | None, new_status: str | None) -> Character: - * if old_status is None and new_status is not None: - * return c.add_status(new_status) - * elif new_status is None and old_status is not None: - * return c.remove_status(old_status) - * elif new_status is not None and old_status is not None: - * return c.replace_status(old_status, new_status) - * else: - * return c - * - * def __post_init__(self): - * if self.old_status is None and self.new_status is None: - * raise ValueError("At least one of old_status or new_status must be non-None") - * - * def do(self, combat: CombatStatus, source: Union[Action, Effect, None] = None) -> CombatStatus: - * return combat.alter_character( - * self.target, lambda c: StatusEffect.alter_status(c, self.old_status, self.new_status)) - * - * def undo(self, combat: CombatStatus, source: Union[Action, Effect, None] = None) -> CombatStatus: - * return combat.alter_character( - * self.target, lambda c: StatusEffect.alter_status(c, self.new_status, self.old_status)) - * - * def log_message(self, user: Target | None = None) -> str | None: - * if self.old_status is not None and self.new_status is None: - * return log_substitute(v=f'@T: [- {self.old_status}]', user=user, target=self.target) - * if self.new_status is not None and self.old_status is None: - * return log_substitute(v=f'@T: [+ {self.old_status}]', user=user, target=self.target) - * if self.old_status is not None and self.new_status is not None: - * return log_substitute(v=f'@T: [{self.old_status} -> {self.new_status}]', user=user, target=self.target) - * pass - * - * - * @dataclass(**JsonableDataclassArgs) - * class FPBonusEffect(Effect): - * user: Target - * rerolls: int - * modifier: int - * fp_spent: int - * old_fp: int - * new_fp: int - * - * def do(self, combat: CombatStatus, source: Union[Action, Effect, None] = None) -> CombatStatus: - * return combat.alter_character( - * self.user, - * lambda c: c.set_counter_current(Counter.SP, self.new_fp) - * ).add_fp_spent(self.fp_spent) - * - * def undo(self, combat: CombatStatus, source: Union[Action, Effect, None] = None) -> CombatStatus: - * return combat.alter_character( - * self.user, - * lambda c: c.set_counter_current(Counter.SP, self.old_fp) - * ).add_fp_spent(-self.fp_spent) - * - * def log_message(self, user: Target | None = None) -> str | None: - * bonuses = [] - * if self.rerolls > 0: - * if self.rerolls > 1: - * bonuses.append(f"{self.rerolls} rerolls") - * else: - * bonuses.append("a reroll") - * if self.modifier != 0: - * bonuses.append(f"a {self.modifier:+} {'bonus' if self.modifier > 0 else 'penalty'}") - * if len(bonuses) == 0: - * return None - * affected = '' - * if self.user != user: - * affected = log_substitute(" on @U's roll", user=user, target=self.user) - * return f"{log_substitute('@T', user=user, target=self.user)} " \ - * f"spent **{self.fp_spent} FP** for {' and '.join(bonuses)}{affected}!" - * - * TODO: add an FP gain effect for villains (affects all party members) and fumbles and trait failures - * - * @dataclass(**JsonableDataclassArgs) - * class MissEffect(LogOnly, Effect): - * miss_type: MissType - * target: Target - * - * def log_message(self, user: Target | None = None) -> str | None: - * if self.miss_type == MissType.Missed: - * return log_substitute(f"It missed @T!", user=user, target=self.target) - * elif self.miss_type == MissType.Dodged: - * return log_substitute(f"@T dodged it!", user=user, target=self.target) - * elif self.miss_type == MissType.Avoided: - * return log_substitute(f"@T avoided it!", user=user, target=self.target) - * elif self.miss_type == MissType.Blocked: - * return log_substitute("@T blocked it!", user=user, target=self.target) - * elif self.miss_type == MissType.Immunity: - * return log_substitute("@T is immune!", user=user, target=self.target) - * elif self.miss_type == MissType.Repelled: - * return log_substitute("@T repelled it!", user=user, target=self.target) - * elif self.miss_type == MissType.Countered: - * return log_substitute("@T countered it!", user=user, target=self.target) - * elif self.miss_type == MissType.Parried: - * return log_substitute("@T parried it!", user=user, target=self.target) - * elif self.miss_type == MissType.Protected: - * return log_substitute("@T was protected from it!", user=user, target=self.target) - * elif self.miss_type == MissType.Resisted: - * return log_substitute("@T resisted it!", user=user, target=self.target) - * elif self.miss_type == MissType.Shielded: - * return log_substitute("@T shielded against it!", user=user, target=self.target) - * else: - * return log_substitute(f"@T: {self.miss_type.value}", user=user, target=self.target) - * - * - * @dataclass(**JsonableDataclassArgs) - * class ClockTickEffect(Effect): - * clock_index: int - * old_definition: Clock - * new_value: int - * delta: int - * - * def do(self, combat: "CombatStatus", source: Union[Action, "Effect", None] = None) -> "CombatStatus": - * return combat.alter_clock(self.clock_index, lambda c: c.set_value(self.new_value)) - * - * def undo(self, combat: "CombatStatus", source: Union[Action, "Effect", None] = None) -> "CombatStatus": - * return combat.alter_clock(self.clock_index, lambda c: c.set_value(self.old_definition.current)) - * - * def log_message(self, user: Target | None = None) -> str | None: - * return (f'The clock **{self.old_definition.name}** ticked {"up" if self.delta > 0 else "down"} {self.delta} ' - * f'tick{"" if abs(self.delta) == 1 else "s"}.') - */ - -const DoableEvaluators = [ - GenericActionEvaluator, -] as const satisfies readonly DoableEvaluator[] - -export type Doable = typeof DoableEvaluators[number] extends DoableEvaluator ? ActionType : never - -export enum DoableDirection { - Do = "do", - Undo = "undo", -} - -export function evaluateDoable(doable: T, gameState: GameState, direction: DoableDirection): DoableResults { - const evaluator: DoableEvaluator|undefined = DoableEvaluators.find((evaluator) => evaluator.type === doable.type) - if (!evaluator) { - return { - resultState: gameState, - logEntry: null - } - } - return evaluator.evaluate(doable, gameState, direction) -} - -export function evaluateDoables(doables: readonly Doable[], gameState: GameState, direction: DoableDirection): DoablesResults { - let currentState = gameState - const sortedDoables = direction === DoableDirection.Undo ? doables.slice().reverse() : doables - const logEntries: LogEntry[] = [] - for (const doable of sortedDoables) { - const {resultState, logEntry} = evaluateDoable(doable, gameState, direction) - currentState = resultState - if (logEntry) { - logEntries.push(logEntry) - } - } - return { - resultState: currentState, - logEntries: direction === DoableDirection.Undo ? logEntries.reverse() : logEntries, - } -} \ No newline at end of file diff --git a/src/model/GameState.ts b/src/model/GameState.ts index af97b36..6d7302b 100644 --- a/src/model/GameState.ts +++ b/src/model/GameState.ts @@ -1,25 +1,60 @@ -import {Character, CharacterId} from "./Character"; +import {Character, CharacterId, CharacterSide, SPType} from "./Character"; -export interface Clock {} +export enum ClockMode { + HEROES_FILL = "heroes fill", + HEROES_EMPTY = "heroes empty", +} + +export interface Clock { + readonly id: string + readonly text: string + readonly segments: number + readonly filled: number + readonly mode: ClockMode +} export interface SessionState { - readonly fpUsed: number - readonly upUsed: number + readonly usedSp: {readonly [key in SPType]?: number} } export interface ConflictState { - readonly round: number|null - readonly activeSideIsAllies: boolean + readonly round: number + readonly activeSide: CharacterSide readonly activeCharacterId: string|null - readonly timeoutAt: number|null + readonly timers: readonly TimerState[] +} + +export interface BaseTimerState { + readonly type: string + readonly id: string + readonly text: string +} + +export interface CountupTimerState extends BaseTimerState { + readonly type: "up" + readonly timeStartAt: number +} + +export interface CountdownTimerState extends BaseTimerState { + readonly type: "down" + readonly timeEndAt: number + readonly timeStartAt: number|null } +export type TimerState = CountupTimerState|CountdownTimerState; + export interface GameState { readonly session: SessionState readonly conflict?: ConflictState readonly clocks: readonly Clock[] readonly characters: readonly Character[] - // TODO: add "status definitions" and have character statuses reference them +} + +export interface PastState { + // The unix timestamp in ms when changing _away_ from this state. + readonly timestamp: number + readonly logMarkdown: string + readonly state: GameState } export function getCharacterById(state: GameState, id: CharacterId): Character|undefined { diff --git a/src/ui/AnimationHook.ts b/src/ui/AnimationHook.ts new file mode 100644 index 0000000..e761cdd --- /dev/null +++ b/src/ui/AnimationHook.ts @@ -0,0 +1,22 @@ +import {useCallback, useEffect, useRef} from "react"; + +export function useAnimationFrame(callback: (delta: number, current: number) => void): void { + // Use useRef for mutable variables that we want to persist + // without triggering a re-render on their change + const requestRef = useRef(0); + const previousTimeRef = useRef(0); + + const animate = useCallback(function animate(time: number) { + if (previousTimeRef.current != 0) { + const deltaTime = time - previousTimeRef.current; + callback(deltaTime, time) + } + previousTimeRef.current = time; + requestRef.current = requestAnimationFrame(animate); + }, [callback]) + + useEffect(() => { + requestRef.current = requestAnimationFrame(animate); + return () => cancelAnimationFrame(requestRef.current); + }, [animate]); +} \ No newline at end of file diff --git a/src/ui/App.css b/src/ui/App.css index e69de29..68f6bd9 100644 --- a/src/ui/App.css +++ b/src/ui/App.css @@ -0,0 +1,122 @@ +.ally-head, .enemy-head, .session-head { + color: white; + position: sticky; + text-align: center; + padding: 2px 0 5px; + text-shadow: 0 0 5px black; + align-self: stretch; + user-select: none; + border-bottom: 1px solid; + top: 0; + z-index: 90; +} + +.ally-head.inactive, .enemy-head.inactive { + color: #aaa; +} + +.totalFPSpent, .totalUPSpent { + color: white; + line-height: 60px; + -webkit-text-stroke: 2px black; + text-shadow: 2px 2px 2px black; + font-size: 45px; + letter-spacing: -3px; + text-align: center; + font-weight: bold; + height: 60px; + width: 60px; + background-repeat: no-repeat; + background-position: center; + user-select: none; + flex: 0 0 auto; +} + +.totalFPSpent { + background-image: url("fabula-points.svg"); +} + +.totalUPSpent { + background-image: url("ultima-points.svg"); +} + +.totalSPSpent { + color: white; + line-height: 60px; + text-shadow: 1px 1px 3px black; + font-size: 30px; + text-align: center; + font-weight: bold; + height: 60px; + width: 200px; + white-space: nowrap; + user-select: none; + flex: 0 0 auto; +} + +.ally-head.inactive { + background: linear-gradient(to right, transparent 0%, darkcyan 50%, transparent 100%); + border-bottom-color: darkcyan; +} + +.session-head { + background: linear-gradient(to right, transparent 0%, goldenrod 10%, gold 50%, goldenrod 90%, transparent 100%); + border-bottom-color: gold; +} + +.enemy-head.inactive { + background: linear-gradient(to right, transparent 0%, maroon 50%, transparent 100%); + border-bottom: 1px solid maroon; +} + +.ally-head { + background: linear-gradient(to right, transparent 0%, darkcyan 10%, cyan 50%, darkcyan 90%, transparent 100%); + border-bottom-color: cyan; +} + +.enemy-head { + background: linear-gradient(to right, transparent 0%, maroon 10%, red 50%, maroon 90%, transparent 100%); + border-bottom: 1px solid red; +} + +.ally-head.active::before, .ally-head.active::after { + color: lightpink; +} + +.enemy-head.active::before, .enemy-head.active::after { + color: lightskyblue; +} + +.ally-head.active::before, .enemy-head.active::before { + content: "❮"; +} + +.ally-head.active::after, .enemy-head.active::after { + content: "❯"; +} + +.appHelpName { + font-weight: bold; +} + +.appHelpValue { + margin-left: 5px; + font-style: italic; + font-size: smaller; +} + +.appHelpHeader { + text-align: left; +} + +.appHelpDescription { + text-align: left; + font-size: smaller; +} + +.appHelpValue::before { + content: "(" +} +.appHelpValue::after { + content: ")" +} \ No newline at end of file diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 97403b7..5b2a8fe 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -1,43 +1,96 @@ import React, {useEffect, useState} from 'react'; import './App.css'; -import {Container} from "react-bootstrap"; -import {Character, CharacterStatus} from "./CharacterStatus"; - -export interface PastState { - // This is the timestamp of the change that exited this state. - readonly timestampMs: number - // This is the list of actions that changed this state. - -} - +import {Col, Container, Row, Stack} from "react-bootstrap"; +import {CharacterStatus} from "./CharacterStatus"; +import {GameState} from "../model/GameState"; +import OverlayTrigger from "react-bootstrap/OverlayTrigger"; +import Tooltip from "react-bootstrap/Tooltip"; +import {TurnTimer} from "./TurnTimer"; +import {CharacterSide} from "../model/Character"; function useJson(url: string): T | null { - const [data, setData] = useState(null); - useEffect(() => { - let ignore = false; - fetch(url) - .then(response => response.json() as T) - .then(json => { - if (!ignore) { - setData(json); - } - }); - return () => { - ignore = true; - }; - }, [url]); - return data as T|null; + const [data, setData] = useState(null); + useEffect(() => { + let ignore = false; + fetch(url) + .then(response => response.json() as T) + .then(json => { + if (!ignore) { + setData(json); + } + }); + return () => { + ignore = true; + }; + }, [url]); + return data as T | null; } function App() { - const state = useJson("/api/current.json") - return - - {state && state.characters.map((character) => - )} + const state = useJson("/api/current.json") + const startTime = Date.now() + const endTime = Date.now() + 2 * 60 * 1000 + return + + + + +

Session

+ + + +
+ Fabula Points spent + {state?.session.usedSp.Fabula ?? 0}
+
+ The party earns 1 EXP for each (#-players) Fabula Points spent during the session. +
+ + } placement={"right"}> +
{state?.session.usedSp.Fabula ?? 0}
+
+
Points Spent
+ +
+ Ultima Points spent + {state?.session.usedSp.Ultima ?? 0}
+
+ The party earns 1 EXP for each Ultima Point spent during the session. +
+ + } placement={"left"}> +
{state?.session.usedSp.Ultima ?? 0}
+
+
+ + + +
+
+ + + +

Allies

+ {state && state.characters.filter((character) => character.side === CharacterSide.Ally).map((character) => + )} +
+ + + +

Enemies

+ {state && state.characters.filter((character) => character.side === CharacterSide.Enemy).map((character) => + )} +
+ +
; } diff --git a/src/ui/CharacterStatus.css b/src/ui/CharacterStatus.css index a154fc5..0d02eee 100644 --- a/src/ui/CharacterStatus.css +++ b/src/ui/CharacterStatus.css @@ -1,13 +1,13 @@ .characterStatus { height: 150px; - width: 500px; + width: 545px; position: relative; box-sizing: content-box; } .characterHeader { position: absolute; - left: 160px; + left: 205px; bottom: 55px; z-index: 4; } @@ -68,7 +68,7 @@ .characterHp { position: absolute; height: 60px; - left: 112px; + left: 157px; right: 0; bottom: 28px; overflow: visible; @@ -104,7 +104,7 @@ } .characterMp { - left: 95px; + left: 140px; right: 155px; z-index: 3; } @@ -128,7 +128,7 @@ position: absolute; top: 10px; bottom: 15px; - left: 50px; + left: 90px; width: 125px; background-repeat: no-repeat; background-position: center; @@ -137,6 +137,77 @@ z-index: 0; } +.characterZeroGauge { + position: absolute; + top: 5px; + bottom: 10px; + left: 50px; + width: 65px; +} +.characterZeroBar, .characterZeroBarBack, .characterZeroBarPulse{ + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-repeat: no-repeat; + background-position: left bottom; + background-size: auto 135px; + z-index: 0; +} + +.characterZeroBarBack { + background-image: url("./zero-bar-empty.svg"); +} +.characterZeroBar { + background-image: url("./zero-bar.svg"); +} + +@keyframes zeroBarPulse { + from { + opacity: 0; + } + 50% { + opacity: 80%; + transform: scaleX(120%) scaleY(110%) translateX(-3%); + } + to { + opacity: 0; + transform: scaleX(150%) scaleY(120%) translateX(-6%); + } +} + +.characterZeroBarPulse { + background-image: url("./zero-bar-full-pulse.svg"); + transform-origin: 10% 50%; + opacity: 0; + pointer-events: none; + transition: opacity 0.5s; +} + +.characterZeroBarPulse.active { + animation: 1s ease-out infinite zeroBarPulse; + pointer-events: none; +} + +.characterKOBar { + position: absolute; + top: 43px; + bottom: 68px; + left: 90px; + width: 125px; + background-color: black; + z-index: 0; + transform: rotate(-20deg); + text-align: center; + font-size: 27px; + line-height: 42px; + font-weight: bold; + color: white; + user-select: none; + border-radius: 15px 0; +} + .characterTurns { position: absolute; top: 5px; @@ -210,12 +281,30 @@ background: url("ultima-points.svg"); } -.characterSp { +.characterBp { + position: absolute; + color: white; + line-height: 40px; + -webkit-text-stroke: 2px black; + text-shadow: 2px 2px 2px black; + font-size: 30px; + letter-spacing: -3px; + text-align: center; + font-weight: bold; + top: 95px; + left: 5px; + width: 40px; + height: 40px; + user-select: none; + background: url("blood-points.svg") +} + +.characterSp, .characterBp { opacity: 50%; transition: opacity 0.3s ease-out; } -.characterSp:hover { +.characterSp:hover, .characterBp:hover { opacity: 100%; } @@ -223,17 +312,20 @@ position: absolute; top: 5px; right: 5px; - left: 180px; + left: 225px; display: flex; flex-direction: row; flex-wrap: nowrap; + overflow-x: auto; } .characterStatusIcon { position: relative; + flex: 0 0 36px; width: 36px; height: 48px; background-size: contain; + background-repeat: no-repeat; margin-left: 5px; overflow: visible; } diff --git a/src/ui/CharacterStatus.tsx b/src/ui/CharacterStatus.tsx index 7e3cc2b..18e811a 100644 --- a/src/ui/CharacterStatus.tsx +++ b/src/ui/CharacterStatus.tsx @@ -5,6 +5,7 @@ import {isDefined} from "../types/type_check"; import {SpringyValueInterpolatables, useSpringyValue} from "./SpringyValueHook"; import "./CharacterStatus.css"; import DefaultPortrait from "./default-portrait.svg"; +import DefaultStatus from "./default-status.svg"; import OverlayTrigger from "react-bootstrap/OverlayTrigger"; import Tooltip from "react-bootstrap/Tooltip"; import {altTo as to} from "./FixedInterpolation"; @@ -14,7 +15,7 @@ import { CharacterTurnState, healthToBounds, healthToFraction, - hpToHealth, SPType, spTypeToDescription, turnStateToDescription, + hpToHealth, spTypeToDescription, turnStateToDescription, turnStateToTitle } from "../model/Character"; @@ -74,7 +75,7 @@ const ipBarStyle: SpringyValueInterpolatables = { } export function CharacterStatus({character, active}: {character: Character, active: boolean}): ReactElement { - const {name, level, health, statuses} = character + const {name, altName, level, health, koText, statuses} = character const {hp, maxHp} = character const effectiveMaxHp = maxHp ?? 100 @@ -94,7 +95,7 @@ export function CharacterStatus({character, active}: {character: Character, acti return { hpText: isDefined(hp) ? to([hpRecentSpring], recentValue => `${Math.round(logged(recentValue))}`) - : to([hpRecentSpring], recentValue => hpToHealth(recentValue, maxHp) ?? "???"), + : "", hpBarStyleInterpolated: evaluateResourceBarStyles(hpBarStyle, hpInterpolate), hpTextStyleInterpolated: { color: to([hpRecentSpring], recentValue => healthToColor(hpToHealth(recentValue, maxHp))) @@ -157,6 +158,44 @@ export function CharacterStatus({character, active}: {character: Character, acti } }, [sp, spType, spRecentSpring]) + const {bp, maxBp} = character + const {springs: [, , {v: bpRecentSpring}]} = useSpringyValue({ + current: bp, + starting: 0, + flash: isDefined(bp) && bp > 0, + }) + const {bpText} = useMemo(() => { + if (isDefined(bp) && isDefined(maxBp)) { + return { + bpText: to([bpRecentSpring], (recentValue) => recentValue.toFixed(0)) + } + } else { + return {} + } + }, [bp, bpRecentSpring]) + + const {zp, maxZp} = character + const {springs: [, , {v: zpRecentSpring}]} = useSpringyValue({ + current: zp, + max: maxZp, + starting: 0, + flash: isDefined(maxZp) && isDefined(zp) && zp >= maxZp, + }) + const {zpStyle} = useMemo(() => { + if (isDefined(zp) && isDefined(maxZp)) { + return { + zpStyle: { + bottom: 0, + top: "auto", + height: to([zpRecentSpring],(recentValue) => { + return ((225/104) + (600 * ((recentValue / maxZp) ** 2) / 13) + (50 * (recentValue / maxZp))).toFixed(3) + "%" + }) + } + } + } else { + return {} + } + }, [zp, zpRecentSpring]) const {turnsLeft, turnsTotal, canAct} = character const {turnsState, turnsText} = useMemo(() => { if (!isDefined(turnsTotal) || !isDefined(turnsLeft)) { @@ -168,7 +207,7 @@ export function CharacterStatus({character, active}: {character: Character, acti turnsState: CharacterTurnState.Active, turnsText: "🞂", } - } else if (hp === 0 && isDefined(maxHp) && maxHp > 0) { + } else if (effectiveHp === 0) { return { turnsState: CharacterTurnState.Downed, turnsText: (isDefined(turnsTotal) && turnsLeft === 0) ? "✓" : "", @@ -201,18 +240,15 @@ export function CharacterStatus({character, active}: {character: Character, acti const filter = { color: 100, brightness: 100, + showKOBar: false, } if (isDefined(effectiveMaxHp) && Math.round(recentValue) < 1 && effectiveMaxHp > 0) { - filter.color *= 0.50 - filter.brightness *= 0.25 - } - if (canAct === false || turnsTotal === 0) { - filter.color *= 0.50 - filter.brightness *= 0.50 - } - if (isDefined(turnsTotal) && turnsLeft === 0) { - filter.color *= 0.75 - filter.brightness *= 0.75 + filter.color *= 0.35 + filter.brightness *= 0.40 + filter.showKOBar = true + } else if (canAct === false || turnsTotal === 0) { + filter.color *= 1 + filter.brightness *= 0.40 } return filter }) @@ -221,6 +257,14 @@ export function CharacterStatus({character, active}: {character: Character, acti grayscale: to([portraitFilterInterpolated], ({color}) => 100 - color), brightness: to([portraitFilterInterpolated], ({brightness}) => brightness), }) + const {opacity: koOpacitySpring} = useSpring({ + opacity: to([portraitFilterInterpolated], ({showKOBar}) => showKOBar ? 100 : 0) + }) + const characterKOBarStyleInterpolated = useMemo(() => { + return { + opacity: to([koOpacitySpring], (opacity: number) => `${opacity}%`) + } + }, [koOpacitySpring]) const characterPortraitStyleInterpolated = useMemo(() => { return { backgroundImage: to([hpFlashSpring], (flashValue: number) => { @@ -265,6 +309,23 @@ export function CharacterStatus({character, active}: {character: Character, acti return
+ {isDefined(maxZp) && isDefined(zp) && +
+ Zero Charge + {(100 * zp / maxZp).toFixed(0)}% - {zp}/{maxZp}
+ {
+ The amount of energy stored up towards unleashing the might of a Zero Power. When the gauge is full, let loose! +
} + + } placement={"right"}> +
+
+ +
= maxZp ? " active" : "")} /> +
+ } + {isDefined(effectiveMaxHp) && effectiveHp < 1 && effectiveMaxHp > 0 && {koText ?? "KO"}} {isDefined(turnsState) && @@ -283,19 +344,19 @@ export function CharacterStatus({character, active}: {character: Character, acti }
- Lv - {level ?? "??"} -
-
{name ?? "???"}
+ Lv + {level ?? "??"} +
+
{name ?? altName ?? "???"}
{isDefined(hpText) &&
- {hpText} + {isDefined(hp) && {hpText}}
} {isDefined(mpText) &&
@@ -328,6 +389,22 @@ export function CharacterStatus({character, active}: {character: Character, acti {spText} } + {isDefined(bpText) && + +
+ Blood Points + {bp}/{maxBp}
+ {isDefined(spType) &&
+ The current number of blood points, used for Vampire abilities. +
} + + } placement={"right"}> + + + {bpText} + +
} {isDefined(statuses) &&
{statuses.map(({id, name, count, description, iconUrl}) => @@ -341,7 +418,7 @@ export function CharacterStatus({character, active}: {character: Character, acti
} } placement={"bottom"}> -
{count}
+
{count}
)}
} diff --git a/src/ui/SpringyValueHook.ts b/src/ui/SpringyValueHook.ts index 53f2703..5bbd3e8 100644 --- a/src/ui/SpringyValueHook.ts +++ b/src/ui/SpringyValueHook.ts @@ -1,5 +1,5 @@ import {useCallback, useMemo, useState} from "react"; -import {AnimatedProps, SpringConfig, SpringValue, to, useSpring, useSprings, useTrail} from "@react-spring/web"; +import {AnimatedProps, SpringConfig, SpringValue, to, useSpring, useTrail} from "@react-spring/web"; import {FluidValue} from "@react-spring/shared"; export interface UseSpringyValueProps { diff --git a/src/ui/TurnTimer.css b/src/ui/TurnTimer.css new file mode 100644 index 0000000..92fa8cb --- /dev/null +++ b/src/ui/TurnTimer.css @@ -0,0 +1,20 @@ +.turnTimer { + min-width: 320px; + color: white; + text-shadow: 1px 1px 3px black; +} + +.turnTimerTitle { + font-size: 25px; + text-align: center; + margin: 0; + user-select: none; +} + +.turnTimerTime { + margin: 0; + font-size: 60px; + text-align: center; + font-weight: bold; + user-select: none; +} \ No newline at end of file diff --git a/src/ui/TurnTimer.tsx b/src/ui/TurnTimer.tsx new file mode 100644 index 0000000..d4c4e3d --- /dev/null +++ b/src/ui/TurnTimer.tsx @@ -0,0 +1,56 @@ +import {ReactElement, useCallback, useRef, useState} from "react"; +import {ProgressBar, Stack} from "react-bootstrap"; +import {useAnimationFrame} from "./AnimationHook"; +import {isDefined} from "../types/type_check"; +import "./TurnTimer.css"; +import formatDuration from "format-duration"; + +export interface TurnTimerArgs { + readonly title?: string + readonly startTime?: number + readonly endTime?: number + readonly displayedTime?: number + readonly resolutionMs?: number +} + +const DEFAULT_RESOLUTION_MS = 100; + +export function TurnTimer({title, startTime, endTime, displayedTime, resolutionMs = DEFAULT_RESOLUTION_MS}: TurnTimerArgs): ReactElement { + const [currentTime, setCurrentTime] = useState(() => displayedTime ?? Date.now()) + const accumulatedTime = useRef(0) + const animationCallback = useCallback(isDefined(displayedTime) + ? () => null + : (delta: number) => { + accumulatedTime.current += delta + if (accumulatedTime.current > resolutionMs) { + accumulatedTime.current %= resolutionMs + setCurrentTime(Date.now()) + } + }, [displayedTime, setCurrentTime, accumulatedTime]) + useAnimationFrame(animationCallback) + + if (isDefined(displayedTime) && displayedTime !== currentTime) { + setCurrentTime(displayedTime) + accumulatedTime.current = resolutionMs + } + + let totalTime: number|null = null + let timeRemaining: number|null = null + let timeElapsed: number|null = null + if (isDefined(startTime)) { + if (isDefined(endTime)) { + totalTime = endTime - startTime + timeRemaining = endTime - currentTime + } else { + timeElapsed = currentTime - startTime + } + } else if (isDefined(endTime)) { + timeRemaining = endTime - currentTime + } + + return +

{title}

+ {(timeRemaining !== null || timeElapsed !== null) &&
{formatDuration(timeRemaining ?? timeElapsed ?? 0)}
} + {timeRemaining !== null && totalTime !== null && } +
+} \ No newline at end of file diff --git a/src/ui/blood-points.svg b/src/ui/blood-points.svg new file mode 100644 index 0000000..7a8bbd4 --- /dev/null +++ b/src/ui/blood-points.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/ui/default-status.svg b/src/ui/default-status.svg new file mode 100644 index 0000000..e08221a --- /dev/null +++ b/src/ui/default-status.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ui/zero-bar-empty.svg b/src/ui/zero-bar-empty.svg new file mode 100644 index 0000000..e14b68a --- /dev/null +++ b/src/ui/zero-bar-empty.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/zero-bar-full-pulse.svg b/src/ui/zero-bar-full-pulse.svg new file mode 100644 index 0000000..cbe8aa8 --- /dev/null +++ b/src/ui/zero-bar-full-pulse.svg @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/zero-bar.svg b/src/ui/zero-bar.svg new file mode 100644 index 0000000..93c7ca9 --- /dev/null +++ b/src/ui/zero-bar.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + +