diff --git a/client/package-lock.json b/client/package-lock.json index 0336755..560c484 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -45,9 +45,9 @@ } }, "@types/chromecast-caf-receiver": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/chromecast-caf-receiver/-/chromecast-caf-receiver-3.0.8.tgz", - "integrity": "sha512-Zdl05s8nutvcXFri2ie5U16Sn8hT4I5SbkPW0Dsc84n3VJTQkEra3zroD/rDF0xAPL2fY1BU4w3nJA3X91KAVw==", + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/chromecast-caf-receiver/-/chromecast-caf-receiver-3.0.12.tgz", + "integrity": "sha512-GdR9nGOENDWYhF40FasB0Xnsy3c+e68K90sGVBZx1W1N3LP1NGOmCtaxgUpxk4IuHYmzGrW7I57zWZIbT3D5BQ==", "dev": true }, "@types/chromecast-caf-sender": { @@ -360,15 +360,9 @@ } }, "acorn": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", - "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", - "dev": true - }, - "acorn-dynamic-import": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", - "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.1.tgz", + "integrity": "sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q==", "dev": true }, "ajv": { @@ -772,18 +766,18 @@ "dev": true }, "autoprefixer": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.6.0.tgz", - "integrity": "sha512-kuip9YilBqhirhHEGHaBTZKXL//xxGnzvsD0FtBQa6z+A69qZD6s/BAX9VzDF1i9VKDquTJDQaPLSEhOnL6FvQ==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.6.1.tgz", + "integrity": "sha512-aVo5WxR3VyvyJxcJC3h4FKfwCQvQWb1tSI5VHNibddCVWrcD1NvlxEweg3TSgiPztMnWfjpy2FURKA2kvDE+Tw==", "dev": true, "requires": { - "browserslist": "^4.6.1", - "caniuse-lite": "^1.0.30000971", + "browserslist": "^4.6.3", + "caniuse-lite": "^1.0.30000980", "chalk": "^2.4.2", "normalize-range": "^0.1.2", "num2fraction": "^1.2.2", - "postcss": "^7.0.16", - "postcss-value-parser": "^3.3.1" + "postcss": "^7.0.17", + "postcss-value-parser": "^4.0.0" }, "dependencies": { "ansi-styles": { @@ -795,6 +789,12 @@ "color-convert": "^1.9.0" } }, + "caniuse-lite": { + "version": "1.0.30000984", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000984.tgz", + "integrity": "sha512-n5tKOjMaZ1fksIpQbjERuqCyfgec/m9pferkFQbLmWtqLUdmt12hNhjSwsmPdqeiG2NkITOQhr1VYIwWSAceiA==", + "dev": true + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -806,12 +806,6 @@ "supports-color": "^5.3.0" } }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2127,9 +2121,9 @@ } }, "css-loader": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.0.0.tgz", - "integrity": "sha512-WR6KZuCkFbnMhRrGPlkwAA7SSCtwqPwpyXJAPhotYkYsc0mKU9n/fu5wufy4jl2WhBw9Ia8gUQMIp/1w98DuPw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.1.0.tgz", + "integrity": "sha512-MuL8WsF/KSrHCBCYaozBKlx+r7vIfUaDTEreo7wR7Vv3J6N0z6fqWjRk3e/6wjneitXN1r/Y9FTK1psYNOBdJQ==", "dev": true, "requires": { "camelcase": "^5.3.1", @@ -2143,7 +2137,31 @@ "postcss-modules-scope": "^2.1.0", "postcss-modules-values": "^3.0.0", "postcss-value-parser": "^4.0.0", - "schema-utils": "^1.0.0" + "schema-utils": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "schema-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.0.1.tgz", + "integrity": "sha512-HJFKJ4JixDpRur06QHwi8uu2kZbng318ahWEKgBjc0ZklcE4FDvmm2wghb448q0IRaABxIESt8vqPFvwgMB80A==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + } } }, "css-prefers-color-scheme": { @@ -2642,9 +2660,9 @@ "dev": true }, "elliptic": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", - "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.0.tgz", + "integrity": "sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg==", "dev": true, "requires": { "bn.js": "^4.4.0", @@ -3190,13 +3208,37 @@ "dev": true }, "file-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.0.0.tgz", - "integrity": "sha512-roAbL6IdSGczwfXxhMi6Zq+jD4IfUpL0jWHD7fvmjdOVb7xBfdRUHe4LpBgO23VtVK5AW1OlWZo0p34Jvx3iWg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.1.0.tgz", + "integrity": "sha512-ajDk1nlByoalZAGR4b0H6oD+EGlWnyW1qbSxzaUc7RFiqmn+RbXQQRbTc72jsiUIlVusJ4Et58ltds8ZwTfnAw==", "dev": true, "requires": { - "loader-utils": "^1.2.2", - "schema-utils": "^1.0.0" + "loader-utils": "^1.2.3", + "schema-utils": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "schema-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.0.1.tgz", + "integrity": "sha512-HJFKJ4JixDpRur06QHwi8uu2kZbng318ahWEKgBjc0ZklcE4FDvmm2wghb448q0IRaABxIESt8vqPFvwgMB80A==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + } } }, "fill-range": { @@ -3254,6 +3296,14 @@ "requires": { "firstline": "1.2.0", "lodash": "4.17.11" + }, + "dependencies": { + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + } } }, "find-up": { @@ -5515,9 +5565,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, "lodash._reinterpolate": { @@ -5539,22 +5589,22 @@ "dev": true }, "lodash.template": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", - "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", "dev": true, "requires": { - "lodash._reinterpolate": "~3.0.0", + "lodash._reinterpolate": "^3.0.0", "lodash.templatesettings": "^4.0.0" } }, "lodash.templatesettings": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", - "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", "dev": true, "requires": { - "lodash._reinterpolate": "~3.0.0" + "lodash._reinterpolate": "^3.0.0" } }, "lodash.uniq": { @@ -5596,18 +5646,18 @@ } }, "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", "dev": true, "requires": { - "pify": "^3.0.0" + "semver": "^6.0.0" }, "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true } } @@ -5786,6 +5836,12 @@ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", "dev": true }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -5916,9 +5972,9 @@ } }, "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "requires": { "for-in": "^1.0.2", @@ -6077,6 +6133,14 @@ "find-elm-dependencies": "2.0.1", "lodash": "4.17.11", "temp": "^0.8.3" + }, + "dependencies": { + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + } } }, "node-forge": { @@ -6440,6 +6504,15 @@ "wrappy": "1" } }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, "opn": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", @@ -7227,12 +7300,12 @@ } }, "postcss-initial": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-3.0.0.tgz", - "integrity": "sha512-WzrqZ5nG9R9fUtrA+we92R4jhVvEB32IIRTzfIG/PLL8UV4CvbF1ugTEHEFX6vWxl41Xt5RTCJPEZkuWzrOM+Q==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-3.0.1.tgz", + "integrity": "sha512-I2Sz83ZSHybMNh02xQDK609lZ1/QOyYeuizCjzEhlMgeV/HcDJapQiH4yTqLjZss0X6/6VvKFXUeObaHpJoINw==", "dev": true, "requires": { - "lodash.template": "^4.2.4", + "lodash.template": "^4.5.0", "postcss": "^7.0.2" } }, @@ -7686,27 +7759,27 @@ } }, "postcss-preset-env": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-6.6.0.tgz", - "integrity": "sha512-I3zAiycfqXpPIFD6HXhLfWXIewAWO8emOKz+QSsxaUZb9Dp8HbF5kUf+4Wy/AxR33o+LRoO8blEWCHth0ZsCLA==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-6.7.0.tgz", + "integrity": "sha512-eU4/K5xzSFwUFJ8hTdTQzo2RBLbDVt83QZrAvI07TULOkmyQlnYlpwep+2yIK+K+0KlZO4BvFcleOCCcUtwchg==", "dev": true, "requires": { - "autoprefixer": "^9.4.9", - "browserslist": "^4.4.2", - "caniuse-lite": "^1.0.30000939", + "autoprefixer": "^9.6.1", + "browserslist": "^4.6.4", + "caniuse-lite": "^1.0.30000981", "css-blank-pseudo": "^0.1.4", "css-has-pseudo": "^0.10.0", "css-prefers-color-scheme": "^3.1.1", - "cssdb": "^4.3.0", - "postcss": "^7.0.14", + "cssdb": "^4.4.0", + "postcss": "^7.0.17", "postcss-attribute-case-insensitive": "^4.0.1", "postcss-color-functional-notation": "^2.0.1", "postcss-color-gray": "^5.0.0", - "postcss-color-hex-alpha": "^5.0.2", + "postcss-color-hex-alpha": "^5.0.3", "postcss-color-mod-function": "^3.0.3", "postcss-color-rebeccapurple": "^4.0.1", - "postcss-custom-media": "^7.0.7", - "postcss-custom-properties": "^8.0.9", + "postcss-custom-media": "^7.0.8", + "postcss-custom-properties": "^8.0.11", "postcss-custom-selectors": "^5.1.2", "postcss-dir-pseudo-class": "^5.0.0", "postcss-double-position-gradients": "^1.0.0", @@ -7728,6 +7801,40 @@ "postcss-replace-overflow-wrap": "^3.0.0", "postcss-selector-matches": "^4.0.0", "postcss-selector-not": "^4.0.0" + }, + "dependencies": { + "browserslist": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.6.tgz", + "integrity": "sha512-D2Nk3W9JL9Fp/gIcWei8LrERCS+eXu9AM5cfXA8WEZ84lFks+ARnZ0q/R69m2SV3Wjma83QDDPxsNKXUwdIsyA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30000984", + "electron-to-chromium": "^1.3.191", + "node-releases": "^1.1.25" + } + }, + "caniuse-lite": { + "version": "1.0.30000984", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000984.tgz", + "integrity": "sha512-n5tKOjMaZ1fksIpQbjERuqCyfgec/m9pferkFQbLmWtqLUdmt12hNhjSwsmPdqeiG2NkITOQhr1VYIwWSAceiA==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.191", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.191.tgz", + "integrity": "sha512-jasjtY5RUy/TOyiUYM2fb4BDaPZfm6CXRFeJDMfFsXYADGxUN49RBqtgB7EL2RmJXeIRUk9lM1U6A5yk2YJMPQ==", + "dev": true + }, + "node-releases": { + "version": "1.1.25", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.25.tgz", + "integrity": "sha512-fI5BXuk83lKEoZDdH3gRhtsNgh05/wZacuXkgbiYkceE7+QIMXOg98n9ZV7mz27B+kFHnqHcUpscZZlGRSmTpQ==", + "dev": true, + "requires": { + "semver": "^5.3.0" + } + } } }, "postcss-pseudo-class-any-link": { @@ -8013,17 +8120,87 @@ "dev": true }, "prettier-plugin-elm": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/prettier-plugin-elm/-/prettier-plugin-elm-0.4.2.tgz", - "integrity": "sha512-ux3JcE+YGoLRpceRCmAt44j1qknhRg8/UieGK8LA4j6rohaJeKh4RT3ubaIijI1gspxExP4duiVAevqrZ1AsZA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-elm/-/prettier-plugin-elm-0.5.0.tgz", + "integrity": "sha512-NZ52TgVENft2jOiOC1xOKnIFJyIyyW74JTRuNqoh/8WvNBAdMSR8aWMbkuDBHVWS6J7YR/JrayNTmlzZ/yGKXg==", "dev": true, "requires": { "elm-format": "^0.8.1", - "execa": "^1.0.0", - "make-dir": "^1.3.0", + "execa": "^2.0.3", + "make-dir": "^3.0.0", "object-hash": "^1.3.1", - "serialize-error": "^3.0.0", - "temp-dir": "^1.0.0" + "serialize-error": "^4.1.0", + "temp-dir": "^2.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/execa/-/execa-2.0.3.tgz", + "integrity": "sha512-iM124nlyGSrXmuyZF1EMe83ESY2chIYVyDRZKgmcDynid2Q2v/+GuE7gNMl6Sy9Niwf4MC0DDxagOxeMPjuLsw==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.5", + "get-stream": "^5.0.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^3.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "npm-run-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", + "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + }, + "dependencies": { + "path-key": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.0.tgz", + "integrity": "sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg==", + "dev": true + } + } + }, + "p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", + "dev": true + } } }, "pretty-error": { @@ -8923,10 +9100,13 @@ } }, "serialize-error": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-3.0.0.tgz", - "integrity": "sha512-+y3nkkG/go1Vdw+2f/+XUXM1DXX1XcxTl99FfiD/OEPUNw4uo0i6FKABfTAN5ZcgGtjTRZcEbxcE/jtXbEY19A==", - "dev": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-4.1.0.tgz", + "integrity": "sha512-5j9GgyGsP9vV9Uj1S0lDCvlsd+gc2LEPVK7HHHte7IyPwOD4lVQFeaX143gx3U5AnoCi+wbcb3mvaxVysjpxEw==", + "dev": true, + "requires": { + "type-fest": "^0.3.0" + } }, "serialize-javascript": { "version": "1.7.0", @@ -8994,9 +9174,9 @@ "dev": true }, "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dev": true, "requires": { "extend-shallow": "^2.0.1", @@ -9702,6 +9882,12 @@ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, "strip-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", @@ -9800,9 +9986,9 @@ } }, "temp-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", - "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", "dev": true }, "terser": { @@ -10094,6 +10280,12 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "dev": true }, + "type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "dev": true + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -10111,9 +10303,9 @@ "dev": true }, "typescript": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", - "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", + "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", "dev": true }, "uglify-js": { @@ -10176,38 +10368,15 @@ } }, "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } + "set-value": "^2.0.1" } }, "uniq": { @@ -10540,17 +10709,16 @@ } }, "webpack": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.35.0.tgz", - "integrity": "sha512-M5hL3qpVvtr8d4YaJANbAQBc4uT01G33eDpl/psRTBCfjxFTihdhin1NtAKB1ruDwzeVdcsHHV3NX+QsAgOosw==", + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.38.0.tgz", + "integrity": "sha512-lbuFsVOq8PZY+1Ytz/mYOvYOo+d4IJ31hHk/7iyoeWtwN33V+5HYotSH+UIb9tq914ey0Hot7z6HugD+je3sWw==", "dev": true, "requires": { "@webassemblyjs/ast": "1.8.5", "@webassemblyjs/helper-module-context": "1.8.5", "@webassemblyjs/wasm-edit": "1.8.5", "@webassemblyjs/wasm-parser": "1.8.5", - "acorn": "^6.0.5", - "acorn-dynamic-import": "^4.0.0", + "acorn": "^6.2.0", "ajv": "^6.1.0", "ajv-keywords": "^3.1.0", "chrome-trace-event": "^1.0.0", @@ -10572,9 +10740,9 @@ }, "dependencies": { "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", "dev": true, "requires": { "fast-deep-equal": "^2.0.1", @@ -10689,9 +10857,9 @@ } }, "webpack-cli": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.5.tgz", - "integrity": "sha512-w0j/s42c5UhchwTmV/45MLQnTVwRoaUTu9fM5LuyOd/8lFoCNCELDogFoecx5NzRUndO0yD/gF2b02XKMnmAWQ==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.6.tgz", + "integrity": "sha512-0vEa83M7kJtxK/jUhlpZ27WHIOndz5mghWL2O53kiDoA9DIxSKnfqB92LoqEn77cT4f3H2cZm1BMEat/6AZz3A==", "dev": true, "requires": { "chalk": "2.4.2", diff --git a/client/package.json b/client/package.json index 5a497bd..3b6ba93 100644 --- a/client/package.json +++ b/client/package.json @@ -28,36 +28,36 @@ }, "devDependencies": { "@types/chrome": "0.0.86", - "@types/chromecast-caf-receiver": "^3.0.8", + "@types/chromecast-caf-receiver": "^3.0.12", "@types/chromecast-caf-sender": "^1.0.1", "app-manifest-loader": "^2.3.1", "chromecast-device-emulator": "^1.2.7", "clean-webpack-plugin": "^3.0.0", "compression-webpack-plugin": "^3.0.0", "css-hot-loader": "^1.4.4", - "css-loader": "^3.0.0", + "css-loader": "^3.1.0", "cssnano": "^4.1.10", "elm": "^0.19.0-no-deps", "elm-hot-webpack-loader": "^1.0.2", "elm-webpack-loader": "^5.0.0", "extract-loader": "^3.1.0", - "file-loader": "^4.0.0", + "file-loader": "^4.1.0", "html-loader": "^0.5.5", "html-webpack-plugin": "^3.2.0", "node-sass": "^4.12.0", "postcss-import": "^12.0.1", "postcss-loader": "^3.0.0", - "postcss-preset-env": "^6.6.0", + "postcss-preset-env": "^6.7.0", "prettier": "1.18.2", - "prettier-plugin-elm": "^0.4.2", + "prettier-plugin-elm": "^0.5.0", "sass-loader": "^7.1.0", "style-loader": "^0.23.1", "terser-webpack-plugin": "^1.3.0", "ts-loader": "^6.0.4", - "typescript": "^3.5.2", + "typescript": "^3.5.3", "uglifyjs-webpack-plugin": "^2.1.3", - "webpack": "^4.35.0", - "webpack-cli": "^3.3.5", + "webpack": "^4.38.0", + "webpack-cli": "^3.3.6", "webpack-dev-server": "^3.7.2" } } diff --git a/client/src/elm/MassiveDecks.elm b/client/src/elm/MassiveDecks.elm index f773ded..d8de285 100644 --- a/client/src/elm/MassiveDecks.elm +++ b/client/src/elm/MassiveDecks.elm @@ -56,7 +56,9 @@ init flags url key = Route.fromUrl url { settings, browserLanguages } = - Json.decodeValue Decoders.flags flags |> Result.toMaybe |> Maybe.withDefault (Flags Settings.defaults []) + Json.decodeValue Decoders.flags flags + |> Result.toMaybe + |> Maybe.withDefault (Flags Settings.defaults []) ( initialisedSettings, settingsCmd ) = Settings.init settings @@ -109,7 +111,8 @@ update msg model = case msg of ChangePage route -> if oldRoute /= route then - changePageFromRoute model.shared route model.page |> Util.modelLift (\p -> { model | page = p }) + changePageFromRoute model.shared route model.page + |> Util.modelLift (\p -> { model | page = p }) else ( model, Cmd.none ) @@ -154,7 +157,9 @@ update msg model = ) Route.Redirect redirect -> - ( changePageFromRoute model.shared redirect model.page, ( shared.settings, Cmd.none ) ) + ( changePageFromRoute model.shared redirect model.page + , ( shared.settings, Cmd.none ) + ) in ( { model | shared = { shared | settings = settings } diff --git a/client/src/elm/MassiveDecks/Animated.elm b/client/src/elm/MassiveDecks/Animated.elm index 9ae175f..21de0ed 100644 --- a/client/src/elm/MassiveDecks/Animated.elm +++ b/client/src/elm/MassiveDecks/Animated.elm @@ -3,6 +3,7 @@ module MassiveDecks.Animated exposing , Msg(..) , State , animate + , defaultDuration , enterAfter , exitAfter , subscriptions @@ -92,6 +93,13 @@ exitAfter milliseconds item = Exit item |> Cmd.after milliseconds +{-| If changed, sync to CSS. +-} +defaultDuration : Int +defaultDuration = + 200 + + {- Private -} diff --git a/client/src/elm/MassiveDecks/Card.elm b/client/src/elm/MassiveDecks/Card.elm index 3ee4e5e..8c9a188 100644 --- a/client/src/elm/MassiveDecks/Card.elm +++ b/client/src/elm/MassiveDecks/Card.elm @@ -1,151 +1,45 @@ module MassiveDecks.Card exposing - ( id - , slotCount - , source - , view - , viewFilled - , viewUnknownCall - , viewUnknownResponse + ( view + , viewUnknown ) import Html exposing (Html) import Html.Attributes as HtmlA import MassiveDecks.Card.Model exposing (..) -import MassiveDecks.Card.Parts as Parts exposing (Parts) import MassiveDecks.Card.Source as Source import MassiveDecks.Card.Source.Model as Source exposing (Source) -import MassiveDecks.Model exposing (..) import MassiveDecks.Pages.Lobby.Configure.Model as Configure -import MassiveDecks.Strings exposing (MdString(..)) -import MassiveDecks.Strings.Languages as Lang import MassiveDecks.Util.Html as Html import MassiveDecks.Util.Maybe as Maybe -import MassiveDecks.Util.String as String {-| Render a card to HTML, with the given side face-up. -} -view : Shared -> List Configure.Deck -> Side -> List (Html.Attribute msg) -> Card -> Html msg -view shared decks visibleSide extraAttributes card = - viewInternal shared decks visibleSide extraAttributes [] card +view : String -> List Configure.Deck -> Side -> List (Html.Attribute msg) -> ViewBody msg -> ViewInstructions msg -> Source -> Html msg +view cardTypeClass decks visibleSide attributes viewBody viewInstructions source = + Html.div + (HtmlA.classList + [ ( "game-card", True ) + , ( cardTypeClass, True ) + , ( "face-down", visibleSide == Back ) + ] + :: attributes + ) + [ Html.div [ HtmlA.class "aspect" ] [ front decks viewBody viewInstructions source, back ] ] -{-| View a call rendered with the given play filling the slots. +{-| Render an unknown card to HTML, face-down. -} -viewFilled : Shared -> List Configure.Deck -> Side -> List (Html.Attribute msg) -> List String -> Call -> Html msg -viewFilled shared decks visibleSide extraAttributes play card = - viewInternal shared decks visibleSide extraAttributes play (C card) - - -{-| Render an unknown response to HTML, face-down. --} -viewUnknownResponse : List (Html.Attribute msg) -> Html msg -viewUnknownResponse extraAttributes = - viewUnknown (HtmlA.class "response" :: extraAttributes) - - -{-| Render an unknown response to HTML, face-down. --} -viewUnknownCall : List (Html.Attribute msg) -> Html msg -viewUnknownCall extraAttributes = - viewUnknown (HtmlA.class "call" :: extraAttributes) - - -{-| Get the source of a card. --} -source : Card -> Source -source card = - card |> details |> .source - - -{-| Get the id of a card. --} -id : Card -> Id -id card = - card |> details |> .id - - -{-| How many slots there are on a call. --} -slotCount : Call -> Int -slotCount call = - call.body |> Parts.slotCount +viewUnknown : String -> List (Html.Attribute msg) -> Html msg +viewUnknown cardTypeClass attributes = + Html.div (HtmlA.class "game-card face-down" :: (HtmlA.class cardTypeClass :: attributes)) + [ Html.div [ HtmlA.class "aspect" ] [ back ] ] {- Private -} -viewInternal : Shared -> List Configure.Deck -> Side -> List (Html.Attribute msg) -> List String -> Card -> Html msg -viewInternal shared decks visibleSide extraAttributes play card = - Html.div - ([ HtmlA.classList - [ ( "game-card", True ) - , ( cardTypeClass card, True ) - , ( "face-down", visibleSide == Back ) - ] - ] - ++ extraAttributes - ) - [ Html.div [ HtmlA.class "aspect" ] - [ front shared decks play card - , back - ] - ] - - -viewUnknown : List (Html.Attribute msg) -> Html msg -viewUnknown extraAttributes = - Html.div - ([ HtmlA.class "game-card face-down" ] ++ extraAttributes) - [ Html.div [ HtmlA.class "aspect" ] - [ back ] - ] - - -instructions : Shared -> Parts -> List (Html msg) -instructions shared parts = - let - slots = - Parts.slotCount parts - - instructionViews = - List.concat [ extraCardsInstruction shared slots, pickInstruction shared slots ] - in - if List.length instructionViews > 0 then - [ Html.ol - [ HtmlA.class "instructions" ] - instructionViews - ] - - else - [] - - -extraCardsInstruction : Shared -> Int -> List (Html msg) -extraCardsInstruction shared slots = - let - extraCards = - slots - 1 - in - if extraCards > 0 then - [ Html.li [] [ Draw { numberOfCards = extraCards } |> Lang.html shared ] - ] - - else - [] - - -pickInstruction : Shared -> Int -> List (Html msg) -pickInstruction shared slots = - if slots > 1 then - [ Html.li [] [ Pick { numberOfCards = slots } |> Lang.html shared ] - ] - - else - [] - - cardSide : Side -> List (Html msg) -> Html msg cardSide side content = Html.div [ HtmlA.classList [ ( "side", True ), ( "front", side == Front ), ( "back", side == Back ) ] ] content @@ -159,37 +53,17 @@ back = ] -front : Shared -> List Configure.Deck -> List String -> Card -> Html msg -front shared decks play card = +front : List Configure.Deck -> ViewBody msg -> ViewInstructions msg -> Source -> Html msg +front decks (ViewBody viewBody) viewInstructions source = cardSide Front - [ Html.div [ HtmlA.class "content" ] - (case card of - C call -> - Parts.viewFilled play call.body - - R response -> - [ Html.p [] [ Html.span [] [ response.body |> String.capitalise |> Html.text ] ] ] - ) - , info shared decks card + [ Html.div [ HtmlA.class "content" ] (viewBody ()) + , info decks viewInstructions source ] -info : Shared -> List Configure.Deck -> Card -> Html msg -info shared decks card = - let - instructionsInfo = - case card of - C call -> - instructions shared call.body - - R _ -> - [] - in - Html.div [ HtmlA.class "info" ] - ([ card |> source |> viewSource decks - ] - ++ instructionsInfo - ) +info : List Configure.Deck -> ViewInstructions msg -> Source -> Html msg +info decks (ViewInstructions viewInstructions) source = + Html.div [ HtmlA.class "info" ] ((source |> viewSource decks) :: viewInstructions ()) viewSource : List Configure.Deck -> Source -> Html msg @@ -211,23 +85,3 @@ viewSource decks s = makeLink : Html msg -> String -> Html msg makeLink text url = Html.blankA [ HtmlA.href url ] [ text ] - - -cardTypeClass : Card -> String -cardTypeClass card = - case card of - C _ -> - "call" - - R _ -> - "response" - - -details : Card -> Details -details card = - case card of - C call -> - call.details - - R response -> - response.details diff --git a/client/src/elm/MassiveDecks/Card/Call.elm b/client/src/elm/MassiveDecks/Card/Call.elm new file mode 100644 index 0000000..bfe259f --- /dev/null +++ b/client/src/elm/MassiveDecks/Card/Call.elm @@ -0,0 +1,103 @@ +module MassiveDecks.Card.Call exposing (slotCount, view, viewFilled, viewUnknown) + +import Html exposing (Html) +import Html.Attributes as HtmlA +import MassiveDecks.Card as Card +import MassiveDecks.Card.Model exposing (..) +import MassiveDecks.Card.Parts as Parts exposing (Parts) +import MassiveDecks.Game.Rules exposing (Rules) +import MassiveDecks.Model exposing (Shared) +import MassiveDecks.Pages.Lobby.Configure.Model as Configure exposing (Config) +import MassiveDecks.Strings exposing (MdString(..)) +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.Util.Maybe as Maybe + + +{-| How many slots there are on a call. +-} +slotCount : Call -> Int +slotCount call = + call.body |> Parts.slotCount + + +{-| Render the call to HTML. +-} +view : Shared -> Config -> Side -> List (Html.Attribute msg) -> Call -> Html msg +view shared config side attributes call = + viewInternal shared config side attributes Parts.view call + + +{-| Render the call to HTML, with the slots filled with the given values. +-} +viewFilled : Shared -> Config -> Side -> List (Html.Attribute msg) -> List String -> Call -> Html msg +viewFilled shared config side attributes fillWith call = + viewInternal shared config side attributes (Parts.viewFilled fillWith) call + + +{-| Render an unknown response to HTML, face-down. +-} +viewUnknown : List (Html.Attribute msg) -> Html msg +viewUnknown attributes = + Card.viewUnknown "call" attributes + + + +{- Private -} + + +viewInternal : Shared -> Config -> Side -> List (Html.Attribute msg) -> (Parts -> List (Html msg)) -> Call -> Html msg +viewInternal shared config side attributes viewParts call = + Card.view + "call" + config.decks + side + attributes + (viewBody viewParts call) + (viewInstructions shared config.rules call) + call.details.source + + +viewBody : (Parts -> List (Html msg)) -> Call -> ViewBody msg +viewBody viewParts call = + ViewBody (\() -> viewParts call.body) + + +viewInstructions : Shared -> Rules -> Call -> ViewInstructions msg +viewInstructions shared rules call = + ViewInstructions (\() -> instructions shared rules call.body) + + +instructions : Shared -> Rules -> Parts -> List (Html msg) +instructions shared rules parts = + let + slots = + Parts.slotCount parts + + instructionViews = + List.concat [ extraCardsInstruction shared rules slots, pickInstruction shared slots ] + in + [ Html.ol [ HtmlA.class "instructions" ] instructionViews ] + |> Maybe.justIf (List.length instructionViews > 0) + |> Maybe.withDefault [] + + +extraCardsInstruction : Shared -> Rules -> Int -> List (Html msg) +extraCardsInstruction shared rules slots = + if Maybe.isJust rules.houseRules.packingHeat || slots > 2 then + let + extraCards = + slots - 1 + in + [ Html.li [] [ Draw { numberOfCards = extraCards } |> Lang.html shared ] ] + |> Maybe.justIf (extraCards > 0) + |> Maybe.withDefault [] + + else + [] + + +pickInstruction : Shared -> Int -> List (Html msg) +pickInstruction shared slots = + [ Html.li [] [ Pick { numberOfCards = slots } |> Lang.html shared ] ] + |> Maybe.justIf (slots > 1) + |> Maybe.withDefault [] diff --git a/client/src/elm/MassiveDecks/Card/Model.elm b/client/src/elm/MassiveDecks/Card/Model.elm index b80df3d..68ba106 100644 --- a/client/src/elm/MassiveDecks/Card/Model.elm +++ b/client/src/elm/MassiveDecks/Card/Model.elm @@ -1,17 +1,18 @@ module MassiveDecks.Card.Model exposing ( Call - , Card(..) , Details , Id , Response , Side(..) - , Type , UnknownResponse + , ViewBody(..) + , ViewInstructions(..) , call , frontSide , response ) +import Html exposing (Html) import MassiveDecks.Card.Parts exposing (..) import MassiveDecks.Card.Source.Model exposing (..) @@ -40,17 +41,10 @@ frontSide isFront = Back -{-| A general card. --} -type Card - = C Call - | R Response - - {-| A call card. -} type alias Call = - Type Parts + Card Parts {-| A simple constructor for a call card. @@ -65,7 +59,7 @@ call parts id source = {-| A response card. -} type alias Response = - Type String + Card String {-| A simple constructor for a response card. @@ -79,13 +73,13 @@ response text id source = {-| A response that isn't known to the user. Only the back can be displayed. -} -type alias UnknownResponse = - Id +type UnknownResponse + = UnknownResponse Id {-| The data for a type of card. -} -type alias Type body = +type alias Card body = { details : Details , body : body } @@ -97,3 +91,15 @@ type alias Details = { source : Source , id : Id } + + +{-| A function to render the body of a card. +-} +type ViewBody msg + = ViewBody (() -> List (Html msg)) + + +{-| A function to render the instructions for a card. +-} +type ViewInstructions msg + = ViewInstructions (() -> List (Html msg)) diff --git a/client/src/elm/MassiveDecks/Card/Response.elm b/client/src/elm/MassiveDecks/Card/Response.elm new file mode 100644 index 0000000..c918873 --- /dev/null +++ b/client/src/elm/MassiveDecks/Card/Response.elm @@ -0,0 +1,42 @@ +module MassiveDecks.Card.Response exposing (view, viewUnknown) + +import Html exposing (Html) +import MassiveDecks.Card as Card +import MassiveDecks.Card.Model exposing (..) +import MassiveDecks.Pages.Lobby.Configure.Model exposing (Config) +import MassiveDecks.Util.String as String + + +{-| Render the response to HTML. +-} +view : Config -> Side -> List (Html.Attribute msg) -> Response -> Html msg +view config side attributes response = + Card.view + "response" + config.decks + side + attributes + (viewBody response) + viewInstructions + response.details.source + + +{-| Render an unknown response to HTML, face-down. +-} +viewUnknown : List (Html.Attribute msg) -> Html msg +viewUnknown attributes = + Card.viewUnknown "response" attributes + + + +{- Private -} + + +viewBody : Response -> ViewBody msg +viewBody response = + ViewBody (\() -> [ Html.p [] [ Html.span [] [ response.body |> String.capitalise |> Html.text ] ] ]) + + +viewInstructions : ViewInstructions msg +viewInstructions = + ViewInstructions (\() -> []) diff --git a/client/src/elm/MassiveDecks/Components/Form/Message.elm b/client/src/elm/MassiveDecks/Components/Form/Message.elm index fe9122a..f618015 100644 --- a/client/src/elm/MassiveDecks/Components/Form/Message.elm +++ b/client/src/elm/MassiveDecks/Components/Form/Message.elm @@ -11,7 +11,7 @@ module MassiveDecks.Components.Form.Message exposing ) import FontAwesome.Attributes as Icon -import FontAwesome.Icon as Icon +import FontAwesome.Icon as Icon exposing (Icon) import FontAwesome.Solid as Icon import Html exposing (Html) import Html.Attributes as HtmlA @@ -20,11 +20,13 @@ import MassiveDecks.Components as Components import MassiveDecks.Model exposing (..) import MassiveDecks.Strings exposing (MdString) import MassiveDecks.Strings.Languages as Lang +import Svg.Attributes as Svg type alias Fix msg = - { text : MdString - , msg : msg + { description : MdString + , icon : Icon + , action : msg } @@ -48,7 +50,7 @@ info mdString = Just { severity = Info , description = mdString - , fix = Nothing + , fixes = [] } @@ -57,7 +59,7 @@ warning mdString = Just { severity = Warning , description = mdString - , fix = Nothing + , fixes = [] } @@ -66,16 +68,16 @@ error mdString = Just { severity = Error , description = mdString - , fix = Nothing + , fixes = [] } -errorWithFix : MdString -> MdString -> msg -> Message msg -errorWithFix errorString fixString fix = +errorWithFix : MdString -> List (Fix msg) -> Message msg +errorWithFix errorString fixes = Just { severity = Error , description = errorString - , fix = Just { text = fixString, msg = fix } + , fixes = fixes } @@ -91,12 +93,12 @@ none = type alias InternalMessage msg = { severity : Severity , description : MdString - , fix : Maybe (Fix msg) + , fixes : List (Fix msg) } internalMessage : Shared -> InternalMessage msg -> Html msg -internalMessage shared { severity, description, fix } = +internalMessage shared { severity, description, fixes } = let ( class, icon ) = case severity of @@ -108,16 +110,20 @@ internalMessage shared { severity, description, fix } = Error -> ( "inline-error", Icon.exclamationCircle ) - - fixLink = - case fix of - Just { text, msg } -> - [ Html.text " ", Components.linkButton [ msg |> HtmlE.onClick ] [ text |> Lang.html shared ] ] - - Nothing -> - [] in Html.span [ HtmlA.class class ] - [ Icon.viewStyled [ Icon.fw ] icon - , Html.span [] ((description |> Lang.html shared) :: fixLink) + [ Icon.viewStyled [ Icon.fw, Svg.class "message-type-icon" ] icon + , Html.span [] [ description |> Lang.html shared ] + , Html.ul [ HtmlA.class "fixes" ] (fixes |> List.map (viewFix shared)) + ] + + +viewFix : Shared -> Fix msg -> Html msg +viewFix shared { icon, description, action } = + Html.li [] + [ Components.iconButton + [ action |> HtmlE.onClick + , description |> Lang.title shared + ] + icon ] diff --git a/client/src/elm/MassiveDecks/Game.elm b/client/src/elm/MassiveDecks/Game.elm index ab34674..185b310 100644 --- a/client/src/elm/MassiveDecks/Game.elm +++ b/client/src/elm/MassiveDecks/Game.elm @@ -14,7 +14,7 @@ import FontAwesome.Solid as Icon import Html exposing (Html) import Html.Attributes as HtmlA import Html.Events as HtmlE -import MassiveDecks.Card as Card +import MassiveDecks.Card.Call as Call import MassiveDecks.Card.Model as Card import MassiveDecks.Card.Play as Play exposing (Play) import MassiveDecks.Components as Components @@ -32,7 +32,7 @@ import MassiveDecks.Game.Rules as Rules import MassiveDecks.Messages as Global import MassiveDecks.Model exposing (..) import MassiveDecks.Pages.Lobby.Actions as Actions -import MassiveDecks.Pages.Lobby.Configure.Model as Configure +import MassiveDecks.Pages.Lobby.Configure.Model as Configure exposing (Config) import MassiveDecks.Pages.Lobby.Events as Events import MassiveDecks.Pages.Lobby.Messages as Lobby import MassiveDecks.Pages.Lobby.Model as Lobby exposing (Lobby) @@ -40,8 +40,6 @@ import MassiveDecks.Strings as Strings exposing (MdString) import MassiveDecks.Strings.Languages as Lang import MassiveDecks.User as User exposing (User) import MassiveDecks.Util as Util -import MassiveDecks.Util.Html as Html -import MassiveDecks.Util.Html.Attributes as HtmlA import MassiveDecks.Util.Maybe as Maybe import Set import Task @@ -88,7 +86,7 @@ update msg model = picks.cards ++ [ id ] extra = - max 0 (List.length picked - (playingRound.call |> Card.slotCount)) + max 0 (List.length picked - (playingRound.call |> Call.slotCount)) newRound = Round.P { playingRound | pick = { picks | cards = picked |> List.drop extra } } @@ -181,9 +179,9 @@ update msg model = ( { model | playStyles = playStyles }, Cmd.none ) AdvanceRound -> - case model.nextRound of - Just nextRound -> - ( { model | game = { game | round = Round.P nextRound }, nextRound = Nothing } + case model.completeRound of + Just _ -> + ( { model | completeRound = Nothing } , Cmd.none ) @@ -199,22 +197,27 @@ subscriptions model = Sub.none -view : Shared -> Lobby.Auth -> List Configure.Deck -> Dict User.Id User -> Model -> Html Global.Msg -view shared auth decks users model = +view : Shared -> Lobby.Auth -> Config -> Dict User.Id User -> Model -> Html Global.Msg +view shared auth config users model = let { instruction, action, content, fillCallWith } = - case game.round of - Round.P round -> - Playing.view shared auth decks model round + case model.completeRound of + Just completeRound -> + Complete.view shared (Round.C completeRound /= game.round) config users completeRound - Round.R round -> - Revealing.view shared auth decks round + Nothing -> + case game.round of + Round.P round -> + Playing.view auth config model round - Round.J round -> - Judging.view shared auth decks round + Round.R round -> + Revealing.view auth config round - Round.C round -> - Complete.view shared (Maybe.isJust model.nextRound) decks users round + Round.J round -> + Judging.view auth config round + + Round.C round -> + Complete.view shared False config users round game = model.game @@ -226,7 +229,7 @@ view shared auth decks users model = fillCallWith |> List.map .body renderedCall = - call |> Card.viewFilled shared decks Card.Front [] parts + call |> Call.viewFilled shared config Card.Front [] parts help = let @@ -415,13 +418,29 @@ applyGameEvent auth gameEvent model = Events.RoundStarted { id, czar, players, call, drawn } -> let - drawnAsList = - drawn |> Maybe.withDefault [] - - ( newRound, cmd ) = - Playing.init (Round.playing id czar players call Set.empty) Round.noPick + game = + model.game in - ( { model | nextRound = Just newRound, hand = model.hand ++ drawnAsList }, cmd ) + case model.game.round of + Round.C c -> + let + drawnAsList = + drawn |> Maybe.withDefault [] + + ( newRound, cmd ) = + Playing.init (Round.playing id czar players call Set.empty) Round.noPick + in + ( { model + | game = { game | round = Round.P newRound } + , completeRound = Just c + , hand = model.hand ++ drawnAsList + } + , cmd + ) + + _ -> + -- TODO: Error + ( model, Cmd.none ) Events.StartRevealing { plays, drawn } -> case model.game.round of @@ -458,7 +477,7 @@ applyGameStarted lobby round hand = lobby.users |> Dict.toList |> List.map (\( id, _ ) -> id) defaultPlayer = - { control = Player.Human, score = 0 } + { score = 0 } game = { round = Round.P round diff --git a/client/src/elm/MassiveDecks/Game/Model.elm b/client/src/elm/MassiveDecks/Game/Model.elm index a1ed88a..5911fa1 100644 --- a/client/src/elm/MassiveDecks/Game/Model.elm +++ b/client/src/elm/MassiveDecks/Game/Model.elm @@ -25,7 +25,7 @@ type alias Model = { game : Game , hand : List Card.Response , playStyles : PlayStyles - , nextRound : Maybe Round.Playing + , completeRound : Maybe Round.Complete } @@ -36,7 +36,7 @@ emptyModel game = { game = game , hand = [] , playStyles = Dict.empty - , nextRound = Nothing + , completeRound = Nothing } diff --git a/client/src/elm/MassiveDecks/Game/Player.elm b/client/src/elm/MassiveDecks/Game/Player.elm index e109bb1..43f3df0 100644 --- a/client/src/elm/MassiveDecks/Game/Player.elm +++ b/client/src/elm/MassiveDecks/Game/Player.elm @@ -1,6 +1,5 @@ module MassiveDecks.Game.Player exposing - ( Control(..) - , PlayState(..) + ( PlayState(..) , Player , Role(..) , Score @@ -19,8 +18,7 @@ import Set {-| A player in the game. -} type alias Player = - { control : Control - , score : Score + { score : Score } @@ -30,13 +28,6 @@ type alias Score = Int -{-| How the player is being controlled. --} -type Control - = Human - | Computer - - {-| The role the player currently has in the game. -} type Role diff --git a/client/src/elm/MassiveDecks/Game/Round/Complete.elm b/client/src/elm/MassiveDecks/Game/Round/Complete.elm index cbc817b..1a1dffb 100644 --- a/client/src/elm/MassiveDecks/Game/Round/Complete.elm +++ b/client/src/elm/MassiveDecks/Game/Round/Complete.elm @@ -6,14 +6,14 @@ import FontAwesome.Solid as Icon import Html exposing (Html) import Html.Attributes as HtmlA import Html.Keyed as HtmlK -import MassiveDecks.Card as Card import MassiveDecks.Card.Model as Card +import MassiveDecks.Card.Response as Response import MassiveDecks.Game.Action.Model as Action import MassiveDecks.Game.Model exposing (..) import MassiveDecks.Game.Round as Round import MassiveDecks.Messages as Global import MassiveDecks.Model exposing (..) -import MassiveDecks.Pages.Lobby.Configure.Model as Configure +import MassiveDecks.Pages.Lobby.Configure.Model as Configure exposing (Config) import MassiveDecks.Strings as Strings import MassiveDecks.Strings.Languages as Lang import MassiveDecks.User as User exposing (User) @@ -21,8 +21,8 @@ import MassiveDecks.Util.Html as Html import MassiveDecks.Util.Maybe as Maybe -view : Shared -> Bool -> List Configure.Deck -> Dict User.Id User -> Round.Complete -> RoundView Global.Msg -view shared nextRoundReady decks users round = +view : Shared -> Bool -> Config -> Dict User.Id User -> Round.Complete -> RoundView Global.Msg +view shared nextRoundReady config users round = let winning = round.plays |> Dict.get round.winner @@ -31,22 +31,16 @@ view shared nextRoundReady decks users round = , action = Action.Advance |> Maybe.justIf nextRoundReady , content = HtmlK.ul [ HtmlA.class "complete plays cards" ] - (round.plays |> Dict.toList |> List.map (viewPlay shared decks users round.winner)) + (round.plays |> Dict.toList |> List.map (viewPlay shared config users round.winner)) , fillCallWith = winning |> Maybe.withDefault [] } -viewPlay : - Shared - -> List Configure.Deck - -> Dict User.Id User - -> User.Id - -> ( User.Id, List Card.Response ) - -> ( String, Html Global.Msg ) -viewPlay shared decks users winner ( id, responses ) = +viewPlay : Shared -> Config -> Dict User.Id User -> User.Id -> ( User.Id, List Card.Response ) -> ( String, Html Global.Msg ) +viewPlay shared config users winner ( id, responses ) = let cards = - responses |> List.map (\r -> ( r.details.id, Card.view shared decks Card.Front [] (Card.R r) )) + responses |> List.map (\r -> ( r.details.id, Response.view config Card.Front [] r )) playedBy = users |> Dict.get id |> Maybe.map .name |> Maybe.withDefault (Strings.UnknownUser |> Lang.string shared) diff --git a/client/src/elm/MassiveDecks/Game/Round/Judging.elm b/client/src/elm/MassiveDecks/Game/Round/Judging.elm index 42afdcc..a41293f 100644 --- a/client/src/elm/MassiveDecks/Game/Round/Judging.elm +++ b/client/src/elm/MassiveDecks/Game/Round/Judging.elm @@ -1,9 +1,9 @@ module MassiveDecks.Game.Round.Judging exposing (view) import Html.Attributes as HtmlA -import MassiveDecks.Card as Card import MassiveDecks.Card.Model as Card import MassiveDecks.Card.Play as Play +import MassiveDecks.Card.Response as Response import MassiveDecks.Game.Action.Model as Action import MassiveDecks.Game.Messages exposing (..) import MassiveDecks.Game.Model exposing (..) @@ -11,8 +11,7 @@ import MassiveDecks.Game.Player as Player import MassiveDecks.Game.Round as Round import MassiveDecks.Game.Round.Plays as Plays import MassiveDecks.Messages as Global -import MassiveDecks.Model exposing (..) -import MassiveDecks.Pages.Lobby.Configure.Model as Configure +import MassiveDecks.Pages.Lobby.Configure.Model as Configure exposing (Config) import MassiveDecks.Pages.Lobby.Messages as Lobby import MassiveDecks.Pages.Lobby.Model exposing (Auth) import MassiveDecks.Strings as Strings @@ -20,8 +19,8 @@ import MassiveDecks.Util.Maybe as Maybe import Set exposing (Set) -view : Shared -> Auth -> List Configure.Deck -> Round.Judging -> RoundView Global.Msg -view shared auth decks round = +view : Auth -> Config -> Round.Judging -> RoundView Global.Msg +view auth config round = let role = Player.role (Round.J round) auth.claims.uid @@ -42,7 +41,7 @@ view shared auth decks round = |> Maybe.withDefault [] details = - round.plays |> List.map (playDetails shared decks round.liked) + round.plays |> List.map (playDetails config round.liked) in { instruction = Just instruction , action = action @@ -55,12 +54,12 @@ view shared auth decks round = {- Private -} -playDetails : Shared -> List Configure.Deck -> Set Play.Id -> Play.Known -> Plays.Details Global.Msg -playDetails shared decks liked { id, responses } = +playDetails : Config -> Set Play.Id -> Play.Known -> Plays.Details Global.Msg +playDetails config liked { id, responses } = let cards = responses - |> List.map (\r -> Card.view shared decks Card.Front [] (Card.R r)) + |> List.map (\r -> Response.view config Card.Front [] r) attrs = [ HtmlA.class "liked" ] |> Maybe.justIf (Set.member id liked) |> Maybe.withDefault [] diff --git a/client/src/elm/MassiveDecks/Game/Round/Playing.elm b/client/src/elm/MassiveDecks/Game/Round/Playing.elm index cec8c30..da5e9ce 100644 --- a/client/src/elm/MassiveDecks/Game/Round/Playing.elm +++ b/client/src/elm/MassiveDecks/Game/Round/Playing.elm @@ -8,16 +8,16 @@ import Html exposing (Html) import Html.Attributes as HtmlA import Html.Events as HtmlE import Html.Keyed as HtmlK -import MassiveDecks.Card as Card +import MassiveDecks.Card.Call as Call import MassiveDecks.Card.Model as Card +import MassiveDecks.Card.Response as Response import MassiveDecks.Game.Action.Model as Action import MassiveDecks.Game.Messages as Game import MassiveDecks.Game.Model exposing (..) import MassiveDecks.Game.Player as Player import MassiveDecks.Game.Round as Round import MassiveDecks.Messages as Global -import MassiveDecks.Model exposing (..) -import MassiveDecks.Pages.Lobby.Configure.Model as Configure +import MassiveDecks.Pages.Lobby.Configure.Model as Configure exposing (Config) import MassiveDecks.Pages.Lobby.Messages as Lobby import MassiveDecks.Pages.Lobby.Model as Lobby import MassiveDecks.Strings as Strings @@ -33,7 +33,7 @@ init round pick = let cmd = round.players - |> playStylesGenerator (Card.slotCount round.call) + |> playStylesGenerator (Call.slotCount round.call) |> Random.generate (Game.SetPlayStyles >> lift) in ( { round | pick = pick } @@ -41,11 +41,11 @@ init round pick = ) -view : Shared -> Lobby.Auth -> List Configure.Deck -> Model -> Round.Playing -> RoundView Global.Msg -view shared auth decks model round = +view : Lobby.Auth -> Config -> Model -> Round.Playing -> RoundView Global.Msg +view auth config model round = let slots = - Card.slotCount round.call + Call.slotCount round.call missingFromPick = slots - (round.pick.cards |> List.length) @@ -68,7 +68,7 @@ view shared auth decks model round = hand = HtmlK.ul [ HtmlA.classList [ ( "hand", True ), ( "cards", True ), ( "not-playing", notPlaying ) ] ] - (model.hand |> List.map (round.pick.cards |> viewHandCard shared decks)) + (model.hand |> List.map (round.pick.cards |> viewHandCard config)) backgroundPlays = Html.div [ HtmlA.class "background-plays" ] @@ -94,17 +94,16 @@ view shared auth decks model round = {- Private -} -viewHandCard : Shared -> List Configure.Deck -> List Card.Id -> Card.Response -> ( String, Html Global.Msg ) -viewHandCard shared decks picked response = +viewHandCard : Config -> List Card.Id -> Card.Response -> ( String, Html Global.Msg ) +viewHandCard config picked response = ( response.details.id - , Card.view - shared - decks + , Response.view + config Card.Front [ HtmlA.classList [ ( "picked", List.member response.details.id picked ) ] , response.details.id |> Game.Pick |> lift |> HtmlE.onClick ] - (Card.R response) + response ) @@ -123,7 +122,8 @@ viewBackgroundPlay playStyles slots played for = viewBackgroundPlayCard : CardStyle -> Html msg viewBackgroundPlayCard playStyle = - Card.viewUnknownResponse [ "rotate(" ++ String.fromFloat playStyle.rotation ++ "turn)" |> HtmlA.style "transform" ] + Response.viewUnknown + [ "rotate(" ++ String.fromFloat playStyle.rotation ++ "turn)" |> HtmlA.style "transform" ] lift : Game.Msg -> Global.Msg diff --git a/client/src/elm/MassiveDecks/Game/Round/Revealing.elm b/client/src/elm/MassiveDecks/Game/Round/Revealing.elm index 1eeb216..d1ee668 100644 --- a/client/src/elm/MassiveDecks/Game/Round/Revealing.elm +++ b/client/src/elm/MassiveDecks/Game/Round/Revealing.elm @@ -1,24 +1,24 @@ module MassiveDecks.Game.Round.Revealing exposing (view) -import MassiveDecks.Card as Card +import MassiveDecks.Card.Call as Call import MassiveDecks.Card.Model as Card import MassiveDecks.Card.Play as Play exposing (Play) +import MassiveDecks.Card.Response as Response import MassiveDecks.Game.Messages exposing (..) import MassiveDecks.Game.Model exposing (RoundView) import MassiveDecks.Game.Player as Player import MassiveDecks.Game.Round as Round import MassiveDecks.Game.Round.Plays as Plays import MassiveDecks.Messages as Global -import MassiveDecks.Model exposing (..) -import MassiveDecks.Pages.Lobby.Configure.Model as Configure +import MassiveDecks.Pages.Lobby.Configure.Model as Configure exposing (Config) import MassiveDecks.Pages.Lobby.Messages as Lobby import MassiveDecks.Pages.Lobby.Model exposing (Auth) import MassiveDecks.Strings as Strings import MassiveDecks.Util.Maybe as Maybe -view : Shared -> Auth -> List Configure.Deck -> Round.Revealing -> RoundView Global.Msg -view shared auth decks round = +view : Auth -> Config -> Round.Revealing -> RoundView Global.Msg +view auth config round = let role = Player.role (Round.R round) auth.claims.uid @@ -32,10 +32,10 @@ view shared auth decks round = Strings.WaitingForCzarInstruction slots = - Card.slotCount round.call + Call.slotCount round.call plays = - round.plays |> List.map (playDetails shared decks slots (role == Player.RCzar)) + round.plays |> List.map (playDetails config slots (role == Player.RCzar)) -- TODO: Last revealed. lastRevealed = @@ -52,12 +52,12 @@ view shared auth decks round = {- Private -} -playDetails : Shared -> List Configure.Deck -> Int -> Bool -> Play -> Plays.Details Global.Msg -playDetails shared decks slots isCzar { id, responses } = +playDetails : Config -> Int -> Bool -> Play -> Plays.Details Global.Msg +playDetails config slots isCzar { id, responses } = let cards = responses - |> Maybe.map (List.map (\r -> Card.view shared decks Card.Front [] (Card.R r))) - |> Maybe.withDefault (List.repeat slots (Card.viewUnknownResponse [])) + |> Maybe.map (List.map (\r -> Response.view config Card.Front [] r)) + |> Maybe.withDefault (List.repeat slots (Response.viewUnknown [])) in Plays.Details id cards (Reveal id |> Lobby.GameMsg |> Global.LobbyMsg |> Maybe.justIf isCzar) [] diff --git a/client/src/elm/MassiveDecks/Models/Decoders.elm b/client/src/elm/MassiveDecks/Models/Decoders.elm index dfb56af..47936ed 100644 --- a/client/src/elm/MassiveDecks/Models/Decoders.elm +++ b/client/src/elm/MassiveDecks/Models/Decoders.elm @@ -21,7 +21,7 @@ module MassiveDecks.Models.Decoders exposing import Dict exposing (Dict) import Json.Decode as Json -import MassiveDecks.Card.Model as Card exposing (Card) +import MassiveDecks.Card.Model as Card exposing (Call, Response) import MassiveDecks.Card.Parts as Parts exposing (Part, Parts) import MassiveDecks.Card.Play as Play exposing (Play) import MassiveDecks.Card.Source.Cardcast.Model as Cardcast @@ -242,24 +242,23 @@ rando = player : Json.Decoder Player player = - Json.map2 Player - (Json.field "control" control) + Json.map Player (Json.field "score" score) -control : Json.Decoder Player.Control +control : Json.Decoder User.Control control = Json.string |> Json.andThen controlByName -controlByName : String -> Json.Decoder Player.Control +controlByName : String -> Json.Decoder User.Control controlByName name = case name of "Human" -> - Json.succeed Player.Human + Json.succeed User.Human "Computer" -> - Json.succeed Player.Computer + Json.succeed User.Computer _ -> unknownValue "user controller" name @@ -277,12 +276,13 @@ users = user : Json.Decoder User user = - Json.map5 User + Json.map6 User (Json.field "name" Json.string) (Json.field "presence" userPresence) (Json.field "connection" userConnection) (Json.field "privilege" privilege) (Json.field "role" userRole) + (Json.field "control" control) userConnection : Json.Decoder User.Connection @@ -423,8 +423,7 @@ eventByName name = connection User.Disconnected "Joined" -> - Json.field "name" Json.string - |> Json.andThen (\n -> Events.UserJoined { name = n } |> presence) + userJoined |> Json.andThen presence "Left" -> presence Events.UserLeft @@ -606,6 +605,14 @@ presence state = (Json.field "user" userId) +userJoined : Json.Decoder Events.PresenceState +userJoined = + Json.map3 (\n -> \p -> \c -> Events.UserJoined { name = n, privilege = p, control = c }) + (Json.field "name" Json.string) + (Json.maybe (Json.field "privilege" privilege) |> Json.map (Maybe.withDefault User.Unprivileged)) + (Json.maybe (Json.field "control" control) |> Json.map (Maybe.withDefault User.Human)) + + publicSet : Json.Decoder Events.ConfigChanged publicSet = Json.map (\public -> Events.PublicSet { public = public }) @@ -713,11 +720,6 @@ cardId = Json.string -card : Json.Decoder Card -card = - Json.oneOf [ call |> Json.map Card.C, response |> Json.map Card.R ] - - response : Json.Decoder Card.Response response = Json.map3 Card.response diff --git a/client/src/elm/MassiveDecks/Pages/Lobby.elm b/client/src/elm/MassiveDecks/Pages/Lobby.elm index f8953e6..f0466a1 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby.elm @@ -61,7 +61,7 @@ import Weightless.Attributes as WlA changeRoute : Route -> Model -> ( Model, Cmd Global.Msg ) changeRoute r model = - ( { model | route = r, lobby = Nothing, selectedUser = Nothing }, Cmd.none ) + ( { model | route = r, lobby = Nothing }, Cmd.none ) init : Shared -> Route -> Maybe Auth -> Route.Fork ( Model, Cmd Global.Msg ) @@ -80,7 +80,6 @@ init shared r auth = ( { route = r , auth = a , lobby = Nothing - , selectedUser = Nothing , configure = Configure.init , notificationId = 0 , notifications = [] @@ -112,9 +111,6 @@ subscriptions model = update : Msg -> Model -> ( Model, Cmd Global.Msg ) update msg model = case msg of - SelectUser user -> - ( { model | selectedUser = user }, Cmd.none ) - GameMsg gameMsg -> case model.lobby of Just l -> @@ -173,19 +169,21 @@ update msg model = let newUsers = case state of - Events.UserJoined { name } -> + Events.UserJoined { name, privilege, control } -> Dict.insert user { name = name , presence = User.Joined , connection = User.Connected - , privilege = User.Unprivileged + , privilege = privilege , role = User.Player + , control = control } lobby.users Events.UserLeft -> - Dict.remove user lobby.users + lobby.users + |> Dict.update user (Maybe.map (\u -> { u | presence = User.Left })) message = case state of @@ -199,11 +197,8 @@ update msg model = Events.PrivilegeSet { user, privilege } -> let - updateUser = - \id -> \u -> { u | privilege = privilege } |> Maybe.justIf (user == id) |> Maybe.withDefault u - users = - lobby.users |> Dict.map updateUser + lobby.users |> Dict.update user (Maybe.map (\u -> { u | privilege = privilege })) in ( { model | lobby = Just { lobby | users = users } }, Cmd.none ) @@ -236,7 +231,7 @@ update msg model = Util.lift (\notifications -> { model | notifications = notifications }) (NotificationMsg >> Global.LobbyMsg) - (Animated.update { removeDone = Just notificationAnimationDuration } notificationMsg model.notifications) + (Animated.update { removeDone = Just Animated.defaultDuration } notificationMsg model.notifications) ToggleInviteDialog -> ( { model | inviteDialogOpen = not model.inviteDialogOpen }, Cmd.none ) @@ -299,7 +294,7 @@ view shared model = ] :: HtmlK.ol [ HtmlA.class "notifications" ] notifications :: (model.lobby - |> Maybe.map (viewLobby shared model.configure model.auth model.selectedUser) + |> Maybe.map (viewLobby shared model.configure model.auth) |> Maybe.withDefault [ Html.div [ HtmlA.class "loading" ] [ Icon.viewStyled [ Icon.spin, Icon.fa3x ] Icon.circleNotch ] @@ -402,11 +397,6 @@ notificationDuration = 3500 -notificationAnimationDuration : Int -notificationAnimationDuration = - 200 - - applySync : Model -> Lobby -> Maybe (List Card.Response) -> Maybe (List Card.Id) -> ( Model, Cmd Global.Msg ) applySync model state hand pick = let @@ -500,8 +490,8 @@ username shared users id = |> Maybe.withDefault (Strings.UnknownUser |> Lang.string shared) -viewLobby : Shared -> Configure.Model -> Auth -> Maybe User.Id -> Lobby -> List (Html Global.Msg) -viewLobby shared configure auth selectedUser lobby = +viewLobby : Shared -> Configure.Model -> Auth -> Lobby -> List (Html Global.Msg) +viewLobby shared configure auth lobby = let game = lobby.game |> Maybe.map .game @@ -510,35 +500,59 @@ viewLobby shared configure auth selectedUser lobby = auth.claims.pvg == User.Privileged in [ Html.div [ HtmlA.id "lobby-content" ] - [ viewUsers shared auth.claims.uid selectedUser lobby.users game + [ viewUsers shared auth.claims.uid lobby.users game , Html.div [ HtmlA.id "scroll-frame" ] [ lobby.game - |> Maybe.map (Game.view shared auth lobby.config.decks lobby.users) + |> Maybe.map (Game.view shared auth lobby.config lobby.users) |> Maybe.withDefault (Configure.view shared privileged configure auth.claims.gc lobby lobby.config) ] ] ] -viewUsers : Shared -> User.Id -> Maybe User.Id -> Dict User.Id User -> Maybe Game -> Html Global.Msg -viewUsers shared player selectedUser users game = +viewUsers : Shared -> User.Id -> Dict User.Id User -> Maybe Game -> Html Global.Msg +viewUsers shared player users game = + let + ( active, inactive ) = + users |> Dict.toList |> List.partition (\( _, user ) -> user.presence == User.Joined) + + activeGroups = + active |> byRole |> List.map (viewRoleGroup shared player game) + + inactiveGroup = + if List.isEmpty inactive then + [] + + else + [ viewUserListGroup shared player game ( ( "left", Strings.Left ), inactive ) ] + + groups = + List.concat [ activeGroups, inactiveGroup ] + in Wl.card [ HtmlA.id "users" ] - [ Html.div [ HtmlA.class "collapsible" ] - [ users - |> Dict.toList - |> byRole - |> List.map (viewRoleGroup shared player selectedUser game) - |> HtmlK.ol [] - ] - ] + [ Html.div [ HtmlA.class "collapsible" ] [ HtmlK.ol [] groups ] ] -viewRoleGroup : Shared -> User.Id -> Maybe User.Id -> Maybe Game -> ( User.Role, List ( User.Id, User ) ) -> ( String, Html Global.Msg ) -viewRoleGroup shared player selectedUser game ( role, users ) = - ( role |> roleId - , Html.li [] - [ Html.span [] [ role |> roleDescription |> Lang.html shared ] - , HtmlK.ol [] (users |> List.map (viewUser shared player selectedUser game)) +viewRoleGroup : Shared -> User.Id -> Maybe Game -> ( User.Role, List ( User.Id, User ) ) -> ( String, Html Global.Msg ) +viewRoleGroup shared player game ( role, users ) = + let + idAndDescription = + case role of + User.Player -> + ( "players", Strings.Players ) + + User.Spectator -> + ( "spectators", Strings.Spectators ) + in + viewUserListGroup shared player game ( idAndDescription, users ) + + +viewUserListGroup : Shared -> User.Id -> Maybe Game -> ( ( String, MdString ), List ( User.Id, User ) ) -> ( String, Html Global.Msg ) +viewUserListGroup shared player game ( ( id, description ), users ) = + ( id + , Html.li [ HtmlA.class id ] + [ Html.span [] [ description |> Lang.html shared ] + , HtmlK.ol [] (users |> List.map (viewUser shared player game)) ] ) @@ -555,8 +569,8 @@ byRole users = |> List.filter (\( _, us ) -> not (List.isEmpty us)) -viewUser : Shared -> User.Id -> Maybe User.Id -> Maybe Game -> ( User.Id, User ) -> ( String, Html Global.Msg ) -viewUser shared player selectedUser game ( userId, user ) = +viewUser : Shared -> User.Id -> Maybe Game -> ( User.Id, User ) -> ( String, Html Global.Msg ) +viewUser shared player game ( userId, user ) = let ( secondary, score ) = userDetails shared game userId user @@ -565,18 +579,49 @@ viewUser shared player selectedUser game ( userId, user ) = "user-" ++ userId menuItems = - [ Menu.button Icon.userCog Strings.Promote Strings.Promote Nothing - , Menu.Separator - , Menu.button Icon.userClock Strings.SetAway Strings.SetAway Nothing - , Menu.button Icon.ban Strings.KickUser Strings.KickUser Nothing - ] + if user.control == User.Human then + let + privilegeMenuItems = + case user.privilege of + User.Unprivileged -> + Just [ Menu.button Icon.userPlus Strings.Promote Strings.Promote Nothing ] + + User.Privileged -> + Just [ Menu.button Icon.userMinus Strings.Demote Strings.Demote Nothing ] + + presenceMenuItems = + case user.presence of + User.Joined -> + Just + [ Menu.button Icon.userClock Strings.SetAway Strings.SetAway Nothing + , Menu.button Icon.ban Strings.KickUser Strings.KickUser Nothing + ] + + User.Left -> + Nothing + in + [ privilegeMenuItems, presenceMenuItems ] + |> List.filterMap identity + |> List.intersperse [ Menu.Separator ] + |> List.concat + + else + [] + + ( menu, clickable ) = + if List.isEmpty menuItems then + ( Html.nothing, HtmlA.nothing ) + + else + ( menuItems |> Menu.view shared id ( WlA.XRight, WlA.YCenter ) ( WlA.XLeft, WlA.YCenter ) + , WlA.clickable + ) in ( userId , Html.li [] [ Wl.listItem - [ Just userId |> SelectUser |> Global.LobbyMsg |> HtmlE.onClick - , HtmlA.classList [ ( "mdc-menu-surface--anchor", True ), ( "you", player == userId ) ] - , WlA.clickable + [ HtmlA.classList [ ( "you", player == userId ) ] + , clickable , HtmlA.id id ] [ Html.div [ HtmlA.class "user compressed-terms" ] @@ -585,7 +630,7 @@ viewUser shared player selectedUser game ( userId, user ) = ) , Html.span [ WlA.listItemSlot WlA.AfterItem ] score ] - , Menu.view shared id ( WlA.XRight, WlA.YCenter ) ( WlA.XLeft, WlA.YCenter ) menuItems + , menu ] ) @@ -601,7 +646,7 @@ userDetails shared game userId user = details = [ Strings.Privileged |> Maybe.justIf (user.privilege == User.Privileged) - , player |> Maybe.andThen (\p -> Strings.Ai |> Maybe.justIf (p.control == Player.Computer)) + , Strings.Ai |> Maybe.justIf (user.control == User.Computer) , round |> Maybe.andThen (\r -> Strings.Czar |> Maybe.justIf (Player.isCzar r userId)) , playStateDetail round userId ] @@ -635,26 +680,6 @@ viewDetails shared details = details |> List.filterMap identity |> List.map (Lang.html shared) |> List.intersperse (Html.text " ") -roleDescription : User.Role -> Strings.MdString -roleDescription role = - case role of - User.Player -> - Strings.Players - - User.Spectator -> - Strings.Spectators - - -roleId : User.Role -> String -roleId role = - case role of - User.Player -> - "players" - - User.Spectator -> - "spectators" - - viewCastButton : Auth -> List (Html.Attribute Global.Msg) -> List (Html Global.Msg) viewCastButton auth attrs = [ Components.iconButtonStyled diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Configure.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Configure.elm index e4a5ac8..d6e6dd3 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby/Configure.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Configure.elm @@ -142,7 +142,7 @@ view shared canEdit model gameCode lobby config = ] ] , Wl.tabGroup [ WlA.align WlA.Center ] (tabs |> List.map (tab shared model.tab)) - , tabContent shared canEdit model lobby config + , tabContent shared canEdit model config ] , Wl.card [] [ startGameSegment shared canEdit lobby config @@ -265,8 +265,11 @@ startGameProblems users config = if noDecks then [ Message.errorWithFix Strings.NeedAtLeastOneDeck - Strings.NoDecksHint - ("CAHBS" |> Cardcast.playCode |> Source.Cardcast |> AddDeck |> lift) + [ { description = Strings.NoDecksHint + , icon = Icon.plus + , action = "CAHBS" |> Cardcast.playCode |> Source.Cardcast |> AddDeck |> lift + } + ] |> Just ] @@ -283,15 +286,33 @@ startGameProblems users config = ] playerCount = - users |> Dict.values |> List.filter (\user -> user.role == User.Player) |> List.length + users + |> Dict.values + |> List.filter (\user -> user.role == User.Player && user.presence == User.Joined) + |> List.length + + aiPlayers = + config.rules.houseRules.rando |> Maybe.map .number |> Maybe.withDefault 0 playerIssues = [ Message.errorWithFix Strings.NeedAtLeastThreePlayers - Strings.Invite - (Lobby.ToggleInviteDialog |> Global.LobbyMsg) - -- TODO: 3! 2 for testing. - |> Maybe.justIf (playerCount < 2) + [ { description = Strings.Invite + , icon = Icon.bullhorn + , action = Lobby.ToggleInviteDialog |> Global.LobbyMsg + } + , { description = Strings.AddAnAiPlayer + , icon = Icon.robot + , action = + { number = 3 - playerCount + aiPlayers } + |> Just + |> Rules.RandoChange + |> HouseRuleChange Remote + |> Lobby.ConfigureMsg + |> Global.LobbyMsg + } + ] + |> Maybe.justIf (playerCount < 3) ] in [ deckIssues, playerIssues ] |> List.concat |> List.filterMap identity @@ -334,28 +355,32 @@ tabName target = Strings.ConfigurePrivacy -tabContent : Shared -> Bool -> Model -> Lobby -> Config -> Html Global.Msg -tabContent shared canEdit model lobby config = - case model.tab of - Decks -> - configureDecks shared canEdit model lobby config +tabContent : Shared -> Bool -> Model -> Config -> Html Global.Msg +tabContent shared canEdit model config = + let + viewTab = + case model.tab of + Decks -> + configureDecks - Rules -> - configureRules shared canEdit model lobby config + Rules -> + configureRules - Privacy -> - configureGameSettings shared canEdit model lobby config + Privacy -> + configureGameSettings + in + viewTab shared canEdit model config -configureRules : Shared -> Bool -> Model -> Lobby -> Config -> Html Global.Msg -configureRules shared canEdit model lobby config = +configureRules : Shared -> Bool -> Model -> Config -> Html Global.Msg +configureRules shared canEdit model config = Html.div [ HtmlA.class "rules" ] [ Html.div [ HtmlA.class "core-rules" ] [ Html.h3 [] [ Strings.GameRulesTitle |> Lang.html shared ] , handSize shared canEdit model config , scoreLimit shared canEdit model config ] - , houseRules shared canEdit model lobby config + , houseRules shared canEdit model config ] @@ -438,8 +463,8 @@ scoreLimit shared canEdit model config = [ Message.info Strings.ScoreLimitDescription ] -houseRules : Shared -> Bool -> Model -> Lobby -> Config -> Html Global.Msg -houseRules shared canEdit model lobby config = +houseRules : Shared -> Bool -> Model -> Config -> Html Global.Msg +houseRules shared canEdit model config = Html.div [ HtmlA.class "house-rules" ] [ Html.h3 [] [ Strings.HouseRulesTitle |> Lang.html shared ] , rando shared canEdit model config @@ -567,8 +592,8 @@ rebootSettings shared canEdit value localChange = ] -configureDecks : Shared -> Bool -> Model -> Lobby -> Config -> Html Global.Msg -configureDecks shared canEdit model lobby config = +configureDecks : Shared -> Bool -> Model -> Config -> Html Global.Msg +configureDecks shared canEdit model config = let hint = if canEdit then @@ -628,8 +653,8 @@ configureDecks shared canEdit model lobby config = ) -configureGameSettings : Shared -> Bool -> Model -> Lobby -> Config -> Html Global.Msg -configureGameSettings shared canEdit model lobby config = +configureGameSettings : Shared -> Bool -> Model -> Config -> Html Global.Msg +configureGameSettings shared canEdit model config = let passwordAttrs = List.concat diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Configure/Model.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Configure/Model.elm index bdaa619..963e9dc 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby/Configure/Model.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Configure/Model.elm @@ -4,6 +4,7 @@ module MassiveDecks.Pages.Lobby.Configure.Model exposing , DeckError , Model , Tab(..) + , fake ) import MassiveDecks.Card.Source.Model as Source @@ -50,3 +51,23 @@ type alias Config = , version : String , public : Bool } + + +{-| A fake configuration. +-} +fake : Config +fake = + { rules = + { handSize = 0 + , scoreLimit = Nothing + , houseRules = + { rando = Nothing + , packingHeat = Nothing + , reboot = Nothing + } + } + , decks = [] + , password = Nothing + , version = "" + , public = False + } diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm index 461f8f5..bb23e24 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm @@ -33,7 +33,7 @@ type Event {-| The user's intentional presence in the lobby. -} type PresenceState - = UserJoined { name : String } + = UserJoined { name : String, privilege : User.Privilege, control : User.Control } | UserLeft diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Messages.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Messages.elm index 1f6e0d9..9593032 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby/Messages.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Messages.elm @@ -6,12 +6,10 @@ import MassiveDecks.Models.MdError exposing (MdError) import MassiveDecks.Pages.Lobby.Configure.Messages as Configure import MassiveDecks.Pages.Lobby.Events exposing (Event) import MassiveDecks.Pages.Lobby.Model exposing (..) -import MassiveDecks.User as User exposing (User) type Msg - = SelectUser (Maybe User.Id) - | GameMsg Game.Msg + = GameMsg Game.Msg | EventReceived Event | ErrorReceived MdError | ConfigureMsg Configure.Msg diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Model.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Model.elm index fd3a6a9..3fee7a6 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby/Model.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Model.elm @@ -28,7 +28,6 @@ type alias Model = , auth : Auth , lobby : Maybe Lobby , configure : Configure.Model - , selectedUser : Maybe User.Id , notificationId : NotificationId , notifications : List (Animated Notification) , inviteDialogOpen : Bool diff --git a/client/src/elm/MassiveDecks/Pages/Spectate.elm b/client/src/elm/MassiveDecks/Pages/Spectate.elm index fd735e1..0731445 100644 --- a/client/src/elm/MassiveDecks/Pages/Spectate.elm +++ b/client/src/elm/MassiveDecks/Pages/Spectate.elm @@ -11,9 +11,11 @@ import FontAwesome.Solid as Icon import Html exposing (Html) import Html.Attributes as HtmlA import MassiveDecks.Card as Card -import MassiveDecks.Card.Model as Card exposing (Card) +import MassiveDecks.Card.Call as Call +import MassiveDecks.Card.Model as Card import MassiveDecks.Card.Parts as Parts import MassiveDecks.Card.Play as Play exposing (Play) +import MassiveDecks.Card.Response as Response import MassiveDecks.Card.Source.Cardcast.Model as Cardcast import MassiveDecks.Card.Source.Model as Source import MassiveDecks.Messages exposing (..) @@ -101,7 +103,7 @@ view shared model = turns 1 / toFloat playCount slots = - Card.slotCount model.call + Call.slotCount model.call qr = Route.externalUrl shared.origin (Route.Start { section = Start.Join (Just model.route.lobby.gameCode) }) @@ -112,10 +114,9 @@ view shared model = [ Html.div [ HtmlA.class "spectate" ] [ Html.div [ HtmlA.class "middle" ] - [ Card.view shared [] Card.Front [] (Card.C model.call) - - --, HtmlA.style "--play-count" (model.plays |> List.length |> String.fromInt) - , Html.ul [ HtmlA.class "plays" ] (viewPlays shared slots angle model.plays) + [ -- Call.view shared config Card.Front [] model.call + --, HtmlA.style "--play-count" (model.plays |> List.length |> String.fromInt) + Html.ul [ HtmlA.class "plays" ] (viewPlays shared slots angle model.plays) ] ] , Html.div [ HtmlA.class "join-info" ] @@ -282,7 +283,7 @@ viewPlayingPlay slots play = Just p -> List.map (\a -> - Card.viewUnknownResponse [ rotated a ] + Response.viewUnknown [ rotated a ] ) (p.animation |> Maybe.withDefault (defaultedRotations slots p.animation)) @@ -292,7 +293,8 @@ viewPlayingPlay slots play = viewKnownPlay : Shared -> Play.Known -> Rotations -> List (Html msg) viewKnownPlay shared play rotations = - List.map2 (\c -> \a -> Card.view shared [] Card.Front [ rotated a ] (Card.R c)) play.responses rotations + --List.map2 (\c -> \a -> Response.view config Card.Front [ rotated a ] c) play.responses rotations + [] defaultedRotations : Int -> Maybe Rotations -> Rotations diff --git a/client/src/elm/MassiveDecks/Pages/Spectate/Model.elm b/client/src/elm/MassiveDecks/Pages/Spectate/Model.elm index ef4ddf7..c0e2ff1 100644 --- a/client/src/elm/MassiveDecks/Pages/Spectate/Model.elm +++ b/client/src/elm/MassiveDecks/Pages/Spectate/Model.elm @@ -9,7 +9,7 @@ module MassiveDecks.Pages.Spectate.Model exposing , playingPlay ) -import MassiveDecks.Card.Model as Card exposing (Card) +import MassiveDecks.Card.Model as Card exposing (Call, Response) import MassiveDecks.Card.Play as Play import MassiveDecks.Pages.Spectate.Route exposing (..) import MassiveDecks.User as User diff --git a/client/src/elm/MassiveDecks/Pages/Start.elm b/client/src/elm/MassiveDecks/Pages/Start.elm index 8284359..132a1be 100644 --- a/client/src/elm/MassiveDecks/Pages/Start.elm +++ b/client/src/elm/MassiveDecks/Pages/Start.elm @@ -14,9 +14,10 @@ import Html exposing (Html) import Html.Attributes as HtmlA import Html.Events as HtmlE import Http -import MassiveDecks.Card as Card -import MassiveDecks.Card.Model as Card exposing (Card) +import MassiveDecks.Card.Call as Call +import MassiveDecks.Card.Model as Card import MassiveDecks.Card.Parts as Parts +import MassiveDecks.Card.Response as Response import MassiveDecks.Card.Source.Model as Source import MassiveDecks.Components.Form as Form import MassiveDecks.Components.Form.Message as Message @@ -25,6 +26,7 @@ import MassiveDecks.Icon as Icon import MassiveDecks.Messages as Global import MassiveDecks.Model exposing (..) import MassiveDecks.Models.MdError as MdError exposing (MdError) +import MassiveDecks.Pages.Lobby.Configure.Model as Configure import MassiveDecks.Pages.Lobby.GameCode as GameCode exposing (GameCode) import MassiveDecks.Pages.Lobby.Model as Lobby import MassiveDecks.Pages.Lobby.Token as Token @@ -146,10 +148,10 @@ view : Shared -> Model -> List (Html Global.Msg) view shared model = [ Html.div [ HtmlA.class "page start" ] [ Html.header [ HtmlA.class "title-card" ] - [ Html.h1 [] [ Html.div [ HtmlA.class "card-slicer" ] [ Card.viewUnknownCall [] ] ] + [ Html.h1 [] [ Html.div [ HtmlA.class "card-slicer" ] [ Call.viewUnknown [] ] ] , Html.div [ HtmlA.class "subtitle" ] [ Html.div [ HtmlA.class "card-slicer" ] - [ Card.view shared [] Card.Front [] (subtitleCard shared) + [ Response.view Configure.fake Card.Front [] (subtitleCard shared) ] ] ] @@ -233,9 +235,9 @@ loadingOrLoaded model = [ model.newLobbyRequest ] |> List.any HttpData.loadingOrLoaded -subtitleCard : Shared -> Card +subtitleCard : Shared -> Card.Response subtitleCard shared = - Card.response (Strings.ShortGameDescription |> Lang.string shared) "" Source.Fake |> Card.R + Card.response (Strings.ShortGameDescription |> Lang.string shared) "" Source.Fake gameCodeForRoute : Route -> Maybe GameCode @@ -487,7 +489,7 @@ aboutContent shared = , Html.p [] [ Strings.RulesPlaying |> html ] , Html.p [] [ Strings.RulesJudging |> html ] , Html.figure [ HtmlA.class "example-card" ] - [ Card.view shared [] Card.Front [] examplePick2 + [ Call.view shared Configure.fake Card.Front [] examplePick2 , Html.figcaption [] [ Strings.ExamplePickDescription |> html ] ] @@ -516,7 +518,7 @@ houseRule shared ( name, description ) = ] -examplePick2 : Card +examplePick2 : Card.Call examplePick2 = Card.call (Parts.unsafeFromList @@ -526,4 +528,3 @@ examplePick2 = ) "" Source.Fake - |> Card.C diff --git a/client/src/elm/MassiveDecks/Strings.elm b/client/src/elm/MassiveDecks/Strings.elm index 7319c6b..7cde2c3 100644 --- a/client/src/elm/MassiveDecks/Strings.elm +++ b/client/src/elm/MassiveDecks/Strings.elm @@ -122,6 +122,8 @@ type MdString | PlayersDescription -- A description of a group of users who are playing in the game. | Spectators -- A short term for a group of users who are only spectating the game. | SpectatorsDescription -- A description of a group of users who are only spectating the game. + | Left -- A short term for a group of users who have left the game. + | LeftDescription -- A description of a group of users who have left the game. | Privileged -- The short term for a player who has privileges over the game. | PrivilegedDescription -- A description of a player who has privileges over the game (e.g: can change settings) | Ai -- The short term for a player who is controlled by the computer. @@ -138,6 +140,7 @@ type MdString | EndGameDescription -- A description of the action of ending the game early. | KickUser -- A short term for the action of forcing a user to leave the game permanently. | Promote -- A short term for the action of allowing a user to edit the game configuration. + | Demote -- A short term for the action of no longer allowing a user to edit the game configuration. -- Notifications | UserJoined { username : String } -- A notification that a user has joined the game. | UserLeft { username : String } -- A notification that a user has left the game. @@ -164,6 +167,7 @@ type MdString | ScoreLimitDescription -- The description of the above rule. | NeedAtLeastOneDeck -- A description of the problem that the game needs at least one deck to start. | NeedAtLeastThreePlayers -- A description of the problem that the game needs at least three players to start. + | AddAnAiPlayer -- A description of adding an AI player to the game. | PasswordShared -- A warning that game passwords are visible to anyone else in the game. | PasswordNotSecured -- A warning that game passwords are not stored securely and should not be used elsewhere. | LobbyPassword -- A short label for the lobby password. diff --git a/client/src/elm/MassiveDecks/Strings/Languages/En.elm b/client/src/elm/MassiveDecks/Strings/Languages/En.elm index 918d025..8474a58 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/En.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/En.elm @@ -238,7 +238,7 @@ translate mdString = ] HouseRuleRandoCardrissianNumber -> - [ Text "Number of Randos" ] + [ Text "AI Players" ] HouseRuleRandoCardrissianNumberDescription -> [ Text "The number of AI players that will be in the game." ] @@ -444,6 +444,12 @@ translate mdString = SpectatorsDescription -> [ Text "Users watching the game without playing." ] + Left -> + [ Text "Left" ] + + LeftDescription -> + [ Text "Users who have left the game." ] + Privileged -> [ Text "Owner" ] @@ -495,6 +501,9 @@ translate mdString = Promote -> [ Text "Promote" ] + Demote -> + [ Text "Demote" ] + -- Notifications UserConnected { username } -> [ Text username, Text " has reconnected to the game." ] @@ -582,6 +591,9 @@ translate mdString = NeedAtLeastThreePlayers -> [ Text "You need at least three players to start the game." ] + AddAnAiPlayer -> + [ Text "Add an AI player to the game." ] + PasswordShared -> [ Text "Anyone in the game can see the password! " , Text "Hiding it above only affects you (useful if streaming, etc…)." diff --git a/client/src/elm/MassiveDecks/Strings/Render.elm b/client/src/elm/MassiveDecks/Strings/Render.elm index 49e2aa4..81470d6 100644 --- a/client/src/elm/MassiveDecks/Strings/Render.elm +++ b/client/src/elm/MassiveDecks/Strings/Render.elm @@ -117,7 +117,7 @@ enhanceHtml context mdString unenhanced = GameCodeTerm -> term context GameCodeDescription Icon.qrcode unenhanced - GameCode { code } -> + GameCode _ -> [ Html.span [ HtmlA.class "game-code", GameCodeSpecificDescription |> asString context |> HtmlA.title ] unenhanced @@ -138,6 +138,9 @@ enhanceHtml context mdString unenhanced = Spectators -> term context SpectatorsDescription Icon.eye unenhanced + Left -> + term context LeftDescription Icon.signOutAlt unenhanced + Privileged -> term context PrivilegedDescription Icon.userCog unenhanced diff --git a/client/src/elm/MassiveDecks/User.elm b/client/src/elm/MassiveDecks/User.elm index d5e5900..f5cc8b9 100644 --- a/client/src/elm/MassiveDecks/User.elm +++ b/client/src/elm/MassiveDecks/User.elm @@ -1,5 +1,6 @@ module MassiveDecks.User exposing ( Connection(..) + , Control(..) , Id , Presence(..) , Privilege(..) @@ -59,6 +60,13 @@ type Connection | Disconnected +{-| How the user is being controlled. +-} +type Control + = Human + | Computer + + {-| A request to register a new user in a lobby. -} type alias Registration = @@ -75,4 +83,5 @@ type alias User = , connection : Connection , privilege : Privilege , role : Role + , control : Control } diff --git a/client/src/scss/animation.scss b/client/src/scss/animation.scss index d6c8a7f..1055603 100644 --- a/client/src/scss/animation.scss +++ b/client/src/scss/animation.scss @@ -1,3 +1,5 @@ +// If the duration is changed, sync to Animated.elm + .animation { transition: transform 200ms ease-in, opacity 200ms ease-in; opacity: 0; diff --git a/client/src/scss/massive-decks.scss b/client/src/scss/massive-decks.scss index 89581f6..0adb7b3 100644 --- a/client/src/scss/massive-decks.scss +++ b/client/src/scss/massive-decks.scss @@ -23,6 +23,10 @@ body { min-width: $min-page-width; } +#game { + background: $background url(../../assets/images/background.svg) repeat; +} + .game-code-input { --input-font-family: "Source Code Pro", monospace; } @@ -193,8 +197,8 @@ a, display: flex; align-items: center; - .svg-inline--fa { - margin-right: 0.3em; + .message-type-icon { + margin: 0.3em; } } @@ -205,6 +209,8 @@ a, .inline-error { @include message; color: $error; + background-color: lighten($error, 55%); + border-radius: 0.5em; } .warning { @@ -212,6 +218,20 @@ a, color: #6f6401; } +.fixes { + display: inline-block; + padding: 0; + margin: 0 0 0 auto; + + li { + display: inline-block; + + wl-button { + margin: 0; + } + } +} + .remove-from-flow { position: absolute; } diff --git a/client/src/scss/pages/lobby.scss b/client/src/scss/pages/lobby.scss index b5f0624..2c98b22 100644 --- a/client/src/scss/pages/lobby.scss +++ b/client/src/scss/pages/lobby.scss @@ -121,6 +121,10 @@ li { margin-top: 0.5em; } + + .left { + opacity: 0.3; + } } @media screen and (max-width: $users-width * 2) { diff --git a/server/.eslintrc.json b/server/.eslintrc.json index 8dfa7c5..9806931 100644 --- a/server/.eslintrc.json +++ b/server/.eslintrc.json @@ -32,6 +32,8 @@ "allowTypedFunctionExpressions": true, "allowHigherOrderFunctions": true } - ] + ], + + "@typescript-eslint/no-empty-interface": "off" } } diff --git a/server/package-lock.json b/server/package-lock.json index 83a5ea6..9fd5d8c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -5,18 +5,18 @@ "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", "dev": true, "requires": { "@babel/highlight": "^7.0.0" } }, "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", + "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", "dev": true, "requires": { "chalk": "^2.0.0", @@ -154,6 +154,12 @@ "@types/express": "*" } }, + "@types/json-schema": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", + "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", + "dev": true + }, "@types/json-stable-stringify": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/@types/json-stable-stringify/-/json-stable-stringify-1.0.32.tgz", @@ -237,9 +243,9 @@ "dev": true }, "@types/uuid": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.4.tgz", - "integrity": "sha512-tPIgT0GUmdJQNSHxp0X2jnpQfBSTfGxUMc/2CXBU2mnyTFVYVa2ojpoQ74w0U2yn2vw3jnC640+77lkFFpdVDw==", + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.5.tgz", + "integrity": "sha512-MNL15wC3EKyw1VLF+RoVO4hJJdk9t/Hlv3rt1OL65Qvuadm4BYo6g9ZJQqoq7X8NBFSsQXgAujWciovh2lpVjA==", "dev": true, "requires": { "@types/node": "*" @@ -262,12 +268,12 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.11.0.tgz", - "integrity": "sha512-mXv9ccCou89C8/4avKHuPB2WkSZyY/XcTQUXd5LFZAcLw1I3mWYVjUu6eS9Ja0QkP/ClolbcW9tb3Ov/pMdcqw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.13.0.tgz", + "integrity": "sha512-WQHCozMnuNADiqMtsNzp96FNox5sOVpU8Xt4meaT4em8lOG1SrOv92/mUbEHQVh90sldKSfcOc/I0FOb/14G1g==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "1.11.0", + "@typescript-eslint/experimental-utils": "1.13.0", "eslint-utils": "^1.3.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^2.0.1", @@ -275,31 +281,32 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.11.0.tgz", - "integrity": "sha512-7LbfaqF6B8oa8cp/315zxKk8FFzosRzzhF8Kn/ZRsRsnpm7Qcu25cR/9RnAQo5utZ2KIWVgaALr+ZmcbG47ruw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz", + "integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "1.11.0", + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "1.13.0", "eslint-scope": "^4.0.0" } }, "@typescript-eslint/parser": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.11.0.tgz", - "integrity": "sha512-5xBExyXaxVyczrZvbRKEXvaTUFFq7gIM9BynXukXZE0zF3IQP/FxF4mPmmh3gJ9egafZFqByCpPTFm3dk4SY7Q==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.13.0.tgz", + "integrity": "sha512-ITMBs52PCPgLb2nGPoeT4iU3HdQZHcPaZVw+7CsFagRJHUhyeTgorEwHXhFf3e7Evzi8oujKNpHc8TONth8AdQ==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "1.11.0", - "@typescript-eslint/typescript-estree": "1.11.0", + "@typescript-eslint/experimental-utils": "1.13.0", + "@typescript-eslint/typescript-estree": "1.13.0", "eslint-visitor-keys": "^1.0.0" } }, "@typescript-eslint/typescript-estree": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.11.0.tgz", - "integrity": "sha512-fquUHF5tAx1sM2OeRCC7wVxFd1iMELWMGCzOSmJ3pLzArj9+kRixdlC4d5MncuzXpjEqc6045p3KwM0o/3FuUA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz", + "integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==", "dev": true, "requires": { "lodash.unescape": "4.0.1", @@ -330,9 +337,9 @@ } }, "acorn": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", - "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.1.tgz", + "integrity": "sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q==", "dev": true }, "acorn-jsx": { @@ -342,9 +349,9 @@ "dev": true }, "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -871,12 +878,6 @@ "text-hex": "1.0.x" } }, - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true - }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -912,9 +913,9 @@ } }, "content-security-policy-builder": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.0.0.tgz", - "integrity": "sha512-j+Nhmj1yfZAikJLImCvPJFE29x/UuBi+/MWqggGGc515JKaZrjuei2RhULJmy0MsstW3E3htl002bwmBNMKr7w==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz", + "integrity": "sha512-/MtLWhJVvJNkA9dVLAp6fg9LxD2gfI6R2Fi1hPmfjYXSahJJzcfvoeDOxSyp4NvxMuwWv3WMssE9o31DoULHrQ==" }, "content-type": { "version": "1.0.4", @@ -1067,9 +1068,9 @@ } }, "dns-prefetch-control": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.1.0.tgz", - "integrity": "sha1-YN20V3dOF48flBXwyrsOhbCzALI=" + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.2.0.tgz", + "integrity": "sha512-hvSnros73+qyZXhHFjx2CMLwoj3Fe7eR9EJsFsqmcI1bB2OBWL/+0YzaEaKssCHnj/6crawNnUyw74Gm2EKe+Q==" }, "doctrine": { "version": "3.0.0", @@ -1081,9 +1082,9 @@ } }, "dont-sniff-mimetype": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz", - "integrity": "sha1-WTKJDcn04vGeXrAqIAJuXl78j1g=" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.1.0.tgz", + "integrity": "sha512-ZjI4zqTaxveH2/tTlzS1wFp+7ncxNZaIEWYg3lzZRHkKf5zPT/MnEG6WL0BhHMJUabkh8GeU5NL5j+rEUCb7Ug==" }, "dot-prop": { "version": "4.2.0", @@ -1157,9 +1158,9 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.0.1.tgz", - "integrity": "sha512-DyQRaMmORQ+JsWShYsSg4OPTjY56u1nCjAmICrE8vLWqyLKxhFXOthwMj1SA8xwfrv0CofLNVnqbfyhwCkaO0w==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.1.0.tgz", + "integrity": "sha512-QhrbdRD7ofuV09IuE2ySWBz0FyXCq0rriLTZXZqaWSI79CVtHVRdkFuFTViiqzZhkCgfOh9USpriuGN2gIpZDQ==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -1168,7 +1169,7 @@ "cross-spawn": "^6.0.5", "debug": "^4.0.1", "doctrine": "^3.0.0", - "eslint-scope": "^4.0.3", + "eslint-scope": "^5.0.0", "eslint-utils": "^1.3.1", "eslint-visitor-keys": "^1.0.0", "espree": "^6.0.0", @@ -1176,30 +1177,37 @@ "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^3.1.0", + "glob-parent": "^5.0.0", "globals": "^11.7.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^6.2.2", + "inquirer": "^6.4.1", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.3.0", - "lodash": "^4.17.11", + "lodash": "^4.17.14", "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", "optionator": "^0.8.2", "progress": "^2.0.0", "regexpp": "^2.0.1", - "semver": "^5.5.1", - "strip-ansi": "^4.0.0", - "strip-json-comments": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", "table": "^5.2.3", - "text-table": "^0.2.0" + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" }, "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -1211,6 +1219,14 @@ "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } } }, "debug": { @@ -1222,11 +1238,57 @@ "ms": "^2.1.1" } }, + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "glob-parent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", + "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, "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 + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true } } }, @@ -1259,10 +1321,13 @@ } }, "eslint-utils": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", - "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", - "dev": true + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.0.tgz", + "integrity": "sha512-7ehnzPaP5IIEh1r1tkjuIrxqhNkzUJa9z3R92tLJdZIVdWaczEhr3EbhGtsMrVxi1KeR8qA7Off6SWc5WNQqyQ==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.0.0" + } }, "eslint-visitor-keys": { "version": "1.0.0", @@ -1420,9 +1485,9 @@ "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==" }, "express-winston": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/express-winston/-/express-winston-3.2.0.tgz", - "integrity": "sha512-AcVSV016U7aPs1Uknoil962SU124UgNkdakP564X6w+lH0oS1FhV3uctUj2/dOog7np4uvsAHhBmSoICjZMDmA==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/express-winston/-/express-winston-3.2.1.tgz", + "integrity": "sha512-yh49mpFcoLAFs4yuplIutTAqs+jgRQYB2bLoDTpMvQDARzQKHvABfj2PYMPfmk087+OsEBJmm96i6//q9RJnnw==", "requires": { "chalk": "^2.4.1", "lodash": "^4.17.10" @@ -1458,9 +1523,9 @@ } }, "external-editor": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", - "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "dev": true, "requires": { "chardet": "^0.7.0", @@ -2406,25 +2471,25 @@ "integrity": "sha512-dEHCG2LraR6PNvSGxosZHIRgxF5sNLOIBFEHbj8lfP9WWmu/PWPMzsip1drdVSOFi51N2pU7gZavrgn7sbGFuw==" }, "helmet": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.18.0.tgz", - "integrity": "sha512-TsKlGE5UVkV0NiQ4PllV9EVfZklPjyzcMEMjWlyI/8S6epqgRT+4s4GHVgc25x0TixsKvp3L7c91HQQt5l0+QA==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.20.0.tgz", + "integrity": "sha512-Ob+TqmQFZ5f7WgP8kBbAzNPsbf6p1lOj5r+327/ymw/IILWih3wcx9u/u/S8Mwv5wbBkO7Li6x5s23t3COhUKw==", "requires": { "depd": "2.0.0", - "dns-prefetch-control": "0.1.0", - "dont-sniff-mimetype": "1.0.0", + "dns-prefetch-control": "0.2.0", + "dont-sniff-mimetype": "1.1.0", "expect-ct": "0.2.0", "feature-policy": "0.3.0", "frameguard": "3.1.0", - "helmet-crossdomain": "0.3.0", - "helmet-csp": "2.7.1", - "hide-powered-by": "1.0.0", + "helmet-crossdomain": "0.4.0", + "helmet-csp": "2.8.0", + "hide-powered-by": "1.1.0", "hpkp": "2.0.0", "hsts": "2.2.0", "ienoopen": "1.1.0", "nocache": "2.1.0", "referrer-policy": "1.2.0", - "x-xss-protection": "1.1.0" + "x-xss-protection": "1.2.0" }, "dependencies": { "depd": { @@ -2435,25 +2500,25 @@ } }, "helmet-crossdomain": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/helmet-crossdomain/-/helmet-crossdomain-0.3.0.tgz", - "integrity": "sha512-YiXhj0E35nC4Na5EPE4mTfoXMf9JTGpN4OtB4aLqShKuH9d2HNaJX5MQoglO6STVka0uMsHyG5lCut5Kzsy7Lg==" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz", + "integrity": "sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA==" }, "helmet-csp": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.7.1.tgz", - "integrity": "sha512-sCHwywg4daQ2mY0YYwXSZRsgcCeerUwxMwNixGA7aMLkVmPTYBl7gJoZDHOZyXkqPrtuDT3s2B1A+RLI7WxSdQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.8.0.tgz", + "integrity": "sha512-MlCPeM0Sm3pS9RACRihx70VeTHmkQwa7sum9EK1tfw1VZyvFU0dBWym9nHh3CRkTRNlyNm/WFCMvuh9zXkOjNw==", "requires": { "camelize": "1.0.0", - "content-security-policy-builder": "2.0.0", + "content-security-policy-builder": "2.1.0", "dasherize": "2.0.0", "platform": "1.3.5" } }, "hide-powered-by": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.0.0.tgz", - "integrity": "sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys=" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.1.0.tgz", + "integrity": "sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg==" }, "hpkp": { "version": "2.0.0", @@ -2518,9 +2583,9 @@ "dev": true }, "import-fresh": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz", - "integrity": "sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", + "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", "dev": true, "requires": { "parent-module": "^1.0.0", @@ -2561,9 +2626,9 @@ "dev": true }, "inquirer": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.4.1.tgz", - "integrity": "sha512-/Jw+qPZx4EDYsaT6uz7F4GJRNFMRdKNeUZw3ZnKV8lyuUgz/YWRCSUAJMZSVhSq4Ec0R2oYnyi6b3d4JXcL5Nw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.0.tgz", + "integrity": "sha512-scfHejeG/lVZSpvCXpsB4j/wQNPM5JC8kiElOI0OUTwmc1RTpXr4H32/HOlQHcZiYl2z2VElwuCVDRG8vFmbnA==", "dev": true, "requires": { "ansi-escapes": "^3.2.0", @@ -2572,7 +2637,7 @@ "cli-width": "^2.0.0", "external-editor": "^3.0.3", "figures": "^2.0.0", - "lodash": "^4.17.11", + "lodash": "^4.17.12", "mute-stream": "0.0.7", "run-async": "^2.2.0", "rxjs": "^6.4.0", @@ -2587,6 +2652,12 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -2976,9 +3047,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash.includes": { "version": "4.3.0", @@ -3175,9 +3246,9 @@ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "requires": { "for-in": "^1.0.2", @@ -3954,9 +4025,9 @@ "dev": true }, "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dev": true, "requires": { "extend-shallow": "^2.0.1", @@ -4269,13 +4340,13 @@ } }, "table": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.1.tgz", - "integrity": "sha512-E6CK1/pZe2N75rGZQotFOdmzWQ1AILtgYbMAbAjvms0S1l5IDB47zG3nCnFGB/w+7nB3vKofbLXCH7HPBo864w==", + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.4.tgz", + "integrity": "sha512-IIfEAUx5QlODLblLrGTTLJA7Tk0iLSGBvgY8essPRVNGHAzThujww1YqHLs6h3HfTg55h++RzLHH5Xw/rfv+mg==", "dev": true, "requires": { - "ajv": "^6.9.1", - "lodash": "^4.17.11", + "ajv": "^6.10.2", + "lodash": "^4.17.14", "slice-ansi": "^2.1.0", "string-width": "^3.0.0" }, @@ -4286,6 +4357,12 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -4410,18 +4487,6 @@ "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, - "ts-json-schema-generator": { - "version": "0.50.2", - "resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-0.50.2.tgz", - "integrity": "sha512-S6n8kHpU2y7IJvr07rA2Xn9wbKRWTs31jvgpFQnkWn4n7jWXOj/yGEzp1UN/qQOBB0r3qSZjsOMr2KzuaSWTGA==", - "dev": true, - "requires": { - "commander": "~2.20.0", - "glob": "~7.1.4", - "json-stable-stringify": "^1.0.1", - "typescript": "~3.5.2" - } - }, "ts-postgres": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ts-postgres/-/ts-postgres-1.1.0.tgz", @@ -4454,9 +4519,9 @@ "dev": true }, "tsutils": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.14.0.tgz", - "integrity": "sha512-SmzGbB0l+8I0QwsPgjooFRaRvHLBLNYM8SeQ0k6rtNDru5sCGeLJcZdwilNndN+GysuFjF5EIYgN8GfFG6UeUw==", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.14.1.tgz", + "integrity": "sha512-kiuZzD1uUA5DxGj/uxbde+ymp6VVdAxdzOIlAFbYKrPyla8/uiJ9JLBm1QsPhOm4Muj0/+cWEDP99yoCUcSl6Q==", "dev": true, "requires": { "tslib": "^1.8.1" @@ -4481,9 +4546,9 @@ } }, "typescript": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", - "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", + "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", "dev": true }, "typescript-json-schema": { @@ -4543,38 +4608,15 @@ } }, "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } + "set-value": "^2.0.1" } }, "unique-string": { @@ -4705,6 +4747,12 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" }, + "v8-compile-cache": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", + "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", + "dev": true + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -4856,9 +4904,9 @@ "integrity": "sha1-fnLj+6I+D/Zp3dU34chX09SxYU8=" }, "x-xss-protection": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.1.0.tgz", - "integrity": "sha512-rx3GzJlgEeZ08MIcDsU2vY2B1QEriUKJTSiNHHUIem6eg9pzVOr2TL3Y4Pd6TMAM5D5azGjcxqI62piITBDHVg==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.2.0.tgz", + "integrity": "sha512-xN0kV+8XfOQM2OPPBdEbGtbvJNNP1pvZR7sE6d44cjJFQG4OiGDdienPg5iOUGswBTiGbBvtYDURd30BMJwwqg==" }, "xdg-basedir": { "version": "3.0.0", diff --git a/server/package.json b/server/package.json index 60ffe32..faa5615 100644 --- a/server/package.json +++ b/server/package.json @@ -25,16 +25,16 @@ "config.json5" ], "dependencies": { - "ajv": "^6.10.0", + "ajv": "^6.10.2", "axios": "^0.19.0", "body-parser": "^1.19.0", "express": "^4.17.1", "express-async-errors": "^3.1.1", - "express-winston": "^3.2.0", + "express-winston": "^3.2.1", "express-ws": "^4.0.0", "generic-pool": "^3.7.1", "hashids": "^1.2.2", - "helmet": "^3.18.0", + "helmet": "^3.20.0", "http-status-codes": "^1.3.2", "json5": "^2.1.0", "jsonwebtoken": "^8.5.1", @@ -55,16 +55,16 @@ "@types/json5": "0.0.30", "@types/jsonwebtoken": "^8.3.2", "@types/source-map-support": "^0.5.0", - "@types/uuid": "^3.4.4", + "@types/uuid": "^3.4.5", "@types/wu": "^2.1.39", - "@typescript-eslint/eslint-plugin": "^1.11.0", - "@typescript-eslint/parser": "^1.11.0", - "eslint": "^6.0.1", + "@typescript-eslint/eslint-plugin": "^1.13.0", + "@typescript-eslint/parser": "^1.13.0", + "eslint": "^6.1.0", "eslint-config-prettier": "^6.0.0", "eslint-plugin-prettier": "^3.1.0", "nodemon": "^1.19.1", "prettier": "1.18.2", - "typescript": "^3.5.2", + "typescript": "^3.5.3", "typescript-json-validator": "^2.2.2" } } diff --git a/server/src/ts/action/privileged/configure/change-house-rule.ts b/server/src/ts/action/privileged/configure/change-house-rule.ts index 5842128..55f2522 100644 --- a/server/src/ts/action/privileged/configure/change-house-rule.ts +++ b/server/src/ts/action/privileged/configure/change-house-rule.ts @@ -4,6 +4,7 @@ import * as rules from "../../../games/rules"; import { Handler } from "../../handler"; import * as configure from "../configure"; import * as houseRuleChanged from "../../../events/lobby-event/configured/house-rule-changed"; +import * as rando from "../../../games/rules/rando"; /** * Set the hand size for the lobby. @@ -24,24 +25,40 @@ export const is = (action: Action): action is ChangeHouseRule => action.action === name; export const handle: Handler = (auth, lobby, action) => { - const houseRules = lobby.config.rules.houseRules; + const hr = lobby.config.rules.houseRules; + const events = []; + let changed = false; switch (action.change.houseRule) { case "PackingHeat": - houseRules.packingHeat = action.change.settings; + if (hr.packingHeat !== action.change.settings) { + hr.packingHeat = action.change.settings; + changed = true; + } break; case "Rando": - houseRules.rando = action.change.settings; + const userEvents = rando.change(lobby, hr.rando, action.change.settings); + if (userEvents !== null) { + events.push(...userEvents); + changed = true; + } break; case "Reboot": - houseRules.reboot = action.change.settings; + if (hr.reboot !== action.change.settings) { + hr.reboot = action.change.settings; + changed = true; + } break; } - lobby.config.version += 1; - - return { - lobby, - events: [ + if (changed) { + lobby.config.version += 1; + events.push( event.targetAll(houseRuleChanged.of(action.change, lobby.config.version)) - ] - }; + ); + return { + lobby, + events + }; + } else { + return {}; + } }; diff --git a/server/src/ts/events/game-event/game-started.ts b/server/src/ts/events/game-event/game-started.ts index 20ceba6..8bd8cd1 100644 --- a/server/src/ts/events/game-event/game-started.ts +++ b/server/src/ts/events/game-event/game-started.ts @@ -1,4 +1,5 @@ import * as card from "../../games/cards/card"; +import * as round from "../../games/game/round"; import * as publicRound from "../../games/game/round/public"; /** @@ -11,10 +12,10 @@ export interface GameStarted { } export const of = ( - round: publicRound.Playing, + startedRound: round.Playing, hand?: card.Response[] ): GameStarted => ({ event: "GameStarted", - round, + round: round.censor(startedRound), ...(hand !== undefined ? { hand } : {}) }); diff --git a/server/src/ts/events/game-event/round-started.ts b/server/src/ts/events/game-event/round-started.ts index e4741c0..f33e85f 100644 --- a/server/src/ts/events/game-event/round-started.ts +++ b/server/src/ts/events/game-event/round-started.ts @@ -1,6 +1,6 @@ import * as card from "../../games/cards/card"; +import { Round } from "../../games/game/round"; import * as user from "../../user"; -import * as round from "../../games/game/round"; /** * Indicates a new round has started. @@ -14,17 +14,11 @@ export interface RoundStarted { drawn?: card.Response[]; } -export const of = ( - id: round.Id, - czar: user.Id, - players: user.Id[], - call: card.Call, - drawn?: card.Response[] -): RoundStarted => ({ +export const of = (round: Round, drawn?: card.Response[]): RoundStarted => ({ event: "RoundStarted", - id: id.toString(), - czar, - players, - call, + id: round.id.toString(), + czar: round.czar, + players: Array.from(round.players.keys()), + call: round.call, ...(drawn === undefined ? {} : { drawn }) }); diff --git a/server/src/ts/events/lobby-event/presence-changed.ts b/server/src/ts/events/lobby-event/presence-changed.ts index 4d4b06e..2823840 100644 --- a/server/src/ts/events/lobby-event/presence-changed.ts +++ b/server/src/ts/events/lobby-event/presence-changed.ts @@ -1,3 +1,4 @@ +import { User } from "../../user"; import * as user from "../../user"; /** @@ -15,12 +16,16 @@ interface Base { export interface Joined extends Base { event: "Joined"; name: user.Name; + privilege?: user.Privilege; + control?: user.Control; } -export const joined = (user: user.Id, name: user.Name): Joined => ({ +export const joined = (id: user.Id, user: User): Joined => ({ event: "Joined", - user, - name + user: id, + name: user.name, + ...(user.privilege !== "Unprivileged" ? { privilege: user.privilege } : {}), + ...(user.control !== "Human" ? { control: user.control } : {}) }); /** diff --git a/server/src/ts/games/game.ts b/server/src/ts/games/game.ts index c9f30c6..cbb4cff 100644 --- a/server/src/ts/games/game.ts +++ b/server/src/ts/games/game.ts @@ -1,7 +1,16 @@ +import * as event from "../event"; +import * as gameStarted from "../events/game-event/game-started"; +import * as playSubmitted from "../events/game-event/play-submitted"; +import * as roundStarted from "../events/game-event/round-started"; +import { ServerState } from "../server-state"; +import * as finishedPlaying from "../timeout/finished-playing"; +import { User } from "../user"; import * as user from "../user"; import * as util from "../util"; +import * as card from "./cards/card"; import { Decks } from "./cards/decks"; import * as decks from "./cards/decks"; +import * as play from "./cards/play"; import * as round from "./game/round"; import { Round } from "./game/round"; import * as publicRound from "./game/round/public"; @@ -10,6 +19,8 @@ import * as player from "./player"; import { Player } from "./player"; import * as rules from "./rules"; import { Rules } from "./rules"; +import * as lobby from "../lobby"; +import * as timeout from "../timeout"; import wu from "wu"; /** @@ -45,7 +56,6 @@ export const censor: (game: Game) => Public = game => ({ const newPlayerForUser = (decks: Decks, rules: Rules): Player => ({ hand: decks.responses.draw(rules.handSize), - control: "Human", score: 0 }); @@ -79,8 +89,96 @@ export const start = ( }; }; -export const nextCzar = (game: Game): user.Id => { - const current = game.round.czar; - const index = game.playerOrder.findIndex(id => id === current) + 1; - return game.playerOrder[index >= game.playerOrder.length ? 0 : index]; +export const atStartOfRound = ( + server: ServerState, + first: boolean, + game: Game & { round: round.Playing } +): { + game: Game & { round: round.Playing }; + events?: Iterable; + timeouts?: Iterable; +} => { + const slotCount = card.slotCount(game.round.call); + + const events = []; + if ( + slotCount > 2 || + (slotCount === 2 && game.rules.houseRules.packingHeat !== undefined) + ) { + const responseDeck = game.decks.responses; + const drawnByPlayer = new Map(); + for (const [id, playerState] of game.players) { + if (player.role(game, id) === "Player") { + const drawn = responseDeck.draw(slotCount - 1); + drawnByPlayer.set(id, { drawn }); + playerState.hand.push(...drawn); + } + } + if (!first) { + events.push( + event.additionally(roundStarted.of(game.round), drawnByPlayer) + ); + } + } else { + if (!first) { + events.push(event.targetAll(roundStarted.of(game.round))); + } + } + + if (first) { + events.push( + event.playerSpecificAddition( + gameStarted.of(game.round), + (id, user, player) => ({ + hand: player.hand + }) + ) + ); + } + + const ais = game.rules.houseRules.rando.current; + for (const ai of ais) { + const player = game.players.get(ai) as Player; + const plays = game.round.plays; + const playId = play.id(); + plays.push({ + id: playId, + play: player.hand.slice(0, slotCount), + playedBy: ai, + revealed: false + }); + events.push(event.targetAll(playSubmitted.of(ai))); + } + + const timeouts = []; + const timeout = finishedPlaying.ifNeeded(game.round); + if (timeout !== undefined) { + timeouts.push({ + timeout: timeout, + after: server.config.timeouts.finishedPlayingDelay + }); + } + + return { game, events, timeouts }; +}; + +const canBeCzar = (user: User): boolean => + user.control !== "Computer" && + user.presence === "Joined" && + user.role === "Player"; + +const czarAfter = (lobby: lobby.WithActiveGame, nextIndex: number): user.Id => { + const game = lobby.game; + const potentialCzar = + game.playerOrder[nextIndex >= game.playerOrder.length ? 0 : nextIndex]; + return canBeCzar(lobby.users.get(potentialCzar) as User) + ? potentialCzar + : czarAfter(lobby, nextIndex + 1); +}; + +export const nextCzar = (lobby: lobby.WithActiveGame): user.Id => { + const game = lobby.game; + const current = game.round.czar; + const nextIndex = game.playerOrder.findIndex(id => id === current) + 1; + return czarAfter(lobby, nextIndex); }; diff --git a/server/src/ts/games/player.ts b/server/src/ts/games/player.ts index 94e2740..824a59b 100644 --- a/server/src/ts/games/player.ts +++ b/server/src/ts/games/player.ts @@ -7,7 +7,6 @@ import { Game } from "./game"; */ export interface Player { hand: Hand; - control: Control; score: Score; } @@ -15,7 +14,6 @@ export interface Player { * A player containing only state all users can see. */ export interface Public { - control: Control; score: Score; } @@ -24,11 +22,6 @@ export interface Public { */ export type Role = "Czar" | "Player"; -/** - * Who controls the player. - */ -export type Control = "Human" | "Computer"; - /** * How many points the player has scored. * @TJS-type integer @@ -40,7 +33,6 @@ export type Score = number; * Produce a public version of the given player. */ export const censor = (player: Player): Public => ({ - control: player.control, score: player.score }); diff --git a/server/src/ts/games/rules.ts b/server/src/ts/games/rules.ts index ff50ffb..cd3e603 100644 --- a/server/src/ts/games/rules.ts +++ b/server/src/ts/games/rules.ts @@ -1,4 +1,9 @@ -/** The rules for a standard game.*/ +import { HouseRules } from "./rules/houseRules"; +import * as houseRules from "./rules/houseRules"; +import * as rando from "./rules/rando"; + +/** The rules for a standard game. + */ export interface Rules { /** * The number of cards in each player's hand. @@ -21,13 +26,7 @@ export interface Rules { export interface Public { handSize: number; scoreLimit?: number; - houseRules: HouseRules; -} - -export interface HouseRules { - packingHeat?: PackingHeat; - reboot?: Reboot; - rando?: Rando; + houseRules: houseRules.Public; } /** @@ -36,13 +35,12 @@ export interface HouseRules { export const create = (): Rules => ({ handSize: 10, scoreLimit: 25, - houseRules: {} + houseRules: houseRules.create() }); /** * Configuration for the "Packing Heat" house rule. */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PackingHeat {} /** @@ -60,18 +58,9 @@ export interface Reboot { cost: number; } -export interface Rando { - /** - * The number of AI players to add to the game. - * @TJS-type integer - * @minimum 1 - * @maximum 10 - */ - number: number; -} - export const censor = (rules: Rules): Public => ({ - ...rules + ...rules, + houseRules: houseRules.censor(rules.houseRules) }); export interface ChangeBase { @@ -80,7 +69,7 @@ export interface ChangeBase { } export type ChangePackingHeat = ChangeBase<"PackingHeat", PackingHeat>; -export type ChangeRando = ChangeBase<"Rando", Rando>; +export type ChangeRando = ChangeBase<"Rando", rando.Public>; export type ChangeReboot = ChangeBase<"Reboot", Reboot>; export type Change = ChangePackingHeat | ChangeRando | ChangeReboot; diff --git a/server/src/ts/games/rules/houseRules.ts b/server/src/ts/games/rules/houseRules.ts new file mode 100644 index 0000000..c7990c4 --- /dev/null +++ b/server/src/ts/games/rules/houseRules.ts @@ -0,0 +1,30 @@ +import { PackingHeat, Reboot, Rules } from "../rules"; +import { Rando } from "./rando"; +import * as rando from "./rando"; + +/** + * Non-standard rules that can be applied to a game. + */ +export interface HouseRules { + packingHeat?: PackingHeat; + reboot?: Reboot; + rando: Rando; +} + +/** + * The public view of the internal model. + */ +export interface Public { + packingHeat?: PackingHeat; + reboot?: Reboot; + rando?: rando.Public; +} + +export const create = (): HouseRules => ({ + rando: rando.create() +}); + +export const censor = (houseRules: HouseRules): Public => ({ + ...houseRules, + rando: rando.censor(houseRules.rando) +}); diff --git a/server/src/ts/games/rules/rando.ts b/server/src/ts/games/rules/rando.ts new file mode 100644 index 0000000..83d7129 --- /dev/null +++ b/server/src/ts/games/rules/rando.ts @@ -0,0 +1,149 @@ +import { RegisterUser } from "../../action/initial/register-user"; +import * as event from "../../event"; +import * as presenceChanged from "../../events/lobby-event/presence-changed"; +import { Lobby } from "../../lobby"; +import * as lobby from "../../lobby"; +import { User } from "../../user"; +import * as util from "../../util"; +import * as user from "../../user"; + +/** + * The maximum number of AI players allowed in a single game. + */ +const max = 10; + +/** + * The default name for an AI. + */ +export const aiName = "Rando Cardrissian"; + +/** + * Apart from the one inherited from Cards against Humanity (a reference to + * Star Wars), we use some references to famous AIs and computers from films + * and games. + */ +export const aiNames = new Set([ + "HAL 9000", // 2001: A Space Odyssey + "GLaDOS", // Portal + "Wheatley", // Portal + "TEC-XX", // Paper Mario + "EDI", // Mass Effect + "343 Guilty Spark", // Halo + "Cortana", // Halo + "J.A.R.V.I.S.", // MCU + "Deep Thought", // HHGTTG + "Gibson", // Hackers + "Skynet", // Terminator + "Project 2501", // GITS + "SHODAN", // System Shock + "Mr. House" // Fallout: New Vegas +]); + +/** + * The internal model of the Rando house rule. + */ +export interface Rando { + /** + * The ids of active AI players in the game. + */ + current: user.Id[]; + /** + * The names or ids of potential AI players that are not currently active in + * the game. + */ + unused: (user.Id | RegisterUser)[]; +} + +/** + * Create the initial model. + */ +export const create = (): Rando => ({ + current: [], + unused: [aiName] + .concat(util.shuffled(aiNames).slice(0, max - 1)) + .map(name => ({ name })) +}); + +/** + * The public view of the Rando house rule. + */ +export interface Public { + /** + * The number of AI players to add to the game. + * @TJS-type integer + * @minimum 1 + * @maximum 10 + */ + number: number; +} + +/** + * Get the public view of the given internal model. + */ +export const censor = (rando: Rando): Public | undefined => + rando.current.length > 0 ? { number: rando.current.length } : undefined; + +const isId = (ai: user.Id | RegisterUser): ai is user.Id => + typeof ai === "string"; + +export const createIfNeeded = ( + inLobby: Lobby, + ai: user.Id | RegisterUser +): { user: user.Id; events: Iterable } => { + if (isId(ai)) { + return { + user: ai, + events: [ + event.targetAll( + presenceChanged.joined(ai, inLobby.users.get(ai) as User) + ) + ] + }; + } else { + return lobby.addUser(inLobby, ai, user => ({ + ...user, + control: "Computer" + })); + } +}; + +/** + * Change the internal model to match the given public representation. + */ +export function* change( + inLobby: Lobby, + config: Rando, + changeTo?: Public +): Iterable | null { + const want = changeTo !== undefined ? changeTo.number : 0; + const have = config.current.length; + const eventsCollection = []; + if (want === have) { + return null; + } + if (want > have) { + const toAdd = want - have; + const added = config.unused + .splice(0, toAdd) + .map(ai => createIfNeeded(inLobby, ai)); + for (const { user, events } of added) { + const userData = inLobby.users.get(user) as User; + userData.presence = "Joined"; + config.current.push(user); + eventsCollection.push(...events); + } + } else if (have > want) { + const toRemove = have - want; + const removed = config.current.splice( + config.current.length - toRemove, + toRemove + ); + for (const ai of removed) { + const user = inLobby.users.get(ai) as User; + user.presence = "Left"; + eventsCollection.push(event.targetAll(presenceChanged.left(ai))); + } + config.unused.splice(0, 0, ...removed); + } + yield* eventsCollection; +} diff --git a/server/src/ts/index.ts b/server/src/ts/index.ts index b08a019..6f5f060 100644 --- a/server/src/ts/index.ts +++ b/server/src/ts/index.ts @@ -126,7 +126,7 @@ async function main(): Promise { return { change: { lobby, - events: [event.targetAll(presenceChanged.joined(id, newUser.name))], + events: [event.targetAll(presenceChanged.joined(id, newUser))], timeouts: [ { timeout: userDisconnect.of(id), diff --git a/server/src/ts/lobby.ts b/server/src/ts/lobby.ts index 1aff53c..9a94a39 100644 --- a/server/src/ts/lobby.ts +++ b/server/src/ts/lobby.ts @@ -1,4 +1,7 @@ import { CreateLobby } from "./action/initial/create-lobby"; +import { RegisterUser } from "./action/initial/register-user"; +import * as event from "./event"; +import * as presenceChanged from "./events/lobby-event/presence-changed"; import * as game from "./games/game"; import { Game } from "./games/game"; import * as rules from "./games/rules"; @@ -8,7 +11,6 @@ import { GameCode } from "./lobby/game-code"; import * as user from "./user"; import { User } from "./user"; import * as util from "./util"; -import * as token from "./user/token"; /** * A game lobby. @@ -132,3 +134,22 @@ export const censor = (lobby: Lobby): Public => ({ config: config.censor(lobby.config), ...(lobby.game === undefined ? {} : { game: game.censor(lobby.game) }) }); + +export const addUser = ( + lobby: Lobby, + registration: RegisterUser, + change?: (user: User) => User +): { + user: user.Id; + events: Iterable; +} => { + const newUser = user.create(registration); + const changedUser = change === undefined ? newUser : change(newUser); + const id = lobby.nextUserId.toString(); + lobby.nextUserId += 1; + lobby.users.set(id, changedUser); + return { + user: id, + events: [event.targetAll(presenceChanged.joined(id, changedUser))] + }; +}; diff --git a/server/src/ts/task.ts b/server/src/ts/task.ts index 93e5592..5891da0 100644 --- a/server/src/ts/task.ts +++ b/server/src/ts/task.ts @@ -15,12 +15,16 @@ export abstract class TaskBase implements Task { } protected abstract async begin(server: ServerState): Promise; - protected abstract resolve(lobby: Lobby, work: T): Change; + protected abstract resolve( + lobby: Lobby, + work: T, + server: ServerState + ): Change; public async handle(server: ServerState): Promise { const work = await this.begin(server); await change.apply(server, this.gameCode, lobby => - this.resolve(lobby, work) + this.resolve(lobby, work, server) ); } } diff --git a/server/src/ts/task/start-game.ts b/server/src/ts/task/start-game.ts index bf7ac1c..18b377f 100644 --- a/server/src/ts/task/start-game.ts +++ b/server/src/ts/task/start-game.ts @@ -1,11 +1,8 @@ import wu from "wu"; -import * as event from "../event"; -import * as gameStarted from "../events/game-event/game-started"; import * as decks from "../games/cards/decks"; import * as source from "../games/cards/source"; import * as sources from "../games/cards/sources"; import * as game from "../games/game"; -import * as round from "../games/game/round"; import { Lobby } from "../lobby"; import { Change } from "../lobby/change"; import { GameCode } from "../lobby/game-code"; @@ -28,20 +25,23 @@ export class StartGame extends task.TaskBase { ); } - protected resolve(lobby: Lobby, work: decks.Templates[]): Change { + protected resolve( + lobby: Lobby, + work: decks.Templates[], + server: ServerState + ): Change { if (lobby.game !== undefined) { return {}; } const lobbyGame = game.start(work, lobby.users.keys(), lobby.config.rules); - const gameRound = round.censor(lobbyGame.round); - const baseEvent = gameStarted.of(gameRound); - const events = [ - event.playerSpecificAddition(baseEvent, (id, user, player) => ({ - hand: player.hand - })) - ]; - lobby.game = lobbyGame; - return { lobby, events }; + + const atStartOfRound = game.atStartOfRound(server, true, lobbyGame); + lobby.game = atStartOfRound.game; + return { + lobby, + events: atStartOfRound.events, + timeouts: atStartOfRound.timeouts + }; } // This is super unlikely timing-wise, and if it happens, the user just has diff --git a/server/src/ts/timeout/round-start.ts b/server/src/ts/timeout/round-start.ts index 50374f7..e47a195 100644 --- a/server/src/ts/timeout/round-start.ts +++ b/server/src/ts/timeout/round-start.ts @@ -1,10 +1,9 @@ import wu from "wu"; -import * as event from "../event"; -import * as roundStarted from "../events/game-event/round-started"; +import { Game } from "../games/game"; import * as game from "../games/game"; +import * as lobby from "../lobby"; +import { Playing } from "../games/game/round"; import * as timeout from "../timeout"; -import * as card from "../games/cards/card"; -import * as player from "../games/player"; /** * Indicates that the round should start if it is still appropriate to do so. @@ -21,15 +20,14 @@ export const handle: timeout.Handler = ( server, timeout, gameCode, - lobby + inLobby ) => { - const lobbyGame = lobby.game; - if (lobbyGame !== undefined) { + if (lobby.hasActiveGame(inLobby)) { + const lobbyGame = inLobby.game; const gameRound = lobbyGame.round; if (gameRound.stage === "Complete") { - const czar = game.nextCzar(lobbyGame); + const czar = game.nextCzar(inLobby); const [call] = lobbyGame.decks.calls.replace(gameRound.call); - const slotCount = card.slotCount(call); const roundId = gameRound.id + 1; const playersInRound = new Set( wu(lobbyGame.playerOrder).filter(id => id !== czar) @@ -37,37 +35,24 @@ export const handle: timeout.Handler = ( lobbyGame.decks.responses.discard( gameRound.plays.flatMap(play => play.play) ); - lobbyGame.round = { - stage: "Playing", - id: roundId, - czar: czar, - players: playersInRound, - call: call, - plays: [] - }; - const playersArray = Array.from(playersInRound); - const baseEvent = roundStarted.of(roundId, czar, playersArray, call); - - let events; - if ( - slotCount > 2 || - (slotCount === 2 && - lobbyGame.rules.houseRules.packingHeat !== undefined) - ) { - const responseDeck = lobbyGame.decks.responses; - const drawnByPlayer = new Map(); - for (const [id, playerState] of lobbyGame.players) { - if (player.role(lobbyGame, id) === "Player") { - const drawn = responseDeck.draw(slotCount - 1); - drawnByPlayer.set(id, { drawn }); - playerState.hand.push(...drawn); - } + const updatedGame: Game & { round: Playing } = { + ...lobbyGame, + round: { + stage: "Playing", + id: roundId, + czar: czar, + players: playersInRound, + call: call, + plays: [] } - events = [event.additionally(baseEvent, drawnByPlayer)]; - } else { - events = [event.targetAll(baseEvent)]; - } - return { lobby, events }; + }; + const atStartOfRound = game.atStartOfRound(server, false, updatedGame); + inLobby.game = atStartOfRound.game; + return { + inLobby, + events: atStartOfRound.events, + timeouts: atStartOfRound.timeouts + }; } } return {}; diff --git a/server/src/ts/user.ts b/server/src/ts/user.ts index 0537d3b..47d5242 100644 --- a/server/src/ts/user.ts +++ b/server/src/ts/user.ts @@ -6,6 +6,7 @@ export interface User { presence: Presence; connection: Connection; privilege: Privilege; + control: Control; role: Role; } @@ -17,6 +18,7 @@ export interface Public { presence: Presence; connection: Connection; privilege: Privilege; + control: Control; role: Role; } @@ -52,6 +54,11 @@ export type Connection = "Connected" | "Disconnected"; */ export type Role = "Spectator" | "Player"; +/** + * Who/what is controlling the player—a human or the computer? + */ +export type Control = "Human" | "Computer"; + /** * If the user is playing. */ @@ -69,26 +76,21 @@ export const isSpectating: (user: User) => boolean = user => * @param registration The details of the user to create. * @param privilege The level of privilege the user has. */ -export function create( +export const create = ( registration: RegisterUser, privilege: Privilege = "Unprivileged" -): User { - return { - name: registration.name, - presence: "Joined", - connection: "Connected", - privilege: privilege, - role: "Player" - }; -} +): User => ({ + name: registration.name, + presence: "Joined", + connection: "Connected", + privilege: privilege, + control: "Human", + role: "Player" +}); /** * Gives a version of the user with only publicly visible properties. */ export const censor: (user: User) => Public = user => ({ - name: user.name, - presence: user.presence, - connection: user.connection, - privilege: user.privilege, - role: user.role + ...user });