diff --git a/README.md b/README.md index c2c82f0e..1bda0a57 100755 --- a/README.md +++ b/README.md @@ -84,12 +84,18 @@ This D&D Battle Tracker simply aims to automate the process of tracking combat u * [Next button](https://game-icons.net/1x1/delapouite/next-button.html) * [Previous button](https://game-icons.net/1x1/delapouite/previous-button.html) * [Dice 20 faces 20](https://game-icons.net/1x1/delapouite/dice-twenty-faces-twenty.html) + * [Scroll quill icon](https://game-icons.net/1x1/delapouite/scroll-quill.html) * Icons by [Lorc](http://lorcblog.blogspot.com/): * [Magnifying glass](https://game-icons.net/1x1/lorc/magnifying-glass.html) * [Crossed swords](https://game-icons.net/1x1/lorc/crossed-swords.html) * [Padlock](https://game-icons.net/1x1/lorc/padlock.html) * [Skull crossed bones](https://game-icons.net/1x1/lorc/skull-crossed-bones.html) * [Charm](https://game-icons.net/1x1/lorc/charm.html) +* Spell School Icons by [Jasper_Ward-Berry](https://www.reddit.com/r/DnD/comments/jxca4n/oc_spell_school_vector_symbols/): + +### Images + +* [Dragon sculpture](https://www.artstation.com/artwork/VyrVER) Dragon's Domain: A Medieval Castle Library 1 by [Oana Rinaldi](https://www.artstation.com/oanarinaldi) ### Fonts diff --git a/package-lock.json b/package-lock.json index 548644e2..7f11ccce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -858,7 +858,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dev": true, "requires": { "@babel/highlight": "^7.18.6" } @@ -913,7 +912,6 @@ "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz", "integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==", - "dev": true, "requires": { "@babel/types": "^7.20.7", "@jridgewell/gen-mapping": "^0.3.2", @@ -924,7 +922,6 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, "requires": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -937,7 +934,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", - "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -1025,8 +1021,7 @@ "@babel/helper-environment-visitor": { "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==" }, "@babel/helper-explode-assignable-expression": { "version": "7.18.6", @@ -1041,7 +1036,6 @@ "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", - "dev": true, "requires": { "@babel/template": "^7.18.10", "@babel/types": "^7.19.0" @@ -1051,7 +1045,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -1069,7 +1062,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -1153,7 +1145,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -1161,14 +1152,12 @@ "@babel/helper-string-parser": { "version": "7.19.4", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", - "dev": true + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" }, "@babel/helper-validator-identifier": { "version": "7.19.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" }, "@babel/helper-validator-option": { "version": "7.18.6", @@ -1203,7 +1192,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", @@ -1213,8 +1201,7 @@ "@babel/parser": { "version": "7.20.13", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.13.tgz", - "integrity": "sha512-gFDLKMfpiXCsjt4za2JA9oTMn70CeseCehb11kRZgvd7+F67Hih3OHOK24cRrWECJ/ljfPGac6ygXAs/C8kIvw==", - "dev": true + "integrity": "sha512-gFDLKMfpiXCsjt4za2JA9oTMn70CeseCehb11kRZgvd7+F67Hih3OHOK24cRrWECJ/ljfPGac6ygXAs/C8kIvw==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.18.6", @@ -2045,7 +2032,6 @@ "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", - "dev": true, "requires": { "@babel/code-frame": "^7.18.6", "@babel/parser": "^7.20.7", @@ -2056,7 +2042,6 @@ "version": "7.20.13", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.13.tgz", "integrity": "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==", - "dev": true, "requires": { "@babel/code-frame": "^7.18.6", "@babel/generator": "^7.20.7", @@ -2074,7 +2059,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "requires": { "ms": "2.1.2" } @@ -2082,8 +2066,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -2091,7 +2074,6 @@ "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", - "dev": true, "requires": { "@babel/helper-string-parser": "^7.19.4", "@babel/helper-validator-identifier": "^7.19.1", @@ -2110,6 +2092,29 @@ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true }, + "@emotion/is-prop-valid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", + "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", + "requires": { + "@emotion/memoize": "^0.8.0" + } + }, + "@emotion/memoize": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + }, + "@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, "@eslint/eslintrc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", @@ -2738,14 +2743,12 @@ "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" }, "@jridgewell/set-array": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" }, "@jridgewell/source-map": { "version": "0.3.2", @@ -2773,14 +2776,12 @@ "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, "@jridgewell/trace-mapping": { "version": "0.3.17", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "dev": true, "requires": { "@jridgewell/resolve-uri": "3.1.0", "@jridgewell/sourcemap-codec": "1.4.14" @@ -4114,7 +4115,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -4421,6 +4421,23 @@ "@babel/helper-define-polyfill-provider": "^0.3.3" } }, + "babel-plugin-styled-components": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", + "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-module-imports": "^7.16.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "lodash": "^4.17.11", + "picomatch": "^2.3.0" + } + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" + }, "babel-preset-current-node-syntax": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", @@ -4666,6 +4683,11 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, + "camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==" + }, "caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -4688,7 +4710,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -4764,6 +4785,11 @@ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "dev": true }, + "classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "clean-css": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.0.tgz", @@ -4838,7 +4864,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -4846,8 +4871,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "colord": { "version": "2.9.3", @@ -5006,6 +5030,11 @@ "which": "^2.0.1" } }, + "css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==" + }, "css-declaration-sorter": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz", @@ -5054,6 +5083,11 @@ } } }, + "css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==" + }, "css-minimizer-webpack-plugin": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-4.2.2.tgz", @@ -5122,6 +5156,16 @@ "nth-check": "^2.0.1" } }, + "css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "requires": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "css-tree": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", @@ -5723,8 +5767,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" }, "escodegen": { "version": "2.0.0", @@ -6298,6 +6341,11 @@ } } }, + "exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -6745,8 +6793,7 @@ "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" }, "globalthis": { "version": "1.0.3", @@ -6829,8 +6876,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" }, "has-property-descriptors": { "version": "1.0.0", @@ -7172,6 +7218,11 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, + "hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -9104,8 +9155,7 @@ "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, "json-parse-even-better-errors": { "version": "2.3.1", @@ -9225,8 +9275,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.debounce": { "version": "4.0.8", @@ -9363,6 +9412,14 @@ "tmpl": "1.0.5" } }, + "matchmediaquery": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.3.1.tgz", + "integrity": "sha512-Hlk20WQHRIm9EE9luN1kjRjYXAQToHOIAHPJn9buxBwuhfTHoKUcX+lXBbxc85DVQfXYbEQ4HcwQdd128E3qHQ==", + "requires": { + "css-mediaquery": "^0.1.2" + } + }, "mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", @@ -10157,8 +10214,7 @@ "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, "pirates": { "version": "4.0.5", @@ -10488,8 +10544,7 @@ "postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "prelude-ls": { "version": "1.2.1", @@ -10729,6 +10784,50 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-loader-spinner": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-5.3.4.tgz", + "integrity": "sha512-G2vw4ssX+RDZ/vfaeva06yfNqyFViv/u+tVZ3kFLy5TKNlNx2DbuwreBSpRtPespQA+VxinxUJsigwLwG9erOg==", + "requires": { + "react-is": "^18.2.0", + "styled-components": "^5.3.5", + "styled-tools": "^1.7.2" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, + "react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "requires": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + } + }, + "react-responsive": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-9.0.2.tgz", + "integrity": "sha512-+4CCab7z8G8glgJoRjAwocsgsv6VA2w7JPxFWHRc7kvz8mec1/K5LutNC2MG28Mn8mu6+bu04XZxHv5gyfT7xQ==", + "requires": { + "hyphenate-style-name": "^1.0.0", + "matchmediaquery": "^0.3.0", + "prop-types": "^15.6.1", + "shallow-equal": "^1.2.1" + } + }, "react-switch": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/react-switch/-/react-switch-7.0.0.tgz", @@ -11012,6 +11111,15 @@ "glob": "^7.1.3" } }, + "rodal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/rodal/-/rodal-2.0.1.tgz", + "integrity": "sha512-nb+Zn8TcIvRVNr3r4gJUd+zzxhkL3rEXu+npudvT/wJ0ukPhpMLCgQsUwhj9Kh2Via1uZrf3cQZMNiqt6n0i2g==", + "requires": { + "classnames": "^2.2.6", + "prop-types": "^15.6.0" + } + }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -11232,6 +11340,16 @@ "kind-of": "^6.0.2" } }, + "shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==" + }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -11588,6 +11706,28 @@ "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" }, + "styled-components": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.9.tgz", + "integrity": "sha512-Aj3kb13B75DQBo2oRwRa/APdB5rSmwUfN5exyarpX+x/tlM/rwZA2vVk2vQgVSP6WKaZJHWwiFrzgHt+CLtB4A==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + } + }, + "styled-tools": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/styled-tools/-/styled-tools-1.7.2.tgz", + "integrity": "sha512-IjLxzM20RMwAsx8M1QoRlCG/Kmq8lKzCGyospjtSXt/BTIIcvgTonaxQAsKnBrsZNwhpHzO9ADx5te0h76ILVg==" + }, "stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -11621,7 +11761,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -11816,8 +11955,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" }, "to-regex-range": { "version": "5.0.1", @@ -12156,6 +12294,14 @@ "makeerror": "1.0.12" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index 8a7486c0..f1b212cb 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,11 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-highlight-words": "^0.18.0", + "react-loader-spinner": "^5.3.4", + "react-modal": "^3.16.1", + "react-responsive": "^9.0.2", "react-switch": "^7.0.0", + "rodal": "^2.0.1", "subscriptions-transport-ws": "^0.11.0" }, "scripts": { diff --git a/src/components/App.css b/src/components/App.css index 26c99c3e..e4027bf2 100755 --- a/src/components/App.css +++ b/src/components/App.css @@ -1246,19 +1246,18 @@ input[type="number"] { } #close-creature-stats{ font-size: 1rem; - /* margin-left: 20px; */ cursor: pointer; - border: 0px solid #822000; - border: 0px; - box-shadow: none; - width: auto; - border-radius: 0%; - height: auto; - background-color: transparent; - display: flex; - align-items: center; - justify-content: center; - color: grey + border: 0px solid #822000; + border: 0px; + box-shadow: none; + width: auto; + border-radius: 0%; + height: auto; + background-color: transparent; + display: flex; + align-items: center; + justify-content: center; + color: grey } .flexRow{ display: flex; @@ -1343,3 +1342,273 @@ input[type="number"] { outline: 2px solid #822000; } /* scroll to active initiative */ + +/* spell casting */ + +.used-spells { + text-decoration: line-through; +} + +.spell-level { + margin-right: 1rem; + line-height: 30px; +} + +#close-spell-stats{ + font-size: 1rem; + cursor: pointer; + border: 0px solid #822000; + border: 0px; + box-shadow: none; + width: auto; + border-radius: 0%; + height: auto; + background-color: transparent; + display: flex; + align-items: center; + justify-content: center; + color: grey +} + + .spell-info { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr; + grid-template-rows: 1fr; + gap: 5px 5px; + grid-auto-flow: row; + grid-template-areas: + ". . . . . "; + margin-bottom: 20px; + } + + .reset-spell-button{ + text-align: center; + color: #822000; + border-radius: 15px; + font-weight: bold; + cursor: pointer; + margin-left: 30px; + } + + .wrap-spell-buttons{ + display: flex; + flex-direction: row; + align-items: center; + margin-left: 10px; + } + + .edit-spell { + cursor: pointer; + border: 1.5px solid #979AA4; + font-weight: bold; + background-color: #EBE1AD; + border-radius: 15px; + box-shadow: 0 0 3px #979AA4; + padding: 0px; + height: 30px; + min-height: 20px; + width: 30px; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + color: #979AA4; + margin: 2px; + font-size: 1.3rem; + } + + .spell-toolbar{ + display: flex; + flex-direction: row; + margin-bottom: 20px; + } + .spell-input{ + width: 120px; + margin-left: 20px; + } + +/* spell casting */ + +.checkbox-wrapper{ + align-items: center; + display: flex; + justify-content: center; +} +/* checkbox */ +.checkbox-wrapper input[type="checkbox"] { + /* removing default appearance */ + -webkit-appearance: none; + appearance: none; + /* creating a custom design */ + width: 1.6em; + height: 1.6em; + /* border-radius: 0.15em; */ + margin-right: 0.5em; + border: 0.15em solid #822000; + border-radius: 10px; + box-shadow: 0 0 5px #979AA4; + outline: none; + cursor: pointer; +} + +input.checked { + background-color: #822000; + position: relative; +} + +input.checked::before { + content: "\2718"; + font-size: 1.20em; + color: #EBE1AD; + position: absolute; + right: 5px; + top: -3px; +} + +.checkbox-wrapper input[type="checkbox"]:disabled { + border-color: #c0c0c0; + background-color: #c0c0c0; +} + +.checkbox-wrapper input[type="checkbox"]:disabled + span { + color: #c0c0c0; +} + +.highlight-text{ + font-size: 13.5px; + line-height: 1.2em; + display: inline; + margin: 0; +} +#spell-level-text{ + font-size: 13.5px; + line-height: 1.7em; + margin: 0; +} + +.spell-text{ + color: #000; + text-decoration: underline solid #704cd9; +} + +/* .checkbox-wrapper input[type="checkbox"]:focus { + box-shadow: 0 0 20px #007a7e; +} */ + +/* spell modal block */ +.spell-modal-parent { + --auto-grid-min-size: 10rem; + display: grid; + grid-gap: 2rem; + grid-template-columns: repeat(auto-fill, minmax(var(--auto-grid-min-size), 1fr)); + margin: 0; + padding: 0; + box-sizing: border-box; + } + +.spell-modal-separator{ + width: 100%; + height: 3px; + margin-bottom: 20px; + margin-top: 20px; + background-color: #822000; +} + +.spell-modal-item{ + font-size: 18px; + color: #242527; +} + +.spell-modal-item-header{ + text-transform: uppercase; + font-size: 12px; + font-weight: bold; + color: #000; + margin-bottom: 5px; +} +.spell-modal-header{ + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + flex-direction: row; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + height: 120px; + padding: 0 10px 10px; + background-image: url('/src/images/spell_background.png'); + background-position: bottom center; + background-size: cover; +} + +.spell-modal-body{ + margin: 20px +} + +.spell-modal-icon{ + background: rgb(212,6,36,1); + align-items: center; + display: flex; + justify-content: center; + width: 50px; + height: 50px; + border-radius: 5px; + margin-right: 10px; +} +.evocation-background{ + background: radial-gradient(circle, rgba(212,6,36,1) 33%, rgba(80,20,20,1) 92%); +} +.conjuration-background{ + background: radial-gradient(circle, rgba(249,255,131,1) 28%, rgba(173,93,34,1) 92%); +} +.abjuration-background{ + background: radial-gradient(circle, rgba(41,199,242,1) 29%, rgba(43,105,120,1) 89%); +} +.enchantment-background{ + background: radial-gradient(circle, rgba(212,109,214,1) 29%, rgba(67,15,71,1) 89%); +} +.divination-background{ + background: radial-gradient(circle, rgba(124,158,176,1) 32%, rgba(11,28,60,1) 100%); +} +.necromancy-background{ + background: radial-gradient(circle, rgba(101,219,110,1) 19%, rgba(6,23,4,1) 100%); +} +.transmutation-background{ + background: radial-gradient(circle, rgba(200,162,67,1) 30%, rgba(88,54,13,1) 83%); +} +.illusion-background{ + background: radial-gradient(circle, rgba(215,131,255,1) 9%, rgba(50,20,62,1) 92%); +} + + +.spell-modal-header-title{ + display: block; + font-size: 28px; + font-family: Roboto,Helvetica,sans-serif; + color: #fff; + font-weight: bold; +} + +.spell-anchor{ + cursor: pointer; +} + +.spell-modal-loading{ + display: flex; + justify-content: center; + align-items: center; + height: 100%; +} + +@media (max-width:768px) { + .spell-modal-parent { + --auto-grid-min-size: 6rem; + } + .spell-modal-note + .spell-modal-description{ + font-size: 14px; + } + .spell-modal-header{ + height: 70px; + } +} diff --git a/src/components/app/DungeonMasterApp.js b/src/components/app/DungeonMasterApp.js index f11a2777..3e701976 100755 --- a/src/components/app/DungeonMasterApp.js +++ b/src/components/app/DungeonMasterApp.js @@ -39,6 +39,11 @@ import { toggleCreatureLock, toggleCreatureShare, toggleCreatureHitPointsShare, + updateCreatureSpells, + resetSpells, + addSpellSlot, + removeSpellSlot, + createSpellLevel, } from '../../state/CreatureManager'; import { save, @@ -130,6 +135,11 @@ function DungeonMasterApp({ toggleCreatureLock: updateBattle(toggleCreatureLock, false), toggleCreatureShare: updateBattle(toggleCreatureShare), toggleCreatureHitPointsShare: updateBattle(toggleCreatureHitPointsShare), + updateCreatureSpells: updateBattle(updateCreatureSpells), + resetSpells: updateBattle(resetSpells), + addSpellSlot: updateBattle(addSpellSlot), + removeSpellSlot: updateBattle(removeSpellSlot), + createSpellLevel: updateBattle(createSpellLevel), }; const onScrollActiveInitiative = () => { diff --git a/src/components/buttons/CheckBox.js b/src/components/buttons/CheckBox.js new file mode 100644 index 00000000..2707c492 --- /dev/null +++ b/src/components/buttons/CheckBox.js @@ -0,0 +1,20 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import React from 'react'; + +function Checkbox({ + label, checked, onChange, ...props +}) { + return ( +
+ +
+ ); +} + +export default Checkbox; diff --git a/src/components/buttons/CreatureSpellCreator.js b/src/components/buttons/CreatureSpellCreator.js new file mode 100644 index 00000000..065d9c91 --- /dev/null +++ b/src/components/buttons/CreatureSpellCreator.js @@ -0,0 +1,26 @@ +import React from 'react'; +import SpellScrollIcon from '../icons/SpellScroll'; + +function CreatureSpellCreator({ + toggled, + name, + onToggleCreateSpell, +}) { + const buttonTitle = `Creature spells creator ${toggled ? 'enabled' : 'disabled'}`; + const buttonAriaLabel = `${name} hit points share ${toggled ? 'enabled' : 'disabled'}`; + const buttonClass = 'creature-title-button'; + + return ( + + ); +} + +export default CreatureSpellCreator; diff --git a/src/components/creature/CreatureHeader.js b/src/components/creature/CreatureHeader.js index 63bddcf9..55bdd90e 100644 --- a/src/components/creature/CreatureHeader.js +++ b/src/components/creature/CreatureHeader.js @@ -4,6 +4,7 @@ import CreatureLocker from '../buttons/CreatureLocker'; import MonsterSearcher from '../buttons/MonsterSearcher'; import CreatureSharer from '../buttons/CreatureSharer'; import CreatureHitPointsSharer from '../buttons/CreatureHitPointsSharer'; +import CreatureSpellCreator from '../buttons/CreatureSpellCreator'; function getName(expanded, active, name, multiColumn) { const maxLength = 22; @@ -25,11 +26,13 @@ export default function CreatureHeader({ expandHandler, focused, multiColumn, + onToggleCreateSpell, }) { const { alive, name, hitPointsShared, locked, shared, } = creature; const expandedOrActive = expanded || active; + const [toggledCreateSpell, setToggleCreateSpell] = React.useState(false); const nameClass = 'creature-name'; const nameModifier = alive ? '' : 'collapsed-creature--name__dead'; @@ -41,6 +44,11 @@ export default function CreatureHeader({ const controlsClass = 'creature-header--controls'; const controlsClasses = expandedOrActive && multiColumn ? `${controlsClass} ${controlsClass}__multicolumn` : controlsClass; + const toggleCreateSpell = () => { + onToggleCreateSpell(); + setToggleCreateSpell((prev) => !prev); + }; + const creatureExpander = ( ); + const creatureSpellCreator = !playerSession && ( + + ); + const monsterSearcher = !playerSession && ( ); diff --git a/src/components/creature/CreatureStats.js b/src/components/creature/CreatureStats.js index 3658b520..ad27d818 100644 --- a/src/components/creature/CreatureStats.js +++ b/src/components/creature/CreatureStats.js @@ -1,70 +1,14 @@ /* eslint-disable max-len */ import React, { useState } from 'react'; -import Highlighter from 'react-highlight-words'; - import { - beautifySnakeWord, capitalizeWord, DamageTypesObject, getAbilityWithSign, getArmorClass, getModifierSign, getProficiencyBonus, + beautifySnakeWord, capitalizeWord, getAbilityWithSign, getArmorClass, getModifierSign, getProficiencyBonus, } from '../../util/characterSheet'; import ExternalLink from '../page/ExternalLink'; +import DescriptionHighlight from './DescriptionHighlight'; +import SpellStat from './spells/SpellStat'; const SAVING_THROW_CUT = 'Saving Throw:'; const SKILL_CUT = 'Skill:'; -const HIT_CUT = 'Hit:'; - -const renderHighlighter = (text) => { - try { - const splitText = text.split(HIT_CUT); - if (splitText.length === 0) { - return

text

; - } - - const attackRegexp = /\d+d\d+( \+ \d+)?/g; - const attackWords = text.match(attackRegexp) ?? []; - - const damageRegexp = /\+\d+ to hit/g; - const damageWords = text.match(damageRegexp) ?? []; - - const allWords = [...attackWords, ...damageWords]; - - return splitText.map((item, index) => { - const finalWords = allWords.filter((word) => item.includes(word)); - const textLine = index === 0 ? item : `${HIT_CUT}${item}`; - - const isAttackType = splitText.length === 2 && index === 0; - - const damageTypeRegexp = /\b\w+\b damage/g; - const foundDamages = textLine.match(damageTypeRegexp) ?? []; - - const filterDamageTypes = foundDamages.map((currentDamage) => currentDamage.replace(' damage', '')).filter((thisDamage) => thisDamage.toLowerCase() in DamageTypesObject); - const finalDamageTypesArr = Array.from(new Set(filterDamageTypes)); - - const sentence = textLine.split(' '); - - const newSentence = sentence.map((word) => { - if (finalDamageTypesArr.includes(word)) { - return `${word} ${DamageTypesObject[word]}`; - } - return word; - }).join(' '); - - return ( - - - - ); - }); - } catch (error) { - console.log('regex error', error); - return

text

; - } -}; export default function CreatureStats({ creature, @@ -75,8 +19,6 @@ export default function CreatureStats({ const savingThrows = creature.proficiencies ? creature.proficiencies.filter((ability) => ability.proficiency?.index.includes('saving-throw')) : []; const skills = creature.proficiencies ? creature.proficiencies.filter((ability) => ability.proficiency?.index.includes('skill')) : []; - console.log(`creature ${creature.name}`, creature); - const toggleCreatureStats = () => { setCreatureStats((prevValue) => !prevValue); }; @@ -309,19 +251,32 @@ export default function CreatureStats({ - {creature.special_abilities?.length > 0 && creature.special_abilities.map((ability) => ( -
-

- {ability.name} - . - {' '} -

-

- {renderHighlighter(ability.desc)} - {' '} -

-
- ))} + {creature.special_abilities?.length > 0 && creature.special_abilities.map((ability) => { + if (ability.name === 'Spellcasting') { + return ( + + ); + } + return ( +
+

+ {ability.name} + . + {' '} +

+

+ + {' '} +

+
+ ); + })}
@@ -336,7 +291,9 @@ export default function CreatureStats({ {' '}

- {renderHighlighter(action.desc)} + {' '}

@@ -355,7 +312,9 @@ export default function CreatureStats({ {' '}

- {renderHighlighter(action.desc)} + {' '}

diff --git a/src/components/creature/CreatureWrapper.js b/src/components/creature/CreatureWrapper.js index f06afc18..ab25a80c 100644 --- a/src/components/creature/CreatureWrapper.js +++ b/src/components/creature/CreatureWrapper.js @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ import React, { Component } from 'react'; import CollapsedCreature from './CollapsedCreature'; import ExpandedCreature from './ExpandedCreature'; @@ -8,7 +9,7 @@ import CreatureHeader from './CreatureHeader'; import CreatureRemover from '../buttons/CreatureRemover'; import { getAvailableConditions } from '../../state/ConditionsManager'; import { getHitPointsBar, shouldShowHitPoints } from '../../display/displayLogic'; -import CreatureStats from './CreatureStats'; +import { getRemainingSpellSlots } from '../../util/spells'; function getCreatureAriaLabel(creature, active, expanded) { const { name } = creature; @@ -39,11 +40,13 @@ class CreatureWrapper extends Component { this.state = { expanded: false, + showSpellCreator: false, }; this.expandCreatureHandler = this.expandCreatureHandler.bind(this); this.focusHandler = this.focusHandler.bind(this); this.hasBrowserFocus = this.hasBrowserFocus.bind(this); + this.onToggleCreateSpell = this.onToggleCreateSpell.bind(this); this.newCreatureToolbar = window.FLAG_creatureToolbar; } @@ -63,6 +66,7 @@ class CreatureWrapper extends Component { const { expanded, + showSpellCreator, } = this.state; const shouldUpdate = JSON.stringify(nextProps.creature) !== JSON.stringify(creature) @@ -70,7 +74,8 @@ class CreatureWrapper extends Component { || nextProps.focused !== focused || nextProps.toolbarFocused !== toolbarFocused || nextState.expanded !== expanded - || nextProps.round !== round; + || nextProps.round !== round + || nextState.showSpellCreator !== showSpellCreator; return shouldUpdate; } @@ -82,6 +87,10 @@ class CreatureWrapper extends Component { } } + onToggleCreateSpell() { + this.setState((prevState) => ({ ...prevState, expanded: true, showSpellCreator: !prevState.showSpellCreator })); + } + expandCreatureHandler() { this.setState((prevState) => ({ ...prevState, expanded: !prevState.expanded })); } @@ -134,22 +143,28 @@ class CreatureWrapper extends Component { const alreadyFocused = this.hasBrowserFocus('#creature-wrapper'); const toolbarAlreadyFocused = this.hasBrowserFocus('#creature-toolbar'); - const { expanded } = this.state; + const { expanded, showSpellCreator } = this.state; const showExpanded = active || expanded; const activeClassModifier = active ? 'creature-wrapper__active' : ''; const classes = `creature-wrapper ${activeClassModifier}`; const creatureAriaLabel = getCreatureAriaLabel(creature, active, expanded); const { + addSpellSlot, + removeSpellSlot, + resetSpells, + updateCreatureSpells, removeCreature, removeNoteFromCreature, toggleCreatureLock, toggleCreatureShare, toggleCreatureHitPointsShare, + createSpellLevel, } = creatureManagement; const healthPoints = ( {showExpanded ? ( - <> - - {creature.apiData && ( -
- -
- )} - + ) : ( diff --git a/src/components/creature/DescriptionHighlight.js b/src/components/creature/DescriptionHighlight.js new file mode 100644 index 00000000..7288edfb --- /dev/null +++ b/src/components/creature/DescriptionHighlight.js @@ -0,0 +1,61 @@ +import React from 'react'; +import Highlighter from 'react-highlight-words'; +import { DamageTypesObject } from '../../util/characterSheet'; + +const HIT_CUT = 'Hit:'; + +function DescriptionHighlight({ text }) { + const splitText = text.split(HIT_CUT); + if (splitText.length === 0) { + return

text

; + } + + const attackRegexp = /\d+d\d+( \+ \d+)?/g; + const attackWords = text.match(attackRegexp) ?? []; + + const damageRegexp = /\+\d+ to hit/g; + const damageWords = text.match(damageRegexp) ?? []; + + const allWords = [...attackWords, ...damageWords]; + + if (!allWords.length) return

{text}

; + + return splitText.map((item, index) => { + const finalWords = allWords.filter((word) => item.includes(word)); + const textLine = index === 0 ? item : `${HIT_CUT}${item}`; + + const isAttackType = splitText.length === 2 && index === 0; + + const damageTypeRegexp = /\b\w+\b damage/g; + const foundDamages = textLine.match(damageTypeRegexp) ?? []; + + const filterDamageTypes = foundDamages.map((currentDamage) => currentDamage.replace(' damage', '')).filter((thisDamage) => thisDamage.toLowerCase() in DamageTypesObject); + const finalDamageTypesArr = Array.from(new Set(filterDamageTypes)); + + const sentence = textLine.split(' '); + + const newSentence = sentence.map((word) => { + if (finalDamageTypesArr.includes(word)) { + return `${word} ${DamageTypesObject[word]}`; + } + return word; + }).join(' '); + + const defaultClassName = 'highlight-text'; + + return ( + + + + ); + }); +} + +export default DescriptionHighlight; diff --git a/src/components/creature/ExpandedCreature.js b/src/components/creature/ExpandedCreature.js index 31c411e8..d7389de4 100644 --- a/src/components/creature/ExpandedCreature.js +++ b/src/components/creature/ExpandedCreature.js @@ -1,6 +1,9 @@ import React from 'react'; import CreatureNoteList from './CreatureNoteList'; +import CreatureStats from './CreatureStats'; import CreatureStatus from './CreatureStatus'; +import SpellCasting from './spells/SpellCasting'; +import SpellToolbar from './toolbar/SpellToolbar'; export default function ExpandedCreature({ creature, @@ -10,6 +13,13 @@ export default function ExpandedCreature({ healthPoints, showHealth, playerSession, + updateCreatureSpells, + showSpellCreator, + resetSpells, + createSpellLevel, + addSpellSlot, + removeSpellSlot, + active, }) { const { initiative, id, conditions, notes, shared, @@ -32,6 +42,24 @@ export default function ExpandedCreature({ )} { (showHealth || showInitiative) &&
}
+ {showSpellCreator && ( + + )} + + {creature.spellData && ( + + )} + {creature.apiData && ( + + )} ); } diff --git a/src/components/creature/HealthPoints.js b/src/components/creature/HealthPoints.js index f1269279..1f94bbdd 100644 --- a/src/components/creature/HealthPoints.js +++ b/src/components/creature/HealthPoints.js @@ -9,6 +9,7 @@ function HealthPoints({ className, playerSession, armorClass, + spellsLeft, }) { const displayLong = !short && !playerSession; const displayTempHp = tempHp !== null && tempHp !== 0; @@ -56,17 +57,32 @@ function HealthPoints({ )} -
+
{short && `HP ${shortHpDisplay} `} {!short && ( <> {hpLabel} {' '} {longHpDisplay} + {' '} )}
- + {typeof spellsLeft === 'number' && ( +
+ {' '} + {' '} + {short && `Spells: ${spellsLeft} `} + {!short && ( + <> + Spell Slots: + {' '} + {spellsLeft} + {' '} + + )} +
+ )} ); } diff --git a/src/components/creature/spells/SpellCasting.js b/src/components/creature/spells/SpellCasting.js new file mode 100644 index 00000000..7a5e34ab --- /dev/null +++ b/src/components/creature/spells/SpellCasting.js @@ -0,0 +1,152 @@ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable max-len */ +import React, { useState } from 'react'; +import { capitalizeWord } from '../../../util/characterSheet'; +import Checkbox from '../../buttons/CheckBox'; + +const slotsExpired = (slots) => slots.every((slot) => slot.used); + +export default function SpellCasting({ + creature, + active, + updateCreatureSpells, + addSpellSlot, + removeSpellSlot, + showSpellCreator, +}) { + const { spellData } = creature; + + const [showSpells, setShowSpells] = useState(active || showSpellCreator); + + const [isHovering, setIsHovering] = useState(false); + const [hoverLevel, setLevel] = useState(''); + + const handleMouseOver = (level) => { + setLevel(level); + setIsHovering(true); + }; + + const onClickRandom = () => { + setIsHovering(false); + setLevel(''); + }; + + if (!spellData) return null; + + const toggleSpells = () => { + setShowSpells((prevValue) => !prevValue); + }; + + if (!showSpells) { + return ( +
+ +
+ ); + } + + const onChangeSlot = (event, level, slotIndex) => { + const { checked } = event.target; + updateCreatureSpells(creature.id, { level, slotIndex, value: checked }); + }; + + const onAddSpellSlot = (level) => { + addSpellSlot(creature.id, level); + handleMouseOver(level); + }; + + const onRemoveSpellSlot = (level) => { + removeSpellSlot(creature.id, level); + handleMouseOver(level); + }; + + return ( +
+
Spellcasting
+
+ {spellData.school && ( + + School: + {' '} + {capitalizeWord(spellData.school)} + + )} + {spellData.level && ( + + Level: + {' '} + {spellData.level} + + )} + {spellData.saveDC && ( + + Save DC: + {' '} + {spellData.saveDC} + + )} + {spellData.modifier && ( + + Spell MOD: + {' '} + {spellData.modifier} + + )} + {spellData.ability && ( + + Spell Class: + {' '} + {spellData.ability} + + )} +
+ {spellData.spells.map((item) => { + if (item.level === '0') return null; + const isDisabled = slotsExpired(item.slots); + return ( +
{}} + onMouseOver={() => { + if (isDisabled) return; + handleMouseOver(item.level); + }} + onClick={onClickRandom} + key={`Item-Level-${item.level}`} + style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }} + > + + Level + {' '} + {item.level} + + {item.slots.map((slot) => ( + onChangeSlot(ev, item.level, slot.slotIndex)} + /> + ))} + {isHovering && item.level === hoverLevel && !isDisabled && ( +
+ + +
+ )} + +
+ ); + })} +
+ +
+ +
+ ); +} diff --git a/src/components/creature/spells/SpellList.js b/src/components/creature/spells/SpellList.js new file mode 100644 index 00000000..4078fe2c --- /dev/null +++ b/src/components/creature/spells/SpellList.js @@ -0,0 +1,50 @@ +/* eslint-disable jsx-a11y/anchor-is-valid */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +import React from 'react'; +import SpellModal from './SpellModal'; + +export default function SpellList({ + knownSpells, +}) { + const [visible, setVisible] = React.useState(false); + const [spell, setSpell] = React.useState(null); + + const onClose = () => { + setVisible(false); + setSpell(null); + }; + + const onOpenSpell = (currentSpell) => { + setVisible(true); + setSpell(currentSpell); + }; + + const renderModal = () => { + if (!spell) return null; + return ( + + ); + }; + + return ( + <> + {renderModal()} + {knownSpells.map((item, index) => ( + + onOpenSpell(item)}> + + {item.name} + + + + {knownSpells.length - index !== 1 && {', '}} + + ))} + + ); +} diff --git a/src/components/creature/spells/SpellModal.js b/src/components/creature/spells/SpellModal.js new file mode 100644 index 00000000..c60f7a21 --- /dev/null +++ b/src/components/creature/spells/SpellModal.js @@ -0,0 +1,163 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import React, { useEffect } from 'react'; +import Modal from 'react-modal'; +import { Triangle } from 'react-loader-spinner'; +import { useMediaQuery } from 'react-responsive'; + +import DescriptionHighlight from '../DescriptionHighlight'; +import { + spellIcon, spellIconBackground, spellModalStyle, spellModalStyleMobile, spellModalStyleWeb, +} from './utils'; + +const BASE_API_URL = 'https://www.dnd5eapi.co'; + +export default function SpellModal({ + currentSpell, + visible, + onClose, +}) { + const [loading, setLoading] = React.useState(false); + const [spellInfo, setSpellInfo] = React.useState(null); + + const isTabletOrMobile = useMediaQuery({ maxWidth: 1224 }); + + useEffect(() => { + setLoading(true); + fetch(`${BASE_API_URL}${currentSpell.url}`, { 'Content-Type': 'application/json' }) + .then((response) => response.json()) + .then((data) => { + setSpellInfo(data); + }) + .catch((error) => { + setSpellInfo(null); + console.log('error', error); + }) + .finally(() => { + setLoading(false); + }); + }, [currentSpell]); + + const renderSpellInfo = () => { + if (loading) { + return ( +
+ +
+ ); + } + if (!loading && !spellInfo) return

Spell not found

; // add fallback; + return ( +
+
+
+ {spellIcon(spellInfo.school.name)} +
+
+ {spellInfo.name} +
+
+
+
+
+
+ Level +
+
+ {spellInfo.level} +
+ +
+
+
+ Casting Time +
+
+ {spellInfo.casting_time} +
+ +
+
+
+ Range/Area +
+
+ {spellInfo.range} +
+
+
+
+ Components +
+
+ {spellInfo.components.map((el, index) => ({`${el}${spellInfo.components.length - index === 1 ? '' : ', '}`}))} +
+
+
+
+ Duration +
+
+ {spellInfo.duration} +
+ +
+
+
+ School +
+
+ {spellInfo.school?.name} +
+ +
+
+
+ Attack/Save +
+
+ {spellInfo.dc?.dc_type?.name ?? 'None'} +
+
+
+
+ Damage/Effect +
+
+ None +
+
+
+ {/* separator line below */} +
+
+ {spellInfo.desc.map((el) => (

))} +
+
+ {spellInfo.higher_level.map((el) => (

))} +
+
+
+ ); + }; + + console.log('spellInfo', spellInfo); + return ( + + {renderSpellInfo()} + + ); +} diff --git a/src/components/creature/spells/SpellStat.js b/src/components/creature/spells/SpellStat.js new file mode 100644 index 00000000..bade33b4 --- /dev/null +++ b/src/components/creature/spells/SpellStat.js @@ -0,0 +1,94 @@ +import React from 'react'; +import DescriptionHighlight from '../DescriptionHighlight'; +import SpellList from './SpellList'; + +const getLevelPrefix = (level) => { + switch (level) { + case '1': + return 'st'; + case '2': + return 'nd'; + case '3': + return 'rd'; + default: + return 'th'; + } +}; + +export default function SpellStat({ + description, + spellData, +}) { + if (!spellData) { + return ( +
+

+ Spellcasting + . + {' '} +

+ + {description && ( +

+ + {' '} +

+ )} +
+ ); + } + // cut the text until the first spell + const newDescription = description.split('prepared:'); + const newText = newDescription[0]; + + const spellHeader = newText ? `${newText} prepared:` : ''; + + return ( +
+
+

+ Spellcasting + . + {' '} +

+ + {spellHeader && ( +

+ +

+ + )} +
+
+

+ {spellData.spells.map((spellThread) => { + const levelName = spellThread.level === '0' ? 'Cantrips' : ` ${spellThread.level}${getLevelPrefix(spellThread.level)} level`; + const suffix = spellThread.level === '0' ? '(at will):' : `(${spellThread.slots.length} slots):`; + return ( + + + {levelName} + {' '} + {suffix} + {' '} + + {spellThread.knownSpells && ( + + )} + +
+
+ + ); + })} +

+ +
+ ); +} diff --git a/src/components/creature/spells/utils.js b/src/components/creature/spells/utils.js new file mode 100644 index 00000000..d81daccd --- /dev/null +++ b/src/components/creature/spells/utils.js @@ -0,0 +1,90 @@ +import React from 'react'; +import Abjuration from '../../icons/spells/Abjuration'; +import Conjuration from '../../icons/spells/Conjuration'; +import Divination from '../../icons/spells/Divination'; +import Enchantment from '../../icons/spells/Enchantment'; +import Evocation from '../../icons/spells/Evocation'; +import Illusion from '../../icons/spells/Illusion'; +import Necromancy from '../../icons/spells/Necromancy'; +import Transmutation from '../../icons/spells/Transmutation'; + +export const spellModalStyle = { + content: { + padding: 0, + top: '50%', + left: '50%', + right: 'auto', + bottom: 'auto', + marginRight: '-50%', + transform: 'translate(-50%, -50%)', + backgroundColor: '#FDF1DC', + borderRadius: '20px', + boxShadow: '0px 1px 18px -4px rgba(117,74,50,0.98)', + WebkitOverflowScrolling: 'touch', + overflowScrolling: 'touch', + '-webkit-box-shadow': '0px 1px 18px -4px rgba(117,74,50,0.98)', + '-moz-box-shadow': '0px 1px 18px -4px rgba(117,74,50,0.98)', + }, +}; + +export const spellModalStyleWeb = { + content: { + width: '55%', + height: '55%', + }, +}; + +export const spellModalStyleMobile = { + content: { + width: '80%', + height: '60%', + }, +}; + +export const spellIconBackground = (school) => { + const loweredSchool = school.toLowerCase(); + switch (loweredSchool) { + case 'abjuration': + return 'abjuration-background'; + case 'conjuration': + return 'conjuration-background'; + case 'divination': + return 'divination-background'; + case 'enchantment': + return 'enchantment-background'; + case 'evocation': + return 'evocation-background'; + case 'illusion': + return 'illusion-background'; + case 'necromancy': + return 'necromancy-background'; + case 'transmutation': + return 'transmutation-background'; + default: + return ''; + } +}; + +export const spellIcon = (school) => { + const loweredSchool = school.toLowerCase(); + switch (loweredSchool) { + case 'abjuration': + return ; + case 'conjuration': + return ; + case 'divination': + return ; + case 'enchantment': + return ; + case 'evocation': + return ; + case 'illusion': + return ; + case 'necromancy': + return ; + case 'transmutation': + return ; + default: + return null; + } +}; diff --git a/src/components/creature/toolbar/NewCreatureToolbar.js b/src/components/creature/toolbar/NewCreatureToolbar.js index f6155f71..bc7d83d5 100644 --- a/src/components/creature/toolbar/NewCreatureToolbar.js +++ b/src/components/creature/toolbar/NewCreatureToolbar.js @@ -19,8 +19,6 @@ export default function NewCreatureToolbar({ creature }) { const buttonClass = `${toolbarClass}-button`; const textButtonClass = `${buttonClass} ${buttonClass}__text`; - console.log('>>> FOCUSED', focused); - return (
addSpellSlots(id, { spellLevel, slotNumber })} + rightControls={{ + rightTitle: 'Add spell slots', + RightSubmitIcon: , + }} + inputId={`spellLevel-${id}`} + /> + ); +} diff --git a/src/components/creature/toolbar/SpellToolbar.js b/src/components/creature/toolbar/SpellToolbar.js new file mode 100644 index 00000000..fadc217a --- /dev/null +++ b/src/components/creature/toolbar/SpellToolbar.js @@ -0,0 +1,70 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import React, { useState } from 'react'; +import SpellCreateInput from './SpellCreateInput'; + +const SPELL_LEVELS = 9; + +const levels = Array.from({ length: SPELL_LEVELS }, (_, i) => i + 1); + +export default function SpellToolbar({ + creatureId, + createSpellLevel, + resetSpells, +}) { + const styleClass = 'form--input creature-toolbar--select creature-toolbar--dropdown'; + + const [currentSpell, setSpellLevel] = useState(1); + + const onChangeLevel = (event) => { + event.preventDefault(); + setSpellLevel(event.target.value); + }; + + const addSpellSlots = (id, { spellLevel, slotNumber }) => { + if (!spellLevel || !slotNumber) return; + createSpellLevel(id, { level: spellLevel, slotNumber }); + setSpellLevel(1); + }; + + const onResetSpells = () => { + resetSpells(creatureId); + }; + + return ( +
+

Add spells

+
+
+ +
+
+ +
+ + ↻ Reset Spells + +
+
+ + ); +} diff --git a/src/components/icons/SpellScroll.js b/src/components/icons/SpellScroll.js new file mode 100644 index 00000000..6cbdc6b7 --- /dev/null +++ b/src/components/icons/SpellScroll.js @@ -0,0 +1,11 @@ +import React from 'react'; +// https://game-icons.net/1x1/delapouite/scroll-quill.html +function SpellScrollIcon({ enabled }) { + const fill = enabled ? '#fff' : '#822000'; + return ( + + + + ); +} +export default SpellScrollIcon; diff --git a/src/components/icons/spells/Abjuration.js b/src/components/icons/spells/Abjuration.js new file mode 100644 index 00000000..482b3b42 --- /dev/null +++ b/src/components/icons/spells/Abjuration.js @@ -0,0 +1,10 @@ +import React from 'react'; + +function Abjuration() { + return ( + + + + ); +} +export default Abjuration; diff --git a/src/components/icons/spells/Conjuration.js b/src/components/icons/spells/Conjuration.js new file mode 100644 index 00000000..ce89fae2 --- /dev/null +++ b/src/components/icons/spells/Conjuration.js @@ -0,0 +1,10 @@ +import React from 'react'; + +function Conjuration() { + return ( + + + + ); +} +export default Conjuration; diff --git a/src/components/icons/spells/Divination.js b/src/components/icons/spells/Divination.js new file mode 100644 index 00000000..baf4245a --- /dev/null +++ b/src/components/icons/spells/Divination.js @@ -0,0 +1,10 @@ +import React from 'react'; + +function Divination() { + return ( + + + + ); +} +export default Divination; diff --git a/src/components/icons/spells/Enchantment.js b/src/components/icons/spells/Enchantment.js new file mode 100644 index 00000000..5496ba33 --- /dev/null +++ b/src/components/icons/spells/Enchantment.js @@ -0,0 +1,10 @@ +import React from 'react'; + +function Enchantment() { + return ( + + + + ); +} +export default Enchantment; diff --git a/src/components/icons/spells/Evocation.js b/src/components/icons/spells/Evocation.js new file mode 100644 index 00000000..56c3d6e3 --- /dev/null +++ b/src/components/icons/spells/Evocation.js @@ -0,0 +1,10 @@ +import React from 'react'; + +function Evocation() { + return ( + + + + ); +} +export default Evocation; diff --git a/src/components/icons/spells/Illusion.js b/src/components/icons/spells/Illusion.js new file mode 100644 index 00000000..20dd712a --- /dev/null +++ b/src/components/icons/spells/Illusion.js @@ -0,0 +1,10 @@ +import React from 'react'; + +function Illusion() { + return ( + + + + ); +} +export default Illusion; diff --git a/src/components/icons/spells/Necromancy.js b/src/components/icons/spells/Necromancy.js new file mode 100644 index 00000000..3db8cc83 --- /dev/null +++ b/src/components/icons/spells/Necromancy.js @@ -0,0 +1,10 @@ +import React from 'react'; + +function Necromancy() { + return ( + + + + ); +} +export default Necromancy; diff --git a/src/components/icons/spells/Transmutation.js b/src/components/icons/spells/Transmutation.js new file mode 100644 index 00000000..777c33eb --- /dev/null +++ b/src/components/icons/spells/Transmutation.js @@ -0,0 +1,10 @@ +import React from 'react'; + +function Transmutation() { + return ( + + + + ); +} +export default Transmutation; diff --git a/src/components/page/CreateCreatureForm.js b/src/components/page/CreateCreatureForm.js index a1c93981..5f9d6f20 100644 --- a/src/components/page/CreateCreatureForm.js +++ b/src/components/page/CreateCreatureForm.js @@ -10,6 +10,7 @@ import D20Icon from '../icons/D20Icon'; import rollDice from '../../util/rollDice'; import DropdownOption from '../creature/toolbar/DropdownOption'; import { calculateAbilityModifier, getArmorClass } from '../../util/characterSheet'; +import { getCreatureSpellData } from '../../util/spells'; const BASE_API_URL = 'https://www.dnd5eapi.co'; @@ -137,7 +138,11 @@ function CreateCreatureForm({ createCreatureErrors, createCreature: propsCreateC name: monster.name, healthPoints: data.hit_points, armorClass: getArmorClass(data.armor_class), - apiData: data, + spellData: getCreatureSpellData(data.special_abilities), + apiData: { + ...data, + spellData: getCreatureSpellData(data.special_abilities), + }, })); }) .finally(() => { diff --git a/src/images/spell_background.png b/src/images/spell_background.png new file mode 100644 index 00000000..48647762 Binary files /dev/null and b/src/images/spell_background.png differ diff --git a/src/state/CreatureListManager.test.js b/src/state/CreatureListManager.test.js index 9fd19300..6a9a9e2a 100644 --- a/src/state/CreatureListManager.test.js +++ b/src/state/CreatureListManager.test.js @@ -360,7 +360,7 @@ describe('addCreature', () => { ariaAnnouncements: ['creatures added'], }; - const newState = addCreature(defaultState, creature); + const newState = addCreature(defaultState, { ...creature, syncMultipleInitiatives: true }); expect(newState).toEqual(expectedState); expect(createCreature.mock.calls.length).toBe(2); @@ -437,7 +437,7 @@ describe('addCreature', () => { ariaAnnouncements: ['creatures added'], }; - const newState = addCreature(initialState, creature); + const newState = addCreature(initialState, { ...creature, syncMultipleInitiatives: true }); expect(newState).toEqual(expectedState); expect(createCreature.mock.calls.length).toBe(2); diff --git a/src/state/CreatureManager.js b/src/state/CreatureManager.js index 4e42edf2..6373bd6c 100644 --- a/src/state/CreatureManager.js +++ b/src/state/CreatureManager.js @@ -1,4 +1,6 @@ +/* eslint-disable max-len */ import getSecondsElapsed from './TimeManager'; +import { findAndChangeSpellSlot, sortSpells } from '../util/spells'; import { allConditions, addCondition, removeCondition } from './ConditionsManager'; function findCreature(creatures, creatureId) { @@ -30,6 +32,131 @@ export function killCreature(state, creatureId) { ); } +export function updateCreatureSpells(state, creatureId, currentSpell) { + const creature = findCreature(state.creatures, creatureId); + + const ariaAnnouncement = `Spells changed for ${creature.name}`; + const { spellData } = creature; + + const newSpells = findAndChangeSpellSlot(spellData.spells, currentSpell); + + const newData = { ...spellData, spells: newSpells }; + + return updateCreature(state, creatureId, { spellData: newData }, ariaAnnouncement); +} + +export function resetSpells(state, creatureId) { + const creature = findCreature(state.creatures, creatureId); + + const ariaAnnouncement = `Spells changed for ${creature.name}`; + const { spellData } = creature; + + const newSpells = spellData.spells.map((spell) => { + const slots = spell.slots.map((slot) => ({ ...slot, used: false })); + return { ...spell, slots }; + }); + + const newData = { ...spellData, spells: newSpells }; + + return updateCreature(state, creatureId, { spellData: newData }, ariaAnnouncement); +} + +export function addSpellSlot(state, creatureId, spellLevel) { + const creature = findCreature(state.creatures, creatureId); + + const ariaAnnouncement = `Spells slot added for ${creature.name}`; + const { spellData } = creature; + + const newSpells = spellData.spells.map((spell) => { + if (spell.level === spellLevel) { + const { slots } = spell; + return { ...spell, slots: [...slots, { used: false, slotIndex: slots.length + 1 }] }; + } + return spell; + }); + + const newData = { ...spellData, spells: newSpells }; + + return updateCreature(state, creatureId, { spellData: newData }, ariaAnnouncement); +} + +export function removeSpellSlot(state, creatureId, spellLevel) { + const creature = findCreature(state.creatures, creatureId); + + const ariaAnnouncement = `Spells slot removed for ${creature.name}`; + const { spellData } = creature; + + const newSpells = spellData.spells.map((spell) => { + if (spell.level === spellLevel) { + const { slots } = spell; + const updatedSlots = slots.filter((_, index) => slots.length - index !== 1); + if (updatedSlots.length === 0) return null; + return { ...spell, slots: updatedSlots }; + } + return spell; + }).filter((spell) => spell !== null); + + const newData = { ...spellData, spells: newSpells }; + + return updateCreature(state, creatureId, { spellData: newData }, ariaAnnouncement); +} + +export function createSpellLevel(state, creatureId, { level, slotNumber }) { + const creature = findCreature(state.creatures, creatureId); + + const ariaAnnouncement = `Spell level added for ${creature.name}`; + + const stringLevel = `${level}`; + + // if the creature doesn't have spell data, create it + if (!creature.spellData) { + const newData = { + // TODO: make this into a function + spells: [ + { + slots: Array.from({ length: slotNumber }, (_, index) => ({ used: false, slotIndex: index })), + count: slotNumber, + level: stringLevel, + knownSpells: [], + }, + ], + + }; + + return updateCreature(state, creatureId, { spellData: newData }, ariaAnnouncement); + } + const { spellData } = creature; + // update + // update slots for existing spell level + if (spellData.spells.some((spell) => spell.level === stringLevel)) { + const newSpells = spellData.spells.map((spell) => { + if (spell.level === stringLevel) { + const { slots } = spell; + const newSlots = Array.from({ length: slotNumber }, (_, index) => ({ used: false, slotIndex: slots.length + index })); + const updatedSlots = [...slots, ...newSlots]; + return { ...spell, slots: updatedSlots }; + } + return spell; + }).filter((spell) => spell !== null); + + const newData = { ...spellData, spells: sortSpells(newSpells) }; + + return updateCreature(state, creatureId, { spellData: newData }, ariaAnnouncement); + } + // if the creature does have spell data, add the new spell level + // TODO: make this into a function + const newSpells = [...spellData.spells, { + slots: Array.from({ length: slotNumber }, (_, index) => ({ used: false, slotIndex: index })), + count: slotNumber, + level: stringLevel, + knownSpells: [], + }]; + + const newData = { ...spellData, spells: sortSpells(newSpells) }; + + return updateCreature(state, creatureId, { spellData: newData }, ariaAnnouncement); +} + export function stabalizeCreature(state, creatureId) { const creature = findCreature(state.creatures, creatureId); const ariaAnnouncement = `${creature.name} stabalized`; @@ -112,11 +239,10 @@ export function getRawName(name) { } export function createCreature(creatureId, { - armorClass, name, number, initiative, healthPoints,apiData, + armorClass, name, number, initiative, healthPoints, apiData, spellData, }) { const groupedName = number ? `${name} #${number}` : name; - return { - armorClass, + const finalCreature = { name: groupedName, initiative, healthPoints, @@ -129,8 +255,18 @@ export function createCreature(creatureId, { locked: false, shared: true, hitPointsShared: true, - apiData }; + + if (armorClass) { + finalCreature.armorClass = armorClass; + } + if (apiData) { + finalCreature.apiData = apiData; + } + if (spellData) { + finalCreature.spellData = spellData; + } + return finalCreature; } export function validateCreature(name, initiative, healthPoints, multiplier) { diff --git a/src/state/SyncManager.js b/src/state/SyncManager.js index 2c7fcd51..90cc41e2 100644 --- a/src/state/SyncManager.js +++ b/src/state/SyncManager.js @@ -5,6 +5,13 @@ export function share(state, createBattle, updateBattle, date) { if (!state.shareEnabled) { return state; } + // remove additional data from creatures to fix share battle + const newCreatures = state.creatures.map((creature) => { + const { + spellData, apiData, armorClass, ...rest + } = creature; + return rest; + }); const battleId = state.battleId || nanoid(11); @@ -13,7 +20,7 @@ export function share(state, createBattle, updateBattle, date) { battleinput: { battleId, round: state.round, - creatures: state.creatures, + creatures: newCreatures, activeCreature: state.activeCreature, expdate: Math.floor(date.getTime() / 1000.0) + 86400, }, diff --git a/src/util/spells.js b/src/util/spells.js new file mode 100644 index 00000000..475aee9e --- /dev/null +++ b/src/util/spells.js @@ -0,0 +1,68 @@ +/* eslint-disable max-len */ +export const getCreatureSpellData = (abilities) => { + if (!abilities) return undefined; + const data = abilities.find((ability) => ability.name === 'Spellcasting'); + if (!data || !data?.spellcasting) return undefined; + + const { spellcasting } = data; + + const apiSpells = spellcasting.spells ?? []; + + // for cantrips + const spells = [{ level: '0', count: 100, slots: [] }]; + + // eslint-disable-next-line no-restricted-syntax + for (const [key, value] of Object.entries(spellcasting.slots)) { + spells.push({ + level: key, + count: value, + slots: Array.from({ length: value }, (_, i) => ({ used: false, slotIndex: i })), + }); + } + + const newSpells = spells.map((spell) => { + const knownSpells = apiSpells.filter((s) => `${s.level}` === spell.level); + return { + ...spell, + knownSpells, + }; + }); + + const spellData = { + saveDC: spellcasting.dc, + modifier: spellcasting.modifier, + level: spellcasting.level, + school: spellcasting.school, + ability: spellcasting.ability.name, + spells: newSpells, + }; + + return spellData; +}; + +export const changeSpellSlot = (slots, { slotIndex, value }) => slots.map((currentSlot) => { + if (currentSlot.slotIndex === slotIndex) { + return { + ...currentSlot, + used: value, + }; + } + return currentSlot; +}); + +export const findAndChangeSpellSlot = (spells, { level, slotIndex, value }) => spells.map((currentLevel) => { + if (currentLevel.level === level) { + return { + ...currentLevel, + slots: changeSpellSlot(currentLevel.slots, { slotIndex, value }), + }; + } + return currentLevel; +}); + +export const sortSpells = (spells) => [...spells].sort((a, b) => a.level - b.level); + +export const getRemainingSpellSlots = (spells) => spells.reduce((acc, spell) => { + const remaining = spell.slots.filter((slot) => !slot.used).length; + return acc + remaining; +}, 0); diff --git a/test-integration/page-object-models/dmApp.js b/test-integration/page-object-models/dmApp.js index 62912bde..174cd84e 100644 --- a/test-integration/page-object-models/dmApp.js +++ b/test-integration/page-object-models/dmApp.js @@ -32,7 +32,6 @@ export default class DmApp extends DndBattleTracker { async addCreature(name, initiative, hp, multiply) { await this.enterCreatureName(name); - if (initiative) { const initiativeField = await screen.findByText('Initiative (optional)'); await this.user.type(initiativeField, initiative);