diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..014f1cd23b876f6edb72454e7dd1c69022f8eb2a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+*.png filter=lfs diff=lfs merge=lfs -text
+client/public/Assests/*.png filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..05576fb8dc7b9ea0b76563f9c237d39245ccca67
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,40 @@
+# Dependencies
+node_modules/
+.pnp
+.pnp.js
+
+# Build outputs
+client/dist/
+server/dist/
+
+# Environment files
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# Logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
+
+# TypeScript
+*.tsbuildinfo
+
+# Testing
+coverage/
+
+# Temp
+*.tmp
+*.temp
diff --git a/Assests/card_all_or_nothing.png b/Assests/card_all_or_nothing.png
new file mode 100644
index 0000000000000000000000000000000000000000..021695e8a6e610037cf47e90fd04e07971e46b99
--- /dev/null
+++ b/Assests/card_all_or_nothing.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0690f18de06f83335dcd3ba07d4a06bb48a4e05e924844f8159b728060ef76b2
+size 320710
diff --git a/Assests/card_back.png b/Assests/card_back.png
new file mode 100644
index 0000000000000000000000000000000000000000..e870567a8a41ffdd617605451410a2a55731e3f8
--- /dev/null
+++ b/Assests/card_back.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d294014849a4e23e2054ef43a4215fe6b0c9496566a8bc1335ae22c8bcde6ef3
+size 28975
diff --git a/Assests/card_criminal_mummy.png b/Assests/card_criminal_mummy.png
new file mode 100644
index 0000000000000000000000000000000000000000..a0f6cdd5fc0aceabf3dda76fcb5e005f07ac0378
--- /dev/null
+++ b/Assests/card_criminal_mummy.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:918b9aea9a56aa39a7d3f176d1e612788a449dc05c3dbef8f06383884e646539
+size 319244
diff --git a/Assests/card_flip_the_table.png b/Assests/card_flip_the_table.png
new file mode 100644
index 0000000000000000000000000000000000000000..a32a10a59fd7fa0725ac181b038722e525b645fd
--- /dev/null
+++ b/Assests/card_flip_the_table.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7b76b8c56a28a8df57e617c6a78f4645ae56f77a515a12f68b37fe3e5612e392
+size 321460
diff --git a/Assests/card_give_and_take.png b/Assests/card_give_and_take.png
new file mode 100644
index 0000000000000000000000000000000000000000..8776cd9b09bf4ba0a3bf697613d6e409782e13de
--- /dev/null
+++ b/Assests/card_give_and_take.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4ef9c38b8e389f579121f3f1b85da6b707b7a0c32e97b3f2b621076175f0b36c
+size 321111
diff --git a/Assests/card_king_ra_says_no.png b/Assests/card_king_ra_says_no.png
new file mode 100644
index 0000000000000000000000000000000000000000..a4c4889b5851d1ed155bf6e92227dca489bba4ee
--- /dev/null
+++ b/Assests/card_king_ra_says_no.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f07792dd2d7467fa6a16cf8c2e3cef6be999429e3a7d0bf85cf4a0cddf8e5392
+size 321493
diff --git a/Assests/card_me_or_you.png.png b/Assests/card_me_or_you.png.png
new file mode 100644
index 0000000000000000000000000000000000000000..44df6c73138e97c972d079c16e6bca18afd876b9
--- /dev/null
+++ b/Assests/card_me_or_you.png.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2e17124016c04444f974f5aa4f4fa30e9cc3e179aa707b953af4b783e18520f7
+size 323002
diff --git a/Assests/card_mummy.png b/Assests/card_mummy.png
new file mode 100644
index 0000000000000000000000000000000000000000..a8530b5c4a30641fdb3087fcafa8aa2f0722bab1
--- /dev/null
+++ b/Assests/card_mummy.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dfb39f07dba40122e6dea960307255553bc139821c8f2240cbc38f923dcd09bb
+size 320949
diff --git a/Assests/card_safe_travels.png b/Assests/card_safe_travels.png
new file mode 100644
index 0000000000000000000000000000000000000000..de661ed55ea594066c7bfeca77bb7d67b17b2b39
--- /dev/null
+++ b/Assests/card_safe_travels.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:edacf1cd263c427b268c8df85b7d27d6deeab422bcaf8ae2a43b926cea070e60
+size 321360
diff --git a/Assests/card_sharp_eye.png b/Assests/card_sharp_eye.png
new file mode 100644
index 0000000000000000000000000000000000000000..6dea5f719aba405d946dcc5c06684c6020d1cc20
--- /dev/null
+++ b/Assests/card_sharp_eye.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c84d6fd20e5937515558353a8ed9a39149475f19431c4fd2832f68516a1ff0b3
+size 321997
diff --git a/Assests/card_shuffle_it.png b/Assests/card_shuffle_it.png
new file mode 100644
index 0000000000000000000000000000000000000000..58e3888ef26214562e0680f65ca68aa9f4530724
--- /dev/null
+++ b/Assests/card_shuffle_it.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d2a49f03e994ed6cf1dd8e3c55f9c4baca7fcaac97aa19112b92545d6660e45d
+size 319863
diff --git a/Assests/card_spellbound.png b/Assests/card_spellbound.png
new file mode 100644
index 0000000000000000000000000000000000000000..811f748d0ef0f2893b2064e5c156f173f908af7a
--- /dev/null
+++ b/Assests/card_spellbound.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ded909785128761f4f5a19fab39e5b05e41db6eefe12cf169bb48b6000321267
+size 321458
diff --git a/Assests/card_take_a_lap.png b/Assests/card_take_a_lap.png
new file mode 100644
index 0000000000000000000000000000000000000000..d89bde0ceb61a119c56ca4e76025439d2dc84497
--- /dev/null
+++ b/Assests/card_take_a_lap.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8f2d1f5cddd0ad4df7b25ff1cbdb18d5b8d549f6ed465eddb3dff060b01e9e54
+size 322435
diff --git a/Assests/card_this_is_on_you.png b/Assests/card_this_is_on_you.png
new file mode 100644
index 0000000000000000000000000000000000000000..f3bab9bb1ed0baa23e7357d0c9468831be88f785
--- /dev/null
+++ b/Assests/card_this_is_on_you.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:acbbfbb51df15671db46588fcc7137facef6c9a853a3bcad8b52f65e065a332f
+size 322545
diff --git a/Assests/card_wait_a_sec.png b/Assests/card_wait_a_sec.png
new file mode 100644
index 0000000000000000000000000000000000000000..786c909d4cb3ddb3b8089526348818f7339884c3
--- /dev/null
+++ b/Assests/card_wait_a_sec.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:35989714a07df7046ae7e70f78f6bbc4a18e5bfe4f375721238fd501fc4c5d3f
+size 321074
diff --git a/Assests/menu_background.png b/Assests/menu_background.png
new file mode 100644
index 0000000000000000000000000000000000000000..141700ef7b057c7b19612881cf296d5ccb65a8eb
--- /dev/null
+++ b/Assests/menu_background.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:93878ed4802b269056ab87816a216683ced847697076bd707a66f8ca7ea3446b
+size 8463889
diff --git a/Assests/menu_background_old.png b/Assests/menu_background_old.png
new file mode 100644
index 0000000000000000000000000000000000000000..3424a4c1229317d0294e7694000f5b80d0db83e4
--- /dev/null
+++ b/Assests/menu_background_old.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a29c272f332937823a8a7e44a7d3282e0acd808cf3df65a970cce1b43c945b87
+size 8324271
diff --git a/Assests/ui_button.png b/Assests/ui_button.png
new file mode 100644
index 0000000000000000000000000000000000000000..6e9c11693a88328877a48aa5c6500238e38ddcf9
--- /dev/null
+++ b/Assests/ui_button.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8788102ef3b46ddb0c467eeeb9a5d4c7d67074063832fa339dbdc8998fca2feb
+size 312387
diff --git a/Assests/ui_logo.png b/Assests/ui_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..21633d27170a832835a77d2e1f6f3156d7bcb063
--- /dev/null
+++ b/Assests/ui_logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0672ca6041db16db0f883aab6fc63f9034f8b53d29bb8aa6b320cb73b687fbdc
+size 296651
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..80e7597edfc3562ba11c96703b52314e6ad47717
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+ Mummy Card Game - هتتحنط هنا
+
+
+
+
+
+
+
+
diff --git a/client/package-lock.json b/client/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..ec71d7ef9dd377b38b4350c86530bc09b2ba4fa5
--- /dev/null
+++ b/client/package-lock.json
@@ -0,0 +1,2426 @@
+{
+ "name": "mummy-card-game-client",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "mummy-card-game-client",
+ "version": "1.0.0",
+ "dependencies": {
+ "framer-motion": "^10.16.4",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "socket.io-client": "^4.7.2",
+ "zustand": "^4.4.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.28",
+ "@types/react-dom": "^18.2.13",
+ "@vitejs/plugin-react": "^4.1.0",
+ "autoprefixer": "^10.4.16",
+ "postcss": "^8.4.31",
+ "tailwindcss": "^3.3.3",
+ "typescript": "^5.2.2",
+ "vite": "^4.4.11"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "0.8.8",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
+ "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emotion/memoize": "0.7.4"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
+ "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
+ "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
+ "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
+ "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
+ "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
+ "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
+ "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
+ "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
+ "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
+ "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
+ "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
+ "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
+ "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
+ "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
+ "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
+ "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
+ "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
+ "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
+ "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
+ "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
+ "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
+ "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.28",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.24",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
+ "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1",
+ "caniuse-lite": "^1.0.30001766",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
+ "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001769",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
+ "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.286",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
+ "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/engine.io-client": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
+ "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.4.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.18.3",
+ "xmlhttprequest-ssl": "~2.1.1"
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.18.20",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
+ "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.18.20",
+ "@esbuild/android-arm64": "0.18.20",
+ "@esbuild/android-x64": "0.18.20",
+ "@esbuild/darwin-arm64": "0.18.20",
+ "@esbuild/darwin-x64": "0.18.20",
+ "@esbuild/freebsd-arm64": "0.18.20",
+ "@esbuild/freebsd-x64": "0.18.20",
+ "@esbuild/linux-arm": "0.18.20",
+ "@esbuild/linux-arm64": "0.18.20",
+ "@esbuild/linux-ia32": "0.18.20",
+ "@esbuild/linux-loong64": "0.18.20",
+ "@esbuild/linux-mips64el": "0.18.20",
+ "@esbuild/linux-ppc64": "0.18.20",
+ "@esbuild/linux-riscv64": "0.18.20",
+ "@esbuild/linux-s390x": "0.18.20",
+ "@esbuild/linux-x64": "0.18.20",
+ "@esbuild/netbsd-x64": "0.18.20",
+ "@esbuild/openbsd-x64": "0.18.20",
+ "@esbuild/sunos-x64": "0.18.20",
+ "@esbuild/win32-arm64": "0.18.20",
+ "@esbuild/win32-ia32": "0.18.20",
+ "@esbuild/win32-x64": "0.18.20"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/framer-motion": {
+ "version": "10.18.0",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.18.0.tgz",
+ "integrity": "sha512-oGlDh1Q1XqYPksuTD/usb0I70hq95OUzmL9+6Zd+Hs4XV0oaISBa/UUMSjYiq6m8EUF32132mOJ8xVZS+I0S6w==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ },
+ "optionalDependencies": {
+ "@emotion/is-prop-valid": "^0.8.2"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "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==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.1.1"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "jiti": ">=1.21.0",
+ "postcss": ">=8.0.9",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "3.29.5",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
+ "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=14.18.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/socket.io-client": {
+ "version": "4.8.3",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
+ "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.4.1",
+ "engine.io-client": "~6.6.1",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
+ "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.4.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.19",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.7",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "4.5.14",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz",
+ "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.18.10",
+ "postcss": "^8.4.27",
+ "rollup": "^3.27.1"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "@types/node": ">= 14",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "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.1.2",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
+ "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/client/package.json b/client/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..c065f007bb0df1addfaa38c219f0da71fc58758f
--- /dev/null
+++ b/client/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "mummy-card-game-client",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "framer-motion": "^10.16.4",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "socket.io-client": "^4.7.2",
+ "zustand": "^4.4.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.28",
+ "@types/react-dom": "^18.2.13",
+ "@vitejs/plugin-react": "^4.1.0",
+ "autoprefixer": "^10.4.16",
+ "postcss": "^8.4.31",
+ "tailwindcss": "^3.3.3",
+ "typescript": "^5.2.2",
+ "vite": "^4.4.11"
+ }
+}
diff --git a/client/postcss.config.js b/client/postcss.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..2e7af2b7f1a6f391da1631d93968a9d487ba977d
--- /dev/null
+++ b/client/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/client/src/App.tsx b/client/src/App.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4a89cfcb82582fb598c64db1c46e6fe89dec4d99
--- /dev/null
+++ b/client/src/App.tsx
@@ -0,0 +1,51 @@
+import { useEffect } from 'react';
+import { useGameStore } from './store/gameStore';
+import { initializeSocket, disconnectSocket } from './socket/socket';
+import { MainMenu } from './components/screens/MainMenu';
+import { Lobby } from './components/screens/Lobby';
+import { Room } from './components/screens/Room';
+import { GameBoard } from './components/screens/GameBoard';
+import { LandscapeOverlay } from './components/ui/LandscapeOverlay';
+import { ToastContainer } from './components/ui/ToastContainer';
+import { MummyEventOverlay } from './components/ui/MummyEventOverlay';
+import { ModalRenderer } from './components/modals/ModalRenderer';
+
+function App() {
+ const currentScreen = useGameStore((state) => state.currentScreen);
+
+ useEffect(() => {
+ initializeSocket();
+ return () => {
+ disconnectSocket();
+ };
+ }, []);
+
+ const renderScreen = () => {
+ switch (currentScreen) {
+ case 'menu':
+ return ;
+ case 'lobby':
+ return ;
+ case 'room':
+ return ;
+ case 'game':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+ <>
+
+
+ {renderScreen()}
+
+
+
+
+ >
+ );
+}
+
+export default App;
diff --git a/client/src/components/game/Card.tsx b/client/src/components/game/Card.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9cad61544be95e3302844df5008613be7ace4ff1
--- /dev/null
+++ b/client/src/components/game/Card.tsx
@@ -0,0 +1,111 @@
+import { motion } from 'framer-motion';
+import type { CardInstance, CardId } from '@shared/types';
+import { CARD_DATABASE, CARD_ASSETS, CARD_BACK_ASSET } from '@shared/types';
+
+interface CardProps {
+ card?: CardInstance;
+ cardId?: CardId;
+ faceDown?: boolean;
+ selected?: boolean;
+ disabled?: boolean;
+ onClick?: () => void;
+ size?: 'small' | 'medium' | 'large';
+ className?: string;
+ showValue?: boolean;
+}
+
+const sizeClasses = {
+ small: 'w-16',
+ medium: 'w-24',
+ large: 'w-32',
+};
+
+export function Card({
+ card,
+ cardId,
+ faceDown = false,
+ selected = false,
+ disabled = false,
+ onClick,
+ size = 'medium',
+ className = '',
+ showValue = true,
+}: CardProps) {
+ const actualCardId = card?.cardId ?? cardId;
+ const cardDef = actualCardId ? CARD_DATABASE[actualCardId] : null;
+
+ const imageSrc = faceDown || !actualCardId
+ ? `/${CARD_BACK_ASSET}`
+ : `/${CARD_ASSETS[actualCardId]}`;
+
+ const isMummy = actualCardId === 'mummified';
+ const isHalf = cardDef?.isHalf;
+
+ return (
+
+
{
+ // Fallback if image doesn't exist
+ (e.target as HTMLImageElement).src = `/${CARD_BACK_ASSET}`;
+ }}
+ />
+
+ {!faceDown && cardDef && (
+ <>
+
+
+ {isHalf && (
+ ½
+ )}
+
+ {showValue && cardDef.value > 0 && (
+
+ {cardDef.value}
+
+ )}
+
+
+
{cardDef.nameEn}
+
{cardDef.nameAr}
+
+ >
+ )}
+
+ );
+}
+
+interface CardBackProps {
+ count?: number;
+ onClick?: () => void;
+ size?: 'small' | 'medium' | 'large';
+ className?: string;
+}
+
+export function CardBack({ count, onClick, size = 'medium', className = '' }: CardBackProps) {
+ return (
+
+
+ {count !== undefined && (
+
+ {count}
+
+ )}
+
+ );
+}
diff --git a/client/src/components/modals/ArrangeHandModal.tsx b/client/src/components/modals/ArrangeHandModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..da4ad9178408def670f4de931476068006ee019b
--- /dev/null
+++ b/client/src/components/modals/ArrangeHandModal.tsx
@@ -0,0 +1,124 @@
+import { useState, useEffect, useRef } from 'react';
+import { motion } from 'framer-motion';
+import { useGameStore } from '../../store/gameStore';
+import { Card } from '../game/Card';
+import { emitArrangeHand } from '../../socket/socket';
+
+const AUTO_CONFIRM_TIMEOUT = 15000; // 15 seconds
+
+interface ArrangeHandModalProps {
+ onClose: () => void;
+}
+
+export function ArrangeHandModal({ onClose }: ArrangeHandModalProps) {
+ const myHand = useGameStore((state) => state.myHand);
+ const [orderedCards, setOrderedCards] = useState(myHand.map(c => c.instanceId));
+ const [selectedIndex, setSelectedIndex] = useState(null);
+ const [timeLeft, setTimeLeft] = useState(AUTO_CONFIRM_TIMEOUT / 1000);
+ const hasConfirmed = useRef(false);
+
+ const handleCardClick = (index: number) => {
+ if (selectedIndex === null) {
+ // First card selected
+ setSelectedIndex(index);
+ } else if (selectedIndex === index) {
+ // Clicked same card, deselect
+ setSelectedIndex(null);
+ } else {
+ // Second card selected - swap them
+ const newOrder = [...orderedCards];
+ [newOrder[selectedIndex], newOrder[index]] = [newOrder[index], newOrder[selectedIndex]];
+ setOrderedCards(newOrder);
+ setSelectedIndex(null);
+ }
+ };
+
+ const handleConfirm = () => {
+ if (hasConfirmed.current) return;
+ hasConfirmed.current = true;
+ emitArrangeHand(orderedCards);
+ onClose();
+ };
+
+ // Auto-confirm timer
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setTimeLeft((prev) => {
+ if (prev <= 1) {
+ clearInterval(interval);
+ handleConfirm();
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+
+ return () => clearInterval(interval);
+ }, []);
+
+ return (
+
+ e.stopPropagation()}
+ >
+ 🔀 Arrange Your Cards
+
+ Someone is about to steal from you!
+
+
+ Tap two cards to swap their positions. Hide your good cards!
+
+
+ ⏱️ Auto-confirm in {timeLeft}s
+
+
+
+ {orderedCards.map((instanceId, index) => {
+ const card = myHand.find(c => c.instanceId === instanceId);
+ if (!card) return null;
+
+ const isSelected = selectedIndex === index;
+
+ return (
+
handleCardClick(index)}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ animate={isSelected ? { y: -10 } : { y: 0 }}
+ >
+
+ #{index + 1}
+
+ );
+ })}
+
+
+ {selectedIndex !== null && (
+
+ Card #{selectedIndex + 1} selected. Tap another card to swap!
+
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/modals/BlindStealModal.tsx b/client/src/components/modals/BlindStealModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ffae668b5790011ab8715f1a8670e0069184ac98
--- /dev/null
+++ b/client/src/components/modals/BlindStealModal.tsx
@@ -0,0 +1,62 @@
+import { motion } from 'framer-motion';
+import { emitBlindStealSelect } from '../../socket/socket';
+
+interface BlindStealModalProps {
+ cardCount: number;
+ onClose: () => void;
+}
+
+export function BlindStealModal({ cardCount, onClose }: BlindStealModalProps) {
+ const handleSelect = (position: number) => {
+ emitBlindStealSelect(position);
+ onClose();
+ };
+
+ return (
+
+ e.stopPropagation()}
+ >
+ 🥷 Steal a Card
+
+ Choose a card to steal (you can't see what they are!)
+
+
+
+ {Array.from({ length: cardCount }).map((_, index) => (
+
handleSelect(index)}
+ initial={{ opacity: 0, rotateY: 180 }}
+ animate={{ opacity: 1, rotateY: 0 }}
+ transition={{ delay: index * 0.1 }}
+ whileHover={{ scale: 1.1, y: -10 }}
+ whileTap={{ scale: 0.95 }}
+ >
+
+ ?
+
+
+ {index + 1}
+
+
+ ))}
+
+
+
+ The victim may have rearranged their cards!
+
+
+
+ );
+}
diff --git a/client/src/components/modals/CardTypeSelectModal.tsx b/client/src/components/modals/CardTypeSelectModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..21c04ce95537d93f5f94a2c36e83dbe984000e38
--- /dev/null
+++ b/client/src/components/modals/CardTypeSelectModal.tsx
@@ -0,0 +1,72 @@
+import { motion } from 'framer-motion';
+import { CARD_DATABASE, type CardId } from '@shared/types';
+import { emitSpellboundRequest } from '../../socket/socket';
+
+interface CardTypeSelectModalProps {
+ targetId?: string;
+ onClose: () => void;
+}
+
+// Cards that can be requested via Spellbound
+const REQUESTABLE_CARDS: CardId[] = [
+ 'sharp_eye', 'wait_a_sec', 'me_or_you', 'spellbound', 'criminal_mummy',
+ 'give_and_take', 'shuffle_it', 'king_ra_says_no', 'safe_travels',
+ 'take_a_lap', 'all_or_nothing', 'flip_the_table', 'this_is_on_you'
+];
+
+export function CardTypeSelectModal({ targetId, onClose }: CardTypeSelectModalProps) {
+ const handleSelect = (cardId: CardId) => {
+ if (targetId) {
+ emitSpellboundRequest(targetId, cardId);
+ }
+ onClose();
+ };
+
+ return (
+
+ e.stopPropagation()}
+ >
+ 🔮 Spellbound
+
+ Ask for a specific card. If they don't have it, YOU draw!
+
+
+
+ {REQUESTABLE_CARDS.map((cardId, index) => {
+ const card = CARD_DATABASE[cardId];
+ return (
+
handleSelect(cardId)}
+ initial={{ opacity: 0, scale: 0.9 }}
+ animate={{ opacity: 1, scale: 1 }}
+ transition={{ delay: index * 0.03 }}
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ >
+ {card.nameEn}
+ {card.nameAr}
+
+ );
+ })}
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/modals/DuelResultModal.tsx b/client/src/components/modals/DuelResultModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..afa1a4057a629e9743859419e08e6c0739e11c91
--- /dev/null
+++ b/client/src/components/modals/DuelResultModal.tsx
@@ -0,0 +1,109 @@
+import { motion } from 'framer-motion';
+import { Card } from '../game/Card';
+import { useGameStore } from '../../store/gameStore';
+import type { CardInstance } from '@shared/types';
+
+interface DuelResultModalProps {
+ challenger?: { playerId: string; card: CardInstance };
+ opponent?: { playerId: string; card: CardInstance };
+ winnerId?: string;
+ onClose: () => void;
+}
+
+export function DuelResultModal({ challenger, opponent, winnerId, onClose }: DuelResultModalProps) {
+ const gameState = useGameStore((state) => state.gameState);
+ const playerId = useGameStore((state) => state.playerId);
+
+ const challengerName = gameState?.players.find(p => p.id === challenger?.playerId)?.name ?? 'Challenger';
+ const opponentName = gameState?.players.find(p => p.id === opponent?.playerId)?.name ?? 'Opponent';
+ const winnerName = gameState?.players.find(p => p.id === winnerId)?.name ?? 'Winner';
+ const isWinner = winnerId === playerId;
+
+ return (
+
+ e.stopPropagation()}
+ >
+ ⚔️ Duel Result
+
+
+ {/* Challenger */}
+
+ {challengerName}
+ {challenger?.card && (
+
+ )}
+ {winnerId === challenger?.playerId && (
+
+ 🏆 WINNER!
+
+ )}
+
+
+ {/* VS */}
+
+ VS
+
+
+ {/* Opponent */}
+
+ {opponentName}
+ {opponent?.card && (
+
+ )}
+ {winnerId === opponent?.playerId && (
+
+ 🏆 WINNER!
+
+ )}
+
+
+
+
+ {isWinner ? "🎉 You won the duel!" : `${winnerName} won the duel!`}
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/modals/FlipTableModal.tsx b/client/src/components/modals/FlipTableModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..24f7c650fec9b76c3621ecad275bebf6e37d79f6
--- /dev/null
+++ b/client/src/components/modals/FlipTableModal.tsx
@@ -0,0 +1,114 @@
+import { useState, useRef } from 'react';
+import { motion } from 'framer-motion';
+import { Card } from '../game/Card';
+import { emitRearrangeTopCards } from '../../socket/socket';
+import type { CardInstance } from '@shared/types';
+
+interface FlipTableModalProps {
+ cards: CardInstance[];
+ onClose: () => void;
+}
+
+export function FlipTableModal({ cards, onClose }: FlipTableModalProps) {
+ const [orderedCards, setOrderedCards] = useState(cards.map(c => c.instanceId));
+ const [selectedIndex, setSelectedIndex] = useState(null);
+ const hasConfirmed = useRef(false);
+
+ const handleCardClick = (index: number) => {
+ if (selectedIndex === null) {
+ setSelectedIndex(index);
+ } else if (selectedIndex === index) {
+ setSelectedIndex(null);
+ } else {
+ // Swap cards
+ const newOrder = [...orderedCards];
+ [newOrder[selectedIndex], newOrder[index]] = [newOrder[index], newOrder[selectedIndex]];
+ setOrderedCards(newOrder);
+ setSelectedIndex(null);
+ }
+ };
+
+ const handleConfirm = () => {
+ if (hasConfirmed.current) return;
+ hasConfirmed.current = true;
+ emitRearrangeTopCards(orderedCards);
+ onClose();
+ };
+
+ const getCardByInstanceId = (instanceId: string) => {
+ return cards.find(c => c.instanceId === instanceId);
+ };
+
+ return (
+
+ e.stopPropagation()}
+ >
+ 🔄 Flip the Table
+
+ Rearrange the top {cards.length} cards of the deck
+
+
+ Tap two cards to swap positions. Position 1 will be drawn first.
+
+
+
+ {orderedCards.map((instanceId, index) => {
+ const card = getCardByInstanceId(instanceId);
+ if (!card) return null;
+
+ const isMummy = card.cardId === 'mummified';
+ const isSelected = selectedIndex === index;
+
+ return (
+ handleCardClick(index)}
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ animate={isSelected ? { y: -10 } : { y: 0 }}
+ >
+
+
+ {index + 1}
+
+ {isMummy && (
+ 💀
+ )}
+
+ );
+ })}
+
+
+ {selectedIndex !== null && (
+
+ Position #{selectedIndex + 1} selected. Tap another card to swap!
+
+ )}
+
+
+ 💡 Tip: Push mummies to higher positions (drawn later)
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/modals/GameOverModal.tsx b/client/src/components/modals/GameOverModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..761b4417a4d70ced1280ba64e84ecb2d0d24b56a
--- /dev/null
+++ b/client/src/components/modals/GameOverModal.tsx
@@ -0,0 +1,124 @@
+import { motion } from 'framer-motion';
+import { useGameStore } from '../../store/gameStore';
+import { emitLeaveRoom } from '../../socket/socket';
+
+interface GameOverModalProps {
+ winnerId?: string;
+ winnerName?: string;
+ onClose: () => void;
+}
+
+export function GameOverModal({ winnerId, winnerName, onClose }: GameOverModalProps) {
+ const playerId = useGameStore((state) => state.playerId);
+ const reset = useGameStore((state) => state.reset);
+
+ const isWinner = winnerId === playerId;
+
+ const handleLeave = () => {
+ emitLeaveRoom();
+ reset();
+ onClose();
+ };
+
+ return (
+
+ e.stopPropagation()}
+ >
+ {isWinner ? (
+ <>
+
+ 🏆
+
+
+ YOU WIN!
+
+
+ مبروك! انت الفائز
+
+
+ You survived the mummies!
+
+ >
+ ) : (
+ <>
+
+ 👑
+
+
+ GAME OVER
+
+
+ {winnerName} wins!
+
+
+ Better luck next time!
+
+ >
+ )}
+
+
+
+
+
+ {/* Confetti effect for winner */}
+ {isWinner && (
+
+ {Array.from({ length: 50 }).map((_, i) => (
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/client/src/components/modals/KingRaPromptModal.tsx b/client/src/components/modals/KingRaPromptModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ff26935842b90b3efae5449a86c381ff1251ec36
--- /dev/null
+++ b/client/src/components/modals/KingRaPromptModal.tsx
@@ -0,0 +1,138 @@
+import { useState, useEffect, useRef } from 'react';
+import { motion } from 'framer-motion';
+import { CARD_DATABASE, type CardId } from '@shared/types';
+import { useGameStore } from '../../store/gameStore';
+import { emitKingRaResponse } from '../../socket/socket';
+
+interface KingRaPromptModalProps {
+ playerId?: string;
+ cardPlayed?: CardId;
+ timeout: number;
+ onClose: () => void;
+}
+
+export function KingRaPromptModal({ playerId, cardPlayed, timeout, onClose }: KingRaPromptModalProps) {
+ const [timeLeft, setTimeLeft] = useState(timeout);
+ const gameState = useGameStore((state) => state.gameState);
+ const myHand = useGameStore((state) => state.myHand);
+ const hasResponded = useRef(false);
+ const isMounted = useRef(true);
+
+ const hasKingRa = myHand.some(c => c.cardId === 'king_ra_says_no');
+ console.log('KingRaPromptModal render - myHand:', myHand.map(c => c.cardId), 'hasKingRa:', hasKingRa);
+ const playerName = gameState?.players.find(p => p.id === playerId)?.name ?? 'A player';
+ const cardDef = cardPlayed ? CARD_DATABASE[cardPlayed] : null;
+
+ // Track mount state
+ useEffect(() => {
+ isMounted.current = true;
+ hasResponded.current = false; // Reset on mount
+ return () => {
+ isMounted.current = false;
+ };
+ }, []);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ if (!isMounted.current) {
+ clearInterval(interval);
+ return;
+ }
+
+ setTimeLeft((prev) => {
+ if (prev <= 100) {
+ clearInterval(interval);
+ // Auto-decline on timeout
+ if (!hasResponded.current && isMounted.current) {
+ hasResponded.current = true;
+ emitKingRaResponse(false);
+ onClose();
+ }
+ return 0;
+ }
+ return prev - 100;
+ });
+ }, 100);
+
+ return () => clearInterval(interval);
+ }, [onClose]);
+
+ const handleResponse = (useKingRa: boolean) => {
+ console.log('King Ra handleResponse called:', { useKingRa, hasResponded: hasResponded.current, hasKingRa });
+ if (hasResponded.current) {
+ console.log('Already responded, ignoring');
+ return;
+ }
+ hasResponded.current = true;
+ console.log('Emitting kingRaResponse:', useKingRa);
+ emitKingRaResponse(useKingRa);
+ onClose();
+ };
+
+ const progressPercent = (timeLeft / timeout) * 100;
+
+ return (
+
+ e.stopPropagation()}
+ >
+ 👑 King Ra Says NO?
+
+
+
+ {playerName} played:
+
+
+ {cardDef?.nameEn}
+
+
+ {cardDef?.nameAr}
+
+
+
+ {/* Timer */}
+
+
+
+
+
+ {hasKingRa
+ ? 'Do you want to cancel this action?'
+ : "You don't have King Ra Says NO!"}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/modals/ModalRenderer.tsx b/client/src/components/modals/ModalRenderer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..fb096ebc459cc9836d1269270c442f0a0f53b832
--- /dev/null
+++ b/client/src/components/modals/ModalRenderer.tsx
@@ -0,0 +1,91 @@
+import { AnimatePresence } from 'framer-motion';
+import { useGameStore } from '../../store/gameStore';
+import { PeekCardsModal } from './PeekCardsModal';
+import { TargetSelectModal } from './TargetSelectModal';
+import { CardTypeSelectModal } from './CardTypeSelectModal';
+import { BlindStealModal } from './BlindStealModal';
+import { ArrangeHandModal } from './ArrangeHandModal';
+import { ViewHandModal } from './ViewHandModal';
+import { FlipTableModal } from './FlipTableModal';
+import { PlaceMummyModal } from './PlaceMummyModal';
+import { KingRaPromptModal } from './KingRaPromptModal';
+import { DuelResultModal } from './DuelResultModal';
+import { SwapResultModal } from './SwapResultModal';
+import { GameOverModal } from './GameOverModal';
+
+export function ModalRenderer() {
+ const activeModal = useGameStore((state) => state.activeModal);
+ const modalData = useGameStore((state) => state.modalData);
+ const closeModal = useGameStore((state) => state.closeModal);
+
+ const renderModal = () => {
+ switch (activeModal) {
+ case 'peek-cards':
+ return ;
+
+ case 'target-select':
+ return ;
+
+ case 'card-type-select':
+ return ;
+
+ case 'blind-steal':
+ return ;
+
+ case 'arrange-hand':
+ return ;
+
+ case 'view-hand':
+ return ;
+
+ case 'flip-table':
+ return ;
+
+ case 'place-mummy':
+ return ;
+
+ case 'king-ra-prompt':
+ return (
+
+ );
+
+ case 'duel-result':
+ return (
+
+ );
+
+ case 'swap-result':
+ return (
+
+ );
+
+ case 'game-over':
+ return ;
+
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+ {activeModal && renderModal()}
+
+ );
+}
diff --git a/client/src/components/modals/PeekCardsModal.tsx b/client/src/components/modals/PeekCardsModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2679a3288e507eabf84be7fc14e29baafaf3f503
--- /dev/null
+++ b/client/src/components/modals/PeekCardsModal.tsx
@@ -0,0 +1,57 @@
+import { motion } from 'framer-motion';
+import { Card } from '../game/Card';
+import type { CardInstance } from '@shared/types';
+
+interface PeekCardsModalProps {
+ cards: CardInstance[];
+ onClose: () => void;
+}
+
+export function PeekCardsModal({ cards, onClose }: PeekCardsModalProps) {
+ return (
+
+ e.stopPropagation()}
+ >
+ 👁️ Peek at the Deck
+
+ Top {cards.length} cards (first card is on top)
+
+
+
+ {cards.map((card, index) => (
+
+
+
+ {index + 1}
+
+ {card.cardId === 'mummified' && (
+ 💀
+ )}
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/modals/PlaceMummyModal.tsx b/client/src/components/modals/PlaceMummyModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5c96a11f1a09438ae64057447fbc8dbaf48bf482
--- /dev/null
+++ b/client/src/components/modals/PlaceMummyModal.tsx
@@ -0,0 +1,137 @@
+import { useState, useEffect, useRef } from 'react';
+import { motion } from 'framer-motion';
+import { emitPlaceMummy } from '../../socket/socket';
+
+interface PlaceMummyModalProps {
+ deckSize: number;
+ onClose: () => void;
+}
+
+const AUTO_CONFIRM_TIMEOUT = 5000; // 5 seconds
+
+export function PlaceMummyModal({ deckSize, onClose }: PlaceMummyModalProps) {
+ const [position, setPosition] = useState(Math.floor(deckSize / 2));
+ const [timeLeft, setTimeLeft] = useState(AUTO_CONFIRM_TIMEOUT / 1000);
+ const hasConfirmed = useRef(false);
+
+ const handleConfirm = () => {
+ if (hasConfirmed.current) return;
+ hasConfirmed.current = true;
+ emitPlaceMummy(position);
+ onClose();
+ };
+
+ // Auto-confirm timer
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setTimeLeft((prev) => {
+ if (prev <= 1) {
+ handleConfirm();
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+
+ return () => clearInterval(interval);
+ }, []);
+
+ const positionLabel = () => {
+ if (position === 0) return 'Top (next draw!)';
+ if (position === deckSize) return 'Bottom (last draw)';
+ if (position <= deckSize * 0.25) return 'Near top (risky!)';
+ if (position >= deckSize * 0.75) return 'Near bottom (safe)';
+ return 'Middle';
+ };
+
+ return (
+
+ e.stopPropagation()}
+ >
+ 💀 Hide the Mummy
+
+ Auto-confirm in {timeLeft}s
+
+
+ You defused the mummy! Now hide it back in the deck.
+
+
+ Only you know where it's placed!
+
+
+ {/* Visual deck representation */}
+
+
+ {/* Position indicator */}
+
+
+ 💀
+ Mummy Here
+
+
+
+ {/* Top label */}
+
Top (drawn first)
+
+ {/* Bottom label */}
+
Bottom (drawn last)
+
+
+
+ {/* Slider */}
+
+
setPosition(parseInt(e.target.value))}
+ className="w-full accent-egyptian-gold"
+ />
+
+ Top (0)
+ {positionLabel()}
+ Bottom ({deckSize})
+
+
+
+ {/* Number input */}
+
+
+ setPosition(Math.min(deckSize, Math.max(0, parseInt(e.target.value) || 0)))}
+ className="w-full mt-1 text-center"
+ />
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/modals/SwapResultModal.tsx b/client/src/components/modals/SwapResultModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1b8f3e990c0c1f8d3c4073922193468d03accf6a
--- /dev/null
+++ b/client/src/components/modals/SwapResultModal.tsx
@@ -0,0 +1,62 @@
+import { motion } from 'framer-motion';
+import type { CardInstance } from '@shared/types';
+import { Card } from '../game/Card';
+
+interface SwapResultModalProps {
+ gaveCard?: CardInstance;
+ receivedCard?: CardInstance;
+ otherPlayer?: string;
+ onClose: () => void;
+}
+
+export function SwapResultModal({ gaveCard, receivedCard, otherPlayer, onClose }: SwapResultModalProps) {
+ return (
+
+ e.stopPropagation()}
+ >
+ 🔄 Card Swap
+
+ Exchange with {otherPlayer}
+
+
+
+ {/* What you gave */}
+
+
You Gave
+ {gaveCard &&
}
+
+
+ {/* Arrow */}
+
+ ⇄
+
+
+ {/* What you received */}
+
+
You Received
+ {receivedCard &&
}
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/modals/TargetSelectModal.tsx b/client/src/components/modals/TargetSelectModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f977073e5c92aa3fed0b890c6bb20a85fdff6e5f
--- /dev/null
+++ b/client/src/components/modals/TargetSelectModal.tsx
@@ -0,0 +1,68 @@
+import { motion } from 'framer-motion';
+import type { Player } from '@shared/types';
+
+interface TargetSelectModalProps {
+ players: Player[];
+ onClose: () => void;
+}
+
+export function TargetSelectModal({ players, onClose }: TargetSelectModalProps) {
+ const handleSelect = (playerId: string) => {
+ // Call the callback set by GameBoard
+ // The callback returns true if we should NOT close (e.g., opening another modal)
+ const callback = (window as any).__targetSelectCallback;
+ if (callback) {
+ const keepOpen = callback(playerId);
+ if (keepOpen) return; // Don't close if another modal is being opened
+ }
+ onClose();
+ };
+
+ return (
+
+ e.stopPropagation()}
+ >
+ 🎯 Select Target
+ Choose an opponent
+
+
+ {players.map((player, index) => (
+
handleSelect(player.id)}
+ initial={{ opacity: 0, x: -20 }}
+ animate={{ opacity: 1, x: 0 }}
+ transition={{ delay: index * 0.1 }}
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ >
+
+
+ {player.name.charAt(0).toUpperCase()}
+
+
{player.name}
+
+ {player.cardCount} cards
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/modals/ViewHandModal.tsx b/client/src/components/modals/ViewHandModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..67a62460f5a5b5e9f8a1518fb951c07baeb4b068
--- /dev/null
+++ b/client/src/components/modals/ViewHandModal.tsx
@@ -0,0 +1,81 @@
+import { useState } from 'react';
+import { motion } from 'framer-motion';
+import { Card } from '../game/Card';
+import { emitBurnCard } from '../../socket/socket';
+import type { CardInstance } from '@shared/types';
+import { useGameStore } from '../../store/gameStore';
+
+interface ViewHandModalProps {
+ cards: CardInstance[];
+ targetId?: string;
+ onClose: () => void;
+}
+
+export function ViewHandModal({ cards, targetId, onClose }: ViewHandModalProps) {
+ const [selectedCard, setSelectedCard] = useState(null);
+ const gameState = useGameStore((state) => state.gameState);
+ const targetName = gameState?.players.find(p => p.id === targetId)?.name ?? 'Opponent';
+
+ const handleBurn = () => {
+ if (selectedCard) {
+ emitBurnCard(selectedCard);
+ onClose();
+ }
+ };
+
+ return (
+
+ e.stopPropagation()}
+ >
+ 👁️ {targetName}'s Hand
+
+ Select one card to BURN (discard)
+
+
+ 🔥 The chosen card will be destroyed!
+
+
+
+ {cards.map((card, index) => (
+ setSelectedCard(card.instanceId)}
+ >
+
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/screens/GameBoard.tsx b/client/src/components/screens/GameBoard.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..55c8f389b733d0638c7728457c74b0870c802f5d
--- /dev/null
+++ b/client/src/components/screens/GameBoard.tsx
@@ -0,0 +1,283 @@
+import { useState, useEffect } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { useGameStore } from '../../store/gameStore';
+import { Card, CardBack } from '../game/Card';
+import { emitDrawCard, emitPlayCard } from '../../socket/socket';
+import type { CardInstance, Player } from '@shared/types';
+import { CARD_DATABASE } from '@shared/types';
+
+export function GameBoard() {
+ const gameState = useGameStore((state) => state.gameState);
+ const myHand = useGameStore((state) => state.myHand);
+ const playerId = useGameStore((state) => state.playerId);
+ const isMyTurn = useGameStore((state) => state.isMyTurn);
+ const turnsRemaining = useGameStore((state) => state.turnsRemaining);
+ const selectedCards = useGameStore((state) => state.selectedCards);
+ const openModal = useGameStore((state) => state.openModal);
+ const addToast = useGameStore((state) => state.addToast);
+ const activeModal = useGameStore((state) => state.activeModal);
+ const reactionWindowActive = useGameStore((state) => state.reactionWindowActive);
+
+ const [pendingCard, setPendingCard] = useState(null);
+
+ // Clear pendingCard when relevant modals close (not target-select or card-type-select)
+ useEffect(() => {
+ if (pendingCard && activeModal !== 'target-select' && activeModal !== 'card-type-select') {
+ setPendingCard(null);
+ }
+ }, [activeModal]);
+
+ // Also clear pendingCard when it's no longer in hand (was played)
+ useEffect(() => {
+ if (pendingCard && !myHand.some(c => c.instanceId === pendingCard.instanceId)) {
+ setPendingCard(null);
+ }
+ }, [myHand, pendingCard]);
+
+ if (!gameState) {
+ return Loading game...
;
+ }
+
+ const currentPlayer = gameState.players[gameState.currentPlayerIndex];
+ const otherPlayers = gameState.players.filter(p => p.id !== playerId);
+ const myPlayer = gameState.players.find(p => p.id === playerId);
+
+ const handleCardClick = (card: CardInstance) => {
+ // Block card plays during reaction window (except handled by modal)
+ if (reactionWindowActive) {
+ addToast('Wait for reaction window to end!', 'warning');
+ return;
+ }
+
+ if (!isMyTurn) {
+ addToast("It's not your turn!", 'warning');
+ return;
+ }
+
+ const cardDef = CARD_DATABASE[card.cardId];
+
+ // Check if it's a half card
+ if (cardDef.isHalf) {
+ const count = myHand.filter(c => c.cardId === card.cardId).length;
+ if (count < 2) {
+ addToast('Half cards require 2 copies to play!', 'warning');
+ return;
+ }
+ }
+
+ // Check if card needs a target
+ const cardsNeedingTarget = [
+ 'me_or_you', 'spellbound', 'criminal_mummy', 'give_and_take',
+ 'all_or_nothing', 'this_is_on_you'
+ ];
+
+ if (cardsNeedingTarget.includes(card.cardId)) {
+ // Open target selection modal
+ setPendingCard(card);
+ const alivePlayers = otherPlayers.filter(p => p.isAlive);
+ openModal('target-select', { targetPlayers: alivePlayers });
+ return;
+ }
+
+ // Special handling for spellbound (needs card type selection too)
+ // This is handled after target selection in modal
+
+ // Play the card directly
+ emitPlayCard(card.instanceId);
+ };
+
+ const handleDrawCard = () => {
+ if (!isMyTurn) {
+ addToast("It's not your turn!", 'warning');
+ return;
+ }
+ emitDrawCard();
+ };
+
+ const handleTargetSelect = (targetId: string): boolean => {
+ if (!pendingCard) return false;
+
+ if (pendingCard.cardId === 'spellbound') {
+ // Emit the card play first to consume the card
+ emitPlayCard(pendingCard.instanceId, targetId);
+ // Then open card type selection modal
+ openModal('card-type-select', { spellboundTargetId: targetId });
+ setPendingCard(null);
+ return true; // Keep target modal from closing since we're opening another
+ } else {
+ emitPlayCard(pendingCard.instanceId, targetId);
+ setPendingCard(null);
+ return false;
+ }
+ };
+
+ // Subscribe to target selection from modal
+ const setTargetSelectCallback = (callback: (targetId: string) => void) => {
+ (window as any).__targetSelectCallback = callback;
+ };
+ setTargetSelectCallback(handleTargetSelect);
+
+ return (
+
+ {/* Top area - Other players */}
+
+ {otherPlayers.map((player) => (
+
+ ))}
+
+
+ {/* Middle area - Deck and discard */}
+
+ {/* Deck */}
+
+
+ Deck
+
+
+ {/* Turn info */}
+
+
+
+ {currentPlayer?.id === playerId ? "Your Turn!" : `${currentPlayer?.name}'s Turn`}
+
+ {turnsRemaining > 1 && (
+
+ {turnsRemaining} turns remaining
+
+ )}
+
+
+
+ {/* Discard pile */}
+
+ {gameState.discardPile.length > 0 ? (
+
+ ) : (
+
+ )}
+
Discard ({gameState.discardPile.length})
+
+
+
+ {/* Bottom area - My hand */}
+
+ {/* My player info */}
+
+
+
+ {myPlayer?.name.charAt(0).toUpperCase()}
+
+
{myPlayer?.name} (You)
+
+
{myHand.length} cards
+
+
+ {/* Hand */}
+
+
+ {myHand.map((card, index) => (
+
+ handleCardClick(card)}
+ disabled={!isMyTurn}
+ />
+
+ ))}
+
+
+
+ {/* Draw button for mobile */}
+ {isMyTurn && (
+
+ Draw Card (End Turn)
+
+ )}
+
+
+ );
+}
+
+interface PlayerDisplayProps {
+ player: Player;
+ isCurrentTurn: boolean;
+}
+
+function PlayerDisplay({ player, isCurrentTurn }: PlayerDisplayProps) {
+ return (
+
+
+
+ {player.name.charAt(0).toUpperCase()}
+
+
+
+ {player.name}
+
+ {!player.isAlive &&
☠️ Mummified
}
+
+
+
+ {player.isAlive && (
+
+ {Array.from({ length: Math.min(player.cardCount, 8) }).map((_, i) => (
+
+ ))}
+ {player.cardCount > 8 && (
+
+{player.cardCount - 8}
+ )}
+
+ )}
+
+ );
+}
diff --git a/client/src/components/screens/Lobby.tsx b/client/src/components/screens/Lobby.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..716d8f009ae95b55e93812a0ff48f025d70bf8f0
--- /dev/null
+++ b/client/src/components/screens/Lobby.tsx
@@ -0,0 +1,185 @@
+import { useState, useEffect } from 'react';
+import { motion } from 'framer-motion';
+import { useGameStore } from '../../store/gameStore';
+import { emitCreateRoom, emitJoinRoom, emitGetRooms } from '../../socket/socket';
+
+export function Lobby() {
+ const [roomName, setRoomName] = useState('');
+ const [joinCode, setJoinCode] = useState('');
+ const [activeTab, setActiveTab] = useState<'create' | 'join' | 'browse'>('create');
+
+ const playerName = useGameStore((state) => state.playerName);
+ const roomList = useGameStore((state) => state.roomList);
+ const setScreen = useGameStore((state) => state.setScreen);
+
+ useEffect(() => {
+ emitGetRooms();
+ const interval = setInterval(emitGetRooms, 5000);
+ return () => clearInterval(interval);
+ }, []);
+
+ const handleCreateRoom = () => {
+ if (!roomName.trim()) {
+ useGameStore.getState().addToast('Please enter a room name!', 'warning');
+ return;
+ }
+ emitCreateRoom(playerName, roomName.trim());
+ };
+
+ const handleJoinByCode = () => {
+ if (!joinCode.trim()) {
+ useGameStore.getState().addToast('Please enter a room code!', 'warning');
+ return;
+ }
+ emitJoinRoom(playerName, joinCode.trim().toUpperCase());
+ };
+
+ const handleJoinRoom = (roomId: string) => {
+ emitJoinRoom(playerName, roomId);
+ };
+
+ const handleBack = () => {
+ setScreen('menu');
+ };
+
+ const availableRooms = roomList.filter(r => !r.isPlaying && r.playerCount < r.maxPlayers);
+
+ return (
+
+
+ {/* Header */}
+
+
+
Lobby
+ {playerName}
+
+
+ {/* Tabs */}
+
+ {(['create', 'join', 'browse'] as const).map((tab) => (
+
+ ))}
+
+
+ {/* Create Tab */}
+ {activeTab === 'create' && (
+
+ Create a new room for your friends to join.
+
+
+
+ setRoomName(e.target.value)}
+ placeholder="My Game Room"
+ className="w-full"
+ maxLength={30}
+ />
+
+
+
+
+ )}
+
+ {/* Join Tab */}
+ {activeTab === 'join' && (
+
+ Enter a room code to join.
+
+
+
+ setJoinCode(e.target.value.toUpperCase())}
+ placeholder="ABC123"
+ className="w-full uppercase tracking-widest text-center text-xl"
+ maxLength={6}
+ />
+
+
+
+
+ )}
+
+ {/* Browse Tab */}
+ {activeTab === 'browse' && (
+
+ Available rooms:
+
+ {availableRooms.length === 0 ? (
+
+ No rooms available. Create one!
+
+ ) : (
+
+ {availableRooms.map((room) => (
+
+
+
{room.name}
+
+ {room.playerCount}/{room.maxPlayers} players
+
+
+
+
+ ))}
+
+ )}
+
+
+
+ )}
+
+
+ );
+}
diff --git a/client/src/components/screens/MainMenu.tsx b/client/src/components/screens/MainMenu.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..49043da790569649d4671cda1ff27b4fe7280507
--- /dev/null
+++ b/client/src/components/screens/MainMenu.tsx
@@ -0,0 +1,90 @@
+import { useState } from 'react';
+import { motion } from 'framer-motion';
+import { useGameStore } from '../../store/gameStore';
+
+export function MainMenu() {
+ const [playerName, setPlayerName] = useState('');
+ const setScreen = useGameStore((state) => state.setScreen);
+ const setPlayerNameInStore = useGameStore((state) => state.setPlayerName);
+ const isConnected = useGameStore((state) => state.isConnected);
+
+ const handlePlay = () => {
+ if (!playerName.trim()) {
+ useGameStore.getState().addToast('Please enter your name!', 'warning');
+ return;
+ }
+ setPlayerNameInStore(playerName.trim());
+ setScreen('lobby');
+ };
+
+ return (
+
+
+ {/* Logo */}
+ {
+ (e.target as HTMLImageElement).style.display = 'none';
+ }}
+ />
+
+
+ Mummy Card Game
+
+
+ هتتحنط هنا
+
+
+ {/* Connection status */}
+
+
+
+ {isConnected ? 'Connected to server' : 'Connecting...'}
+
+
+
+ {/* Name input */}
+
+
+ setPlayerName(e.target.value)}
+ placeholder="Enter your name..."
+ className="w-full"
+ maxLength={20}
+ onKeyDown={(e) => e.key === 'Enter' && handlePlay()}
+ />
+
+
+ {/* Play button */}
+
+ Play Game
+
+
+
+ );
+}
diff --git a/client/src/components/screens/Room.tsx b/client/src/components/screens/Room.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..228c9e05efa3d1c414630f46ae3c6b8a73aea7d4
--- /dev/null
+++ b/client/src/components/screens/Room.tsx
@@ -0,0 +1,161 @@
+import { motion } from 'framer-motion';
+import { useGameStore } from '../../store/gameStore';
+import { emitSetReady, emitStartGame, emitLeaveRoom } from '../../socket/socket';
+
+export function Room() {
+ const currentRoom = useGameStore((state) => state.currentRoom);
+ const playerId = useGameStore((state) => state.playerId);
+
+ if (!currentRoom) {
+ return Loading...
;
+ }
+
+ const isHost = currentRoom.hostId === playerId;
+ const currentPlayer = currentRoom.players.find(p => p.id === playerId);
+ const isReady = currentPlayer?.isReady ?? false;
+
+ const allReady = currentRoom.players.every(p => p.isReady || p.isHost);
+ const canStart = currentRoom.players.length >= 2 && (allReady || isHost);
+
+ const handleLeave = () => {
+ emitLeaveRoom();
+ };
+
+ const handleReady = () => {
+ emitSetReady(!isReady);
+ };
+
+ const handleStart = () => {
+ emitStartGame();
+ };
+
+ const copyRoomCode = () => {
+ navigator.clipboard.writeText(currentRoom.id);
+ useGameStore.getState().addToast('Room code copied!', 'success');
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
{currentRoom.name}
+
+ {currentRoom.players.length}/{currentRoom.maxPlayers}
+
+
+
+ {/* Room Code */}
+
+
Room Code:
+
+
Click to copy
+
+
+ {/* Players */}
+
+
Players
+
+ {currentRoom.players.map((player, index) => (
+
+
+
+ {player.name.charAt(0).toUpperCase()}
+
+
+
+ {player.name}
+ {player.id === playerId && ' (You)'}
+
+ {player.isHost && (
+
👑 Host
+ )}
+
+
+
+ {player.isHost ? (
+ Ready
+ ) : player.isReady ? (
+ ✓ Ready
+ ) : (
+ Waiting...
+ )}
+
+
+ ))}
+
+
+ {/* Empty slots */}
+ {Array.from({ length: currentRoom.maxPlayers - currentRoom.players.length }).map((_, i) => (
+
+
Waiting for player...
+
+ ))}
+
+
+ {/* Actions */}
+
+ {!isHost && (
+
+ )}
+
+ {isHost && (
+
+ )}
+
+
+ {/* Min players warning */}
+ {currentRoom.players.length < 2 && (
+
+ Need at least 2 players to start
+
+ )}
+
+
+ );
+}
diff --git a/client/src/components/ui/LandscapeOverlay.tsx b/client/src/components/ui/LandscapeOverlay.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..710b320de8397b7e756f1f6e8ce54ce87a0761f1
--- /dev/null
+++ b/client/src/components/ui/LandscapeOverlay.tsx
@@ -0,0 +1,32 @@
+export function LandscapeOverlay() {
+ return (
+
+
📱
+
+ Rotate Your Device
+
+
+ Please rotate your device to landscape mode
+
+
+ من فضلك قم بتدوير جهازك
+
+
+
+ );
+}
diff --git a/client/src/components/ui/MummyEventOverlay.tsx b/client/src/components/ui/MummyEventOverlay.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ed66c38dcb4f3112650dd9fb5c9224b18071d58d
--- /dev/null
+++ b/client/src/components/ui/MummyEventOverlay.tsx
@@ -0,0 +1,100 @@
+import { motion, AnimatePresence } from 'framer-motion';
+import { useGameStore } from '../../store/gameStore';
+
+export function MummyEventOverlay() {
+ const mummyEvent = useGameStore((state) => state.mummyEvent);
+
+ if (!mummyEvent) return null;
+
+ const getEventStyle = () => {
+ switch (mummyEvent.type) {
+ case 'drawn':
+ return {
+ bg: 'from-amber-900/95 to-amber-950/95',
+ icon: '⚠️',
+ title: 'MUMMY DRAWN!',
+ subtitle: `${mummyEvent.playerName} drew a mummy card!`,
+ textColor: 'text-amber-300',
+ };
+ case 'defused':
+ return {
+ bg: 'from-green-900/95 to-green-950/95',
+ icon: '🛡️',
+ title: 'MUMMY DEFUSED!',
+ subtitle: `${mummyEvent.playerName} defused the mummy!`,
+ textColor: 'text-green-300',
+ };
+ case 'eliminated':
+ return {
+ bg: 'from-red-900/95 to-red-950/95',
+ icon: '💀',
+ title: 'MUMMIFIED!',
+ subtitle: `${mummyEvent.playerName} has been eliminated!`,
+ textColor: 'text-red-300',
+ };
+ }
+ };
+
+ const style = getEventStyle();
+
+ return (
+
+
+
+
+ {style.icon}
+
+
+
+ {style.title}
+
+
+
+ {style.subtitle}
+
+
+
+
+ );
+}
diff --git a/client/src/components/ui/ToastContainer.tsx b/client/src/components/ui/ToastContainer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..45142e7a3a8a82563dbfa8c59b50e47cec9a87c7
--- /dev/null
+++ b/client/src/components/ui/ToastContainer.tsx
@@ -0,0 +1,26 @@
+import { motion, AnimatePresence } from 'framer-motion';
+import { useGameStore } from '../../store/gameStore';
+
+export function ToastContainer() {
+ const toasts = useGameStore((state) => state.toasts);
+ const removeToast = useGameStore((state) => state.removeToast);
+
+ return (
+
+
+ {toasts.map((toast) => (
+ removeToast(toast.id)}
+ >
+ {toast.message}
+
+ ))}
+
+
+ );
+}
diff --git a/client/src/index.css b/client/src/index.css
new file mode 100644
index 0000000000000000000000000000000000000000..30bb2fe0d65246e5360ed8122636d2049f5b5aca
--- /dev/null
+++ b/client/src/index.css
@@ -0,0 +1,281 @@
+@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&display=swap');
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* Base styles */
+* {
+ box-sizing: border-box;
+ -webkit-tap-highlight-color: transparent;
+}
+
+html, body {
+ margin: 0;
+ padding: 0;
+ overflow: hidden;
+ touch-action: none;
+ user-select: none;
+ -webkit-user-select: none;
+}
+
+body {
+ font-family: 'Cinzel', serif;
+ background-color: #1a1a2e;
+ color: #f5f5dc;
+ min-height: 100vh;
+ min-height: 100dvh;
+}
+
+#root {
+ min-height: 100vh;
+ min-height: 100dvh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* Landscape lock overlay */
+.landscape-required {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
+ z-index: 9999;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ text-align: center;
+ padding: 20px;
+}
+
+@media screen and (max-width: 768px) and (orientation: portrait) {
+ .landscape-required {
+ display: flex !important;
+ }
+
+ .game-content {
+ display: none !important;
+ }
+}
+
+/* Card styles */
+.card {
+ @apply relative rounded-lg overflow-hidden cursor-pointer transition-all duration-200;
+ aspect-ratio: 2/3;
+ transform-style: preserve-3d;
+ perspective: 1000px;
+}
+
+.card:hover {
+ @apply scale-105;
+ z-index: 10;
+}
+
+.card.selected {
+ @apply ring-4 ring-egyptian-gold scale-110;
+ z-index: 20;
+}
+
+.card.disabled {
+ @apply opacity-50 cursor-not-allowed;
+}
+
+.card.disabled:hover {
+ transform: none;
+}
+
+.card-image {
+ @apply w-full h-full object-cover;
+}
+
+.card-overlay {
+ @apply absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent;
+}
+
+.card-name {
+ @apply absolute bottom-0 left-0 right-0 p-2 text-center;
+}
+
+.card-name-en {
+ @apply text-xs font-semibold text-papyrus;
+}
+
+.card-name-ar {
+ @apply text-sm font-bold text-egyptian-gold;
+ direction: rtl;
+}
+
+/* Half card indicator */
+.half-indicator {
+ @apply absolute top-1 right-1 bg-egyptian-gold text-black text-xs font-bold px-1 rounded;
+}
+
+/* Mummy card danger */
+.card.mummy {
+ @apply animate-pulse-danger;
+}
+
+/* Button styles */
+.btn {
+ @apply px-6 py-3 rounded-lg font-semibold transition-all duration-200;
+ @apply disabled:opacity-50 disabled:cursor-not-allowed;
+}
+
+.btn-primary {
+ @apply bg-egyptian-gold text-nile-blue hover:bg-yellow-500;
+ @apply active:scale-95;
+}
+
+.btn-danger {
+ @apply bg-mummy-red text-white hover:bg-red-700;
+ @apply active:scale-95;
+}
+
+.btn-secondary {
+ @apply bg-nile-blue text-papyrus border-2 border-egyptian-gold;
+ @apply hover:bg-egyptian-gold hover:text-nile-blue;
+ @apply active:scale-95;
+}
+
+/* Modal styles */
+.modal-overlay {
+ @apply fixed inset-0 bg-black/70 flex items-center justify-center z-50;
+ @apply backdrop-blur-sm;
+}
+
+.modal-content {
+ @apply bg-gradient-to-br from-nile-blue to-gray-900 rounded-xl p-6;
+ @apply border-2 border-egyptian-gold shadow-2xl;
+ @apply max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto;
+}
+
+.modal-title {
+ @apply text-xl font-bold text-egyptian-gold mb-4 text-center;
+}
+
+/* Toast/Notification styles */
+.toast {
+ @apply fixed top-4 left-1/2 transform -translate-x-1/2 z-50;
+ @apply px-6 py-3 rounded-lg font-semibold shadow-lg;
+ @apply animate-bounce;
+}
+
+.toast-info {
+ @apply bg-nile-blue text-papyrus border border-egyptian-gold;
+}
+
+.toast-success {
+ @apply bg-green-700 text-white;
+}
+
+.toast-warning {
+ @apply bg-yellow-600 text-black;
+}
+
+.toast-danger {
+ @apply bg-mummy-red text-white;
+}
+
+/* Player avatar */
+.player-avatar {
+ @apply w-12 h-12 rounded-full bg-egyptian-gold flex items-center justify-center;
+ @apply text-nile-blue font-bold text-lg;
+}
+
+.player-avatar.current-turn {
+ @apply ring-4 ring-green-500 animate-pulse;
+}
+
+.player-avatar.eliminated {
+ @apply opacity-50 grayscale;
+}
+
+/* Deck pile */
+.deck-pile {
+ @apply relative cursor-pointer;
+}
+
+.deck-pile::after {
+ @apply absolute -bottom-1 -right-1 bg-egyptian-gold text-nile-blue;
+ @apply text-xs font-bold px-2 py-1 rounded;
+ content: attr(data-count);
+}
+
+/* Game board */
+.game-board {
+ @apply flex-1 flex flex-col justify-between p-4;
+ background-size: cover;
+ background-position: center;
+}
+
+/* Hand area */
+.hand-area {
+ @apply flex justify-center items-end gap-1 p-2;
+ max-width: 100%;
+ flex-wrap: wrap;
+ overflow: hidden;
+}
+
+.hand-area .card {
+ @apply flex-shrink-0;
+ width: 60px;
+ max-width: 80px;
+}
+
+@media (min-width: 768px) {
+ .hand-area {
+ @apply gap-2 p-4;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ }
+
+ .hand-area .card {
+ width: 100px;
+ max-width: 120px;
+ }
+}
+
+/* Scrollbar styling */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #d4a843;
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #e5b854;
+}
+
+/* King Ra countdown */
+.king-ra-timer {
+ @apply w-full h-2 bg-gray-700 rounded-full overflow-hidden mt-2;
+}
+
+.king-ra-timer-bar {
+ @apply h-full bg-gradient-to-r from-egyptian-gold to-mummy-red;
+ transition: width 0.1s linear;
+}
+
+/* Input styles */
+input[type="text"], input[type="number"] {
+ @apply w-full px-4 py-2 rounded-lg bg-nile-blue/50 border border-egyptian-gold;
+ @apply text-papyrus placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-egyptian-gold;
+}
+
+/* Room code display */
+.room-code {
+ @apply bg-nile-blue px-4 py-2 rounded-lg text-2xl font-bold tracking-widest;
+ @apply border-2 border-egyptian-gold text-egyptian-gold;
+}
diff --git a/client/src/main.tsx b/client/src/main.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2339d59cf67910c2bd93c28509f754e71476374e
--- /dev/null
+++ b/client/src/main.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './index.css';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/client/src/socket/socket.ts b/client/src/socket/socket.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6fcfc430f3525a3ff08015ffb0dd4303072c511c
--- /dev/null
+++ b/client/src/socket/socket.ts
@@ -0,0 +1,330 @@
+import { io, Socket } from 'socket.io-client';
+import type { ServerToClientEvents, ClientToServerEvents, CardId } from '@shared/types';
+import { useGameStore } from '../store/gameStore';
+
+const SERVER_URL = import.meta.env.PROD
+ ? window.location.origin
+ : 'http://localhost:3001';
+
+export const socket: Socket = io(SERVER_URL, {
+ autoConnect: false,
+ transports: ['websocket'],
+ upgrade: false,
+ reconnectionAttempts: 5,
+ reconnectionDelay: 1000,
+});
+
+let isInitialized = false;
+const processedEvents = new Set();
+
+// Prevent duplicate event processing (clears every 5 seconds)
+function shouldProcessEvent(eventKey: string): boolean {
+ if (processedEvents.has(eventKey)) return false;
+ processedEvents.add(eventKey);
+ setTimeout(() => processedEvents.delete(eventKey), 5000);
+ return true;
+}
+
+export function initializeSocket(): void {
+ // Prevent multiple initializations (React StrictMode, hot reload)
+ if (isInitialized) {
+ if (!socket.connected) {
+ socket.connect();
+ }
+ return;
+ }
+ isInitialized = true;
+
+ const store = useGameStore.getState();
+
+ socket.on('connect', () => {
+ console.log('Connected to server:', socket.id);
+ store.setPlayerId(socket.id ?? null);
+ store.setConnected(true);
+ socket.emit('getRooms');
+ });
+
+ socket.on('disconnect', () => {
+ console.log('Disconnected from server');
+ store.setConnected(false);
+ });
+
+ socket.on('error', (message) => {
+ store.addToast(message, 'danger');
+ });
+
+ // Room events
+ socket.on('roomList', (rooms) => {
+ store.setRoomList(rooms);
+ });
+
+ socket.on('roomJoined', (room) => {
+ store.setRoom(room);
+ store.setScreen('room');
+ });
+
+ socket.on('roomUpdated', (room) => {
+ store.setRoom(room);
+ });
+
+ socket.on('roomLeft', () => {
+ store.setRoom(null);
+ store.setScreen('lobby');
+ socket.emit('getRooms');
+ });
+
+ // Game events
+ socket.on('gameStarted', (state, hand) => {
+ store.setGameState(state);
+ store.setMyHand(hand);
+ store.setScreen('game');
+ });
+
+ socket.on('gameStateUpdated', (state) => {
+ store.setGameState(state);
+ });
+
+ socket.on('handUpdated', (hand) => {
+ store.setMyHand(hand);
+ });
+
+ // Card events
+ socket.on('cardPlayed', (_playerId, _cardId, _targetId) => {
+ // Card play events are handled visually, no toast needed
+ });
+
+ socket.on('peekCards', (cards) => {
+ const eventKey = `peekCards-${cards.map(c => c.instanceId).join(',')}`;
+ if (shouldProcessEvent(eventKey)) {
+ store.openModal('peek-cards', { peekCards: cards });
+ }
+ });
+
+ socket.on('duelResult', (challenger, opponent, winnerId) => {
+ const eventKey = `duelResult-${challenger.card.instanceId}-${opponent.card.instanceId}`;
+ if (shouldProcessEvent(eventKey)) {
+ store.openModal('duel-result', {
+ duelChallenger: challenger,
+ duelOpponent: opponent,
+ duelWinnerId: winnerId,
+ });
+ }
+ });
+
+ socket.on('swapResult', (youGave, youReceived, otherPlayerName) => {
+ const eventKey = `swapResult-${youGave.instanceId}-${youReceived.instanceId}`;
+ if (shouldProcessEvent(eventKey)) {
+ store.openModal('swap-result', {
+ swapGaveCard: youGave,
+ swapReceivedCard: youReceived,
+ swapOtherPlayer: otherPlayerName,
+ });
+ }
+ });
+
+ socket.on('spellboundResult', (_success, _cardId) => {
+ // Result shown in modal/game state update
+ });
+
+ socket.on('blindStealStart', (_thiefId, _targetId, cardCount) => {
+ const state = useGameStore.getState();
+ if (socket.id === state.playerId) {
+ const eventKey = `blindSteal-${Date.now()}`;
+ if (shouldProcessEvent(eventKey)) {
+ store.openModal('blind-steal', { blindStealCount: cardCount });
+ }
+ }
+ });
+
+ socket.on('arrangeHandPrompt', (_thiefId) => {
+ const eventKey = `arrangeHand-${Date.now()}`;
+ if (shouldProcessEvent(eventKey)) {
+ store.openModal('arrange-hand');
+ }
+ });
+
+ socket.on('viewHand', (targetId, cards) => {
+ const eventKey = `viewHand-${targetId}-${cards.map(c => c.instanceId).join(',')}`;
+ if (shouldProcessEvent(eventKey)) {
+ store.openModal('view-hand', { viewHandCards: cards, viewHandTargetId: targetId });
+ }
+ });
+
+ socket.on('flipTableCards', (cards) => {
+ const eventKey = `flipTable-${cards.map(c => c.instanceId).join(',')}`;
+ if (shouldProcessEvent(eventKey)) {
+ store.openModal('flip-table', { flipTableCards: cards });
+ }
+ });
+
+ // King Ra events
+ socket.on('kingRaPrompt', (playerId, cardPlayed, timeout) => {
+ const state = useGameStore.getState();
+
+ // Don't show prompt to the player who played the card (they can't cancel their own action)
+ if (playerId === state.playerId) {
+ return;
+ }
+
+ // Don't show if action was recently resolved (stale event from network delay)
+ if (Date.now() - state.lastActionResolvedAt < 2000) {
+ return;
+ }
+
+ // Don't show if a non-related modal is open (action already resolved and moved on)
+ const currentModal = state.activeModal;
+ if (currentModal && currentModal !== 'king-ra-prompt') {
+ // Another modal is already showing (like duel result), skip this
+ return;
+ }
+
+ // Show the reaction window to ALL players (they can only use it if they have King Ra)
+ store.openModal('king-ra-prompt', {
+ kingRaPlayerId: playerId,
+ kingRaCardPlayed: cardPlayed,
+ kingRaTimeout: timeout,
+ });
+ });
+
+ socket.on('kingRaResponse', (_responderId, _didCancel) => {
+ // King Ra response is shown visually
+ });
+
+ socket.on('actionPending', (_cardId: CardId, _timeout: number) => {
+ store.setReactionWindowActive(true);
+ });
+
+ socket.on('actionResolved', (_cardId: CardId) => {
+ // Reaction window ended, action proceeds - close King Ra modal if open
+ const state = useGameStore.getState();
+ if (state.activeModal === 'king-ra-prompt') {
+ store.closeModal();
+ }
+ store.setReactionWindowActive(false);
+ store.setLastActionResolvedAt(Date.now());
+ });
+
+ socket.on('actionCancelled', (_cardId: CardId, _cancelCount) => {
+ // Close King Ra modal if open
+ const state = useGameStore.getState();
+ if (state.activeModal === 'king-ra-prompt') {
+ store.closeModal();
+ }
+ store.setReactionWindowActive(false);
+ store.setLastActionResolvedAt(Date.now());
+ });
+
+ // Mummy events
+ socket.on('mummyDrawn', (playerId) => {
+ const state = useGameStore.getState().gameState;
+ const player = state?.players.find(p => p.id === playerId);
+ store.setMummyEvent({ type: 'drawn', playerName: player?.name ?? 'A player' });
+ });
+
+ socket.on('mummyDefused', (playerId) => {
+ const state = useGameStore.getState().gameState;
+ const player = state?.players.find(p => p.id === playerId);
+ store.setMummyEvent({ type: 'defused', playerName: player?.name ?? 'A player' });
+ });
+
+ socket.on('placeMummyPrompt', (deckSize) => {
+ const eventKey = `placeMummy-${deckSize}-${Date.now()}`;
+ if (shouldProcessEvent(eventKey)) {
+ store.openModal('place-mummy', { deckSize });
+ }
+ });
+
+ socket.on('playerEliminated', (playerId) => {
+ const state = useGameStore.getState().gameState;
+ const player = state?.players.find(p => p.id === playerId);
+ store.setMummyEvent({ type: 'eliminated', playerName: player?.name ?? 'A player' });
+ });
+
+ // Turn events
+ socket.on('turnStarted', (playerId, turnsRemaining) => {
+ store.setTurnInfo(playerId, turnsRemaining);
+ });
+
+ // Ignore server notifications - only show mummy events via overlay
+ socket.on('notification', () => {
+ // Notifications disabled - mummy events handled separately
+ });
+
+ // Game over
+ socket.on('gameOver', (winnerId, winnerName) => {
+ const eventKey = `gameOver-${winnerId}`;
+ if (shouldProcessEvent(eventKey)) {
+ store.openModal('game-over', { winnerId, winnerName });
+ }
+ });
+
+ // Connect
+ socket.connect();
+}
+
+export function disconnectSocket(): void {
+ socket.disconnect();
+}
+
+// Emit helpers
+export const emitCreateRoom = (playerName: string, roomName: string): void => {
+ socket.emit('createRoom', playerName, roomName);
+};
+
+export const emitJoinRoom = (playerName: string, roomId: string): void => {
+ socket.emit('joinRoom', playerName, roomId);
+};
+
+export const emitLeaveRoom = (): void => {
+ socket.emit('leaveRoom');
+};
+
+export const emitSetReady = (ready: boolean): void => {
+ socket.emit('setReady', ready);
+};
+
+export const emitStartGame = (): void => {
+ socket.emit('startGame');
+};
+
+export const emitPlayCard = (cardInstanceId: string, targetPlayerId?: string, additionalData?: unknown): void => {
+ socket.emit('playCard', cardInstanceId, targetPlayerId, additionalData);
+};
+
+export const emitDrawCard = (): void => {
+ socket.emit('drawCard');
+};
+
+export const emitSpellboundRequest = (targetId: string, requestedCardId: string): void => {
+ socket.emit('spellboundRequest', targetId, requestedCardId as any);
+};
+
+export const emitBlindStealSelect = (position: number): void => {
+ socket.emit('blindStealSelect', position);
+};
+
+export const emitArrangeHand = (newOrder: string[]): void => {
+ socket.emit('arrangeHand', newOrder);
+};
+
+export const emitBurnCard = (cardInstanceId: string): void => {
+ socket.emit('burnCard', cardInstanceId);
+};
+
+export const emitRearrangeTopCards = (newOrder: string[]): void => {
+ socket.emit('rearrangeTopCards', newOrder);
+};
+
+export const emitPlaceMummy = (position: number): void => {
+ socket.emit('placeMummy', position);
+};
+
+export const emitKingRaResponse = (useKingRa: boolean): void => {
+ console.log('socket.ts emitKingRaResponse called with:', useKingRa);
+ socket.emit('kingRaResponse', useKingRa);
+ console.log('socket.ts kingRaResponse emitted');
+};
+
+export const emitGetRooms = (): void => {
+ socket.emit('getRooms');
+};
diff --git a/client/src/store/gameStore.ts b/client/src/store/gameStore.ts
new file mode 100644
index 0000000000000000000000000000000000000000..93cb9b554a8e9905c17598f0cb884ed4f2fbeb0d
--- /dev/null
+++ b/client/src/store/gameStore.ts
@@ -0,0 +1,241 @@
+import { create } from 'zustand';
+import type { Room, GameState, CardInstance, Player, CardId } from '@shared/types';
+
+export type Screen = 'menu' | 'lobby' | 'room' | 'game';
+
+export type ModalType =
+ | 'peek-cards'
+ | 'target-select'
+ | 'card-type-select'
+ | 'blind-steal'
+ | 'arrange-hand'
+ | 'view-hand'
+ | 'flip-table'
+ | 'place-mummy'
+ | 'king-ra-prompt'
+ | 'duel-result'
+ | 'swap-result'
+ | 'game-over'
+ | null;
+
+export interface Toast {
+ id: string;
+ message: string;
+ type: 'info' | 'warning' | 'success' | 'danger';
+}
+
+export interface MummyEvent {
+ type: 'drawn' | 'defused' | 'eliminated';
+ playerName: string;
+}
+
+export interface ModalData {
+ peekCards?: CardInstance[];
+ targetPlayers?: Player[];
+ blindStealCount?: number;
+ viewHandCards?: CardInstance[];
+ viewHandTargetId?: string;
+ flipTableCards?: CardInstance[];
+ deckSize?: number;
+ kingRaPlayerId?: string;
+ kingRaCardPlayed?: CardId;
+ kingRaTimeout?: number;
+ duelChallenger?: { playerId: string; card: CardInstance };
+ duelOpponent?: { playerId: string; card: CardInstance };
+ duelWinnerId?: string;
+ spellboundTargetId?: string;
+ swapGaveCard?: CardInstance;
+ swapReceivedCard?: CardInstance;
+ swapOtherPlayer?: string;
+ winnerId?: string;
+ winnerName?: string;
+}
+
+interface GameStore {
+ // Connection
+ playerId: string | null;
+ playerName: string;
+ isConnected: boolean;
+
+ // Navigation
+ currentScreen: Screen;
+
+ // Room state
+ currentRoom: Room | null;
+ roomList: { id: string; name: string; playerCount: number; maxPlayers: number; isPlaying: boolean }[];
+
+ // Game state
+ gameState: GameState | null;
+ myHand: CardInstance[];
+ selectedCards: string[]; // instanceIds
+
+ // Turn state
+ isMyTurn: boolean;
+ currentTurnPlayerId: string | null;
+ turnsRemaining: number;
+ reactionWindowActive: boolean;
+ lastActionResolvedAt: number; // Timestamp when last action resolved
+
+ // UI state
+ activeModal: ModalType;
+ modalData: ModalData;
+ toasts: Toast[];
+ mummyEvent: MummyEvent | null;
+
+ // Actions
+ setPlayerId: (id: string | null) => void;
+ setPlayerName: (name: string) => void;
+ setConnected: (connected: boolean) => void;
+ setScreen: (screen: Screen) => void;
+ setRoom: (room: Room | null) => void;
+ setRoomList: (rooms: { id: string; name: string; playerCount: number; maxPlayers: number; isPlaying: boolean }[]) => void;
+ setGameState: (state: GameState | null) => void;
+ setMyHand: (hand: CardInstance[]) => void;
+ toggleCardSelection: (instanceId: string) => void;
+ clearSelection: () => void;
+ setTurnInfo: (playerId: string, turnsRemaining: number) => void;
+ setReactionWindowActive: (active: boolean) => void;
+ setLastActionResolvedAt: (timestamp: number) => void;
+ openModal: (type: ModalType, data?: ModalData) => void;
+ closeModal: () => void;
+ addToast: (message: string, type: Toast['type']) => void;
+ removeToast: (id: string) => void;
+ setMummyEvent: (event: MummyEvent | null) => void;
+ reset: () => void;
+}
+
+export const useGameStore = create((set, get) => ({
+ // Initial state
+ playerId: null,
+ playerName: '',
+ isConnected: false,
+ currentScreen: 'menu',
+ currentRoom: null,
+ roomList: [],
+ gameState: null,
+ myHand: [],
+ selectedCards: [],
+ isMyTurn: false,
+ currentTurnPlayerId: null,
+ turnsRemaining: 1,
+ reactionWindowActive: false,
+ lastActionResolvedAt: 0,
+ activeModal: null,
+ modalData: {},
+ toasts: [],
+ mummyEvent: null,
+
+ // Actions
+ setPlayerId: (id) => set({ playerId: id }),
+
+ setPlayerName: (name) => set({ playerName: name }),
+
+ setConnected: (connected) => set({ isConnected: connected }),
+
+ setScreen: (screen) => set({ currentScreen: screen }),
+
+ setRoom: (room) => set({ currentRoom: room }),
+
+ setRoomList: (rooms) => set({ roomList: rooms }),
+
+ setGameState: (state) => {
+ const { playerId } = get();
+ const isMyTurn = state?.players[state.currentPlayerIndex]?.id === playerId;
+ set({
+ gameState: state,
+ isMyTurn,
+ currentTurnPlayerId: state?.players[state.currentPlayerIndex]?.id ?? null,
+ turnsRemaining: state?.turnsRemaining ?? 1,
+ });
+ },
+
+ setMyHand: (hand) => set({ myHand: hand }),
+
+ toggleCardSelection: (instanceId) => {
+ const { selectedCards } = get();
+ if (selectedCards.includes(instanceId)) {
+ set({ selectedCards: selectedCards.filter(id => id !== instanceId) });
+ } else {
+ set({ selectedCards: [...selectedCards, instanceId] });
+ }
+ },
+
+ clearSelection: () => set({ selectedCards: [] }),
+
+ setTurnInfo: (playerId, turnsRemaining) => {
+ const { playerId: myId } = get();
+ set({
+ currentTurnPlayerId: playerId,
+ turnsRemaining,
+ isMyTurn: playerId === myId,
+ });
+ },
+
+ setReactionWindowActive: (active) => set({ reactionWindowActive: active }),
+
+ setLastActionResolvedAt: (timestamp) => set({ lastActionResolvedAt: timestamp }),
+
+ openModal: (type, data = {}) => {
+ const state = get();
+ // Prevent opening the same modal type if it's already open
+ if (state.activeModal === type) return;
+
+ // When opening a result modal (duel, swap, etc.), clear the reaction window state
+ // This prevents King Ra modal from re-appearing after action completes
+ const resultModals = ['duel-result', 'swap-result', 'peek-cards', 'view-hand', 'blind-steal'];
+ if (resultModals.includes(type as string)) {
+ set({ activeModal: type, modalData: data, reactionWindowActive: false });
+ } else {
+ set({ activeModal: type, modalData: data });
+ }
+ },
+
+ closeModal: () => set({ activeModal: null, modalData: {} }),
+
+ addToast: (message, type) => {
+ const id = Date.now().toString();
+ set((state) => ({
+ toasts: [...state.toasts, { id, message, type }],
+ }));
+ // Auto remove after 4 seconds
+ setTimeout(() => {
+ get().removeToast(id);
+ }, 4000);
+ },
+
+ removeToast: (id) => {
+ set((state) => ({
+ toasts: state.toasts.filter(t => t.id !== id),
+ }));
+ },
+
+ setMummyEvent: (event) => {
+ set({ mummyEvent: event });
+ // Auto-clear after 3 seconds
+ if (event) {
+ setTimeout(() => {
+ const current = get().mummyEvent;
+ // Only clear if it's the same event
+ if (current?.type === event.type && current?.playerName === event.playerName) {
+ set({ mummyEvent: null });
+ }
+ }, 3000);
+ }
+ },
+
+ reset: () => set({
+ currentScreen: 'menu',
+ currentRoom: null,
+ gameState: null,
+ myHand: [],
+ selectedCards: [],
+ isMyTurn: false,
+ currentTurnPlayerId: null,
+ turnsRemaining: 1,
+ reactionWindowActive: false,
+ lastActionResolvedAt: 0,
+ activeModal: null,
+ modalData: {},
+ mummyEvent: null,
+ }),
+}));
diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9e4ce1db7cde383134c62ead467346d9442cdd07
--- /dev/null
+++ b/client/src/vite-env.d.ts
@@ -0,0 +1,10 @@
+///
+
+interface ImportMetaEnv {
+ readonly PROD: boolean;
+ readonly DEV: boolean;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/client/tailwind.config.js b/client/tailwind.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..af1f306def082a161e8e7261b937f5dc0395566b
--- /dev/null
+++ b/client/tailwind.config.js
@@ -0,0 +1,48 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ 'egyptian-gold': '#d4a843',
+ 'egyptian-brown': '#8b4513',
+ 'papyrus': '#f5f5dc',
+ 'sand': '#c2b280',
+ 'nile-blue': '#1a3a5c',
+ 'mummy-red': '#8b0000',
+ },
+ fontFamily: {
+ 'game': ['Cinzel', 'serif'],
+ },
+ animation: {
+ 'card-flip': 'cardFlip 0.6s ease-in-out',
+ 'card-glow': 'cardGlow 1.5s ease-in-out infinite',
+ 'shake': 'shake 0.5s ease-in-out',
+ 'pulse-danger': 'pulseDanger 1s ease-in-out infinite',
+ },
+ keyframes: {
+ cardFlip: {
+ '0%': { transform: 'rotateY(0deg)' },
+ '100%': { transform: 'rotateY(180deg)' },
+ },
+ cardGlow: {
+ '0%, 100%': { boxShadow: '0 0 5px rgba(212, 168, 67, 0.5)' },
+ '50%': { boxShadow: '0 0 20px rgba(212, 168, 67, 0.8)' },
+ },
+ shake: {
+ '0%, 100%': { transform: 'translateX(0)' },
+ '25%': { transform: 'translateX(-5px)' },
+ '75%': { transform: 'translateX(5px)' },
+ },
+ pulseDanger: {
+ '0%, 100%': { boxShadow: '0 0 5px rgba(139, 0, 0, 0.5)' },
+ '50%': { boxShadow: '0 0 20px rgba(139, 0, 0, 1)' },
+ },
+ },
+ },
+ },
+ plugins: [],
+}
diff --git a/client/tsconfig.json b/client/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..f8e4e9102ab5f37f02560d39f630af4c142f6cea
--- /dev/null
+++ b/client/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "paths": {
+ "@/*": ["./src/*"],
+ "@shared/*": ["../shared/*"],
+ "@assets/*": ["../Assests/*"]
+ },
+ "baseUrl": "."
+ },
+ "include": ["src", "../shared"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/client/tsconfig.node.json b/client/tsconfig.node.json
new file mode 100644
index 0000000000000000000000000000000000000000..42872c59f5b01c9155864572bc2fbd5833a7406c
--- /dev/null
+++ b/client/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/client/vite.config.ts b/client/vite.config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2196f63393f1eab519e3e5e3da385daa2357b575
--- /dev/null
+++ b/client/vite.config.ts
@@ -0,0 +1,27 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react()],
+ publicDir: path.resolve(__dirname, '../Assests'),
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ '@shared': path.resolve(__dirname, '../shared'),
+ },
+ },
+ build: {
+ outDir: 'dist',
+ emptyOutDir: true,
+ },
+ server: {
+ port: 5173,
+ proxy: {
+ '/socket.io': {
+ target: 'http://localhost:3001',
+ ws: true,
+ },
+ },
+ },
+});
diff --git a/deploy.md b/deploy.md
new file mode 100644
index 0000000000000000000000000000000000000000..2bc52e04a6045f3ed48659137566f6a3fe2331f9
--- /dev/null
+++ b/deploy.md
@@ -0,0 +1,492 @@
+# Mummy Card Game - Deployment Guide
+
+This guide covers multiple ways to deploy your Mummy Card Game for use outside localhost.
+
+---
+
+## Table of Contents
+1. [Quick Deploy with Render (Recommended)](#option-1-render-recommended---free)
+2. [Deploy with Railway](#option-2-railway)
+3. [Deploy with Fly.io](#option-3-flyio)
+4. [Deploy with VPS (DigitalOcean/AWS)](#option-4-vps-digitalocean-aws-etc)
+5. [Local Network (LAN Party)](#option-5-local-network-lan-party)
+
+---
+
+## Prerequisites
+
+Before deploying, you need to prepare your project:
+
+### Step 1: Update Server to Serve Static Files
+
+First, modify your server to serve the built client files in production.
+
+Edit `server/src/index.ts`:
+
+```typescript
+import express from 'express';
+import { createServer } from 'http';
+import { Server } from 'socket.io';
+import cors from 'cors';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { RoomManager } from './rooms/RoomManager.js';
+import { setupSocketHandlers } from './socket/handlers.js';
+import type { ServerToClientEvents, ClientToServerEvents } from '../../shared/types.js';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const app = express();
+app.use(cors());
+
+// Serve static files from client build
+const clientPath = path.join(__dirname, '../../client/dist');
+app.use(express.static(clientPath));
+
+const httpServer = createServer(app);
+
+const io = new Server(httpServer, {
+ cors: {
+ origin: '*', // Allow all origins in production
+ methods: ['GET', 'POST'],
+ },
+});
+
+const roomManager = new RoomManager();
+
+io.on('connection', (socket) => {
+ console.log(`Player connected: ${socket.id}`);
+ setupSocketHandlers(io, socket, roomManager);
+
+ socket.on('disconnect', () => {
+ console.log(`Player disconnected: ${socket.id}`);
+ roomManager.handleDisconnect(socket.id, io);
+ });
+});
+
+// Serve index.html for all non-API routes (SPA support)
+app.get('*', (req, res) => {
+ res.sendFile(path.join(clientPath, 'index.html'));
+});
+
+const PORT = process.env.PORT || 3001;
+
+httpServer.listen(PORT, () => {
+ console.log(`🎮 Mummy Card Game Server running on port ${PORT}`);
+});
+```
+
+### Step 2: Update Vite Build Config
+
+Edit `client/vite.config.ts` to set the correct output directory:
+
+```typescript
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react()],
+ publicDir: path.resolve(__dirname, '../Assests'),
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ '@shared': path.resolve(__dirname, '../shared'),
+ },
+ },
+ build: {
+ outDir: 'dist',
+ emptyOutDir: true,
+ },
+ server: {
+ port: 5173,
+ proxy: {
+ '/socket.io': {
+ target: 'http://localhost:3001',
+ ws: true,
+ },
+ },
+ },
+});
+```
+
+### Step 3: Create Root package.json
+
+Create a `package.json` in the root `Web` folder:
+
+```json
+{
+ "name": "mummy-card-game",
+ "version": "1.0.0",
+ "scripts": {
+ "install:all": "cd client && npm install && cd ../server && npm install",
+ "build:client": "cd client && npm run build",
+ "build:server": "cd server && npm run build",
+ "build": "npm run build:client && npm run build:server",
+ "start": "cd server && npm start",
+ "dev": "cd server && npm run dev"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+}
+```
+
+---
+
+## Option 1: Render (Recommended - Free)
+
+Render offers free hosting for web services with WebSocket support.
+
+### Step 1: Push to GitHub
+
+```bash
+# In the Web folder
+git init
+git add .
+git commit -m "Initial commit"
+git branch -M main
+git remote add origin https://github.com/YOUR_USERNAME/mummy-card-game.git
+git push -u origin main
+```
+
+### Step 2: Create Render Account
+1. Go to [render.com](https://render.com)
+2. Sign up with GitHub
+
+### Step 3: Create Web Service
+1. Click **"New +"** → **"Web Service"**
+2. Connect your GitHub repository
+3. Configure:
+ - **Name**: `mummy-card-game`
+ - **Region**: Choose closest to you
+ - **Branch**: `main`
+ - **Root Directory**: Leave empty (or `Web` if repo has parent folder)
+ - **Runtime**: `Node`
+ - **Build Command**: `cd client && npm install && npm run build && cd ../server && npm install && npm run build`
+ - **Start Command**: `cd server && npm start`
+ - **Instance Type**: Free
+
+4. Click **"Create Web Service"**
+
+### Step 4: Wait for Build
+- Build takes 2-5 minutes
+- Your app will be available at: `https://mummy-card-game.onrender.com`
+
+### Note on Free Tier
+- Free services spin down after 15 minutes of inactivity
+- First request after spin-down takes ~30 seconds
+- Upgrade to paid ($7/month) for always-on
+
+---
+
+## Option 2: Railway
+
+Railway offers simple deployment with generous free tier.
+
+### Step 1: Push to GitHub (same as above)
+
+### Step 2: Create Railway Account
+1. Go to [railway.app](https://railway.app)
+2. Sign up with GitHub
+
+### Step 3: Deploy
+1. Click **"New Project"**
+2. Select **"Deploy from GitHub repo"**
+3. Choose your repository
+4. Railway auto-detects Node.js
+
+### Step 4: Configure
+1. Go to **Settings** → **General**
+2. Set **Root Directory**: `/` (or where Web folder is)
+3. Go to **Settings** → **Build**
+4. Set **Build Command**: `cd client && npm install && npm run build && cd ../server && npm install && npm run build`
+5. Set **Start Command**: `cd server && npm start`
+
+### Step 5: Generate Domain
+1. Go to **Settings** → **Networking**
+2. Click **"Generate Domain"**
+3. Your app is live!
+
+---
+
+## Option 3: Fly.io
+
+Fly.io provides excellent WebSocket support and global edge deployment.
+
+### Step 1: Install Fly CLI
+
+```bash
+# Windows (PowerShell)
+pwsh -Command "iwr https://fly.io/install.ps1 -useb | iex"
+
+# Or with npm
+npm install -g flyctl
+```
+
+### Step 2: Login
+```bash
+fly auth login
+```
+
+### Step 3: Create fly.toml
+
+Create `fly.toml` in the `Web` folder:
+
+```toml
+app = "mummy-card-game"
+primary_region = "iad"
+
+[build]
+ builder = "heroku/buildpacks:20"
+
+[env]
+ PORT = "8080"
+ NODE_ENV = "production"
+
+[http_service]
+ internal_port = 8080
+ force_https = true
+ auto_stop_machines = true
+ auto_start_machines = true
+ min_machines_running = 0
+
+[[services]]
+ internal_port = 8080
+ protocol = "tcp"
+
+ [[services.ports]]
+ handlers = ["http"]
+ port = 80
+
+ [[services.ports]]
+ handlers = ["tls", "http"]
+ port = 443
+```
+
+### Step 4: Create Dockerfile
+
+Create `Dockerfile` in the `Web` folder:
+
+```dockerfile
+FROM node:18-alpine
+
+WORKDIR /app
+
+# Copy package files
+COPY client/package*.json ./client/
+COPY server/package*.json ./server/
+
+# Install dependencies
+RUN cd client && npm install
+RUN cd server && npm install
+
+# Copy source code
+COPY . .
+
+# Build client and server
+RUN cd client && npm run build
+RUN cd server && npm run build
+
+# Expose port
+EXPOSE 8080
+
+# Start server
+CMD ["node", "server/dist/index.js"]
+```
+
+### Step 5: Deploy
+```bash
+fly launch
+fly deploy
+```
+
+Your app will be at: `https://mummy-card-game.fly.dev`
+
+---
+
+## Option 4: VPS (DigitalOcean, AWS, etc.)
+
+For full control, deploy to a VPS.
+
+### Step 1: Create VPS
+1. Create a Ubuntu 22.04 droplet/instance
+2. SSH into your server
+
+### Step 2: Install Node.js
+```bash
+curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
+sudo apt-get install -y nodejs
+```
+
+### Step 3: Clone and Build
+```bash
+git clone https://github.com/YOUR_USERNAME/mummy-card-game.git
+cd mummy-card-game/Web
+
+# Install and build
+cd client && npm install && npm run build
+cd ../server && npm install && npm run build
+```
+
+### Step 4: Install PM2 (Process Manager)
+```bash
+sudo npm install -g pm2
+```
+
+### Step 5: Start with PM2
+```bash
+cd server
+pm2 start dist/index.js --name mummy-game
+pm2 save
+pm2 startup
+```
+
+### Step 6: Setup Nginx (Optional - for domain/SSL)
+```bash
+sudo apt install nginx
+sudo nano /etc/nginx/sites-available/mummy-game
+```
+
+Add:
+```nginx
+server {
+ listen 80;
+ server_name yourdomain.com;
+
+ location / {
+ proxy_pass http://localhost:3001;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ }
+}
+```
+
+Enable and restart:
+```bash
+sudo ln -s /etc/nginx/sites-available/mummy-game /etc/nginx/sites-enabled/
+sudo nginx -t
+sudo systemctl restart nginx
+```
+
+### Step 7: Setup SSL with Let's Encrypt
+```bash
+sudo apt install certbot python3-certbot-nginx
+sudo certbot --nginx -d yourdomain.com
+```
+
+---
+
+## Option 5: Local Network (LAN Party)
+
+Play with friends on the same WiFi network.
+
+### Step 1: Find Your IP Address
+
+```bash
+# Windows
+ipconfig
+# Look for "IPv4 Address" under your WiFi adapter (e.g., 192.168.1.100)
+
+# Mac/Linux
+ifconfig
+# or
+ip addr
+```
+
+### Step 2: Update Server CORS
+
+In `server/src/index.ts`, update CORS:
+
+```typescript
+const io = new Server(httpServer, {
+ cors: {
+ origin: '*', // Allow all
+ methods: ['GET', 'POST'],
+ },
+});
+```
+
+### Step 3: Build and Run
+
+```bash
+# Build client
+cd client
+npm run build
+
+# Start server
+cd ../server
+npm run build
+npm start
+```
+
+### Step 4: Connect
+- Your device: `http://localhost:3001`
+- Other devices on network: `http://YOUR_IP:3001` (e.g., `http://192.168.1.100:3001`)
+
+### Tip: Allow Through Firewall
+
+Windows:
+```powershell
+# Run as Administrator
+netsh advfirewall firewall add rule name="Mummy Game" dir=in action=allow protocol=TCP localport=3001
+```
+
+---
+
+## Troubleshooting
+
+### WebSocket Connection Failed
+- Ensure your hosting supports WebSockets
+- Check if server CORS allows your domain
+- Verify the client is connecting to the correct URL
+
+### Build Fails
+- Check Node.js version (needs 18+)
+- Run `npm install` in both client and server folders
+- Check for TypeScript errors: `npm run build`
+
+### Assets Not Loading
+- Ensure `Assests` folder is copied to `client/dist` after build
+- Or configure Vite to copy it (already done via `publicDir`)
+
+### Free Tier Limits
+- Render: 750 hours/month free
+- Railway: $5 credit/month
+- Fly.io: 3 shared VMs free
+
+---
+
+## Quick Commands Reference
+
+```bash
+# Local development
+cd server && npm run dev # Start server (dev mode)
+cd client && npm run dev # Start client (dev mode)
+
+# Production build
+cd client && npm run build
+cd server && npm run build
+cd server && npm start
+
+# Or with root package.json
+npm run build
+npm start
+```
+
+---
+
+## Sharing with Friends
+
+Once deployed, share your URL:
+1. **Render**: `https://your-app-name.onrender.com`
+2. **Railway**: `https://your-app-name.up.railway.app`
+3. **Fly.io**: `https://your-app-name.fly.dev`
+4. **Custom VPS**: `https://yourdomain.com`
+5. **LAN**: `http://YOUR_LOCAL_IP:3001`
+
+Players just need to open the link in their browser - no installation required!
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..ff55b5e68dce8761101aee47278c91a3129b1e52
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "mummy-card-game",
+ "version": "1.0.0",
+ "description": "Multiplayer Egyptian-themed card game with real-time gameplay",
+ "scripts": {
+ "install:all": "cd client && npm install && cd ../server && npm install",
+ "build:client": "cd client && npm run build",
+ "build:server": "cd server && npm run build",
+ "build": "npm run build:client && npm run build:server",
+ "start": "cd server && npm start",
+ "dev": "cd server && npm run dev"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "keywords": [
+ "card-game",
+ "multiplayer",
+ "socket.io",
+ "react",
+ "typescript"
+ ],
+ "license": "MIT"
+}
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..b896a088e192ee3a9dbe560c544fcccce3e854a3
--- /dev/null
+++ b/readme.md
@@ -0,0 +1 @@
+npm run dev
\ No newline at end of file
diff --git a/server/package-lock.json b/server/package-lock.json
new file mode 100644
index 0000000000000000000000000000000000000000..2ad62b4177d307b91b860c788552621adc10a9d5
--- /dev/null
+++ b/server/package-lock.json
@@ -0,0 +1,1770 @@
+{
+ "name": "mummy-card-game-server",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "mummy-card-game-server",
+ "version": "1.0.0",
+ "dependencies": {
+ "cors": "^2.8.5",
+ "express": "^4.18.2",
+ "socket.io": "^4.7.2",
+ "uuid": "^9.0.0"
+ },
+ "devDependencies": {
+ "@types/cors": "^2.8.14",
+ "@types/express": "^4.17.18",
+ "@types/node": "^20.8.0",
+ "@types/uuid": "^9.0.3",
+ "tsx": "^4.1.0",
+ "typescript": "^5.2.2"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
+ "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
+ "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
+ "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
+ "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
+ "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
+ "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
+ "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
+ "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
+ "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
+ "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
+ "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
+ "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
+ "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
+ "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
+ "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
+ "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
+ "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
+ "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
+ "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
+ "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
+ "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.25",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
+ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "^1"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.19.8",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz",
+ "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "20.19.33",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz",
+ "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.10",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
+ "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "<1"
+ }
+ },
+ "node_modules/@types/serve-static/node_modules/@types/send": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
+ "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/uuid": {
+ "version": "9.0.8",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
+ "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "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==",
+ "license": "MIT",
+ "engines": {
+ "node": "^4.5.0 || >= 5.9"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/engine.io": {
+ "version": "6.6.5",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz",
+ "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/cors": "^2.8.12",
+ "@types/node": ">=10.0.0",
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "~0.7.2",
+ "cors": "~2.8.5",
+ "debug": "~4.4.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.18.3"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/engine.io/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
+ "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.3",
+ "@esbuild/android-arm": "0.27.3",
+ "@esbuild/android-arm64": "0.27.3",
+ "@esbuild/android-x64": "0.27.3",
+ "@esbuild/darwin-arm64": "0.27.3",
+ "@esbuild/darwin-x64": "0.27.3",
+ "@esbuild/freebsd-arm64": "0.27.3",
+ "@esbuild/freebsd-x64": "0.27.3",
+ "@esbuild/linux-arm": "0.27.3",
+ "@esbuild/linux-arm64": "0.27.3",
+ "@esbuild/linux-ia32": "0.27.3",
+ "@esbuild/linux-loong64": "0.27.3",
+ "@esbuild/linux-mips64el": "0.27.3",
+ "@esbuild/linux-ppc64": "0.27.3",
+ "@esbuild/linux-riscv64": "0.27.3",
+ "@esbuild/linux-s390x": "0.27.3",
+ "@esbuild/linux-x64": "0.27.3",
+ "@esbuild/netbsd-arm64": "0.27.3",
+ "@esbuild/netbsd-x64": "0.27.3",
+ "@esbuild/openbsd-arm64": "0.27.3",
+ "@esbuild/openbsd-x64": "0.27.3",
+ "@esbuild/openharmony-arm64": "0.27.3",
+ "@esbuild/sunos-x64": "0.27.3",
+ "@esbuild/win32-arm64": "0.27.3",
+ "@esbuild/win32-ia32": "0.27.3",
+ "@esbuild/win32-x64": "0.27.3"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.13.6",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
+ "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/socket.io": {
+ "version": "4.8.3",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
+ "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "base64id": "~2.0.0",
+ "cors": "~2.8.5",
+ "debug": "~4.4.1",
+ "engine.io": "~6.6.0",
+ "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.6",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz",
+ "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "~4.4.1",
+ "ws": "~8.18.3"
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-adapter/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
+ "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.4.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/socket.io/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tsx": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.27.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "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
+ }
+ }
+ }
+ }
+}
diff --git a/server/package.json b/server/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..b3024a9be04933afff6f0767ed0ba66ae2b84fd3
--- /dev/null
+++ b/server/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "mummy-card-game-server",
+ "version": "1.0.0",
+ "description": "Multiplayer Mummy Card Game Server",
+ "main": "dist/index.js",
+ "scripts": {
+ "dev": "tsx watch src/index.ts",
+ "build": "tsc",
+ "start": "node dist/index.js"
+ },
+ "dependencies": {
+ "cors": "^2.8.5",
+ "express": "^4.18.2",
+ "socket.io": "^4.7.2",
+ "uuid": "^9.0.0"
+ },
+ "devDependencies": {
+ "@types/cors": "^2.8.14",
+ "@types/express": "^4.17.18",
+ "@types/node": "^20.8.0",
+ "@types/uuid": "^9.0.3",
+ "tsx": "^4.1.0",
+ "typescript": "^5.2.2"
+ }
+}
diff --git a/server/src/game/GameEngine.ts b/server/src/game/GameEngine.ts
new file mode 100644
index 0000000000000000000000000000000000000000..32ee0ff48f8a825083d8d9626c910af282916b3b
--- /dev/null
+++ b/server/src/game/GameEngine.ts
@@ -0,0 +1,415 @@
+import { v4 as uuidv4 } from 'uuid';
+import type {
+ CardId,
+ CardInstance,
+ Player,
+ GameState,
+ PlayerHand,
+ CARD_DATABASE,
+ PendingAction,
+} from '../../../shared/types.js';
+import { CARD_DATABASE as CardDB } from '../../../shared/types.js';
+
+// Helper to shuffle array
+function shuffleArray(array: T[]): T[] {
+ const shuffled = [...array];
+ for (let i = shuffled.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
+ }
+ return shuffled;
+}
+
+// Helper to create card instances
+function createCardInstances(cardId: CardId, count: number): CardInstance[] {
+ return Array.from({ length: count }, () => ({
+ instanceId: uuidv4(),
+ cardId,
+ }));
+}
+
+export class GameEngine {
+ private deck: CardInstance[] = [];
+ private discardPile: CardInstance[] = [];
+ private playerHands: Map = new Map();
+ private players: Player[] = [];
+ private currentPlayerIndex: number = 0;
+ private turnsRemaining: number = 1;
+ private phase: 'waiting' | 'playing' | 'game_over' = 'waiting';
+ private winnerId: string | null = null;
+
+ // Pending action for King Ra reactions
+ public pendingAction: PendingAction | null = null;
+
+ // Callbacks for socket events
+ public onStateChange?: (state: GameState) => void;
+ public onHandChange?: (playerId: string, hand: CardInstance[]) => void;
+ public onNotification?: (playerId: string | null, message: string, type: 'info' | 'warning' | 'success' | 'danger') => void;
+
+ constructor(players: Player[]) {
+ this.players = players.map(p => ({ ...p, isAlive: true, cardCount: 0 }));
+ }
+
+ // Initialize the game
+ initializeGame(): void {
+ const playerCount = this.players.length;
+
+ // Create the deck based on player count
+ this.deck = [];
+
+ // Add N copies of each normal card
+ const normalCards: CardId[] = [
+ 'sharp_eye', 'wait_a_sec', 'me_or_you', 'spellbound',
+ 'criminal_mummy', 'shuffle_it', 'king_ra_says_no', 'safe_travels'
+ ];
+
+ for (const cardId of normalCards) {
+ this.deck.push(...createCardInstances(cardId, playerCount));
+ }
+
+ // Add N copies of each half card
+ const halfCards: CardId[] = [
+ 'give_and_take', 'all_or_nothing', 'flip_the_table', 'this_is_on_you'
+ ];
+
+ for (const cardId of halfCards) {
+ this.deck.push(...createCardInstances(cardId, playerCount));
+ }
+
+ // Add Take a Lap cards: N+1 total (N distributed, 1 in deck)
+ const defuseCards = createCardInstances('take_a_lap', playerCount + 1);
+
+ // Add Mummy cards: N-1
+ const mummyCards = createCardInstances('mummified', playerCount - 1);
+
+ // Shuffle deck (without defuse and mummy for now)
+ this.deck = shuffleArray(this.deck);
+
+ // Distribute starting hands
+ for (const player of this.players) {
+ const hand: CardInstance[] = [];
+
+ // Give 1 defuse card
+ hand.push(defuseCards.pop()!);
+
+ // Give 4 random cards from deck (non-mummy, non-defuse)
+ for (let i = 0; i < 4 && this.deck.length > 0; i++) {
+ hand.push(this.deck.pop()!);
+ }
+
+ this.playerHands.set(player.id, shuffleArray(hand));
+ player.cardCount = hand.length;
+ }
+
+ // Add remaining defuse card to deck
+ if (defuseCards.length > 0) {
+ this.deck.push(...defuseCards);
+ }
+
+ // Add mummy cards to deck
+ this.deck.push(...mummyCards);
+
+ // Final shuffle
+ this.deck = shuffleArray(this.deck);
+
+ // Start the game
+ this.phase = 'playing';
+ this.currentPlayerIndex = 0;
+ this.turnsRemaining = 1;
+ }
+
+ // Get current game state (public info only)
+ getGameState(): GameState {
+ return {
+ phase: this.phase,
+ players: this.players.map(p => ({
+ ...p,
+ cardCount: this.playerHands.get(p.id)?.length ?? 0,
+ })),
+ currentPlayerIndex: this.currentPlayerIndex,
+ turnsRemaining: this.turnsRemaining,
+ deckCount: this.deck.length,
+ discardPile: this.discardPile.map(c => c.cardId),
+ winnerId: this.winnerId,
+ };
+ }
+
+ // Get a player's hand
+ getPlayerHand(playerId: string): CardInstance[] {
+ return this.playerHands.get(playerId) ?? [];
+ }
+
+ // Get current player
+ getCurrentPlayer(): Player | null {
+ return this.players[this.currentPlayerIndex] ?? null;
+ }
+
+ // Check if it's a player's turn
+ isPlayerTurn(playerId: string): boolean {
+ const current = this.getCurrentPlayer();
+ return current?.id === playerId && this.phase === 'playing';
+ }
+
+ // Check if player has card
+ hasCard(playerId: string, cardId: CardId): boolean {
+ const hand = this.playerHands.get(playerId);
+ return hand?.some(c => c.cardId === cardId) ?? false;
+ }
+
+ // Count cards of type in hand
+ countCards(playerId: string, cardId: CardId): number {
+ const hand = this.playerHands.get(playerId);
+ return hand?.filter(c => c.cardId === cardId).length ?? 0;
+ }
+
+ // Find card instance in hand
+ findCardInstance(playerId: string, instanceId: string): CardInstance | null {
+ const hand = this.playerHands.get(playerId);
+ return hand?.find(c => c.instanceId === instanceId) ?? null;
+ }
+
+ // Remove card from hand
+ removeCardFromHand(playerId: string, instanceId: string): CardInstance | null {
+ const hand = this.playerHands.get(playerId);
+ if (!hand) return null;
+
+ const index = hand.findIndex(c => c.instanceId === instanceId);
+ if (index === -1) return null;
+
+ const [card] = hand.splice(index, 1);
+ this.updatePlayerCardCount(playerId);
+ return card;
+ }
+
+ // Add card to hand
+ addCardToHand(playerId: string, card: CardInstance): void {
+ const hand = this.playerHands.get(playerId);
+ if (hand) {
+ hand.push(card);
+ this.updatePlayerCardCount(playerId);
+ }
+ }
+
+ // Update player card count
+ private updatePlayerCardCount(playerId: string): void {
+ const player = this.players.find(p => p.id === playerId);
+ if (player) {
+ player.cardCount = this.playerHands.get(playerId)?.length ?? 0;
+ }
+ }
+
+ // Discard card
+ discardCard(card: CardInstance): void {
+ this.discardPile.push(card);
+ }
+
+ // Draw top card from deck
+ drawTopCard(): CardInstance | null {
+ return this.deck.pop() ?? null;
+ }
+
+ // Peek at top N cards
+ peekTopCards(count: number): CardInstance[] {
+ const startIndex = Math.max(0, this.deck.length - count);
+ return this.deck.slice(startIndex).reverse(); // Top card first
+ }
+
+ // Insert card at position in deck (0 = top)
+ insertCardInDeck(card: CardInstance, position: number): void {
+ const actualPosition = this.deck.length - position;
+ this.deck.splice(Math.max(0, actualPosition), 0, card);
+ }
+
+ // Rearrange top N cards
+ rearrangeTopCards(newOrder: string[]): void {
+ const count = newOrder.length;
+ const topCards = this.deck.splice(-count);
+
+ // Sort by new order
+ const orderedCards = newOrder.map(instanceId =>
+ topCards.find(c => c.instanceId === instanceId)!
+ ).filter(Boolean);
+
+ // Put back in reverse (so first in array is on top)
+ this.deck.push(...orderedCards.reverse());
+ }
+
+ // Shuffle deck
+ shuffleDeck(): void {
+ this.deck = shuffleArray(this.deck);
+ }
+
+ // Get deck size
+ getDeckSize(): number {
+ return this.deck.length;
+ }
+
+ // End current turn
+ endTurn(skipDraw: boolean = false): void {
+ this.turnsRemaining--;
+
+ if (this.turnsRemaining <= 0) {
+ this.moveToNextPlayer();
+ }
+ }
+
+ // Move to next alive player
+ private moveToNextPlayer(): void {
+ const alivePlayers = this.players.filter(p => p.isAlive);
+
+ if (alivePlayers.length <= 1) {
+ this.endGame(alivePlayers[0]?.id ?? null);
+ return;
+ }
+
+ // Find next alive player
+ let nextIndex = (this.currentPlayerIndex + 1) % this.players.length;
+ while (!this.players[nextIndex].isAlive) {
+ nextIndex = (nextIndex + 1) % this.players.length;
+ }
+
+ this.currentPlayerIndex = nextIndex;
+ this.turnsRemaining = 1;
+ }
+
+ // Set turns remaining (for Safe Travels)
+ setNextPlayerTurns(turns: number): void {
+ // Find next player
+ const alivePlayers = this.players.filter(p => p.isAlive);
+ if (alivePlayers.length <= 1) return;
+
+ let nextIndex = (this.currentPlayerIndex + 1) % this.players.length;
+ while (!this.players[nextIndex].isAlive) {
+ nextIndex = (nextIndex + 1) % this.players.length;
+ }
+
+ this.currentPlayerIndex = nextIndex;
+ this.turnsRemaining = turns;
+ }
+
+ // Eliminate player
+ eliminatePlayer(playerId: string): void {
+ const player = this.players.find(p => p.id === playerId);
+ if (!player) return;
+
+ player.isAlive = false;
+
+ // Discard their hand
+ const hand = this.playerHands.get(playerId) ?? [];
+ for (const card of hand) {
+ this.discardCard(card);
+ }
+ this.playerHands.set(playerId, []);
+ player.cardCount = 0;
+
+ // Check win condition
+ const alivePlayers = this.players.filter(p => p.isAlive);
+ if (alivePlayers.length === 1) {
+ this.endGame(alivePlayers[0].id);
+ } else if (this.getCurrentPlayer()?.id === playerId) {
+ // If eliminated player was current, move to next
+ this.moveToNextPlayer();
+ }
+ }
+
+ // End game
+ private endGame(winnerId: string | null): void {
+ this.phase = 'game_over';
+ this.winnerId = winnerId;
+ }
+
+ // Check if player can use Take a Lap (defuse)
+ canDefuse(playerId: string): boolean {
+ return this.hasCard(playerId, 'take_a_lap');
+ }
+
+ // Use defuse card
+ useDefuse(playerId: string): CardInstance | null {
+ const hand = this.playerHands.get(playerId);
+ if (!hand) return null;
+
+ const defuseIndex = hand.findIndex(c => c.cardId === 'take_a_lap');
+ if (defuseIndex === -1) return null;
+
+ const [defuseCard] = hand.splice(defuseIndex, 1);
+ this.discardCard(defuseCard);
+ this.updatePlayerCardCount(playerId);
+
+ return defuseCard;
+ }
+
+ // Get alive players except one
+ getOtherAlivePlayers(excludeId: string): Player[] {
+ return this.players.filter(p => p.isAlive && p.id !== excludeId);
+ }
+
+ // Get player by ID
+ getPlayer(playerId: string): Player | null {
+ return this.players.find(p => p.id === playerId) ?? null;
+ }
+
+ // Find lowest value card in hand (excluding defuse and mummy)
+ getLowestValueCard(playerId: string): CardInstance | null {
+ const hand = this.playerHands.get(playerId);
+ if (!hand || hand.length === 0) return null;
+
+ const playableCards = hand.filter(c =>
+ c.cardId !== 'take_a_lap' && c.cardId !== 'mummified'
+ );
+
+ if (playableCards.length === 0) return null;
+
+ return playableCards.reduce((lowest, card) => {
+ const lowestValue = CardDB[lowest.cardId].value;
+ const cardValue = CardDB[card.cardId].value;
+ return cardValue < lowestValue ? card : lowest;
+ });
+ }
+
+ // Find highest value card in hand (excluding mummy)
+ getHighestValueCard(playerId: string): CardInstance | null {
+ const hand = this.playerHands.get(playerId);
+ if (!hand || hand.length === 0) return null;
+
+ const playableCards = hand.filter(c => c.cardId !== 'mummified');
+
+ if (playableCards.length === 0) return null;
+
+ return playableCards.reduce((highest, card) => {
+ const highestValue = CardDB[highest.cardId].value;
+ const cardValue = CardDB[card.cardId].value;
+ return cardValue > highestValue ? card : highest;
+ });
+ }
+
+ // Get random card from hand (for blind steal)
+ getRandomCard(playerId: string, position: number): CardInstance | null {
+ const hand = this.playerHands.get(playerId);
+ if (!hand || position < 0 || position >= hand.length) return null;
+ return hand[position];
+ }
+
+ // Rearrange player's hand
+ rearrangeHand(playerId: string, newOrder: string[]): void {
+ const hand = this.playerHands.get(playerId);
+ if (!hand) return;
+
+ const newHand = newOrder
+ .map(instanceId => hand.find(c => c.instanceId === instanceId))
+ .filter((c): c is CardInstance => c !== undefined);
+
+ this.playerHands.set(playerId, newHand);
+ }
+
+ // Check if game is over
+ isGameOver(): boolean {
+ return this.phase === 'game_over';
+ }
+
+ // Get players with King Ra card (excluding one player)
+ getPlayersWithKingRa(excludeId: string): string[] {
+ return this.players
+ .filter(p => p.isAlive && p.id !== excludeId && this.hasCard(p.id, 'king_ra_says_no'))
+ .map(p => p.id);
+ }
+}
diff --git a/server/src/index.ts b/server/src/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5648f60cd5cdc976b5bd0bca25e1ab9c7a54c887
--- /dev/null
+++ b/server/src/index.ts
@@ -0,0 +1,56 @@
+import express from 'express';
+import { createServer } from 'http';
+import { Server } from 'socket.io';
+import cors from 'cors';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { RoomManager } from './rooms/RoomManager.js';
+import { setupSocketHandlers } from './socket/handlers.js';
+import type { ServerToClientEvents, ClientToServerEvents } from '../../shared/types.js';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const app = express();
+app.use(cors());
+
+// Serve static files from client build in production
+const clientPath = path.join(__dirname, '../../client/dist');
+app.use(express.static(clientPath));
+
+const httpServer = createServer(app);
+
+// Allow all origins in production, restrict in development
+const allowedOrigins = process.env.NODE_ENV === 'production'
+ ? true // Allow all origins
+ : ['http://localhost:5173', 'http://localhost:3000', 'http://127.0.0.1:5173'];
+
+const io = new Server(httpServer, {
+ cors: {
+ origin: allowedOrigins,
+ methods: ['GET', 'POST'],
+ },
+});
+
+const roomManager = new RoomManager();
+
+io.on('connection', (socket) => {
+ console.log(`Player connected: ${socket.id}`);
+ setupSocketHandlers(io, socket, roomManager);
+
+ socket.on('disconnect', () => {
+ console.log(`Player disconnected: ${socket.id}`);
+ roomManager.handleDisconnect(socket.id, io);
+ });
+});
+
+// Serve index.html for all non-API routes (SPA support)
+app.get('*', (req, res) => {
+ res.sendFile(path.join(clientPath, 'index.html'));
+});
+
+const PORT = process.env.PORT || 3001;
+
+httpServer.listen(PORT, () => {
+ console.log(`🎮 Mummy Card Game Server running on port ${PORT}`);
+});
diff --git a/server/src/rooms/RoomManager.ts b/server/src/rooms/RoomManager.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c3e98b342a4003a82d6af901775553834db9a2c6
--- /dev/null
+++ b/server/src/rooms/RoomManager.ts
@@ -0,0 +1,194 @@
+import { v4 as uuidv4 } from 'uuid';
+import { Server, Socket } from 'socket.io';
+import { GameEngine } from '../game/GameEngine.js';
+import type {
+ Room,
+ RoomInfo,
+ Player,
+ ServerToClientEvents,
+ ClientToServerEvents,
+ CardInstance,
+ GameState,
+} from '../../../shared/types.js';
+
+interface GameRoom extends Room {
+ game: GameEngine | null;
+}
+
+export class RoomManager {
+ private rooms: Map = new Map();
+ private playerRooms: Map = new Map(); // socketId -> roomId
+
+ createRoom(hostSocketId: string, playerName: string, roomName: string): GameRoom {
+ const roomId = uuidv4().substring(0, 6).toUpperCase();
+
+ const host: Player = {
+ id: hostSocketId,
+ name: playerName,
+ isAlive: true,
+ cardCount: 0,
+ isReady: false,
+ isHost: true,
+ };
+
+ const room: GameRoom = {
+ id: roomId,
+ name: roomName,
+ hostId: hostSocketId,
+ players: [host],
+ maxPlayers: 6,
+ gameState: null,
+ game: null,
+ };
+
+ this.rooms.set(roomId, room);
+ this.playerRooms.set(hostSocketId, roomId);
+
+ return room;
+ }
+
+ joinRoom(socketId: string, playerName: string, roomId: string): GameRoom | null {
+ const room = this.rooms.get(roomId);
+ if (!room) return null;
+ if (room.players.length >= room.maxPlayers) return null;
+ if (room.gameState?.phase === 'playing') return null;
+
+ const player: Player = {
+ id: socketId,
+ name: playerName,
+ isAlive: true,
+ cardCount: 0,
+ isReady: false,
+ isHost: false,
+ };
+
+ room.players.push(player);
+ this.playerRooms.set(socketId, roomId);
+
+ return room;
+ }
+
+ leaveRoom(socketId: string): { room: GameRoom | null; wasHost: boolean } {
+ const roomId = this.playerRooms.get(socketId);
+ if (!roomId) return { room: null, wasHost: false };
+
+ const room = this.rooms.get(roomId);
+ if (!room) return { room: null, wasHost: false };
+
+ const wasHost = room.hostId === socketId;
+ room.players = room.players.filter(p => p.id !== socketId);
+ this.playerRooms.delete(socketId);
+
+ // If game is in progress, eliminate the player
+ if (room.game && room.gameState?.phase === 'playing') {
+ room.game.eliminatePlayer(socketId);
+ room.gameState = room.game.getGameState();
+ }
+
+ // If room is empty, delete it
+ if (room.players.length === 0) {
+ this.rooms.delete(roomId);
+ return { room: null, wasHost };
+ }
+
+ // Transfer host if needed
+ if (wasHost && room.players.length > 0) {
+ room.hostId = room.players[0].id;
+ room.players[0].isHost = true;
+ }
+
+ return { room, wasHost };
+ }
+
+ handleDisconnect(socketId: string, io: Server): void {
+ const { room } = this.leaveRoom(socketId);
+ if (room) {
+ // Notify remaining players
+ io.to(room.id).emit('roomUpdated', this.getRoomInfo(room));
+
+ if (room.gameState) {
+ io.to(room.id).emit('gameStateUpdated', room.gameState);
+
+ // Emit player eliminated event for disconnected player
+ io.to(room.id).emit('playerEliminated', socketId);
+
+ // Check for game over
+ if (room.gameState.phase === 'game_over' && room.gameState.winnerId) {
+ const winner = room.players.find(p => p.id === room.gameState?.winnerId);
+ io.to(room.id).emit('gameOver', room.gameState.winnerId, winner?.name ?? 'Unknown');
+ }
+ }
+ }
+ }
+
+ setPlayerReady(socketId: string, ready: boolean): GameRoom | null {
+ const roomId = this.playerRooms.get(socketId);
+ if (!roomId) return null;
+
+ const room = this.rooms.get(roomId);
+ if (!room) return null;
+
+ const player = room.players.find(p => p.id === socketId);
+ if (player) {
+ player.isReady = ready;
+ }
+
+ return room;
+ }
+
+ canStartGame(room: GameRoom): boolean {
+ if (room.players.length < 2) return false;
+ return room.players.every(p => p.isReady || p.isHost);
+ }
+
+ startGame(roomId: string): GameRoom | null {
+ const room = this.rooms.get(roomId);
+ if (!room || !this.canStartGame(room)) return null;
+
+ // Create game engine
+ room.game = new GameEngine(room.players);
+ room.game.initializeGame();
+ room.gameState = room.game.getGameState();
+
+ return room;
+ }
+
+ getRoom(roomId: string): GameRoom | null {
+ return this.rooms.get(roomId) ?? null;
+ }
+
+ getPlayerRoom(socketId: string): GameRoom | null {
+ const roomId = this.playerRooms.get(socketId);
+ if (!roomId) return null;
+ return this.rooms.get(roomId) ?? null;
+ }
+
+ getRoomList(): RoomInfo[] {
+ return Array.from(this.rooms.values()).map(room => ({
+ id: room.id,
+ name: room.name,
+ playerCount: room.players.length,
+ maxPlayers: room.maxPlayers,
+ isPlaying: room.gameState?.phase === 'playing',
+ }));
+ }
+
+ private getRoomInfo(room: GameRoom): Room {
+ return {
+ id: room.id,
+ name: room.name,
+ hostId: room.hostId,
+ players: room.players,
+ maxPlayers: room.maxPlayers,
+ gameState: room.gameState,
+ };
+ }
+
+ // Update game state
+ updateGameState(roomId: string): void {
+ const room = this.rooms.get(roomId);
+ if (room?.game) {
+ room.gameState = room.game.getGameState();
+ }
+ }
+}
diff --git a/server/src/socket/handlers.ts b/server/src/socket/handlers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7d2746b631dd43210042d74d95ee848a7d83b7f9
--- /dev/null
+++ b/server/src/socket/handlers.ts
@@ -0,0 +1,888 @@
+import { Server, Socket } from 'socket.io';
+import { RoomManager } from '../rooms/RoomManager.js';
+import type {
+ ServerToClientEvents,
+ ClientToServerEvents,
+ CardId,
+ CardInstance,
+ PendingAction,
+} from '../../../shared/types.js';
+import { CARD_DATABASE } from '../../../shared/types.js';
+
+type GameSocket = Socket;
+type GameServer = Server;
+
+const KING_RA_TIMEOUT = 5000; // 5 seconds for reactions
+
+export function setupSocketHandlers(
+ io: GameServer,
+ socket: GameSocket,
+ roomManager: RoomManager
+): void {
+ // ============ LOBBY HANDLERS ============
+
+ socket.on('getRooms', () => {
+ socket.emit('roomList', roomManager.getRoomList());
+ });
+
+ socket.on('createRoom', (playerName: string, roomName: string) => {
+ const room = roomManager.createRoom(socket.id, playerName, roomName);
+ socket.join(room.id);
+ socket.emit('roomJoined', {
+ id: room.id,
+ name: room.name,
+ hostId: room.hostId,
+ players: room.players,
+ maxPlayers: room.maxPlayers,
+ gameState: room.gameState,
+ });
+ io.emit('roomList', roomManager.getRoomList());
+ });
+
+ socket.on('joinRoom', (playerName: string, roomId: string) => {
+ const room = roomManager.joinRoom(socket.id, playerName, roomId);
+ if (!room) {
+ socket.emit('error', 'Could not join room. Room may be full or in progress.');
+ return;
+ }
+
+ socket.join(room.id);
+ socket.emit('roomJoined', {
+ id: room.id,
+ name: room.name,
+ hostId: room.hostId,
+ players: room.players,
+ maxPlayers: room.maxPlayers,
+ gameState: room.gameState,
+ });
+
+ io.to(room.id).emit('roomUpdated', {
+ id: room.id,
+ name: room.name,
+ hostId: room.hostId,
+ players: room.players,
+ maxPlayers: room.maxPlayers,
+ gameState: room.gameState,
+ });
+
+ io.emit('roomList', roomManager.getRoomList());
+ });
+
+ socket.on('leaveRoom', () => {
+ const { room, wasHost } = roomManager.leaveRoom(socket.id);
+ socket.emit('roomLeft');
+
+ if (room) {
+ io.to(room.id).emit('roomUpdated', {
+ id: room.id,
+ name: room.name,
+ hostId: room.hostId,
+ players: room.players,
+ maxPlayers: room.maxPlayers,
+ gameState: room.gameState,
+ });
+ }
+
+ io.emit('roomList', roomManager.getRoomList());
+ });
+
+ socket.on('setReady', (ready: boolean) => {
+ const room = roomManager.setPlayerReady(socket.id, ready);
+ if (room) {
+ io.to(room.id).emit('roomUpdated', {
+ id: room.id,
+ name: room.name,
+ hostId: room.hostId,
+ players: room.players,
+ maxPlayers: room.maxPlayers,
+ gameState: room.gameState,
+ });
+ }
+ });
+
+ socket.on('startGame', () => {
+ const playerRoom = roomManager.getPlayerRoom(socket.id);
+ if (!playerRoom || playerRoom.hostId !== socket.id) {
+ socket.emit('error', 'Only the host can start the game.');
+ return;
+ }
+
+ if (playerRoom.players.length < 2) {
+ socket.emit('error', 'Need at least 2 players to start.');
+ return;
+ }
+
+ const room = roomManager.startGame(playerRoom.id);
+ if (!room || !room.game || !room.gameState) {
+ socket.emit('error', 'Could not start game.');
+ return;
+ }
+
+ // Send game started to all players with their hands
+ for (const player of room.players) {
+ const hand = room.game.getPlayerHand(player.id);
+ io.to(player.id).emit('gameStarted', room.gameState, hand);
+ }
+
+ // Notify whose turn it is
+ const currentPlayer = room.game.getCurrentPlayer();
+ if (currentPlayer) {
+ io.to(room.id).emit('turnStarted', currentPlayer.id, room.gameState.turnsRemaining);
+ }
+
+ io.emit('roomList', roomManager.getRoomList());
+ });
+
+ // ============ GAME ACTION HANDLERS ============
+
+ socket.on('playCard', async (cardInstanceId: string, targetPlayerId?: string, additionalData?: any) => {
+ const room = roomManager.getPlayerRoom(socket.id);
+ if (!room?.game) return;
+
+ const game = room.game;
+
+ // Verify it's player's turn
+ if (!game.isPlayerTurn(socket.id)) {
+ socket.emit('error', "It's not your turn!");
+ return;
+ }
+
+ // Find the card
+ const card = game.findCardInstance(socket.id, cardInstanceId);
+ if (!card) {
+ socket.emit('error', 'Card not found in your hand.');
+ return;
+ }
+
+ const cardDef = CARD_DATABASE[card.cardId];
+
+ // Check for half cards - need 2 copies
+ if (cardDef.isHalf) {
+ const count = game.countCards(socket.id, card.cardId);
+ if (count < 2) {
+ socket.emit('error', 'Half cards require 2 copies to play!');
+ return;
+ }
+ }
+
+ // King Ra cannot be played during your turn
+ if (card.cardId === 'king_ra_says_no') {
+ socket.emit('error', 'King Ra Says NO can only be played reactively!');
+ return;
+ }
+
+ // Take a Lap cannot be played manually
+ if (card.cardId === 'take_a_lap') {
+ socket.emit('error', 'Take a Lap is used automatically when drawing a mummy!');
+ return;
+ }
+
+ // Remove and discard the card(s)
+ game.removeCardFromHand(socket.id, cardInstanceId);
+ game.discardCard(card);
+
+ // For half cards, remove second copy
+ let secondCard: CardInstance | null = null;
+ if (cardDef.isHalf) {
+ const hand = game.getPlayerHand(socket.id);
+ const secondInstance = hand.find(c => c.cardId === card.cardId);
+ if (secondInstance) {
+ secondCard = game.removeCardFromHand(socket.id, secondInstance.instanceId);
+ if (secondCard) game.discardCard(secondCard);
+ }
+ }
+
+ // IMMEDIATELY update the player's hand in UI
+ socket.emit('handUpdated', game.getPlayerHand(socket.id));
+
+ // Update game state for all players
+ io.to(room.id).emit('gameStateUpdated', game.getGameState());
+
+ // Notify all players about the card played
+ io.to(room.id).emit('cardPlayed', socket.id, card.cardId, targetPlayerId);
+
+ // Cards that cannot be cancelled by King Ra (self-affecting or neutral)
+ const nonCancellableCards = [
+ 'shuffle_it', // Just shuffles deck
+ 'sharp_eye', // Just peeks at cards
+ 'wait_a_sec', // Just skips drawing
+ 'flip_the_table' // Rearranges deck
+ ];
+
+ // For cancellable cards, show reaction window to ALL players
+ if (!nonCancellableCards.includes(card.cardId)) {
+ // Get all other alive players
+ const otherPlayers = game.getOtherAlivePlayers(socket.id);
+
+ if (otherPlayers.length > 0) {
+ // Set up pending action
+ game.pendingAction = {
+ action: { type: 'PLAY_CARD', playerId: socket.id, cardInstanceId, targetId: targetPlayerId, additionalData },
+ cardPlayed: card.cardId,
+ playerId: socket.id,
+ targetId: targetPlayerId,
+ kingRaResponses: new Map(),
+ kingRaCount: 0,
+ timeoutAt: Date.now() + KING_RA_TIMEOUT,
+ };
+
+ // Notify the player that their action is pending reactions
+ socket.emit('actionPending', card.cardId, KING_RA_TIMEOUT);
+
+ // Send reaction window to ALL other players (they'll auto-decline if no King Ra)
+ for (const player of otherPlayers) {
+ io.to(player.id).emit('kingRaPrompt', socket.id, card.cardId, KING_RA_TIMEOUT);
+ }
+
+ // Set timeout
+ setTimeout(() => {
+ if (game.pendingAction && game.pendingAction.cardPlayed === card.cardId) {
+ resolvePendingAction(io, room.id, game, roomManager);
+ }
+ }, KING_RA_TIMEOUT);
+
+ return;
+ }
+ }
+
+ // Execute card effect immediately
+ await executeCardEffect(io, socket, room.id, game, card.cardId, targetPlayerId, additionalData, roomManager);
+ });
+
+ socket.on('kingRaResponse', (useKingRa: boolean) => {
+ console.log('kingRaResponse received:', { socketId: socket.id, useKingRa });
+
+ const room = roomManager.getPlayerRoom(socket.id);
+ if (!room?.game?.pendingAction) {
+ console.log('No pending action found');
+ return;
+ }
+
+ const game = room.game;
+ const pending = game.pendingAction;
+
+ // Check if pending action exists
+ if (!pending) {
+ console.log('Pending is null');
+ return;
+ }
+
+ console.log('Pending action:', { cardPlayed: pending.cardPlayed, kingRaCount: pending.kingRaCount });
+
+ // Record response
+ pending.kingRaResponses.set(socket.id, true);
+
+ const hasKingRaCard = game.hasCard(socket.id, 'king_ra_says_no');
+ console.log('Player has King Ra card:', hasKingRaCard);
+
+ if (useKingRa && hasKingRaCard) {
+ console.log('Using King Ra card!');
+ // Use King Ra
+ const hand = game.getPlayerHand(socket.id);
+ const kingRaCard = hand.find(c => c.cardId === 'king_ra_says_no');
+ if (kingRaCard) {
+ console.log('Found King Ra card, removing it');
+ game.removeCardFromHand(socket.id, kingRaCard.instanceId);
+ game.discardCard(kingRaCard);
+ pending.kingRaCount++;
+
+ console.log('Emitting kingRaResponse');
+ io.to(room.id).emit('kingRaResponse', socket.id, true);
+ io.to(socket.id).emit('handUpdated', game.getPlayerHand(socket.id));
+
+ // Check for chain reactions - allow countering the King Ra
+ const otherPlayersWithKingRa = game.getPlayersWithKingRa(socket.id)
+ .filter((id: string) => !pending.kingRaResponses.has(id));
+
+ if (otherPlayersWithKingRa.length > 0) {
+ // Reset timeout and ask others who can counter
+ pending.timeoutAt = Date.now() + KING_RA_TIMEOUT;
+ pending.kingRaResponses.clear(); // Clear responses for the new round
+
+ for (const playerId of otherPlayersWithKingRa) {
+ io.to(playerId).emit('kingRaPrompt', socket.id, 'king_ra_says_no', KING_RA_TIMEOUT);
+ }
+
+ setTimeout(() => {
+ if (game.pendingAction === pending) {
+ resolvePendingAction(io, room.id, game, roomManager);
+ }
+ }, KING_RA_TIMEOUT);
+
+ return;
+ } else {
+ // No one can counter - resolve immediately
+ resolvePendingAction(io, room.id, game, roomManager);
+ }
+ }
+ }
+ // For declines, just let the timeout handle resolution
+ // Don't resolve early - wait for the full 5 seconds so others can decide
+ });
+
+ socket.on('drawCard', async () => {
+ const room = roomManager.getPlayerRoom(socket.id);
+ if (!room?.game) return;
+
+ const game = room.game;
+
+ if (!game.isPlayerTurn(socket.id)) {
+ socket.emit('error', "It's not your turn!");
+ return;
+ }
+
+ // Block drawing while waiting for arrange or other pending actions
+ if (game.pendingAction?.waitingForArrange) {
+ socket.emit('error', 'Please wait for card arrangement to complete!');
+ return;
+ }
+
+ await handleDrawCard(io, socket, room.id, game, roomManager);
+ });
+
+ socket.on('placeMummy', (position: number) => {
+ const room = roomManager.getPlayerRoom(socket.id);
+ if (!room?.game) return;
+
+ const game = room.game;
+
+ // Create mummy card and insert
+ const mummyCard: CardInstance = {
+ instanceId: `mummy_${Date.now()}`,
+ cardId: 'mummified',
+ };
+
+ game.insertCardInDeck(mummyCard, position);
+
+ // End turn
+ game.endTurn();
+ roomManager.updateGameState(room.id);
+
+ io.to(room.id).emit('gameStateUpdated', game.getGameState());
+
+ const currentPlayer = game.getCurrentPlayer();
+ if (currentPlayer) {
+ io.to(room.id).emit('turnStarted', currentPlayer.id, game.getGameState().turnsRemaining);
+ }
+ });
+
+ socket.on('spellboundRequest', (targetId: string, requestedCardId: CardId) => {
+ const room = roomManager.getPlayerRoom(socket.id);
+ if (!room?.game) return;
+
+ const game = room.game;
+
+ // Check if target has the card
+ if (game.hasCard(targetId, requestedCardId)) {
+ // Success - take one copy
+ const targetHand = game.getPlayerHand(targetId);
+ const cardToTake = targetHand.find(c => c.cardId === requestedCardId);
+
+ if (cardToTake) {
+ game.removeCardFromHand(targetId, cardToTake.instanceId);
+ game.addCardToHand(socket.id, cardToTake);
+
+ socket.emit('spellboundResult', true, requestedCardId);
+ io.to(socket.id).emit('handUpdated', game.getPlayerHand(socket.id));
+ io.to(targetId).emit('handUpdated', game.getPlayerHand(targetId));
+ }
+ } else {
+ // Failure - asker draws as penalty
+ socket.emit('spellboundResult', false);
+
+ handleDrawCard(io, socket, room.id, game, roomManager, true);
+ }
+
+ roomManager.updateGameState(room.id);
+ io.to(room.id).emit('gameStateUpdated', game.getGameState());
+ });
+
+ socket.on('blindStealSelect', (position: number) => {
+ const room = roomManager.getPlayerRoom(socket.id);
+ if (!room?.game) return;
+
+ const game = room.game;
+ const pending = game.pendingAction;
+
+ if (!pending || pending.action.type !== 'PLAY_CARD') return;
+
+ const targetId = pending.targetId;
+ if (!targetId) return;
+
+ const stolenCard = game.getRandomCard(targetId, position);
+ if (stolenCard) {
+ game.removeCardFromHand(targetId, stolenCard.instanceId);
+ game.addCardToHand(socket.id, stolenCard);
+
+ io.to(socket.id).emit('handUpdated', game.getPlayerHand(socket.id));
+ io.to(targetId).emit('handUpdated', game.getPlayerHand(targetId));
+ }
+
+ game.pendingAction = null;
+ roomManager.updateGameState(room.id);
+ io.to(room.id).emit('gameStateUpdated', game.getGameState());
+ });
+
+ socket.on('arrangeHand', (newOrder: string[]) => {
+ const room = roomManager.getPlayerRoom(socket.id);
+ if (!room?.game) return;
+
+ const game = room.game;
+ game.rearrangeHand(socket.id, newOrder);
+ io.to(socket.id).emit('handUpdated', game.getPlayerHand(socket.id));
+
+ // If this was for criminal_mummy, notify the thief they can now pick
+ if (game.pendingAction?.waitingForArrange && game.pendingAction?.cardPlayed === 'criminal_mummy') {
+ game.pendingAction.waitingForArrange = false;
+ const thiefId = game.pendingAction.playerId;
+ const currentHand = game.getPlayerHand(socket.id);
+ io.to(thiefId).emit('blindStealStart', thiefId, socket.id, currentHand.length);
+ }
+ });
+
+ socket.on('burnCard', (cardInstanceId: string) => {
+ const room = roomManager.getPlayerRoom(socket.id);
+ if (!room?.game) return;
+
+ const game = room.game;
+ const pending = game.pendingAction;
+
+ if (!pending || pending.action.type !== 'PLAY_CARD') return;
+
+ const targetId = pending.targetId;
+ if (!targetId) return;
+
+ const card = game.removeCardFromHand(targetId, cardInstanceId);
+ if (card) {
+ game.discardCard(card);
+
+ io.to(targetId).emit('handUpdated', game.getPlayerHand(targetId));
+ }
+
+ game.pendingAction = null;
+ roomManager.updateGameState(room.id);
+ io.to(room.id).emit('gameStateUpdated', game.getGameState());
+ });
+
+ socket.on('rearrangeTopCards', (newOrder: string[]) => {
+ const room = roomManager.getPlayerRoom(socket.id);
+ if (!room?.game) return;
+
+ room.game.rearrangeTopCards(newOrder);
+ room.game.pendingAction = null;
+
+ roomManager.updateGameState(room.id);
+ io.to(room.id).emit('gameStateUpdated', room.game.getGameState());
+ });
+}
+
+// ============ HELPER FUNCTIONS ============
+
+async function handleDrawCard(
+ io: GameServer,
+ socket: GameSocket,
+ roomId: string,
+ game: any,
+ roomManager: RoomManager,
+ isPenalty: boolean = false
+): Promise {
+ const card = game.drawTopCard();
+ if (!card) {
+ socket.emit('error', 'Deck is empty!');
+ return;
+ }
+
+ if (card.cardId === 'mummified') {
+ // Mummy drawn!
+ io.to(roomId).emit('mummyDrawn', socket.id);
+
+ if (game.canDefuse(socket.id)) {
+ // Use defuse
+ game.useDefuse(socket.id);
+ io.to(roomId).emit('mummyDefused', socket.id);
+
+ socket.emit('handUpdated', game.getPlayerHand(socket.id));
+
+ // Place mummy back at random position
+ const mummyCard: CardInstance = {
+ instanceId: `mummy_${Date.now()}`,
+ cardId: 'mummified',
+ };
+ const randomPosition = Math.floor(Math.random() * (game.getDeckSize() + 1));
+ game.insertCardInDeck(mummyCard, randomPosition);
+
+ // End turn
+ game.endTurn();
+ roomManager.updateGameState(roomId);
+ io.to(roomId).emit('gameStateUpdated', game.getGameState());
+
+ const currentPlayer = game.getCurrentPlayer();
+ if (currentPlayer) {
+ io.to(roomId).emit('turnStarted', currentPlayer.id, game.getGameState().turnsRemaining);
+ }
+ } else {
+ // Eliminated!
+ game.eliminatePlayer(socket.id);
+
+ io.to(roomId).emit('playerEliminated', socket.id);
+
+ roomManager.updateGameState(roomId);
+ const gameState = game.getGameState();
+ io.to(roomId).emit('gameStateUpdated', gameState);
+
+ if (gameState.phase === 'game_over' && gameState.winnerId) {
+ const winner = game.getPlayer(gameState.winnerId);
+ io.to(roomId).emit('gameOver', gameState.winnerId, winner?.name ?? 'Unknown');
+ } else {
+ const currentPlayer = game.getCurrentPlayer();
+ if (currentPlayer) {
+ io.to(roomId).emit('turnStarted', currentPlayer.id, gameState.turnsRemaining);
+ }
+ }
+ }
+ } else {
+ // Normal card
+ game.addCardToHand(socket.id, card);
+ socket.emit('handUpdated', game.getPlayerHand(socket.id));
+
+ if (!isPenalty) {
+ // End turn
+ game.endTurn();
+ }
+
+ roomManager.updateGameState(roomId);
+ io.to(roomId).emit('gameStateUpdated', game.getGameState());
+
+ if (!isPenalty) {
+ const currentPlayer = game.getCurrentPlayer();
+ if (currentPlayer) {
+ io.to(roomId).emit('turnStarted', currentPlayer.id, game.getGameState().turnsRemaining);
+ }
+ }
+ }
+}
+
+function resolvePendingAction(
+ io: GameServer,
+ roomId: string,
+ game: any,
+ roomManager: RoomManager
+): void {
+ const pending = game.pendingAction;
+ if (!pending) {
+ console.log('resolvePendingAction: No pending action');
+ return;
+ }
+
+ const cancelled = pending.kingRaCount % 2 === 1;
+ const cardId = pending.cardPlayed as keyof typeof CARD_DATABASE;
+
+ console.log('resolvePendingAction:', { cancelled, kingRaCount: pending.kingRaCount, cardId });
+
+ if (cancelled) {
+ console.log('ACTION CANCELLED - King Ra count odd');
+ io.to(roomId).emit('actionCancelled', pending.cardPlayed, pending.kingRaCount);
+ game.pendingAction = null;
+ } else {
+ // Notify all players that reaction window is over and action proceeds
+ io.to(roomId).emit('actionResolved', pending.cardPlayed);
+
+ // Clear pendingAction BEFORE executing so card effects can set up their own
+ game.pendingAction = null;
+
+ // Execute the original action
+ const playerSocket = io.sockets.sockets.get(pending.playerId);
+ if (playerSocket) {
+ executeCardEffect(
+ io,
+ playerSocket as GameSocket,
+ roomId,
+ game,
+ pending.cardPlayed,
+ pending.targetId,
+ pending.action.type === 'PLAY_CARD' ? pending.action.additionalData : undefined,
+ roomManager
+ );
+ }
+ }
+}
+
+async function executeCardEffect(
+ io: GameServer,
+ socket: GameSocket,
+ roomId: string,
+ game: any,
+ cardId: CardId,
+ targetId?: string,
+ additionalData?: any,
+ roomManager?: RoomManager
+): Promise {
+ const playerId = socket.id;
+
+ switch (cardId) {
+ case 'sharp_eye': {
+ // Peek at top 3 cards
+ const topCards = game.peekTopCards(3);
+ socket.emit('peekCards', topCards);
+ break;
+ }
+
+ case 'wait_a_sec': {
+ // End turn without drawing
+ game.endTurn();
+ roomManager?.updateGameState(roomId);
+ io.to(roomId).emit('gameStateUpdated', game.getGameState());
+
+ const currentPlayer = game.getCurrentPlayer();
+ if (currentPlayer) {
+ io.to(roomId).emit('turnStarted', currentPlayer.id, game.getGameState().turnsRemaining);
+ }
+ break;
+ }
+
+ case 'me_or_you': {
+ // Duel
+ if (!targetId) {
+ socket.emit('error', 'Must select an opponent for duel!');
+ return;
+ }
+
+ const card1 = game.drawTopCard();
+ const card2 = game.drawTopCard();
+
+ if (!card1 || !card2) {
+ socket.emit('error', 'Not enough cards in deck for duel!');
+ return;
+ }
+
+ const value1 = CARD_DATABASE[card1.cardId as keyof typeof CARD_DATABASE].value;
+ const value2 = CARD_DATABASE[card2.cardId as keyof typeof CARD_DATABASE].value;
+
+ let winnerId: string;
+
+ if (value1 > value2) {
+ winnerId = playerId;
+ game.addCardToHand(playerId, card1);
+ game.addCardToHand(playerId, card2);
+ } else if (value2 > value1) {
+ winnerId = targetId;
+ game.addCardToHand(targetId, card1);
+ game.addCardToHand(targetId, card2);
+ } else {
+ // Tie - shuffle back and try again
+ game.insertCardInDeck(card1, Math.floor(Math.random() * game.getDeckSize()));
+ game.insertCardInDeck(card2, Math.floor(Math.random() * game.getDeckSize()));
+ game.shuffleDeck();
+ break;
+ }
+
+ // Only show duel cards to the two players involved
+ io.to(playerId).emit('duelResult',
+ { playerId, card: card1 },
+ { playerId: targetId, card: card2 },
+ winnerId
+ );
+ io.to(targetId).emit('duelResult',
+ { playerId, card: card1 },
+ { playerId: targetId, card: card2 },
+ winnerId
+ );
+
+ // Check if winner got a mummy
+ const wonCards = winnerId === playerId ? [card1, card2] : [card1, card2];
+ for (const wonCard of wonCards) {
+ if (wonCard.cardId === 'mummified') {
+ io.to(roomId).emit('mummyDrawn', winnerId);
+
+ if (game.canDefuse(winnerId)) {
+ game.useDefuse(winnerId);
+ io.to(roomId).emit('mummyDefused', winnerId);
+ game.removeCardFromHand(winnerId, wonCard.instanceId);
+
+ // Place mummy back at random position
+ const mummyCard: CardInstance = {
+ instanceId: `mummy_${Date.now()}`,
+ cardId: 'mummified',
+ };
+ const randomPosition = Math.floor(Math.random() * (game.getDeckSize() + 1));
+ game.insertCardInDeck(mummyCard, randomPosition);
+ } else {
+ game.eliminatePlayer(winnerId);
+ io.to(roomId).emit('playerEliminated', winnerId);
+ }
+ }
+ }
+
+ io.to(playerId).emit('handUpdated', game.getPlayerHand(playerId));
+ io.to(targetId).emit('handUpdated', game.getPlayerHand(targetId));
+ break;
+ }
+
+ case 'spellbound': {
+ // Ask for specific card - handled by separate event
+ // Just send prompt to player
+ if (!targetId) {
+ socket.emit('error', 'Must select a target player!');
+ return;
+ }
+ // Client will send spellboundRequest with the requested card type
+ break;
+ }
+
+ case 'criminal_mummy': {
+ // Blind steal
+ if (!targetId) {
+ socket.emit('error', 'Must select a target player!');
+ return;
+ }
+
+ const targetHand = game.getPlayerHand(targetId);
+ if (targetHand.length === 0) {
+ socket.emit('error', 'Target has no cards!');
+ return;
+ }
+
+ // Set pending action for blind steal with arrange state tracking
+ game.pendingAction = {
+ action: { type: 'PLAY_CARD', playerId, cardInstanceId: '', targetId },
+ cardPlayed: 'criminal_mummy',
+ playerId,
+ targetId,
+ kingRaResponses: new Map(),
+ kingRaCount: 0,
+ timeoutAt: Date.now() + 15000,
+ waitingForArrange: true, // Track if we're waiting for arrange
+ };
+
+ // Prompt victim to rearrange
+ io.to(targetId).emit('arrangeHandPrompt', playerId);
+
+ // Auto-timeout after 15 seconds if victim doesn't confirm
+ setTimeout(() => {
+ if (game.pendingAction?.waitingForArrange && game.pendingAction?.cardPlayed === 'criminal_mummy') {
+ // Time's up - proceed to blind steal
+ game.pendingAction.waitingForArrange = false;
+ const currentHand = game.getPlayerHand(targetId);
+ io.to(playerId).emit('blindStealStart', playerId, targetId, currentHand.length);
+ }
+ }, 15000);
+
+ break;
+ }
+
+ case 'give_and_take': {
+ // Swap lowest for highest
+ if (!targetId) {
+ socket.emit('error', 'Must select a target player!');
+ return;
+ }
+
+ const lowestCard = game.getLowestValueCard(playerId);
+ const highestCard = game.getHighestValueCard(targetId);
+
+ if (!lowestCard || !highestCard) {
+ socket.emit('error', 'Not enough cards to swap!');
+ return;
+ }
+
+ // Perform swap
+ game.removeCardFromHand(playerId, lowestCard.instanceId);
+ game.removeCardFromHand(targetId, highestCard.instanceId);
+ game.addCardToHand(playerId, highestCard);
+ game.addCardToHand(targetId, lowestCard);
+
+ const playerName = game.getPlayer(playerId)?.name ?? 'A player';
+ const targetName = game.getPlayer(targetId)?.name ?? 'someone';
+
+ // Emit swap result to both players (show what they gave and received)
+ io.to(playerId).emit('swapResult', lowestCard, highestCard, targetName);
+ io.to(targetId).emit('swapResult', highestCard, lowestCard, playerName);
+
+ io.to(playerId).emit('handUpdated', game.getPlayerHand(playerId));
+ io.to(targetId).emit('handUpdated', game.getPlayerHand(targetId));
+ break;
+ }
+
+ case 'shuffle_it': {
+ // Reshuffle deck
+ game.shuffleDeck();
+ break;
+ }
+
+ case 'safe_travels': {
+ // End turn, next player takes 2 turns
+ game.setNextPlayerTurns(2);
+ roomManager?.updateGameState(roomId);
+
+ const currentPlayer = game.getCurrentPlayer();
+ if (currentPlayer) {
+ io.to(roomId).emit('turnStarted', currentPlayer.id, 2);
+ }
+
+ io.to(roomId).emit('gameStateUpdated', game.getGameState());
+ break;
+ }
+
+ case 'all_or_nothing': {
+ // View hand, burn a card
+ if (!targetId) {
+ socket.emit('error', 'Must select a target player!');
+ return;
+ }
+
+ const targetHand = game.getPlayerHand(targetId);
+
+ // Set pending action
+ game.pendingAction = {
+ action: { type: 'PLAY_CARD', playerId, cardInstanceId: '', targetId },
+ cardPlayed: 'all_or_nothing',
+ playerId,
+ targetId,
+ kingRaResponses: new Map(),
+ kingRaCount: 0,
+ timeoutAt: Date.now() + 30000,
+ };
+
+ // Show hand to player
+ socket.emit('viewHand', targetId, targetHand);
+ break;
+ }
+
+ case 'flip_the_table': {
+ // Peek and rearrange top 5
+ const topCards = game.peekTopCards(5);
+
+ // Set pending action
+ game.pendingAction = {
+ action: { type: 'PLAY_CARD', playerId, cardInstanceId: '' },
+ cardPlayed: 'flip_the_table',
+ playerId,
+ kingRaResponses: new Map(),
+ kingRaCount: 0,
+ timeoutAt: Date.now() + 30000,
+ };
+
+ socket.emit('flipTableCards', topCards);
+ break;
+ }
+
+ case 'this_is_on_you': {
+ // Force opponent to draw
+ if (!targetId) {
+ socket.emit('error', 'Must select a target player!');
+ return;
+ }
+
+ // Make target draw
+ const targetSocket = io.sockets.sockets.get(targetId);
+ if (targetSocket && roomManager) {
+ await handleDrawCard(io, targetSocket as GameSocket, roomId, game, roomManager, true);
+ }
+ break;
+ }
+ }
+
+ // Update game state
+ roomManager?.updateGameState(roomId);
+ io.to(roomId).emit('gameStateUpdated', game.getGameState());
+}
diff --git a/server/tsconfig.json b/server/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..ceb42129c487a163ad79c472f19ee4a052da2631
--- /dev/null
+++ b/server/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "lib": ["ES2020"],
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ },
+ "include": ["src/**/*", "../shared/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/shared/types.ts b/shared/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..56955b46c46199fd2d4ad6d131352b698617681a
--- /dev/null
+++ b/shared/types.ts
@@ -0,0 +1,313 @@
+// ============================================
+// SHARED TYPES - Used by both client and server
+// ============================================
+
+// Card Types
+export type CardId =
+ | 'sharp_eye'
+ | 'wait_a_sec'
+ | 'me_or_you'
+ | 'spellbound'
+ | 'criminal_mummy'
+ | 'give_and_take'
+ | 'shuffle_it'
+ | 'king_ra_says_no'
+ | 'safe_travels'
+ | 'take_a_lap'
+ | 'all_or_nothing'
+ | 'flip_the_table'
+ | 'this_is_on_you'
+ | 'mummified';
+
+export interface CardDefinition {
+ id: CardId;
+ nameEn: string;
+ nameAr: string;
+ value: number;
+ isHalf: boolean;
+ description: string;
+}
+
+export interface CardInstance {
+ instanceId: string; // Unique ID for each card instance
+ cardId: CardId;
+}
+
+// Card Database
+export const CARD_DATABASE: Record = {
+ sharp_eye: {
+ id: 'sharp_eye',
+ nameEn: 'Sharp Eye',
+ nameAr: 'كلك نظر',
+ value: 1,
+ isHalf: false,
+ description: 'Peek at the top 3 cards of the draw pile.',
+ },
+ wait_a_sec: {
+ id: 'wait_a_sec',
+ nameEn: 'Wait a Sec',
+ nameAr: 'اتقل سيكا',
+ value: 3,
+ isHalf: false,
+ description: 'End your turn immediately without drawing a card.',
+ },
+ me_or_you: {
+ id: 'me_or_you',
+ nameEn: 'Me or You',
+ nameAr: 'يا أنا يا أنت',
+ value: 4,
+ isHalf: false,
+ description: 'Duel! Both draw a card, higher value wins both.',
+ },
+ spellbound: {
+ id: 'spellbound',
+ nameEn: 'Spellbound',
+ nameAr: 'سحرالك',
+ value: 5,
+ isHalf: false,
+ description: 'Ask a player for a specific card. If wrong, you draw.',
+ },
+ criminal_mummy: {
+ id: 'criminal_mummy',
+ nameEn: 'Criminal Mummy',
+ nameAr: 'موميا سوابق',
+ value: 3,
+ isHalf: false,
+ description: 'Steal one random card from an opponent blindly.',
+ },
+ give_and_take: {
+ id: 'give_and_take',
+ nameEn: 'Give & Take',
+ nameAr: 'الدنيا أخذ وعطى',
+ value: 0,
+ isHalf: true,
+ description: 'Swap your lowest card with opponent\'s highest.',
+ },
+ shuffle_it: {
+ id: 'shuffle_it',
+ nameEn: 'Shuffle It',
+ nameAr: 'هشقبلهالك',
+ value: 3,
+ isHalf: false,
+ description: 'Reshuffle the entire draw pile.',
+ },
+ king_ra_says_no: {
+ id: 'king_ra_says_no',
+ nameEn: 'King Ra Says NO',
+ nameAr: 'الملك رع بيقول لع',
+ value: 4,
+ isHalf: false,
+ description: 'Cancel any player\'s action. Reactive only!',
+ },
+ safe_travels: {
+ id: 'safe_travels',
+ nameEn: 'Safe Travels',
+ nameAr: 'طريق السلامة أنت',
+ value: 3,
+ isHalf: false,
+ description: 'End turn without drawing, next player takes 2 turns.',
+ },
+ take_a_lap: {
+ id: 'take_a_lap',
+ nameEn: 'Take a Lap',
+ nameAr: 'خدلك لفة',
+ value: 6,
+ isHalf: false,
+ description: 'Defuse the mummy card and place it back in deck.',
+ },
+ all_or_nothing: {
+ id: 'all_or_nothing',
+ nameEn: 'All or Nothing',
+ nameAr: 'فيها لأخفيها',
+ value: 0,
+ isHalf: true,
+ description: 'View opponent\'s hand, choose one card to burn.',
+ },
+ flip_the_table: {
+ id: 'flip_the_table',
+ nameEn: 'Flip the Table',
+ nameAr: 'اقلب الترابيزة',
+ value: 0,
+ isHalf: true,
+ description: 'Peek at top 5 cards and rearrange them.',
+ },
+ this_is_on_you: {
+ id: 'this_is_on_you',
+ nameEn: 'This is on You',
+ nameAr: 'البس أنت',
+ value: 0,
+ isHalf: true,
+ description: 'Force an opponent to draw the top card.',
+ },
+ mummified: {
+ id: 'mummified',
+ nameEn: "You're Mummified",
+ nameAr: 'هتتحنط هنا',
+ value: 0,
+ isHalf: false,
+ description: 'If drawn without defuse, you are eliminated!',
+ },
+};
+
+// Game State Types
+export type GamePhase = 'waiting' | 'playing' | 'game_over';
+
+export interface Player {
+ id: string;
+ name: string;
+ isAlive: boolean;
+ cardCount: number; // Only count shown to others
+ isReady: boolean;
+ isHost: boolean;
+}
+
+export interface PlayerHand {
+ playerId: string;
+ cards: CardInstance[];
+}
+
+export interface GameState {
+ phase: GamePhase;
+ players: Player[];
+ currentPlayerIndex: number;
+ turnsRemaining: number;
+ deckCount: number;
+ discardPile: CardId[]; // Only shows card types, not instances
+ winnerId: string | null;
+}
+
+// Room Types
+export interface Room {
+ id: string;
+ name: string;
+ hostId: string;
+ players: Player[];
+ maxPlayers: number;
+ gameState: GameState | null;
+}
+
+export interface RoomInfo {
+ id: string;
+ name: string;
+ playerCount: number;
+ maxPlayers: number;
+ isPlaying: boolean;
+}
+
+// Socket Event Types
+export interface ServerToClientEvents {
+ // Lobby events
+ roomList: (rooms: RoomInfo[]) => void;
+ roomJoined: (room: Room) => void;
+ roomUpdated: (room: Room) => void;
+ roomLeft: () => void;
+ error: (message: string) => void;
+
+ // Game events
+ gameStarted: (state: GameState, hand: CardInstance[]) => void;
+ gameStateUpdated: (state: GameState) => void;
+ handUpdated: (hand: CardInstance[]) => void;
+
+ // Card action events
+ cardPlayed: (playerId: string, cardId: CardId, targetId?: string) => void;
+ peekCards: (cards: CardInstance[]) => void;
+ duelResult: (challenger: { playerId: string; card: CardInstance }, opponent: { playerId: string; card: CardInstance }, winnerId: string) => void;
+ spellboundResult: (success: boolean, cardId?: CardId) => void;
+ blindStealStart: (thiefId: string, victimId: string, cardCount: number) => void;
+ arrangeHandPrompt: (thiefId: string) => void;
+ viewHand: (targetId: string, cards: CardInstance[]) => void;
+ flipTableCards: (cards: CardInstance[]) => void;
+ swapResult: (youGave: CardInstance, youReceived: CardInstance, otherPlayerName: string) => void;
+
+ // King Ra events
+ kingRaPrompt: (playerId: string, cardPlayed: CardId, timeout: number) => void;
+ kingRaResponse: (responderId: string, didCancel: boolean) => void;
+ actionPending: (cardId: CardId, timeout: number) => void;
+ actionResolved: (cardId: CardId) => void;
+ actionCancelled: (cardId: CardId, cancelCount: number) => void;
+
+ // Mummy events
+ mummyDrawn: (playerId: string) => void;
+ mummyDefused: (playerId: string) => void;
+ placeMummyPrompt: (deckSize: number) => void;
+ playerEliminated: (playerId: string) => void;
+
+ // Turn events
+ turnStarted: (playerId: string, turnsRemaining: number) => void;
+ turnEnded: (playerId: string) => void;
+
+ // Game end
+ gameOver: (winnerId: string, winnerName: string) => void;
+
+ // Notifications
+ notification: (message: string, type: 'info' | 'warning' | 'success' | 'danger') => void;
+}
+
+export interface ClientToServerEvents {
+ // Lobby events
+ createRoom: (playerName: string, roomName: string) => void;
+ joinRoom: (playerName: string, roomId: string) => void;
+ leaveRoom: () => void;
+ setReady: (ready: boolean) => void;
+ startGame: () => void;
+ getRooms: () => void;
+
+ // Game actions
+ playCard: (cardInstanceId: string, targetPlayerId?: string, additionalData?: any) => void;
+ drawCard: () => void;
+
+ // Card-specific actions
+ spellboundRequest: (targetId: string, requestedCardId: CardId) => void;
+ blindStealSelect: (position: number) => void;
+ arrangeHand: (newOrder: string[]) => void; // Array of instanceIds
+ burnCard: (cardInstanceId: string) => void;
+ rearrangeTopCards: (newOrder: string[]) => void; // For flip the table
+ placeMummy: (position: number) => void;
+
+ // King Ra response
+ kingRaResponse: (useKingRa: boolean) => void;
+}
+
+// Action types for game logic
+export type GameAction =
+ | { type: 'PLAY_CARD'; playerId: string; cardInstanceId: string; targetId?: string; additionalData?: any }
+ | { type: 'DRAW_CARD'; playerId: string }
+ | { type: 'KING_RA_RESPONSE'; playerId: string; useKingRa: boolean }
+ | { type: 'PLACE_MUMMY'; playerId: string; position: number }
+ | { type: 'BLIND_STEAL_SELECT'; playerId: string; position: number }
+ | { type: 'ARRANGE_HAND'; playerId: string; newOrder: string[] }
+ | { type: 'BURN_CARD'; playerId: string; cardInstanceId: string }
+ | { type: 'REARRANGE_TOP_CARDS'; playerId: string; newOrder: string[] }
+ | { type: 'SPELLBOUND_REQUEST'; playerId: string; targetId: string; requestedCardId: CardId };
+
+// Pending action for King Ra reactions
+export interface PendingAction {
+ action: GameAction;
+ cardPlayed: CardId;
+ playerId: string;
+ targetId?: string;
+ kingRaResponses: Map; // playerId -> hasResponded
+ kingRaCount: number; // Number of King Ra cards played
+ timeoutAt: number;
+ waitingForArrange?: boolean; // For Criminal Mummy - waiting for victim to arrange hand
+}
+
+// Asset mapping for cards
+export const CARD_ASSETS: Record = {
+ sharp_eye: 'card_sharp_eye.png',
+ wait_a_sec: 'card_wait_a_sec.png',
+ me_or_you: 'card_me_or_you.png.png', // Note: file has double extension
+ spellbound: 'card_spellbound.png',
+ criminal_mummy: 'card_criminal_mummy.png',
+ give_and_take: 'card_give_and_take.png',
+ shuffle_it: 'card_shuffle_it.png',
+ king_ra_says_no: 'card_king_ra_says_no.png',
+ safe_travels: 'card_safe_travels.png',
+ take_a_lap: 'card_take_a_lap.png',
+ all_or_nothing: 'card_all_or_nothing.png',
+ flip_the_table: 'card_flip_the_table.png',
+ this_is_on_you: 'card_this_is_on_you.png',
+ mummified: 'card_mummy.png',
+};
+
+export const CARD_BACK_ASSET = 'card_back.png';