diff --git a/package-lock.json b/package-lock.json index 3ca3f6a..52577f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,37 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "express": "^4.18.2" + "express": "^4.18.2", + "socket.io": "^4.7.2", + "socket.io-client": "^4.7.2" }, "devDependencies": { "nodemon": "^3.0.1" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.4.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.6.tgz", + "integrity": "sha512-q0RkvNgMweWWIvSMDiXhflGUKMdIxBo2M2tYM/0kEGDueQByFzK4KZAgu5YHGFNxziTlppNpTIBcqHQAxlfHdA==" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -57,6 +82,14 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -196,6 +229,18 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -234,6 +279,96 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2.tgz", + "integrity": "sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz", + "integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -662,6 +797,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -883,6 +1026,120 @@ "node": ">=10" } }, + "node_modules/socket.io": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", + "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dependencies": { + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-client": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", + "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -977,6 +1234,34 @@ "node": ">= 0.8" } }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -985,6 +1270,29 @@ } }, "dependencies": { + "@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "20.4.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.6.tgz", + "integrity": "sha512-q0RkvNgMweWWIvSMDiXhflGUKMdIxBo2M2tYM/0kEGDueQByFzK4KZAgu5YHGFNxziTlppNpTIBcqHQAxlfHdA==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1021,6 +1329,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1124,6 +1437,15 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1152,6 +1474,75 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, + "engine.io": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2.tgz", + "integrity": "sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "engine.io-client": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.2.tgz", + "integrity": "sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1466,6 +1857,11 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -1624,6 +2020,93 @@ "semver": "^7.5.3" } }, + "socket.io": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", + "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-adapter": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "requires": { + "ws": "~8.11.0" + } + }, + "socket.io-client": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz", + "integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1691,6 +2174,17 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "requires": {} + }, + "xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 29e89f5..f1daf71 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ }, "homepage": "https://github.com/jacob-medina/totk-bingo#readme", "dependencies": { - "express": "^4.18.2" + "express": "^4.18.2", + "socket.io": "^4.7.2", + "socket.io-client": "^4.7.2" }, "devDependencies": { "nodemon": "^3.0.1" diff --git a/public/assets/css/style.css b/public/assets/css/style.css index c6e69a6..3bf049a 100644 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -29,8 +29,11 @@ --bg-highlight-light: rgba(255,255,255,0.15); --bg-highlight-dark: rgba(223, 209, 186, 0.241); - --green-dark: rgba(42, 138, 92, 0.5); - --green-light: rgba(35, 205, 123, 0.3); + --green-dark: #39a372; + --green-light: #23cd7b; + + --orange-dark: #a6691f; + --orange-light: #d9901a; --bg-img-dark: url("../images/background-dark.png") repeat; --bg-img-light: url("../images/background-light.png") repeat center/70%; @@ -92,6 +95,7 @@ body { --highlight: var(--highlight-light); --bg-highlight: var(--bg-highlight-light); --green: var(--green-dark); + --orange: var(--orange-dark); --bg-img: var(--bg-img-dark); background: var(--bg-img); @@ -106,6 +110,7 @@ body[data-color-mode="light"] { --highlight: var(--highlight-dark); --bg-highlight: var(--bg-highlight-dark); --green: var(--green-light); + --orange: var(--orange-light); --bg-img: var(--bg-img-light); } @@ -221,8 +226,13 @@ input[type="text"]:is(:focus, :hover) ::placeholder { padding: 0.5rem; } -.text-icon-btn:hover, -.icon-btn:hover, +.text-icon-btn[disabled], +.icon-btn[disabled] { + opacity: 60%; +} + +.text-icon-btn:hover:not([disabled]), +.icon-btn:hover:not([disabled]), div:has( > input[type="checkbox"]:hover) label { color: var(--highlight); border-color: var(--highlight); @@ -435,14 +445,34 @@ h1 { color: var(--highlight); } -.board-item:hover:not(.done) { +.board-item:hover:not(:is(.done-1, .done-2)) { background-color: var(--bg-highlight); } -.done, -.board-item:active:not(.done) { - background-color: var(--green); +:is(.done-1, .done-2) { backdrop-filter: blur(3px); + position: relative; + isolation: isolate; +} + +:is(.done-1, .done-2)::before { + content: ""; + + position: absolute; + width: 100%; + height: 100%; + + z-index: -1; + opacity: 80%; +} + +.done-1::before { +/* .board-item:active:not(.done) { */ + background-color: var(--green); +} + +.done-2::before { + background-color: var(--orange); } .board-item p { @@ -523,6 +553,47 @@ footer { } +/* RACE */ +.race-menu :is(h2, h3) { + font-family: "Hylia Serif"; +} + +.player-container { + gap: 2rem; +} + +.player-container .material-symbols-outlined { + font-size: 3rem; +} + +.player-container p { + margin: 0; + + font-family: "Hylia Serif"; + font-size: 2rem; +} + +.player-container .ready-icon { + font-size: 2rem; +} + +/* .player-container .ready-icon[data-ready=true] { + color: var(--green); +} */ + +.player-1 { + color: var(--green); +} + +.player-2 { + color: var(--orange); +} + +.player-container .ready-icon { + color: var(--text-color); + /* opacity: 80%; */ +} + /* MEDIA QUERIES */ /* @media screen and (min-width: 680px) { diff --git a/public/assets/modules/board.js b/public/assets/modules/board.js new file mode 100644 index 0000000..da56cac --- /dev/null +++ b/public/assets/modules/board.js @@ -0,0 +1,185 @@ +import { MagicSquare } from "./MagicSquare.js"; +import { challengeTypes, challTypeWeightReduce, weightedChallengeTypes } from "./ChallengeType.js"; +import { isBetween, clamp, hideElement, showElement } from "./helper.js"; +import { getWeightedRandom } from "./WeightedValue.js"; +import { uniqueEquipment } from "./uniqueEpuipment.js"; +import { fetchData } from "./fetchData.js"; +import { defaultBoardSize, defaultDiffMultiplier } from "./options.js"; +import { titleCase, simplifyName, getPlural, romanNumeral } from "./formatName.js"; + +let diffMultiplier = defaultDiffMultiplier; + +async function fetchAndGenerateBoard(searchParams, rand) { + const data = await fetchData(); + + const size = Number(searchParams.get('boardSize') ?? defaultBoardSize); + diffMultiplier = Number(searchParams.get('difficulty') ?? defaultDiffMultiplier); + + const entries = createChallenges(size, data, searchParams, rand); + generateBingoBoard(size, entries); + + endLoading(); +} + + +// create challenges based on magic square difficulty +function createChallenges(size, data, searchParams, rand) { + const magicSquare = new MagicSquare(size); + const entries = []; + + let excludeChallTypes = []; + if (searchParams.get("exclude")) { + excludeChallTypes = searchParams.get('exclude').split(','); // get different challenge types + excludeChallTypes = excludeChallTypes.map(el => el.split('-').join(" ")); // replace dashes with spaces in name + } + + // get all entries for challenge types + challengeTypes.forEach(type => type.getEntries(data)); + + // choose a random entry for each difficulty + for (const difficulty of magicSquare._matrix.flat()) { + // adjust difficulty by diffMultiplier + const adjustedDiff = clamp(Math.round(difficulty * diffMultiplier), 0, 23); + + // get challenge types with current difficulty in their range + const validChallengeTypes = weightedChallengeTypes.filter(challengeType => { + challengeType = challengeType.value; + if (excludeChallTypes.includes(challengeType.name)) return false; + return isBetween(adjustedDiff, challengeType.diffMin, challengeType.diffMax); + }); + + if (validChallengeTypes.length < 1) return []; + + const randChallengeType = getWeightedRandom(validChallengeTypes, rand); + randChallengeType.weight = Math.round(randChallengeType.weight * challTypeWeightReduce * 100) / 100; // reduce change to get same challenge type again + + // TODO: get unique entries + let randEntry = randChallengeType.getRandomEntry(rand); + randEntry = structuredClone(randEntry); + randEntry.challengeType = randChallengeType.name; + randEntry.difficulty = adjustedDiff; + randEntry.amount = randChallengeType.calcAmount(adjustedDiff); + + entries.push(randEntry); + } + + return entries; +} + + +function generateBingoBoard(size=5, entries) { + $('.bingo-board').text(''); // clear board + + if (entries.length < size * size) { + console.error('Could not get enough entries!'); + const errorMessage = $('
Could not get enough challenges!
') + $('.bingo-board').append(errorMessage); + return; + } + + $('.bingo-board').css('grid-template-columns', `repeat(${size}, ${100/size}%)`); + $('.bingo-board').css('grid-template-rows', `repeat(${size}, ${100/size}%)`); + + for (let i=0; i < size*size; i++) { + const entry = entries[i]; + + const boardItem = $(`
`); + boardItem.data('entry', entry); + + const challengeText = $('

'); + const itemText = $('

'); + const stats = $('

'); + const difficultyText = $('

'); + + const challenge = getChallenge(entry); + challengeText.text(challenge.challenge); + itemText.text(challenge.entry); + difficultyText.text(entry.difficulty); + + stats.append(difficultyText); + boardItem.append(challengeText, itemText); + $('.bingo-board').append(boardItem); + } +} + + +// returns challenge text appropriate to the type of entry +function getChallenge({category, id, name, edible, amount, challengeType}) { + // normal main quests + if (category === "main quests") return {challenge: `main quest:`, entry: `'${name}'`}; + + // normal side adventues + if (category === "side adventures") return {challenge: `side adventure:`, entry: `'${name}'`}; + + // normal side quests + if (category === "side quests") return {challenge: `side quest:`, entry: `'${name}'`}; + + // normal shrine quests + if (category === "shrine quests") return {challenge: `shrine quest:`, entry: `'${name}'`}; + + // normal armor sets + if (category === "armor sets") return {challenge: `obtain the`, entry: `${name} Set`}; + + // unique bosses + if (id === 200) return {challenge: "defeat", entry: titleCase("demon king ganondorf")}; + if ((id >= 191 && id <= 201) || id === 165) return {challenge: "defeat", entry: titleCase(name)}; + + // unique misc items + if (name === 'autobuild') return {challenge: "obtain", entry: titleCase(name)}; // autobuild + + // unique creatures + if (name === 'patricia') return {challenge: "feed", entry: titleCase(name)}; + if (name === 'dondon') return {challenge: "feed a", entry: titleCase(name)}; + if (id >= 2 && id <= 5) return {challenge: "ride the", entry: titleCase(name)}; // unique horses + if (id === 159) return {challenge: "defeat a", entry: titleCase(name)}; // training construct + if ((id >= 188 && id <= 190) || id === 202) return {challenge: "ride", entry: titleCase(name)}; // dragons + + if (uniqueEquipment.includes(name)) return {challenge: "obtain the", entry: titleCase(name)}; + + // armor + if (challengeType === 'armor ★') return {challenge: "obtain the", entry: `${titleCase(name)}★`}; + if (category === "armor") return {challenge: "obtain the", entry: titleCase(name)}; + + // let amount = //rand.randInt(5) + 1; + name = getPlural(amount, simplifyName(name, id)); + name = titleCase(romanNumeral(name)); + + const vowels = ['a','e','i','o','u']; + if (amount === 1) amount = "a" + (vowels.includes(name.charAt(0).toLowerCase()) ? "n" : ""); + + if (id === 504) return {challenge: `open ${amount}`, entry: titleCase(name)}; // treasure chest + if (id === 509 || id === -13) return {challenge: `discover ${amount}`, entry: titleCase(name)}; // wells, caves + if (id >= 505 && id <= 508) return {challenge: `mine ${amount}`, entry: titleCase(name)}; // ore deposits + if (id === 72) return {challenge: `collect ${amount}`, entry: titleCase(name)}; // fairies + if ([1, 6, 8].includes(id)) return {challenge: `ride ${amount}`, entry: titleCase(name)}; // horses, sand seals + if (id === 7) return {challenge: `find ${amount}`, entry: titleCase(name)}; // donkeys + if ([14, 18, 19, 29].includes(id)) return {challenge: `feed ${amount}`, entry: titleCase(name)}; // white goats, hateno cows, hylian retriever + if (id === 52) return {challenge: `anger ${amount}`, entry: titleCase(name)}; // coocoos + if ([-8, -10, -17].includes(id)) return {challenge: `collect ${amount}`, entry: titleCase(name)}; // sage's will, yiga schematic, korok seeds + if ([-12, -14, -15].includes(id)) return {challenge: `activate ${amount}`, entry: titleCase(name)}; // towers, cherry blossoms, lightroot + if (id === -9) return {challenge: `install ${amount}`, entry: titleCase(name)}; // Hudson signs + if (category === "shrines") return {challenge: `complete ${amount}`, entry: titleCase(name)}; // shrines + + // normal materials & equipment + if (category === "materials" || category === "equipment") return {challenge: `collect ${amount}`, entry: name}; + + // normal creatures + if (category === "creatures" && edible === true) return {challenge: `collect ${amount}`, entry: name}; + if (category === "creatures") return {challenge: `hunt ${amount}`, entry: name}; + + // normal monsters + if (category === "monsters") return {challenge: `kill ${amount}`, entry: name}; + + return {challenge: "", entry: name} +} + + +function endLoading() { + $('.title-container').css('animation-name', 'loading-end'); + $('.title').attr('data-loading', 'false'); + + showElement('main'); + showElement('footer'); +} + +export { fetchAndGenerateBoard }; \ No newline at end of file diff --git a/public/assets/modules/options.js b/public/assets/modules/options.js index 61dbfdf..1dd401d 100644 --- a/public/assets/modules/options.js +++ b/public/assets/modules/options.js @@ -8,6 +8,7 @@ const defaultDiffMultiplier = 1.0; let boardSize = defaultBoardSize; let diffMultiplier = defaultDiffMultiplier; +let excludeItems = []; function handleRandomSeedBtn(reload=true) { @@ -70,34 +71,55 @@ function resetOptions() { function setBoardSize(val) { - boardSize = val; + boardSize = Math.floor(Number(val)); } function setDifficulty(val) { - diffMultiplier = val; + diffMultiplier = parseFloat(val).toFixed(1); } function setBoardSizeValue(val) { - if (val && val.type !== 'input') { - setBoardSize(val); - $('#board-size-range').val(boardSize); - } - else setBoardSize($('#board-size-range').val()); + setBoardSize(val); + $('#board-size-range').val(boardSize); const text = boardSize + 'x' + boardSize; $('.board-size').text(text); } function setDifficultyValue(val) { - if (val && val.type !== 'input') { - setDifficulty(val); - $('#difficulty-range').val(diffMultiplier); - } - else setDifficulty( parseFloat($('#difficulty-range').val()).toFixed(1) ); + setDifficulty(val); + $('#difficulty-range').val(val); $('.difficulty').text(diffMultiplier); } +function getExcludeArray(searchParams, spaces=false) { + const exclude = searchParams.get("exclude"); + let excludeTypes = []; + if (exclude) { + excludeTypes = exclude.split(','); + if (spaces) excludeTypes = excludeTypes.map(item => item.split('-').join(' ')); + } + return excludeTypes; +} + + +function setExclude(items) { + excludeItems = items; + const excludeUl = $('.exclude-items'); + excludeUl.text(''); + + if (items.length === 0) { + $('.exclude-items-container').text('Include all challenge types'); + return; + } + + items.forEach(item => { + item = titleCase(item); + excludeUl.append(`

  • ${item}
  • `); + }); +} + function generateChallengeOptions() { const params = new URLSearchParams(new URL(location.href).search); @@ -121,4 +143,4 @@ function generateChallengeOptions() { optionsContainer.append(html); } -export { boardSize, diffMultiplier, defaultBoardSize, defaultDiffMultiplier, newBoard, handleRandomSeedBtn, handleOptionsFormSubmit, resetOptions, setBoardSize, setDifficulty, setBoardSizeValue, setDifficultyValue, generateChallengeOptions }; \ No newline at end of file +export { boardSize, diffMultiplier, getExcludeArray, defaultBoardSize, defaultDiffMultiplier, newBoard, handleRandomSeedBtn, handleOptionsFormSubmit, resetOptions, setBoardSize, setDifficulty, setBoardSizeValue, setDifficultyValue, setExclude, generateChallengeOptions }; \ No newline at end of file diff --git a/public/assets/modules/race.js b/public/assets/modules/race.js new file mode 100644 index 0000000..381c276 --- /dev/null +++ b/public/assets/modules/race.js @@ -0,0 +1,198 @@ +import { fetchAndGenerateBoard } from "./board.js"; +import { SeededRandom } from "./SeededRandom.js"; +import { updateShareURL } from "./share.js"; +import { hideElement, showElement } from "./helper.js"; +import { titleCase } from "./formatName.js"; + +let clientNum = 1; +let room = null; + +function onConnect(socket) { + socket.on('connect', () => { + // console.log(`You connected with ID: ${socket.id}`); + + socket.on('client-joined-room', clientNum => { + // $('.ready-btn').prop('disabled', false); + generateReadyStatus(socket); + }); + + socket.on('client-left-room', ({ id, clientNum }) => { + $(`.player-${clientNum}`).remove(); + + // if host leaves, exit room + if (clientNum === 1) { + leaveRoom(); + return; + } + + // $('.ready-btn').prop('disabled', false); + generateReadyStatus(socket); + // console.warn(`Client ${id} left the room!`); + }); + + socket.on('ready', (clientNum, allReady) => { + let playerDiv = $(`.player-${clientNum}`); + if (playerDiv) { + const readyIcon = $(`.player-${clientNum} .ready-icon`); + readyIcon.text('check_circle'); + readyIcon.attr('data-ready', 'true'); + } + + else { + generateReadyStatus(socket); + } + + if (allReady) beginRace(); + }); + + socket.on('receive-board-click', ({boardItem, clientNum, active}) => { + boardItem = $('#' + boardItem); + + if (active === true) { + boardItem.addClass(`done-${clientNum}`); + return; + } + + boardItem.removeClass(`done-${clientNum}`); + }); + }); +} + +function createRoom(event, socket, urlSearchParams) { + event.preventDefault(); + + const roomName = $('#create-room-name').val(); + socket.emit('create-room', { + room: roomName, + params: urlSearchParams.toString() + }); + + socket.on('create-room-response', ({res, error}) => { + if (error) { + const errorEl = $('.create-room-options .error'); + errorEl.text(error); + showElement(errorEl); + // console.warn(error); + return; + } + + room = roomName.toLowerCase(); + clientNum = 1; + + $('#room-name-title').text(titleCase(room)); + // $('.ready-btn').prop('disabled', true); + clearUI(); + generatePlayerMenu([{ + clientNum: clientNum, + ready: false + }]); + generateRaceMenu(); + }); +} + + +function joinRoom(event, socket, urlSearchParams) { + event.preventDefault(); + + const roomName = $('#join-room-name').val(); + socket.emit('join-room', roomName); + + socket.on('join-room-response', ({res, error}) => { + if (error) { + const errorEl = $('.join-room-options .error'); + errorEl.text(error); + showElement(errorEl); + // console.warn(error); + return; + } + + room = roomName; + clientNum = 2; + $('#room-name-title').text(titleCase(room)); + + socket.emit('request-ready-status', roomName, (clients) => + generatePlayerMenu(clients) + ); + + // console.log(res.message); + + urlSearchParams = new URLSearchParams(res.params); + const seed = urlSearchParams.get("seed"); + const rand = new SeededRandom(seed); + + clearUI(); + $('#seed-input').val(seed); + updateShareURL(); + fetchAndGenerateBoard(urlSearchParams, rand); + generateRaceMenu(); + }); +} + +function generateReadyStatus(socket) { + socket.emit('request-ready-status', room, (clients) => + generatePlayerMenu(clients) + ); +} + + +function leaveRoom() { + location.reload(); +} + + +function clearUI() { + // hide menu buttons + hideElement('.reroll-btn'); + hideElement('button[data-bs-target="#options-sidebar"]'); + hideElement('button[data-bs-target="#share-sidebar"]'); + hideElement('button[data-bs-target="#race-sidebar"]'); + + // close sidebar + const raceSidebar = bootstrap.Offcanvas.getInstance('#race-sidebar'); + raceSidebar.hide(); +} + + +function generatePlayerMenu(clients) { + $('.players').text(''); // clear players + + for (const { clientNum, ready } of clients) { + const readyIcon = ready ? 'check_circle' : 'radio_button_unchecked'; + const host = (clientNum === 1) ? '(host)' : '(host)'; + + $('.players').append( + `
    +
    + person +

    Player ${clientNum}

    + ${host} +
    + ${readyIcon} +
    ` + ) + } +} + +function generateRaceMenu() { + $('.bingo-board').addClass('hide'); + $('.race-menu').removeClass('hide'); +} + +function handleReadyBtn(socket) { + $('.ready-btn').css('visibility', 'hidden'); + + const readyIcon = $(`.player-${clientNum} .ready-icon`); + if (!readyIcon) return; + + readyIcon.text('check_circle'); + readyIcon.attr('data-ready', 'true'); + + socket.emit('ready', room, socket.id); +} + +function beginRace() { + $('.race-menu').addClass('hide'); + $('.bingo-board').removeClass('hide'); +} + +export { onConnect, createRoom, joinRoom, room, clientNum, handleReadyBtn, generatePlayerMenu, leaveRoom }; \ No newline at end of file diff --git a/public/index.html b/public/index.html index 57f8903..2a7349c 100644 --- a/public/index.html +++ b/public/index.html @@ -38,6 +38,11 @@

    Bingo

    share + + @@ -60,7 +65,7 @@

    Options

    Seed
    - + + + +

    Wait for others to be ready.

    + + + +
    +
    diff --git a/public/main.js b/public/main.js index 9e5009c..1142583 100644 --- a/public/main.js +++ b/public/main.js @@ -1,18 +1,21 @@ import { SeededRandom, getRandSeed } from "./assets/modules/SeededRandom.js"; -import { MagicSquare } from "./assets/modules/MagicSquare.js"; -import { challengeTypes, challTypeWeightReduce, weightedChallengeTypes } from "./assets/modules/ChallengeType.js"; -import { getWeightedRandom } from "./assets/modules/WeightedValue.js"; -import { uniqueEquipment } from "./assets/modules/uniqueEpuipment.js"; -import { fetchData } from "./assets/modules/fetchData.js"; -import { titleCase, simplifyName, getPlural, romanNumeral } from "./assets/modules/formatName.js"; -import { handleRandomSeedBtn, newBoard, handleOptionsFormSubmit, setBoardSizeValue, setDifficultyValue, generateChallengeOptions, boardSize, defaultBoardSize, diffMultiplier, defaultDiffMultiplier, resetOptions } from "./assets/modules/options.js"; -import { isBetween, clamp, hideElement, showElement } from "./assets/modules/helper.js"; +import { challengeTypes } from "./assets/modules/ChallengeType.js"; + +import { handleRandomSeedBtn, newBoard, handleOptionsFormSubmit, getExcludeArray, setBoardSizeValue, setDifficultyValue, setExclude, generateChallengeOptions, boardSize, defaultBoardSize, diffMultiplier, defaultDiffMultiplier, resetOptions } from "./assets/modules/options.js"; +import { hideElement, showElement } from "./assets/modules/helper.js"; import { updateShareURL, copyShareURL } from "./assets/modules/share.js"; +import { fetchAndGenerateBoard } from "./assets/modules/board.js"; + +import { io } from "https://cdn.socket.io/4.3.2/socket.io.esm.min.js"; +import { createRoom, joinRoom, room, clientNum, handleReadyBtn, generatePlayerMenu, onConnect, leaveRoom } from './assets/modules/race.js'; let rand; let url; let searchParams; +const socket = io(location.origin); + +onConnect(socket); function init() { url = new URL(location.href); @@ -27,10 +30,12 @@ function init() { rand = new SeededRandom(seed); $('#seed-input').val(seed); - updateShareURL(); - setBoardSizeValue( searchParams.get('boardSize') ?? defaultBoardSize ); - setDifficultyValue( searchParams.get('difficulty') ?? defaultDiffMultiplier ); + setBoardSizeValue(searchParams.get('boardSize') ?? defaultBoardSize); + setDifficultyValue(searchParams.get('difficulty') ?? defaultDiffMultiplier); + setExclude(getExcludeArray(searchParams, true)); + + updateShareURL(); setColorMode(getColorMode()); @@ -41,20 +46,31 @@ function init() { $('.options-form').on('submit', handleOptionsFormSubmit); $('.reroll-btn').on('click', () => handleRandomSeedBtn()); $('.random-seed-btn').on('click', () => handleRandomSeedBtn(false)); - $('#board-size-range').on('input', setBoardSizeValue); - $('#difficulty-range').on('input', setDifficultyValue); + $('#board-size-range').on('input', () => setBoardSizeValue($('#board-size-range').val())); + $('#difficulty-range').on('input', () => setDifficultyValue($('#difficulty-range').val())); $('.build-btn').on('click', handleOptionsFormSubmit); $('.reset-btn').on('click', resetOptions); // share $('.copy-link-btn').on('click', copyShareURL); + // race + $('.create-room-options').on('submit', (e) => createRoom(e, socket, searchParams)); + $('.join-room-options').on('submit', (e) => joinRoom(e, socket, searchParams)) + // $('.create-room-btn').on('click', () => createRoom(socket, searchParams)); + // $('.join-room-btn').on('click', () => joinRoom(socket, searchParams)); + document.getElementById('race-sidebar').addEventListener('hidden.bs.offcanvas', e => { + $('#race-sidebar .error').addClass('hide'); + }); + $('.ready-btn').on('click', () => handleReadyBtn(socket)); + $('.exit-room-btn').on('click', leaveRoom); + // debug $('.show-stats-btn').on('click', showBoardStats); $('.hide-stats-btn').on('click', hideBoardStats); generateChallengeOptions(); - fetchAndGenerateBoard(boardSize); + fetchAndGenerateBoard(searchParams, rand); } $(init()); @@ -62,179 +78,17 @@ $(init()); function handleBoardClick() { const boardItem = $(this); - if (boardItem.hasClass('done')) { - boardItem.removeClass('done'); - } - - else boardItem.addClass('done'); -} - - -async function fetchAndGenerateBoard(size=5) { - const data = await fetchData(); - const entries = createChallenges(size, data); - generateBingoBoard(size, entries); - - endLoading(); -} - - -// create challenges based on magic square difficulty -function createChallenges(size, data) { - const magicSquare = new MagicSquare(size); - const entries = []; - - let excludeChallTypes = []; - if (searchParams.get("exclude")) { - excludeChallTypes = searchParams.get('exclude').split(','); // get different challenge types - excludeChallTypes = excludeChallTypes.map(el => el.split('-').join(" ")); // replace dashes with spaces in name - } - - // get all entries for challenge types - challengeTypes.forEach(type => type.getEntries(data)); - - // choose a random entry for each difficulty - for (const difficulty of magicSquare._matrix.flat()) { - // adjust difficulty by diffMultiplier - const adjustedDiff = clamp(Math.round(difficulty * diffMultiplier), 0, 23); - - // get challenge types with current difficulty in their range - const validChallengeTypes = weightedChallengeTypes.filter(challengeType => { - challengeType = challengeType.value; - if (excludeChallTypes.includes(challengeType.name)) return false; - return isBetween(adjustedDiff, challengeType.diffMin, challengeType.diffMax); - }); - - if (validChallengeTypes.length < 1) return []; - - const randChallengeType = getWeightedRandom(validChallengeTypes, rand); - randChallengeType.weight = Math.round(randChallengeType.weight * challTypeWeightReduce * 100) / 100; // reduce change to get same challenge type again - - // TODO: get unique entries - let randEntry = randChallengeType.getRandomEntry(rand); - randEntry = structuredClone(randEntry); - randEntry.challengeType = randChallengeType.name; - randEntry.difficulty = adjustedDiff; - randEntry.amount = randChallengeType.calcAmount(adjustedDiff); - - entries.push(randEntry); - } - - return entries; -} + const oppositeNum = (clientNum === 1) ? 2 : 1; + if (boardItem.hasClass(`done-${oppositeNum}`)) return; // cancel if taken by other client -function generateBingoBoard(size=5, entries) { - if (entries.length < size * size) { - console.error('Could not get enough entries!'); - const errorMessage = $('
    Could not get enough challenges!
    ') - $('.bingo-board').append(errorMessage); - return; - } - - $('.bingo-board').css('grid-template-columns', `repeat(${size}, ${100/size}%)`); - $('.bingo-board').css('grid-template-rows', `repeat(${size}, ${100/size}%)`); - - for (let i=0; i < size*size; i++) { - const entry = entries[i]; - - const boardItem = $('
    '); - boardItem.data('entry', entry); - - const challengeText = $('

    '); - const itemText = $('

    '); - const stats = $('

    '); - const difficultyText = $('

    '); - - const challenge = getChallenge(entry); - challengeText.text(challenge.challenge); - itemText.text(challenge.entry); - difficultyText.text(entry.difficulty); - - stats.append(difficultyText); - boardItem.append(challengeText, itemText); - $('.bingo-board').append(boardItem); - } -} - - -// returns challenge text appropriate to the type of entry -function getChallenge({category, id, name, edible, amount, challengeType}) { - // normal main quests - if (category === "main quests") return {challenge: `main quest:`, entry: `'${name}'`}; - - // normal side adventues - if (category === "side adventures") return {challenge: `side adventure:`, entry: `'${name}'`}; - - // normal side quests - if (category === "side quests") return {challenge: `side quest:`, entry: `'${name}'`}; - - // normal shrine quests - if (category === "shrine quests") return {challenge: `shrine quest:`, entry: `'${name}'`}; - - // normal armor sets - if (category === "armor sets") return {challenge: `obtain the`, entry: `${name} Set`}; + boardItem.toggleClass(`done-${clientNum}`); - // unique bosses - if (id === 200) return {challenge: "defeat", entry: titleCase("demon king ganondorf")}; - if ((id >= 191 && id <= 201) || id === 165) return {challenge: "defeat", entry: titleCase(name)}; - - // unique misc items - if (name === 'autobuild') return {challenge: "obtain", entry: titleCase(name)}; // autobuild - - // unique creatures - if (name === 'patricia') return {challenge: "feed", entry: titleCase(name)}; - if (name === 'dondon') return {challenge: "feed a", entry: titleCase(name)}; - if (id >= 2 && id <= 5) return {challenge: "ride the", entry: titleCase(name)}; // unique horses - if (id === 159) return {challenge: "defeat a", entry: titleCase(name)}; // training construct - if ((id >= 188 && id <= 190) || id === 202) return {challenge: "ride", entry: titleCase(name)}; // dragons - - if (uniqueEquipment.includes(name)) return {challenge: "obtain the", entry: titleCase(name)}; - - // armor - if (challengeType === 'armor ★') return {challenge: "obtain the", entry: `${titleCase(name)}★`}; - if (category === "armor") return {challenge: "obtain the", entry: titleCase(name)}; - - // let amount = //rand.randInt(5) + 1; - name = getPlural(amount, simplifyName(name, id)); - name = titleCase(romanNumeral(name)); - - const vowels = ['a','e','i','o','u']; - if (amount === 1) amount = "a" + (vowels.includes(name.charAt(0).toLowerCase()) ? "n" : ""); - - if (id === 504) return {challenge: `open ${amount}`, entry: titleCase(name)}; // treasure chest - if (id === 509 || id === -13) return {challenge: `discover ${amount}`, entry: titleCase(name)}; // wells, caves - if (id >= 505 && id <= 508) return {challenge: `mine ${amount}`, entry: titleCase(name)}; // ore deposits - if (id === 72) return {challenge: `collect ${amount}`, entry: titleCase(name)}; // fairies - if ([1, 6, 8].includes(id)) return {challenge: `ride ${amount}`, entry: titleCase(name)}; // horses, sand seals - if (id === 7) return {challenge: `find ${amount}`, entry: titleCase(name)}; // donkeys - if ([14, 18, 19, 29].includes(id)) return {challenge: `feed ${amount}`, entry: titleCase(name)}; // white goats, hateno cows, hylian retriever - if (id === 52) return {challenge: `anger ${amount}`, entry: titleCase(name)}; // coocoos - if ([-8, -10, -17].includes(id)) return {challenge: `collect ${amount}`, entry: titleCase(name)}; // sage's will, yiga schematic, korok seeds - if ([-12, -14, -15].includes(id)) return {challenge: `activate ${amount}`, entry: titleCase(name)}; // towers, cherry blossoms, lightroot - if (id === -9) return {challenge: `install ${amount}`, entry: titleCase(name)}; // Hudson signs - if (category === "shrines") return {challenge: `complete ${amount}`, entry: titleCase(name)}; // shrines - - // normal materials & equipment - if (category === "materials" || category === "equipment") return {challenge: `collect ${amount}`, entry: name}; - - // normal creatures - if (category === "creatures" && edible === true) return {challenge: `collect ${amount}`, entry: name}; - if (category === "creatures") return {challenge: `hunt ${amount}`, entry: name}; - - // normal monsters - if (category === "monsters") return {challenge: `kill ${amount}`, entry: name}; - - return {challenge: "", entry: name} -} - - -function endLoading() { - $('.title-container').css('animation-name', 'loading-end'); - $('.title').attr('data-loading', 'false'); - - showElement('main'); - showElement('footer'); + socket.emit('board-click', room, { + boardItem: boardItem.attr('id'), + clientNum: clientNum, + active: boardItem.hasClass(`done-${clientNum}`) + }); } diff --git a/server.js b/server.js index d6e70aa..b51d920 100644 --- a/server.js +++ b/server.js @@ -2,10 +2,201 @@ const express = require('express'); const path = require('path'); const entries = require('./lib/entries'); +// const { clientNum } = require('./public/assets/modules/race'); const app = express(); +const server = require('http').createServer(app); +const io = require('socket.io')(server); + const PORT = process.env.PORT || 3000; +const rooms = []; + +// returns the room object with a given name +function getRoom(roomName) { + return rooms.find(room => room.name === roomName); +} + +// returns the number of clients connected to a room +function getAmountClients(room) { + amountConnected = room.clients.reduce((sum, client) => { + return client.connected ? sum + 1: sum; }, + 0); + return amountConnected; +} + +// returns the number of clients who are ready in the waiting room +function getAmountReadyClients(room) { + amountReady = room.clients.reduce((sum, client) => { + return client.ready ? sum + 1: sum; }, + 0); + return amountReady; +} + +class Client { + constructor(id, num, connected=true) { + this.id = id; + this.num = num; + this.connected = connected; + this.timeout = null; + this.ready = false; + } +} + + +io.on('connection', socket => { + console.info(`${socket.id} joined.`); + + // Create room + socket.on('create-room', ({room, params}) => { + room = room.toLowerCase(); + + if (room.length < 1) { + socket.emit('create-room-response', { + error: 'Room name must be at least 1 character long!' + }); + return; + } + + if (getRoom(room)) { + socket.emit('create-room-response', { + error: 'Room name already in use! Choose a different name.' + }); + return; + } + + rooms.push({ + name: room, + params: params, + clients: [new Client(socket.id, 1)] + }); + socket.join(room); + + socket.emit('create-room-response', { + res: `Created room ${room}` + }); + + console.info(`Room "${room}" created by ${socket.id}`); + + }); + + + // Join room + socket.on('join-room', (roomName) => { + roomName = roomName.toLowerCase(); + + const room = getRoom(roomName); + + if (room == null) { + socket.emit('join-room-response', { + error: 'Could not find room!' + }); + return; + } + + if (getAmountClients(room) >= 2) { + socket.emit('join-room-response', { + error: 'Room is full!' + }); + return; + } + + // let clientNum; + // let unconnectedClient = room.clients.find(client => !client.connected); + + // if (unconnectedClient) { + // unconnectedClient = new Client(socket.id, unconnectedClient.num); + // clientNum = unconnectedClient.num; + // } + + // else { + room.clients.push(new Client(socket.id, room.clients.length + 1)); + let clientNum = room.clients.length; + + socket.join(roomName); + + socket.to(roomName).emit('client-joined-room', clientNum); + + socket.emit('join-room-response', { + res: { + message: `Joined room ${roomName}`, + params: room.params + } + }); + + console.info(rooms); + }); + + + socket.on('ready', (roomName, id) => { + const room = getRoom(roomName); + const client = room.clients.find(c => c.id === id); + if (client == null) return; + + client.ready = true; + + const allReady = (getAmountReadyClients(room) >= getAmountClients(room)) && (getAmountClients(room) > 1); + + io.in(roomName).emit('ready', client.num, allReady); + }); + + socket.on('request-ready-status', (roomName, cb) => { + const room = getRoom(roomName); + if (room == null) { + cb([]); + return; + } + + const clients = []; + + for (const client of room.clients) { + clients.push({ + clientNum: client.num, + ready: client.ready + }); + } + + cb(clients); + }) + + + socket.on('disconnecting', reason => { + for (const roomName of socket.rooms) { + if (roomName === socket.id) continue; // pass over default room + + const room = getRoom(roomName); + const clientIndex = room.clients.findIndex(c => c.id === socket.id); + const client = room.clients[clientIndex]; + + client.connected = false; + room.clients.splice(clientIndex, 1); + + socket.to(roomName).emit('client-left-room', { + id: socket.id, + clientNum: client.num + }); + } + console.log(`Client ${socket.id} disconnected.`); + }); + + + socket.on('board-click', (room, obj) => { + socket.to(room).emit('receive-board-click', obj); + }); +}); + +// continually delete rooms that have no connections +setInterval(() => { + for (room of rooms) { + // cancel if there are still connected clients in room + if (getAmountClients(room) > 0) continue; + + // remove room + rooms.splice(rooms.findIndex(rm => rm.name === room.name), 1); + console.info(`Room "${room.name}" deleted. No more clients connected.`); + } +}, 5000); + // static public folder app.use(express.static('public')); @@ -14,6 +205,6 @@ app.get('/api/entries', (req, res) => { res.json(entries); }); -app.listen(PORT, () => { +server.listen(PORT, () => { console.log(`Serving static assets on http://localhost:${PORT}`); }); \ No newline at end of file