commit 9bb5e9da9c0fdf7f2118c54fcc69752b7440bad8 Author: é‚¹æ–¹æˆ Date: Thu Jan 1 02:21:09 2026 +0800 first commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..c01e319 Binary files /dev/null and b/.DS_Store differ diff --git a/.cookie b/.cookie new file mode 100644 index 0000000..72b03cf --- /dev/null +++ b/.cookie @@ -0,0 +1 @@ +2á°N*F)‹JÙË4UJK \ No newline at end of file diff --git a/.trae/documents/Nakama æœåŠ¡ç«¯é›†æˆè§„划.md b/.trae/documents/Nakama æœåŠ¡ç«¯é›†æˆè§„划.md new file mode 100644 index 0000000..a3f27c7 --- /dev/null +++ b/.trae/documents/Nakama æœåŠ¡ç«¯é›†æˆè§„划.md @@ -0,0 +1,40 @@ +# Nakama æœåŠ¡ç«¯é›†æˆè§„划 (修订版:4人自动匹é…) + +æ ¹æ®æ‚¨çš„补充需求(4人自动匹é…ã€æ— éœ€æ‰‹åŠ¨ç®¡ç†æˆ¿é—´ï¼‰ï¼Œæˆ‘调整了规划如下。我们将继续基于“动物扫雷大作战â€é¡¹ç›®è¿›è¡Œå¼€å‘。 + +## 1. 核心æµç¨‹ï¼šè‡ªåŠ¨åŒ¹é… (Auto-Matchmaking) + +我们将完全å±è”½â€œæˆ¿é—´â€æ¦‚念,采用类似《王者è£è€€ã€‹æˆ–《英雄è”ç›Ÿã€‹çš„åŒ¹é…æœºåˆ¶ï¼š + +1. **点击“开始游æˆâ€**: 客户端调用 Nakama Matchmaker APIï¼Œå‚æ•°è®¾å®šä¸º `min_count=4, max_count=4`。 +2. **æœåŠ¡ç«¯æŽ’é˜Ÿ**: Nakama è‡ªåŠ¨å°†è¯·æ±‚çš„çŽ©å®¶æ”¾å…¥åŒ¹é…æ± ã€‚ +3. **åŒ¹é…æˆåŠŸ**: å½“å‡‘é½ 4 人åŽï¼ŒæœåŠ¡ç«¯è‡ªåŠ¨åˆ›å»ºä¸€ä¸ªæƒå¨æ¯”èµ› (Authoritative Match)。 +4. **自动入场**: å®¢æˆ·ç«¯æ”¶åˆ°åŒ¹é…æˆåŠŸé€šçŸ¥ (Match Found),æºå¸¦ Token 自动加入比赛,进入游æˆç”»é¢ã€‚ + +## 2. 系统架构与实施 + +### 第一阶段:基础设施 (ä¸å˜) +* **动作**: é…ç½® Docker (Nakama + CockroachDB)。 +* **动作**: å‰ç«¯å®‰è£… `nakama-js`。 + +### 第二阶段:æœåŠ¡ç«¯é€»è¾‘ (TypeScript) +* **Match Handler (比赛控制器)**: + * **硬性é™åˆ¶**: ä»…å½“å‡‘é½ 4 人时游æˆé€»è¾‘æ‰æ­£å¼å¼€å§‹ï¼ˆæˆ–等待超时添加 AI,目å‰å…ˆæŒ‰çº¯çœŸäººè§„划)。 + * **回åˆåˆ¶é€»è¾‘**: 维护 4 人行动顺åºï¼ˆå¦‚:P1 -> P2 -> P3 -> P4 -> P1...)。 + * **状æ€åŒæ­¥**: 广播 100 个格å­çš„状æ€ã€4 å玩家的血é‡/é“å…·/Buff。 + +### 第三阶段:å‰ç«¯æ”¹é€  (针对 4 人匹é…) +* **UI 调整**: + * 主界é¢å¢žåŠ â€œå¼€å§‹åŒ¹é… (4人)â€å¤§æŒ‰é’®ã€‚ + * 增加“匹é…中...â€çš„ç­‰å¾…çŠ¶æ€æç¤ºã€‚ + * 游æˆå†…固定显示 4 ä¸ªçŽ©å®¶çš„å¤´åƒæ§½ä½ï¼ˆå¦‚ `App.tsx` 中已有的布局,需确ä¿èƒ½åŠ¨æ€æ˜ å°„ P1-P4)。 +* **逻辑对接**: + * **Socket 监å¬**: ç›‘å¬ `onmatchmakermatched` 事件 -> 自动执行 `joinMatch`。 + * **游æˆå¼€å§‹**: 收到æœåŠ¡ç«¯ `OP_GAME_START` ä¿¡å·åŽï¼Œè§£é”棋盘交互。 + +## 3. 下一步执行计划 +1. **环境æ­å»º**: å¯åЍ Nakama æœåŠ¡ç«¯ã€‚ +2. **å‰ç«¯é›†æˆ**: å®žçŽ°â€œç‚¹å‡»åŒ¹é… -> 等待 -> 自动进入游æˆâ€çš„完整链路。 +3. **逻辑è¿ç§»**: 将现有的 Mock æ•°æ®æ›¿æ¢ä¸ºæœåŠ¡ç«¯æ•°æ®ã€‚ + +请确认是å¦å¼€å§‹æ‰§è¡ŒçŽ¯å¢ƒæ­å»ºï¼Ÿ diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..0bada80 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,140 @@ +# 扫雷游æˆéƒ¨ç½²æ•™ç¨‹ + +## 项目结构 +``` +game/ +├── app/ # React å‰ç«¯ï¼ˆç”¨æˆ·çŽ©æ¸¸æˆï¼‰ +├── server/ # Nakama æ¸¸æˆæœåŠ¡å™¨é€»è¾‘ +├── docker-compose.yml +└── 游æˆé€»è¾‘文档.txt +``` + +--- + +## 1. 部署 Nakama æ¸¸æˆæœåС噍 + +### 1.1 æœåС噍准备 +```bash +# 安装 Docker å’Œ Docker Compose +apt install docker.io docker-compose +``` + +### 1.2 å¯åЍæœåŠ¡ +```bash +cd /path/to/game + +# 构建并å¯åЍ +docker-compose up -d --build + +# [推è] æ–¹å¼2:分别部署 (业务åŽç«¯ + æ¸¸æˆæœ) +# 1. å¯åŠ¨ä¸šåŠ¡åŽç«¯ (在 bindbox_game 目录) +# 2. å¯åŠ¨æ¸¸æˆæœ (使用 docker-compose.cloud.yml, 需确ä¿ç½‘络互通) +# docker-compose -f docker-compose.cloud.yml up -d + +# [推è] æ–¹å¼3:全é‡åˆå¹¶éƒ¨ç½² (业务åŽç«¯ + æ¸¸æˆæœ + æ•°æ®åº“) +# docker-compose -f docker-compose.all.yml up -d + +# 云端部署 (由于是云端,通常使用拉å–的镜åƒ) +# docker-compose -f docker-compose.cloud.yml up -d + +# 查看日志 +docker-compose logs -f nakama +``` + +### 1.3 éªŒè¯æœåŠ¡ +- Nakama API: http://your-server:7350 +- Nakama Console: http://your-server:7351 +- CockroachDB UI: http://your-server:8081 + +--- + +## 2. 部署å‰ç«¯æ¸¸æˆ + +### 2.1 构建å‰ç«¯ +```bash +cd /path/to/game/app + +# 安装ä¾èµ– +npm install + +# 构建生产版本 +npm run build +``` + +### 2.2 é…ç½® Nginx +```nginx +server { + listen 80; + server_name game.1024tool.vip; + + root /var/www/game; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +} +``` + +### 2.3 部署文件 +```bash +# 将构建产物å¤åˆ¶åˆ°æœåС噍 +scp -r dist/* root@server:/var/www/game/ + +# é‡è½½ Nginx +nginx -s reload +``` + +--- + +## 3. é…ç½®å°ç¨‹åºå¯¹æŽ¥ + +### 3.1 åŽç«¯é…置(bindbox_game) +在 `system_configs` 表添加: +```sql +INSERT INTO system_configs (key, value) VALUES +('nakama_server', 'wss://game.1024tool.vip:7350'), +('nakama_key', 'defaultkey'); +``` + +### 3.2 å‰ç«¯è·³è½¬ +å°ç¨‹åºæ‰«é›·å…¥å£å·²é…置跳转到: +``` +https://game.1024tool.vip?ticket={ticket_token} +``` + +--- + +## 4. 完整æµç¨‹ + +``` +å°ç¨‹åºç‚¹å‡»ã€Œè¿›å…¥æ¸¸æˆã€ + ↓ +调用 /api/app/games/enter + ↓ +èŽ·å– ticket_token + ↓ +跳转 webview: game.1024tool.vip?ticket=xxx + ↓ +游æˆå‰ç«¯è¿žæŽ¥ Nakama æœåС噍 + ↓ +开始对战 +``` + +--- + +## 5. 常用命令 + +```bash +# 查看æœåŠ¡çŠ¶æ€ +docker-compose ps + +# é‡å¯æœåŠ¡ +docker-compose restart nakama + +# 查看实时日志 +docker-compose logs -f + +# åœæ­¢æœåŠ¡ +docker-compose down +``` diff --git a/app/.env.production b/app/.env.production new file mode 100644 index 0000000..00f9c66 --- /dev/null +++ b/app/.env.production @@ -0,0 +1,5 @@ +# Production environment configuration +VITE_NAKAMA_HOST=game.1024tool.vip +VITE_NAKAMA_PORT=443 +VITE_NAKAMA_SERVER_KEY=defaultkey +VITE_NAKAMA_USE_SSL=true diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/app/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/app/dist.zip b/app/dist.zip new file mode 100644 index 0000000..a5b630e Binary files /dev/null and b/app/dist.zip differ diff --git a/app/eslint.config.js b/app/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/app/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..0c4f36d --- /dev/null +++ b/app/index.html @@ -0,0 +1,13 @@ + + + + + + + app + + +
+ + + diff --git a/app/package-lock.json b/app/package-lock.json new file mode 100644 index 0000000..adf9d48 --- /dev/null +++ b/app/package-lock.json @@ -0,0 +1,4109 @@ +{ + "name": "app", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "app", + "version": "0.0.0", + "dependencies": { + "@heroiclabs/nakama-js": "^2.8.0", + "@types/uuid": "^11.0.0", + "@types/ws": "^8.18.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "uuid": "^13.0.0", + "ws": "^8.18.3" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.23", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.16", + "typescript": "~5.6.2", + "typescript-eslint": "^8.18.0", + "vite": "^5.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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@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.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@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.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@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, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "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, + "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, + "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, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.5" + }, + "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, + "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, + "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.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@heroiclabs/nakama-js": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@heroiclabs/nakama-js/-/nakama-js-2.8.0.tgz", + "integrity": "sha512-E3bH/pqosASGHmVttsa708UjoLYkzZ4Sy3JUZV0TMK3oZK19QVyKrWhqjwyFwvKI2WyVf30xiRD+2ffvmfpw4A==", + "dependencies": { + "@scarf/scarf": "^1.1.1", + "base64-arraybuffer": "^1.0.2", + "js-base64": "^3.7.4", + "whatwg-fetch": "^3.6.2" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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, + "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, + "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, + "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 + }, + "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, + "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, + "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, + "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, + "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 + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.5.tgz", + "integrity": "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.5.tgz", + "integrity": "sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.5.tgz", + "integrity": "sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.5.tgz", + "integrity": "sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.5.tgz", + "integrity": "sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.5.tgz", + "integrity": "sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.5.tgz", + "integrity": "sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.5.tgz", + "integrity": "sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.5.tgz", + "integrity": "sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.5.tgz", + "integrity": "sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.5.tgz", + "integrity": "sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.5.tgz", + "integrity": "sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.5.tgz", + "integrity": "sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.5.tgz", + "integrity": "sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.5.tgz", + "integrity": "sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.5.tgz", + "integrity": "sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.5.tgz", + "integrity": "sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.5.tgz", + "integrity": "sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.5.tgz", + "integrity": "sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.5.tgz", + "integrity": "sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.5.tgz", + "integrity": "sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.5.tgz", + "integrity": "sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true + }, + "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, + "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, + "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, + "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, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "24.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz", + "integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/uuid": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-11.0.0.tgz", + "integrity": "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==", + "deprecated": "This is a stub types definition. uuid provides its own type definitions, so you do not need this installed.", + "dependencies": { + "uuid": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz", + "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/type-utils": "8.50.0", + "@typescript-eslint/utils": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.50.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", + "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz", + "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.50.0", + "@typescript-eslint/types": "^8.50.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", + "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz", + "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz", + "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/utils": "8.50.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", + "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", + "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/project-service": "8.50.0", + "@typescript-eslint/tsconfig-utils": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/visitor-keys": "8.50.0", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", + "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.50.0", + "@typescript-eslint/types": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", + "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.50.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "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, + "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/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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 + }, + "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, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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 + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", + "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" + } + ], + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", + "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/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.8.tgz", + "integrity": "sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA==", + "dev": true, + "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, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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, + "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" + } + ], + "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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "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" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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, + "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, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "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, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "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 + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "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==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "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 + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "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, + "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, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true + }, + "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, + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "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, + "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, + "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, + "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, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "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, + "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, + "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, + "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, + "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, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "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, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==" + }, + "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==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "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, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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, + "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 + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "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, + "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, + "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, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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==", + "dev": true + }, + "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, + "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" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "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 + }, + "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, + "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, + "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, + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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 + }, + "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 + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "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, + "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, + "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" + } + ], + "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, + "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" + } + ], + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "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" + } + ], + "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, + "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 + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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" + } + ] + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "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, + "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, + "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, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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, + "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/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.5.tgz", + "integrity": "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.5", + "@rollup/rollup-android-arm64": "4.53.5", + "@rollup/rollup-darwin-arm64": "4.53.5", + "@rollup/rollup-darwin-x64": "4.53.5", + "@rollup/rollup-freebsd-arm64": "4.53.5", + "@rollup/rollup-freebsd-x64": "4.53.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", + "@rollup/rollup-linux-arm-musleabihf": "4.53.5", + "@rollup/rollup-linux-arm64-gnu": "4.53.5", + "@rollup/rollup-linux-arm64-musl": "4.53.5", + "@rollup/rollup-linux-loong64-gnu": "4.53.5", + "@rollup/rollup-linux-ppc64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-gnu": "4.53.5", + "@rollup/rollup-linux-riscv64-musl": "4.53.5", + "@rollup/rollup-linux-s390x-gnu": "4.53.5", + "@rollup/rollup-linux-x64-gnu": "4.53.5", + "@rollup/rollup-linux-x64-musl": "4.53.5", + "@rollup/rollup-openharmony-arm64": "4.53.5", + "@rollup/rollup-win32-arm64-msvc": "4.53.5", + "@rollup/rollup-win32-ia32-msvc": "4.53.5", + "@rollup/rollup-win32-x64-gnu": "4.53.5", + "@rollup/rollup-win32-x64-msvc": "4.53.5", + "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" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + }, + "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, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "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-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz", + "integrity": "sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==", + "dev": true, + "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.6", + "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", + "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, + "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, + "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, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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 + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.50.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.50.0.tgz", + "integrity": "sha512-Q1/6yNUmCpH94fbgMUMg2/BSAr/6U7GBk61kZTv1/asghQOWOjTlp9K8mixS5NcJmm2creY+UFfGeW/+OcA64A==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.50.0", + "@typescript-eslint/parser": "8.50.0", + "@typescript-eslint/typescript-estree": "8.50.0", + "@typescript-eslint/utils": "8.50.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==" + }, + "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" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.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 + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "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/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..cd1d4f5 --- /dev/null +++ b/app/package.json @@ -0,0 +1,38 @@ +{ + "name": "app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@heroiclabs/nakama-js": "^2.8.0", + "@types/uuid": "^11.0.0", + "@types/ws": "^8.18.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "uuid": "^13.0.0", + "ws": "^8.18.3" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.23", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.16", + "typescript": "~5.6.2", + "typescript-eslint": "^8.18.0", + "vite": "^5.4.11" + } +} diff --git a/app/postcss.config.js b/app/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/app/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/app/public/vite.svg b/app/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/app/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/scripts/simulate_bots.ts b/app/scripts/simulate_bots.ts new file mode 100644 index 0000000..05a6c14 --- /dev/null +++ b/app/scripts/simulate_bots.ts @@ -0,0 +1,53 @@ +import { Client } from '@heroiclabs/nakama-js'; +import WebSocket from 'ws'; +import { v4 as uuidv4 } from 'uuid'; + +// Polyfill WebSocket for Node.js +global.WebSocket = WebSocket as any; + +const startBot = async (idx: number) => { + const client = new Client('defaultkey', 'localhost', '7350', false); + const deviceId = `bot-player-${idx}-${uuidv4()}`; + + try { + const session = await client.authenticateDevice(deviceId, true, `Bot-${idx}`); + console.log(`[Bot ${idx}] Authenticated: ${session.user_id}`); + + const socket = client.createSocket(false, false); + await socket.connect(session, true); + console.log(`[Bot ${idx}] Socket Connected`); + + // Listen for match + socket.onmatchmakermatched = async (matched) => { + // The matched object might have different property names depending on SDK version or response + console.log(`[Bot ${idx}] Matchmaker Matched:`, matched); + + const matchId = matched.match_id || (matched as any).matchId; + const token = matched.token; + + if (matchId) { + console.log(`[Bot ${idx}] Joining Match ID: ${matchId}`); + const match = await socket.joinMatch(matchId, token); + console.log(`[Bot ${idx}] Joined Match: ${match.match_id}`); + } else { + console.error(`[Bot ${idx}] Match ID missing in matched data`); + } + }; + + // Start Matchmaking + console.log(`[Bot ${idx}] Joining Matchmaker...`); + await socket.addMatchmaker('*', 4, 4); + + } catch (err) { + console.error(`[Bot ${idx}] Error:`, err); + } +}; + +// Start 3 Bots +console.log('Starting 3 Bots to fill the match...'); +for (let i = 1; i <= 3; i++) { + startBot(i); +} + +// Keep script running +setInterval(() => {}, 10000); diff --git a/app/scripts/simulate_bots_fixed.ts b/app/scripts/simulate_bots_fixed.ts new file mode 100644 index 0000000..a60510c --- /dev/null +++ b/app/scripts/simulate_bots_fixed.ts @@ -0,0 +1,52 @@ +import { Client } from '@heroiclabs/nakama-js'; +import WebSocket from 'ws'; +import { v4 as uuidv4 } from 'uuid'; + +// Polyfill WebSocket for Node.js +global.WebSocket = WebSocket as any; + +const startBot = async (idx: number) => { + const client = new Client('defaultkey', 'localhost', '7350', false); + const deviceId = `bot-player-${idx}-${uuidv4()}`; + + try { + const session = await client.authenticateDevice(deviceId, true, `Bot-${idx}`); + console.log(`[Bot ${idx}] Authenticated: ${session.user_id}`); + + const socket = client.createSocket(false, false); + await socket.connect(session, true); + console.log(`[Bot ${idx}] Socket Connected`); + + // Use matchmaker to find existing matches + socket.onmatchmakermatched = async (matched) => { + console.log(`[Bot ${idx}] Matchmaker Matched:`, matched); + + const matchId = matched.match_id || (matched as any).matchId; + const token = matched.token; + + if (matchId) { + console.log(`[Bot ${idx}] Joining Match ID: ${matchId}`); + const match = await socket.joinMatch(matchId, token); + console.log(`[Bot ${idx}] Joined Match: ${match.match_id}`); + } else { + console.error(`[Bot ${idx}] Match ID missing in matched data`); + } + }; + + // Start Matchmaking - this will find existing matches + console.log(`[Bot ${idx}] Joining Matchmaker...`); + await socket.addMatchmaker('*', 4, 4); + + } catch (err) { + console.error(`[Bot ${idx}] Error:`, err); + } +}; + +// Start 3 Bots +console.log('Starting 3 Bots to fill the match...'); +for (let i = 1; i <= 3; i++) { + startBot(i); +} + +// Keep script running +setInterval(() => {}, 10000); \ No newline at end of file diff --git a/app/scripts/simulate_bots_matchmaker.ts b/app/scripts/simulate_bots_matchmaker.ts new file mode 100644 index 0000000..c783c58 --- /dev/null +++ b/app/scripts/simulate_bots_matchmaker.ts @@ -0,0 +1,56 @@ +import { Client } from '@heroiclabs/nakama-js'; +import WebSocket from 'ws'; +import { v4 as uuidv4 } from 'uuid'; + +// Polyfill WebSocket for Node.js +global.WebSocket = WebSocket as any; + +const startBot = async (idx: number) => { + const client = new Client('defaultkey', 'localhost', '7350', false); + const deviceId = `bot-player-${idx}-${uuidv4()}`; + + try { + const session = await client.authenticateDevice(deviceId, true, `Bot-${idx}`); + console.log(`[Bot ${idx}] Authenticated: ${session.user_id}`); + + const socket = client.createSocket(false, false); + await socket.connect(session, true); + console.log(`[Bot ${idx}] Socket Connected`); + + // Use matchmaker to find matches with our custom handler + socket.onmatchmakermatched = async (matched) => { + console.log(`[Bot ${idx}] Matchmaker Matched:`, matched); + + const matchId = matched.match_id || (matched as any).matchId; + const token = matched.token; + + if (matchId) { + console.log(`[Bot ${idx}] Joining Match ID: ${matchId}`); + const match = await socket.joinMatch(matchId, token); + console.log(`[Bot ${idx}] Joined Match: ${match.match_id}`); + + // Request game state after joining + console.log(`[Bot ${idx}] Requesting game state...`); + socket.sendMatchState(match.match_id, { action: 'getState' }); + } else { + console.error(`[Bot ${idx}] Match ID missing in matched data`); + } + }; + + // Start Matchmaking with query for our custom match + console.log(`[Bot ${idx}] Joining Matchmaker for animal_minesweeper...`); + await socket.addMatchmaker('label:animal_minesweeper', 4, 4); + + } catch (err) { + console.error(`[Bot ${idx}] Error:`, err); + } +}; + +// Start 3 Bots +console.log('Starting 3 Bots to fill the match...'); +for (let i = 1; i <= 3; i++) { + startBot(i); +} + +// Keep script running +setInterval(() => {}, 10000); \ No newline at end of file diff --git a/app/scripts/test_matchmaker.cjs b/app/scripts/test_matchmaker.cjs new file mode 100644 index 0000000..bab1bb5 --- /dev/null +++ b/app/scripts/test_matchmaker.cjs @@ -0,0 +1,56 @@ +// Test script to simulate 4 players joining matchmaker +const { Client } = require('@heroiclabs/nakama-js'); +const { v4: uuidv4 } = require('uuid'); + +async function simulatePlayer(playerId) { + const client = new Client('defaultkey', 'localhost', '7350', false); + const deviceId = uuidv4(); + + console.log(`Player ${playerId}: Authenticating with deviceId ${deviceId}`); + const session = await client.authenticateDevice(deviceId, true, deviceId); + console.log(`Player ${playerId}: Authenticated as ${session.user_id}`); + + const socket = client.createSocket(false, false); + await socket.connect(session, true); + console.log(`Player ${playerId}: Socket connected`); + + socket.onmatchmakermatched = async (matched) => { + console.log(`Player ${playerId}: MATCHED! Match ID: ${matched.match_id}`); + try { + const match = await socket.joinMatch(matched.match_id, matched.token); + console.log(`Player ${playerId}: Joined match ${match.match_id}`); + } catch (e) { + console.error(`Player ${playerId}: Failed to join match`, e); + } + }; + + socket.onmatchdata = (data) => { + console.log(`Player ${playerId}: Received match data`, data.op_code); + }; + + const ticket = await socket.addMatchmaker('*', 4, 4); + console.log(`Player ${playerId}: Added to matchmaker with ticket ${ticket.ticket}`); + + return socket; +} + +async function main() { + console.log('Starting 4 player simulation...'); + + const promises = []; + for (let i = 1; i <= 4; i++) { + promises.push(simulatePlayer(i)); + } + + try { + await Promise.all(promises); + console.log('All 4 players added to matchmaker. Waiting for match...'); + + // Keep the script running to receive match events + await new Promise(resolve => setTimeout(resolve, 30000)); + } catch (error) { + console.error('Error:', error); + } +} + +main(); diff --git a/app/scripts/test_matchmaker.mjs b/app/scripts/test_matchmaker.mjs new file mode 100644 index 0000000..8ecdf55 --- /dev/null +++ b/app/scripts/test_matchmaker.mjs @@ -0,0 +1,56 @@ +// Test script to simulate 4 players joining matchmaker +import { Client } from '@heroiclabs/nakama-js'; +import { v4 as uuidv4 } from 'uuid'; + +async function simulatePlayer(playerId) { + const client = new Client('defaultkey', 'localhost', '7350', false); + const deviceId = uuidv4(); + + console.log(`Player ${playerId}: Authenticating with deviceId ${deviceId}`); + const session = await client.authenticateDevice(deviceId, true, deviceId); + console.log(`Player ${playerId}: Authenticated as ${session.user_id}`); + + const socket = client.createSocket(false, false); + await socket.connect(session, true); + console.log(`Player ${playerId}: Socket connected`); + + socket.onmatchmakermatched = async (matched) => { + console.log(`Player ${playerId}: MATCHED! Match ID: ${matched.match_id}`); + try { + const match = await socket.joinMatch(matched.match_id, matched.token); + console.log(`Player ${playerId}: Joined match ${match.match_id}`); + } catch (e) { + console.error(`Player ${playerId}: Failed to join match`, e); + } + }; + + socket.onmatchdata = (data) => { + console.log(`Player ${playerId}: Received match data`, data.op_code); + }; + + const ticket = await socket.addMatchmaker('*', 4, 4); + console.log(`Player ${playerId}: Added to matchmaker with ticket ${ticket.ticket}`); + + return socket; +} + +async function main() { + console.log('Starting 4 player simulation...'); + + const promises = []; + for (let i = 1; i <= 4; i++) { + promises.push(simulatePlayer(i)); + } + + try { + await Promise.all(promises); + console.log('All 4 players added to matchmaker. Waiting for match...'); + + // Keep the script running to receive match events + await new Promise(resolve => setTimeout(resolve, 30000)); + } catch (error) { + console.error('Error:', error); + } +} + +main(); diff --git a/app/src/App.css b/app/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/app/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/app/src/App.tsx b/app/src/App.tsx new file mode 100644 index 0000000..0191825 --- /dev/null +++ b/app/src/App.tsx @@ -0,0 +1,704 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { nakamaManager } from './lib/nakama'; +import './Explosion.css'; +// Types for Game State (Synced with Server) +interface Player { + userId: string; + sessionId: string; + username: string; + avatar: string; + hp: number; + maxHp: number; + character: string; + // Status Flags (synced from server) + shield: boolean; + poisoned: boolean; + curse: boolean; + revive: boolean; + timeBombTurns: number; + skipTurn: boolean; +} + +interface GridCell { + type: 'empty' | 'bomb' | 'item'; + itemId?: string; + revealed: boolean; + neighborBombs?: number; // Added for classic Minesweeper logic +} + +interface GameState { + players: { [userId: string]: Player }; + grid: GridCell[]; + gridSize: number; + turnOrder: string[]; + currentTurnIndex: number; + round: number; + winnerId: string | null; + gameStarted: boolean; +} + +interface LogEntry { + id: string; + type: 'system' | 'action' | 'effect'; + content: React.ReactNode; +} + +interface FloatingLabel { + id: string; + x: number; + y: number; + text: string; + type: 'heal' | 'damage' | 'item'; + cellIndex?: number; + targetUserId?: string; +} + +const App: React.FC = () => { + // Game State + const [gameState, setGameState] = useState(null); + const [logs, setLogs] = useState([]); + const [isConnected, setIsConnected] = useState(false); + const [isMatching, setIsMatching] = useState(false); + const [matchId, setMatchId] = useState(null); + const [myUserId, setMyUserId] = useState(null); + const [floatingLabels, setFloatingLabels] = useState([]); + const [showGuide, setShowGuide] = useState(false); + const [debugMode, setDebugMode] = useState(false); + const [matchingTimer, setMatchingTimer] = useState(0); + const [matchPlayerCount, setMatchPlayerCount] = useState(2); // 动æ€åŒ¹é…人数 + const [turnTimer, setTurnTimer] = useState(15); // 回åˆå€’计时 + const [screenShaking, setScreenShaking] = useState(false); // å±å¹•抖动 + const [damagedPlayers, setDamagedPlayers] = useState([]); // å—伤玩家 + const [healedPlayers, setHealedPlayers] = useState([]); // 治疗玩家 + const [isRefreshing, setIsRefreshing] = useState(false); // 正在刷新token + + const logsEndRef = useRef(null); + const prevGameStateRef = useRef(null); + const myUserIdRef = useRef(null); + + // Auto-scroll logs + useEffect(() => { + logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [logs]); + + // Helpers + const addLog = useCallback((type: 'system' | 'action' | 'effect', content: React.ReactNode) => { + setLogs(prev => [...prev.slice(-49), { id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, type, content }]); + }, []); + + const spawnLabel = useCallback((x: number, y: number, text: string, type: 'heal' | 'damage' | 'item', cellIndex?: number, targetUserId?: string) => { + const id = `${Date.now()}-${Math.random()}`; + setFloatingLabels(prev => [...prev, { id, x, y, text, type, cellIndex, targetUserId }]); + setTimeout(() => { + setFloatingLabels(prev => prev.filter(l => l.id !== id)); + }, 1000); + }, []); + + const describeCellContent = (cell: GridCell): string => { + if (cell.type === 'bomb') return '💣 炸弹ï¼'; + if (cell.type === 'item') { + const itemNames: { [key: string]: string } = { + 'medkit': '💊 医疗包', + 'bomb_timer': 'Ⱐ定时炸弹', + 'poison': 'â˜ ï¸ æ¯’è¯', + 'shield': 'ðŸ›¡ï¸ æŠ¤ç›¾', + 'skip': 'â­ï¸ 跳过', + 'magnifier': '🔠放大镜', + 'knife': '🔪 匕首', + 'revive': '💖 夿´»', + 'lightning': 'âš¡ 闪电', + 'chest': '📦 å®ç®±', + 'curse': '👻 诅咒', + }; + return itemNames[cell.itemId || ''] || '🎠é“å…·'; + } + return '✅ 安全'; + }; + + const getItemIcon = (itemId?: string): string => { + const icons: { [key: string]: string } = { + 'medkit': '💊', 'bomb_timer': 'â°', 'poison': '☠ï¸', 'shield': '🛡ï¸', + 'skip': 'â­ï¸', 'magnifier': 'ðŸ”', 'knife': '🔪', 'revive': '💖', + 'lightning': 'âš¡', 'chest': '📦', 'curse': '👻' + }; + return icons[itemId || ''] || 'ðŸŽ'; + }; + + const renderHeart = (hp: number, maxHp: number) => { + return ( +
+ {Array.from({ length: maxHp }).map((_, i) => ( + + {i < hp ? 'â¤ï¸' : 'ðŸ¤'} + + ))} +
+ ); + }; + + const renderStatusIcons = (player: Player) => { + const icons: React.ReactNode[] = []; + if (player.shield) icons.push(🛡ï¸); + if (player.poisoned) icons.push(☠ï¸); + if (player.curse) icons.push(👻); + if (player.revive) icons.push(💖); + if (player.timeBombTurns > 0) icons.push(â°{player.timeBombTurns}); + if (player.skipTurn) icons.push(â­ï¸); + return icons.length > 0 ?
{icons}
: null; + }; + + const handleCellClick = (index: number) => { + if (!gameState || !matchId || !gameState.gameStarted) return; + const currentPlayerId = gameState.turnOrder[gameState.currentTurnIndex]; + if (currentPlayerId !== myUserIdRef.current) return; + if (gameState.grid[index].revealed) return; + nakamaManager.getSocket()?.sendMatchState(matchId, 3, JSON.stringify({ index })); + }; + + const startMatchmaking = async () => { + if (matchId) { + console.warn('Already in a match'); + return; + } + try { + setIsMatching(true); + addLog('system', `å¼€å§‹åŒ¹é… (${matchPlayerCount}人)...`); + const socket = nakamaManager.getSocket(); + if (socket) { + await socket.addMatchmaker('*', matchPlayerCount, matchPlayerCount); + addLog('system', '正在寻找匹é…...'); + } + } catch (error) { + console.error('Matchmaking error:', error); + setIsMatching(false); + const errorMsg = error instanceof Error ? error.message : '网络连接异常'; + addLog('system', `⌠匹é…失败: ${errorMsg}`); + addLog('system', '💡 请检查网络连接åŽé‡è¯•,或ç¨åŽå†è¯•'); + } + }; + + // 冿¥ä¸€å±€: èŽ·å–æ–°token并釿–°åŒ¹é… + const refreshAndPlayAgain = async () => { + setIsRefreshing(true); + addLog('system', '🔄 æ­£åœ¨èŽ·å–æ–°çš„æ¸¸æˆå‡­è¯...'); + + try { + const backendUrl = 'https://game.1024tool.vip'; + // 从å°ç¨‹åºçŽ¯å¢ƒèŽ·å–用户token + let userToken = ''; + if ((window as any).uni && (window as any).uni.getStorageSync) { + userToken = (window as any).uni.getStorageSync('token') || ''; + } + + if (!userToken) { + addLog('system', '⌠无法获å–用户凭è¯,请返回å°ç¨‹åºé‡æ–°è¿›å…¥'); + setIsRefreshing(false); + // 回退到å°ç¨‹åº + if ((window as any).uni) { + (window as any).uni.navigateBack(); + } else if ((window as any).wx?.miniProgram) { + (window as any).wx.miniProgram.navigateBack(); + } + return; + } + + const resp = await fetch(`${backendUrl}/api/app/games/enter`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${userToken}` + }, + body: JSON.stringify({ game_code: 'minesweeper' }) + }); + + if (!resp.ok) { + const errData = await resp.json().catch(() => ({})); + addLog('system', `âŒ èŽ·å–æ¸¸æˆå‡­è¯å¤±è´¥: ${errData.message || 'æ¸¸æˆæ¬¡æ•°ä¸è¶³'}`); + setIsRefreshing(false); + return; + } + + const data = await resp.json(); + if (data.game_token) { + setCurrentGameToken(data.game_token); + addLog('system', '✅ èŽ·å–æ–°å‡­è¯æˆåŠŸ!'); + + // é‡ç½®æ¸¸æˆçŠ¶æ€ + setGameState(null); + setMatchId(null); + setIsMatching(false); + + // 釿–°åˆå§‹åŒ– Nakama å¹¶å¼€å§‹åŒ¹é… + nakamaManager.initClient(data.nakama_server || 'wss://nakama.1024tool.vip', data.nakama_key || 'defaultkey'); + await nakamaManager.authenticateWithGameToken(data.game_token); + setIsConnected(true); + addLog('system', '✅ 釿–°è¿žæŽ¥æˆåŠŸ'); + + // è‡ªåŠ¨å¼€å§‹åŒ¹é… + setTimeout(() => startMatchmaking(), 500); + } else { + addLog('system', 'âŒ èŽ·å–æ¸¸æˆå‡­è¯å¤±è´¥'); + } + } catch (err) { + console.error('Refresh token error:', err); + addLog('system', '⌠网络错误,请ç¨åŽé‡è¯•'); + } + + setIsRefreshing(false); + }; + + const GuideModal = () => ( +
+
+
+

玩法说明 (How to Play)

+ +
+
+
+

ðŸ›¡ï¸ é“具百科

+
+ {[ + { i: '💊', n: '医疗包', d: '回å¤1点HP并清除中毒状æ€' }, + { i: '🛡ï¸', n: '护盾', d: '抵挡下次å—到的伤害' }, + { i: 'ðŸ”', n: '放大镜', d: 'éšæœºçª¥è§†1个未翻开格å­çš„内容' }, + { i: '🔪', n: '匕首', d: 'å¯¹éšæœºä¸€ä¸ªå¯¹æ‰‹é€ æˆ1点伤害' }, + { i: 'âš¡', n: '闪电', d: '对所有玩家(包括自己)造æˆ1点伤害' }, + { i: 'â­ï¸', n: '好人å¡', d: '获得护盾,但跳过本回åˆ' }, + { i: '💖', n: '夿´»', d: 'HPå½’é›¶æ—¶ç«‹å³ä»¥1HP状æ€å¤æ´»' }, + { i: 'â°', n: '定时炸弹', d: '3回åˆåŽçˆ†ç‚¸ï¼Œå¯¹æºå¸¦è€…造æˆä¼¤å®³' }, + { i: '☠ï¸', n: '毒è¯', d: 'ä½¿éšæœºå¯¹æ‰‹ä¸­æ¯’ï¼Œæ¯æ­¥æ‰£é™¤HP' }, + { i: '👻', n: '诅咒', d: '获得诅咒状æ€ï¼Œç‰¹å®šæ“ä½œä¼šè§¦å‘æ‰£è¡€' }, + { i: '📦', n: 'å®ç®±', d: '神秘大奖,游æˆç»“æŸåŽç»“ç®—' }, + ].map(item => ( +
+ {item.i} +
+
{item.n}
+
{item.d}
+
+
+ ))} +
+
+
+

🾠角色天赋

+
+ {[ + { i: 'ðŸ¶', n: 'å°ç‹—', d: '忠诚:æ¯ç§»åŠ¨ä¸€å®šæ­¥æ•°å¿…è§¦å‘æ”¾å¤§é•œæ•ˆæžœ' }, + { i: 'ðŸ˜', n: '大象', d: '执拗:无法回å¤ç”Ÿå‘½ï¼Œä½†åŸºç¡€HP更高' }, + { i: 'ðŸ¯', n: '虎哥', d: '猛攻:匕首进化为全å±èŒƒå›´ä¼¤å®³' }, + { i: 'ðŸµ', n: '猴å­', d: 'æ•é”ï¼šæ¯æ¬¡ç‚¹å‡»éƒ½æœ‰æ¦‚率å‘现香蕉(回血)' }, + { i: '🦥', n: '树懒', d: '迟缓:翻到炸弹时伤害å‡åŠ(扣1点)' }, + { i: '河马', n: '河马', d: '大胃:无法直接æ¡èµ·é“å…·å¡' }, + ].map(char => ( +
+ {char.i} +
{char.n}:{char.d}
+
+ ))} +
+
+
+
+
+ ); + + // Initialize Nakama with GameToken + useEffect(() => { + const initNakama = async () => { + try { + // Get game token from URL parameters (passed from mini-program) + const urlParams = new URLSearchParams(window.location.search); + const gameToken = urlParams.get('game_token'); + const nakamaServer = urlParams.get('nakama_server') || import.meta.env.VITE_NAKAMA_SERVER || 'ws://localhost:7350'; + const nakamaKey = urlParams.get('nakama_key') || import.meta.env.VITE_NAKAMA_SERVER_KEY || 'defaultkey'; + + if (!gameToken) { + addLog('system', '⌠缺少游æˆä»¤ç‰Œ'); + addLog('system', '💡 请从å°ç¨‹åºæ­£ç¡®å…¥å£è¿›å…¥æ¸¸æˆ,或检查URL傿•°æ˜¯å¦åŒ…å«game_token'); + return; + } + + // Fetch game config to get match_player_count (使用åŒåŸŸé¿å… CORS) + try { + const backendUrl = 'https://game.1024tool.vip'; // 通过 Nginx åå‘代ç†åˆ°åŽç«¯ + const configResp = await fetch(`${backendUrl}/api/internal/game/minesweeper/config`, { + headers: { 'X-Internal-Key': 'bindbox-internal-secret-2024' } + }); + if (configResp.ok) { + const config = await configResp.json(); + if (config.match_player_count && config.match_player_count >= 2) { + setMatchPlayerCount(config.match_player_count); + console.log('Loaded match_player_count from config:', config.match_player_count); + } + } + } catch (err) { + console.warn('Failed to fetch game config:', err); + addLog('system', 'âš ï¸ æ— æ³•åŠ è½½æ¸¸æˆé…ç½®,使用默认设置(2人匹é…)'); + } + + // Initialize client with server info + nakamaManager.initClient(nakamaServer, nakamaKey); + + // Authenticate with game token + const session = await nakamaManager.authenticateWithGameToken(gameToken); + const userId = session.user_id || null; + setMyUserId(userId); + myUserIdRef.current = userId; + setIsConnected(true); + addLog('system', '✅ 已连接到æœåС噍'); + + const socket = nakamaManager.getSocket(); + if (socket) { + socket.onmatchmakermatched = async (matched) => { + console.log('🎯 Matchmaker matched!', { matchId: matched.match_id, users: matched.users?.length }); + addLog('system', `✅ åŒ¹é…æˆåŠŸï¼æ‰¾åˆ° ${matched.users?.length || matchPlayerCount} å玩家`); + setIsMatching(false); + try { + const match = await nakamaManager.joinMatch(matched.match_id, matched.token); + setMatchId(match.match_id); + addLog('system', `加入房间: ${match.match_id}`); + setTimeout(() => { + socket.sendMatchState(match.match_id, 100, JSON.stringify({ action: 'getState' })); + }, 100); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : '未知错误'; + addLog('system', `⌠加入房间失败: ${errorMsg}`); + addLog('system', '💡 å¯èƒ½æ˜¯ç½‘络问题或房间已满,è¯·é‡æ–°åŒ¹é…'); + } + }; + + socket.onmatchdata = (matchData) => { + const opCode = matchData.op_code; + try { + const data = JSON.parse(new TextDecoder().decode(matchData.data)) as GameState; + const prevState = prevGameStateRef.current; + + if (opCode === 1) { // GAME_START + setGameState(data); + prevGameStateRef.current = data; + setFloatingLabels([]); // Reset floating labels on new game + addLog('system', '🎮 游æˆå¼€å§‹ï¼'); + const isMe = data.turnOrder[0] === myUserIdRef.current; + addLog('system', isMe ? '🎯 你是先手,请点击格å­ï¼' : 'Ⳡ对手先手,请等待...'); + } else if (opCode === 2) { // UPDATE_STATE + if (prevState) { + // Cell Reveals + for (let i = 0; i < data.grid.length; i++) { + if (data.grid[i].revealed && !prevState.grid[i].revealed) { + const prevPlayerId = prevState.turnOrder[prevState.currentTurnIndex]; + const playerName = prevPlayerId === myUserIdRef.current ? 'ä½ ' : (prevState.players[prevPlayerId]?.username?.substring(0, 8) || '对手'); + const contentDesc = describeCellContent(data.grid[i]); + const width = data.gridSize || 10; + addLog('action', `${playerName} ç‚¹å‡»äº†æ ¼å­ [${Math.floor(i / width) + 1}, ${i % width + 1}]`); + addLog('effect', `å‘现: ${contentDesc}`); + + // Explicit HP change labels on cells + let cellLabel = contentDesc.split(' ')[0]; + let labelType: 'heal' | 'damage' | 'item' = data.grid[i].type === 'bomb' ? 'damage' : 'item'; + if (data.grid[i].type === 'bomb') { + cellLabel = '-2'; // standard bomb dmg + } else if (data.grid[i].itemId === 'medkit') { + cellLabel = '+1'; + labelType = 'heal'; + } + spawnLabel(50, 50, cellLabel, labelType, i); + } + } + + // HP Changes - 追踪伤害和治疗特效 + Object.keys(data.players).forEach(uid => { + const prevHp = prevState.players[uid]?.hp || 0; + const newHp = data.players[uid]?.hp || 0; + if (newHp < prevHp) { + const pName = uid === myUserIdRef.current ? 'ä½ ' : (data.players[uid]?.username?.substring(0, 8) || '对手'); + addLog('effect', `💔 ${pName} å—到 ${prevHp - newHp} 点伤害ï¼(${newHp}/${data.players[uid].maxHp})`); + spawnLabel(50, 20, `-${prevHp - newHp}`, 'damage', undefined, uid); + // 添加å—伤特效 + setDamagedPlayers(prev => [...prev, uid]); + setTimeout(() => setDamagedPlayers(prev => prev.filter(id => id !== uid)), 600); + // 如果是自己å—伤,å±å¹•抖动 + if (uid === myUserIdRef.current) { + setScreenShaking(true); + setTimeout(() => setScreenShaking(false), 400); + } + } else if (newHp > prevHp) { + const pName = uid === myUserIdRef.current ? 'ä½ ' : (data.players[uid]?.username?.substring(0, 8) || '对手'); + addLog('effect', `🌿 ${pName} 回å¤äº† ${newHp - prevHp} 点生命ï¼(${newHp}/${data.players[uid].maxHp})`); + spawnLabel(50, 20, `+${newHp - prevHp}`, 'heal', undefined, uid); + // 添加治疗特效 + setHealedPlayers(prev => [...prev, uid]); + setTimeout(() => setHealedPlayers(prev => prev.filter(id => id !== uid)), 600); + } + }); + + // Turn Changes + if (data.currentTurnIndex !== prevState.currentTurnIndex) { + if (data.turnOrder[data.currentTurnIndex] === myUserIdRef.current) { + addLog('system', '🎯 轮到你了ï¼'); + } + } + } + setGameState(data); + prevGameStateRef.current = data; + } else if (opCode === 6) { // GAME_OVER + const winnerName = data.winnerId === myUserIdRef.current ? 'ä½ ' : '对手'; + addLog('system', `🆠游æˆç»“æŸï¼${winnerName}获胜ï¼`); + setGameState(prev => prev ? { ...prev, ...data, gameStarted: false } : null); + } else if (opCode === 5) { // GAME_EVENT + // Special event from server (character abilities, item effects) + const event = data as unknown as { type: string; playerId: string; playerName: string; targetId?: string; targetName?: string; itemId?: string; value?: number; message: string }; + const isMe = event.playerId === myUserIdRef.current; + const prefix = isMe ? 'ä½ ' : (event.playerName?.substring(0, 8) || '对手'); + addLog('effect', `${prefix}: ${event.message}`); + + // Show floating damage/heal for affected players + if (event.type === 'damage' || event.type === 'item') { + const isGlobalDamage = event.itemId === 'lightning' || (event.itemId === 'knife' && event.message.includes('所有')); + + if (isGlobalDamage && event.value) { + // Global damage - show on my screen + spawnLabel(50, 50, `-${event.value}`, 'damage', undefined, myUserIdRef.current || undefined); + } else if (event.targetId === myUserIdRef.current && event.value) { + // I'm the target of damage + spawnLabel(50, 50, `-${event.value}`, 'damage', undefined, myUserIdRef.current || undefined); + } + } else if (event.type === 'ability' && event.message.includes('香蕉') && event.playerId === myUserIdRef.current) { + // Monkey banana heal + spawnLabel(50, 50, '+1', 'heal', undefined, myUserIdRef.current || undefined); + } + } + } catch (err) { + console.error('Match data error:', err); + } + }; + } + } catch (error) { + addLog('system', '连接æœåŠ¡å™¨å¤±è´¥'); + } + }; + initNakama(); + }, [addLog, spawnLabel]); + + // 匹é…计时器 + useEffect(() => { + let timer: ReturnType; + if (isMatching) { + setMatchingTimer(0); + timer = setInterval(() => { + setMatchingTimer(prev => prev + 1); + }, 1000); + } + return () => { + if (timer) clearInterval(timer); + setMatchingTimer(0); + }; + }, [isMatching]); + + // 回åˆå€’计时器 - 与æœåŠ¡çš‚15ç§’åŒæ­¥ + useEffect(() => { + if (!gameState?.gameStarted) return; + // æ¯æ¬¡å›žåˆåˆ‡æ¢æ—¶é‡ç½®ä¸º15ç§’ + setTurnTimer(15); + const timer = setInterval(() => { + setTurnTimer(prev => Math.max(0, prev - 1)); + }, 1000); + return () => clearInterval(timer); + }, [gameState?.currentTurnIndex, gameState?.gameStarted]); + + // Render Views + if (!isConnected) { + return ( +
+
+
连接æœåС噍䏭...
+
â³
+
+
+ ); + } + + if (!gameState) { + return ( +
+

动物扫雷大作战

+ {isMatching ? ( +
+
正在寻找对手...
+
🌀
+ + {/* 匹é…状æ€ä¿¡æ¯ */} +
+
+ â±ï¸ + {matchingTimer}ç§’ +
+
+ 🎮 éœ€è¦ {matchPlayerCount} å玩家 +
+ {matchingTimer > 3 && ( +
+ âš¡ 匹é…中...预计1-5ç§’å®Œæˆ +
+ )} + {matchingTimer > 8 && ( +
+ Ⳡ人数ä¸è¶³ï¼Œç»§ç»­ç­‰å¾…中... +
+ )} +
+
+ ) : ( + + )} +
+ {logs.map(log =>
{log.content}
)} +
+
+
+ ); + } + + const myPlayer = gameState.players[myUserId!] || null; + const opponents = Object.values(gameState.players).filter(p => p.userId !== myUserId); + const isMyTurn = gameState.turnOrder[gameState.currentTurnIndex] === myUserId; + + return ( +
+
+
Round {gameState.round}
+

动物扫雷

+ + +
+ +
+
+ {opponents.map(p => ( +
+
+
{p.avatar}
+
+
{p.username || 'Opponent'}
+ {renderHeart(p.hp, p.maxHp)} + {renderStatusIcons(p)} +
+
+
+ ))} +
+ +
+ {/* å›žåˆæŒ‡ç¤ºå™¨å’Œå€’计时 */} +
+
+ {isMyTurn ? '🎯 你的回åˆ' : 'Ⳡ等待对手'} +
+
+ â±ï¸ + {turnTimer} + ç§’ +
+
+ {/* å€’è®¡æ—¶è¿›åº¦æ¡ */} +
+
+
+
+ {gameState.grid.map((cell, idx) => { + const showContent = cell.revealed || debugMode; + return ( +
handleCellClick(idx)} className={`aspect-square rounded-sm relative flex items-center justify-center text-sm lg:text-2xl border border-white/5 transition-all + ${showContent ? (cell.type === 'bomb' ? 'bg-rose-900/50' + (cell.revealed ? ' explosion' : '') : cell.type === 'item' ? `bg-blue-900/40${cell.revealed ? ` item-${cell.itemId || 'generic'}` : ''}` : 'bg-slate-800/50') : 'bg-emerald-900/20 hover:bg-emerald-800/40 cursor-pointer'} + ${debugMode && !cell.revealed ? 'opacity-60 border-dashed border-yellow-500/50' : ''}`}> + {showContent && ( + cell.type === 'bomb' ? '💣' : + cell.type === 'item' ? getItemIcon(cell.itemId) : + (cell.neighborBombs ? ( + + {cell.neighborBombs} + + ) : (debugMode ? '✅' : '')) + )} + {floatingLabels.filter(l => l.cellIndex === idx).map(l => ( +
{l.text}
+ ))} +
+ ); + })} +
+
+ +
+ {myPlayer && ( +
+
+
{myPlayer.avatar}
+
+
我
+ {renderHeart(myPlayer.hp, myPlayer.maxHp)} + {renderStatusIcons(myPlayer)} +
+
+
+ )} +
+

战斗日志

+
+ {logs.map(log =>
{log.content}
)} +
+
+
+
+
+ + {!gameState.gameStarted && gameState.winnerId && ( +
+
+
{gameState.winnerId === myUserId ? 'ðŸ†' : '💀'}
+

{gameState.winnerId === myUserId ? '胜利ï¼' : '失败'}

+ + +
+
+ )} + + {showGuide && } +
+ ); +}; + +export default App; diff --git a/app/src/Explosion.css b/app/src/Explosion.css new file mode 100644 index 0000000..60f23c5 --- /dev/null +++ b/app/src/Explosion.css @@ -0,0 +1,343 @@ +/* Base Animations */ +@keyframes explode { + 0% { + transform: scale(1); + opacity: 1; + } + + 50% { + transform: scale(1.5); + opacity: 0.8; + box-shadow: 0 0 20px #ef4444, 0 0 40px #fb923c; + } + + 100% { + transform: scale(1); + opacity: 1; + box-shadow: 0 0 10px rgba(239, 68, 68, 0.4); + } +} + +@keyframes itemReveal { + 0% { + transform: scale(0.5) rotate(-20deg); + opacity: 0; + } + + 60% { + transform: scale(1.2) rotate(10deg); + } + + 100% { + transform: scale(1) rotate(0); + opacity: 1; + } +} + +@keyframes pulseGlowGreen { + + 0%, + 100% { + box-shadow: 0 0 5px #10b981; + } + + 50% { + box-shadow: 0 0 20px #10b981, 0 0 30px #34d399; + } +} + +@keyframes pulseGlowBlue { + + 0%, + 100% { + box-shadow: 0 0 5px #3b82f6; + } + + 50% { + box-shadow: 0 0 20px #3b82f6, 0 0 30px #60a5fa; + } +} + +@keyframes pulseGlowPurple { + + 0%, + 100% { + box-shadow: 0 0 5px #8b5cf6; + } + + 50% { + box-shadow: 0 0 20px #8b5cf6, 0 0 30px #a78bfa; + } +} + +@keyframes pulseGlowRed { + + 0%, + 100% { + box-shadow: 0 0 5px #ef4444; + } + + 50% { + box-shadow: 0 0 20px #ef4444, 0 0 30px #f87171; + } +} + +@keyframes pulseGlowGold { + + 0%, + 100% { + box-shadow: 0 0 5px #f59e0b; + } + + 50% { + box-shadow: 0 0 20px #f59e0b, 0 0 30px #fbbf24; + } +} + +@keyframes glint { + 0% { + filter: brightness(1); + } + + 50% { + filter: brightness(1.8) contrast(1.2); + } + + 100% { + filter: brightness(1); + } +} + +@keyframes shake { + + 0%, + 100% { + transform: translate(0, 0); + } + + 25% { + transform: translate(-2px, -2px); + } + + 50% { + transform: translate(2px, 2px); + } + + 75% { + transform: translate(-2px, 2px); + } +} + +@keyframes floating { + + 0%, + 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(-5px); + } +} + +/* Item Classes */ +.explosion { + animation: explode 0.5s ease-out; +} + +/* Category: Healing */ +.item-medkit { + animation: itemReveal 0.5s ease-out, pulseGlowGreen 2s infinite; + background-color: rgba(16, 185, 129, 0.2) !important; +} + +.item-revive { + animation: itemReveal 0.5s ease-out, pulseGlowGreen 1.5s infinite; + background-color: rgba(16, 185, 129, 0.3) !important; + border: 1px solid #10b981; +} + +/* Category: Attack */ +.item-knife { + animation: itemReveal 0.5s ease-out, glint 1s infinite; + background-color: rgba(239, 68, 68, 0.1) !important; +} + +.item-lightning { + animation: itemReveal 0.4s ease-out, glint 0.5s infinite; + background-color: rgba(239, 68, 68, 0.2) !important; +} + +.item-poison { + animation: itemReveal 0.6s ease-out, pulseGlowPurple 3s infinite; + background-color: rgba(139, 92, 246, 0.2) !important; +} + +/* Category: Utility */ +.item-shield { + animation: itemReveal 0.5s ease-out, pulseGlowBlue 2s infinite; + background-color: rgba(59, 130, 246, 0.2) !important; + border-radius: 50% !important; +} + +.item-skip { + animation: itemReveal 0.5s ease-out, pulseGlowGold 2s infinite; + background-color: rgba(245, 158, 11, 0.2) !important; + opacity: 0.8; +} + +.item-magnifier { + animation: itemReveal 0.5s ease-out, floating 2s ease-in-out infinite; + background-color: rgba(59, 130, 246, 0.1) !important; +} + +/* Category: Risk/Meta */ +.item-bomb_timer { + animation: itemReveal 0.5s ease-out, pulseGlowRed 0.5s infinite, shake 0.2s infinite; + background-color: rgba(239, 68, 68, 0.3) !important; +} + +.item-curse { + animation: itemReveal 0.7s ease-out, pulseGlowPurple 1s infinite; + background-color: rgba(139, 92, 246, 0.3) !important; + filter: grayscale(0.5) contrast(1.5); +} + +.item-chest { + animation: itemReveal 0.5s ease-out, pulseGlowGold 3s infinite; + background-color: rgba(245, 158, 11, 0.3) !important; +} + +@keyframes floatUp { + 0% { + transform: translateY(0); + opacity: 0; + } + + 20% { + opacity: 1; + } + + 80% { + opacity: 1; + } + + 100% { + transform: translateY(-40px); + opacity: 0; + } +} + +.float-label { + position: absolute; + pointer-events: none; + font-weight: 900; + font-size: 1.5rem; + z-index: 100; + animation: floatUp 1s ease-out forwards; + text-shadow: 0 0 10px rgba(0, 0, 0, 0.8), 2px 2px 0 #000; + white-space: nowrap; +} + +.text-heal { + color: #10b981; +} + +.text-damage { + color: #ef4444; +} + +.text-item { + color: #fbbf24; +} + +/* Generic Item Reveal */ +.item-generic { + animation: itemReveal 0.5s ease-out; +} + +/* HP Damage Pulse */ +.hp-damage { + animation: hpPulse 0.4s ease-in-out; +} + +@keyframes hpPulse { + + 0%, + 100% { + transform: scale(1); + } + + 50% { + transform: scale(1.2); + color: #ef4444; + } +} + +/* å±å¹•抖动效果 - å—åˆ°ä¼¤å®³æ—¶è§¦å‘ */ +@keyframes screenShake { + 0%, 100% { transform: translate(0); } + 10% { transform: translate(-8px, 6px); } + 20% { transform: translate(8px, -6px); } + 30% { transform: translate(-6px, -6px); } + 40% { transform: translate(6px, 6px); } + 50% { transform: translate(-6px, 4px); } + 60% { transform: translate(6px, -4px); } + 70% { transform: translate(-4px, 6px); } + 80% { transform: translate(4px, -6px); } + 90% { transform: translate(-2px, 2px); } +} + +.screen-shake { + animation: screenShake 0.4s ease-in-out; +} + +/* 玩家å—ä¼¤é—ªçƒæ•ˆæžœ */ +@keyframes damageFlash { + 0%, 100% { + background: transparent; + box-shadow: none; + } + 25%, 75% { + background: rgba(239, 68, 68, 0.4); + box-shadow: 0 0 20px rgba(239, 68, 68, 0.6), inset 0 0 15px rgba(239, 68, 68, 0.3); + } + 50% { + background: rgba(239, 68, 68, 0.6); + box-shadow: 0 0 30px rgba(239, 68, 68, 0.8), inset 0 0 20px rgba(239, 68, 68, 0.5); + } +} + +.player-damaged { + animation: damageFlash 0.6s ease-in-out; +} + +/* 玩家治疗效果 */ +@keyframes healGlow { + 0%, 100% { + box-shadow: none; + } + 50% { + box-shadow: 0 0 20px rgba(16, 185, 129, 0.6), inset 0 0 10px rgba(16, 185, 129, 0.3); + } +} + +.player-healed { + animation: healGlow 0.6s ease-in-out; +} + +/* 回åˆå€’è®¡æ—¶è¿›åº¦æ¡ */ +.turn-timer-bar { + transition: width 1s linear; +} + +/* å€’è®¡æ—¶ç´§æ€¥çŠ¶æ€ */ +@keyframes urgentPulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.timer-urgent { + animation: urgentPulse 0.5s ease-in-out infinite; + color: #ef4444 !important; +} \ No newline at end of file diff --git a/app/src/assets/react.svg b/app/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/app/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/index.css b/app/src/index.css new file mode 100644 index 0000000..8479111 --- /dev/null +++ b/app/src/index.css @@ -0,0 +1,9 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + background-color: #0f172a; /* Dark theme base */ + color: white; + font-family: 'Inter', system-ui, -apple-system, sans-serif; +} diff --git a/app/src/lib/nakama.ts b/app/src/lib/nakama.ts new file mode 100644 index 0000000..ca8cafb --- /dev/null +++ b/app/src/lib/nakama.ts @@ -0,0 +1,134 @@ +import { Client, Session } from '@heroiclabs/nakama-js'; +import type { Socket } from '@heroiclabs/nakama-js'; + +// Game token info received from backend +export interface GameTokenInfo { + game_token: string; + expires_at: string; + nakama_server: string; + nakama_key: string; + remaining_times: number; +} + +class NakamaManager { + private client: Client | null = null; + private session: Session | null = null; + private socket: Socket | null = null; + private useSSL: boolean = false; + private gameToken: string | null = null; + + /** + * Initialize the Nakama client with server info from backend + * @param serverUrl WebSocket URL (e.g., "wss://nakama.example.com" or "ws://localhost:7350") + * @param serverKey Server key (default: "defaultkey") + */ + initClient(serverUrl: string, serverKey: string = 'defaultkey') { + // Parse server URL + const url = new URL(serverUrl.replace('wss://', 'https://').replace('ws://', 'http://')); + const host = url.hostname; + const port = url.port || (serverUrl.startsWith('wss') ? '443' : '7350'); + this.useSSL = serverUrl.startsWith('wss'); + + console.log(`Initializing Nakama client: ${this.useSSL ? 'wss' : 'ws'}://${host}:${port}`); + this.client = new Client(serverKey, host, port, this.useSSL); + } + + /** + * Authenticate with a game token from the backend + * @param gameToken JWT game token from /api/app/game/enter + */ + async authenticateWithGameToken(gameToken: string) { + if (!this.client) { + throw new Error('Client not initialized. Call initClient() first.'); + } + + // Return existing session if already authenticated with same token + if (this.session && this.socket && this.gameToken === gameToken) { + console.log('Already authenticated with this token, returning existing session'); + return this.session; + } + + this.gameToken = gameToken; + + // Use authenticateCustom with the game token as the custom ID + // This creates a unique Nakama user per game token + // The actual user verification happens in MatchJoinAttempt on the server + const customId = `game_${Date.now()}_${Math.random().toString(36).substring(7)}`; + + console.log('Authenticating with game token...'); + this.session = await this.client.authenticateCustom(customId, true); + + if (this.session.user_id) { + console.log('Nakama User ID:', this.session.user_id); + } + + // Connect Socket + this.socket = this.client.createSocket(this.useSSL, false); + await this.socket.connect(this.session, true); + console.log('Socket connected'); + + return this.session; + } + + /** + * Find a match using matchmaker + * The game_token is passed as metadata for server-side validation + */ + async findMatch(minCount: number = 2, maxCount: number = 2) { + if (!this.socket) throw new Error('Socket not connected'); + if (!this.gameToken) throw new Error('No game token set'); + + const query = '*'; + // Pass game_token as string properties for matchmaker + const stringProperties = { game_token: this.gameToken }; + + console.log(`Searching for match: ${minCount}-${maxCount} players`); + return this.socket.addMatchmaker(query, minCount, maxCount, stringProperties); + } + + /** + * Join a match with the game token + * @param matchId Match ID from matchmaker + * @param token Matchmaker token (if from matchmaker) + */ + async joinMatch(matchId: string, token?: string) { + if (!this.socket) throw new Error('Socket not connected'); + if (!this.gameToken) throw new Error('No game token set'); + + // Pass game_token in metadata for server validation + const metadata = { game_token: this.gameToken }; + + return this.socket.joinMatch(matchId, token, metadata); + } + + async sendMatchState(matchId: string, opCode: number, data: string) { + if (!this.socket) throw new Error('Socket not connected'); + return this.socket.sendMatchState(matchId, opCode, data); + } + + getSocket() { + return this.socket; + } + + getSession() { + return this.session; + } + + getGameToken() { + return this.gameToken; + } + + /** + * Disconnect and cleanup + */ + async disconnect() { + if (this.socket) { + await this.socket.disconnect(false); + this.socket = null; + } + this.session = null; + this.gameToken = null; + } +} + +export const nakamaManager = new NakamaManager(); diff --git a/app/src/main.tsx b/app/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/app/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/app/tailwind.config.js b/app/tailwind.config.js new file mode 100644 index 0000000..6fea2ec --- /dev/null +++ b/app/tailwind.config.js @@ -0,0 +1,19 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: { + colors: { + 'slate-900': '#0f172a', + 'slate-800': '#1e293b', + 'emerald-500': '#10b981', + 'rose-500': '#f43f5e', + 'blue-500': '#3b82f6', + } + }, + }, + plugins: [], +} diff --git a/app/tsconfig.app.json b/app/tsconfig.app.json new file mode 100644 index 0000000..2d92f9d --- /dev/null +++ b/app/tsconfig.app.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "module": "ESNext", + "types": [ + "vite/client" + ], + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/app/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/app/tsconfig.node.json b/app/tsconfig.node.json new file mode 100644 index 0000000..7eff309 --- /dev/null +++ b/app/tsconfig.node.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": [ + "ES2023" + ], + "module": "ESNext", + "types": [ + "node" + ], + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": [ + "vite.config.ts" + ] +} \ No newline at end of file diff --git a/app/vite.config.ts b/app/vite.config.ts new file mode 100644 index 0000000..351399c --- /dev/null +++ b/app/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + headers: { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }, + }, +}) diff --git a/docker-compose.all.yml b/docker-compose.all.yml new file mode 100644 index 0000000..b49958a --- /dev/null +++ b/docker-compose.all.yml @@ -0,0 +1,97 @@ +# 免釿œåŠ¡éƒ¨ç½² (åŽç«¯ + æ¸¸æˆæœ + æ•°æ®åº“) +# 使用方法: docker-compose -f docker-compose.all.yml up -d +services: + # ---------------------------------------------------- + # 1. 业务åŽç«¯ (Bindbox Game Backend) + # ---------------------------------------------------- + bindbox-game: + image: zfc931912343/bindbox-game:v1.12 + container_name: bindbox-game + restart: always + ports: + - "9991:9991" + volumes: + - ../bindbox_game/logs:/app/logs + # - ../bindbox_game/configs:/app/configs # æŒ‡å‘ bindbox_game 目录下的é…ç½® + environment: + - ACTIVE_ENV=pro + - TZ=Asia/Shanghai + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + # ---------------------------------------------------- + # 2. æ¸¸æˆæ•°æ®åº“ (CockroachDB for Nakama) + # ---------------------------------------------------- + nakama-db: + image: cockroachdb/cockroach:latest-v23.1 + container_name: nakama-db + command: start-single-node --insecure --store=attrs=ssd,path=/var/lib/cockroach/ --cache=.25 --max-sql-memory=.25 + restart: always + volumes: + - nakama-db-data:/var/lib/cockroach + ports: + - "26257:26257" + - "8081:8080" + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:8080/health?ready=1" ] + interval: 3s + timeout: 3s + retries: 5 + environment: + - TZ=Asia/Shanghai + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + # ---------------------------------------------------- + # 3. æ¸¸æˆæœåС噍 (Nakama) + # ---------------------------------------------------- + nakama: + image: zfc931912343/bindbox-saolei:v1.5 + container_name: nakama-server + environment: + # 直接使用æœåŠ¡å访问åŽç«¯ + - MINESWEEPER_BACKEND_URL=http://bindbox-game:9991/api/internal + - TZ=Asia/Shanghai + entrypoint: + - "/bin/sh" + - "-ecx" + - "/nakama/nakama migrate up --database.address root@nakama-db:26257 && exec /nakama/nakama --name nakama1 --database.address root@nakama-db:26257 --logger.level DEBUG --session.token_expiry_sec 7200 --metrics.prometheus_port 9100 --runtime.path /nakama/modules --matchmaker.interval_sec 1 --matchmaker.max_intervals 5" + restart: always + depends_on: + nakama-db: + condition: service_healthy + bindbox-game: + condition: service_started + volumes: + - nakama-data:/nakama/data + ports: + - "7350:7350" + - "7351:7351" + - "9100:9100" + healthcheck: + test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://bindbox-game:9991/" ] + interval: 10s + timeout: 5s + retries: 5 + networks: + - bindbox_net + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" +volumes: + nakama-db-data: + nakama-data: +networks: + bindbox_net: + name: bindbox_net + driver: bridge diff --git a/docker-compose.cloud.yml b/docker-compose.cloud.yml new file mode 100644 index 0000000..8f4a6ff --- /dev/null +++ b/docker-compose.cloud.yml @@ -0,0 +1,55 @@ +# 云端部署专用 - æ‰«é›·æ¸¸æˆæœåŠ¡ +# 使用方法: docker-compose -f docker-compose.cloud.yml up -d + +services: + nakama-db: + image: cockroachdb/cockroach:latest-v23.1 + container_name: nakama-db + command: start-single-node --insecure --store=attrs=ssd,path=/var/lib/cockroach/ + restart: always + volumes: + - nakama-db-data:/var/lib/cockroach + ports: + - "26257:26257" + - "8081:8080" + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:8080/health?ready=1" ] + interval: 3s + timeout: 3s + retries: 5 + + nakama: + image: zfc931912343/bindbox-saolei:v1.3 + container_name: nakama-server + environment: + # 使用 Docker 内部网络访问åŽç«¯æœåŠ¡ (需确ä¿åœ¨åŒä¸€ç½‘络下) + - MINESWEEPER_BACKEND_URL=http://blindbox-mms-api:9991/api/internal + entrypoint: + - "/bin/sh" + - "-ecx" + - "/nakama/nakama migrate up --database.address root@nakama-db:26257 && exec /nakama/nakama --name nakama1 --database.address root@nakama-db:26257 --logger.level DEBUG --session.token_expiry_sec 7200 --metrics.prometheus_port 9100 --runtime.path /nakama/modules" + restart: always + depends_on: + nakama-db: + condition: service_healthy + volumes: + - nakama-data:/nakama/data + ports: + - "7350:7350" + - "7351:7351" + - "9100:9100" + healthcheck: + test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7350/" ] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + nakama-db-data: + nakama-data: + + # 必须加入åŽç«¯æœåŠ¡æ‰€åœ¨çš„ç½‘ç»œæ‰èƒ½é€šè¿‡ service_name 访问 +networks: + default: + name: ${DOCKER_NETWORK_NAME:-bindbox_default} + external: true diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7c96b7a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,52 @@ +services: + cockroachdb: + image: cockroachdb/cockroach:latest-v23.1 + command: start-single-node --insecure --store=attrs=ssd,path=/var/lib/cockroach/ + restart: "no" + volumes: + - data:/var/lib/cockroach + expose: + - "8080" + - "26257" + ports: + - "26257:26257" + - "8081:8080" + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:8080/health?ready=1" ] + interval: 3s + timeout: 3s + retries: 5 + + nakama: + build: + context: ./server + dockerfile: Dockerfile + container_name: wuziqi-nakama-1 + entrypoint: + - "/bin/sh" + - "-ecx" + - > + /nakama/nakama migrate up --database.address root@cockroachdb:26257 && exec /nakama/nakama --name nakama1 --database.address root@cockroachdb:26257 --logger.level DEBUG --session.token_expiry_sec 7200 --metrics.prometheus_port 9100 --runtime.path /nakama/modules + restart: "no" + links: + - "cockroachdb:db" + depends_on: + cockroachdb: + condition: service_healthy + volumes: + - nakama-data:/nakama/data + expose: + - "7348" + ports: + - "7350:7350" + - "7351:7351" + - "9100:9100" + healthcheck: + test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:7350/" ] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + data: + nakama-data: diff --git a/nginx-cors-config.conf b/nginx-cors-config.conf new file mode 100644 index 0000000..219ab36 --- /dev/null +++ b/nginx-cors-config.conf @@ -0,0 +1,27 @@ +# CORS é…置:å…许游æˆå‰ç«¯è®¿é—®é…ç½® API +# 将此é…置添加到 mini-chat.1024tool.vip çš„ Nginx é…置中 + +location /api/internal/game/minesweeper/config { + # OPTIONS é¢„æ£€è¯·æ±‚å¤„ç† + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' 'https://game.1024tool.vip' always; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'X-Internal-Key, Content-Type, Accept' always; + add_header 'Access-Control-Max-Age' 3600; + add_header 'Content-Type' 'text/plain charset=UTF-8'; + add_header 'Content-Length' 0; + return 204; + } + + # 实际请求的 CORS 头 + add_header 'Access-Control-Allow-Origin' 'https://game.1024tool.vip' always; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'X-Internal-Key, Content-Type, Accept' always; + + # åå‘代ç†åˆ°åŽç«¯æœåŠ¡ + proxy_pass http://127.0.0.1:9991; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; +} diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..0f85963 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,16 @@ +FROM heroiclabs/nakama-pluginbuilder:3.21.1 AS builder + +ENV GO111MODULE=on +ENV CGO_ENABLED=1 + +WORKDIR /backend + +COPY go.mod . +COPY go.sum . +COPY main.go . + +RUN go build --trimpath --mod=mod --buildmode=plugin -o ./backend.so + +FROM heroiclabs/nakama:3.21.1 + +COPY --from=builder /backend/backend.so /nakama/modules/ diff --git a/server/backend.so b/server/backend.so new file mode 100644 index 0000000..a1860e9 Binary files /dev/null and b/server/backend.so differ diff --git a/server/build/main.js b/server/build/main.js new file mode 100644 index 0000000..40c6faa --- /dev/null +++ b/server/build/main.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +var match_handler_1 = require("./match_handler"); +var InitModule = function (ctx, logger, nk, initializer) { + logger.info('Nakama Server Module Loaded (TypeScript)'); + // Register Match Handler + initializer.registerMatch('animal_minesweeper', { + matchInit: match_handler_1.matchInit, + matchJoinAttempt: match_handler_1.matchJoinAttempt, + matchJoin: match_handler_1.matchJoin, + matchLeave: match_handler_1.matchLeave, + matchLoop: match_handler_1.matchLoop, + matchTerminate: match_handler_1.matchTerminate, + matchSignal: match_handler_1.matchSignal + }); + logger.info('Match Handler Registered: animal_minesweeper'); +}; diff --git a/server/build/match_handler.js b/server/build/match_handler.js new file mode 100644 index 0000000..e76e6ab --- /dev/null +++ b/server/build/match_handler.js @@ -0,0 +1,170 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.matchSignal = exports.matchTerminate = exports.matchLoop = exports.matchLeave = exports.matchJoin = exports.matchJoinAttempt = exports.matchInit = void 0; +var types_1 = require("./types"); +var MAX_PLAYERS = 4; +var GRID_SIZE = 100; // 10x10 +var BOMB_COUNT = 30; +var ITEM_COUNT_MIN = 5; +var ITEM_COUNT_MAX = 10; +var matchInit = function (ctx, logger, nk, params) { + logger.info('Match initialized'); + // Generate Grid + var grid = Array(GRID_SIZE).fill(null).map(function () { return ({ type: 'empty', revealed: false }); }); + // Place Bombs + var bombsPlaced = 0; + while (bombsPlaced < BOMB_COUNT) { + var idx = Math.floor(Math.random() * GRID_SIZE); + if (grid[idx].type === 'empty') { + grid[idx].type = 'bomb'; + bombsPlaced++; + } + } + // Place Items + var itemCount = Math.floor(Math.random() * (ITEM_COUNT_MAX - ITEM_COUNT_MIN + 1)) + ITEM_COUNT_MIN; + var itemsPlaced = 0; + while (itemsPlaced < itemCount) { + var idx = Math.floor(Math.random() * GRID_SIZE); + if (grid[idx].type === 'empty') { + grid[idx].type = 'item'; + grid[idx].itemId = types_1.ItemTypes[Math.floor(Math.random() * types_1.ItemTypes.length)]; + itemsPlaced++; + } + } + var state = { + players: {}, + grid: grid, + turnOrder: [], + currentTurnIndex: 0, + round: 1, + winnerId: null, + gameStarted: false + }; + return { + state: state, + tickRate: 1, // 1 tick per second is enough for turn-based, but maybe higher for responsiveness + label: 'Animal Minesweeper' + }; +}; +exports.matchInit = matchInit; +var matchJoinAttempt = function (ctx, logger, nk, dispatcher, tick, state, presence, metadata) { + if (state.gameStarted) { + return { state: state, accept: false, rejectMessage: 'Game already started' }; + } + if (Object.keys(state.players).length >= MAX_PLAYERS) { + return { state: state, accept: false, rejectMessage: 'Match full' }; + } + return { state: state, accept: true }; +}; +exports.matchJoinAttempt = matchJoinAttempt; +var matchJoin = function (ctx, logger, nk, dispatcher, tick, state, presences) { + presences.forEach(function (presence) { + // Random character assignment for now + var character = types_1.CharacterTypes[Math.floor(Math.random() * types_1.CharacterTypes.length)]; + state.players[presence.userId] = { + userId: presence.userId, + sessionId: presence.sessionId, + username: presence.username, + avatar: 'ðŸ¦', // Placeholder, map character to emoji later + hp: 4, // Default HP + maxHp: 4, + status: [], + character: character + }; + state.turnOrder.push(presence.userId); + logger.info("Player joined: ".concat(presence.userId)); + }); + // Check if full to start game + // Fix: Use >= to handle potential race condition where multiple players join in same tick + if (Object.keys(state.players).length >= MAX_PLAYERS && !state.gameStarted) { + state.gameStarted = true; + logger.info('Game Started! Broadcasting GAME_START'); + // Broadcast Game Start + dispatcher.broadcastMessage(types_1.OpCode.GAME_START, JSON.stringify(state)); + } + else { + logger.info("Player joined. Count: ".concat(Object.keys(state.players).length, ". Broadcasting UPDATE_STATE")); + // Broadcast updated state (new player joined) + dispatcher.broadcastMessage(types_1.OpCode.UPDATE_STATE, JSON.stringify(state)); + } + return { state: state }; +}; +exports.matchJoin = matchJoin; +var matchLeave = function (ctx, logger, nk, dispatcher, tick, state, presences) { + presences.forEach(function (presence) { + delete state.players[presence.userId]; + var idx = state.turnOrder.indexOf(presence.userId); + if (idx > -1) { + state.turnOrder.splice(idx, 1); + // Adjust turn index if necessary + if (idx < state.currentTurnIndex) { + state.currentTurnIndex--; + } + if (state.currentTurnIndex >= state.turnOrder.length) { + state.currentTurnIndex = 0; + } + } + }); + // Broadcast updated state (player left) + if (state.gameStarted && Object.keys(state.players).length > 0) { + dispatcher.broadcastMessage(types_1.OpCode.UPDATE_STATE, JSON.stringify(state)); + } + // If game in progress and players drop, handle logic (end game or continue) + if (Object.keys(state.players).length < 2 && state.gameStarted) { + state.winnerId = Object.keys(state.players)[0] || null; + dispatcher.broadcastMessage(types_1.OpCode.GAME_OVER, JSON.stringify({ winnerId: state.winnerId })); + return null; // End match + } + // Broadcast updated state (player left) + if (state.gameStarted) { + dispatcher.broadcastMessage(types_1.OpCode.UPDATE_STATE, JSON.stringify(state)); + } + return { state: state }; +}; +exports.matchLeave = matchLeave; +var matchLoop = function (ctx, logger, nk, dispatcher, tick, state, messages) { + if (!state.gameStarted) + return { state: state }; + messages.forEach(function (message) { + if (message.opCode === types_1.OpCode.MOVE) { + var data = JSON.parse(nk.binaryToString(message.data)); + handleMove(state, message.sender.userId, data.index, logger, dispatcher); + } + }); + return { state: state }; +}; +exports.matchLoop = matchLoop; +var matchTerminate = function (ctx, logger, nk, dispatcher, tick, state, graceSeconds) { + return { state: state }; +}; +exports.matchTerminate = matchTerminate; +var matchSignal = function (ctx, logger, nk, dispatcher, tick, state, data) { + return { state: state, data: data }; +}; +exports.matchSignal = matchSignal; +// --- Helper Functions --- +function handleMove(state, userId, cellIndex, logger, dispatcher) { + // Validate Turn + var currentUserId = state.turnOrder[state.currentTurnIndex]; + if (userId !== currentUserId) { + return; // Not your turn + } + var cell = state.grid[cellIndex]; + if (cell.revealed) + return; // Already revealed + // Reveal Cell + cell.revealed = true; + var player = state.players[userId]; + // Logic + if (cell.type === 'bomb') { + player.hp -= 2; + // Check death... + } + else if (cell.type === 'item') { + // Add item logic... + } + // Next Turn + state.currentTurnIndex = (state.currentTurnIndex + 1) % state.turnOrder.length; + // Broadcast Update + dispatcher.broadcastMessage(types_1.OpCode.UPDATE_STATE, JSON.stringify(state)); +} diff --git a/server/build/types.js b/server/build/types.js new file mode 100644 index 0000000..62c976d --- /dev/null +++ b/server/build/types.js @@ -0,0 +1,19 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CharacterTypes = exports.ItemTypes = exports.OpCode = void 0; +var OpCode; +(function (OpCode) { + OpCode[OpCode["GAME_START"] = 1] = "GAME_START"; + OpCode[OpCode["UPDATE_STATE"] = 2] = "UPDATE_STATE"; + OpCode[OpCode["MOVE"] = 3] = "MOVE"; + OpCode[OpCode["USE_ITEM"] = 4] = "USE_ITEM"; + OpCode[OpCode["TURN_CHANGE"] = 5] = "TURN_CHANGE"; + OpCode[OpCode["GAME_OVER"] = 6] = "GAME_OVER"; +})(OpCode || (exports.OpCode = OpCode = {})); +exports.ItemTypes = [ + 'medkit', 'bomb_timer', 'poison', 'shield', 'skip', + 'magnifier', 'knife', 'revive', 'lightning', 'chest', 'curse' +]; +exports.CharacterTypes = [ + 'elephant', 'cat', 'dog', 'monkey', 'chicken', 'sloth', 'hippo', 'tiger' +]; diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..2582e59 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,7 @@ +module wuziqi-server + +go 1.21 + +require github.com/heroiclabs/nakama-common v1.31.0 + +require google.golang.org/protobuf v1.31.0 // indirect diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..a3f0bb8 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,10 @@ +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/heroiclabs/nakama-common v1.31.0 h1:oaJbwVRUiFXA77gXF3XNrGCmR0CXf7+2vXEvaBLkP6w= +github.com/heroiclabs/nakama-common v1.31.0/go.mod h1:Os8XeXGvHAap/p6M/8fQ3gle4eEXDGRQmoRNcPQTjXs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..75a6525 --- /dev/null +++ b/server/main.go @@ -0,0 +1,1193 @@ +package main + +import ( + "bytes" + "context" + "database/sql" + "encoding/json" + "fmt" + "io/ioutil" + "math/rand" + "net/http" + "os" + "strings" + "time" + + "github.com/heroiclabs/nakama-common/runtime" +) + +// --- Constants & Enums --- + +const ( + OpCodeGameStart = 1 + OpCodeUpdateState = 2 + OpCodeMove = 3 + OpCodeGameEvent = 5 // Special game events (item use, character ability) + OpCodeGameOver = 6 + OpCodeGetState = 100 // New opcode for requesting current state + + MaxPlayers = 2 // Default value, will be overridden by config +) + +var ( + BackendBaseURL = "http://host.docker.internal:9991/api/internal" // Default + InternalAPIKey = "bindbox-internal-secret-2024" // Must match backend +) + +var httpClient = &http.Client{ + Timeout: 5 * time.Second, +} + +// Helper function to make authenticated internal API requests +func makeInternalRequest(method, url string, body []byte) (*http.Response, error) { + req, err := http.NewRequest(method, url, bytes.NewBuffer(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Internal-Key", InternalAPIKey) + return httpClient.Do(req) +} + +type MinesweeperConfig struct { + GridSize int `json:"grid_size"` + BombCount int `json:"bomb_count"` + ItemMin int `json:"item_min"` + ItemMax int `json:"item_max"` + HPInit int `json:"hp_init"` + MatchPlayerCount int `json:"match_player_count"` // Required players to start match + EnabledItems map[string]bool `json:"enabled_items"` + ItemWeights map[string]int `json:"item_weights"` +} + +func getMinesweeperConfig(logger runtime.Logger) *MinesweeperConfig { + url := BackendBaseURL + "/game/minesweeper/config" + logger.Info("Fetching minesweeper config from: %s", url) + + resp, err := makeInternalRequest("GET", url, nil) + if err != nil { + logger.Error("Network error fetching config (check BackendBaseURL): %v", err) + return nil + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + logger.Error("Backend config API returned status: %d (URL: %s)", resp.StatusCode, url) + return nil + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + logger.Error("Failed to read config response body: %v", err) + return nil + } + + var config MinesweeperConfig + if err := json.Unmarshal(body, &config); err != nil { + logger.Error("Failed to parse minesweeper config: %v. Raw body: %s", err, string(body)) + return nil + } + + return &config +} + +type VerifyTicketResponse struct { + Valid bool `json:"valid"` + UserID string `json:"user_id"` + RemainingTimes int `json:"remaining_times"` +} + +type SettleGameResponse struct { + Success bool `json:"success"` + Reward string `json:"reward"` +} + +// GameTokenInfo contains validated token information from backend +type GameTokenInfo struct { + Valid bool `json:"valid"` + UserID int64 `json:"user_id"` + Username string `json:"username"` + Avatar string `json:"avatar"` + GameType string `json:"game_type"` + Ticket string `json:"ticket"` + Error string `json:"error"` +} + +// validateGameToken validates a game token with the backend and returns user info +func validateGameToken(logger runtime.Logger, gameToken string) *GameTokenInfo { + logger.Info("Validating game token with backend") + + reqBody, _ := json.Marshal(map[string]string{ + "game_token": gameToken, + }) + + resp, err := makeInternalRequest("POST", BackendBaseURL+"/game/validate-token", reqBody) + if err != nil { + logger.Error("Failed to call backend validate-token API: %v", err) + return nil + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + logger.Error("Backend validate-token returned non-200 status: %d", resp.StatusCode) + return nil + } + + body, _ := ioutil.ReadAll(resp.Body) + var result GameTokenInfo + if err := json.Unmarshal(body, &result); err != nil { + logger.Error("Failed to parse validate-token response: %v", err) + return nil + } + + if !result.Valid { + logger.Warn("Game token invalid: %s", result.Error) + return nil + } + + return &result +} + +func verifyTicketWithBackend(logger runtime.Logger, userID string, ticket string) bool { + logger.Info("Verifying ticket with backend: %s for user %s", ticket, userID) + + reqBody, _ := json.Marshal(map[string]string{ + "user_id": userID, + "ticket": ticket, + }) + + resp, err := makeInternalRequest("POST", BackendBaseURL+"/game/verify", reqBody) + if err != nil { + logger.Error("Failed to call backend verify API: %v", err) + return false // Fail safe + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + logger.Error("Backend returned non-200 status: %d", resp.StatusCode) + return false + } + + body, _ := ioutil.ReadAll(resp.Body) + var result VerifyTicketResponse + if err := json.Unmarshal(body, &result); err != nil { + logger.Error("Failed to parse backend response: %v", err) + return false + } + + return result.Valid +} + +// settleGameWithBackend settles the game with the backend using the real user ID +func settleGameWithBackend(logger runtime.Logger, realUserID int64, ticket, matchID string, win bool, score int) { + logger.Info("Settling game with backend for realUserID %d (Win: %v)", realUserID, win) + + reqBody, _ := json.Marshal(map[string]interface{}{ + "user_id": fmt.Sprintf("%d", realUserID), // Backend expects string + "ticket": ticket, + "match_id": matchID, + "win": win, + "score": score, + }) + + // Async call to not block game loop too much, or use goroutine + go func() { + resp, err := makeInternalRequest("POST", BackendBaseURL+"/game/settle", reqBody) + if err != nil { + logger.Error("Failed to call backend settle API: %v", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + logger.Error("Backend settle returned non-200: %d", resp.StatusCode) + } else { + logger.Info("Game settled successfully with backend") + } + }() +} + +var ( + ItemTypes = []string{ + "medkit", "bomb_timer", "poison", "shield", "skip", + "magnifier", "knife", "revive", "lightning", "chest", "curse", + } + CharacterTypes = []string{ + "elephant", "cat", "dog", "monkey", "chicken", "sloth", "hippo", "tiger", + } + + CharacterData = map[string]struct { + MaxHP int + Avatar string + Desc string + }{ + "elephant": {MaxHP: 5, Avatar: "ðŸ˜", Desc: "High HP, can't use some items"}, + "cat": {MaxHP: 3, Avatar: "ðŸ±", Desc: "Max dmg taken is 1"}, + "dog": {MaxHP: 4, Avatar: "ðŸ¶", Desc: "Periodic magnifier"}, + "monkey": {MaxHP: 4, Avatar: "ðŸ’", Desc: "Get banana (heal) chance"}, + "chicken": {MaxHP: 4, Avatar: "ðŸ”", Desc: "Get item on dmg chance"}, + "sloth": {MaxHP: 4, Avatar: "🦥", Desc: "Immune poison, bomb dmg reduced"}, + "hippo": {MaxHP: 4, Avatar: "🦛", Desc: "Cant pick items, resist death chance"}, + "tiger": {MaxHP: 4, Avatar: "ðŸ¯", Desc: "Stronger knife"}, + } +) + +// --- Structs --- + +type GridCell struct { + Type string `json:"type"` // "empty" | "bomb" | "item" + ItemID string `json:"itemId,omitempty"` + Revealed bool `json:"revealed"` + NeighborBombs int `json:"neighborBombs"` // Count of adjacent bombs +} + +type Player struct { + UserID string `json:"userId"` // Nakama user ID + RealUserID int64 `json:"realUserId"` // Backend user ID (for settlements) + SessionID string `json:"sessionId"` + Username string `json:"username"` + Avatar string `json:"avatar"` + HP int `json:"hp"` + MaxHP int `json:"maxHp"` + Status []string `json:"status"` // Visual status tags + Character string `json:"character"` + Ticket string `json:"ticket"` // Store the ticket used to join + + // Status Flags + Shield bool `json:"shield"` + SkipTurn bool `json:"skipTurn"` + Poisoned bool `json:"poisoned"` + PoisonSteps int `json:"poisonSteps"` // Steps taken since poisoned + Revive bool `json:"revive"` + Curse bool `json:"curse"` + TimeBombTurns int `json:"timeBombTurns"` // Countdown for time bomb (0 = no bomb) + + // Character ability usage counters (for limits) + MonkeyBananaCount int `json:"monkeyBananaCount"` // Max 2 per game + ChickenItemCount int `json:"chickenItemCount"` // Max 2 per game + HippoDeathImmune bool `json:"hippoDeathImmune"` // Used once = true + + // Magnifier reveals (cell index -> cell type) + RevealedCells map[int]string `json:"revealedCells"` +} + +type GameState struct { + Players map[string]*Player `json:"players"` + Grid []*GridCell `json:"grid"` + GridSize int `json:"gridSize"` // Side length of the square grid + TurnOrder []string `json:"turnOrder"` + CurrentTurnIndex int `json:"currentTurnIndex"` + Round int `json:"round"` + GlobalTurnCount int `json:"globalTurnCount"` // Total turns taken (for Dog ability) + WinnerID string `json:"winnerId"` + GameStarted bool `json:"gameStarted"` + LastMoveTimestamp int64 `json:"lastMoveTimestamp"` // Unix timestamp in seconds +} + +type MoveMessage struct { + Index int `json:"index"` +} + +type GetStateMessage struct { + Action string `json:"action"` +} + +type MatchState struct { + State *GameState + HPInit int + MatchPlayerCount int // Dynamic player count for matching + ValidatedPlayers map[string]*GameTokenInfo // Cache: NakamaUserID -> validated token info +} + +// GameEvent represents a special event to be displayed in client logs +type GameEvent struct { + Type string `json:"type"` // "ability", "item", "damage", "heal", "status" + PlayerID string `json:"playerId"` + PlayerName string `json:"playerName"` + TargetID string `json:"targetId,omitempty"` + TargetName string `json:"targetName,omitempty"` + ItemID string `json:"itemId,omitempty"` + Value int `json:"value,omitempty"` + Message string `json:"message"` +} + +func broadcastEvent(dispatcher runtime.MatchDispatcher, event GameEvent) { + data, _ := json.Marshal(event) + dispatcher.BroadcastMessage(OpCodeGameEvent, data, nil, nil, true) +} + +// --- Match Handler Methods --- +type MatchHandler struct{} + +func (m *MatchHandler) MatchInit(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, params map[string]interface{}) (interface{}, int, string) { + logger.Error("MatchInit called (Go) - Dynamic Config Version") + + // Fetch dynamic config + apiConfig := getMinesweeperConfig(logger) + + // Set defaults if fetch fails + gridSizeSide := 10 + totalCells := 100 + bombCount := 30 + itemCountMin := 5 + itemCountMax := 10 + hpInit := 4 + + if apiConfig != nil { + if apiConfig.GridSize > 0 && apiConfig.GridSize <= 30 { + gridSizeSide = apiConfig.GridSize + } + totalCells = gridSizeSide * gridSizeSide + if apiConfig.BombCount > 0 && apiConfig.BombCount < totalCells { + bombCount = apiConfig.BombCount + } + if apiConfig.ItemMax >= apiConfig.ItemMin && apiConfig.ItemMin >= 0 { + itemCountMin = apiConfig.ItemMin + itemCountMax = apiConfig.ItemMax + } + if apiConfig.HPInit > 0 { + hpInit = apiConfig.HPInit + } + logger.Info("Using dynamic config: %+v (Final: Grid=%d, Bombs=%d, HP=%d)", apiConfig, gridSizeSide, bombCount, hpInit) + } + + // Dynamic match player count + matchPlayerCount := MaxPlayers // Default + if apiConfig != nil && apiConfig.MatchPlayerCount >= 2 && apiConfig.MatchPlayerCount <= 10 { + matchPlayerCount = apiConfig.MatchPlayerCount + logger.Info("Using dynamic match_player_count: %d", matchPlayerCount) + } + + // Generate Grid + grid := make([]*GridCell, totalCells) + for i := 0; i < totalCells; i++ { + grid[i] = &GridCell{Type: "empty", Revealed: false} + } + + // Place Bombs + bombsPlaced := 0 + for bombsPlaced < bombCount && bombsPlaced < totalCells { + idx := rand.Intn(totalCells) + if grid[idx].Type == "empty" { + grid[idx].Type = "bomb" + bombsPlaced++ + } + } + + // Filter enabled items and calculate weights + var pool []string + if apiConfig != nil && len(apiConfig.EnabledItems) > 0 { + for _, it := range ItemTypes { + if enabled, ok := apiConfig.EnabledItems[it]; ok && enabled { + weight := 10 + if w, ok := apiConfig.ItemWeights[it]; ok && w > 0 { + weight = w + } + for i := 0; i < weight; i++ { + pool = append(pool, it) + } + } + } + } + + // Fallback pool if empty + if len(pool) == 0 { + pool = ItemTypes + } + + // Place Items + itemCount := rand.Intn(itemCountMax-itemCountMin+1) + itemCountMin + itemsPlaced := 0 + for itemsPlaced < itemCount && (bombsPlaced+itemsPlaced) < totalCells { + idx := rand.Intn(totalCells) + if grid[idx].Type == "empty" { + grid[idx].Type = "item" + grid[idx].ItemID = pool[rand.Intn(len(pool))] + itemsPlaced++ + } + } + + // Calculate Neighbor Bombs for all cells + for i := 0; i < totalCells; i++ { + if grid[i].Type == "bomb" { + continue + } + + count := 0 + row := i / gridSizeSide + col := i % gridSizeSide + + // Check all 8 neighbors + for r := row - 1; r <= row+1; r++ { + for c := col - 1; c <= col+1; c++ { + // Skip valid check + if r >= 0 && r < gridSizeSide && c >= 0 && c < gridSizeSide { + // Skip self + if r == row && c == col { + continue + } + neighborIdx := r*gridSizeSide + c + if grid[neighborIdx].Type == "bomb" { + count++ + } + } + } + } + grid[i].NeighborBombs = count + } + + state := &GameState{ + Players: make(map[string]*Player), + Grid: grid, + GridSize: gridSizeSide, + TurnOrder: make([]string, 0), + CurrentTurnIndex: 0, + Round: 1, + WinnerID: "", + GameStarted: false, + } + + // Store hpInit in params or just use local, but MatchJoin needs it + // We can store it in MatchState for future joins + tickRate := 10 + label := "Animal Minesweeper" + + return &MatchState{ + State: state, + HPInit: hpInit, + MatchPlayerCount: matchPlayerCount, + ValidatedPlayers: make(map[string]*GameTokenInfo), + }, tickRate, label +} + +func (m *MatchHandler) MatchJoinAttempt(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presence runtime.Presence, metadata map[string]string) (interface{}, bool, string) { + matchState := state.(*MatchState) + gameState := matchState.State + + // 1. Validate GameToken (REQUIRED - prevents free play and spoofing) + gameToken, ok := metadata["game_token"] + if !ok || gameToken == "" { + logger.Warn("MatchJoinAttempt: No game_token provided for user %s, rejecting", presence.GetUserId()) + return state, false, "Game token required to join" + } + + // Validate token with backend and get real user info + tokenInfo := validateGameToken(logger, gameToken) + if tokenInfo == nil { + logger.Warn("MatchJoinAttempt: Invalid game_token for user %s", presence.GetUserId()) + return state, false, "Invalid or expired game token" + } + + // Cache the validated info for use in MatchJoin + matchState.ValidatedPlayers[presence.GetUserId()] = tokenInfo + logger.Info("MatchJoinAttempt: Validated user %s (RealUserID: %d, Username: %s)", + presence.GetUserId(), tokenInfo.UserID, tokenInfo.Username) + + if gameState.GameStarted { + return state, false, "Game already started" + } + if len(gameState.Players) >= matchState.MatchPlayerCount { + return state, false, "Match full" + } + + return state, true, "" +} + +func (m *MatchHandler) MatchJoin(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} { + matchState := state.(*MatchState) + gameState := matchState.State + + for _, presence := range presences { + if _, exists := gameState.Players[presence.GetUserId()]; exists { + continue + } + + // Get validated token info from cache + tokenInfo := matchState.ValidatedPlayers[presence.GetUserId()] + if tokenInfo == nil { + // This should not happen if MatchJoinAttempt worked correctly + logger.Error("MatchJoin: No cached token info for user %s, rejecting", presence.GetUserId()) + continue + } + + character := CharacterTypes[rand.Intn(len(CharacterTypes))] + charData := CharacterData[character] + initialHP := charData.MaxHP + if matchState.HPInit > 0 { + initialHP = matchState.HPInit + } + + // Use real user info from validated token + username := tokenInfo.Username + if username == "" { + username = presence.GetUsername() // Fallback to Nakama username + } + + player := &Player{ + UserID: presence.GetUserId(), + RealUserID: tokenInfo.UserID, // Backend user ID for settlements + SessionID: presence.GetSessionId(), + Username: username, + Avatar: charData.Avatar, + HP: initialHP, + MaxHP: initialHP, + Status: make([]string, 0), + Character: character, + RevealedCells: make(map[int]string), + Ticket: tokenInfo.Ticket, + } + + gameState.Players[presence.GetUserId()] = player + gameState.TurnOrder = append(gameState.TurnOrder, presence.GetUserId()) + logger.Info("Player joined: %s (RealUserID: %d, Username: %s)", + presence.GetUserId(), tokenInfo.UserID, username) + } + + // Check if full to start game + if len(gameState.Players) >= matchState.MatchPlayerCount && !gameState.GameStarted { + gameState.GameStarted = true + gameState.LastMoveTimestamp = time.Now().Unix() + logger.Info("Game Started! TurnOrder: %v, First player: %s", gameState.TurnOrder, gameState.TurnOrder[0]) + + // Consume game tickets for all players on match success + for _, player := range gameState.Players { + go func(p *Player) { + consumeReqBody, _ := json.Marshal(map[string]string{ + "user_id": fmt.Sprintf("%d", p.RealUserID), + "game_code": "minesweeper", + "ticket": p.Ticket, + }) + resp, err := makeInternalRequest("POST", BackendBaseURL+"/game/consume-ticket", consumeReqBody) + if err != nil { + logger.Error("Failed to consume ticket for user %d: %v", p.RealUserID, err) + return + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + logger.Error("Backend consume-ticket returned non-200 for user %d: %d", p.RealUserID, resp.StatusCode) + } else { + logger.Info("Successfully consumed ticket for user %d on match success", p.RealUserID) + } + }(player) + } + + data, _ := json.Marshal(gameState) + dispatcher.BroadcastMessage(OpCodeGameStart, data, nil, nil, true) + } else { + logger.Info("Player count: %d. Broadcasting UPDATE_STATE", len(gameState.Players)) + data, _ := json.Marshal(gameState) + dispatcher.BroadcastMessage(OpCodeUpdateState, data, nil, nil, true) + } + + return matchState +} + +func (m *MatchHandler) MatchLeave(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} { + matchState := state.(*MatchState) + gameState := matchState.State + + for _, presence := range presences { + delete(gameState.Players, presence.GetUserId()) + + // Remove from turn order + for i, uid := range gameState.TurnOrder { + if uid == presence.GetUserId() { + // Remove element + gameState.TurnOrder = append(gameState.TurnOrder[:i], gameState.TurnOrder[i+1:]...) + + // Adjust turn index + if i < gameState.CurrentTurnIndex { + gameState.CurrentTurnIndex-- + } + if len(gameState.TurnOrder) > 0 && gameState.CurrentTurnIndex >= len(gameState.TurnOrder) { + gameState.CurrentTurnIndex = 0 + } + break + } + } + } + + // Broadcast update if game is running + if gameState.GameStarted && len(gameState.Players) > 0 { + data, _ := json.Marshal(gameState) + dispatcher.BroadcastMessage(OpCodeUpdateState, data, nil, nil, true) + } + + // End game if less than 2 players (always need at least 2 to continue) + if gameState.GameStarted && len(gameState.Players) < 2 { + winnerID := "" + if len(gameState.Players) == 1 { + for uid := range gameState.Players { + winnerID = uid + break + } + } + gameState.WinnerID = winnerID + + endData, _ := json.Marshal(map[string]string{"winnerId": winnerID}) + dispatcher.BroadcastMessage(OpCodeGameOver, endData, nil, nil, true) + return nil // End match + } + + return matchState +} + +func (m *MatchHandler) MatchLoop(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, messages []runtime.MatchData) interface{} { + matchState := state.(*MatchState) + gameState := matchState.State + + // Only log when there are messages to process + if len(messages) > 0 { + logger.Info("MatchLoop processing %d messages", len(messages)) + } + + for _, message := range messages { + logger.Info("Processing message with op_code: %d, sender: %s", message.GetOpCode(), message.GetUserId()) + switch message.GetOpCode() { + case OpCodeMove: + if !gameState.GameStarted { + logger.Info("MatchLoop: Game not started ignoring move. GameStarted=%v", gameState.GameStarted) + continue // Skip move messages if game hasn't started + } + logger.Info("MatchLoop: Processing OpCodeMove") + var move MoveMessage + if err := json.Unmarshal(message.GetData(), &move); err != nil { + logger.Error("Failed to parse move data: %v", err) + continue + } + handleMove(gameState, message.GetUserId(), move.Index, logger, dispatcher) + + case OpCodeGetState: + // Handle get state request - send current state to requesting player + logger.Debug("Sending current game state to player: %s", message.GetUserId()) + data, _ := json.Marshal(gameState) + dispatcher.BroadcastMessage(OpCodeUpdateState, data, []runtime.Presence{message}, nil, true) + } + } + + // Inactivity Timeout Check (15 seconds) + if gameState.GameStarted && gameState.WinnerID == "" { + now := time.Now().Unix() + if now-gameState.LastMoveTimestamp >= 15 { + currentUserID := gameState.TurnOrder[gameState.CurrentTurnIndex] + logger.Info("Inactivity timeout for player %s (15s). Deducting 1 HP and advancing turn.", currentUserID) + + player := gameState.Players[currentUserID] + if player != nil { + // Deduct 1 HP + applyDamage(gameState, player, 1) + + // Advance Turn + advanceTurn(gameState, logger) + + // Update last move timestamp + gameState.LastMoveTimestamp = time.Now().Unix() + + // Check Game Over + if checkGameOver(gameState, dispatcher, logger) { + return matchState + } + + // Broadcast update + data, _ := json.Marshal(gameState) + dispatcher.BroadcastMessage(OpCodeUpdateState, data, nil, nil, true) + } + } + } + + return matchState +} + +func (m *MatchHandler) MatchTerminate(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, graceSeconds int) interface{} { + return state +} + +func (m *MatchHandler) MatchSignal(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, data string) (interface{}, string) { + return state, data +} + +// --- Helper Functions --- + +func handleMove(state *GameState, userID string, cellIndex int, logger runtime.Logger, dispatcher runtime.MatchDispatcher) { + logger.Info("handleMove: userID=%s, cellIndex=%d", userID, cellIndex) + + if len(state.TurnOrder) == 0 { + return + } + + currentUserID := state.TurnOrder[state.CurrentTurnIndex] + if userID != currentUserID { + logger.Info("handleMove: Not your turn. Expected=%s, Got=%s", currentUserID, userID) + return + } + + if cellIndex < 0 || cellIndex >= len(state.Grid) { + return + } + + cell := state.Grid[cellIndex] + if cell.Revealed { + return + } + + // Reveal + cell.Revealed = true + player := state.Players[userID] + + // Increment global turn counter + state.GlobalTurnCount++ + + // 狗狗天赋: å®šæœŸè§¦å‘æ”¾å¤§é•œæ•ˆæžœ + // 4人局æ¯6回åˆè§¦å‘一次,6人局æ¯9回åˆè§¦å‘一次,自动é€è§†ä¸€ä¸ªéšæœºæœªç¿»å¼€çš„æ ¼å­ + if player.Character == "dog" { + interval := 6 + if len(state.Players) >= 6 { + interval = 9 + } + if state.GlobalTurnCount%interval == 0 { + // éšæœºé€‰æ‹©ä¸€ä¸ªæœªç¿»å¼€çš„æ ¼å­é€è§†ç»™çީ家 + for i := 0; i < 100; i++ { + idx := rand.Intn(len(state.Grid)) + if !state.Grid[idx].Revealed { + logger.Info("Dog %s activates magnifier! Cell %d is %s", player.UserID, idx, state.Grid[idx].Type) + broadcastEvent(dispatcher, GameEvent{ + Type: "ability", PlayerID: player.UserID, PlayerName: player.Username, + Message: "🶠狗狗触å‘嗅觉天赋,获得了一个格å­çš„ä¿¡æ¯ï¼", + }) + break + } + } + } + } + + // 猴å­å¤©èµ‹: é¦™è•‰æ¦‚çŽ‡å›žå¤ + // æ¯æ¬¡è¡ŒåŠ¨æœ‰15%概率获得香蕉(回å¤1点HP),æ¯å±€æ¸¸æˆæœ€å¤šç”Ÿæ•ˆ2次 + if player.Character == "monkey" && player.MonkeyBananaCount < 2 && rand.Float32() < 0.15 { + healPlayer(player, 1) + player.MonkeyBananaCount++ + logger.Info("Monkey %s found a banana! (%d/2)", player.UserID, player.MonkeyBananaCount) + broadcastEvent(dispatcher, GameEvent{ + Type: "ability", PlayerID: player.UserID, PlayerName: player.Username, + Value: 1, Message: "🌠猴å­å‘现了香蕉,回å¤1点血é‡ï¼", + }) + } + + // Handle Cell Content + if cell.Type == "bomb" { + dmg := 2 + if player.Character == "sloth" { + dmg = 1 + } + logger.Info("Player %s stepped on bomb (dmg=%d)", player.UserID, dmg) + broadcastEvent(dispatcher, GameEvent{ + Type: "damage", PlayerID: player.UserID, PlayerName: player.Username, + Value: dmg, Message: fmt.Sprintf("💣 踩到炸弹,å—到%d点伤害ï¼", dmg), + }) + applyDamage(state, player, dmg) + + } else if cell.Type == "item" { + if player.Character == "hippo" { + logger.Info("Hippo %s cannot pick up items", player.UserID) + broadcastEvent(dispatcher, GameEvent{ + Type: "ability", PlayerID: player.UserID, PlayerName: player.Username, + ItemID: cell.ItemID, Message: "🦛 河马无法拾å–é“å…·ï¼", + }) + } else { + resolveItem(state, player, cell.ItemID, logger, dispatcher) + } + } + + // Check Game Over + if checkGameOver(state, dispatcher, logger) { + return + } + + // Advance Turn + advanceTurn(state, logger) + + // Update last move timestamp + state.LastMoveTimestamp = time.Now().Unix() + + // Double check Game Over (poison might have killed someone) + if checkGameOver(state, dispatcher, logger) { + return + } + + // Broadcast + data, _ := json.Marshal(state) + dispatcher.BroadcastMessage(OpCodeUpdateState, data, nil, nil, true) +} + +// resolveItem 处ç†é“具效果 +// é“具被拾å–åŽè‡ªåŠ¨ä½¿ç”¨ï¼Œæ ¹æ®ä¸åŒé“具类型触å‘对应效果 +func resolveItem(state *GameState, player *Player, item string, logger runtime.Logger, dispatcher runtime.MatchDispatcher) { + logger.Info("Player %s used item: %s", player.UserID, item) + + // 大象角色é™åˆ¶: 无法使用医疗包ã€å¥½äººå¡ã€å¤æ´»ç”² + if player.Character == "elephant" && (item == "medkit" || item == "skip" || item == "revive") { + logger.Info("Elephant refused item %s", item) + broadcastEvent(dispatcher, GameEvent{ + Type: "ability", PlayerID: player.UserID, PlayerName: player.Username, + ItemID: item, Message: "😠大象无法使用该é“å…·ï¼", + }) + return + } + + switch item { + case "medkit": + player.Poisoned = false + player.PoisonSteps = 0 + healPlayer(player, 1) + broadcastEvent(dispatcher, GameEvent{ + Type: "item", PlayerID: player.UserID, PlayerName: player.Username, + ItemID: item, Value: 1, Message: "💊 使用医疗包,回å¤1血并解除中毒ï¼", + }) + case "bomb_timer": + player.TimeBombTurns = 3 + logger.Info("Player %s has a time bomb! Explodes in 3 turns", player.UserID) + broadcastEvent(dispatcher, GameEvent{ + Type: "item", PlayerID: player.UserID, PlayerName: player.Username, + ItemID: item, Value: 3, Message: "Ⱐ定时炸弹å¯åŠ¨ï¼Œ3回åˆåŽçˆ†ç‚¸ï¼", + }) + case "poison": + target := getRandomAliveTarget(state, player.UserID) + if target != nil { + if target.Character == "sloth" { + logger.Info("Sloth resisted poison") + broadcastEvent(dispatcher, GameEvent{ + Type: "item", PlayerID: player.UserID, PlayerName: player.Username, + TargetID: target.UserID, TargetName: target.Username, + ItemID: item, Message: "🦥 树懒å…疫了毒è¯ï¼", + }) + } else { + target.Poisoned = true + broadcastEvent(dispatcher, GameEvent{ + Type: "item", PlayerID: player.UserID, PlayerName: player.Username, + TargetID: target.UserID, TargetName: target.Username, + ItemID: item, Message: fmt.Sprintf("â˜ ï¸ %s 中毒了ï¼", target.Username), + }) + } + } + case "shield": + player.Shield = true + broadcastEvent(dispatcher, GameEvent{ + Type: "item", PlayerID: player.UserID, PlayerName: player.Username, + ItemID: item, Message: "ðŸ›¡ï¸ èŽ·å¾—æŠ¤ç›¾ï¼Œå¯æŠµæŒ¡ä¸€æ¬¡ä¼¤å®³ï¼", + }) + case "skip": + player.SkipTurn = true + player.Shield = true + broadcastEvent(dispatcher, GameEvent{ + Type: "item", PlayerID: player.UserID, PlayerName: player.Username, + ItemID: item, Message: "â­ï¸ 好人å¡ï¼šè·³è¿‡å›žåˆå¹¶èŽ·å¾—æŠ¤ç›¾ï¼", + }) + case "magnifier": + for i := 0; i < 100; i++ { + idx := rand.Intn(len(state.Grid)) + if !state.Grid[idx].Revealed { + cellType := state.Grid[idx].Type + if state.Grid[idx].Type == "item" { + cellType = state.Grid[idx].ItemID + } + player.RevealedCells[idx] = cellType + logger.Info("Magnifier: Player %s can now see cell %d (%s)", player.UserID, idx, cellType) + broadcastEvent(dispatcher, GameEvent{ + Type: "item", PlayerID: player.UserID, PlayerName: player.Username, + ItemID: item, Message: "🔠放大镜:é€è§†äº†ä¸€ä¸ªéšè—æ ¼å­ï¼", + }) + break + } + } + case "knife": + dmg := 1 + isAOE := false + if player.Character == "tiger" { + dmg = 2 + isAOE = true + } + + if isAOE { + broadcastEvent(dispatcher, GameEvent{ + Type: "item", PlayerID: player.UserID, PlayerName: player.Username, + ItemID: item, Value: dmg, Message: fmt.Sprintf("ðŸ¯ðŸ”ª è€è™Žçš„飞刀对所有敌人造æˆ%d点伤害ï¼", dmg), + }) + for _, p := range state.Players { + if p.UserID != player.UserID && p.HP > 0 { + applyDamage(state, p, dmg) + } + } + } else { + target := getRandomAliveTarget(state, player.UserID) + if target != nil { + broadcastEvent(dispatcher, GameEvent{ + Type: "item", PlayerID: player.UserID, PlayerName: player.Username, + TargetID: target.UserID, TargetName: target.Username, + ItemID: item, Value: dmg, Message: fmt.Sprintf("🔪 飞刀命中 %s,造æˆ%d点伤害ï¼", target.Username, dmg), + }) + applyDamage(state, target, dmg) + } + } + case "revive": + player.Revive = true + broadcastEvent(dispatcher, GameEvent{ + Type: "item", PlayerID: player.UserID, PlayerName: player.Username, + ItemID: item, Message: "💖 èŽ·å¾—å¤æ´»ç”²ï¼Œå¯å…疫一次死亡ï¼", + }) + case "lightning": + broadcastEvent(dispatcher, GameEvent{ + Type: "item", PlayerID: player.UserID, PlayerName: player.Username, + ItemID: item, Value: 1, Message: "âš¡ 闪电对所有玩家造æˆ1点伤害ï¼", + }) + for _, p := range state.Players { + applyDamage(state, p, 1) + } + case "chest": + // Meta reward, logic ignored + case "curse": + player.Curse = true + } +} + +func applyDamage(state *GameState, target *Player, amount int) { + if target.HP <= 0 { + return + } + + // 护盾优先抵挡伤害(消耗护盾,ä¸å—伤) + if target.Shield { + target.Shield = false + return // 完全格挡 + } + + // 猫咪天赋: 所有伤害强制为1点(诅咒加æˆä¹Ÿæ— æ•ˆï¼‰ + if target.Character == "cat" { + amount = 1 + target.Curse = false // 诅咒被消耗但ä¸ç”Ÿæ•ˆ + } else if target.Curse { + // éžçŒ«å’ªè§’色: è¯…å’’ä½¿ä¼¤å®³ç¿»å€ + amount *= 2 + target.Curse = false + } + + // Apply damage + target.HP -= amount + + // å¤å¤å¤©èµ‹: å—伤时有概率获得é“å…· + // 8%概率获得好人å¡/护盾/放大镜之一,æ¯å±€æœ€å¤šè§¦å‘2次 + if target.Character == "chicken" && target.HP > 0 && target.ChickenItemCount < 2 { + if rand.Float32() < 0.08 { + target.ChickenItemCount++ + // éšæœºèŽ·å¾—: 好人å¡(skip), 护盾(shield), 放大镜(magnifier) + items := []string{"skip", "shield", "magnifier"} + item := items[rand.Intn(len(items))] + switch item { + case "skip": + target.SkipTurn = true + target.Shield = true // 好人å¡é™„带护盾效果 + case "shield": + target.Shield = true + case "magnifier": + // æ”¾å¤§é•œæ•ˆæžœåœ¨å…¶ä»–åœ°æ–¹å¤„ç† + } + } + } + + // Death Check + if target.HP <= 0 { + if target.Revive { + target.Revive = false + target.HP = 1 + } else if target.Character == "hippo" && !target.HippoDeathImmune { + // 55% chance to survive death (once per game) + if rand.Float32() < 0.55 { + target.HP = 1 + target.HippoDeathImmune = true // Mark as used + } + } + } + + if target.HP < 0 { + target.HP = 0 + } +} + +func healPlayer(p *Player, amount int) { + if p.HP < p.MaxHP { + p.HP += amount + if p.HP > p.MaxHP { + p.HP = p.MaxHP + } + } +} + +func getRandomAliveTarget(state *GameState, excludeID string) *Player { + candidates := []*Player{} + for _, p := range state.Players { + if p.UserID != excludeID && p.HP > 0 { + candidates = append(candidates, p) + } + } + if len(candidates) == 0 { + return nil + } + + return candidates[rand.Intn(len(candidates))] +} + +func advanceTurn(state *GameState, logger runtime.Logger) { + scanCount := 0 + + for { + state.CurrentTurnIndex = (state.CurrentTurnIndex + 1) % len(state.TurnOrder) + scanCount++ + + // Prevent infinite loop if everyone skips/is dead + if scanCount > len(state.TurnOrder)*2 { + break + } + + nextUID := state.TurnOrder[state.CurrentTurnIndex] + nextPlayer := state.Players[nextUID] + + if nextPlayer.HP <= 0 { + continue + } + + // Handle Time Bomb countdown + if nextPlayer.TimeBombTurns > 0 { + nextPlayer.TimeBombTurns-- + if nextPlayer.TimeBombTurns == 0 { + // BOOM! Time bomb explodes + dmg := 2 + if nextPlayer.Character == "sloth" { + dmg = 1 // Sloth takes reduced bomb damage + } + logger.Info("Time bomb exploded on player %s! Taking %d damage", nextPlayer.UserID, dmg) + applyDamage(state, nextPlayer, dmg) + if nextPlayer.HP <= 0 { + continue // Died from bomb, skip turn + } + } + } + + // Handle Poison + if nextPlayer.Poisoned { + nextPlayer.PoisonSteps++ + if nextPlayer.PoisonSteps%2 == 0 { + applyDamage(state, nextPlayer, 1) + if nextPlayer.HP <= 0 { + continue // Died from poison, skip turn + } + } + } + + // Handle Skip + if nextPlayer.SkipTurn { + nextPlayer.SkipTurn = false + logger.Info("Player %s skipped turn", nextPlayer.UserID) + continue + } + + // Found valid player + break + } + + // Game Over check should happen outside +} + +func checkGameOver(state *GameState, dispatcher runtime.MatchDispatcher, logger runtime.Logger) bool { + alive := []string{} + for _, p := range state.Players { + if p.HP > 0 { + alive = append(alive, p.UserID) + } + } + + if len(alive) <= 1 { + winnerID := "" + if len(alive) == 1 { + winnerID = alive[0] + } + state.WinnerID = winnerID + state.GameStarted = false + + // 2. Settle Game with Backend using RealUserID + if winnerID != "" { + winnerPlayer := state.Players[winnerID] + if winnerPlayer != nil && winnerPlayer.RealUserID > 0 { + settleGameWithBackend(logger, winnerPlayer.RealUserID, winnerPlayer.Ticket, "", true, 100) + } else { + logger.Error("Winner player has no RealUserID, cannot settle") + } + } + + endData, _ := json.Marshal(map[string]interface{}{ + "winnerId": winnerID, + "gameState": state, + }) + dispatcher.BroadcastMessage(OpCodeGameOver, endData, nil, nil, true) + return true + } + return false +} + +// Presence helper for broadcast +type UserIDPresence struct { + UserID string + SessionID string + Username string +} + +func (p *UserIDPresence) GetUserId() string { return p.UserID } +func (p *UserIDPresence) GetSessionId() string { return p.SessionID } +func (p *UserIDPresence) GetNodeId() string { return "" } +func (p *UserIDPresence) GetHidden() bool { return false } +func (p *UserIDPresence) GetPersistence() bool { return false } +func (p *UserIDPresence) GetUsername() string { return p.Username } +func (p *UserIDPresence) GetStatus() string { return "" } +func (p *UserIDPresence) GetReason() runtime.PresenceReason { return runtime.PresenceReasonUnknown } + +// --- Init Module --- + +func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error { + logger.Error("Nakama Go Module Loaded - Dynamic Config Version") + + // Seed random + rand.Seed(time.Now().UnixNano()) + + // Initialize Backend URL from Env if exists + envURL := os.Getenv("MINESWEEPER_BACKEND_URL") + if envURL != "" { + BackendBaseURL = strings.TrimSuffix(envURL, "/") + logger.Info("Setting BackendBaseURL from environment: %s", BackendBaseURL) + } + + if err := initializer.RegisterMatch("animal_minesweeper", func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule) (runtime.Match, error) { + logger.Error("Creating new MatchHandler") + return &MatchHandler{}, nil + }); err != nil { + logger.Error("Unable to register match: %v", err) + return err + } + + // Register matchmaker matched hook - when 4 players are matched, create an authoritative match + if err := initializer.RegisterMatchmakerMatched(func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, entries []runtime.MatchmakerEntry) (string, error) { + logger.Info("Matchmaker matched! Creating authoritative match for %d players", len(entries)) + + // Create an authoritative match using our custom handler + matchId, err := nk.MatchCreate(ctx, "animal_minesweeper", nil) + if err != nil { + logger.Error("Failed to create match: %v", err) + return "", err + } + + logger.Info("Created authoritative match: %s", matchId) + return matchId, nil + }); err != nil { + logger.Error("Unable to register matchmaker matched hook: %v", err) + return err + } + + logger.Error("Match registration completed successfully") + return nil +} diff --git a/server/runtime.go b/server/runtime.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/server/runtime.go @@ -0,0 +1 @@ +package main diff --git a/游æˆé€»è¾‘文档.txt b/游æˆé€»è¾‘文档.txt new file mode 100644 index 0000000..3384e3e --- /dev/null +++ b/游æˆé€»è¾‘文档.txt @@ -0,0 +1,39 @@ +# 玩家设定 +1. æ¯äººè¡€é‡åˆå§‹åŒ–4ç‚¹è¡€é‡ +2. æ¡åˆ°é“具自动使用 + +# 棋盘设定 +1. 10*10 100个格å­ï¼Œé“å…·æ•°é‡ï¼Œéšæœº5-10个,炸弹数é‡ä¸º30 + + +# 扫雷逻辑 +1. 玩家点击æŸä¸ªæ ¼å­ï¼Œå¦‚果点击的是炸弹,扣除2点血é‡ï¼Œå¦‚果血é‡ä¸º0,玩家淘汰,剩下最åŽä¸€ä¸ªçŽ©å®¶å­˜æ´»ï¼Œåˆ™æ¸¸æˆç»“æŸï¼Œå­˜æ´»çŽ©å®¶ä¸ºèµ¢å®¶ +2. 玩家点击æŸä¸ªæ ¼å­ï¼Œå¦‚果点击的是é“具,则触å‘对应的é“具效果。 +3. çŽ©å®¶èŽ·å¾—éšæœºé¡ºåºï¼Œéšæœºä½ç½® +4. 掉线å…许é‡è¿ž +5. å…许观察比赛 + + +# é“具逻辑 +1. 医疗包 æ¢å¤1点血é‡,å¯ä»¥è§£é™¤ä¸­æ¯’效果 +2. 定时炸弹 踩到炸弹åŽï¼Œ3个回åˆåŽè¢«ç‚¸ï¼Œæ‰£é™¤2ç‚¹è¡€é‡ +3. 毒è¯ç“¶ éšæœºè®©ä¸€ä¸ªå­˜æ´»çš„玩家中毒,中毒åŽï¼Œæ¯2ä¸ªå›žåˆæ‰£é™¤1点血é‡ï¼Œé™¤äº†åŒ»ç–—包,ä¸èƒ½è§£é™¤ã€‚ +4. 护盾 å…ç–«1次伤害 +5. å¥½äººå¡ è·³è¿‡ä¸€ä¸ªè¡ŒåŠ¨å›žåˆï¼Œä¹Ÿæœ‰æŠ¤ç›¾çš„æ•ˆæžœï¼Œå…疫伤害 +6. 放大镜 ä¸ºçŽ©å®¶éšæœºæŸ¥çœ‹ä¸€ä¸ªæ ¼å­ï¼Œå¹¶æŠŠä¿¡æ¯å‘Šè¯‰è¯¥çީ家 +7. 飞刀 é™¤è¯¥çŽ©å®¶å¤–ï¼Œéšæœºä¸€ä¸ªçŽ©å®¶ï¼Œå—到1点伤害 +8. 夿´»ç”² å…疫一次死亡,ä¿ç•™1ç‚¹è¡€é‡ +9. 闪电 所有玩家扣除1ç‚¹è¡€é‡ +10. å®ç®± 游æˆç»“æŸçŽ©å®¶èŽ·å¾—1个å®ç®±ï¼Œå®ç®±å¯ä»¥å¼€å‡ºéšæœºçš„é“å…·å¡ç¢Žç‰‡ï¼Œ4ä¸ªç¢Žç‰‡åˆæˆä¸€ä¸ªé“å…·å¡ +11. 诅咒 下一次å—伤的伤害值乘以2 +# 角色逻辑 +1. 大象 è¡€é‡åˆå§‹åŒ–为5,无法使用医疗包ã€å¥½äººå¡ã€å¤æ´»ç”²è¿™ä¸‰ç§é“å…· +2. 猫咪 è¡€é‡åˆå§‹åŒ–为3,所有å—到的伤害强制为1点(包括诅咒加æˆï¼‰ +3. 汪汪 4(6)人赛中,æ¯6(9)回åˆè§¦å‘一次放大镜的é“å…·æ•ˆæžœï¼ŒèŽ·å¾—ä¸€ä¸ªéšæœºæ ¼å­ä¿¡æ¯ +4. çŒ´å­ æ¯å›žåˆæœ‰15%概率获得一个香蕉(回å¤1点血é‡ï¼‰ï¼Œæ¯å±€æœ€å¤šç”Ÿæ•ˆ2次 +5. å¤å¤ æ¯æ¬¡è¢«ä¼¤å®³ï¼Œæœ‰æ¦‚率获得一个é“具,é“å…·åªèƒ½æ˜¯å¥½äººå¡ã€æŠ¤ç›¾ã€æ”¾å¤§é•œï¼Œæ¦‚率为8%,æ¯å±€æœ€å¤šç”Ÿæ•ˆ2次 +6. 懒懒 å…疫毒è¯ç“¶ï¼Œå®šæ—¶ç‚¸å¼¹çš„伤害é™ä½Žä¸º1 +7. 河马 无法拾å–é“具,但有概率å…疫死亡,æ¯å±€æœ€å¤šç”Ÿæ•ˆ1次,并且概率为55% +8. è€è™Ž 当è€è™Žåœ¨åœºä¸Šæ—¶ï¼Œä½¿ç”¨é£žåˆ€çš„伤害å˜ä¸ºå…¨ä½“伤害,并且伤害为2 + + diff --git a/说明文档.md b/说明文档.md new file mode 100644 index 0000000..87084e8 --- /dev/null +++ b/说明文档.md @@ -0,0 +1,34 @@ +# 说明文档.md + +## 1. 项目规划 +### 1.1 项目简介 +本项目为一个基于扫雷机制的多人在线策略游æˆï¼ˆæš‚定å:动物扫雷大作战)。结åˆäº†RPG元素(血é‡ã€è§’色技能ã€é“具),支æŒå¤šäººå¯¹æˆ˜ã€‚ + +### 1.2 实施方案 +- **技术栈**: + - **Frontend**: React + Vite + TailwindCSS + @heroiclabs/nakama-js + - **Backend**: Nakama Server + CockroachDB (Docker) + - **Language**: TypeScript (Full Stack) +- **核心模å—**: + - **æ¸¸æˆæ ¸å¿ƒé€»è¾‘**: è¿ç§»è‡³æœåŠ¡ç«¯ (Authoritative Server) + - **å¤šäººç½‘ç»œåŒæ­¥**: Nakama Match (State Sync) + - **匹é…系统**: Nakama Matchmaker (4人自动匹é…) + +## 2. 进度记录 +| 任务ID | 任务åç§° | çŠ¶æ€ | 开始时间 | å®Œæˆæ—¶é—´ | 备注 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| P0-01 | UI设计方案制定 | å·²å®Œæˆ | 2025-12-17 | 2025-12-17 | 确立“现代æžç®€/轻科幻â€é£Žæ ¼ | +| P0-02 | 项目åˆå§‹åŒ– | å·²å®Œæˆ | 2025-12-17 | 2025-12-17 | Vite+React+Tailwindæ­å»º | +| P1-01 | æœåŠ¡ç«¯çŽ¯å¢ƒæ­å»º | å·²å®Œæˆ | 2025-12-17 | 2025-12-17 | Docker Compose (Nakama+CockroachDB) | +| P1-02 | å‰ç«¯SDKé›†æˆ | 进行中 | 2025-12-17 | - | 安装 nakama-js, å°è£… NakamaManager | + +## 3. 待办事项 +- [x] 确认UI设计方案 +- [x] æ­å»ºé¡¹ç›®åŸºç¡€ç»“æž„ +- [x] 创建 Docker Compose 环境 +- [x] å‰ç«¯å®‰è£… Nakama SDK +- [ ] å¯åŠ¨å¹¶éªŒè¯ Nakama æœåŠ¡ +- [ ] 实现å‰ç«¯è‡ªåŠ¨åŒ¹é…逻辑 (4人) +- [ ] 开呿œåŠ¡ç«¯æ ¸å¿ƒé€»è¾‘ (TypeScript) + - [ ] 匹é…处ç†å™¨ (Match Handler) + - [ ] 游æˆå¾ªçŽ¯ä¸ŽçŠ¶æ€åŒæ­¥