diff --git a/README.md b/README.md index 979f6c9b14a7a5c3c8549950089e53365f4030c9..4191460875aee3490572e22a4765b71eb21876fa 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,33 @@ -# � 涂色抢地盘游戏 - 开发指南 +# 鼠标大作战游戏 ## 项目概述 -这是一个基于Vue3和.NET 8的多人实时涂色抢地盘游戏,玩家通过涂色来占领画布区域,最终占领面积最大的玩家获胜。项目设计简单有趣,适合学生在4天时间内完成。 - +这是一个基于Vue3和.NET 8的多人实时涂色抢地盘游戏,玩家通过涂色来占领画布区域获得分值,同时添加了有趣的玩法供玩家额外提高分值,最后分最高者得胜。 ### � 游戏特色 -- **� 简单规则**: 涂色抢地盘,面积最大者获胜 +- **� 简单规则**: 涂色抢地盘,分值最大者获胜 - **⚡ 快节奏**: 每局3-5分钟,节奏紧凑刺激 -- **👥 多人对战**: 支持2-6人实时竞技 +- **👥 多人对战**: 支持2-10人实时竞技 - **🎯 公平竞技**: 防作弊机制,公平的游戏环境 - **📱 响应式**: 支持PC和移动端游戏 -### 🎯 核心玩法 +### 🎯 核心玩法 1. **房间匹配**: 创建或加入游戏房间 2. **颜色分配**: 系统自动为每位玩家分配专属颜色 3. **实时涂色**: 在画布上涂色占领区域,不能覆盖他人颜色 4. **面积统计**: 实时显示各玩家占领面积排名 -5. **胜负判定**: 时间结束后,占领面积最大的玩家获胜 +5. **胜负判定**: 时间结束后,分值最大的玩家获胜 +6. **玩法说明**: + - 移动:W/S 前后,A/D 左右旋转;J 键发射炮弹。 + - 炮弹:带飞行动画,命中墙体或玩家时在命中点产生实心爆炸并涂色。 + - 击杀与复活:被命中将死亡 5 秒,期间角色隐藏,仅显示黑色倒计时,时间到随机空地复活。 + - 击杀奖励:被击杀玩家有 25% 概率给予击杀者一个随机道具,结果会显示在右侧“击杀信息”。 + - 道具与背包:拾取的道具进入上方背包栏,按 K/L/U/I/O 使用,对应库存 -1;同类型持续效果可叠加延长(K 为即时生效不叠加)。 + - 道具刷新:系统周期刷新(约每 10 秒),地图会定期出现新的可拾取道具。 + - 隐身区域:地图上淡蓝色圆形范围内处于隐身状态。 ## 技术架构 -- **前端**: Vue 3 + Vite + Pinia + Socket.io-client +- **前端**: Vue 3 + Vite + Pinia + @microsoft/signalr - **后端**: ASP.NET Core 8 + SignalR + Entity Framework Core - **架构模式**: 简洁DDD + CQRS(轻量级) - **数据库**: PostgreSQL + Redis (游戏会话缓存) @@ -33,10 +40,6 @@ - **事件驱动**: 领域事件实现松耦合通信 - **分层清晰**: Domain → Application → Infrastructure → API -### 游戏界面参考 - - -![20250814233619](https://oss.9ihub.com/test/20250814233619.png) ## 项目结构 ``` @@ -143,240 +146,45 @@ territory-paint-game/ └── DEVELOPMENT.md # 开发文档 ``` -## 📅 4天开发计划 - -### 第1天:项目搭建和基础功能 -**目标**:完成项目架构搭建和基础涂色功能 - -**上午(4小时)**: -- [ ] 创建Vue3前端项目和.NET8后端项目 -- [ ] 设置PostgreSQL数据库和Redis缓存 -- [ ] 配置基础项目结构和依赖 - -**下午(4小时)**: -- [ ] 实现基础Canvas涂色功能 -- [ ] 开发面积计算算法 -- [ ] 创建游戏房间基础UI - -### 第2天:实时通信和多人游戏 -**目标**:实现实时多人涂色和游戏逻辑 - -**上午(4小时)**: -- [ ] 配置SignalR实时通信 -- [ ] 实现房间创建/加入功能 -- [ ] 开发实时涂色同步机制 - -**下午(4小时)**: -- [ ] 实现玩家颜色分配系统 -- [ ] 开发游戏状态管理(等待/游戏中/结束) -- [ ] 添加涂色冲突处理机制 - -### 第3天:游戏完善和用户体验 -**目标**:完善游戏功能和优化用户体验 - -**上午(4小时)**: -- [ ] 实现实时面积统计和排行榜 -- [ ] 开发游戏倒计时系统 -- [ ] 添加游戏结算功能 - -**下午(4小时)**: -- [ ] 优化UI/UX设计 -- [ ] 添加音效和动画效果 -- [ ] 实现响应式设计(移动端适配) - -### 第4天:测试优化和部署 -**目标**:测试、优化和部署上线 - -**上午(4小时)**: -- [ ] 进行多人游戏功能测试 -- [ ] 修复发现的bug和性能问题 -- [ ] 优化网络同步性能 - -**下午(4小时)**: -- [ ] 配置Docker容器化部署 -- [ ] 部署到测试环境并测试 -- [ ] 完善文档和演示准备 - -## 🎮 核心功能实现要点 - -### 1. 面积计算算法 -```javascript -// 像素级面积计算 -class AreaCalculator { - constructor(canvas) { - this.canvas = canvas; - this.ctx = canvas.getContext('2d'); - this.playerAreas = new Map(); - } - - calculateAreas() { - const imageData = this.ctx.getImageData(0, 0, - this.canvas.width, this.canvas.height); - const data = imageData.data; - const areas = new Map(); - - for (let i = 0; i < data.length; i += 4) { - const color = `rgb(${data[i]},${data[i+1]},${data[i+2]})`; - areas.set(color, (areas.get(color) || 0) + 1); - } - - return areas; - } -} -``` -### 2. 实时同步机制 -```javascript -// 前端涂色同步 -class PaintSynchronizer { - constructor(socketService) { - this.socket = socketService; - this.paintBuffer = []; - this.syncInterval = 50; // 50ms批量同步 - } - - addPaintAction(x, y, color) { - this.paintBuffer.push({ x, y, color, timestamp: Date.now() }); - - if (this.paintBuffer.length >= 10) { - this.flushBuffer(); - } - } - - flushBuffer() { - if (this.paintBuffer.length > 0) { - this.socket.emit('BatchPaintAction', this.paintBuffer); - this.paintBuffer = []; - } - } -} -``` +## 🚀 快速开始 -### 3. 游戏状态管理 -```csharp -// 后端游戏状态管理 -public class GameService -{ - public async Task CreateRoom(string roomName, int maxPlayers = 6) - { - var room = new GameRoom - { - Id = Guid.NewGuid(), - Name = roomName, - MaxPlayers = maxPlayers, - Status = GameStatus.Waiting, - GameDuration = 180 // 3分钟 - }; - - await _repository.AddAsync(room); - return room; - } - - public async Task StartGame(Guid roomId) - { - var room = await _repository.GetByIdAsync(roomId); - room.Status = GameStatus.Playing; - room.StartTime = DateTime.UtcNow; - - // 为玩家分配颜色 - await AssignPlayerColors(roomId); - - // 启动游戏计时器 - _ = Task.Run(() => GameTimer(roomId, room.GameDuration)); - } -} -``` - if (!this.isDrawing) return; - - this.ctx.lineTo(x, y); - this.ctx.stroke(); - - // 发送绘图数据到其他用户 - this.emitDrawAction({ - type: 'draw', - tool: this.currentTool, - points: [x, y], - color: this.ctx.strokeStyle, - width: this.ctx.lineWidth - }); - } -} +### 环境要求 +- .NET 8 SDK +- PostgreSQL 16 +- Node.js 18+ +- Docker & Docker Compose (可选) + +### 本地开发环境运行 + +#### 1. 克隆项目 +```bash +git clone https://gitee.com/grade-23-full-stack-class-2/lightweight-realtime-collab-app-class2-group2.git +cd lightweight-realtime-collab-app-class2-group2 ``` -### 3. 状态管理(Pinia) -```javascript -// stores/whiteboard.js -export const useWhiteboardStore = defineStore('whiteboard', { - state: () => ({ - currentRoom: null, - onlineUsers: [], - drawingHistory: [], - currentTool: 'pen', - currentColor: '#000000', - currentSize: 3 - }), - - actions: { - setTool(tool) { - this.currentTool = tool; - }, - - addDrawAction(action) { - this.drawingHistory.push(action); - }, - - updateOnlineUsers(users) { - this.onlineUsers = users; - } +#### 2. 数据库设置 +```bash + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Port=5432;Database=TerritoryGame;UserName=postgres;Password=123456;", + "Redis": "localhost" } -}); ``` -## 开发计划详细安排 - -### 第1天:项目搭建和基础功能 -**上午(3小时):** -- 创建Vue3项目,配置Vite和相关依赖 -- 创建.NET 8 API项目,配置SignalR -- 设计数据库表结构,配置EF Core - -**下午(4小时):** -- 实现基础Canvas绘图功能(画笔、橡皮擦) -- 创建工具栏组件和基础UI -- 测试本地绘图功能 - -### 第2天:实时协作核心功能 -**上午(3小时):** -- 配置SignalR Hub和前端Socket连接 -- 实现房间加入/离开功能 -- 测试基础实时通信 - -**下午(4小时):** -- 实现实时绘图数据同步 -- 添加在线用户列表显示 -- 实现用户光标位置同步 - -### 第3天:功能完善和优化 -**上午(3小时):** -- 添加更多绘图工具(直线、矩形、圆形) -- 实现撤销/重做功能 -- 添加颜色和画笔大小选择 - -**下午(4小时):** -- 实现画布保存和导出功能 -- 添加房间管理功能 -- 性能优化和错误处理 - -### 第4天:测试和部署 -**上午(3小时):** -- 功能测试和Bug修复 -- 添加响应式设计支持 -- 代码优化和文档编写 - -**下午(4小时):** -- 配置Docker容器化 -- 部署到测试环境 -- 最终测试和演示准备 +# 启动后端服务 +cd .\backend\src\ +dotnet restore +dotnet ef database update -p TerritoryGame.Infrastructure -s TerritoryGame.API 运行数据库迁移 +dotnet run --project TerritoryGame.API +# API服务将在 http://localhost:5115 启动 + +# 启动前端项目 +cd .\frontend +pnpm install +pnpm run dev +# 访问 http://localhost:5173 + + ## 关键技术难点解决方案 @@ -408,7 +216,7 @@ export const useWhiteboardStore = defineStore('whiteboard', { ### 前端技术栈 - **Vue 3 Composition API** 和现代前端开发模式 - **Canvas API** 和图形绘制技术 -- **实时通信** WebSocket和Socket.io的使用 +- **实时通信** Microsoft和SignalR的使用 - **状态管理** Pinia的状态管理最佳实践 ### 后端架构设计 @@ -431,34 +239,40 @@ export const useWhiteboardStore = defineStore('whiteboard', { - **代码质量** 单元测试和代码规范 - **性能优化** 缓存策略和并发处理 -## 📊 评估标准 - -### 功能实现(35%) -- **基础功能完整性**:核心涂色和实时同步功能 -- **游戏逻辑正确性**:面积计算、胜负判定等 -- **用户交互体验**:界面友好性和操作流畅度 - -### 架构设计(30%) -- **DDD架构实现**:领域层、应用层、基础设施层的正确划分 -- **CQRS模式应用**:命令和查询的合理分离 -- **代码组织结构**:分层清晰,职责明确 -- **设计模式使用**:仓储模式、事件驱动等 - -### 代码质量(25%) -- **代码规范性**:命名规范、代码风格一致性 -- **注释和文档**:关键业务逻辑的注释说明 -- **错误处理**:异常处理和边界情况考虑 -- **性能考虑**:并发处理、缓存使用等 - -### 创新特性(10%) -- **额外功能实现**:超出基础需求的功能 -- **技术创新应用**:新技术或优化方案的使用 -- **用户体验优化**:细节处理和交互改进 - -### DDD实践评分重点 -- **领域模型设计**:聚合根、实体、值对象的合理设计 -- **业务逻辑封装**:复杂游戏规则在领域服务中的实现 -- **事件驱动通信**:领域事件的正确使用和处理 -- **分层架构遵循**:各层职责清晰,依赖关系正确 - -这个项目既实用又有挑战性,非常适合作为高职学生的综合实训项目! +## 🎨 项目截图 +更多项目截图请见[项目截图](docs/暑期集训/08项目截图/项目截图.md) + +### 游戏界面 +![](https://lusiyi-picgo.oss-cn-beijing.aliyuncs.com/images/游戏界面2.png) +## 🌐 在线演示 +- 项目演示录屏可下载:[项目演示视频](docs/暑期集训/04项目演示视频/鼠标大作战游戏演示视频.mp4) + +### 成员部署地址 +| -------------------------- | +| | +| | +| | +| | + +## 🤝 贡献指南 + +1. Fork 本仓库 +2. 创建特性分支 (`git checkout -b feature/新功能`) +3. 提交更改 (`git commit -am '添加新功能'`) +4. 推送到分支 (`git push origin feature/新功能`) +5. 创建 Pull Request + +## 📄 许可证 + +本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情 + +## 📞 联系方式 + +- 项目负责人:[胡富文] +- 邮箱:[myhfw003@github.com] +- 技术支持:[技术交流群] + +--- + +**开发时间**: 1周 (2025年8月15日 - 2025年8月20日) +**文档更新**: 2025年8月21日 \ No newline at end of file diff --git a/backend/src/TerritoryGame.API/Controllers/AuthController.cs b/backend/src/TerritoryGame.API/Controllers/AuthController.cs new file mode 100644 index 0000000000000000000000000000000000000000..8673a2b764ddc36fa96846025f50e19368c735cd --- /dev/null +++ b/backend/src/TerritoryGame.API/Controllers/AuthController.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using TerritoryGame.Infrastructure.Data; +using TerritoryGame.Domain.Models; +using TerritoryGame.Infrastructure.Security; + +namespace TerritoryGame.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AuthController : ControllerBase +{ + private readonly TerritoryGameDbContext _db; + public AuthController(TerritoryGameDbContext db) => _db = db; + + public record RegisterRequest(string Username, string Password); + public record LoginRequest(string Username, string Password); + + [HttpPost("register")] + public async Task Register(RegisterRequest req) + { + if (string.IsNullOrWhiteSpace(req.Username) || string.IsNullOrWhiteSpace(req.Password)) + return BadRequest("用户名或密码不能为空"); + if (await _db.Users.AnyAsync(u => u.Username == req.Username)) + return Conflict("用户名已存在"); + var user = new User + { + Id = Guid.NewGuid(), + Username = req.Username, + PasswordHash = PasswordHasher.Hash(req.Password) + }; + _db.Users.Add(user); + await _db.SaveChangesAsync(); + return Ok(new { user.Id, user.Username }); + } + + [HttpPost("login")] + public async Task Login(LoginRequest req) + { + var user = await _db.Users.FirstOrDefaultAsync(u => u.Username == req.Username); + if (user == null) return Unauthorized("用户名或密码错误"); + if (!PasswordHasher.Verify(req.Password, user.PasswordHash)) return Unauthorized("用户名或密码错误"); + return Ok(new { user.Id, user.Username }); + } +} diff --git a/backend/src/TerritoryGame.API/Controllers/RoomsController.cs b/backend/src/TerritoryGame.API/Controllers/RoomsController.cs new file mode 100644 index 0000000000000000000000000000000000000000..288f610565109401951a7733fa8a1c20ad16a97a --- /dev/null +++ b/backend/src/TerritoryGame.API/Controllers/RoomsController.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using TerritoryGame.Application.Services; +using TerritoryGame.API.Hubs; +using System; +using System.Linq; + +namespace TerritoryGame.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class RoomsController : ControllerBase +{ + private readonly IGameManager _gameManager; + private readonly IHubContext _hubContext; + public RoomsController(IGameManager gameManager, IHubContext hubContext) + { + _gameManager = gameManager; + _hubContext = hubContext; + } + + [HttpGet] + public IActionResult List() => Ok(_gameManager.ListGames()); + + public record CreateRoomRequest(string Name, int MaxPlayers = 6, int DurationMinutes = 3, string? Password = null); + + // 按房间号/名称搜索(优先精确匹配,找不到则退化为包含匹配) + [HttpGet("search")] + public IActionResult Search([FromQuery] string q) + { + var all = _gameManager.ListGames(); + if (string.IsNullOrWhiteSpace(q)) return Ok(all); + var exact = all.Where(r => string.Equals(r.Code ?? r.Name, q, StringComparison.OrdinalIgnoreCase)); + var result = exact.Any() ? exact : all.Where(r => (r.Code ?? r.Name)?.Contains(q, StringComparison.OrdinalIgnoreCase) == true); + return Ok(result); + } + + [HttpPost] + public async Task Create(CreateRoomRequest req) + { + var dto = await _gameManager.CreateGameAsync(req.Name, req.MaxPlayers, req.DurationMinutes, req.Password); + // Create 阶段还没有玩家加入,HostPlayerName 为空,保持广播,后续 Join 会通过 OnRoomSnapshot 带来房主名称 + await _hubContext.Clients.All.SendAsync("OnRoomCreated", dto); + return Ok(dto); + } +} diff --git a/backend/src/TerritoryGame.API/Hubs/GameHub.cs b/backend/src/TerritoryGame.API/Hubs/GameHub.cs new file mode 100644 index 0000000000000000000000000000000000000000..4fc2e50977f88f00d42e61bc812a4ed72fd9bb52 --- /dev/null +++ b/backend/src/TerritoryGame.API/Hubs/GameHub.cs @@ -0,0 +1,504 @@ +using Microsoft.AspNetCore.SignalR; +using System.Collections.Concurrent; +using TerritoryGame.Application.Services; +using TerritoryGame.Application.Common; + +namespace TerritoryGame.API.Hubs; + +public class GameHub : Hub +{ + private readonly IGameManager _gameManager; + private readonly IHubContext _hubContext; // 用于后台计时广播,避免 Hub 实例释放后失效 + private static readonly ConcurrentDictionary _lastMoveAt = new(); + + public GameHub(IGameManager gameManager, IHubContext hubContext) + { + _gameManager = gameManager; + _hubContext = hubContext; + } + + public async Task JoinRoom(string gameId, string playerName, string? password = null) + { + try + { + var resolved = _gameManager.ResolveGameId(gameId); + // 如果还不存在并且 gameId 是 Guid 形式则按 Guid 解析,否则按名称解析失败即报错 + if (resolved == null) + { + // 尝试 Guid 解析(兼容原逻辑) + if (Guid.TryParse(gameId, out var parsed) && _gameManager.GetGameSnapshot(parsed) != null) + { + resolved = parsed; + } + else + { + throw new Exception("房间不存在"); + } + } + var player = await _gameManager.AddPlayerAsync(resolved.Value, playerName, Context.ConnectionId, password); + var groupKey = resolved.Value.ToString(); + await Groups.AddToGroupAsync(Context.ConnectionId, groupKey); + await Clients.Group(groupKey).SendAsync("OnPlayerJoined", new { player.Id, player.Name, player.Color, connectionId = Context.ConnectionId }); + // 告知调用者自己的玩家信息(用于本地 UI 颜色显示等) + await Clients.Caller.SendAsync("OnSelf", new { player.Id, player.Name, player.Color }); + var count = _gameManager.GetPlayerCount(resolved.Value); + await Clients.Group(groupKey).SendAsync("OnPlayerCount", count); + // 向所有客户端广播该房间人数变化(未加入房间的页面也能更新列表) + await Clients.All.SendAsync("OnPlayerCountChanged", new { gameId, count }); + var snapshot = _gameManager.GetGameSnapshot(resolved.Value); + if (snapshot != null) + { + // 组内用于房内 UI,同步;对所有人广播用于大厅列表(更新房主名/状态/人数等) + await Clients.Group(groupKey).SendAsync("OnRoomSnapshot", snapshot); + await Clients.All.SendAsync("OnRoomSnapshot", snapshot); + } + // 下发地图与道具与隐身区域 + var snapshot2 = _gameManager.GetGameSnapshot(resolved.Value); + await Clients.Caller.SendAsync("OnMap", new { width = snapshot2?.CanvasWidth, height = snapshot2?.CanvasHeight, walls = _gameManager.GetWalls(resolved.Value), items = _gameManager.GetItems(resolved.Value), zones = _gameManager.GetStealthZones(resolved.Value) }); + await SendAreaStats(groupKey); + } + catch (Exception ex) + { + await Clients.Caller.SendAsync("JoinFailed", new { message = ex.Message }); + } + } + + // 仅查看房间详情时请求一次快照 + public Task RequestRoomSnapshot(string gameId) + { + var resolved = _gameManager.ResolveGameId(gameId); + if (resolved == null) return Task.CompletedTask; + var snapshot = _gameManager.GetGameSnapshot(resolved.Value); + if (snapshot != null) + { + return Clients.Caller.SendAsync("OnRoomSnapshot", snapshot); + } + return Task.CompletedTask; + } + + public async Task LeaveRoom(string gameId) + { + var resolved = _gameManager.ResolveGameId(gameId); + if (resolved == null) return; + var result = await _gameManager.RemovePlayerAsync(resolved.Value, Context.ConnectionId); + var groupKey = resolved.Value.ToString(); + await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupKey); + _lastMoveAt.TryRemove(Context.ConnectionId, out _); // 清理频控缓存,防止重连后沿用旧时间 + if (result.Success) + { + if (result.GameDeleted) + { + // 房间被删除(例如房主离开时),通知所有客户端 + await Clients.All.SendAsync("OnRoomDeleted", new { gameId }); + } + else + { + await Clients.Group(groupKey).SendAsync("OnPlayerLeft", new { connectionId = Context.ConnectionId, playerId = result.RemovedPlayerId, wasHost = result.WasHost }); + var count = _gameManager.GetPlayerCount(resolved.Value); + await Clients.Group(groupKey).SendAsync("OnPlayerCount", count); + await Clients.All.SendAsync("OnPlayerCountChanged", new { gameId, count }); + var snapshot = _gameManager.GetGameSnapshot(resolved.Value); + if (snapshot != null) + { + await Clients.Group(groupKey).SendAsync("OnRoomSnapshot", snapshot); + await Clients.All.SendAsync("OnRoomSnapshot", snapshot); + } + await SendAreaStats(groupKey); + } + } + } + + public async Task StartGame(string gameId) + { + var resolved = _gameManager.ResolveGameId(gameId); + if (resolved == null) + { + await Clients.Caller.SendAsync("OnStartFailed", new { gameId, reason = "房间不存在" }); + return; + } + // 更明确的失败原因:人数不足 + if (_gameManager.GetPlayerCount(resolved.Value) < 2) + { + await Clients.Caller.SendAsync("OnStartFailed", new { gameId, reason = "至少两名玩家才能开始" }); + return; + } + var started = await _gameManager.StartGameAsync(resolved.Value); + if (started) + { + var groupKey = resolved.Value.ToString(); + await Clients.Group(groupKey).SendAsync("OnGameStarted", new { gameId }); + var snapshot = _gameManager.GetGameSnapshot(resolved.Value); + if (snapshot != null) + { + await Clients.Group(groupKey).SendAsync("OnRoomSnapshot", snapshot); + await Clients.All.SendAsync("OnRoomSnapshot", snapshot); + } + var remain = _gameManager.GetRemainingSeconds(resolved.Value); + await Clients.Group(groupKey).SendAsync("OnTimerUpdate", remain); + // 启动后台计时:使用加入房间时的 group key(即传入的 gameId 字符串,不强制换成 Guid.ToString()) + _ = RunTimer(resolved.Value, groupKey); + } + else + { + await Clients.Caller.SendAsync("OnStartFailed", new { gameId, reason = "已开始或无法开始" }); + } + } + + public async Task Draw(DrawCommandDto cmd) + { + var gameId = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gameId == null) return; + var update = await _gameManager.ProcessDrawAsync(gameId.Value, Context.ConnectionId, cmd.X, cmd.Y, cmd.Size, cmd.Color); + if (update) + { + await Clients.OthersInGroup(gameId.Value.ToString()).SendAsync("OnPaintActionReceived", new + { + playerConnectionId = Context.ConnectionId, + x = cmd.X, + y = cmd.Y, + color = cmd.Color, + size = cmd.Size + }); + await SendAreaStats(gameId.Value.ToString()); + } + } + + // 查询自己的背包 + public Task RequestInventory() + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return Task.CompletedTask; + var inv = _gameManager.GetInventory(gid.Value, Context.ConnectionId); + return Clients.Caller.SendAsync("OnInventory", inv); + } + + // 激活道具:type in [energy, stealth, speed, noclip, bigbang] + public Task ActivateItem(string type) + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return Task.CompletedTask; + var (ok, energy, inv) = _gameManager.ActivateItem(gid.Value, Context.ConnectionId, type); + _ = Clients.Caller.SendAsync("OnEnergy", energy); + // 同步背包 + _ = Clients.Caller.SendAsync("OnInventory", inv); + // 同步自身状态(包含 stealthUntil/speedUntil/noclipUntil/bigBangUntil),用于更新前端剩余时间显示 + var self = _gameManager.GetSelfState(gid.Value, Context.ConnectionId); + if (self != null) + { + _ = Clients.Caller.SendAsync("OnPlayerMoved", new { moved = self, energy }); + } + return Task.CompletedTask; + } + + // 新:移动(forward -1/0/1, rotate -1/0/1) + public Task Move(int forward, int rotate) + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return Task.CompletedTask; + // 频率限制:每连接 >=60ms + var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + var last = _lastMoveAt.GetOrAdd(Context.ConnectionId, 0); + if (now - last < 50) return Task.CompletedTask; + _lastMoveAt[Context.ConnectionId] = now; + var (ok, moved, energy, itemsChanged) = _gameManager.Move(gid.Value, Context.ConnectionId, forward, rotate); + if (ok && moved != null) + { + _ = Clients.Caller.SendAsync("OnEnergy", energy); // 仅调用者更新能量条 + // 仅在复活时把权威位置回显给调用者,避免平时重复回显导致本地抖动 + bool respawned = false; + try + { + var prop = moved.GetType().GetProperty("respawned"); + if (prop != null) + { + var val = prop.GetValue(moved); + if (val is bool b) respawned = b; + else if (val is string s) respawned = string.Equals(s, "true", StringComparison.OrdinalIgnoreCase); + } + } + catch { } + if (respawned) + { + _ = Clients.Caller.SendAsync("OnPlayerMoved", new { moved, energy }); + } + if (itemsChanged) + { + _ = Clients.Group(gid.Value.ToString()).SendAsync("OnItems", _gameManager.GetItems(gid.Value)); + // 给拾取者同步背包 + try + { + var inv = _gameManager.GetInventory(gid.Value, Context.ConnectionId); + _ = Clients.Caller.SendAsync("OnInventory", inv); + } + catch { } + } + // 给其他人广播;不给 Caller 再发一次,避免本地卡顿 + return Clients.OthersInGroup(gid.Value.ToString()).SendAsync("OnPlayerMoved", new { moved, energy }); + } + return Task.CompletedTask; + } + + // 新:客户端权威设置位置/角度(x,y 为世界坐标像素,angle 为弧度) + public Task ClientSetTransform(int x, int y, double angle) + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return Task.CompletedTask; + // 频率限制:每连接 >=60ms + var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + var last = _lastMoveAt.GetOrAdd(Context.ConnectionId, 0); + if (now - last < 50) return Task.CompletedTask; + _lastMoveAt[Context.ConnectionId] = now; + var (ok, moved, energy, itemsChanged) = _gameManager.ClientSetTransform(gid.Value, Context.ConnectionId, x, y, angle); + if (ok && moved != null) + { + _ = Clients.Caller.SendAsync("OnEnergy", energy); + // 仅在复活时把权威位置回显给调用者,避免平时重复回显导致本地抖动 + bool respawned = false; + try + { + var prop = moved.GetType().GetProperty("respawned"); + if (prop != null) + { + var val = prop.GetValue(moved); + if (val is bool b) respawned = b; + else if (val is string s) respawned = string.Equals(s, "true", StringComparison.OrdinalIgnoreCase); + } + } + catch { } + if (respawned) + { + _ = Clients.Caller.SendAsync("OnPlayerMoved", new { moved, energy }); + } + if (itemsChanged) + { + _ = Clients.Group(gid.Value.ToString()).SendAsync("OnItems", _gameManager.GetItems(gid.Value)); + // 给拾取者同步背包 + try + { + var inv = _gameManager.GetInventory(gid.Value, Context.ConnectionId); + _ = Clients.Caller.SendAsync("OnInventory", inv); + } + catch { } + } + // 仅向其他人广播,避免 Caller 自己收到重复 OnPlayerMoved 导致抖动 + return Clients.OthersInGroup(gid.Value.ToString()).SendAsync("OnPlayerMoved", new { moved, energy }); + } + return Task.CompletedTask; + } + + // 新:喷涂(前方扇形) + public async Task Spray(int radius, int angleWidth) + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return; + var (ok, energy, sector, stats, color) = _gameManager.Spray(gid.Value, Context.ConnectionId, radius, angleWidth, allowOverwrite: true); + if (!ok) { await Clients.Caller.SendAsync("OnEnergy", energy); return; } + await Clients.Group(gid.Value.ToString()).SendAsync("OnSpray", new { sector, radius, angleWidth, color }); + await Clients.Caller.SendAsync("OnEnergy", energy); + if (stats != null) + { + var mapped = stats.Select(s => new { playerId = s.PlayerId, playerName = s.PlayerName, color = s.Color, area = s.AreaPercent }); + await Clients.Group(gid.Value.ToString()).SendAsync("UpdateAreaStats", mapped); + } + } + + // 新:开火(炮弹爆炸,不规则涂抹 + 击杀 + 能量消耗) + public async Task FireCannon() + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return; + var (ok, energy, affected, explosion, killed, color, killFeed) = _gameManager.FireCannon(gid.Value, Context.ConnectionId); + if (!ok) { await Clients.Caller.SendAsync("OnEnergy", energy); return; } + // 弹道:起点->终点(客户端可做飞行动画) + if (explosion != null) + { + var ox = (int)explosion.GetType().GetProperty("ox")!.GetValue(explosion)!; + var oy = (int)explosion.GetType().GetProperty("oy")!.GetValue(explosion)!; + var ex = (int)explosion.GetType().GetProperty("x")!.GetValue(explosion)!; + var ey = (int)explosion.GetType().GetProperty("y")!.GetValue(explosion)!; + await Clients.Group(gid.Value.ToString()).SendAsync("OnProjectile", new { ox, oy, x = ex, y = ey, color }); + // 小延迟,等待前端绘制弹道 + await Task.Delay(90); + } + // 仅发送圆心与半径,减少广播数据量;客户端自行填充像素与渲染 + if (explosion != null) + { + var ex = (int)explosion.GetType().GetProperty("x")!.GetValue(explosion)!; + var ey = (int)explosion.GetType().GetProperty("y")!.GetValue(explosion)!; + var outer = (int)explosion.GetType().GetProperty("outer")!.GetValue(explosion)!; + await Clients.Group(gid.Value.ToString()).SendAsync("OnExplosion", new { x = ex, y = ey, r = outer, color, killed }); + } + await Clients.Caller.SendAsync("OnEnergy", energy); + // 击杀信息广播(含是否掉落道具) + if (killFeed != null) + { + await Clients.Group(gid.Value.ToString()).SendAsync("OnKillFeed", killFeed); + } + // 若因击杀获得道具,调用者背包已变化;这里直接同步一次背包(代价很小,省去服务端判断) + try + { + var inv = _gameManager.GetInventory(gid.Value, Context.ConnectionId); + await Clients.Caller.SendAsync("OnInventory", inv); + } + catch { } + // 新增:广播分数榜 + var scores = _gameManager.GetScores(gid.Value); + await Clients.Group(gid.Value.ToString()).SendAsync("OnScores", scores); + // 道具清单可能在移动发生变化,这里不刷新 + await SendAreaStats(gid.Value.ToString()); + } + + // 主动请求地图(墙体与当前道具) + public Task RequestMap() + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return Task.CompletedTask; + var snapshot = _gameManager.GetGameSnapshot(gid.Value); + return Clients.Caller.SendAsync("OnMap", new { width = snapshot?.CanvasWidth, height = snapshot?.CanvasHeight, walls = _gameManager.GetWalls(gid.Value), items = _gameManager.GetItems(gid.Value), zones = _gameManager.GetStealthZones(gid.Value) }); + } + + // 请求当前所有玩家运行时状态(位置/角度) + public Task SyncState() + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return Task.CompletedTask; + var states = _gameManager.GetPlayerStates(gid.Value); + return Clients.Caller.SendAsync("OnPlayerStates", states); + } + + private async Task SendAreaStats(string gameId) + { + var stats = _gameManager.GetAreaStats(Guid.Parse(gameId)) + .Select(s => new { playerId = s.PlayerId, playerName = s.PlayerName, color = s.Color, area = s.AreaPercent }); + await Clients.Group(gameId).SendAsync("UpdateAreaStats", stats); + } + + // 客户端可主动请求一次最新统计(例如进入游戏页时) + public Task RequestAreaStats(string gameId) + { + return SendAreaStats(gameId); + } + + // 客户端主动请求一次分数榜 + public Task RequestScores(string gameId) + { + var resolved = _gameManager.ResolveGameId(gameId); + if (resolved == null) return Task.CompletedTask; + var scores = _gameManager.GetScores(resolved.Value); + return Clients.Caller.SendAsync("OnScores", scores); + } + + // 主动补偿获取自身信息 + public Task GetSelf() + { + var dto = _gameManager.GetPlayerByConnection(Context.ConnectionId); + if (dto != null) + return Clients.Caller.SendAsync("OnSelf", new { dto.Id, dto.Name, dto.Color }); + return Task.CompletedTask; + } + + private async Task RunTimer(Guid gameGuid, string groupKey) + { + try + { + while (true) + { + var remaining = _gameManager.GetRemainingSeconds(gameGuid); + await _hubContext.Clients.Group(groupKey).SendAsync("OnTimerUpdate", remaining); + // 计时器驱动:检查并刷新道具,有新增则广播 + try + { + if (_gameManager.CheckAndSpawnItems(gameGuid)) + { + var items = _gameManager.GetItems(gameGuid); + await _hubContext.Clients.Group(groupKey).SendAsync("OnItems", items); + } + } + catch { } + if (remaining <= 0) + { + var result = _gameManager.GetResult(gameGuid); + await _hubContext.Clients.Group(groupKey).SendAsync("OnGameEnded", result); + break; + } + await Task.Delay(1000); + } + } + catch (Exception ex) + { + Console.WriteLine($"[RunTimer][Error] {gameGuid} {ex.Message}"); + } + } + + // 客户端可单独请求一次当前剩余时间(防止刷新后未收到 StartGame 初始推送) + public Task RequestTimer(string gameId) + { + var resolved = _gameManager.ResolveGameId(gameId); + if (resolved == null) return Task.CompletedTask; + var remain = _gameManager.GetRemainingSeconds(resolved.Value); + return Clients.Caller.SendAsync("OnTimerUpdate", remain); + } + + // 客户端补偿请求自身信息(如首次 OnSelf 丢失) + public Task RequestSelf() + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return Task.CompletedTask; + var snapshot = _gameManager.GetGameSnapshot(gid.Value); + if (snapshot == null) return Task.CompletedTask; + var player = snapshot.Players.FirstOrDefault(p => p != null && p.Id != Guid.Empty && _gameManager.GetGameIdByConnection(Context.ConnectionId) == gid); + // 上面方式拿不到 connection => 直接通过内部结构(简化:重新查 runtime PlayerId 映射) + // 为避免访问内部状态,可重新调用 AddPlayer 时已存储映射,这里简单返回颜色集合中第一个匹配颜色(退化方案) + // 这里直接忽略复杂映射,仅返回全部玩家列表以供客户端定位 + return Clients.Caller.SendAsync("OnSelfSnapshot", new { players = snapshot.Players }); + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + var gameId = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gameId != null) + { + await LeaveRoom(gameId.Value.ToString()); + } + _lastMoveAt.TryRemove(Context.ConnectionId, out _); // 清理频控 + await base.OnDisconnectedAsync(exception); + } + + // 房主踢人 + public async Task KickPlayer(string gameId, Guid targetPlayerId) + { + var resolved = _gameManager.ResolveGameId(gameId); + if (resolved == null) { await Clients.Caller.SendAsync("OnKickResult", new { ok = false, reason = "房间不存在" }); return; } + var (ok, removedId, removedConn, gameDeleted, isHost, reason) = _gameManager.KickPlayer(resolved.Value, Context.ConnectionId, targetPlayerId); + if (!ok) + { + await Clients.Caller.SendAsync("OnKickResult", new { ok, reason = reason ?? "失败" }); + return; + } + var groupKey = resolved.Value.ToString(); + if (gameDeleted) + { + await Clients.All.SendAsync("OnRoomDeleted", new { gameId }); + if (!string.IsNullOrEmpty(removedConn)) _lastMoveAt.TryRemove(removedConn, out _); + return; + } + if (!string.IsNullOrEmpty(removedConn)) + { + await Groups.RemoveFromGroupAsync(removedConn, groupKey); + _lastMoveAt.TryRemove(removedConn, out _); + await Clients.Client(removedConn).SendAsync("OnKicked", new { gameId }); + } + await Clients.Group(groupKey).SendAsync("OnPlayerKicked", new { playerId = removedId }); + var count = _gameManager.GetPlayerCount(resolved.Value); + await Clients.Group(groupKey).SendAsync("OnPlayerCount", count); + await Clients.All.SendAsync("OnPlayerCountChanged", new { gameId, count }); + var snapshot = _gameManager.GetGameSnapshot(resolved.Value); + if (snapshot != null) + { + await Clients.Group(groupKey).SendAsync("OnRoomSnapshot", snapshot); + await Clients.All.SendAsync("OnRoomSnapshot", snapshot); + } + await SendAreaStats(groupKey); + await Clients.Caller.SendAsync("OnKickResult", new { ok = true }); + } +} diff --git a/backend/src/TerritoryGame.API/Program.cs b/backend/src/TerritoryGame.API/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..ce6590a60b32725407e9638a378d5b137ec9dbf7 --- /dev/null +++ b/backend/src/TerritoryGame.API/Program.cs @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore; +using TerritoryGame.Infrastructure; +using TerritoryGame.Application; +using TerritoryGame.Infrastructure.Data; +using TerritoryGame.API.Services; + +var builder = WebApplication.CreateBuilder(args); + +// 添加服务到容器 +builder.Services.AddInfrastructure(builder.Configuration); +builder.Services.AddApplicationServices(); +builder.Services.AddSignalR(); +builder.Services.AddControllers(); +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.AllowAnyHeader().AllowAnyMethod().AllowCredentials().SetIsOriginAllowed(_ => true); + }); +}); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddHostedService(); + +var app = builder.Build(); + +// 关闭启动时自动迁移(避免 PendingModelChanges 阻塞启动) +// 如需迁移,请手动执行 dotnet-ef 或在 CI 中进行 + + +// 配置中间件 +// app.MapHub("/gameHub"); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseCors(); + +app.MapHub("/hubs/game"); +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/backend/src/TerritoryGame.API/Properties/launchSettings.json b/backend/src/TerritoryGame.API/Properties/launchSettings.json new file mode 100644 index 0000000000000000000000000000000000000000..a8ed14b3d10f76216330b9d60ff77ae278ea39f5 --- /dev/null +++ b/backend/src/TerritoryGame.API/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:60278", + "sslPort": 44338 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5115", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7219;http://localhost:5115", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/backend/src/TerritoryGame.API/Services/IdleRoomCleanupService.cs b/backend/src/TerritoryGame.API/Services/IdleRoomCleanupService.cs new file mode 100644 index 0000000000000000000000000000000000000000..210bda15c1eb5f01e8369cf200147d54d8ca57f5 --- /dev/null +++ b/backend/src/TerritoryGame.API/Services/IdleRoomCleanupService.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using TerritoryGame.API.Hubs; +using TerritoryGame.Application.Services; + +namespace TerritoryGame.API.Services; + +public class IdleRoomCleanupService : BackgroundService +{ + private readonly ILogger _logger; + private readonly IGameManager _gameManager; + private readonly IHubContext _hubContext; + private readonly TimeSpan _period = TimeSpan.FromMinutes(1); + private readonly TimeSpan _idle = TimeSpan.FromMinutes(30); + + public IdleRoomCleanupService(ILogger logger, IGameManager gameManager, IHubContext hubContext) + { + _logger = logger; + _gameManager = gameManager; + _hubContext = hubContext; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + var removed = _gameManager.CleanupIdleRooms(_idle).ToList(); + foreach (var id in removed) + { + await _hubContext.Clients.All.SendAsync("OnRoomDeleted", new { gameId = id.ToString() }, cancellationToken: stoppingToken); + _logger.LogInformation("Idle room deleted: {GameId}", id); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "IdleRoomCleanup error"); + } + await Task.Delay(_period, stoppingToken); + } + } +} diff --git a/backend/src/TerritoryGame.API/TerritoryGame.API.csproj b/backend/src/TerritoryGame.API/TerritoryGame.API.csproj new file mode 100644 index 0000000000000000000000000000000000000000..0bfaff0ba7b1ed311ec6e5644c80ea045463aac7 --- /dev/null +++ b/backend/src/TerritoryGame.API/TerritoryGame.API.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/backend/src/TerritoryGame.API/TerritoryGame.API.http b/backend/src/TerritoryGame.API/TerritoryGame.API.http new file mode 100644 index 0000000000000000000000000000000000000000..354d6e080f16e63dbb4a9d429965896449a88257 --- /dev/null +++ b/backend/src/TerritoryGame.API/TerritoryGame.API.http @@ -0,0 +1,6 @@ +@TerritoryGame.API_HostAddress = http://localhost:5115 + +GET {{TerritoryGame.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/backend/src/TerritoryGame.API/appsettings.json b/backend/src/TerritoryGame.API/appsettings.json new file mode 100644 index 0000000000000000000000000000000000000000..f7cf59cc297516eaca9a6c25f0bb99db95592761 --- /dev/null +++ b/backend/src/TerritoryGame.API/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Port=5432;Database=TerritoryGame;UserName=postgres;Password=123456;", + "Redis": "localhost" + } +} diff --git a/backend/src/TerritoryGame.Application/Class1.cs b/backend/src/TerritoryGame.Application/Class1.cs new file mode 100644 index 0000000000000000000000000000000000000000..8e7a47a9ca8bec27e6a612c27a7b821fd3f6f195 --- /dev/null +++ b/backend/src/TerritoryGame.Application/Class1.cs @@ -0,0 +1,6 @@ +namespace TerritoryGame.Application; + +public class Class1 +{ + +} diff --git a/backend/src/TerritoryGame.Application/Common/AreaStatDto.cs b/backend/src/TerritoryGame.Application/Common/AreaStatDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..bd1e41005e81bf1ce461bdc3b7a48acdbb153804 --- /dev/null +++ b/backend/src/TerritoryGame.Application/Common/AreaStatDto.cs @@ -0,0 +1,9 @@ +namespace TerritoryGame.Application.Common; + +public class AreaStatDto +{ + public Guid PlayerId { get; set; } + public string PlayerName { get; set; } = string.Empty; + public string Color { get; set; } = string.Empty; + public double AreaPercent { get; set; } +} diff --git a/backend/src/TerritoryGame.Application/Common/DrawCommandDto.cs b/backend/src/TerritoryGame.Application/Common/DrawCommandDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..dec38f31c13d27a77fa3a22f3e0fb6d5994a8245 --- /dev/null +++ b/backend/src/TerritoryGame.Application/Common/DrawCommandDto.cs @@ -0,0 +1,9 @@ +namespace TerritoryGame.Application.Common; + +public class DrawCommandDto +{ + public int X { get; set; } + public int Y { get; set; } + public int Size { get; set; } + public string Color { get; set; } = string.Empty; +} diff --git a/backend/src/TerritoryGame.Application/Common/GameDto.cs b/backend/src/TerritoryGame.Application/Common/GameDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..8838ad07753dede959913dbf0d5281c0248f2ae8 --- /dev/null +++ b/backend/src/TerritoryGame.Application/Common/GameDto.cs @@ -0,0 +1,21 @@ +using TerritoryGame.Domain.Models; + +namespace TerritoryGame.Application.Common; + +public class GameDto +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public string? Code { get; set; } + public string? HostPlayerName { get; set; } + public GameStatus Status { get; set; } + public int MaxPlayers { get; set; } + public int CurrentPlayers { get; set; } + public int DurationMinutes { get; set; } + // 地图尺寸(像素级活动区域,不等于前端画布视口尺寸) + public int CanvasWidth { get; set; } + public int CanvasHeight { get; set; } + public bool HasPassword { get; set; } + public string? Password { get; set; } // 仅在创建者返回时可选回传;生产应避免明文 + public List Players { get; set; } = new(); +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Common/GameResultDto.cs b/backend/src/TerritoryGame.Application/Common/GameResultDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..2ba981a818d2cfae935e9c0f79a5675b8a9f13d6 --- /dev/null +++ b/backend/src/TerritoryGame.Application/Common/GameResultDto.cs @@ -0,0 +1,21 @@ +namespace TerritoryGame.Application.Common; + +public class ScoreEntryDto +{ + public Guid PlayerId { get; set; } + public string PlayerName { get; set; } = string.Empty; + public string Color { get; set; } = string.Empty; + public int Score { get; set; } + public int Kills { get; set; } + public int PaintTiles { get; set; } + public double AreaPercent { get; set; } // 用于展示 +} + +public class GameResultDto +{ + public Guid GameId { get; set; } + public Guid? WinnerPlayerId { get; set; } + public double WinnerAreaPercent { get; set; } + public int WinnerScore { get; set; } + public IEnumerable Players { get; set; } = Enumerable.Empty(); +} diff --git a/backend/src/TerritoryGame.Application/Common/Mappings/MappingProfile.cs b/backend/src/TerritoryGame.Application/Common/Mappings/MappingProfile.cs new file mode 100644 index 0000000000000000000000000000000000000000..cd8035b9c8ec3a5d67c413e0090e7a2fef754354 --- /dev/null +++ b/backend/src/TerritoryGame.Application/Common/Mappings/MappingProfile.cs @@ -0,0 +1,13 @@ +using AutoMapper; +using TerritoryGame.Domain.Models; + +namespace TerritoryGame.Application.Common.Mappings; + +public class MappingProfile : Profile +{ + public MappingProfile() + { + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Common/PlayerDto.cs b/backend/src/TerritoryGame.Application/Common/PlayerDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..5fd14546d6725c3161da1534dc9ee2e6c94ce36a --- /dev/null +++ b/backend/src/TerritoryGame.Application/Common/PlayerDto.cs @@ -0,0 +1,9 @@ +namespace TerritoryGame.Application.Common; + +public class PlayerDto +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public string Color { get; set; } = null!; + public int Score { get; set; } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/DependencyInjection.cs b/backend/src/TerritoryGame.Application/DependencyInjection.cs new file mode 100644 index 0000000000000000000000000000000000000000..19673f5545381feb9978c4a39ad0405a19686b83 --- /dev/null +++ b/backend/src/TerritoryGame.Application/DependencyInjection.cs @@ -0,0 +1,28 @@ +// Application/DependencyInjection.cs +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; +using AutoMapper; +using TerritoryGame.Application.Common.Mappings; +using TerritoryGame.Application.Services; + +namespace TerritoryGame.Application; + +public static class DependencyInjection +{ + public static IServiceCollection AddApplicationServices(this IServiceCollection services) + { + // 修正后的AutoMapper配置 + services.AddAutoMapper(config => + { + config.AddProfile(); + }, Assembly.GetExecutingAssembly()); + + // 添加MediatR + services.AddMediatR(cfg => + cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); + + services.AddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Application/Services/GameManager.cs b/backend/src/TerritoryGame.Application/Services/GameManager.cs new file mode 100644 index 0000000000000000000000000000000000000000..1fce0f3bbdb1ecb0490b7a318b0a19d92700e63a --- /dev/null +++ b/backend/src/TerritoryGame.Application/Services/GameManager.cs @@ -0,0 +1,1058 @@ +using System.Collections.Concurrent; +using TerritoryGame.Domain.Models; +using TerritoryGame.Domain.Services; +using TerritoryGame.Application.Common; + +namespace TerritoryGame.Application.Services; + +internal class GameState +{ + public Game Game { get; set; } = null!; + public DateTime LastActivityUtc { get; set; } = DateTime.UtcNow; // 最近一次活动时间(创建/加入/离开/开始) + public Dictionary PlayersByConnection { get; set; } = new(); + public DateTime? StartedAt { get; set; } + public Guid?[,] Ownership { get; set; } = null!; // 每像素归属玩家Id + public Dictionary PixelCount { get; set; } = new(); + public Guid? HostPlayerId { get; set; } + public Dictionary Runtime { get; set; } = new(); + // 新增:墙体、道具、刷新与随机 + public List Walls { get; set; } = new(); + public List Items { get; set; } = new(); + public DateTime NextItemSpawnAt { get; set; } = DateTime.MinValue; + public Random Rng { get; set; } = new Random(); + // 新增:隐身区域 + public List Zones { get; set; } = new(); + // 新增:粗网格占领用于计分(递减收益按占地规模衰减) + public Guid?[,] OwnerGrid { get; set; } = null!; // 每格归属玩家Id(Guid),null为无人 +} + +public class PlayerRuntime +{ + public int X { get; set; } + public int Y { get; set; } + public double Angle { get; set; } + public int Energy { get; set; } = 100; // 0-100 + public DateTime LastEnergyTick { get; set; } = DateTime.UtcNow; + public DateTime LastSprayAt { get; set; } = DateTime.MinValue; // 最近一次喷涂时间 + public DateTime? DeadUntil { get; set; } = null; // 被击杀后复活时间 + public DateTime? InkUntil { get; set; } = null; // 墨水增益截止(保留,不再掉落) + public DateTime? StealthUntil { get; set; } = null; // 隐身道具截止 + public DateTime? NoclipUntil { get; set; } = null; // 穿墙道具截止 + // 新:加速道具(将三角形从穿墙替换为加速) + public DateTime? SpeedUntil { get; set; } = null; // 加速截止 + // 新:能量无限与无敌 + public DateTime? EnergyUnlimitedUntil { get; set; } = null; // 无限能量截止 + public DateTime? InvincibleUntil { get; set; } = null; // 无敌截止 + // 复活锁:复活后短时间内忽略客户端上报的位置,防止“原地复活”被客户端立刻覆盖 + public DateTime? RespawnLockUntil { get; set; } = null; + // 新:计分/统计(用于排行榜) + public int Score { get; set; } = 0; + public int Kills { get; set; } = 0; + public int PaintTiles { get; set; } = 0; + // 背包:道具库存 + public Dictionary Inventory { get; } = new(); + // 大爆炸Buff:在持续时间内所有炮弹爆炸半径翻倍(可叠加时长) + public DateTime? BigBangUntil { get; set; } = null; +} + +public record RectWall(int X, int Y, int W, int H); +public record Item(string Type, int X, int Y, int R); +public record StealthZone(int X, int Y, int R); + +public class GameManager : IGameManager +{ + private readonly ConcurrentDictionary _games = new(); + private readonly ConcurrentDictionary _connectionToGame = new(); + + // —— 计分参数(可调) —— + static class ScoreConfig + { + public const int TileSize = 10; // 粗网格大小(像素) + public const int KillBasePoints = 100; // 基础击杀分 + public const int TileBasePoints = 2; // 基础每格分 + public const double KillExpLambda = 0.25; // 击杀递减系数 + public const double KillMinFactor = 0.30; // 击杀最低系数 + public const double TileExpLambda = 0.15; // 占地递减系数 + public const double TileMinFactor = 0.40; // 占地最低系数 + public const int TileDecayUnit = 120; // 每占领多少格降低一个等级 + } + static double ExpDecay(int level, double lambda, double minFactor) => Math.Max(minFactor, Math.Exp(-lambda * level)); + + public Task CreateGameAsync(string name, int maxPlayers, int durationMinutes, string? password = null) + { + // 限制最大 10 人 + if (maxPlayers > 10) maxPlayers = 10; + var game = new Game + { + Id = Guid.NewGuid(), + Name = name, + MaxPlayers = maxPlayers, + DurationMinutes = durationMinutes, + Password = string.IsNullOrWhiteSpace(password) ? null : password, + // 地图扩大一倍(仅地图,不影响前端画布视口) + CanvasWidth = 2500, + CanvasHeight = 1000 + }; + var state = new GameState + { + Game = game, + Ownership = new Guid?[game.CanvasWidth, game.CanvasHeight], + LastActivityUtc = DateTime.UtcNow + }; + InitWalls(state); + InitStealthZones(state); + // 初始化粗网格(用于得分) + EnsureGrid(state); + // 首个道具刷新时间:10 秒 + state.NextItemSpawnAt = DateTime.UtcNow.AddSeconds(10); + _games[game.Id] = state; + return Task.FromResult(new GameDto + { + Id = game.Id, + Name = game.Name, + Code = game.Name, + MaxPlayers = game.MaxPlayers, + Status = game.Status, + CurrentPlayers = 0, + DurationMinutes = game.DurationMinutes, + CanvasWidth = game.CanvasWidth, + CanvasHeight = game.CanvasHeight, + HostPlayerName = null, + HasPassword = game.Password != null, + Password = game.Password, + Players = new() + }); + } + + public Task AddPlayerAsync(Guid gameId, string playerName, string connectionId, string? password = null) + { + var state = _games[gameId]; + if (state.Game.GamePlayers.Count >= state.Game.MaxPlayers) throw new InvalidOperationException("房间已满"); + if (state.Game.Password != null && state.Game.Password != password) + throw new InvalidOperationException("密码错误"); + // 颜色:从 10 个高区分度颜色中随机分配,避免相近 + string[] palette = new[] { + "#1f77b4", // blue + "#ff7f0e", // orange + "#2ca02c", // green + "#d62728", // red + "#9467bd", // purple + "#8c564b", // brown + "#e377c2", // pink + "#7f7f7f", // gray + "#bcbd22", // olive + "#17becf" // cyan + }; + var used = state.Game.GamePlayers.Select(gp => gp.Player.Color).ToHashSet(); + var available = palette.Where(c => !used.Contains(c)).ToList(); + string color = available.Count > 0 ? available[state.Rng.Next(available.Count)] : palette[state.Rng.Next(palette.Length)]; + var player = new Player + { + Id = Guid.NewGuid(), + Name = playerName, + ConnectionId = connectionId, + Color = color + }; + state.Game.GamePlayers.Add(new GamePlayer { GameId = gameId, PlayerId = player.Id, Player = player, Game = state.Game }); + state.PlayersByConnection[connectionId] = player; + _connectionToGame[connectionId] = gameId; + if (state.HostPlayerId == null) state.HostPlayerId = player.Id; // 第一位加入者为房主 + state.LastActivityUtc = DateTime.UtcNow; + // 初始化运行时:随机位置(避免边界) + var pos = RandomFreePosition(state, 18); + state.Runtime[player.Id] = new PlayerRuntime + { + X = pos.x, + Y = pos.y, + Angle = state.Rng.NextDouble() * Math.PI * 2, + Energy = 100, + LastEnergyTick = DateTime.UtcNow + }; + return Task.FromResult(new PlayerDto { Id = player.Id, Name = player.Name, Color = player.Color, Score = 0 }); + } + + public Task RemovePlayerAsync(Guid gameId, string connectionId) + { + if (!_games.TryGetValue(gameId, out var state)) return Task.FromResult(new RemovePlayerResult(false, false, null, false)); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return Task.FromResult(new RemovePlayerResult(false, false, null, false)); + var wasHost = state.HostPlayerId == player.Id; + // 从房间玩家列表移除 + state.Game.GamePlayers = state.Game.GamePlayers.Where(gp => gp.PlayerId != player.Id).ToList(); + // 运行时状态清理,避免“假人”残留 + if (state.Runtime.ContainsKey(player.Id)) state.Runtime.Remove(player.Id); + // 反向像素计数无需强制清理;区域归属在后续覆盖中自然变化 + // 连接映射移除 + state.PlayersByConnection.Remove(connectionId); + _connectionToGame.TryRemove(connectionId, out _); + // 固定调色板,不需要释放随机颜色池 + if (wasHost) + { + // 需求变更:房主离开时直接解散房间(踢出所有人) + _games.TryRemove(gameId, out _); + return Task.FromResult(new RemovePlayerResult(true, true, player.Id, wasHost)); + } + var remaining = state.Game.GamePlayers.Count; + if (remaining == 0) + { + // 无人剩余也删除房间 + _games.TryRemove(gameId, out _); + return Task.FromResult(new RemovePlayerResult(true, true, player.Id, wasHost)); + } + state.LastActivityUtc = DateTime.UtcNow; + return Task.FromResult(new RemovePlayerResult(true, false, player.Id, wasHost)); + } + + public (bool success, Guid? removedPlayerId, string? removedConnectionId, bool gameDeleted, bool wasHostCaller, string? reason) KickPlayer(Guid gameId, string hostConnectionId, Guid targetPlayerId) + { + if (!_games.TryGetValue(gameId, out var state)) return (false, null, null, false, false, "房间不存在"); + if (!state.PlayersByConnection.TryGetValue(hostConnectionId, out var hostPlayer)) return (false, null, null, false, false, "无效调用"); + var isHost = state.HostPlayerId == hostPlayer.Id; + if (!isHost) return (false, null, null, false, false, "只有房主可以踢人"); + // 找到目标玩家及其连接 + string? targetConn = state.PlayersByConnection.FirstOrDefault(kv => kv.Value.Id == targetPlayerId).Key; + if (targetConn == null) return (false, null, null, false, true, "目标不在房间"); + // 踢人:重用 RemovePlayerAsync 的逻辑,但这里避免递归 await,直接内联必要清理 + var wasHost = state.HostPlayerId == targetPlayerId; + // 从列表与运行时移除 + state.Game.GamePlayers = state.Game.GamePlayers.Where(gp => gp.PlayerId != targetPlayerId).ToList(); + if (state.Runtime.ContainsKey(targetPlayerId)) state.Runtime.Remove(targetPlayerId); + state.PlayersByConnection.Remove(targetConn); + _connectionToGame.TryRemove(targetConn, out _); + if (wasHost) + { + // 若目标恰好是房主,解散房间 + _games.TryRemove(gameId, out _); + return (true, targetPlayerId, targetConn, true, true, null); + } + var remaining = state.Game.GamePlayers.Count; + if (remaining == 0) + { + _games.TryRemove(gameId, out _); + return (true, targetPlayerId, targetConn, true, true, null); + } + state.LastActivityUtc = DateTime.UtcNow; + return (true, targetPlayerId, targetConn, false, true, null); + } + + public Task StartGameAsync(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var state)) return Task.FromResult(false); + if (state.StartedAt != null) return Task.FromResult(false); + // 至少 2 名玩家才能开始 + if (state.Game.GamePlayers.Count < 2) return Task.FromResult(false); + state.StartedAt = DateTime.UtcNow; + state.Game.Status = GameStatus.InProgress; + state.LastActivityUtc = DateTime.UtcNow; + return Task.FromResult(true); + } + + public Task ProcessDrawAsync(Guid gameId, string connectionId, int x, int y, int size, string color) + { + // 兼容旧的点涂抹(不覆盖) + if (!_games.TryGetValue(gameId, out var state)) return Task.FromResult(false); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return Task.FromResult(false); + if (state.Game.Status != GameStatus.InProgress) return Task.FromResult(false); + int r = size / 2; + for (int dx = -r; dx <= r; dx++) + { + for (int dy = -r; dy <= r; dy++) + { + int px = x + dx; + int py = y + dy; + if (px < 0 || py < 0 || px >= state.Game.CanvasWidth || py >= state.Game.CanvasHeight) continue; + if (dx * dx + dy * dy > r * r) continue; + if (state.Ownership[px, py] != null) continue; // 不覆盖 + state.Ownership[px, py] = player.Id; + if (!state.PixelCount.ContainsKey(player.Id)) state.PixelCount[player.Id] = 0; + state.PixelCount[player.Id]++; + } + } + return Task.FromResult(true); + } + + public IEnumerable GetPlayerStates(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var state)) return Enumerable.Empty(); + var now = DateTime.UtcNow; + return state.Runtime.Select(kv => { + var pid = kv.Key; + var gp = state.Game.GamePlayers.FirstOrDefault(p => p.PlayerId == pid); + var color = gp?.Player?.Color; + return new + { + playerId = kv.Key, + x = kv.Value.X, + y = kv.Value.Y, + angle = kv.Value.Angle, + color, + deadUntil = kv.Value.DeadUntil, + stealthUntil = kv.Value.StealthUntil, + speedUntil = kv.Value.SpeedUntil, + noclipUntil = kv.Value.NoclipUntil, + bigBangUntil = kv.Value.BigBangUntil, + invincibleUntil = kv.Value.InvincibleUntil, + energyUnlimitedUntil = kv.Value.EnergyUnlimitedUntil, + stealth = (kv.Value.StealthUntil != null && kv.Value.StealthUntil > now) || InStealthZone(state, kv.Value.X, kv.Value.Y) + }; + }); + } + + // 获取调用者自身的运行时状态(用于激活道具后立刻回发一次,带剩余效果时间) + public object? GetSelfState(Guid gameId, string connectionId) + { + if (!_games.TryGetValue(gameId, out var state)) return null; + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return null; + if (!state.Runtime.TryGetValue(player.Id, out var rt)) return null; + var now = DateTime.UtcNow; + bool stealthFlag = (rt.StealthUntil != null && rt.StealthUntil > now) || InStealthZone(state, rt.X, rt.Y); + return new { playerId = player.Id, x = rt.X, y = rt.Y, angle = rt.Angle, color = player.Color, deadUntil = rt.DeadUntil, stealth = stealthFlag, stealthUntil = rt.StealthUntil, speedUntil = rt.SpeedUntil, noclipUntil = rt.NoclipUntil, bigBangUntil = rt.BigBangUntil, invincibleUntil = rt.InvincibleUntil, energyUnlimitedUntil = rt.EnergyUnlimitedUntil }; + } + + private void RegenEnergy(PlayerRuntime rt) + { + // 喷涂后 250ms 内不回复 + var now = DateTime.UtcNow; + if ((now - rt.LastSprayAt).TotalMilliseconds < 250) return; + // 每 250ms 恢复 2 点 + var delta = (now - rt.LastEnergyTick).TotalMilliseconds; + if (delta < 250) return; + var ticks = (int)(delta / 250.0); + if (ticks <= 0) return; + rt.Energy = Math.Min(100, rt.Energy + ticks * 2); + rt.LastEnergyTick = now; + } + + public (bool ok, object? moved, int? energy, bool itemsChanged) Move(Guid gameId, string connectionId, int forward, int rotate) + { + if (!_games.TryGetValue(gameId, out var state)) return (false, null, null, false); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return (false, null, null, false); + if (!state.Runtime.TryGetValue(player.Id, out var rt)) return (false, null, null, false); + if (state.Game.Status != GameStatus.InProgress) return (false, null, null, false); + // 不再在玩家移动时刷新道具,由计时器驱动刷新广播 + var now = DateTime.UtcNow; + // 死亡冷却中:只回复能量并广播死亡状态 + if (rt.DeadUntil != null && rt.DeadUntil > now) + { + RegenEnergy(rt); + return (true, new { playerId = player.Id, x = rt.X, y = rt.Y, angle = rt.Angle, color = player.Color, deadUntil = rt.DeadUntil, invincibleUntil = rt.InvincibleUntil, energyUnlimitedUntil = rt.EnergyUnlimitedUntil }, rt.Energy, false); + } + // 到点复活 + bool respawned = false; + if (rt.DeadUntil != null && rt.DeadUntil <= now) + { + var pos = RandomFreePosition(state, 18); + rt.X = pos.x; rt.Y = pos.y; rt.DeadUntil = null; + rt.RespawnLockUntil = now.AddMilliseconds(1500); // 延长复活锁,避免客户端旧坐标回拉 + respawned = true; + } + RegenEnergy(rt); + const double rotStep = Math.PI / 60.0; // 3° + double speed = 2.0; + if (rt.SpeedUntil != null && rt.SpeedUntil > now) speed *= 2.0; // 加速x2 + if (rotate != 0) rt.Angle += rotate * rotStep; + // 归一化角度 + if (rt.Angle > Math.PI) rt.Angle -= 2 * Math.PI; else if (rt.Angle < -Math.PI) rt.Angle += 2 * Math.PI; + if (forward != 0) + { + var nx = rt.X + (int)Math.Round(Math.Cos(rt.Angle) * speed * forward); + var ny = rt.Y + (int)Math.Round(Math.Sin(rt.Angle) * speed * forward); + nx = Math.Clamp(nx, 0, state.Game.CanvasWidth - 1); + ny = Math.Clamp(ny, 0, state.Game.CanvasHeight - 1); + // 碰撞检测:不可进入墙体(除非处于穿墙状态) + var now2 = DateTime.UtcNow; + bool canClip = rt.NoclipUntil != null && rt.NoclipUntil > now2; + if (canClip || !InsideAnyWall(state, nx, ny)) { rt.X = nx; rt.Y = ny; } + } + // 拾取道具(移除地图上的道具,需要广播) + bool itemsChanged = TryPickup(state, player.Id); + bool stealthFlag = (rt.StealthUntil != null && rt.StealthUntil > DateTime.UtcNow) || InStealthZone(state, rt.X, rt.Y); + return (true, new { playerId = player.Id, x = rt.X, y = rt.Y, angle = rt.Angle, color = player.Color, deadUntil = rt.DeadUntil, stealth = stealthFlag, stealthUntil = rt.StealthUntil, speedUntil = rt.SpeedUntil, noclipUntil = rt.NoclipUntil, bigBangUntil = rt.BigBangUntil, invincibleUntil = rt.InvincibleUntil, energyUnlimitedUntil = rt.EnergyUnlimitedUntil, respawned }, rt.Energy, itemsChanged); + } + + // 前端权威:直接设置坐标与角度(做边界/墙体/死亡校验) + public (bool ok, object? moved, int? energy, bool itemsChanged) ClientSetTransform(Guid gameId, string connectionId, int x, int y, double angle) + { + if (!_games.TryGetValue(gameId, out var state)) return (false, null, null, false); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return (false, null, null, false); + if (!state.Runtime.TryGetValue(player.Id, out var rt)) return (false, null, null, false); + if (state.Game.Status != GameStatus.InProgress) return (false, null, null, false); + // 不再在客户端上报位置时刷新道具,由计时器驱动刷新广播 + var now = DateTime.UtcNow; + // 死亡中:拒绝移动,仅返回当前状态 + if (rt.DeadUntil != null && rt.DeadUntil > now) + { + RegenEnergy(rt); + return (true, new { playerId = player.Id, x = rt.X, y = rt.Y, angle = rt.Angle, color = player.Color, deadUntil = rt.DeadUntil, invincibleUntil = rt.InvincibleUntil, energyUnlimitedUntil = rt.EnergyUnlimitedUntil }, rt.Energy, false); + } + // 到点复活 + bool respawned = false; + if (rt.DeadUntil != null && rt.DeadUntil <= now) + { + var pos = RandomFreePosition(state, 18); + rt.X = pos.x; rt.Y = pos.y; rt.DeadUntil = null; + rt.RespawnLockUntil = now.AddMilliseconds(1500); // 延长复活锁 + respawned = true; + } + RegenEnergy(rt); + // 归一化角度 + if (angle > Math.PI) angle -= 2 * Math.PI; else if (angle < -Math.PI) angle += 2 * Math.PI; + // 复活锁:在短时间内忽略客户端坐标,防止原地覆盖复活点 + bool inRespawnLock = rt.RespawnLockUntil != null && rt.RespawnLockUntil > now; + if (!inRespawnLock) + { + // 边界限制 + x = Math.Clamp(x, 0, state.Game.CanvasWidth - 1); + y = Math.Clamp(y, 0, state.Game.CanvasHeight - 1); + // 墙体阻挡(除非穿墙状态) + bool canClip = rt.NoclipUntil != null && rt.NoclipUntil > now; + if (!(canClip || !InsideAnyWall(state, x, y))) + { + // 若被墙阻挡,保持原位但同步角度 + rt.Angle = angle; + return (true, new { playerId = player.Id, x = rt.X, y = rt.Y, angle = rt.Angle, color = player.Color, deadUntil = rt.DeadUntil }, rt.Energy, false); + } + // 接受客户端位置 + rt.X = x; rt.Y = y; rt.Angle = angle; + } + else + { + // 仅同步角度 + rt.Angle = angle; + } + // 拾取道具(移除地图上的道具,需要广播) + bool itemsChanged = TryPickup(state, player.Id); + bool stealthFlag = (rt.StealthUntil != null && rt.StealthUntil > DateTime.UtcNow) || InStealthZone(state, rt.X, rt.Y); + return (true, new { playerId = player.Id, x = rt.X, y = rt.Y, angle = rt.Angle, color = player.Color, deadUntil = rt.DeadUntil, stealth = stealthFlag, stealthUntil = rt.StealthUntil, speedUntil = rt.SpeedUntil, noclipUntil = rt.NoclipUntil, bigBangUntil = rt.BigBangUntil, invincibleUntil = rt.InvincibleUntil, energyUnlimitedUntil = rt.EnergyUnlimitedUntil, respawned }, rt.Energy, itemsChanged); + } + + public (bool ok, int energy, IEnumerable? sector, IEnumerable? stats, string? color) Spray(Guid gameId, string connectionId, int radius, int angleWidthDeg, bool allowOverwrite) + { + if (!_games.TryGetValue(gameId, out var state)) return (false, 0, null, null, null); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return (false, 0, null, null, null); + if (!state.Runtime.TryGetValue(player.Id, out var rt)) return (false, 0, null, null, player.Color); + if (state.Game.Status != GameStatus.InProgress) return (false, rt.Energy, null, null, player.Color); + RegenEnergy(rt); + // 消耗:喷涂一次基础 12 能量;若处于无限能量则跳过消耗 + const int cost = 12; + bool energyFree = rt.EnergyUnlimitedUntil != null && rt.EnergyUnlimitedUntil > DateTime.UtcNow; + if (!energyFree) + { + if (rt.Energy < cost) return (false, rt.Energy, null, null, player.Color); + rt.Energy -= cost; + } + rt.LastSprayAt = DateTime.UtcNow; + var affected = new List(); + int r = radius; + double half = angleWidthDeg * Math.PI / 360.0; + int minX = Math.Max(0, rt.X - r); + int maxX = Math.Min(state.Game.CanvasWidth - 1, rt.X + r); + int minY = Math.Max(0, rt.Y - r); + int maxY = Math.Min(state.Game.CanvasHeight - 1, rt.Y + r); + for (int x = minX; x <= maxX; x++) + { + for (int y = minY; y <= maxY; y++) + { + int dx = x - rt.X; + int dy = y - rt.Y; + if (dx * dx + dy * dy > r * r) continue; + var ang = Math.Atan2(dy, dx); + var diff = ang - rt.Angle; + while (diff > Math.PI) diff -= 2 * Math.PI; + while (diff < -Math.PI) diff += 2 * Math.PI; + if (Math.Abs(diff) > half) continue; + var prev = state.Ownership[x, y]; + if (prev == player.Id) continue; // 已是本人 + if (prev != null && !allowOverwrite) continue; + // 覆盖逻辑:减少前所属计数 + if (prev != null && allowOverwrite) + { + if (state.PixelCount.ContainsKey(prev.Value)) + { + state.PixelCount[prev.Value] = Math.Max(0, state.PixelCount[prev.Value] - 1); + } + } + state.Ownership[x, y] = player.Id; + if (!state.PixelCount.ContainsKey(player.Id)) state.PixelCount[player.Id] = 0; + state.PixelCount[player.Id]++; + affected.Add(new { x, y }); + } + } + var stats = GetAreaStats(gameId).ToList(); + return (true, rt.Energy, affected, stats, player.Color); + } + + // —— 炮弹:飞行-爆炸-涂抹-击杀 —— + public (bool ok, int energy, IEnumerable? affected, object? explosion, IEnumerable? killed, string? color, IEnumerable? killFeed) FireCannon(Guid gameId, string connectionId) + { + if (!_games.TryGetValue(gameId, out var state)) return (false, 0, null, null, null, null, null); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return (false, 0, null, null, null, null, null); + if (!state.Runtime.TryGetValue(player.Id, out var rt)) return (false, 0, null, null, null, player.Color, null); + if (state.Game.Status != GameStatus.InProgress) return (false, rt.Energy, null, null, null, player.Color, null); + SpawnItemIfDue(state); + var now = DateTime.UtcNow; + if (rt.DeadUntil != null && rt.DeadUntil > now) return (false, rt.Energy, null, null, null, player.Color, null); + RegenEnergy(rt); + bool energyFree = rt.EnergyUnlimitedUntil != null && rt.EnergyUnlimitedUntil > now; + const int cost = 12; + if (!energyFree) + { + if (rt.Energy < cost) return (false, rt.Energy, null, null, null, player.Color, null); + rt.Energy -= cost; + } + rt.LastSprayAt = now; + // 参数 + int maxRange = (int)Math.Round(260 * 0.75); // 降低至原来的 75% + int outer = 44; // 爆炸范围 + if (rt.InkUntil != null && rt.InkUntil > now) { outer = (int)(outer * 1.4); } + // 大爆炸Buff:在持续时间内翻倍(不再使用“一次性充能”) + if (rt.BigBangUntil != null && rt.BigBangUntil > now) { outer *= 2; } + var ox = rt.X; var oy = rt.Y; + var farX = ox + (int)Math.Round(Math.Cos(rt.Angle) * maxRange); + var farY = oy + (int)Math.Round(Math.Sin(rt.Angle) * maxRange); + var now2 = DateTime.UtcNow; + var wallHit = (rt.NoclipUntil != null && rt.NoclipUntil > now2) ? null : FirstWallHit(state, ox, oy, farX, farY); + var playerHit = FirstPlayerHit(state, player.Id, ox, oy, farX, farY, skipInvincible: true); + int ex, ey; double tChosen = double.PositiveInfinity; + if (playerHit != null) { ex = playerHit.Value.x; ey = playerHit.Value.y; tChosen = playerHit.Value.t; } + else { ex = farX; ey = farY; tChosen = 1.0; } + if (wallHit != null && wallHit.Value.t < tChosen) { ex = wallHit.Value.x; ey = wallHit.Value.y; tChosen = wallHit.Value.t; } + ex = Math.Clamp(ex, 0, state.Game.CanvasWidth - 1); + ey = Math.Clamp(ey, 0, state.Game.CanvasHeight - 1); + // 实心爆炸并写入 Ownership + var affected = new List(); + FillCircleExplosion(state, ex, ey, outer, player.Id, affected); + // 击杀判定(圆形) + var killed = new List(); + var killFeed = new List(); + int r2 = (int)(outer * outer * 0.9); + foreach (var kv in state.Runtime) + { + var pid = kv.Key; if (pid == player.Id) continue; var pr = kv.Value; if (pr.DeadUntil != null && pr.DeadUntil > now) continue; + // 无敌者不被击杀 + if (pr.InvincibleUntil != null && pr.InvincibleUntil > now) continue; + int dx = pr.X - ex, dy = pr.Y - ey; if (dx * dx + dy * dy <= r2) { pr.DeadUntil = now.AddSeconds(5); killed.Add(pid); } + } + // 击杀奖励:每个被击杀玩家有25%概率给予射手一个随机道具 + if (killed.Count > 0) + { + string[] __types = new[] { "energy", "stealth", "speed", "noclip", "bigbang" }; + foreach (var vid in killed) + { + string? tAwarded = null; + if (state.Rng.NextDouble() < 0.25) + { + tAwarded = __types[state.Rng.Next(__types.Length)]; + if (!rt.Inventory.ContainsKey(tAwarded)) rt.Inventory[tAwarded] = 0; + rt.Inventory[tAwarded]++; + } + var victim = state.Game.GamePlayers.FirstOrDefault(gp => gp.PlayerId == vid)?.Player; + killFeed.Add(new + { + shooterId = player.Id, + shooterName = player.Name, + shooterColor = player.Color, + victimId = vid, + victimName = victim?.Name ?? "", + victimColor = victim?.Color ?? "", + item = tAwarded + }); + } + } + var explosion = new { x = ex, y = ey, outer, ox, oy }; + // —— 计分:递减收益 —— + // 击杀:按已累计击杀数做指数递减 + int killScore = 0; + for (int i = 0; i < killed.Count; i++) + { + int level = rt.Kills + i; // 第 level+1 次击杀 + double factor = ExpDecay(level, ScoreConfig.KillExpLambda, ScoreConfig.KillMinFactor); + killScore += (int)Math.Round(ScoreConfig.KillBasePoints * factor); + } + // 占地:按已占格数分级,新增的每一格按当前等级计分 + var (gained, tileScore) = ApplyExplosionOwnershipAndScore(state, player.Id, ex, ey, outer, rt.PaintTiles); + rt.Kills += killed.Count; + rt.PaintTiles += gained; + rt.Score += killScore + tileScore; + return (true, rt.Energy, affected, explosion, killed, player.Color, killFeed); + } + + // —— 地图/几何与道具 —— + private void InitWalls(GameState s) + { + // 随机生成 8-12 个矩形墙体(轴对齐),保留一定边距,尺寸在合理范围 + var rng = s.Rng; + s.Walls = new List(); + int target = rng.Next(8, 13); + int tries = 0; + int margin = 40; + while (s.Walls.Count < target && tries++ < target * 20) + { + bool horizontal = rng.Next(2) == 0; + int w = horizontal ? rng.Next(160, 361) : rng.Next(18, 37); + int h = horizontal ? rng.Next(18, 37) : rng.Next(160, 361); + int maxX = Math.Max(1, s.Game.CanvasWidth - w - margin); + int maxY = Math.Max(1, s.Game.CanvasHeight - h - margin); + int x = rng.Next(margin, maxX); + int y = rng.Next(margin, maxY); + // 简单相交抑制:避免与现有墙体过大重叠 + var rect = new RectWall(x, y, w, h); + bool overlaps = s.Walls.Any(ex => + { + int ax1 = rect.X, ay1 = rect.Y, ax2 = rect.X + rect.W, ay2 = rect.Y + rect.H; + int bx1 = ex.X, by1 = ex.Y, bx2 = ex.X + ex.W, by2 = ex.Y + ex.H; + int ix = Math.Max(0, Math.Min(ax2, bx2) - Math.Max(ax1, bx1)); + int iy = Math.Max(0, Math.Min(ay2, by2) - Math.Max(ay1, by1)); + int inter = ix * iy; + return inter > (rect.W * rect.H) * 0.6; // 重叠超过 60% 则丢弃 + }); + if (!overlaps) s.Walls.Add(rect); + } + } + private void InitStealthZones(GameState s) + { + var rng = s.Rng; + s.Zones = new List(); + int count = rng.Next(2, 4); // 2~3 个 + int r = 88; + int tries = 0; + int margin = r + 20; + while (s.Zones.Count < count && tries++ < count * 50) + { + int x = rng.Next(margin, s.Game.CanvasWidth - margin); + int y = rng.Next(margin, s.Game.CanvasHeight - margin); + // 中心不得位于墙体内,且与已放置区域保持一定间距 + if (InsideAnyWall(s, x, y)) continue; + bool closeToOthers = s.Zones.Any(z => + { + int dx = x - z.X, dy = y - z.Y; return (dx * dx + dy * dy) < (int)(r * r * 2.25); // 距离 < 1.5r + }); + if (closeToOthers) continue; + s.Zones.Add(new StealthZone(x, y, r)); + } + // 若随机失败,至少保证一个区域 + if (s.Zones.Count == 0) s.Zones.Add(new StealthZone(s.Game.CanvasWidth/2, s.Game.CanvasHeight/2, r)); + } + private bool InsideAnyWall(GameState s, int x, int y) + => s.Walls.Any(w => x >= w.X && x <= w.X + w.W && y >= w.Y && y <= w.Y + w.H); + private bool InStealthZone(GameState s, int x, int y) + => s.Zones.Any(z => { var dx = x - z.X; var dy = y - z.Y; return dx * dx + dy * dy <= z.R * z.R; }); + private (int x, int y) RandomFreePosition(GameState s, int radius) + { + for (int i = 0; i < 200; i++) + { + int x = 20 + s.Rng.Next(0, s.Game.CanvasWidth - 40); + int y = 20 + s.Rng.Next(0, s.Game.CanvasHeight - 40); + if (!InsideAnyWall(s, x, y)) return (x, y); + } + return (s.Game.CanvasWidth/2, s.Game.CanvasHeight/2); + } + private static double? SegIntersect((double x,double y) p, (double x,double y) q, (double x,double y) a, (double x,double y) b) + { + var rx = q.x - p.x; var ry = q.y - p.y; var sx = b.x - a.x; var sy = b.y - a.y; var rxs = rx * sy - ry * sx; if (Math.Abs(rxs) < 1e-8) return null; + var qpx = a.x - p.x; var qpy = a.y - p.y; var t = (qpx * sy - qpy * sx) / rxs; var u = (qpx * ry - qpy * rx) / rxs; if (t >= 0 && t <= 1 && u >= 0 && u <= 1) return t; return null; + } + private (double t, int x,int y)? FirstWallHit(GameState s, int x0, int y0, int x1, int y1) + { + double bestT = double.PositiveInfinity; (double t, int x,int y)? hit = null; var p = ((double)x0, (double)y0); var q = ((double)x1, (double)y1); + foreach (var w in s.Walls) + { + var verts = new []{ (w.X, w.Y), (w.X + w.W, w.Y), (w.X + w.W, w.Y + w.H), (w.X, w.Y + w.H) }; + for (int i = 0; i < 4; i++) + { + var a = ((double)verts[i].Item1, (double)verts[i].Item2); + var b = ((double)verts[(i + 1) % 4].Item1, (double)verts[(i + 1) % 4].Item2); + var t = SegIntersect(p, q, a, b); + if (t != null && t.Value < bestT) + { + bestT = t.Value; var hx = (int)Math.Round(p.Item1 + (q.Item1 - p.Item1) * t.Value); var hy = (int)Math.Round(p.Item2 + (q.Item2 - p.Item2) * t.Value); + hit = (t.Value, hx, hy); + } + } + } + return hit; + } + private (double t, int x, int y, Guid pid)? FirstPlayerHit(GameState s, Guid shooterId, int x0, int y0, int x1, int y1, bool skipInvincible = false) + { + // 直线段与玩家圆形碰撞,返回最早命中的玩家 + double bestT = double.PositiveInfinity; (double t, int x, int y, Guid pid)? hit = null; + double dx = x1 - x0, dy = y1 - y0; double len2 = dx*dx + dy*dy; if (len2 <= 1e-6) return null; + foreach (var kv in s.Runtime) + { + var pid = kv.Key; if (pid == shooterId) continue; var pr = kv.Value; if (pr.DeadUntil != null && pr.DeadUntil > DateTime.UtcNow) continue; + if (skipInvincible && pr.InvincibleUntil != null && pr.InvincibleUntil > DateTime.UtcNow) continue; + double cx = pr.X - x0, cy = pr.Y - y0; double t = (cx*dx + cy*dy) / len2; if (t < 0 || t > 1) continue; + double px = x0 + dx * t, py = y0 + dy * t; double ddx = pr.X - px, ddy = pr.Y - py; double dist2 = ddx*ddx + ddy*ddy; + const int R = 16; if (dist2 <= R * R && t < bestT) + { + bestT = t; hit = (t, (int)Math.Round(px), (int)Math.Round(py), pid); + } + } + return hit; + } + private static double Lerp(double a, double b, double t) => a + (b - a) * t; + private void FillCircleExplosion(GameState s, int cx, int cy, int radius, Guid painter, List outPoints) + { + int r2 = radius * radius; + int minX = Math.Max(0, cx - radius); + int maxX = Math.Min(s.Game.CanvasWidth - 1, cx + radius); + int minY = Math.Max(0, cy - radius); + int maxY = Math.Min(s.Game.CanvasHeight - 1, cy + radius); + for (int x = minX; x <= maxX; x++) + { + int dx = x - cx; int dx2 = dx * dx; + for (int y = minY; y <= maxY; y++) + { + int dy = y - cy; if (dx2 + dy * dy > r2) continue; + var prev = s.Ownership[x, y]; + if (prev == painter) continue; + if (prev != null) + { + if (s.PixelCount.ContainsKey(prev.Value)) s.PixelCount[prev.Value] = Math.Max(0, s.PixelCount[prev.Value] - 1); + } + s.Ownership[x, y] = painter; + if (!s.PixelCount.ContainsKey(painter)) s.PixelCount[painter] = 0; + s.PixelCount[painter]++; + outPoints.Add(new { x, y }); + } + } + } + private void SpawnItemIfDue(GameState s) + { + if (DateTime.UtcNow < s.NextItemSpawnAt) return; + // 每 10 秒刷新一批道具:数量 = 玩家总人数 / 2(向下取整) + s.NextItemSpawnAt = DateTime.UtcNow.AddSeconds(10); + int totalPlayers = s.Game.GamePlayers.Count; + int toSpawn = totalPlayers / 2; // 不管余数 + if (toSpawn <= 0) return; + string[] types = new[] { "energy", "stealth", "speed", "noclip", "bigbang" }; + for (int i = 0; i < toSpawn; i++) + { + var pos = RandomFreePosition(s, 12); + var type = types[s.Rng.Next(types.Length)]; + s.Items.Add(new Item(type, pos.x, pos.y, 12)); + } + } + // 供计时器调用:检查并刷新一批道具,返回是否有新增 + public bool CheckAndSpawnItems(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var s)) return false; + int before = s.Items.Count; + SpawnItemIfDue(s); + return s.Items.Count > before; + } + + // —— 计分:粗网格与得分计算 —— + private void EnsureGrid(GameState s) + { + int tilesW = Math.Max(1, s.Game.CanvasWidth / ScoreConfig.TileSize); + int tilesH = Math.Max(1, s.Game.CanvasHeight / ScoreConfig.TileSize); + if (s.OwnerGrid == null || s.OwnerGrid.GetLength(0) != tilesW || s.OwnerGrid.GetLength(1) != tilesH) + { + s.OwnerGrid = new Guid?[tilesW, tilesH]; + for (int x = 0; x < tilesW; x++) + for (int y = 0; y < tilesH; y++) + s.OwnerGrid[x, y] = null; + } + } + + private (int gained, int tileScore) ApplyExplosionOwnershipAndScore(GameState s, Guid shooterId, int cx, int cy, int r, int paintTilesBefore) + { + EnsureGrid(s); + int ts = ScoreConfig.TileSize; + int tilesW = s.OwnerGrid.GetLength(0); + int tilesH = s.OwnerGrid.GetLength(1); + int minTX = Math.Max(0, (cx - r) / ts), maxTX = Math.Min(tilesW - 1, (cx + r) / ts); + int minTY = Math.Max(0, (cy - r) / ts), maxTY = Math.Min(tilesH - 1, (cy + r) / ts); + int r2 = r * r, gained = 0, tileScore = 0; + for (int tx = minTX; tx <= maxTX; tx++) + { + int gx = tx * ts + ts / 2; + int dx = gx - cx; int dx2 = dx * dx; + for (int ty = minTY; ty <= maxTY; ty++) + { + int gy = ty * ts + ts / 2; + int dy = gy - cy; + if (dx2 + dy * dy > r2) continue; + var prevOwner = s.OwnerGrid[tx, ty]; + if (prevOwner == shooterId) continue; // 自己已占有,无变更 + + // 若有前任拥有者且不是本次涂色者:为其扣分与减少 PaintTiles + if (prevOwner != null && s.Runtime.TryGetValue(prevOwner.Value, out var prevRt)) + { + if (prevRt.PaintTiles > 0) + { + int tilesBeforeLoss = Math.Max(0, prevRt.PaintTiles - 1); + int lossLevel = tilesBeforeLoss / ScoreConfig.TileDecayUnit; + double lossFactor = ExpDecay(lossLevel, ScoreConfig.TileExpLambda, ScoreConfig.TileMinFactor); + int loss = (int)Math.Round(ScoreConfig.TileBasePoints * lossFactor); + prevRt.Score = Math.Max(0, prevRt.Score - loss); // 不允许降为负数(可按需改为允许负分) + prevRt.PaintTiles = Math.Max(0, prevRt.PaintTiles - 1); + } + } + + // 归属变更给当前玩家并为其加分 + s.OwnerGrid[tx, ty] = shooterId; + int totalBeforeThis = paintTilesBefore + gained; + int level = totalBeforeThis / ScoreConfig.TileDecayUnit; + double factor = ExpDecay(level, ScoreConfig.TileExpLambda, ScoreConfig.TileMinFactor); + tileScore += (int)Math.Round(ScoreConfig.TileBasePoints * factor); + gained++; + } + } + return (gained, tileScore); + } + private bool TryPickup(GameState s, Guid playerId) + { + if (!s.Runtime.TryGetValue(playerId, out var rt)) return false; + bool changed = false; + for (int i = s.Items.Count - 1; i >= 0; i--) + { + var it = s.Items[i]; + int dx = rt.X - it.X, dy = rt.Y - it.Y; if (dx * dx + dy * dy <= (it.R + 10) * (it.R + 10)) + { + // 改为捡入背包,不立即生效 + var t = it.Type?.ToLowerInvariant() ?? ""; + if (!rt.Inventory.ContainsKey(t)) rt.Inventory[t] = 0; + rt.Inventory[t]++; + s.Items.RemoveAt(i); changed = true; + } + } + return changed; + } + + public IDictionary GetInventory(Guid gameId, string connectionId) + { + if (!_games.TryGetValue(gameId, out var state)) return new Dictionary(); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return new Dictionary(); + if (!state.Runtime.TryGetValue(player.Id, out var rt)) return new Dictionary(); + return new Dictionary(rt.Inventory); + } + + public (bool ok, int energy, IDictionary inventory) ActivateItem(Guid gameId, string connectionId, string type) + { + if (!_games.TryGetValue(gameId, out var state)) return (false, 0, new Dictionary()); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return (false, 0, new Dictionary()); + if (!state.Runtime.TryGetValue(player.Id, out var rt)) return (false, 0, new Dictionary()); + var key = (type ?? string.Empty).ToLowerInvariant(); + if (!rt.Inventory.TryGetValue(key, out var count) || count <= 0) return (false, rt.Energy, new Dictionary(rt.Inventory)); + rt.Inventory[key] = count - 1; if (rt.Inventory[key] <= 0) rt.Inventory.Remove(key); + var now = DateTime.UtcNow; + switch (key) + { + case "energy": + // 绿色十字:5秒无限能量,同时立刻回满 + rt.Energy = 100; + rt.EnergyUnlimitedUntil = (rt.EnergyUnlimitedUntil != null && rt.EnergyUnlimitedUntil > now) + ? rt.EnergyUnlimitedUntil.Value.AddSeconds(5) + : now.AddSeconds(5); + break; + case "stealth": + // 红色五角星:5秒无敌(可叠加延长);不再授予隐身 + rt.InvincibleUntil = (rt.InvincibleUntil != null && rt.InvincibleUntil > now) + ? rt.InvincibleUntil.Value.AddSeconds(5) + : now.AddSeconds(5); + break; + case "speed": + // 橙色三角形:双倍移速,持续7秒(可叠加延长) + rt.SpeedUntil = (rt.SpeedUntil != null && rt.SpeedUntil > now) + ? rt.SpeedUntil.Value.AddSeconds(7) + : now.AddSeconds(7); + break; + case "noclip": + // 蓝色方块:7秒穿墙(可叠加延长) + rt.NoclipUntil = (rt.NoclipUntil != null && rt.NoclipUntil > now) + ? rt.NoclipUntil.Value.AddSeconds(7) + : now.AddSeconds(7); + break; // 期间可穿墙,炮弹也穿墙 + case "bigbang": + // 紫色六边形:持续Buff,时长 5 秒,可叠加延长 + rt.BigBangUntil = (rt.BigBangUntil != null && rt.BigBangUntil > now) + ? rt.BigBangUntil.Value.AddSeconds(5) + : now.AddSeconds(5); + break; + } + return (true, rt.Energy, new Dictionary(rt.Inventory)); + } + + public IEnumerable GetItems(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var s)) return Enumerable.Empty(); + return s.Items.Select(it => new { type = it.Type, x = it.X, y = it.Y, r = it.R }); + } + public IEnumerable GetWalls(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var s)) return Enumerable.Empty(); + return s.Walls.Select(w => new { x = w.X, y = w.Y, w = w.W, h = w.H }); + } + public IEnumerable GetStealthZones(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var s)) return Enumerable.Empty(); + return s.Zones.Select(z => new { x = z.X, y = z.Y, r = z.R }); + } + + public IEnumerable GetAreaStats(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var state)) return Enumerable.Empty(); + // 重新同步 PixelCount(容错:防止覆盖时负值或不同步) + foreach (var k in state.PixelCount.Keys.ToList()) if (state.PixelCount[k] < 0) state.PixelCount[k] = 0; + double total = state.PixelCount.Values.Sum(); + if (total <= 0) total = 1; + return state.Game.GamePlayers.Select(gp => new AreaStatDto + { + PlayerId = gp.PlayerId, + PlayerName = gp.Player.Name, + Color = gp.Player.Color, + AreaPercent = state.PixelCount.TryGetValue(gp.PlayerId, out var pc) ? pc * 100.0 / total : 0 + }); + } + + public IEnumerable GetScores(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var state)) return Enumerable.Empty(); + var list = new List(); + foreach (var gp in state.Game.GamePlayers) + { + var pid = gp.PlayerId; + state.Runtime.TryGetValue(pid, out var rt); + list.Add(new + { + playerId = pid, + name = gp.Player.Name, + color = gp.Player.Color, + score = rt?.Score ?? 0, + kills = rt?.Kills ?? 0, + paintTiles = rt?.PaintTiles ?? 0 + }); + } + return list; + } + + public Guid? GetGameIdByConnection(string connectionId) + { + if (_connectionToGame.TryGetValue(connectionId, out var gid)) return gid; else return null; + } + + public Guid? ResolveGameId(string input) + { + if (Guid.TryParse(input, out var gid)) + { + return _games.ContainsKey(gid) ? gid : null; + } + // 兼容使用房间“名称/代码”作为路由参数的情况(CreateGame 中 Code=Name) + var match = _games.Values.FirstOrDefault(g => string.Equals(g.Game.Name, input, StringComparison.OrdinalIgnoreCase)); + return match?.Game.Id; + } + + public PlayerDto? GetPlayerByConnection(string connectionId) + { + if (!_connectionToGame.TryGetValue(connectionId, out var gid)) return null; + if (!_games.TryGetValue(gid, out var state)) return null; + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return null; + return new PlayerDto { Id = player.Id, Name = player.Name, Color = player.Color, Score = 0 }; + } + public int GetRemainingSeconds(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var state) || state.StartedAt == null) return 0; + var elapsed = (int)(DateTime.UtcNow - state.StartedAt.Value).TotalSeconds; + var total = state.Game.DurationMinutes * 60; + var remain = Math.Max(0, total - elapsed); + if (remain == 0 && state.Game.Status == GameStatus.InProgress) + { + state.Game.Status = GameStatus.Completed; + } + return remain; + } + + public GameResultDto GetResult(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var state)) return new GameResultDto(); + // 计算面积占比(用于展示) + var areaMap = GetAreaStats(gameId).ToDictionary(a => a.PlayerId, a => a.AreaPercent); + // 组装分数榜(来自 runtime) + var entries = new List(); + foreach (var gp in state.Game.GamePlayers) + { + state.Runtime.TryGetValue(gp.PlayerId, out var rt); + entries.Add(new ScoreEntryDto + { + PlayerId = gp.PlayerId, + PlayerName = gp.Player.Name, + Color = gp.Player.Color, + Score = rt?.Score ?? 0, + Kills = rt?.Kills ?? 0, + PaintTiles = rt?.PaintTiles ?? 0, + AreaPercent = areaMap.TryGetValue(gp.PlayerId, out var ap1) ? ap1 : 0 + }); + } + var ordered = entries.OrderByDescending(e => e.Score).ToList(); + var winner = ordered.FirstOrDefault(); + return new GameResultDto + { + GameId = gameId, + WinnerPlayerId = winner?.PlayerId, + WinnerAreaPercent = winner != null ? (areaMap.TryGetValue(winner.PlayerId, out var ap2) ? ap2 : 0) : 0, + WinnerScore = winner?.Score ?? 0, + Players = ordered + }; + } + + public IEnumerable ListGames() + { + return _games.Values.Select(g => new GameDto + { + Id = g.Game.Id, + Name = g.Game.Name, + Code = g.Game.Name, + MaxPlayers = g.Game.MaxPlayers, + Status = g.Game.Status, + CurrentPlayers = g.Game.GamePlayers.Count, + DurationMinutes = g.Game.DurationMinutes, + CanvasWidth = g.Game.CanvasWidth, + CanvasHeight = g.Game.CanvasHeight, + HostPlayerName = g.HostPlayerId != null ? g.Game.GamePlayers.FirstOrDefault(x=>x.PlayerId==g.HostPlayerId)?.Player?.Name : null, + HasPassword = g.Game.Password != null, + Players = g.Game.GamePlayers.Select(gp => new PlayerDto { Id = gp.PlayerId, Name = gp.Player.Name, Color = gp.Player.Color, Score = 0 }).ToList() + }); + } + + // 清理长期空闲的等待中房间(无任何活动,未开始) + public IEnumerable CleanupIdleRooms(TimeSpan idleFor) + { + var now = DateTime.UtcNow; + var expired = _games.Where(kv => + kv.Value.Game.Status == GameStatus.Waiting + && kv.Value.StartedAt == null + && (now - kv.Value.LastActivityUtc) >= idleFor + ).Select(kv => kv.Key).ToList(); + foreach (var id in expired) + { + _games.TryRemove(id, out _); + } + return expired; + } + + public int GetPlayerCount(Guid gameId) + { + return _games.TryGetValue(gameId, out var state) ? state.Game.GamePlayers.Count : 0; + } + + public GameDto? GetGameSnapshot(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var g)) return null; + return new GameDto + { + Id = g.Game.Id, + Name = g.Game.Name, + Code = g.Game.Name, + MaxPlayers = g.Game.MaxPlayers, + Status = g.Game.Status, + CurrentPlayers = g.Game.GamePlayers.Count, + DurationMinutes = g.Game.DurationMinutes, + CanvasWidth = g.Game.CanvasWidth, + CanvasHeight = g.Game.CanvasHeight, + HostPlayerName = g.HostPlayerId != null ? g.Game.GamePlayers.FirstOrDefault(x=>x.PlayerId==g.HostPlayerId)?.Player?.Name : null, + HasPassword = g.Game.Password != null, + Players = g.Game.GamePlayers.Select(gp => new PlayerDto { Id = gp.PlayerId, Name = gp.Player.Name, Color = gp.Player.Color, Score = 0 }).ToList() + }; + } +} diff --git a/backend/src/TerritoryGame.Application/Services/IGameManager.cs b/backend/src/TerritoryGame.Application/Services/IGameManager.cs new file mode 100644 index 0000000000000000000000000000000000000000..cae300886e050dfc66233d71357b3bb9a558a8a1 --- /dev/null +++ b/backend/src/TerritoryGame.Application/Services/IGameManager.cs @@ -0,0 +1,52 @@ +using TerritoryGame.Application.Common; + +namespace TerritoryGame.Application.Services; + +public interface IGameManager +{ + Task CreateGameAsync(string name, int maxPlayers, int durationMinutes, string? password = null); + // 新增 password 参数用于校验密码房间 + Task AddPlayerAsync(Guid gameId, string playerName, string connectionId, string? password = null); + Task RemovePlayerAsync(Guid gameId, string connectionId); + Task StartGameAsync(Guid gameId); + Task ProcessDrawAsync(Guid gameId, string connectionId, int x, int y, int size, string color); + IEnumerable GetAreaStats(Guid gameId); + IEnumerable GetPlayerStates(Guid gameId); + (bool ok, object? moved, int? energy, bool itemsChanged) Move(Guid gameId, string connectionId, int forward, int rotate); + // 前端权威:由客户端上报绝对位置与角度 + (bool ok, object? moved, int? energy, bool itemsChanged) ClientSetTransform(Guid gameId, string connectionId, int x, int y, double angle); + (bool ok, int energy, IEnumerable? sector, IEnumerable? stats, string? color) Spray(Guid gameId, string connectionId, int radius, int angleWidthDeg, bool allowOverwrite); + // 炮弹(权威爆炸) + // 返回值新增 killFeed:每个元素包含 shooterId/shooterName、victimId/victimName、item(可能为 null) + (bool ok, int energy, IEnumerable? affected, object? explosion, IEnumerable? killed, string? color, IEnumerable? killFeed) FireCannon(Guid gameId, string connectionId); + // 地图与道具 + IEnumerable GetWalls(Guid gameId); + IEnumerable GetItems(Guid gameId); + IEnumerable GetStealthZones(Guid gameId); + // 背包:获取调用者当前道具库存 + IDictionary GetInventory(Guid gameId, string connectionId); + // 激活道具(消耗背包内一个指定类型并应用效果) + (bool ok, int energy, IDictionary inventory) ActivateItem(Guid gameId, string connectionId, string type); + // 计时器驱动:检查并刷新一批道具,返回是否新增 + bool CheckAndSpawnItems(Guid gameId); + // 获取调用者当前运行时状态(含持续效果时间戳) + object? GetSelfState(Guid gameId, string connectionId); + Guid? GetGameIdByConnection(string connectionId); + int GetRemainingSeconds(Guid gameId); + GameResultDto GetResult(Guid gameId); + IEnumerable ListGames(); + int GetPlayerCount(Guid gameId); + GameDto? GetGameSnapshot(Guid gameId); + // 允许通过字符串(可能是 Guid 或 房间号/名称)解析实际游戏 Id + Guid? ResolveGameId(string input); + // 通过连接获取玩家(用于客户端补偿获取自身信息) + PlayerDto? GetPlayerByConnection(string connectionId); + // 房主踢出指定玩家(返回:是否成功、被踢玩家Id、被踢玩家连接Id、房间是否被删除、调用者是否房主) + (bool success, Guid? removedPlayerId, string? removedConnectionId, bool gameDeleted, bool wasHostCaller, string? reason) KickPlayer(Guid gameId, string hostConnectionId, Guid targetPlayerId); + // 清理空闲房间(返回删除的房间Id) + IEnumerable CleanupIdleRooms(TimeSpan idleFor); + // 获取实时分数榜(得分/击杀/占地) + IEnumerable GetScores(Guid gameId); +} + +public record RemovePlayerResult(bool Success, bool GameDeleted, Guid? RemovedPlayerId, bool WasHost); diff --git a/backend/src/TerritoryGame.Application/TerritoryGame.Application.csproj b/backend/src/TerritoryGame.Application/TerritoryGame.Application.csproj new file mode 100644 index 0000000000000000000000000000000000000000..c0c66db3a33466ee1187c064de7da6fc422e9a1f --- /dev/null +++ b/backend/src/TerritoryGame.Application/TerritoryGame.Application.csproj @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + net8.0 + enable + enable + + + diff --git a/backend/src/TerritoryGame.Domain/Class1.cs b/backend/src/TerritoryGame.Domain/Class1.cs new file mode 100644 index 0000000000000000000000000000000000000000..23fe78c8cfced99969de368cdae2afb2697b07a7 --- /dev/null +++ b/backend/src/TerritoryGame.Domain/Class1.cs @@ -0,0 +1,6 @@ +namespace TerritoryGame.Domain; + +public class Class1 +{ + +} diff --git a/backend/src/TerritoryGame.Domain/Model/Game.cs b/backend/src/TerritoryGame.Domain/Model/Game.cs new file mode 100644 index 0000000000000000000000000000000000000000..c174d871edd7033f76526fa48905001daeb20011 --- /dev/null +++ b/backend/src/TerritoryGame.Domain/Model/Game.cs @@ -0,0 +1,31 @@ +// Domain/Models/Game.cs +namespace TerritoryGame.Domain.Models; + +public class Game +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public GameStatus Status { get; set; } = GameStatus.Waiting; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime? StartedAt { get; set; } + public DateTime? EndedAt { get; set; } + public int MaxPlayers { get; set; } = 6; + public int DurationMinutes { get; set; } = 3; + public string? Password { get; set; } // 可选房间密码(纯文本暂存;生产应哈希) + + // 画布设置 + public int CanvasWidth { get; set; } = 800; + public int CanvasHeight { get; set; } = 600; + public string NeutralColor { get; set; } = "#CCCCCC"; + + // 导航属性 + public ICollection GamePlayers { get; set; } = new List(); + public ICollection Pixels { get; set; } = new List(); +} + +public enum GameStatus +{ + Waiting, + InProgress, + Completed +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Domain/Model/GamePlayer.cs b/backend/src/TerritoryGame.Domain/Model/GamePlayer.cs new file mode 100644 index 0000000000000000000000000000000000000000..cba51f31ed440229253d27c6db6be892ddbefa7d --- /dev/null +++ b/backend/src/TerritoryGame.Domain/Model/GamePlayer.cs @@ -0,0 +1,13 @@ +// Domain/Models/GamePlayer.cs +namespace TerritoryGame.Domain.Models; + +public class GamePlayer +{ + public Guid GameId { get; set; } + public Guid PlayerId { get; set; } + public int AreaOccupied { get; set; } // 占领的面积 + + // 导航属性 + public Game Game { get; set; } = null!; + public Player Player { get; set; } = null!; +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Domain/Model/Pixel.cs b/backend/src/TerritoryGame.Domain/Model/Pixel.cs new file mode 100644 index 0000000000000000000000000000000000000000..256eb4dd6491bb2f933f4967e3df80766f1100a4 --- /dev/null +++ b/backend/src/TerritoryGame.Domain/Model/Pixel.cs @@ -0,0 +1,13 @@ +// Domain/Models/Pixel.cs +namespace TerritoryGame.Domain.Models; + +public class Pixel +{ + public Guid GameId { get; set; } + public int X { get; set; } + public int Y { get; set; } + public string? Color { get; set; } // 颜色值或null表示未涂色 + + // 导航属性 + public Game Game { get; set; } = null!; +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Domain/Model/Player.cs b/backend/src/TerritoryGame.Domain/Model/Player.cs new file mode 100644 index 0000000000000000000000000000000000000000..bf7187d40947fc4c24f771f226ad2dfa1334a162 --- /dev/null +++ b/backend/src/TerritoryGame.Domain/Model/Player.cs @@ -0,0 +1,14 @@ +// Domain/Models/Player.cs +namespace TerritoryGame.Domain.Models; + +public class Player +{ + public Guid Id { get; set; } + public string ConnectionId { get; set; } = null!; // SignalR连接ID + public string Name { get; set; } = null!; + public string Color { get; set; } = null!; // 系统分配的颜色,格式如"#RRGGBB" + public int Score { get; set; } + + // 导航属性 + public ICollection GamePlayers { get; set; } = new List(); +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Domain/Model/User.cs b/backend/src/TerritoryGame.Domain/Model/User.cs new file mode 100644 index 0000000000000000000000000000000000000000..5be05ea65da9f0a7eca16d0d9602c5b4003ea718 --- /dev/null +++ b/backend/src/TerritoryGame.Domain/Model/User.cs @@ -0,0 +1,9 @@ +namespace TerritoryGame.Domain.Models; + +public class User +{ + public Guid Id { get; set; } + public string Username { get; set; } = null!; + public string PasswordHash { get; set; } = null!; // {salt}.{hash} + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/backend/src/TerritoryGame.Domain/Services/ColorService.cs b/backend/src/TerritoryGame.Domain/Services/ColorService.cs new file mode 100644 index 0000000000000000000000000000000000000000..8267be2affca0d014bfabbfef027f4f045d7f377 --- /dev/null +++ b/backend/src/TerritoryGame.Domain/Services/ColorService.cs @@ -0,0 +1,47 @@ +// Domain/Services/ColorService.cs +namespace TerritoryGame.Domain.Services; + +public static class ColorService +{ + // 预定义一组美观的、对比度足够的颜色 + private static readonly string[] PredefinedColors = + { + "#FF5252", "#FF4081", "#E040FB", "#7C4DFF", + "#536DFE", "#448AFF", "#40C4FF", "#18FFFF", + "#64FFDA", "#69F0AE", "#B2FF59", "#EEFF41", + "#FFFF00", "#FFD740", "#FFAB40", "#FF6E40" + }; + + private static readonly Random Random = new(); + private static readonly HashSet UsedColors = new(); + + public static string GetRandomColor() + { + lock (Random) + { + // 如果所有颜色都用过了,重置 + if (UsedColors.Count >= PredefinedColors.Length) + { + UsedColors.Clear(); + } + + // 随机选择一个未使用的颜色 + string color; + do + { + color = PredefinedColors[Random.Next(PredefinedColors.Length)]; + } while (UsedColors.Contains(color)); + + UsedColors.Add(color); + return color; + } + } + + public static void ReleaseColor(string color) + { + lock (UsedColors) + { + UsedColors.Remove(color); + } + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Domain/TerritoryGame.Domain.csproj b/backend/src/TerritoryGame.Domain/TerritoryGame.Domain.csproj new file mode 100644 index 0000000000000000000000000000000000000000..fa71b7ae6a34999a3f96c40d9a0b870b311d11dd --- /dev/null +++ b/backend/src/TerritoryGame.Domain/TerritoryGame.Domain.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/backend/src/TerritoryGame.Infrastructure/Class1.cs b/backend/src/TerritoryGame.Infrastructure/Class1.cs new file mode 100644 index 0000000000000000000000000000000000000000..23a9b3da37d7f9b9732d7fd3f44ea39d0c005d8b --- /dev/null +++ b/backend/src/TerritoryGame.Infrastructure/Class1.cs @@ -0,0 +1,6 @@ +namespace TerritoryGame.Infrastructure; + +public class Class1 +{ + +} diff --git a/backend/src/TerritoryGame.Infrastructure/Data/TerritoryGameDbContext.cs b/backend/src/TerritoryGame.Infrastructure/Data/TerritoryGameDbContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..5ec27680f84254a4826cfd2c21d131f9abe7c9b1 --- /dev/null +++ b/backend/src/TerritoryGame.Infrastructure/Data/TerritoryGameDbContext.cs @@ -0,0 +1,57 @@ +// Infrastructure/Data/TerritoryGameDbContext.cs +using Microsoft.EntityFrameworkCore; +using TerritoryGame.Domain.Models; + +namespace TerritoryGame.Infrastructure.Data; + +public class TerritoryGameDbContext : DbContext +{ + public TerritoryGameDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Players { get; set; } + public DbSet Games { get; set; } + public DbSet GamePlayers { get; set; } + public DbSet Pixels { get; set; } + public DbSet Users { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // 配置复合主键 + modelBuilder.Entity() + .HasKey(gp => new { gp.GameId, gp.PlayerId }); + + modelBuilder.Entity() + .HasKey(p => new { p.GameId, p.X, p.Y }); + + // 配置关系 - 设置为必需 + modelBuilder.Entity() + .HasOne(gp => gp.Game) + .WithMany(g => g.GamePlayers) + .HasForeignKey(gp => gp.GameId) + .IsRequired(); + + modelBuilder.Entity() + .HasOne(gp => gp.Player) + .WithMany(p => p.GamePlayers) + .HasForeignKey(gp => gp.PlayerId) + .IsRequired(); + + modelBuilder.Entity() + .HasOne(p => p.Game) + .WithMany(g => g.Pixels) + .HasForeignKey(p => p.GameId) + .IsRequired(); + + // 其他配置 + modelBuilder.Entity() + .Property(g => g.Status) + .HasConversion(); + + modelBuilder.Entity() + .HasIndex(u => u.Username) + .IsUnique(); + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Infrastructure/DependencyInjection.cs b/backend/src/TerritoryGame.Infrastructure/DependencyInjection.cs new file mode 100644 index 0000000000000000000000000000000000000000..9cbb2bc59ea071c204fb2da8486a310a63ab34b4 --- /dev/null +++ b/backend/src/TerritoryGame.Infrastructure/DependencyInjection.cs @@ -0,0 +1,34 @@ +// Infrastructure/DependencyInjection.cs +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using StackExchange.Redis; +using TerritoryGame.Infrastructure.Data; + +namespace TerritoryGame.Infrastructure; + +public static class DependencyInjection +{ + // Infrastructure/DependencyInjection.cs + public static IServiceCollection AddInfrastructure( + this IServiceCollection services, + IConfiguration configuration) + { + // PostgreSQL配置 + services.AddDbContext(options => + options.UseNpgsql(configuration.GetConnectionString("DefaultConnection"))); + + // Redis配置 - 添加null检查 + var redisConnectionString = configuration.GetConnectionString("Redis"); + if (string.IsNullOrWhiteSpace(redisConnectionString)) + { + throw new ArgumentNullException(nameof(redisConnectionString), + "Redis connection string is not configured"); + } + + services.AddSingleton(_ => + ConnectionMultiplexer.Connect(redisConnectionString)); + + return services; + } +} \ No newline at end of file diff --git a/backend/src/TerritoryGame.Infrastructure/Migrations/20250815083643_InitCreate.Designer.cs b/backend/src/TerritoryGame.Infrastructure/Migrations/20250815083643_InitCreate.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..ce7b59518741665e5ef1f8812992cfd0fb754bd1 --- /dev/null +++ b/backend/src/TerritoryGame.Infrastructure/Migrations/20250815083643_InitCreate.Designer.cs @@ -0,0 +1,179 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TerritoryGame.Infrastructure.Data; + +#nullable disable + +namespace TerritoryGame.Infrastructure.Migrations +{ + [DbContext(typeof(TerritoryGameDbContext))] + [Migration("20250815083643_InitCreate")] + partial class InitCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Game", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanvasHeight") + .HasColumnType("integer"); + + b.Property("CanvasWidth") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationMinutes") + .HasColumnType("integer"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxPlayers") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NeutralColor") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.GamePlayer", b => + { + b.Property("GameId") + .HasColumnType("uuid"); + + b.Property("PlayerId") + .HasColumnType("uuid"); + + b.Property("AreaOccupied") + .HasColumnType("integer"); + + b.HasKey("GameId", "PlayerId"); + + b.HasIndex("PlayerId"); + + b.ToTable("GamePlayers"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Pixel", b => + { + b.Property("GameId") + .HasColumnType("uuid"); + + b.Property("X") + .HasColumnType("integer"); + + b.Property("Y") + .HasColumnType("integer"); + + b.Property("Color") + .HasColumnType("text"); + + b.HasKey("GameId", "X", "Y"); + + b.ToTable("Pixels"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConnectionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Score") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.GamePlayer", b => + { + b.HasOne("TerritoryGame.Domain.Models.Game", "Game") + .WithMany("GamePlayers") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TerritoryGame.Domain.Models.Player", "Player") + .WithMany("GamePlayers") + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Pixel", b => + { + b.HasOne("TerritoryGame.Domain.Models.Game", "Game") + .WithMany("Pixels") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Game", b => + { + b.Navigation("GamePlayers"); + + b.Navigation("Pixels"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Player", b => + { + b.Navigation("GamePlayers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/TerritoryGame.Infrastructure/Migrations/20250815083643_InitCreate.cs b/backend/src/TerritoryGame.Infrastructure/Migrations/20250815083643_InitCreate.cs new file mode 100644 index 0000000000000000000000000000000000000000..e7dfdbf6b8701fad8006f7efa5385a966b4e886d --- /dev/null +++ b/backend/src/TerritoryGame.Infrastructure/Migrations/20250815083643_InitCreate.cs @@ -0,0 +1,117 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TerritoryGame.Infrastructure.Migrations +{ + /// + public partial class InitCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Games", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Status = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + StartedAt = table.Column(type: "timestamp with time zone", nullable: true), + EndedAt = table.Column(type: "timestamp with time zone", nullable: true), + MaxPlayers = table.Column(type: "integer", nullable: false), + DurationMinutes = table.Column(type: "integer", nullable: false), + CanvasWidth = table.Column(type: "integer", nullable: false), + CanvasHeight = table.Column(type: "integer", nullable: false), + NeutralColor = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Games", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Players", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ConnectionId = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Color = table.Column(type: "text", nullable: false), + Score = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Players", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Pixels", + columns: table => new + { + GameId = table.Column(type: "uuid", nullable: false), + X = table.Column(type: "integer", nullable: false), + Y = table.Column(type: "integer", nullable: false), + Color = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Pixels", x => new { x.GameId, x.X, x.Y }); + table.ForeignKey( + name: "FK_Pixels_Games_GameId", + column: x => x.GameId, + principalTable: "Games", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "GamePlayers", + columns: table => new + { + GameId = table.Column(type: "uuid", nullable: false), + PlayerId = table.Column(type: "uuid", nullable: false), + AreaOccupied = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GamePlayers", x => new { x.GameId, x.PlayerId }); + table.ForeignKey( + name: "FK_GamePlayers_Games_GameId", + column: x => x.GameId, + principalTable: "Games", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GamePlayers_Players_PlayerId", + column: x => x.PlayerId, + principalTable: "Players", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_GamePlayers_PlayerId", + table: "GamePlayers", + column: "PlayerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GamePlayers"); + + migrationBuilder.DropTable( + name: "Pixels"); + + migrationBuilder.DropTable( + name: "Players"); + + migrationBuilder.DropTable( + name: "Games"); + } + } +} diff --git a/backend/src/TerritoryGame.Infrastructure/Migrations/20250816150245_AddUsers.Designer.cs b/backend/src/TerritoryGame.Infrastructure/Migrations/20250816150245_AddUsers.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..8daafa29d41566f94f2384867a05a6e306cdfffb --- /dev/null +++ b/backend/src/TerritoryGame.Infrastructure/Migrations/20250816150245_AddUsers.Designer.cs @@ -0,0 +1,204 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TerritoryGame.Infrastructure.Data; + +#nullable disable + +namespace TerritoryGame.Infrastructure.Migrations +{ + [DbContext(typeof(TerritoryGameDbContext))] + [Migration("20250816150245_AddUsers")] + partial class AddUsers + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Game", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanvasHeight") + .HasColumnType("integer"); + + b.Property("CanvasWidth") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationMinutes") + .HasColumnType("integer"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxPlayers") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NeutralColor") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.GamePlayer", b => + { + b.Property("GameId") + .HasColumnType("uuid"); + + b.Property("PlayerId") + .HasColumnType("uuid"); + + b.Property("AreaOccupied") + .HasColumnType("integer"); + + b.HasKey("GameId", "PlayerId"); + + b.HasIndex("PlayerId"); + + b.ToTable("GamePlayers"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Pixel", b => + { + b.Property("GameId") + .HasColumnType("uuid"); + + b.Property("X") + .HasColumnType("integer"); + + b.Property("Y") + .HasColumnType("integer"); + + b.Property("Color") + .HasColumnType("text"); + + b.HasKey("GameId", "X", "Y"); + + b.ToTable("Pixels"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConnectionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Score") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.GamePlayer", b => + { + b.HasOne("TerritoryGame.Domain.Models.Game", "Game") + .WithMany("GamePlayers") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TerritoryGame.Domain.Models.Player", "Player") + .WithMany("GamePlayers") + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Pixel", b => + { + b.HasOne("TerritoryGame.Domain.Models.Game", "Game") + .WithMany("Pixels") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Game", b => + { + b.Navigation("GamePlayers"); + + b.Navigation("Pixels"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Player", b => + { + b.Navigation("GamePlayers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/TerritoryGame.Infrastructure/Migrations/20250816150245_AddUsers.cs b/backend/src/TerritoryGame.Infrastructure/Migrations/20250816150245_AddUsers.cs new file mode 100644 index 0000000000000000000000000000000000000000..96b6a0e6678c7ca95f1f68ad193ecdcf58e2a9e5 --- /dev/null +++ b/backend/src/TerritoryGame.Infrastructure/Migrations/20250816150245_AddUsers.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TerritoryGame.Infrastructure.Migrations +{ + /// + public partial class AddUsers : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Username = table.Column(type: "text", nullable: false), + PasswordHash = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_Username", + table: "Users", + column: "Username", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/backend/src/TerritoryGame.Infrastructure/Migrations/TerritoryGameDbContextModelSnapshot.cs b/backend/src/TerritoryGame.Infrastructure/Migrations/TerritoryGameDbContextModelSnapshot.cs new file mode 100644 index 0000000000000000000000000000000000000000..269197798dbf8e61cc533fd8efc682131e6bbdad --- /dev/null +++ b/backend/src/TerritoryGame.Infrastructure/Migrations/TerritoryGameDbContextModelSnapshot.cs @@ -0,0 +1,201 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TerritoryGame.Infrastructure.Data; + +#nullable disable + +namespace TerritoryGame.Infrastructure.Migrations +{ + [DbContext(typeof(TerritoryGameDbContext))] + partial class TerritoryGameDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Game", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanvasHeight") + .HasColumnType("integer"); + + b.Property("CanvasWidth") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationMinutes") + .HasColumnType("integer"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxPlayers") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NeutralColor") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.GamePlayer", b => + { + b.Property("GameId") + .HasColumnType("uuid"); + + b.Property("PlayerId") + .HasColumnType("uuid"); + + b.Property("AreaOccupied") + .HasColumnType("integer"); + + b.HasKey("GameId", "PlayerId"); + + b.HasIndex("PlayerId"); + + b.ToTable("GamePlayers"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Pixel", b => + { + b.Property("GameId") + .HasColumnType("uuid"); + + b.Property("X") + .HasColumnType("integer"); + + b.Property("Y") + .HasColumnType("integer"); + + b.Property("Color") + .HasColumnType("text"); + + b.HasKey("GameId", "X", "Y"); + + b.ToTable("Pixels"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConnectionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Score") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.GamePlayer", b => + { + b.HasOne("TerritoryGame.Domain.Models.Game", "Game") + .WithMany("GamePlayers") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TerritoryGame.Domain.Models.Player", "Player") + .WithMany("GamePlayers") + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Pixel", b => + { + b.HasOne("TerritoryGame.Domain.Models.Game", "Game") + .WithMany("Pixels") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Game", b => + { + b.Navigation("GamePlayers"); + + b.Navigation("Pixels"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Player", b => + { + b.Navigation("GamePlayers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/TerritoryGame.Infrastructure/Security/PasswordHasher.cs b/backend/src/TerritoryGame.Infrastructure/Security/PasswordHasher.cs new file mode 100644 index 0000000000000000000000000000000000000000..120561dbb226db96e5ada7eb1aa3f2af50320fab --- /dev/null +++ b/backend/src/TerritoryGame.Infrastructure/Security/PasswordHasher.cs @@ -0,0 +1,32 @@ +using System.Security.Cryptography; +using System.Text; + +namespace TerritoryGame.Infrastructure.Security; + +public static class PasswordHasher +{ + public static string Hash(string password) + { + var saltBytes = RandomNumberGenerator.GetBytes(16); + var salt = Convert.ToBase64String(saltBytes); + var hash = Compute(password, salt); + return salt + "." + hash; + } + + public static bool Verify(string password, string stored) + { + var parts = stored.Split('.'); + if (parts.Length != 2) return false; + var salt = parts[0]; + var expected = parts[1]; + var actual = Compute(password, salt); + return CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(actual)); + } + + private static string Compute(string password, string salt) + { + using var sha256 = SHA256.Create(); + var bytes = Encoding.UTF8.GetBytes(password + salt); + return Convert.ToBase64String(sha256.ComputeHash(bytes)); + } +} diff --git a/backend/src/TerritoryGame.Infrastructure/TerritoryGame.Infrastructure.csproj b/backend/src/TerritoryGame.Infrastructure/TerritoryGame.Infrastructure.csproj new file mode 100644 index 0000000000000000000000000000000000000000..9ed81c671dc3f5c942d58ce4e7e63acb161823e6 --- /dev/null +++ b/backend/src/TerritoryGame.Infrastructure/TerritoryGame.Infrastructure.csproj @@ -0,0 +1,24 @@ + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + net8.0 + enable + enable + + + diff --git a/backend/src/TerritoryGame.sln b/backend/src/TerritoryGame.sln new file mode 100644 index 0000000000000000000000000000000000000000..bebba03082b585edcb61cd2623631f74570e3fe9 --- /dev/null +++ b/backend/src/TerritoryGame.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerritoryGame.API", "TerritoryGame.API\TerritoryGame.API.csproj", "{B94187C1-B78F-4C32-9733-88AE969266C5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerritoryGame.Application", "TerritoryGame.Application\TerritoryGame.Application.csproj", "{230A48D9-230E-43D3-836C-BC7F1852930C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerritoryGame.Domain", "TerritoryGame.Domain\TerritoryGame.Domain.csproj", "{9092F7DC-2A14-4356-A8AE-B576FFAE3B7E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerritoryGame.Infrastructure", "TerritoryGame.Infrastructure\TerritoryGame.Infrastructure.csproj", "{BD5F3259-81B2-4963-B220-4F9841B2F94B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B94187C1-B78F-4C32-9733-88AE969266C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B94187C1-B78F-4C32-9733-88AE969266C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B94187C1-B78F-4C32-9733-88AE969266C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B94187C1-B78F-4C32-9733-88AE969266C5}.Release|Any CPU.Build.0 = Release|Any CPU + {230A48D9-230E-43D3-836C-BC7F1852930C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {230A48D9-230E-43D3-836C-BC7F1852930C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {230A48D9-230E-43D3-836C-BC7F1852930C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {230A48D9-230E-43D3-836C-BC7F1852930C}.Release|Any CPU.Build.0 = Release|Any CPU + {9092F7DC-2A14-4356-A8AE-B576FFAE3B7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9092F7DC-2A14-4356-A8AE-B576FFAE3B7E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9092F7DC-2A14-4356-A8AE-B576FFAE3B7E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9092F7DC-2A14-4356-A8AE-B576FFAE3B7E}.Release|Any CPU.Build.0 = Release|Any CPU + {BD5F3259-81B2-4963-B220-4F9841B2F94B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD5F3259-81B2-4963-B220-4F9841B2F94B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD5F3259-81B2-4963-B220-4F9841B2F94B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD5F3259-81B2-4963-B220-4F9841B2F94B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/frontend/.editorconfig b/frontend/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..3b510aa687ba5d3dbaec1b9c6989327f84261a21 --- /dev/null +++ b/frontend/.editorconfig @@ -0,0 +1,8 @@ +[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +max_line_length = 100 diff --git a/frontend/.gitattributes b/frontend/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..6313b56c57848efce05faa7aa7e901ccfc2886ea --- /dev/null +++ b/frontend/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..8ee54e8d343e466a213c8c30aa04be77126b170d --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json new file mode 100644 index 0000000000000000000000000000000000000000..29a2402ef050746efe041b9e3393bf33796407c3 --- /dev/null +++ b/frontend/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "singleQuote": true, + "printWidth": 100 +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000000000000000000000000000000000000..7807d8b33d25d86a60b25349e940aa9ae59c3d2e --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,26 @@ +import { defineConfig, globalIgnores } from 'eslint/config' +import globals from 'globals' +import js from '@eslint/js' +import pluginVue from 'eslint-plugin-vue' +import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' + +export default defineConfig([ + { + name: 'app/files-to-lint', + files: ['**/*.{js,mjs,jsx,vue}'], + }, + + globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']), + + { + languageOptions: { + globals: { + ...globals.browser, + }, + }, + }, + + js.configs.recommended, + ...pluginVue.configs['flat/essential'], + skipFormatting, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000000000000000000000000000000000000..d599d86183dd4a4d62aae7656babda4e156b3b84 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + 鼠标大作战 + + +
+ + + diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..5a1f2d222a302a174e710614c6d76531b7bda926 --- /dev/null +++ b/frontend/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "paths": { + "@/*": ["./src/*"] + } + }, + "exclude": ["node_modules", "dist"] +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000000000000000000000000000000000000..3a134bf6d7497c028b2b2831d20fd7a0dc046ef5 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,35 @@ +{ + "name": "frontend", + "version": "0.0.0", + "private": true, + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint . --fix", + "format": "prettier --write src/" + }, + "dependencies": { + "@microsoft/signalr": "^9.0.6", + "axios": "^1.11.0", + "element-plus": "^2.10.7", + "pinia": "^3.0.3", + "vue": "^3.5.18", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "@eslint/js": "^9.31.0", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/eslint-config-prettier": "^10.2.0", + "eslint": "^9.31.0", + "eslint-plugin-vue": "~10.3.0", + "globals": "^16.3.0", + "prettier": "3.6.2", + "vite": "^7.0.6", + "vite-plugin-vue-devtools": "^8.0.0" + } +} diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000000000000000000000000000000000000..27b92250afe18de9adb751bba2c1f251f946a7a1 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,3111 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@microsoft/signalr': + specifier: ^9.0.6 + version: 9.0.6 + axios: + specifier: ^1.11.0 + version: 1.11.0 + element-plus: + specifier: ^2.10.7 + version: 2.10.7(vue@3.5.18) + pinia: + specifier: ^3.0.3 + version: 3.0.3(vue@3.5.18) + vue: + specifier: ^3.5.18 + version: 3.5.18 + vue-router: + specifier: ^4.5.1 + version: 4.5.1(vue@3.5.18) + devDependencies: + '@eslint/js': + specifier: ^9.31.0 + version: 9.33.0 + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.1(vite@7.1.2)(vue@3.5.18) + '@vue/eslint-config-prettier': + specifier: ^10.2.0 + version: 10.2.0(eslint@9.33.0)(prettier@3.6.2) + eslint: + specifier: ^9.31.0 + version: 9.33.0 + eslint-plugin-vue: + specifier: ~10.3.0 + version: 10.3.0(eslint@9.33.0)(vue-eslint-parser@10.2.0(eslint@9.33.0)) + globals: + specifier: ^16.3.0 + version: 16.3.0 + prettier: + specifier: 3.6.2 + version: 3.6.2 + vite: + specifier: ^7.0.6 + version: 7.1.2 + vite-plugin-vue-devtools: + specifier: ^8.0.0 + version: 8.0.0(vite@7.1.2)(vue@3.5.18) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.3': + resolution: {integrity: sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.3': + resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.3': + resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.3': + resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-decorators@7.28.0': + resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.27.1': + resolution: {integrity: sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.0': + resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.3': + resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.2': + resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + engines: {node: '>=6.9.0'} + + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@esbuild/aix-ppc64@0.25.9': + resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.9': + resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.9': + resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.9': + resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.9': + resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.9': + resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.9': + resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.9': + resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.9': + resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.9': + resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.9': + resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.9': + resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.9': + resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.9': + resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.9': + resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.9': + resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.9': + resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.9': + resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.9': + resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.9': + resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.9': + resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.9': + resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.9': + resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.9': + resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.9': + resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.9': + resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.33.0': + resolution: {integrity: sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.3': + resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + + '@microsoft/signalr@9.0.6': + resolution: {integrity: sha512-DrhgzFWI9JE4RPTsHYRxh4yr+OhnwKz8bnJe7eIi7mLLjqhJpEb62CiUy/YbFvLqLzcGzlzz1QWgVAW0zyipMQ==} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rolldown/pluginutils@1.0.0-beta.29': + resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} + + '@rollup/rollup-android-arm-eabi@4.46.2': + resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.46.2': + resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.46.2': + resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.46.2': + resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.46.2': + resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.46.2': + resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.46.2': + resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.46.2': + resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.46.2': + resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.46.2': + resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.46.2': + resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.46.2': + resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.46.2': + resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.46.2': + resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.46.2': + resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.46.2': + resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.46.2': + resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.46.2': + resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==} + cpu: [x64] + os: [win32] + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@sxzz/popperjs-es@2.11.7': + resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/web-bluetooth@0.0.16': + resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==} + + '@vitejs/plugin-vue@6.0.1': + resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vue: ^3.2.25 + + '@vue/babel-helper-vue-transform-on@1.5.0': + resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==} + + '@vue/babel-plugin-jsx@1.5.0': + resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.5.0': + resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.18': + resolution: {integrity: sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==} + + '@vue/compiler-dom@3.5.18': + resolution: {integrity: sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==} + + '@vue/compiler-sfc@3.5.18': + resolution: {integrity: sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==} + + '@vue/compiler-ssr@3.5.18': + resolution: {integrity: sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.7': + resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==} + + '@vue/devtools-core@8.0.0': + resolution: {integrity: sha512-5bPtF0jAFnaGs4C/4+3vGRR5U+cf6Y8UWK0nJflutEDGepHxl5L9JRaPdHQYCUgrzUaf4cY4waNBEEGXrfcs3A==} + peerDependencies: + vue: ^3.0.0 + + '@vue/devtools-kit@7.7.7': + resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==} + + '@vue/devtools-kit@8.0.0': + resolution: {integrity: sha512-b11OeQODkE0bctdT0RhL684pEV2DPXJ80bjpywVCbFn1PxuL3bmMPDoJKjbMnnoWbrnUYXYzFfmMWBZAMhORkQ==} + + '@vue/devtools-shared@7.7.7': + resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} + + '@vue/devtools-shared@8.0.0': + resolution: {integrity: sha512-jrKnbjshQCiOAJanoeJjTU7WaCg0Dz2BUal6SaR6VM/P3hiFdX5Q6Pxl73ZMnrhCxNK9nAg5hvvRGqs+6dtU1g==} + + '@vue/eslint-config-prettier@10.2.0': + resolution: {integrity: sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw==} + peerDependencies: + eslint: '>= 8.21.0' + prettier: '>= 3.0.0' + + '@vue/reactivity@3.5.18': + resolution: {integrity: sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==} + + '@vue/runtime-core@3.5.18': + resolution: {integrity: sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==} + + '@vue/runtime-dom@3.5.18': + resolution: {integrity: sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==} + + '@vue/server-renderer@3.5.18': + resolution: {integrity: sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==} + peerDependencies: + vue: 3.5.18 + + '@vue/shared@3.5.18': + resolution: {integrity: sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==} + + '@vueuse/core@9.13.0': + resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==} + + '@vueuse/metadata@9.13.0': + resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==} + + '@vueuse/shared@9.13.0': + resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansis@4.1.0: + resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} + engines: {node: '>=14'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.11.0: + resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + birpc@2.5.0: + resolution: {integrity: sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + browserslist@4.25.2: + resolution: {integrity: sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001735: + resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + default-browser-id@5.0.0: + resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + engines: {node: '>=18'} + + default-browser@5.2.1: + resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.201: + resolution: {integrity: sha512-ZG65vsrLClodGqywuigc+7m0gr4ISoTQttfVh7nfpLv0M7SIwF4WbFNEOywcqTiujs12AUeeXbFyQieDICAIxg==} + + element-plus@2.10.7: + resolution: {integrity: sha512-bL4yhepL8/0NEQA5+N2Q6ZVKLipIDkiQjK2mqtSmGh6CxJk1yaBMdG5HXfYkbk1htNcT3ULk9g23lzT323JGcA==} + peerDependencies: + vue: ^3.2.0 + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.9: + resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.4: + resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-vue@10.3.0: + resolution: {integrity: sha512-A0u9snqjCfYaPnqqOaH6MBLVWDUIN4trXn8J3x67uDcXvR7X6Ut8p16N+nYhMCQ9Y7edg2BIRGzfyZsY0IdqoQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 + vue-eslint-parser: ^10.0.0 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.33.0: + resolution: {integrity: sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + + execa@9.6.0: + resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} + engines: {node: ^18.19.0 || >=20.5.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-cookie@2.2.0: + resolution: {integrity: sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.3.0: + resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.1.5: + resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} + engines: {node: ^18 || >=20} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pinia@3.0.3: + resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-ms@9.2.0: + resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} + engines: {node: '>=18'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.46.2: + resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-applescript@7.0.0: + resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} + engines: {node: '>=18'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + unplugin-utils@0.2.5: + resolution: {integrity: sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==} + engines: {node: '>=18.12.0'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-dev-rpc@1.1.0: + resolution: {integrity: sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 + + vite-hot-client@2.1.0: + resolution: {integrity: sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite-plugin-inspect@11.3.2: + resolution: {integrity: sha512-nzwvyFQg58XSMAmKVLr2uekAxNYvAbz1lyPmCAFVIBncCgN9S/HPM+2UM9Q9cvc4JEbC5ZBgwLAdaE2onmQuKg==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + vite-plugin-vue-devtools@8.0.0: + resolution: {integrity: sha512-9bWQig8UMu3nPbxX86NJv56aelpFYoBHxB5+pxuQz3pa3Tajc1ezRidj/0dnADA4/UHuVIfwIVYHOvMXYcPshg==} + engines: {node: '>=v14.21.3'} + peerDependencies: + vite: ^6.0.0 || ^7.0.0-0 + + vite-plugin-vue-inspector@5.3.2: + resolution: {integrity: sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==} + peerDependencies: + vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite@7.1.2: + resolution: {integrity: sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-eslint-parser@10.2.0: + resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + vue-router@4.5.1: + resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==} + peerDependencies: + vue: ^3.2.0 + + vue@3.5.18: + resolution: {integrity: sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.0': {} + + '@babel/core@7.28.3': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) + '@babel/helpers': 7.28.3 + '@babel/parser': 7.28.3 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.2 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.3) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.27.1': + dependencies: + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.2 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.3': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 + + '@babel/parser@7.28.3': + dependencies: + '@babel/types': 7.28.2 + + '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.28.3) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.3) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 + + '@babel/traverse@7.28.3': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.3 + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.2': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@ctrl/tinycolor@3.6.1': {} + + '@element-plus/icons-vue@2.3.2(vue@3.5.18)': + dependencies: + vue: 3.5.18 + + '@esbuild/aix-ppc64@0.25.9': + optional: true + + '@esbuild/android-arm64@0.25.9': + optional: true + + '@esbuild/android-arm@0.25.9': + optional: true + + '@esbuild/android-x64@0.25.9': + optional: true + + '@esbuild/darwin-arm64@0.25.9': + optional: true + + '@esbuild/darwin-x64@0.25.9': + optional: true + + '@esbuild/freebsd-arm64@0.25.9': + optional: true + + '@esbuild/freebsd-x64@0.25.9': + optional: true + + '@esbuild/linux-arm64@0.25.9': + optional: true + + '@esbuild/linux-arm@0.25.9': + optional: true + + '@esbuild/linux-ia32@0.25.9': + optional: true + + '@esbuild/linux-loong64@0.25.9': + optional: true + + '@esbuild/linux-mips64el@0.25.9': + optional: true + + '@esbuild/linux-ppc64@0.25.9': + optional: true + + '@esbuild/linux-riscv64@0.25.9': + optional: true + + '@esbuild/linux-s390x@0.25.9': + optional: true + + '@esbuild/linux-x64@0.25.9': + optional: true + + '@esbuild/netbsd-arm64@0.25.9': + optional: true + + '@esbuild/netbsd-x64@0.25.9': + optional: true + + '@esbuild/openbsd-arm64@0.25.9': + optional: true + + '@esbuild/openbsd-x64@0.25.9': + optional: true + + '@esbuild/openharmony-arm64@0.25.9': + optional: true + + '@esbuild/sunos-x64@0.25.9': + optional: true + + '@esbuild/win32-arm64@0.25.9': + optional: true + + '@esbuild/win32-ia32@0.25.9': + optional: true + + '@esbuild/win32-x64@0.25.9': + optional: true + + '@eslint-community/eslint-utils@4.7.0(eslint@9.33.0)': + dependencies: + eslint: 9.33.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.1': {} + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.33.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.3': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.30 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.30': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@microsoft/signalr@9.0.6': + dependencies: + abort-controller: 3.0.0 + eventsource: 2.0.2 + fetch-cookie: 2.2.0 + node-fetch: 2.7.0 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + + '@pkgr/core@0.2.9': {} + + '@polka/url@1.0.0-next.29': {} + + '@rolldown/pluginutils@1.0.0-beta.29': {} + + '@rollup/rollup-android-arm-eabi@4.46.2': + optional: true + + '@rollup/rollup-android-arm64@4.46.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.46.2': + optional: true + + '@rollup/rollup-darwin-x64@4.46.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.46.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.46.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.46.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.46.2': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.46.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.46.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.46.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.46.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.46.2': + optional: true + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@sxzz/popperjs-es@2.11.7': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.20 + + '@types/lodash@4.17.20': {} + + '@types/web-bluetooth@0.0.16': {} + + '@vitejs/plugin-vue@6.0.1(vite@7.1.2)(vue@3.5.18)': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.29 + vite: 7.1.2 + vue: 3.5.18 + + '@vue/babel-helper-vue-transform-on@1.5.0': {} + + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.28.3)': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + '@vue/babel-helper-vue-transform-on': 1.5.0 + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.28.3) + '@vue/shared': 3.5.18 + optionalDependencies: + '@babel/core': 7.28.3 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.28.3)': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/core': 7.28.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/parser': 7.28.3 + '@vue/compiler-sfc': 3.5.18 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.18': + dependencies: + '@babel/parser': 7.28.3 + '@vue/shared': 3.5.18 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.18': + dependencies: + '@vue/compiler-core': 3.5.18 + '@vue/shared': 3.5.18 + + '@vue/compiler-sfc@3.5.18': + dependencies: + '@babel/parser': 7.28.3 + '@vue/compiler-core': 3.5.18 + '@vue/compiler-dom': 3.5.18 + '@vue/compiler-ssr': 3.5.18 + '@vue/shared': 3.5.18 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.18': + dependencies: + '@vue/compiler-dom': 3.5.18 + '@vue/shared': 3.5.18 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.7': + dependencies: + '@vue/devtools-kit': 7.7.7 + + '@vue/devtools-core@8.0.0(vite@7.1.2)(vue@3.5.18)': + dependencies: + '@vue/devtools-kit': 8.0.0 + '@vue/devtools-shared': 8.0.0 + mitt: 3.0.1 + nanoid: 5.1.5 + pathe: 2.0.3 + vite-hot-client: 2.1.0(vite@7.1.2) + vue: 3.5.18 + transitivePeerDependencies: + - vite + + '@vue/devtools-kit@7.7.7': + dependencies: + '@vue/devtools-shared': 7.7.7 + birpc: 2.5.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-kit@8.0.0': + dependencies: + '@vue/devtools-shared': 8.0.0 + birpc: 2.5.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-shared@7.7.7': + dependencies: + rfdc: 1.4.1 + + '@vue/devtools-shared@8.0.0': + dependencies: + rfdc: 1.4.1 + + '@vue/eslint-config-prettier@10.2.0(eslint@9.33.0)(prettier@3.6.2)': + dependencies: + eslint: 9.33.0 + eslint-config-prettier: 10.1.8(eslint@9.33.0) + eslint-plugin-prettier: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.33.0))(eslint@9.33.0)(prettier@3.6.2) + prettier: 3.6.2 + transitivePeerDependencies: + - '@types/eslint' + + '@vue/reactivity@3.5.18': + dependencies: + '@vue/shared': 3.5.18 + + '@vue/runtime-core@3.5.18': + dependencies: + '@vue/reactivity': 3.5.18 + '@vue/shared': 3.5.18 + + '@vue/runtime-dom@3.5.18': + dependencies: + '@vue/reactivity': 3.5.18 + '@vue/runtime-core': 3.5.18 + '@vue/shared': 3.5.18 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.18(vue@3.5.18)': + dependencies: + '@vue/compiler-ssr': 3.5.18 + '@vue/shared': 3.5.18 + vue: 3.5.18 + + '@vue/shared@3.5.18': {} + + '@vueuse/core@9.13.0(vue@3.5.18)': + dependencies: + '@types/web-bluetooth': 0.0.16 + '@vueuse/metadata': 9.13.0 + '@vueuse/shared': 9.13.0(vue@3.5.18) + vue-demi: 0.14.10(vue@3.5.18) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@9.13.0': {} + + '@vueuse/shared@9.13.0(vue@3.5.18)': + dependencies: + vue-demi: 0.14.10(vue@3.5.18) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansis@4.1.0: {} + + argparse@2.0.1: {} + + async-validator@4.2.5: {} + + asynckit@0.4.0: {} + + axios@1.11.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + birpc@2.5.0: {} + + boolbase@1.0.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + browserslist@4.25.2: + dependencies: + caniuse-lite: 1.0.30001735 + electron-to-chromium: 1.5.201 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.2) + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.0.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001735: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.1.3: {} + + dayjs@1.11.13: {} + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + default-browser-id@5.0.0: {} + + default-browser@5.2.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.0 + + define-lazy-prop@3.0.0: {} + + delayed-stream@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.201: {} + + element-plus@2.10.7(vue@3.5.18): + dependencies: + '@ctrl/tinycolor': 3.6.1 + '@element-plus/icons-vue': 2.3.2(vue@3.5.18) + '@floating-ui/dom': 1.7.3 + '@popperjs/core': '@sxzz/popperjs-es@2.11.7' + '@types/lodash': 4.17.20 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 9.13.0(vue@3.5.18) + async-validator: 4.2.5 + dayjs: 1.11.13 + escape-html: 1.0.3 + lodash: 4.17.21 + lodash-es: 4.17.21 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.18 + transitivePeerDependencies: + - '@vue/composition-api' + + entities@4.5.0: {} + + error-stack-parser-es@1.0.5: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.25.9: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.9 + '@esbuild/android-arm': 0.25.9 + '@esbuild/android-arm64': 0.25.9 + '@esbuild/android-x64': 0.25.9 + '@esbuild/darwin-arm64': 0.25.9 + '@esbuild/darwin-x64': 0.25.9 + '@esbuild/freebsd-arm64': 0.25.9 + '@esbuild/freebsd-x64': 0.25.9 + '@esbuild/linux-arm': 0.25.9 + '@esbuild/linux-arm64': 0.25.9 + '@esbuild/linux-ia32': 0.25.9 + '@esbuild/linux-loong64': 0.25.9 + '@esbuild/linux-mips64el': 0.25.9 + '@esbuild/linux-ppc64': 0.25.9 + '@esbuild/linux-riscv64': 0.25.9 + '@esbuild/linux-s390x': 0.25.9 + '@esbuild/linux-x64': 0.25.9 + '@esbuild/netbsd-arm64': 0.25.9 + '@esbuild/netbsd-x64': 0.25.9 + '@esbuild/openbsd-arm64': 0.25.9 + '@esbuild/openbsd-x64': 0.25.9 + '@esbuild/openharmony-arm64': 0.25.9 + '@esbuild/sunos-x64': 0.25.9 + '@esbuild/win32-arm64': 0.25.9 + '@esbuild/win32-ia32': 0.25.9 + '@esbuild/win32-x64': 0.25.9 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.33.0): + dependencies: + eslint: 9.33.0 + + eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.33.0))(eslint@9.33.0)(prettier@3.6.2): + dependencies: + eslint: 9.33.0 + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@9.33.0) + + eslint-plugin-vue@10.3.0(eslint@9.33.0)(vue-eslint-parser@10.2.0(eslint@9.33.0)): + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0) + eslint: 9.33.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.7.2 + vue-eslint-parser: 10.2.0(eslint@9.33.0) + xml-name-validator: 4.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.33.0: + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.33.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + 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.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + event-target-shim@5.0.1: {} + + eventsource@2.0.2: {} + + execa@9.6.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.2.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.1 + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fetch-cookie@2.2.0: + dependencies: + set-cookie-parser: 2.7.1 + tough-cookie: 4.1.4 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.3.0: {} + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hookable@5.5.3: {} + + human-signals@8.0.1: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-plain-obj@4.1.0: {} + + is-stream@4.0.1: {} + + is-unicode-supported@2.1.0: {} + + is-what@4.1.16: {} + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kolorist@1.8.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.21: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.17.21 + lodash-es: 4.17.21 + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + memoize-one@6.0.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + mitt@3.0.1: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + nanoid@5.1.5: {} + + natural-compare@1.4.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-releases@2.0.19: {} + + normalize-wheel-es@1.2.0: {} + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + ohash@2.0.11: {} + + open@10.2.0: + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-ms@4.0.0: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pinia@3.0.3(vue@3.5.18): + dependencies: + '@vue/devtools-api': 7.7.7 + vue: 3.5.18 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.6.2: {} + + pretty-ms@9.2.0: + dependencies: + parse-ms: 4.0.0 + + proxy-from-env@1.1.0: {} + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} + + querystringify@2.2.0: {} + + requires-port@1.0.0: {} + + resolve-from@4.0.0: {} + + rfdc@1.4.1: {} + + rollup@4.46.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.46.2 + '@rollup/rollup-android-arm64': 4.46.2 + '@rollup/rollup-darwin-arm64': 4.46.2 + '@rollup/rollup-darwin-x64': 4.46.2 + '@rollup/rollup-freebsd-arm64': 4.46.2 + '@rollup/rollup-freebsd-x64': 4.46.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.46.2 + '@rollup/rollup-linux-arm-musleabihf': 4.46.2 + '@rollup/rollup-linux-arm64-gnu': 4.46.2 + '@rollup/rollup-linux-arm64-musl': 4.46.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.46.2 + '@rollup/rollup-linux-ppc64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-musl': 4.46.2 + '@rollup/rollup-linux-s390x-gnu': 4.46.2 + '@rollup/rollup-linux-x64-gnu': 4.46.2 + '@rollup/rollup-linux-x64-musl': 4.46.2 + '@rollup/rollup-win32-arm64-msvc': 4.46.2 + '@rollup/rollup-win32-ia32-msvc': 4.46.2 + '@rollup/rollup-win32-x64-msvc': 4.46.2 + fsevents: 2.3.3 + + run-applescript@7.0.0: {} + + semver@6.3.1: {} + + semver@7.7.2: {} + + set-cookie-parser@2.7.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + sirv@3.0.1: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + speakingurl@14.0.1: {} + + strip-final-newline@4.0.0: {} + + strip-json-comments@3.1.1: {} + + superjson@2.2.2: + dependencies: + copy-anything: 3.0.5 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 + + tinyglobby@0.2.14: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + totalist@3.0.1: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@0.0.3: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + unicorn-magic@0.3.0: {} + + universalify@0.2.0: {} + + unplugin-utils@0.2.5: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + update-browserslist-db@1.1.3(browserslist@4.25.2): + dependencies: + browserslist: 4.25.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + util-deprecate@1.0.2: {} + + vite-dev-rpc@1.1.0(vite@7.1.2): + dependencies: + birpc: 2.5.0 + vite: 7.1.2 + vite-hot-client: 2.1.0(vite@7.1.2) + + vite-hot-client@2.1.0(vite@7.1.2): + dependencies: + vite: 7.1.2 + + vite-plugin-inspect@11.3.2(vite@7.1.2): + dependencies: + ansis: 4.1.0 + debug: 4.4.1 + error-stack-parser-es: 1.0.5 + ohash: 2.0.11 + open: 10.2.0 + perfect-debounce: 1.0.0 + sirv: 3.0.1 + unplugin-utils: 0.2.5 + vite: 7.1.2 + vite-dev-rpc: 1.1.0(vite@7.1.2) + transitivePeerDependencies: + - supports-color + + vite-plugin-vue-devtools@8.0.0(vite@7.1.2)(vue@3.5.18): + dependencies: + '@vue/devtools-core': 8.0.0(vite@7.1.2)(vue@3.5.18) + '@vue/devtools-kit': 8.0.0 + '@vue/devtools-shared': 8.0.0 + execa: 9.6.0 + sirv: 3.0.1 + vite: 7.1.2 + vite-plugin-inspect: 11.3.2(vite@7.1.2) + vite-plugin-vue-inspector: 5.3.2(vite@7.1.2) + transitivePeerDependencies: + - '@nuxt/kit' + - supports-color + - vue + + vite-plugin-vue-inspector@5.3.2(vite@7.1.2): + dependencies: + '@babel/core': 7.28.3 + '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.3) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.3) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.3) + '@vue/compiler-dom': 3.5.18 + kolorist: 1.8.0 + magic-string: 0.30.17 + vite: 7.1.2 + transitivePeerDependencies: + - supports-color + + vite@7.1.2: + dependencies: + esbuild: 0.25.9 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.46.2 + tinyglobby: 0.2.14 + optionalDependencies: + fsevents: 2.3.3 + + vue-demi@0.14.10(vue@3.5.18): + dependencies: + vue: 3.5.18 + + vue-eslint-parser@10.2.0(eslint@9.33.0): + dependencies: + debug: 4.4.1 + eslint: 9.33.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + vue-router@4.5.1(vue@3.5.18): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.18 + + vue@3.5.18: + dependencies: + '@vue/compiler-dom': 3.5.18 + '@vue/compiler-sfc': 3.5.18 + '@vue/runtime-dom': 3.5.18 + '@vue/server-renderer': 3.5.18(vue@3.5.18) + '@vue/shared': 3.5.18 + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + ws@7.5.10: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.0 + + xml-name-validator@4.0.0: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + yoctocolors@2.1.1: {} diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/logo.png b/frontend/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bfcfb5351edab6bd620935ea4f01dbd4c6b66893 Binary files /dev/null and b/frontend/public/logo.png differ diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000000000000000000000000000000000000..4be89653e14a8c9cf20e0ace07e89f873864054c --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,4 @@ + + diff --git a/frontend/src/assets/base.css b/frontend/src/assets/base.css new file mode 100644 index 0000000000000000000000000000000000000000..8816868a41b651f318dee87c6784ebcd6e29eca1 --- /dev/null +++ b/frontend/src/assets/base.css @@ -0,0 +1,86 @@ +/* color palette from */ +:root { + --vt-c-white: #ffffff; + --vt-c-white-soft: #f8f8f8; + --vt-c-white-mute: #f2f2f2; + + --vt-c-black: #181818; + --vt-c-black-soft: #222222; + --vt-c-black-mute: #282828; + + --vt-c-indigo: #2c3e50; + + --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); + --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); + --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); + --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); + + --vt-c-text-light-1: var(--vt-c-indigo); + --vt-c-text-light-2: rgba(60, 60, 60, 0.66); + --vt-c-text-dark-1: var(--vt-c-white); + --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); +} + +/* semantic color variables for this project */ +:root { + --color-background: var(--vt-c-white); + --color-background-soft: var(--vt-c-white-soft); + --color-background-mute: var(--vt-c-white-mute); + + --color-border: var(--vt-c-divider-light-2); + --color-border-hover: var(--vt-c-divider-light-1); + + --color-heading: var(--vt-c-text-light-1); + --color-text: var(--vt-c-text-light-1); + + --section-gap: 160px; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--vt-c-black); + --color-background-soft: var(--vt-c-black-soft); + --color-background-mute: var(--vt-c-black-mute); + + --color-border: var(--vt-c-divider-dark-2); + --color-border-hover: var(--vt-c-divider-dark-1); + + --color-heading: var(--vt-c-text-dark-1); + --color-text: var(--vt-c-text-dark-2); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + transition: + color 0.5s, + background-color 0.5s; + line-height: 1.6; + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + Oxygen, + Ubuntu, + Cantarell, + 'Fira Sans', + 'Droid Sans', + 'Helvetica Neue', + sans-serif; + font-size: 15px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/frontend/src/assets/logo.svg b/frontend/src/assets/logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..7565660356e5b3723c9c33d508b830c9cfbea29f --- /dev/null +++ b/frontend/src/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css new file mode 100644 index 0000000000000000000000000000000000000000..36fb845b5232b8594b0d0f0e61a8cff0b73a4128 --- /dev/null +++ b/frontend/src/assets/main.css @@ -0,0 +1,35 @@ +@import './base.css'; + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + font-weight: normal; +} + +a, +.green { + text-decoration: none; + color: hsla(160, 100%, 37%, 1); + transition: 0.4s; + padding: 3px; +} + +@media (hover: hover) { + a:hover { + background-color: hsla(160, 100%, 37%, 0.2); + } +} + +@media (min-width: 1024px) { + body { + display: flex; + place-items: center; + } + + #app { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 0 2rem; + } +} diff --git a/frontend/src/components/AreaStats.vue b/frontend/src/components/AreaStats.vue new file mode 100644 index 0000000000000000000000000000000000000000..07b74c714ae431f74a636d07d99cf09e7172b308 --- /dev/null +++ b/frontend/src/components/AreaStats.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/frontend/src/components/DebugTimer.vue b/frontend/src/components/DebugTimer.vue new file mode 100644 index 0000000000000000000000000000000000000000..524466457163d0dbc4d960e57db9569328d8bcef --- /dev/null +++ b/frontend/src/components/DebugTimer.vue @@ -0,0 +1,28 @@ + + + diff --git a/frontend/src/components/GameCanvas.vue b/frontend/src/components/GameCanvas.vue new file mode 100644 index 0000000000000000000000000000000000000000..e6defb4e259fd2f5aafa3894cd9f9ec6ade8b729 --- /dev/null +++ b/frontend/src/components/GameCanvas.vue @@ -0,0 +1,577 @@ + + + + + diff --git a/frontend/src/components/GameTimer.vue b/frontend/src/components/GameTimer.vue new file mode 100644 index 0000000000000000000000000000000000000000..b3384f39777003a4725ec29b1c70fda36241abd0 --- /dev/null +++ b/frontend/src/components/GameTimer.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/frontend/src/components/PlayerList.vue b/frontend/src/components/PlayerList.vue new file mode 100644 index 0000000000000000000000000000000000000000..eef52d3bafb2f12bef183eab95a0a5e536e749e8 --- /dev/null +++ b/frontend/src/components/PlayerList.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/frontend/src/components/RoomLobby.vue b/frontend/src/components/RoomLobby.vue new file mode 100644 index 0000000000000000000000000000000000000000..85fc6f10c1f9e262af499575728baea851b40817 --- /dev/null +++ b/frontend/src/components/RoomLobby.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000000000000000000000000000000000000..93c01071aee73ff0dae71f7ae577f71fed3049b6 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,17 @@ +// import './assets/main.css' + +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' + +import App from './App.vue' +import router from './router' + +const app = createApp(App) + +app.use(createPinia()) +app.use(ElementPlus) +app.use(router) + +app.mount('#app') diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000000000000000000000000000000000000..688941e4d32ee764f9bb5caf3673840392ce740a --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,33 @@ +import { createRouter, createWebHistory } from 'vue-router' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/lobby', + name: 'Lobby', + component: () => import('../views/LobbyView.vue'), + }, + { + path: '/room/:roomCode', + name: 'Room', + component: () => import('../views/RoomView.vue'), + }, + { + path: '/game/:roomCode', + name: 'Game', + component: () => import('../views/GameView.vue'), + }, + { + path: '/result/:roomCode', + name: 'Result', + component: () => import('../views/ResultView.vue'), + }, + { + path: '/', + redirect: '/lobby', + }, + ], +}) + +export default router diff --git a/frontend/src/services/adminService.js b/frontend/src/services/adminService.js new file mode 100644 index 0000000000000000000000000000000000000000..e71012ab7a7262b4ddf8fa5da89d39092cdf15ca --- /dev/null +++ b/frontend/src/services/adminService.js @@ -0,0 +1,11 @@ +import { ensureConnection } from './socketService' + +/** + * 房主踢人 + * @param {string} gameId + * @param {string} playerId - 目标玩家 Guid 字符串 + */ +export async function kickPlayer(gameId, playerId){ + const conn = await ensureConnection() + await conn.invoke('KickPlayer', gameId, playerId) +} diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js new file mode 100644 index 0000000000000000000000000000000000000000..b49b7f41d16ab7658b79f89a346e4bf3664f87af --- /dev/null +++ b/frontend/src/services/api.js @@ -0,0 +1,31 @@ +/* + services/api.js + 说明:封装 axios 实例,统一处理后端请求配置(baseURL、超时、拦截器等)。 + 使用说明:导出默认的 axios 实例 `api`,其余 service 模块直接 import 使用。 +*/ +import axios from 'axios' + +// 使用固定的后端基础 URL(开发时默认)。生产环境建议改为 import.meta.env.VITE_API_BASE +const baseURL = 'http://localhost:5115/api' + +// 创建 axios 实例:统一设置 content-type、超时等 +const api = axios.create({ + baseURL, + headers: { + 'Content-Type': 'application/json', + }, + // 可以在此处添加超时、拦截器等公共配置 + timeout: 10000, +}) + +// 响应拦截器:统一处理响应错误 +// 注意:不要在库层直接操作 UI(例如弹窗),只做格式化/上报,交由调用方决定如何提示用户。 +api.interceptors.response.use( + (response) => response, + (error) => { + // 统一抛出错误,便于调用处 catch 并处理(例如显示提示或重试) + return Promise.reject(error) + } +) + +export default api diff --git a/frontend/src/services/authService.js b/frontend/src/services/authService.js new file mode 100644 index 0000000000000000000000000000000000000000..06820270f32b8be87b34f43fa7033e73b44095ec --- /dev/null +++ b/frontend/src/services/authService.js @@ -0,0 +1,17 @@ +import api from './api' + +// 注册新用户 +// 参数:username(String), password(String) +// 返回:后端返回的数据对象(通常包含用户名或 token) +export async function register(username, password) { + const { data } = await api.post('/auth/register', { username, password }) + return data +} + +// 用户登录 +// 参数:username(String), password(String) +// 返回:后端返回的数据对象(通常包含用户名或认证信息) +export async function login(username, password) { + const { data } = await api.post('/auth/login', { username, password }) + return data +} diff --git a/frontend/src/services/gameService.js b/frontend/src/services/gameService.js new file mode 100644 index 0000000000000000000000000000000000000000..b40208c991a91ae67ca2e302adb9f21d87b8bdbf --- /dev/null +++ b/frontend/src/services/gameService.js @@ -0,0 +1,27 @@ +import api from './api' + +// 获取游戏相关静态/配置数据 +// 返回:后端返回的数据对象 +export async function fetchGameData() { + try { + const response = await api.get('/game-data') + return response.data + } catch (error) { + // 在这里记录错误,调用方可以决定如何处理(提示或重试) + console.error('Error fetching game data:', error) + throw error + } +} + +// 提交游戏结果(如需要存档到后端) +// 参数:result(Object) - 结构视后端接口而定 +// 返回:后端保存后的响应数据 +export async function submitGameResult(result) { + try { + const response = await api.post('/game-result', result) + return response.data + } catch (error) { + console.error('Error submitting game result:', error) + throw error + } +} diff --git a/frontend/src/services/roomService.js b/frontend/src/services/roomService.js new file mode 100644 index 0000000000000000000000000000000000000000..9b68c853e869a9112556bf5abaf965e378d97b76 --- /dev/null +++ b/frontend/src/services/roomService.js @@ -0,0 +1,24 @@ +import api from './api' + +// 列出房间(无参) +// 返回:房间数组 +export async function listRooms() { + const { data } = await api.get('/rooms') + return data +} + +// 搜索房间 +// 参数:query(String) +// 返回:匹配的房间数组 +export async function searchRooms(query) { + const { data } = await api.get('/rooms/search', { params: { q: query } }) + return data +} + +// 创建房间 +// 参数:name(String), maxPlayers(Number), durationMinutes(Number), password(String|null) +// 返回:创建成功的房间 DTO +export async function createRoom(name, maxPlayers = 10, durationMinutes = 3, password = null) { + const { data } = await api.post('/rooms', { name, maxPlayers, durationMinutes, password }) + return data +} diff --git a/frontend/src/services/socketService.js b/frontend/src/services/socketService.js new file mode 100644 index 0000000000000000000000000000000000000000..2861019d24eaa5a3d1e3301f1c2e5a4d20d3af5d --- /dev/null +++ b/frontend/src/services/socketService.js @@ -0,0 +1,106 @@ +import * as signalR from '@microsoft/signalr' + +// signalR 连接实例(单例) +let connection = null + +/** + * 确保 SignalR 连接可用并返回连接对象。 + * 参数:baseUrl(String) - SignalR 服务基地址,默认本地开发端口。 + * 返回:已连接的 HubConnection。 + * 注意:若尚未建立连接,会自动 start() 并启用自动重连策略。 + */ +export async function ensureConnection(baseUrl = 'http://localhost:5115') { + if (connection && connection.state === signalR.HubConnectionState.Connected) return connection + if (!connection) { + connection = new signalR.HubConnectionBuilder() + .withUrl(`${baseUrl}/hubs/game`) + .withAutomaticReconnect() + .build() + + connection.onreconnected(() => console.log('Reconnected')) + connection.onclose(() => console.log('Disconnected')) + } + if (connection.state === signalR.HubConnectionState.Disconnected) { + await connection.start() + console.log('SignalR connected') + } + return connection +} + +// 获取当前连接引用(可能为 null) +export function getConnection() { return connection } + +// ----------------- 业务方法 ----------------- +// 下面函数为对 Hub 方法的封装,参数按后端约定传递,均只在 connection 可用时调用。 + +/** + * 加入房间 + * @param {string} gameId - 房间 id + * @param {string} playerName - 玩家昵称 + * @param {string|null} password - 房间密码(可选) + */ +export async function joinRoom(gameId, playerName, password = null) { + const conn = await ensureConnection() + await conn.invoke('JoinRoom', gameId, playerName, password) +} + +/** 离开房间(通知服务器) */ +export async function leaveRoom(gameId) { + if (!connection) return + await connection.invoke('LeaveRoom', gameId) +} + +/** 请求服务器开始游戏 */ +export async function startGame(gameId) { + if (!connection) return + await connection.invoke('StartGame', gameId) +} + +/** 画图回传(已迁移为兼容接口,实际游戏使用 Spray/FireCannon) */ +export async function sendDraw(x, y, size, color) { + if (!connection) return + await connection.invoke('Draw', { x, y, size, color }) +} + +/** 上报移动输入(后端将根据 forward/rotate 计算新位置或用于服务器校验) */ +export async function movePlayer(forward, rotate) { + if (!connection) return + await connection.invoke('Move', forward, rotate) +} + +/** 喷涂(扇形)请求,参数为半径与角宽 */ +export async function spray(radius = 50, angleWidth = 60) { + if (!connection) return + await connection.invoke('Spray', radius, angleWidth) +} + +/** 同步一次状态快照(请求服务器返回玩家/地图等初始数据) */ +export async function syncState(){ + if(!connection) return + await connection.invoke('SyncState') +} + +// —— 新:炮弹、地图 —— +/** 发射炮弹(服务器负责计算命中与爆炸) */ +export async function fireCannon(){ + if (!connection) return + await connection.invoke('FireCannon') +} +/** 请求背包 */ +export async function requestInventory(){ if (!connection) return; await connection.invoke('RequestInventory') } +/** 激活道具 */ +export async function activateItem(type){ if (!connection) return; await connection.invoke('ActivateItem', type) } +/** 请求地图与道具信息 */ +export async function requestMap(){ + if (!connection) return + await connection.invoke('RequestMap') +} + +/** + * 客户端权威上报:用于在进入房间或纠偏时上报绝对坐标与角度。 + * 参数:x(Number), y(Number), angle(Number) + */ +export async function clientSetTransform(x, y, angle){ + if (!connection) return + await connection.invoke('ClientSetTransform', x, y, angle) +} diff --git a/frontend/src/stores/counter.js b/frontend/src/stores/counter.js new file mode 100644 index 0000000000000000000000000000000000000000..89805cce5c2e7296bda254f5a45f18a5be0847bf --- /dev/null +++ b/frontend/src/stores/counter.js @@ -0,0 +1,14 @@ +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' + +// 简单计数器 store(示例/测试用途) +// 导出:count、doubleCount、increment +export const useCounterStore = defineStore('counter', () => { + const count = ref(0) + const doubleCount = computed(() => count.value * 2) + function increment() { + count.value++ + } + + return { count, doubleCount, increment } +}) diff --git a/frontend/src/stores/game.js b/frontend/src/stores/game.js new file mode 100644 index 0000000000000000000000000000000000000000..b759822fffbc0108c5c4fe3f07410b112324438a --- /dev/null +++ b/frontend/src/stores/game.js @@ -0,0 +1,51 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useGameStore = defineStore('game', () => { + const isGameStarted = ref(false) + const timer = ref(0) + const canvasState = ref(null) // 存储画布状态 + const result = ref(null) // 游戏结果 + const gameId = ref(null) + const totalDurationSeconds = ref(0) + const energy = ref(100) + + function startGame(id, durationSeconds) { + gameId.value = id || gameId.value + if (durationSeconds && durationSeconds > 0) { + totalDurationSeconds.value = durationSeconds + timer.value = durationSeconds // 初始化剩余时间,等待服务器同步 + } + isGameStarted.value = true + } + + function stopGame() { + isGameStarted.value = false + } + + function setTimer(seconds) { + const prev = timer.value + timer.value = seconds + try { if (typeof window !== 'undefined') console.debug('[GameStore.setTimer]', prev, '->', seconds) } catch { /* ignore */ } + } + + function updateCanvasState(state) { + canvasState.value = state + } + + function setResult(r) { result.value = r } + + function resetGame() { + isGameStarted.value = false + timer.value = 0 + canvasState.value = null + result.value = null + gameId.value = null + totalDurationSeconds.value = 0 + energy.value = 100 + } + + function setEnergy(v){ energy.value = v } + + return { isGameStarted, timer, totalDurationSeconds, energy, canvasState, result, gameId, startGame, stopGame, setTimer, updateCanvasState, setResult, resetGame, setEnergy } +}) diff --git a/frontend/src/stores/player.js b/frontend/src/stores/player.js new file mode 100644 index 0000000000000000000000000000000000000000..8e0244043e2a03e073dacf446b1560a5d016a943 --- /dev/null +++ b/frontend/src/stores/player.js @@ -0,0 +1,28 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +// Player store:维护房间内玩家列表与本客户端的 self 信息 +// 方法说明:addPlayer/removePlayer/setSelf/setSelfColor +export const usePlayerStore = defineStore('player', () => { + const players = ref([]) + const hostId = ref(null) + const selfId = ref(null) + const selfColor = ref('') + + // 添加玩家(参数:{ id, name, color }) + function addPlayer(player) { + players.value.push(player) + } + + // 根据 player.id 移除玩家 + function removePlayer(playerId) { + players.value = players.value.filter(player => player.id !== playerId) + } + + // 设置本客户端的 player id 与颜色 + function setSelf(id, color){ selfId.value = id; if(color) selfColor.value = color } + + function setSelfColor(color){ selfColor.value = color } + + return { players, hostId, selfId, selfColor, addPlayer, removePlayer, setSelf, setSelfColor } +}) diff --git a/frontend/src/stores/room.js b/frontend/src/stores/room.js new file mode 100644 index 0000000000000000000000000000000000000000..93d46e1300abfab5eb0c3ecc1d68d425f65da0bc --- /dev/null +++ b/frontend/src/stores/room.js @@ -0,0 +1,24 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +// Room store:存储当前房间 id 与是否为房主的标识 +// 方法:setRoomId/setIsHost/startGame(startGame 目前为占位) +export const useRoomStore = defineStore('room', () => { + const roomId = ref(null) + const isHost = ref(false) + + function setRoomId(id) { + roomId.value = id + } + + function setIsHost(host) { + isHost.value = host + } + + function startGame() { + console.log('Game started!') + // Add logic to notify the backend to start the game + } + + return { roomId, isHost, setRoomId, setIsHost, startGame } +}) \ No newline at end of file diff --git a/frontend/src/utils/areaCalculator.js b/frontend/src/utils/areaCalculator.js new file mode 100644 index 0000000000000000000000000000000000000000..a6ff0f9a09d725879e3ee7900ab580104d76763f --- /dev/null +++ b/frontend/src/utils/areaCalculator.js @@ -0,0 +1,21 @@ +// 计算某一颜色像素占比 +// 参数:pixelData (ImageData), playerColor { r, g, b } +// 返回:占比字符串(2 位小数) +export function calculateArea(pixelData, playerColor) { + let totalPixels = 0 + let playerPixels = 0 + + for (let i = 0; i < pixelData.data.length; i += 4) { + const r = pixelData.data[i] + const g = pixelData.data[i + 1] + const b = pixelData.data[i + 2] + + totalPixels++ + + if (r === playerColor.r && g === playerColor.g && b === playerColor.b) { + playerPixels++ + } + } + + return ((playerPixels / totalPixels) * 100).toFixed(2) // 返回百分比 +} diff --git a/frontend/src/utils/canvas.js b/frontend/src/utils/canvas.js new file mode 100644 index 0000000000000000000000000000000000000000..b9d464bb328a33fcffc14fbd1df9feb119687959 --- /dev/null +++ b/frontend/src/utils/canvas.js @@ -0,0 +1,20 @@ +// Canvas 工具函数:对 DOM Canvas 进行常用操作的封装 + +/** 清空画布 */ +export function clearCanvas(canvas) { + const ctx = canvas.getContext('2d') + ctx.clearRect(0, 0, canvas.width, canvas.height) +} + +/** 画矩形 */ +export function drawRectangle(canvas, x, y, width, height, color) { + const ctx = canvas.getContext('2d') + ctx.fillStyle = color + ctx.fillRect(x, y, width, height) +} + +/** 获取画布像素数据(ImageData) */ +export function getCanvasPixelData(canvas) { + const ctx = canvas.getContext('2d') + return ctx.getImageData(0, 0, canvas.width, canvas.height) +} diff --git a/frontend/src/utils/gameLogic.js b/frontend/src/utils/gameLogic.js new file mode 100644 index 0000000000000000000000000000000000000000..afdee667841e828aa2e7d7349e8626c27dc1eb05 --- /dev/null +++ b/frontend/src/utils/gameLogic.js @@ -0,0 +1,17 @@ +// 简单的游戏逻辑工具函数 + +/** + * 判断是否可以涂色 + * currentColor: 玩家当前颜色标识 + * targetColor: 目标格子的颜色标识 + * 规则示例:只能涂空白或自己的颜色 + */ +export function canPaint(currentColor, targetColor) { + // 玩家只能涂空白区域或自己的颜色 + return targetColor === 'white' || targetColor === currentColor +} + +/** 检查是否游戏结束(基于计时器) */ +export function checkGameOver(timer) { + return timer <= 0 +} diff --git a/frontend/src/views/GameView.vue b/frontend/src/views/GameView.vue new file mode 100644 index 0000000000000000000000000000000000000000..16631f373d0c8ae84070b8e494629ef5ddbdfc52 --- /dev/null +++ b/frontend/src/views/GameView.vue @@ -0,0 +1,786 @@ + + + + + diff --git a/frontend/src/views/LobbyView.vue b/frontend/src/views/LobbyView.vue new file mode 100644 index 0000000000000000000000000000000000000000..1c7fea7e43e8d78435043f7a6bb5b65452c17ed7 --- /dev/null +++ b/frontend/src/views/LobbyView.vue @@ -0,0 +1,1075 @@ + + + + + + + + diff --git a/frontend/src/views/ResultView.vue b/frontend/src/views/ResultView.vue new file mode 100644 index 0000000000000000000000000000000000000000..07bc5f2b699c0e67512c5403dad7fe880b4e4ba7 --- /dev/null +++ b/frontend/src/views/ResultView.vue @@ -0,0 +1,230 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/RoomView.vue b/frontend/src/views/RoomView.vue new file mode 100644 index 0000000000000000000000000000000000000000..76592eb439c2229729f5bf3a1c74d51081036471 --- /dev/null +++ b/frontend/src/views/RoomView.vue @@ -0,0 +1,152 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000000000000000000000000000000000000..4217010a3178372181948ce34c4d5045dfa18325 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,18 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueDevTools from 'vite-plugin-vue-devtools' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + vueDevTools(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, + }, +}) diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/01\351\241\271\347\233\256\346\200\235\347\273\264\345\257\274\345\233\276/\346\200\235\347\273\264\345\257\274\345\233\276.md" "b/\346\232\221\346\234\237\351\233\206\350\256\255/01\351\241\271\347\233\256\346\200\235\347\273\264\345\257\274\345\233\276/\346\200\235\347\273\264\345\257\274\345\233\276.md" new file mode 100644 index 0000000000000000000000000000000000000000..a05bd27e2e0d7101a570af6a4a548e19c0307df6 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/01\351\241\271\347\233\256\346\200\235\347\273\264\345\257\274\345\233\276/\346\200\235\347\273\264\345\257\274\345\233\276.md" @@ -0,0 +1 @@ +![](https://lusiyi-picgo.oss-cn-beijing.aliyuncs.com/images/多人实时涂色抢地盘游戏思维导图.png) \ No newline at end of file diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/02\351\241\271\347\233\256\346\225\260\346\215\256\347\273\223\346\236\204\350\256\276\350\256\241/SQLQuery1.sql" "b/\346\232\221\346\234\237\351\233\206\350\256\255/02\351\241\271\347\233\256\346\225\260\346\215\256\347\273\223\346\236\204\350\256\276\350\256\241/SQLQuery1.sql" new file mode 100644 index 0000000000000000000000000000000000000000..ae8ee5603b0a9cc206cef4f3511c1979a14022f1 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/02\351\241\271\347\233\256\346\225\260\346\215\256\347\273\223\346\236\204\350\256\276\350\256\241/SQLQuery1.sql" @@ -0,0 +1,57 @@ +-- �������ݿ� +CREATE DATABASE TerritoryGame; + +-- ʹ�����ݿ� +\c TerritoryGame; + +-- ����ˢ�����Ʊ� +CREATE TABLE game_players ( + game_id UUID NOT NULL, + player_id UUID NOT NULL, + PRIMARY KEY (game_id, player_id), + FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE, + FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE +); + +-- �����Ի��� +CREATE TABLE pixels ( + game_id UUID NOT NULL, + x INT NOT NULL, + y INT NOT NULL, + color VARCHAR(7), + PRIMARY KEY (game_id, x, y), + FOREIGN KEY (game_id) REFERENCES games(id) ON DELETE CASCADE +); + +-- ������ɫ�� +CREATE TABLE games ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(50) NOT NULL, + status VARCHAR(50) DEFAULT 'Waiting', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + started_at TIMESTAMP, + ended_at TIMESTAMP, + max_players INT DEFAULT 6, + duration_minutes INT DEFAULT 3, + password VARCHAR(255), + canvas_width INT DEFAULT 800, + canvas_height INT DEFAULT 600, + neutral_color VARCHAR(7) DEFAULT '#CCCCCC' +); + +-- �����û���ɫ������ +CREATE TABLE players ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + connection_id VARCHAR(255) NOT NULL, + name VARCHAR(50) NOT NULL, + color VARCHAR(7) NOT NULL, + score INT DEFAULT 0 +); + +-- �����û��� +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + username VARCHAR(50) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/02\351\241\271\347\233\256\346\225\260\346\215\256\347\273\223\346\236\204\350\256\276\350\256\241/\350\241\250\347\273\223\346\236\204\350\257\264\346\230\216.xlsx" "b/\346\232\221\346\234\237\351\233\206\350\256\255/02\351\241\271\347\233\256\346\225\260\346\215\256\347\273\223\346\236\204\350\256\276\350\256\241/\350\241\250\347\273\223\346\236\204\350\257\264\346\230\216.xlsx" new file mode 100644 index 0000000000000000000000000000000000000000..9636f175acc7d8d71f5af02acedcdcc65692f7d2 Binary files /dev/null and "b/\346\232\221\346\234\237\351\233\206\350\256\255/02\351\241\271\347\233\256\346\225\260\346\215\256\347\273\223\346\236\204\350\256\276\350\256\241/\350\241\250\347\273\223\346\236\204\350\257\264\346\230\216.xlsx" differ diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/03\351\241\271\347\233\256\344\273\213\347\273\215PPT/\346\266\202\350\211\262\346\212\242\345\234\260\347\233\230\346\270\270\346\210\217\351\241\271\347\233\256\344\273\213\347\273\215.pptx" "b/\346\232\221\346\234\237\351\233\206\350\256\255/03\351\241\271\347\233\256\344\273\213\347\273\215PPT/\346\266\202\350\211\262\346\212\242\345\234\260\347\233\230\346\270\270\346\210\217\351\241\271\347\233\256\344\273\213\347\273\215.pptx" new file mode 100644 index 0000000000000000000000000000000000000000..b53e1fb3669d813fb50da12dd2fe4f3eb35d16ee Binary files /dev/null and "b/\346\232\221\346\234\237\351\233\206\350\256\255/03\351\241\271\347\233\256\344\273\213\347\273\215PPT/\346\266\202\350\211\262\346\212\242\345\234\260\347\233\230\346\270\270\346\210\217\351\241\271\347\233\256\344\273\213\347\273\215.pptx" differ diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/04\351\241\271\347\233\256\346\274\224\347\244\272\350\247\206\351\242\221/8\346\234\21021\346\227\245.mp4" "b/\346\232\221\346\234\237\351\233\206\350\256\255/04\351\241\271\347\233\256\346\274\224\347\244\272\350\247\206\351\242\221/8\346\234\21021\346\227\245.mp4" new file mode 100644 index 0000000000000000000000000000000000000000..c2114a6d92db545f7504d491948e160d0d3b6b11 Binary files /dev/null and "b/\346\232\221\346\234\237\351\233\206\350\256\255/04\351\241\271\347\233\256\346\274\224\347\244\272\350\247\206\351\242\221/8\346\234\21021\346\227\245.mp4" differ diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/05\351\241\271\347\233\256\346\211\213\345\206\214/\351\241\271\347\233\256\344\275\277\347\224\250\346\226\207\346\241\243.md" "b/\346\232\221\346\234\237\351\233\206\350\256\255/05\351\241\271\347\233\256\346\211\213\345\206\214/\351\241\271\347\233\256\344\275\277\347\224\250\346\226\207\346\241\243.md" new file mode 100644 index 0000000000000000000000000000000000000000..81b7c8d252c422c58ca93d321b45853299608463 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/05\351\241\271\347\233\256\346\211\213\345\206\214/\351\241\271\347\233\256\344\275\277\347\224\250\346\226\207\346\241\243.md" @@ -0,0 +1,224 @@ +# 📘 多人实时涂色抢地盘游戏 — 项目使用文档 + +--- + +## 🎮 一、项目简介 + +### 项目名称: +**多人实时涂色抢地盘游戏** + +### 项目类型: +基于 Web 的多人实时竞技休闲游戏 + +### 游戏玩法: +多名玩家(2~6人)在同一画布上使用各自分配的专属颜色进行涂色,**不能覆盖其他玩家已涂区域**,最终根据**占领画布面积最大**的玩家来决定胜负。 + +--- + +## 🧩 二、核心特色 + +| 特性 | 说明 | +|------|------| +| 🎨 **实时涂色** | 多人同时在画布上绘制,实时同步绘制内容 | +| 🏆 **竞技排名** | 根据玩家占领画布的面积进行排名,面积最大者获胜 | +| 🤝 **多人协作/竞技** | 支持 2~6 名玩家同场竞技,增强互动性与趣味性 | +| 🛡️ **策略机制** | 不能覆盖他人颜色,有边界保护、冷却机制,防止恶意刷屏 | +| 📱 **实时同步** | 所见即所得,所有玩家的绘制内容实时展示给其他玩家 | +| 🎮 **简单易上手** | 无需复杂操作,选择颜色即可涂色,适合休闲娱乐 | + +--- + +## 🏗️ 三、技术架构 + +### 📱 前端技术栈 + +| 技术 | 说明 | +|------|------| +| **框架** | Vue 3 + Composition API | +| **UI组件库** | https://element-plus.org/ | +| **状态管理** | https://pinia.vuejs.org/ | +| **绘图引擎** | 原生 HTML5 Canvas API | +| **实时通信** | https://socket.io/docs/v4/client-installation/(或与后端 SignalR 对接) | +| **构建工具** | Vite(Vue 默认脚手架) | + +### ⚙️ 后端技术栈 + +| 技术 | 说明 | +|------|------| +| **框架** | ASP.NET Core 8(使用 Minimal APIs) | +| **实时通信** | https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction | +| **数据存储** | + - **Redis**:存储游戏会话、房间信息、实时状态 + - **PostgreSQL**:持久化存储游戏记录、用户数据、排行榜 | +| **架构模式** | 简洁 DDD(领域驱动设计)+ CQRS(可选) | + +--- + +## 🎯 四、游戏规则 + +### 🎮 基础规则 + +| 项目 | 说明 | +|------|------| +| **玩家人数** | 2 ~ 6 人 | +| **游戏时长** | 总时长约 3~5 分钟(准备 30s + 游戏 3~5m + 结算 30s) | +| **胜利条件** | 游戏结束时,**占领画布面积最大**的玩家获胜 | +| **涂色规则** | 每位玩家拥有**专属颜色**,只能涂自己的颜色,**不能覆盖其他玩家已涂区域** | +| **画布机制** | 画布有**边界保护**(边缘有中性区域),有**最小涂色单位**与**绘制冷却时间**,防止过度竞争 | + +--- + +### 📋 游戏流程 + +#### 1. 准备阶段(30秒) +- 玩家进入房间,系统为每人分配一个**专属颜色** +- 显示倒计时,提示玩家准备 +- 玩家不可进行绘制 + +#### 2. 游戏阶段(3~5分钟) +- 所有玩家开始在画布上自由涂色 +- 实时显示各玩家的**占领面积** +- 不能覆盖其他玩家已涂色区域 +- 实时同步所有玩家的绘制动作 + +#### 3. 结算阶段(30秒) +- 游戏结束,统计每个玩家的**最终占领面积** +- 按面积大小进行**排名** +- 展示**获胜者**与排行榜 +- 展示**最终画布效果** + +--- + +### ⚙️ 特殊机制 + +| 机制 | 说明 | +|------|------| +| **边界保护** | 画布边缘有一部分为中性区域,防止先手玩家占据过大优势 | +| **最小涂色单位** | 设置最小笔刷大小,避免过于精细的像素级争夺 | +| **绘制冷却** | 每次绘制有短暂冷却(如 0.1 秒),防止恶意刷屏 | +| **区域锁定** | 已被涂色区域不可被覆盖(颜色隔离) | + +--- + + +## 🧩 五、开发与部署 + +### 📦 前端开发 + +- **运行方式**:使用 `pnpm run dev` 启动本地开发服务器 +- **构建方式**:使用 `pnpm run build` 打包生产版本 +- **部署**:可部署至 Vercel、Netlify、静态托管服务等 + +### ⚙️ 后端开发 + +- **运行方式**:使用 `dotnet watch` 启动 ASP.NET Core 服务 +- **数据库**:Redis(缓存/实时数据)、PostgreSQL(持久化) +- **部署**:可部署至云服务(如 Azure、AWS、阿里云)、Docker 容器、Linux 服务器等 + +--- + +## 🎮 六、玩家操作指南 + +### 📥 进入游戏 + +1. 打开游戏网页 +2. 输入房间号,或由房主创建房间后获取房间号 +3. 加入房间,等待其他玩家 +4. 准备阶段倒计时结束后,进入游戏 + +### 🖌️ 游戏中 + +- 在画布区域使用鼠标左键进行涂色 +- 每位玩家只能使用自己的专属颜色 +- 不能覆盖其他玩家已经涂过的区域 +- 实时查看排名与占领面积 + +### 🏆 游戏结束 + +- 游戏结束后自动展示: + - 各玩家占领面积与排名 + - 获胜者信息 + - 最终画布效果图 + +--- + +## 🤝 七、多人联机说明 + +- 游戏采用 **实时通信技术**(SignalR / Socket.io) +- 所有玩家的绘制内容将**实时同步**到其他玩家画布 +- 房间内所有玩家处于同一游戏状态,包括倒计时、绘图、排名 + +--- + +## 📂 八、项目目录结构(前端示例) + +``` +src/ +├── components/ +│ ├── GameCanvas.vue # 画布组件 +│ ├── GameResult.vue # 游戏结果展示组件 +│ ├── RoomJoin.vue # 加入房间组件 +│ ├── RoomCreate.vue # 创建房间组件 +│ └── Timer.vue # 倒计时组件 +├── views/ +│ ├── GameRoom.vue # 游戏主界面 +│ └── Lobby.vue # 游戏大厅 +├── stores/ +│ └── game.js # Pinia 状态管理 +├── utils/ +│ └── socket.js # Socket.io 或 SignalR 客户端 +├── App.vue +└── main.js +``` + +--- + +## 🔌 九、实时通信协议(简要) + +| 消息类型 | 方向 | 说明 | +|---------|------|------| +| 玩家加入房间 | 前端 → 后端 | 加入指定房间 | +| 玩家绘制动作 | 前端 → 后端 | 发送绘制点坐标与颜色 | +| 绘制同步 | 后端 → 前端 | 广播其他玩家的绘制动作 | +| 倒计时更新 | 后端 → 前端 | 推送各阶段倒计时 | +| 游戏状态变更 | 后端 → 前端 | 准备 → 游戏 → 结算 | +| 游戏结果 | 后端 → 前端 | 推送排名、获胜者、面积统计 | + +--- + +## 📞 十、技术支持与扩展 + +### 常见问题 + +- Q: 能否覆盖别人颜色? + A: 不能,系统禁止覆盖其他玩家已涂区域。 + +- Q: 如何保证绘制同步? + A: 通过实时通信(SignalR / Socket.io)将每位玩家的绘制操作广播给其他人。 + +- Q: 可以部署到手机浏览器吗? + A: 可以,游戏基于 Web 技术,支持 PC 与移动端浏览器。 + +--- + +## ✅ 十一、总结 + +**多人实时涂色抢地盘游戏**是一款简单有趣、玩法直观、支持实时多人互动的休闲竞技游戏。通过 Web 技术栈与实时通信能力,为玩家提供流畅的对战与协作体验。 + +--- + +🎯 **适合场景:** +- 朋友对战 +- 团队休闲 +- 课堂/聚会小游戏 +- Web 游戏原型与展示 + +--- + +🚀 **技术亮点:** +- 实时多人绘制同步 +- Canvas 实时渲染 +- 房间与状态管理 +- 响应式 UI 与交互 + +--- \ No newline at end of file diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Controllers/AuthController.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Controllers/AuthController.cs" new file mode 100644 index 0000000000000000000000000000000000000000..8673a2b764ddc36fa96846025f50e19368c735cd --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Controllers/AuthController.cs" @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using TerritoryGame.Infrastructure.Data; +using TerritoryGame.Domain.Models; +using TerritoryGame.Infrastructure.Security; + +namespace TerritoryGame.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AuthController : ControllerBase +{ + private readonly TerritoryGameDbContext _db; + public AuthController(TerritoryGameDbContext db) => _db = db; + + public record RegisterRequest(string Username, string Password); + public record LoginRequest(string Username, string Password); + + [HttpPost("register")] + public async Task Register(RegisterRequest req) + { + if (string.IsNullOrWhiteSpace(req.Username) || string.IsNullOrWhiteSpace(req.Password)) + return BadRequest("用户名或密码不能为空"); + if (await _db.Users.AnyAsync(u => u.Username == req.Username)) + return Conflict("用户名已存在"); + var user = new User + { + Id = Guid.NewGuid(), + Username = req.Username, + PasswordHash = PasswordHasher.Hash(req.Password) + }; + _db.Users.Add(user); + await _db.SaveChangesAsync(); + return Ok(new { user.Id, user.Username }); + } + + [HttpPost("login")] + public async Task Login(LoginRequest req) + { + var user = await _db.Users.FirstOrDefaultAsync(u => u.Username == req.Username); + if (user == null) return Unauthorized("用户名或密码错误"); + if (!PasswordHasher.Verify(req.Password, user.PasswordHash)) return Unauthorized("用户名或密码错误"); + return Ok(new { user.Id, user.Username }); + } +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Controllers/RoomsController.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Controllers/RoomsController.cs" new file mode 100644 index 0000000000000000000000000000000000000000..288f610565109401951a7733fa8a1c20ad16a97a --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Controllers/RoomsController.cs" @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using TerritoryGame.Application.Services; +using TerritoryGame.API.Hubs; +using System; +using System.Linq; + +namespace TerritoryGame.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class RoomsController : ControllerBase +{ + private readonly IGameManager _gameManager; + private readonly IHubContext _hubContext; + public RoomsController(IGameManager gameManager, IHubContext hubContext) + { + _gameManager = gameManager; + _hubContext = hubContext; + } + + [HttpGet] + public IActionResult List() => Ok(_gameManager.ListGames()); + + public record CreateRoomRequest(string Name, int MaxPlayers = 6, int DurationMinutes = 3, string? Password = null); + + // 按房间号/名称搜索(优先精确匹配,找不到则退化为包含匹配) + [HttpGet("search")] + public IActionResult Search([FromQuery] string q) + { + var all = _gameManager.ListGames(); + if (string.IsNullOrWhiteSpace(q)) return Ok(all); + var exact = all.Where(r => string.Equals(r.Code ?? r.Name, q, StringComparison.OrdinalIgnoreCase)); + var result = exact.Any() ? exact : all.Where(r => (r.Code ?? r.Name)?.Contains(q, StringComparison.OrdinalIgnoreCase) == true); + return Ok(result); + } + + [HttpPost] + public async Task Create(CreateRoomRequest req) + { + var dto = await _gameManager.CreateGameAsync(req.Name, req.MaxPlayers, req.DurationMinutes, req.Password); + // Create 阶段还没有玩家加入,HostPlayerName 为空,保持广播,后续 Join 会通过 OnRoomSnapshot 带来房主名称 + await _hubContext.Clients.All.SendAsync("OnRoomCreated", dto); + return Ok(dto); + } +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Hubs/GameHub.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Hubs/GameHub.cs" new file mode 100644 index 0000000000000000000000000000000000000000..4fc2e50977f88f00d42e61bc812a4ed72fd9bb52 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Hubs/GameHub.cs" @@ -0,0 +1,504 @@ +using Microsoft.AspNetCore.SignalR; +using System.Collections.Concurrent; +using TerritoryGame.Application.Services; +using TerritoryGame.Application.Common; + +namespace TerritoryGame.API.Hubs; + +public class GameHub : Hub +{ + private readonly IGameManager _gameManager; + private readonly IHubContext _hubContext; // 用于后台计时广播,避免 Hub 实例释放后失效 + private static readonly ConcurrentDictionary _lastMoveAt = new(); + + public GameHub(IGameManager gameManager, IHubContext hubContext) + { + _gameManager = gameManager; + _hubContext = hubContext; + } + + public async Task JoinRoom(string gameId, string playerName, string? password = null) + { + try + { + var resolved = _gameManager.ResolveGameId(gameId); + // 如果还不存在并且 gameId 是 Guid 形式则按 Guid 解析,否则按名称解析失败即报错 + if (resolved == null) + { + // 尝试 Guid 解析(兼容原逻辑) + if (Guid.TryParse(gameId, out var parsed) && _gameManager.GetGameSnapshot(parsed) != null) + { + resolved = parsed; + } + else + { + throw new Exception("房间不存在"); + } + } + var player = await _gameManager.AddPlayerAsync(resolved.Value, playerName, Context.ConnectionId, password); + var groupKey = resolved.Value.ToString(); + await Groups.AddToGroupAsync(Context.ConnectionId, groupKey); + await Clients.Group(groupKey).SendAsync("OnPlayerJoined", new { player.Id, player.Name, player.Color, connectionId = Context.ConnectionId }); + // 告知调用者自己的玩家信息(用于本地 UI 颜色显示等) + await Clients.Caller.SendAsync("OnSelf", new { player.Id, player.Name, player.Color }); + var count = _gameManager.GetPlayerCount(resolved.Value); + await Clients.Group(groupKey).SendAsync("OnPlayerCount", count); + // 向所有客户端广播该房间人数变化(未加入房间的页面也能更新列表) + await Clients.All.SendAsync("OnPlayerCountChanged", new { gameId, count }); + var snapshot = _gameManager.GetGameSnapshot(resolved.Value); + if (snapshot != null) + { + // 组内用于房内 UI,同步;对所有人广播用于大厅列表(更新房主名/状态/人数等) + await Clients.Group(groupKey).SendAsync("OnRoomSnapshot", snapshot); + await Clients.All.SendAsync("OnRoomSnapshot", snapshot); + } + // 下发地图与道具与隐身区域 + var snapshot2 = _gameManager.GetGameSnapshot(resolved.Value); + await Clients.Caller.SendAsync("OnMap", new { width = snapshot2?.CanvasWidth, height = snapshot2?.CanvasHeight, walls = _gameManager.GetWalls(resolved.Value), items = _gameManager.GetItems(resolved.Value), zones = _gameManager.GetStealthZones(resolved.Value) }); + await SendAreaStats(groupKey); + } + catch (Exception ex) + { + await Clients.Caller.SendAsync("JoinFailed", new { message = ex.Message }); + } + } + + // 仅查看房间详情时请求一次快照 + public Task RequestRoomSnapshot(string gameId) + { + var resolved = _gameManager.ResolveGameId(gameId); + if (resolved == null) return Task.CompletedTask; + var snapshot = _gameManager.GetGameSnapshot(resolved.Value); + if (snapshot != null) + { + return Clients.Caller.SendAsync("OnRoomSnapshot", snapshot); + } + return Task.CompletedTask; + } + + public async Task LeaveRoom(string gameId) + { + var resolved = _gameManager.ResolveGameId(gameId); + if (resolved == null) return; + var result = await _gameManager.RemovePlayerAsync(resolved.Value, Context.ConnectionId); + var groupKey = resolved.Value.ToString(); + await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupKey); + _lastMoveAt.TryRemove(Context.ConnectionId, out _); // 清理频控缓存,防止重连后沿用旧时间 + if (result.Success) + { + if (result.GameDeleted) + { + // 房间被删除(例如房主离开时),通知所有客户端 + await Clients.All.SendAsync("OnRoomDeleted", new { gameId }); + } + else + { + await Clients.Group(groupKey).SendAsync("OnPlayerLeft", new { connectionId = Context.ConnectionId, playerId = result.RemovedPlayerId, wasHost = result.WasHost }); + var count = _gameManager.GetPlayerCount(resolved.Value); + await Clients.Group(groupKey).SendAsync("OnPlayerCount", count); + await Clients.All.SendAsync("OnPlayerCountChanged", new { gameId, count }); + var snapshot = _gameManager.GetGameSnapshot(resolved.Value); + if (snapshot != null) + { + await Clients.Group(groupKey).SendAsync("OnRoomSnapshot", snapshot); + await Clients.All.SendAsync("OnRoomSnapshot", snapshot); + } + await SendAreaStats(groupKey); + } + } + } + + public async Task StartGame(string gameId) + { + var resolved = _gameManager.ResolveGameId(gameId); + if (resolved == null) + { + await Clients.Caller.SendAsync("OnStartFailed", new { gameId, reason = "房间不存在" }); + return; + } + // 更明确的失败原因:人数不足 + if (_gameManager.GetPlayerCount(resolved.Value) < 2) + { + await Clients.Caller.SendAsync("OnStartFailed", new { gameId, reason = "至少两名玩家才能开始" }); + return; + } + var started = await _gameManager.StartGameAsync(resolved.Value); + if (started) + { + var groupKey = resolved.Value.ToString(); + await Clients.Group(groupKey).SendAsync("OnGameStarted", new { gameId }); + var snapshot = _gameManager.GetGameSnapshot(resolved.Value); + if (snapshot != null) + { + await Clients.Group(groupKey).SendAsync("OnRoomSnapshot", snapshot); + await Clients.All.SendAsync("OnRoomSnapshot", snapshot); + } + var remain = _gameManager.GetRemainingSeconds(resolved.Value); + await Clients.Group(groupKey).SendAsync("OnTimerUpdate", remain); + // 启动后台计时:使用加入房间时的 group key(即传入的 gameId 字符串,不强制换成 Guid.ToString()) + _ = RunTimer(resolved.Value, groupKey); + } + else + { + await Clients.Caller.SendAsync("OnStartFailed", new { gameId, reason = "已开始或无法开始" }); + } + } + + public async Task Draw(DrawCommandDto cmd) + { + var gameId = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gameId == null) return; + var update = await _gameManager.ProcessDrawAsync(gameId.Value, Context.ConnectionId, cmd.X, cmd.Y, cmd.Size, cmd.Color); + if (update) + { + await Clients.OthersInGroup(gameId.Value.ToString()).SendAsync("OnPaintActionReceived", new + { + playerConnectionId = Context.ConnectionId, + x = cmd.X, + y = cmd.Y, + color = cmd.Color, + size = cmd.Size + }); + await SendAreaStats(gameId.Value.ToString()); + } + } + + // 查询自己的背包 + public Task RequestInventory() + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return Task.CompletedTask; + var inv = _gameManager.GetInventory(gid.Value, Context.ConnectionId); + return Clients.Caller.SendAsync("OnInventory", inv); + } + + // 激活道具:type in [energy, stealth, speed, noclip, bigbang] + public Task ActivateItem(string type) + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return Task.CompletedTask; + var (ok, energy, inv) = _gameManager.ActivateItem(gid.Value, Context.ConnectionId, type); + _ = Clients.Caller.SendAsync("OnEnergy", energy); + // 同步背包 + _ = Clients.Caller.SendAsync("OnInventory", inv); + // 同步自身状态(包含 stealthUntil/speedUntil/noclipUntil/bigBangUntil),用于更新前端剩余时间显示 + var self = _gameManager.GetSelfState(gid.Value, Context.ConnectionId); + if (self != null) + { + _ = Clients.Caller.SendAsync("OnPlayerMoved", new { moved = self, energy }); + } + return Task.CompletedTask; + } + + // 新:移动(forward -1/0/1, rotate -1/0/1) + public Task Move(int forward, int rotate) + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return Task.CompletedTask; + // 频率限制:每连接 >=60ms + var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + var last = _lastMoveAt.GetOrAdd(Context.ConnectionId, 0); + if (now - last < 50) return Task.CompletedTask; + _lastMoveAt[Context.ConnectionId] = now; + var (ok, moved, energy, itemsChanged) = _gameManager.Move(gid.Value, Context.ConnectionId, forward, rotate); + if (ok && moved != null) + { + _ = Clients.Caller.SendAsync("OnEnergy", energy); // 仅调用者更新能量条 + // 仅在复活时把权威位置回显给调用者,避免平时重复回显导致本地抖动 + bool respawned = false; + try + { + var prop = moved.GetType().GetProperty("respawned"); + if (prop != null) + { + var val = prop.GetValue(moved); + if (val is bool b) respawned = b; + else if (val is string s) respawned = string.Equals(s, "true", StringComparison.OrdinalIgnoreCase); + } + } + catch { } + if (respawned) + { + _ = Clients.Caller.SendAsync("OnPlayerMoved", new { moved, energy }); + } + if (itemsChanged) + { + _ = Clients.Group(gid.Value.ToString()).SendAsync("OnItems", _gameManager.GetItems(gid.Value)); + // 给拾取者同步背包 + try + { + var inv = _gameManager.GetInventory(gid.Value, Context.ConnectionId); + _ = Clients.Caller.SendAsync("OnInventory", inv); + } + catch { } + } + // 给其他人广播;不给 Caller 再发一次,避免本地卡顿 + return Clients.OthersInGroup(gid.Value.ToString()).SendAsync("OnPlayerMoved", new { moved, energy }); + } + return Task.CompletedTask; + } + + // 新:客户端权威设置位置/角度(x,y 为世界坐标像素,angle 为弧度) + public Task ClientSetTransform(int x, int y, double angle) + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return Task.CompletedTask; + // 频率限制:每连接 >=60ms + var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + var last = _lastMoveAt.GetOrAdd(Context.ConnectionId, 0); + if (now - last < 50) return Task.CompletedTask; + _lastMoveAt[Context.ConnectionId] = now; + var (ok, moved, energy, itemsChanged) = _gameManager.ClientSetTransform(gid.Value, Context.ConnectionId, x, y, angle); + if (ok && moved != null) + { + _ = Clients.Caller.SendAsync("OnEnergy", energy); + // 仅在复活时把权威位置回显给调用者,避免平时重复回显导致本地抖动 + bool respawned = false; + try + { + var prop = moved.GetType().GetProperty("respawned"); + if (prop != null) + { + var val = prop.GetValue(moved); + if (val is bool b) respawned = b; + else if (val is string s) respawned = string.Equals(s, "true", StringComparison.OrdinalIgnoreCase); + } + } + catch { } + if (respawned) + { + _ = Clients.Caller.SendAsync("OnPlayerMoved", new { moved, energy }); + } + if (itemsChanged) + { + _ = Clients.Group(gid.Value.ToString()).SendAsync("OnItems", _gameManager.GetItems(gid.Value)); + // 给拾取者同步背包 + try + { + var inv = _gameManager.GetInventory(gid.Value, Context.ConnectionId); + _ = Clients.Caller.SendAsync("OnInventory", inv); + } + catch { } + } + // 仅向其他人广播,避免 Caller 自己收到重复 OnPlayerMoved 导致抖动 + return Clients.OthersInGroup(gid.Value.ToString()).SendAsync("OnPlayerMoved", new { moved, energy }); + } + return Task.CompletedTask; + } + + // 新:喷涂(前方扇形) + public async Task Spray(int radius, int angleWidth) + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return; + var (ok, energy, sector, stats, color) = _gameManager.Spray(gid.Value, Context.ConnectionId, radius, angleWidth, allowOverwrite: true); + if (!ok) { await Clients.Caller.SendAsync("OnEnergy", energy); return; } + await Clients.Group(gid.Value.ToString()).SendAsync("OnSpray", new { sector, radius, angleWidth, color }); + await Clients.Caller.SendAsync("OnEnergy", energy); + if (stats != null) + { + var mapped = stats.Select(s => new { playerId = s.PlayerId, playerName = s.PlayerName, color = s.Color, area = s.AreaPercent }); + await Clients.Group(gid.Value.ToString()).SendAsync("UpdateAreaStats", mapped); + } + } + + // 新:开火(炮弹爆炸,不规则涂抹 + 击杀 + 能量消耗) + public async Task FireCannon() + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return; + var (ok, energy, affected, explosion, killed, color, killFeed) = _gameManager.FireCannon(gid.Value, Context.ConnectionId); + if (!ok) { await Clients.Caller.SendAsync("OnEnergy", energy); return; } + // 弹道:起点->终点(客户端可做飞行动画) + if (explosion != null) + { + var ox = (int)explosion.GetType().GetProperty("ox")!.GetValue(explosion)!; + var oy = (int)explosion.GetType().GetProperty("oy")!.GetValue(explosion)!; + var ex = (int)explosion.GetType().GetProperty("x")!.GetValue(explosion)!; + var ey = (int)explosion.GetType().GetProperty("y")!.GetValue(explosion)!; + await Clients.Group(gid.Value.ToString()).SendAsync("OnProjectile", new { ox, oy, x = ex, y = ey, color }); + // 小延迟,等待前端绘制弹道 + await Task.Delay(90); + } + // 仅发送圆心与半径,减少广播数据量;客户端自行填充像素与渲染 + if (explosion != null) + { + var ex = (int)explosion.GetType().GetProperty("x")!.GetValue(explosion)!; + var ey = (int)explosion.GetType().GetProperty("y")!.GetValue(explosion)!; + var outer = (int)explosion.GetType().GetProperty("outer")!.GetValue(explosion)!; + await Clients.Group(gid.Value.ToString()).SendAsync("OnExplosion", new { x = ex, y = ey, r = outer, color, killed }); + } + await Clients.Caller.SendAsync("OnEnergy", energy); + // 击杀信息广播(含是否掉落道具) + if (killFeed != null) + { + await Clients.Group(gid.Value.ToString()).SendAsync("OnKillFeed", killFeed); + } + // 若因击杀获得道具,调用者背包已变化;这里直接同步一次背包(代价很小,省去服务端判断) + try + { + var inv = _gameManager.GetInventory(gid.Value, Context.ConnectionId); + await Clients.Caller.SendAsync("OnInventory", inv); + } + catch { } + // 新增:广播分数榜 + var scores = _gameManager.GetScores(gid.Value); + await Clients.Group(gid.Value.ToString()).SendAsync("OnScores", scores); + // 道具清单可能在移动发生变化,这里不刷新 + await SendAreaStats(gid.Value.ToString()); + } + + // 主动请求地图(墙体与当前道具) + public Task RequestMap() + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return Task.CompletedTask; + var snapshot = _gameManager.GetGameSnapshot(gid.Value); + return Clients.Caller.SendAsync("OnMap", new { width = snapshot?.CanvasWidth, height = snapshot?.CanvasHeight, walls = _gameManager.GetWalls(gid.Value), items = _gameManager.GetItems(gid.Value), zones = _gameManager.GetStealthZones(gid.Value) }); + } + + // 请求当前所有玩家运行时状态(位置/角度) + public Task SyncState() + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return Task.CompletedTask; + var states = _gameManager.GetPlayerStates(gid.Value); + return Clients.Caller.SendAsync("OnPlayerStates", states); + } + + private async Task SendAreaStats(string gameId) + { + var stats = _gameManager.GetAreaStats(Guid.Parse(gameId)) + .Select(s => new { playerId = s.PlayerId, playerName = s.PlayerName, color = s.Color, area = s.AreaPercent }); + await Clients.Group(gameId).SendAsync("UpdateAreaStats", stats); + } + + // 客户端可主动请求一次最新统计(例如进入游戏页时) + public Task RequestAreaStats(string gameId) + { + return SendAreaStats(gameId); + } + + // 客户端主动请求一次分数榜 + public Task RequestScores(string gameId) + { + var resolved = _gameManager.ResolveGameId(gameId); + if (resolved == null) return Task.CompletedTask; + var scores = _gameManager.GetScores(resolved.Value); + return Clients.Caller.SendAsync("OnScores", scores); + } + + // 主动补偿获取自身信息 + public Task GetSelf() + { + var dto = _gameManager.GetPlayerByConnection(Context.ConnectionId); + if (dto != null) + return Clients.Caller.SendAsync("OnSelf", new { dto.Id, dto.Name, dto.Color }); + return Task.CompletedTask; + } + + private async Task RunTimer(Guid gameGuid, string groupKey) + { + try + { + while (true) + { + var remaining = _gameManager.GetRemainingSeconds(gameGuid); + await _hubContext.Clients.Group(groupKey).SendAsync("OnTimerUpdate", remaining); + // 计时器驱动:检查并刷新道具,有新增则广播 + try + { + if (_gameManager.CheckAndSpawnItems(gameGuid)) + { + var items = _gameManager.GetItems(gameGuid); + await _hubContext.Clients.Group(groupKey).SendAsync("OnItems", items); + } + } + catch { } + if (remaining <= 0) + { + var result = _gameManager.GetResult(gameGuid); + await _hubContext.Clients.Group(groupKey).SendAsync("OnGameEnded", result); + break; + } + await Task.Delay(1000); + } + } + catch (Exception ex) + { + Console.WriteLine($"[RunTimer][Error] {gameGuid} {ex.Message}"); + } + } + + // 客户端可单独请求一次当前剩余时间(防止刷新后未收到 StartGame 初始推送) + public Task RequestTimer(string gameId) + { + var resolved = _gameManager.ResolveGameId(gameId); + if (resolved == null) return Task.CompletedTask; + var remain = _gameManager.GetRemainingSeconds(resolved.Value); + return Clients.Caller.SendAsync("OnTimerUpdate", remain); + } + + // 客户端补偿请求自身信息(如首次 OnSelf 丢失) + public Task RequestSelf() + { + var gid = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gid == null) return Task.CompletedTask; + var snapshot = _gameManager.GetGameSnapshot(gid.Value); + if (snapshot == null) return Task.CompletedTask; + var player = snapshot.Players.FirstOrDefault(p => p != null && p.Id != Guid.Empty && _gameManager.GetGameIdByConnection(Context.ConnectionId) == gid); + // 上面方式拿不到 connection => 直接通过内部结构(简化:重新查 runtime PlayerId 映射) + // 为避免访问内部状态,可重新调用 AddPlayer 时已存储映射,这里简单返回颜色集合中第一个匹配颜色(退化方案) + // 这里直接忽略复杂映射,仅返回全部玩家列表以供客户端定位 + return Clients.Caller.SendAsync("OnSelfSnapshot", new { players = snapshot.Players }); + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + var gameId = _gameManager.GetGameIdByConnection(Context.ConnectionId); + if (gameId != null) + { + await LeaveRoom(gameId.Value.ToString()); + } + _lastMoveAt.TryRemove(Context.ConnectionId, out _); // 清理频控 + await base.OnDisconnectedAsync(exception); + } + + // 房主踢人 + public async Task KickPlayer(string gameId, Guid targetPlayerId) + { + var resolved = _gameManager.ResolveGameId(gameId); + if (resolved == null) { await Clients.Caller.SendAsync("OnKickResult", new { ok = false, reason = "房间不存在" }); return; } + var (ok, removedId, removedConn, gameDeleted, isHost, reason) = _gameManager.KickPlayer(resolved.Value, Context.ConnectionId, targetPlayerId); + if (!ok) + { + await Clients.Caller.SendAsync("OnKickResult", new { ok, reason = reason ?? "失败" }); + return; + } + var groupKey = resolved.Value.ToString(); + if (gameDeleted) + { + await Clients.All.SendAsync("OnRoomDeleted", new { gameId }); + if (!string.IsNullOrEmpty(removedConn)) _lastMoveAt.TryRemove(removedConn, out _); + return; + } + if (!string.IsNullOrEmpty(removedConn)) + { + await Groups.RemoveFromGroupAsync(removedConn, groupKey); + _lastMoveAt.TryRemove(removedConn, out _); + await Clients.Client(removedConn).SendAsync("OnKicked", new { gameId }); + } + await Clients.Group(groupKey).SendAsync("OnPlayerKicked", new { playerId = removedId }); + var count = _gameManager.GetPlayerCount(resolved.Value); + await Clients.Group(groupKey).SendAsync("OnPlayerCount", count); + await Clients.All.SendAsync("OnPlayerCountChanged", new { gameId, count }); + var snapshot = _gameManager.GetGameSnapshot(resolved.Value); + if (snapshot != null) + { + await Clients.Group(groupKey).SendAsync("OnRoomSnapshot", snapshot); + await Clients.All.SendAsync("OnRoomSnapshot", snapshot); + } + await SendAreaStats(groupKey); + await Clients.Caller.SendAsync("OnKickResult", new { ok = true }); + } +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Program.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Program.cs" new file mode 100644 index 0000000000000000000000000000000000000000..ce6590a60b32725407e9638a378d5b137ec9dbf7 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Program.cs" @@ -0,0 +1,48 @@ +using Microsoft.EntityFrameworkCore; +using TerritoryGame.Infrastructure; +using TerritoryGame.Application; +using TerritoryGame.Infrastructure.Data; +using TerritoryGame.API.Services; + +var builder = WebApplication.CreateBuilder(args); + +// 添加服务到容器 +builder.Services.AddInfrastructure(builder.Configuration); +builder.Services.AddApplicationServices(); +builder.Services.AddSignalR(); +builder.Services.AddControllers(); +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.AllowAnyHeader().AllowAnyMethod().AllowCredentials().SetIsOriginAllowed(_ => true); + }); +}); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddHostedService(); + +var app = builder.Build(); + +// 关闭启动时自动迁移(避免 PendingModelChanges 阻塞启动) +// 如需迁移,请手动执行 dotnet-ef 或在 CI 中进行 + + +// 配置中间件 +// app.MapHub("/gameHub"); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseCors(); + +app.MapHub("/hubs/game"); +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Properties/launchSettings.json" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Properties/launchSettings.json" new file mode 100644 index 0000000000000000000000000000000000000000..a8ed14b3d10f76216330b9d60ff77ae278ea39f5 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Properties/launchSettings.json" @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:60278", + "sslPort": 44338 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5115", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7219;http://localhost:5115", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Services/IdleRoomCleanupService.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Services/IdleRoomCleanupService.cs" new file mode 100644 index 0000000000000000000000000000000000000000..210bda15c1eb5f01e8369cf200147d54d8ca57f5 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/Services/IdleRoomCleanupService.cs" @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using TerritoryGame.API.Hubs; +using TerritoryGame.Application.Services; + +namespace TerritoryGame.API.Services; + +public class IdleRoomCleanupService : BackgroundService +{ + private readonly ILogger _logger; + private readonly IGameManager _gameManager; + private readonly IHubContext _hubContext; + private readonly TimeSpan _period = TimeSpan.FromMinutes(1); + private readonly TimeSpan _idle = TimeSpan.FromMinutes(30); + + public IdleRoomCleanupService(ILogger logger, IGameManager gameManager, IHubContext hubContext) + { + _logger = logger; + _gameManager = gameManager; + _hubContext = hubContext; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + var removed = _gameManager.CleanupIdleRooms(_idle).ToList(); + foreach (var id in removed) + { + await _hubContext.Clients.All.SendAsync("OnRoomDeleted", new { gameId = id.ToString() }, cancellationToken: stoppingToken); + _logger.LogInformation("Idle room deleted: {GameId}", id); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "IdleRoomCleanup error"); + } + await Task.Delay(_period, stoppingToken); + } + } +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/TerritoryGame.API.csproj" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/TerritoryGame.API.csproj" new file mode 100644 index 0000000000000000000000000000000000000000..0bfaff0ba7b1ed311ec6e5644c80ea045463aac7 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/TerritoryGame.API.csproj" @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/TerritoryGame.API.http" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/TerritoryGame.API.http" new file mode 100644 index 0000000000000000000000000000000000000000..354d6e080f16e63dbb4a9d429965896449a88257 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/TerritoryGame.API.http" @@ -0,0 +1,6 @@ +@TerritoryGame.API_HostAddress = http://localhost:5115 + +GET {{TerritoryGame.API_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/appsettings.json" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/appsettings.json" new file mode 100644 index 0000000000000000000000000000000000000000..f7cf59cc297516eaca9a6c25f0bb99db95592761 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.API/appsettings.json" @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Port=5432;Database=TerritoryGame;UserName=postgres;Password=123456;", + "Redis": "localhost" + } +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Class1.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Class1.cs" new file mode 100644 index 0000000000000000000000000000000000000000..8e7a47a9ca8bec27e6a612c27a7b821fd3f6f195 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Class1.cs" @@ -0,0 +1,6 @@ +namespace TerritoryGame.Application; + +public class Class1 +{ + +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/AreaStatDto.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/AreaStatDto.cs" new file mode 100644 index 0000000000000000000000000000000000000000..bd1e41005e81bf1ce461bdc3b7a48acdbb153804 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/AreaStatDto.cs" @@ -0,0 +1,9 @@ +namespace TerritoryGame.Application.Common; + +public class AreaStatDto +{ + public Guid PlayerId { get; set; } + public string PlayerName { get; set; } = string.Empty; + public string Color { get; set; } = string.Empty; + public double AreaPercent { get; set; } +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/DrawCommandDto.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/DrawCommandDto.cs" new file mode 100644 index 0000000000000000000000000000000000000000..dec38f31c13d27a77fa3a22f3e0fb6d5994a8245 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/DrawCommandDto.cs" @@ -0,0 +1,9 @@ +namespace TerritoryGame.Application.Common; + +public class DrawCommandDto +{ + public int X { get; set; } + public int Y { get; set; } + public int Size { get; set; } + public string Color { get; set; } = string.Empty; +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/GameDto.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/GameDto.cs" new file mode 100644 index 0000000000000000000000000000000000000000..8838ad07753dede959913dbf0d5281c0248f2ae8 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/GameDto.cs" @@ -0,0 +1,21 @@ +using TerritoryGame.Domain.Models; + +namespace TerritoryGame.Application.Common; + +public class GameDto +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public string? Code { get; set; } + public string? HostPlayerName { get; set; } + public GameStatus Status { get; set; } + public int MaxPlayers { get; set; } + public int CurrentPlayers { get; set; } + public int DurationMinutes { get; set; } + // 地图尺寸(像素级活动区域,不等于前端画布视口尺寸) + public int CanvasWidth { get; set; } + public int CanvasHeight { get; set; } + public bool HasPassword { get; set; } + public string? Password { get; set; } // 仅在创建者返回时可选回传;生产应避免明文 + public List Players { get; set; } = new(); +} \ No newline at end of file diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/GameResultDto.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/GameResultDto.cs" new file mode 100644 index 0000000000000000000000000000000000000000..2ba981a818d2cfae935e9c0f79a5675b8a9f13d6 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/GameResultDto.cs" @@ -0,0 +1,21 @@ +namespace TerritoryGame.Application.Common; + +public class ScoreEntryDto +{ + public Guid PlayerId { get; set; } + public string PlayerName { get; set; } = string.Empty; + public string Color { get; set; } = string.Empty; + public int Score { get; set; } + public int Kills { get; set; } + public int PaintTiles { get; set; } + public double AreaPercent { get; set; } // 用于展示 +} + +public class GameResultDto +{ + public Guid GameId { get; set; } + public Guid? WinnerPlayerId { get; set; } + public double WinnerAreaPercent { get; set; } + public int WinnerScore { get; set; } + public IEnumerable Players { get; set; } = Enumerable.Empty(); +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/Mappings/MappingProfile.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/Mappings/MappingProfile.cs" new file mode 100644 index 0000000000000000000000000000000000000000..cd8035b9c8ec3a5d67c413e0090e7a2fef754354 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/Mappings/MappingProfile.cs" @@ -0,0 +1,13 @@ +using AutoMapper; +using TerritoryGame.Domain.Models; + +namespace TerritoryGame.Application.Common.Mappings; + +public class MappingProfile : Profile +{ + public MappingProfile() + { + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/PlayerDto.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/PlayerDto.cs" new file mode 100644 index 0000000000000000000000000000000000000000..5fd14546d6725c3161da1534dc9ee2e6c94ce36a --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Common/PlayerDto.cs" @@ -0,0 +1,9 @@ +namespace TerritoryGame.Application.Common; + +public class PlayerDto +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public string Color { get; set; } = null!; + public int Score { get; set; } +} \ No newline at end of file diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/DependencyInjection.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/DependencyInjection.cs" new file mode 100644 index 0000000000000000000000000000000000000000..19673f5545381feb9978c4a39ad0405a19686b83 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/DependencyInjection.cs" @@ -0,0 +1,28 @@ +// Application/DependencyInjection.cs +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; +using AutoMapper; +using TerritoryGame.Application.Common.Mappings; +using TerritoryGame.Application.Services; + +namespace TerritoryGame.Application; + +public static class DependencyInjection +{ + public static IServiceCollection AddApplicationServices(this IServiceCollection services) + { + // 修正后的AutoMapper配置 + services.AddAutoMapper(config => + { + config.AddProfile(); + }, Assembly.GetExecutingAssembly()); + + // 添加MediatR + services.AddMediatR(cfg => + cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); + + services.AddSingleton(); + + return services; + } +} \ No newline at end of file diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Services/GameManager.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Services/GameManager.cs" new file mode 100644 index 0000000000000000000000000000000000000000..1fce0f3bbdb1ecb0490b7a318b0a19d92700e63a --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Services/GameManager.cs" @@ -0,0 +1,1058 @@ +using System.Collections.Concurrent; +using TerritoryGame.Domain.Models; +using TerritoryGame.Domain.Services; +using TerritoryGame.Application.Common; + +namespace TerritoryGame.Application.Services; + +internal class GameState +{ + public Game Game { get; set; } = null!; + public DateTime LastActivityUtc { get; set; } = DateTime.UtcNow; // 最近一次活动时间(创建/加入/离开/开始) + public Dictionary PlayersByConnection { get; set; } = new(); + public DateTime? StartedAt { get; set; } + public Guid?[,] Ownership { get; set; } = null!; // 每像素归属玩家Id + public Dictionary PixelCount { get; set; } = new(); + public Guid? HostPlayerId { get; set; } + public Dictionary Runtime { get; set; } = new(); + // 新增:墙体、道具、刷新与随机 + public List Walls { get; set; } = new(); + public List Items { get; set; } = new(); + public DateTime NextItemSpawnAt { get; set; } = DateTime.MinValue; + public Random Rng { get; set; } = new Random(); + // 新增:隐身区域 + public List Zones { get; set; } = new(); + // 新增:粗网格占领用于计分(递减收益按占地规模衰减) + public Guid?[,] OwnerGrid { get; set; } = null!; // 每格归属玩家Id(Guid),null为无人 +} + +public class PlayerRuntime +{ + public int X { get; set; } + public int Y { get; set; } + public double Angle { get; set; } + public int Energy { get; set; } = 100; // 0-100 + public DateTime LastEnergyTick { get; set; } = DateTime.UtcNow; + public DateTime LastSprayAt { get; set; } = DateTime.MinValue; // 最近一次喷涂时间 + public DateTime? DeadUntil { get; set; } = null; // 被击杀后复活时间 + public DateTime? InkUntil { get; set; } = null; // 墨水增益截止(保留,不再掉落) + public DateTime? StealthUntil { get; set; } = null; // 隐身道具截止 + public DateTime? NoclipUntil { get; set; } = null; // 穿墙道具截止 + // 新:加速道具(将三角形从穿墙替换为加速) + public DateTime? SpeedUntil { get; set; } = null; // 加速截止 + // 新:能量无限与无敌 + public DateTime? EnergyUnlimitedUntil { get; set; } = null; // 无限能量截止 + public DateTime? InvincibleUntil { get; set; } = null; // 无敌截止 + // 复活锁:复活后短时间内忽略客户端上报的位置,防止“原地复活”被客户端立刻覆盖 + public DateTime? RespawnLockUntil { get; set; } = null; + // 新:计分/统计(用于排行榜) + public int Score { get; set; } = 0; + public int Kills { get; set; } = 0; + public int PaintTiles { get; set; } = 0; + // 背包:道具库存 + public Dictionary Inventory { get; } = new(); + // 大爆炸Buff:在持续时间内所有炮弹爆炸半径翻倍(可叠加时长) + public DateTime? BigBangUntil { get; set; } = null; +} + +public record RectWall(int X, int Y, int W, int H); +public record Item(string Type, int X, int Y, int R); +public record StealthZone(int X, int Y, int R); + +public class GameManager : IGameManager +{ + private readonly ConcurrentDictionary _games = new(); + private readonly ConcurrentDictionary _connectionToGame = new(); + + // —— 计分参数(可调) —— + static class ScoreConfig + { + public const int TileSize = 10; // 粗网格大小(像素) + public const int KillBasePoints = 100; // 基础击杀分 + public const int TileBasePoints = 2; // 基础每格分 + public const double KillExpLambda = 0.25; // 击杀递减系数 + public const double KillMinFactor = 0.30; // 击杀最低系数 + public const double TileExpLambda = 0.15; // 占地递减系数 + public const double TileMinFactor = 0.40; // 占地最低系数 + public const int TileDecayUnit = 120; // 每占领多少格降低一个等级 + } + static double ExpDecay(int level, double lambda, double minFactor) => Math.Max(minFactor, Math.Exp(-lambda * level)); + + public Task CreateGameAsync(string name, int maxPlayers, int durationMinutes, string? password = null) + { + // 限制最大 10 人 + if (maxPlayers > 10) maxPlayers = 10; + var game = new Game + { + Id = Guid.NewGuid(), + Name = name, + MaxPlayers = maxPlayers, + DurationMinutes = durationMinutes, + Password = string.IsNullOrWhiteSpace(password) ? null : password, + // 地图扩大一倍(仅地图,不影响前端画布视口) + CanvasWidth = 2500, + CanvasHeight = 1000 + }; + var state = new GameState + { + Game = game, + Ownership = new Guid?[game.CanvasWidth, game.CanvasHeight], + LastActivityUtc = DateTime.UtcNow + }; + InitWalls(state); + InitStealthZones(state); + // 初始化粗网格(用于得分) + EnsureGrid(state); + // 首个道具刷新时间:10 秒 + state.NextItemSpawnAt = DateTime.UtcNow.AddSeconds(10); + _games[game.Id] = state; + return Task.FromResult(new GameDto + { + Id = game.Id, + Name = game.Name, + Code = game.Name, + MaxPlayers = game.MaxPlayers, + Status = game.Status, + CurrentPlayers = 0, + DurationMinutes = game.DurationMinutes, + CanvasWidth = game.CanvasWidth, + CanvasHeight = game.CanvasHeight, + HostPlayerName = null, + HasPassword = game.Password != null, + Password = game.Password, + Players = new() + }); + } + + public Task AddPlayerAsync(Guid gameId, string playerName, string connectionId, string? password = null) + { + var state = _games[gameId]; + if (state.Game.GamePlayers.Count >= state.Game.MaxPlayers) throw new InvalidOperationException("房间已满"); + if (state.Game.Password != null && state.Game.Password != password) + throw new InvalidOperationException("密码错误"); + // 颜色:从 10 个高区分度颜色中随机分配,避免相近 + string[] palette = new[] { + "#1f77b4", // blue + "#ff7f0e", // orange + "#2ca02c", // green + "#d62728", // red + "#9467bd", // purple + "#8c564b", // brown + "#e377c2", // pink + "#7f7f7f", // gray + "#bcbd22", // olive + "#17becf" // cyan + }; + var used = state.Game.GamePlayers.Select(gp => gp.Player.Color).ToHashSet(); + var available = palette.Where(c => !used.Contains(c)).ToList(); + string color = available.Count > 0 ? available[state.Rng.Next(available.Count)] : palette[state.Rng.Next(palette.Length)]; + var player = new Player + { + Id = Guid.NewGuid(), + Name = playerName, + ConnectionId = connectionId, + Color = color + }; + state.Game.GamePlayers.Add(new GamePlayer { GameId = gameId, PlayerId = player.Id, Player = player, Game = state.Game }); + state.PlayersByConnection[connectionId] = player; + _connectionToGame[connectionId] = gameId; + if (state.HostPlayerId == null) state.HostPlayerId = player.Id; // 第一位加入者为房主 + state.LastActivityUtc = DateTime.UtcNow; + // 初始化运行时:随机位置(避免边界) + var pos = RandomFreePosition(state, 18); + state.Runtime[player.Id] = new PlayerRuntime + { + X = pos.x, + Y = pos.y, + Angle = state.Rng.NextDouble() * Math.PI * 2, + Energy = 100, + LastEnergyTick = DateTime.UtcNow + }; + return Task.FromResult(new PlayerDto { Id = player.Id, Name = player.Name, Color = player.Color, Score = 0 }); + } + + public Task RemovePlayerAsync(Guid gameId, string connectionId) + { + if (!_games.TryGetValue(gameId, out var state)) return Task.FromResult(new RemovePlayerResult(false, false, null, false)); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return Task.FromResult(new RemovePlayerResult(false, false, null, false)); + var wasHost = state.HostPlayerId == player.Id; + // 从房间玩家列表移除 + state.Game.GamePlayers = state.Game.GamePlayers.Where(gp => gp.PlayerId != player.Id).ToList(); + // 运行时状态清理,避免“假人”残留 + if (state.Runtime.ContainsKey(player.Id)) state.Runtime.Remove(player.Id); + // 反向像素计数无需强制清理;区域归属在后续覆盖中自然变化 + // 连接映射移除 + state.PlayersByConnection.Remove(connectionId); + _connectionToGame.TryRemove(connectionId, out _); + // 固定调色板,不需要释放随机颜色池 + if (wasHost) + { + // 需求变更:房主离开时直接解散房间(踢出所有人) + _games.TryRemove(gameId, out _); + return Task.FromResult(new RemovePlayerResult(true, true, player.Id, wasHost)); + } + var remaining = state.Game.GamePlayers.Count; + if (remaining == 0) + { + // 无人剩余也删除房间 + _games.TryRemove(gameId, out _); + return Task.FromResult(new RemovePlayerResult(true, true, player.Id, wasHost)); + } + state.LastActivityUtc = DateTime.UtcNow; + return Task.FromResult(new RemovePlayerResult(true, false, player.Id, wasHost)); + } + + public (bool success, Guid? removedPlayerId, string? removedConnectionId, bool gameDeleted, bool wasHostCaller, string? reason) KickPlayer(Guid gameId, string hostConnectionId, Guid targetPlayerId) + { + if (!_games.TryGetValue(gameId, out var state)) return (false, null, null, false, false, "房间不存在"); + if (!state.PlayersByConnection.TryGetValue(hostConnectionId, out var hostPlayer)) return (false, null, null, false, false, "无效调用"); + var isHost = state.HostPlayerId == hostPlayer.Id; + if (!isHost) return (false, null, null, false, false, "只有房主可以踢人"); + // 找到目标玩家及其连接 + string? targetConn = state.PlayersByConnection.FirstOrDefault(kv => kv.Value.Id == targetPlayerId).Key; + if (targetConn == null) return (false, null, null, false, true, "目标不在房间"); + // 踢人:重用 RemovePlayerAsync 的逻辑,但这里避免递归 await,直接内联必要清理 + var wasHost = state.HostPlayerId == targetPlayerId; + // 从列表与运行时移除 + state.Game.GamePlayers = state.Game.GamePlayers.Where(gp => gp.PlayerId != targetPlayerId).ToList(); + if (state.Runtime.ContainsKey(targetPlayerId)) state.Runtime.Remove(targetPlayerId); + state.PlayersByConnection.Remove(targetConn); + _connectionToGame.TryRemove(targetConn, out _); + if (wasHost) + { + // 若目标恰好是房主,解散房间 + _games.TryRemove(gameId, out _); + return (true, targetPlayerId, targetConn, true, true, null); + } + var remaining = state.Game.GamePlayers.Count; + if (remaining == 0) + { + _games.TryRemove(gameId, out _); + return (true, targetPlayerId, targetConn, true, true, null); + } + state.LastActivityUtc = DateTime.UtcNow; + return (true, targetPlayerId, targetConn, false, true, null); + } + + public Task StartGameAsync(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var state)) return Task.FromResult(false); + if (state.StartedAt != null) return Task.FromResult(false); + // 至少 2 名玩家才能开始 + if (state.Game.GamePlayers.Count < 2) return Task.FromResult(false); + state.StartedAt = DateTime.UtcNow; + state.Game.Status = GameStatus.InProgress; + state.LastActivityUtc = DateTime.UtcNow; + return Task.FromResult(true); + } + + public Task ProcessDrawAsync(Guid gameId, string connectionId, int x, int y, int size, string color) + { + // 兼容旧的点涂抹(不覆盖) + if (!_games.TryGetValue(gameId, out var state)) return Task.FromResult(false); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return Task.FromResult(false); + if (state.Game.Status != GameStatus.InProgress) return Task.FromResult(false); + int r = size / 2; + for (int dx = -r; dx <= r; dx++) + { + for (int dy = -r; dy <= r; dy++) + { + int px = x + dx; + int py = y + dy; + if (px < 0 || py < 0 || px >= state.Game.CanvasWidth || py >= state.Game.CanvasHeight) continue; + if (dx * dx + dy * dy > r * r) continue; + if (state.Ownership[px, py] != null) continue; // 不覆盖 + state.Ownership[px, py] = player.Id; + if (!state.PixelCount.ContainsKey(player.Id)) state.PixelCount[player.Id] = 0; + state.PixelCount[player.Id]++; + } + } + return Task.FromResult(true); + } + + public IEnumerable GetPlayerStates(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var state)) return Enumerable.Empty(); + var now = DateTime.UtcNow; + return state.Runtime.Select(kv => { + var pid = kv.Key; + var gp = state.Game.GamePlayers.FirstOrDefault(p => p.PlayerId == pid); + var color = gp?.Player?.Color; + return new + { + playerId = kv.Key, + x = kv.Value.X, + y = kv.Value.Y, + angle = kv.Value.Angle, + color, + deadUntil = kv.Value.DeadUntil, + stealthUntil = kv.Value.StealthUntil, + speedUntil = kv.Value.SpeedUntil, + noclipUntil = kv.Value.NoclipUntil, + bigBangUntil = kv.Value.BigBangUntil, + invincibleUntil = kv.Value.InvincibleUntil, + energyUnlimitedUntil = kv.Value.EnergyUnlimitedUntil, + stealth = (kv.Value.StealthUntil != null && kv.Value.StealthUntil > now) || InStealthZone(state, kv.Value.X, kv.Value.Y) + }; + }); + } + + // 获取调用者自身的运行时状态(用于激活道具后立刻回发一次,带剩余效果时间) + public object? GetSelfState(Guid gameId, string connectionId) + { + if (!_games.TryGetValue(gameId, out var state)) return null; + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return null; + if (!state.Runtime.TryGetValue(player.Id, out var rt)) return null; + var now = DateTime.UtcNow; + bool stealthFlag = (rt.StealthUntil != null && rt.StealthUntil > now) || InStealthZone(state, rt.X, rt.Y); + return new { playerId = player.Id, x = rt.X, y = rt.Y, angle = rt.Angle, color = player.Color, deadUntil = rt.DeadUntil, stealth = stealthFlag, stealthUntil = rt.StealthUntil, speedUntil = rt.SpeedUntil, noclipUntil = rt.NoclipUntil, bigBangUntil = rt.BigBangUntil, invincibleUntil = rt.InvincibleUntil, energyUnlimitedUntil = rt.EnergyUnlimitedUntil }; + } + + private void RegenEnergy(PlayerRuntime rt) + { + // 喷涂后 250ms 内不回复 + var now = DateTime.UtcNow; + if ((now - rt.LastSprayAt).TotalMilliseconds < 250) return; + // 每 250ms 恢复 2 点 + var delta = (now - rt.LastEnergyTick).TotalMilliseconds; + if (delta < 250) return; + var ticks = (int)(delta / 250.0); + if (ticks <= 0) return; + rt.Energy = Math.Min(100, rt.Energy + ticks * 2); + rt.LastEnergyTick = now; + } + + public (bool ok, object? moved, int? energy, bool itemsChanged) Move(Guid gameId, string connectionId, int forward, int rotate) + { + if (!_games.TryGetValue(gameId, out var state)) return (false, null, null, false); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return (false, null, null, false); + if (!state.Runtime.TryGetValue(player.Id, out var rt)) return (false, null, null, false); + if (state.Game.Status != GameStatus.InProgress) return (false, null, null, false); + // 不再在玩家移动时刷新道具,由计时器驱动刷新广播 + var now = DateTime.UtcNow; + // 死亡冷却中:只回复能量并广播死亡状态 + if (rt.DeadUntil != null && rt.DeadUntil > now) + { + RegenEnergy(rt); + return (true, new { playerId = player.Id, x = rt.X, y = rt.Y, angle = rt.Angle, color = player.Color, deadUntil = rt.DeadUntil, invincibleUntil = rt.InvincibleUntil, energyUnlimitedUntil = rt.EnergyUnlimitedUntil }, rt.Energy, false); + } + // 到点复活 + bool respawned = false; + if (rt.DeadUntil != null && rt.DeadUntil <= now) + { + var pos = RandomFreePosition(state, 18); + rt.X = pos.x; rt.Y = pos.y; rt.DeadUntil = null; + rt.RespawnLockUntil = now.AddMilliseconds(1500); // 延长复活锁,避免客户端旧坐标回拉 + respawned = true; + } + RegenEnergy(rt); + const double rotStep = Math.PI / 60.0; // 3° + double speed = 2.0; + if (rt.SpeedUntil != null && rt.SpeedUntil > now) speed *= 2.0; // 加速x2 + if (rotate != 0) rt.Angle += rotate * rotStep; + // 归一化角度 + if (rt.Angle > Math.PI) rt.Angle -= 2 * Math.PI; else if (rt.Angle < -Math.PI) rt.Angle += 2 * Math.PI; + if (forward != 0) + { + var nx = rt.X + (int)Math.Round(Math.Cos(rt.Angle) * speed * forward); + var ny = rt.Y + (int)Math.Round(Math.Sin(rt.Angle) * speed * forward); + nx = Math.Clamp(nx, 0, state.Game.CanvasWidth - 1); + ny = Math.Clamp(ny, 0, state.Game.CanvasHeight - 1); + // 碰撞检测:不可进入墙体(除非处于穿墙状态) + var now2 = DateTime.UtcNow; + bool canClip = rt.NoclipUntil != null && rt.NoclipUntil > now2; + if (canClip || !InsideAnyWall(state, nx, ny)) { rt.X = nx; rt.Y = ny; } + } + // 拾取道具(移除地图上的道具,需要广播) + bool itemsChanged = TryPickup(state, player.Id); + bool stealthFlag = (rt.StealthUntil != null && rt.StealthUntil > DateTime.UtcNow) || InStealthZone(state, rt.X, rt.Y); + return (true, new { playerId = player.Id, x = rt.X, y = rt.Y, angle = rt.Angle, color = player.Color, deadUntil = rt.DeadUntil, stealth = stealthFlag, stealthUntil = rt.StealthUntil, speedUntil = rt.SpeedUntil, noclipUntil = rt.NoclipUntil, bigBangUntil = rt.BigBangUntil, invincibleUntil = rt.InvincibleUntil, energyUnlimitedUntil = rt.EnergyUnlimitedUntil, respawned }, rt.Energy, itemsChanged); + } + + // 前端权威:直接设置坐标与角度(做边界/墙体/死亡校验) + public (bool ok, object? moved, int? energy, bool itemsChanged) ClientSetTransform(Guid gameId, string connectionId, int x, int y, double angle) + { + if (!_games.TryGetValue(gameId, out var state)) return (false, null, null, false); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return (false, null, null, false); + if (!state.Runtime.TryGetValue(player.Id, out var rt)) return (false, null, null, false); + if (state.Game.Status != GameStatus.InProgress) return (false, null, null, false); + // 不再在客户端上报位置时刷新道具,由计时器驱动刷新广播 + var now = DateTime.UtcNow; + // 死亡中:拒绝移动,仅返回当前状态 + if (rt.DeadUntil != null && rt.DeadUntil > now) + { + RegenEnergy(rt); + return (true, new { playerId = player.Id, x = rt.X, y = rt.Y, angle = rt.Angle, color = player.Color, deadUntil = rt.DeadUntil, invincibleUntil = rt.InvincibleUntil, energyUnlimitedUntil = rt.EnergyUnlimitedUntil }, rt.Energy, false); + } + // 到点复活 + bool respawned = false; + if (rt.DeadUntil != null && rt.DeadUntil <= now) + { + var pos = RandomFreePosition(state, 18); + rt.X = pos.x; rt.Y = pos.y; rt.DeadUntil = null; + rt.RespawnLockUntil = now.AddMilliseconds(1500); // 延长复活锁 + respawned = true; + } + RegenEnergy(rt); + // 归一化角度 + if (angle > Math.PI) angle -= 2 * Math.PI; else if (angle < -Math.PI) angle += 2 * Math.PI; + // 复活锁:在短时间内忽略客户端坐标,防止原地覆盖复活点 + bool inRespawnLock = rt.RespawnLockUntil != null && rt.RespawnLockUntil > now; + if (!inRespawnLock) + { + // 边界限制 + x = Math.Clamp(x, 0, state.Game.CanvasWidth - 1); + y = Math.Clamp(y, 0, state.Game.CanvasHeight - 1); + // 墙体阻挡(除非穿墙状态) + bool canClip = rt.NoclipUntil != null && rt.NoclipUntil > now; + if (!(canClip || !InsideAnyWall(state, x, y))) + { + // 若被墙阻挡,保持原位但同步角度 + rt.Angle = angle; + return (true, new { playerId = player.Id, x = rt.X, y = rt.Y, angle = rt.Angle, color = player.Color, deadUntil = rt.DeadUntil }, rt.Energy, false); + } + // 接受客户端位置 + rt.X = x; rt.Y = y; rt.Angle = angle; + } + else + { + // 仅同步角度 + rt.Angle = angle; + } + // 拾取道具(移除地图上的道具,需要广播) + bool itemsChanged = TryPickup(state, player.Id); + bool stealthFlag = (rt.StealthUntil != null && rt.StealthUntil > DateTime.UtcNow) || InStealthZone(state, rt.X, rt.Y); + return (true, new { playerId = player.Id, x = rt.X, y = rt.Y, angle = rt.Angle, color = player.Color, deadUntil = rt.DeadUntil, stealth = stealthFlag, stealthUntil = rt.StealthUntil, speedUntil = rt.SpeedUntil, noclipUntil = rt.NoclipUntil, bigBangUntil = rt.BigBangUntil, invincibleUntil = rt.InvincibleUntil, energyUnlimitedUntil = rt.EnergyUnlimitedUntil, respawned }, rt.Energy, itemsChanged); + } + + public (bool ok, int energy, IEnumerable? sector, IEnumerable? stats, string? color) Spray(Guid gameId, string connectionId, int radius, int angleWidthDeg, bool allowOverwrite) + { + if (!_games.TryGetValue(gameId, out var state)) return (false, 0, null, null, null); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return (false, 0, null, null, null); + if (!state.Runtime.TryGetValue(player.Id, out var rt)) return (false, 0, null, null, player.Color); + if (state.Game.Status != GameStatus.InProgress) return (false, rt.Energy, null, null, player.Color); + RegenEnergy(rt); + // 消耗:喷涂一次基础 12 能量;若处于无限能量则跳过消耗 + const int cost = 12; + bool energyFree = rt.EnergyUnlimitedUntil != null && rt.EnergyUnlimitedUntil > DateTime.UtcNow; + if (!energyFree) + { + if (rt.Energy < cost) return (false, rt.Energy, null, null, player.Color); + rt.Energy -= cost; + } + rt.LastSprayAt = DateTime.UtcNow; + var affected = new List(); + int r = radius; + double half = angleWidthDeg * Math.PI / 360.0; + int minX = Math.Max(0, rt.X - r); + int maxX = Math.Min(state.Game.CanvasWidth - 1, rt.X + r); + int minY = Math.Max(0, rt.Y - r); + int maxY = Math.Min(state.Game.CanvasHeight - 1, rt.Y + r); + for (int x = minX; x <= maxX; x++) + { + for (int y = minY; y <= maxY; y++) + { + int dx = x - rt.X; + int dy = y - rt.Y; + if (dx * dx + dy * dy > r * r) continue; + var ang = Math.Atan2(dy, dx); + var diff = ang - rt.Angle; + while (diff > Math.PI) diff -= 2 * Math.PI; + while (diff < -Math.PI) diff += 2 * Math.PI; + if (Math.Abs(diff) > half) continue; + var prev = state.Ownership[x, y]; + if (prev == player.Id) continue; // 已是本人 + if (prev != null && !allowOverwrite) continue; + // 覆盖逻辑:减少前所属计数 + if (prev != null && allowOverwrite) + { + if (state.PixelCount.ContainsKey(prev.Value)) + { + state.PixelCount[prev.Value] = Math.Max(0, state.PixelCount[prev.Value] - 1); + } + } + state.Ownership[x, y] = player.Id; + if (!state.PixelCount.ContainsKey(player.Id)) state.PixelCount[player.Id] = 0; + state.PixelCount[player.Id]++; + affected.Add(new { x, y }); + } + } + var stats = GetAreaStats(gameId).ToList(); + return (true, rt.Energy, affected, stats, player.Color); + } + + // —— 炮弹:飞行-爆炸-涂抹-击杀 —— + public (bool ok, int energy, IEnumerable? affected, object? explosion, IEnumerable? killed, string? color, IEnumerable? killFeed) FireCannon(Guid gameId, string connectionId) + { + if (!_games.TryGetValue(gameId, out var state)) return (false, 0, null, null, null, null, null); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return (false, 0, null, null, null, null, null); + if (!state.Runtime.TryGetValue(player.Id, out var rt)) return (false, 0, null, null, null, player.Color, null); + if (state.Game.Status != GameStatus.InProgress) return (false, rt.Energy, null, null, null, player.Color, null); + SpawnItemIfDue(state); + var now = DateTime.UtcNow; + if (rt.DeadUntil != null && rt.DeadUntil > now) return (false, rt.Energy, null, null, null, player.Color, null); + RegenEnergy(rt); + bool energyFree = rt.EnergyUnlimitedUntil != null && rt.EnergyUnlimitedUntil > now; + const int cost = 12; + if (!energyFree) + { + if (rt.Energy < cost) return (false, rt.Energy, null, null, null, player.Color, null); + rt.Energy -= cost; + } + rt.LastSprayAt = now; + // 参数 + int maxRange = (int)Math.Round(260 * 0.75); // 降低至原来的 75% + int outer = 44; // 爆炸范围 + if (rt.InkUntil != null && rt.InkUntil > now) { outer = (int)(outer * 1.4); } + // 大爆炸Buff:在持续时间内翻倍(不再使用“一次性充能”) + if (rt.BigBangUntil != null && rt.BigBangUntil > now) { outer *= 2; } + var ox = rt.X; var oy = rt.Y; + var farX = ox + (int)Math.Round(Math.Cos(rt.Angle) * maxRange); + var farY = oy + (int)Math.Round(Math.Sin(rt.Angle) * maxRange); + var now2 = DateTime.UtcNow; + var wallHit = (rt.NoclipUntil != null && rt.NoclipUntil > now2) ? null : FirstWallHit(state, ox, oy, farX, farY); + var playerHit = FirstPlayerHit(state, player.Id, ox, oy, farX, farY, skipInvincible: true); + int ex, ey; double tChosen = double.PositiveInfinity; + if (playerHit != null) { ex = playerHit.Value.x; ey = playerHit.Value.y; tChosen = playerHit.Value.t; } + else { ex = farX; ey = farY; tChosen = 1.0; } + if (wallHit != null && wallHit.Value.t < tChosen) { ex = wallHit.Value.x; ey = wallHit.Value.y; tChosen = wallHit.Value.t; } + ex = Math.Clamp(ex, 0, state.Game.CanvasWidth - 1); + ey = Math.Clamp(ey, 0, state.Game.CanvasHeight - 1); + // 实心爆炸并写入 Ownership + var affected = new List(); + FillCircleExplosion(state, ex, ey, outer, player.Id, affected); + // 击杀判定(圆形) + var killed = new List(); + var killFeed = new List(); + int r2 = (int)(outer * outer * 0.9); + foreach (var kv in state.Runtime) + { + var pid = kv.Key; if (pid == player.Id) continue; var pr = kv.Value; if (pr.DeadUntil != null && pr.DeadUntil > now) continue; + // 无敌者不被击杀 + if (pr.InvincibleUntil != null && pr.InvincibleUntil > now) continue; + int dx = pr.X - ex, dy = pr.Y - ey; if (dx * dx + dy * dy <= r2) { pr.DeadUntil = now.AddSeconds(5); killed.Add(pid); } + } + // 击杀奖励:每个被击杀玩家有25%概率给予射手一个随机道具 + if (killed.Count > 0) + { + string[] __types = new[] { "energy", "stealth", "speed", "noclip", "bigbang" }; + foreach (var vid in killed) + { + string? tAwarded = null; + if (state.Rng.NextDouble() < 0.25) + { + tAwarded = __types[state.Rng.Next(__types.Length)]; + if (!rt.Inventory.ContainsKey(tAwarded)) rt.Inventory[tAwarded] = 0; + rt.Inventory[tAwarded]++; + } + var victim = state.Game.GamePlayers.FirstOrDefault(gp => gp.PlayerId == vid)?.Player; + killFeed.Add(new + { + shooterId = player.Id, + shooterName = player.Name, + shooterColor = player.Color, + victimId = vid, + victimName = victim?.Name ?? "", + victimColor = victim?.Color ?? "", + item = tAwarded + }); + } + } + var explosion = new { x = ex, y = ey, outer, ox, oy }; + // —— 计分:递减收益 —— + // 击杀:按已累计击杀数做指数递减 + int killScore = 0; + for (int i = 0; i < killed.Count; i++) + { + int level = rt.Kills + i; // 第 level+1 次击杀 + double factor = ExpDecay(level, ScoreConfig.KillExpLambda, ScoreConfig.KillMinFactor); + killScore += (int)Math.Round(ScoreConfig.KillBasePoints * factor); + } + // 占地:按已占格数分级,新增的每一格按当前等级计分 + var (gained, tileScore) = ApplyExplosionOwnershipAndScore(state, player.Id, ex, ey, outer, rt.PaintTiles); + rt.Kills += killed.Count; + rt.PaintTiles += gained; + rt.Score += killScore + tileScore; + return (true, rt.Energy, affected, explosion, killed, player.Color, killFeed); + } + + // —— 地图/几何与道具 —— + private void InitWalls(GameState s) + { + // 随机生成 8-12 个矩形墙体(轴对齐),保留一定边距,尺寸在合理范围 + var rng = s.Rng; + s.Walls = new List(); + int target = rng.Next(8, 13); + int tries = 0; + int margin = 40; + while (s.Walls.Count < target && tries++ < target * 20) + { + bool horizontal = rng.Next(2) == 0; + int w = horizontal ? rng.Next(160, 361) : rng.Next(18, 37); + int h = horizontal ? rng.Next(18, 37) : rng.Next(160, 361); + int maxX = Math.Max(1, s.Game.CanvasWidth - w - margin); + int maxY = Math.Max(1, s.Game.CanvasHeight - h - margin); + int x = rng.Next(margin, maxX); + int y = rng.Next(margin, maxY); + // 简单相交抑制:避免与现有墙体过大重叠 + var rect = new RectWall(x, y, w, h); + bool overlaps = s.Walls.Any(ex => + { + int ax1 = rect.X, ay1 = rect.Y, ax2 = rect.X + rect.W, ay2 = rect.Y + rect.H; + int bx1 = ex.X, by1 = ex.Y, bx2 = ex.X + ex.W, by2 = ex.Y + ex.H; + int ix = Math.Max(0, Math.Min(ax2, bx2) - Math.Max(ax1, bx1)); + int iy = Math.Max(0, Math.Min(ay2, by2) - Math.Max(ay1, by1)); + int inter = ix * iy; + return inter > (rect.W * rect.H) * 0.6; // 重叠超过 60% 则丢弃 + }); + if (!overlaps) s.Walls.Add(rect); + } + } + private void InitStealthZones(GameState s) + { + var rng = s.Rng; + s.Zones = new List(); + int count = rng.Next(2, 4); // 2~3 个 + int r = 88; + int tries = 0; + int margin = r + 20; + while (s.Zones.Count < count && tries++ < count * 50) + { + int x = rng.Next(margin, s.Game.CanvasWidth - margin); + int y = rng.Next(margin, s.Game.CanvasHeight - margin); + // 中心不得位于墙体内,且与已放置区域保持一定间距 + if (InsideAnyWall(s, x, y)) continue; + bool closeToOthers = s.Zones.Any(z => + { + int dx = x - z.X, dy = y - z.Y; return (dx * dx + dy * dy) < (int)(r * r * 2.25); // 距离 < 1.5r + }); + if (closeToOthers) continue; + s.Zones.Add(new StealthZone(x, y, r)); + } + // 若随机失败,至少保证一个区域 + if (s.Zones.Count == 0) s.Zones.Add(new StealthZone(s.Game.CanvasWidth/2, s.Game.CanvasHeight/2, r)); + } + private bool InsideAnyWall(GameState s, int x, int y) + => s.Walls.Any(w => x >= w.X && x <= w.X + w.W && y >= w.Y && y <= w.Y + w.H); + private bool InStealthZone(GameState s, int x, int y) + => s.Zones.Any(z => { var dx = x - z.X; var dy = y - z.Y; return dx * dx + dy * dy <= z.R * z.R; }); + private (int x, int y) RandomFreePosition(GameState s, int radius) + { + for (int i = 0; i < 200; i++) + { + int x = 20 + s.Rng.Next(0, s.Game.CanvasWidth - 40); + int y = 20 + s.Rng.Next(0, s.Game.CanvasHeight - 40); + if (!InsideAnyWall(s, x, y)) return (x, y); + } + return (s.Game.CanvasWidth/2, s.Game.CanvasHeight/2); + } + private static double? SegIntersect((double x,double y) p, (double x,double y) q, (double x,double y) a, (double x,double y) b) + { + var rx = q.x - p.x; var ry = q.y - p.y; var sx = b.x - a.x; var sy = b.y - a.y; var rxs = rx * sy - ry * sx; if (Math.Abs(rxs) < 1e-8) return null; + var qpx = a.x - p.x; var qpy = a.y - p.y; var t = (qpx * sy - qpy * sx) / rxs; var u = (qpx * ry - qpy * rx) / rxs; if (t >= 0 && t <= 1 && u >= 0 && u <= 1) return t; return null; + } + private (double t, int x,int y)? FirstWallHit(GameState s, int x0, int y0, int x1, int y1) + { + double bestT = double.PositiveInfinity; (double t, int x,int y)? hit = null; var p = ((double)x0, (double)y0); var q = ((double)x1, (double)y1); + foreach (var w in s.Walls) + { + var verts = new []{ (w.X, w.Y), (w.X + w.W, w.Y), (w.X + w.W, w.Y + w.H), (w.X, w.Y + w.H) }; + for (int i = 0; i < 4; i++) + { + var a = ((double)verts[i].Item1, (double)verts[i].Item2); + var b = ((double)verts[(i + 1) % 4].Item1, (double)verts[(i + 1) % 4].Item2); + var t = SegIntersect(p, q, a, b); + if (t != null && t.Value < bestT) + { + bestT = t.Value; var hx = (int)Math.Round(p.Item1 + (q.Item1 - p.Item1) * t.Value); var hy = (int)Math.Round(p.Item2 + (q.Item2 - p.Item2) * t.Value); + hit = (t.Value, hx, hy); + } + } + } + return hit; + } + private (double t, int x, int y, Guid pid)? FirstPlayerHit(GameState s, Guid shooterId, int x0, int y0, int x1, int y1, bool skipInvincible = false) + { + // 直线段与玩家圆形碰撞,返回最早命中的玩家 + double bestT = double.PositiveInfinity; (double t, int x, int y, Guid pid)? hit = null; + double dx = x1 - x0, dy = y1 - y0; double len2 = dx*dx + dy*dy; if (len2 <= 1e-6) return null; + foreach (var kv in s.Runtime) + { + var pid = kv.Key; if (pid == shooterId) continue; var pr = kv.Value; if (pr.DeadUntil != null && pr.DeadUntil > DateTime.UtcNow) continue; + if (skipInvincible && pr.InvincibleUntil != null && pr.InvincibleUntil > DateTime.UtcNow) continue; + double cx = pr.X - x0, cy = pr.Y - y0; double t = (cx*dx + cy*dy) / len2; if (t < 0 || t > 1) continue; + double px = x0 + dx * t, py = y0 + dy * t; double ddx = pr.X - px, ddy = pr.Y - py; double dist2 = ddx*ddx + ddy*ddy; + const int R = 16; if (dist2 <= R * R && t < bestT) + { + bestT = t; hit = (t, (int)Math.Round(px), (int)Math.Round(py), pid); + } + } + return hit; + } + private static double Lerp(double a, double b, double t) => a + (b - a) * t; + private void FillCircleExplosion(GameState s, int cx, int cy, int radius, Guid painter, List outPoints) + { + int r2 = radius * radius; + int minX = Math.Max(0, cx - radius); + int maxX = Math.Min(s.Game.CanvasWidth - 1, cx + radius); + int minY = Math.Max(0, cy - radius); + int maxY = Math.Min(s.Game.CanvasHeight - 1, cy + radius); + for (int x = minX; x <= maxX; x++) + { + int dx = x - cx; int dx2 = dx * dx; + for (int y = minY; y <= maxY; y++) + { + int dy = y - cy; if (dx2 + dy * dy > r2) continue; + var prev = s.Ownership[x, y]; + if (prev == painter) continue; + if (prev != null) + { + if (s.PixelCount.ContainsKey(prev.Value)) s.PixelCount[prev.Value] = Math.Max(0, s.PixelCount[prev.Value] - 1); + } + s.Ownership[x, y] = painter; + if (!s.PixelCount.ContainsKey(painter)) s.PixelCount[painter] = 0; + s.PixelCount[painter]++; + outPoints.Add(new { x, y }); + } + } + } + private void SpawnItemIfDue(GameState s) + { + if (DateTime.UtcNow < s.NextItemSpawnAt) return; + // 每 10 秒刷新一批道具:数量 = 玩家总人数 / 2(向下取整) + s.NextItemSpawnAt = DateTime.UtcNow.AddSeconds(10); + int totalPlayers = s.Game.GamePlayers.Count; + int toSpawn = totalPlayers / 2; // 不管余数 + if (toSpawn <= 0) return; + string[] types = new[] { "energy", "stealth", "speed", "noclip", "bigbang" }; + for (int i = 0; i < toSpawn; i++) + { + var pos = RandomFreePosition(s, 12); + var type = types[s.Rng.Next(types.Length)]; + s.Items.Add(new Item(type, pos.x, pos.y, 12)); + } + } + // 供计时器调用:检查并刷新一批道具,返回是否有新增 + public bool CheckAndSpawnItems(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var s)) return false; + int before = s.Items.Count; + SpawnItemIfDue(s); + return s.Items.Count > before; + } + + // —— 计分:粗网格与得分计算 —— + private void EnsureGrid(GameState s) + { + int tilesW = Math.Max(1, s.Game.CanvasWidth / ScoreConfig.TileSize); + int tilesH = Math.Max(1, s.Game.CanvasHeight / ScoreConfig.TileSize); + if (s.OwnerGrid == null || s.OwnerGrid.GetLength(0) != tilesW || s.OwnerGrid.GetLength(1) != tilesH) + { + s.OwnerGrid = new Guid?[tilesW, tilesH]; + for (int x = 0; x < tilesW; x++) + for (int y = 0; y < tilesH; y++) + s.OwnerGrid[x, y] = null; + } + } + + private (int gained, int tileScore) ApplyExplosionOwnershipAndScore(GameState s, Guid shooterId, int cx, int cy, int r, int paintTilesBefore) + { + EnsureGrid(s); + int ts = ScoreConfig.TileSize; + int tilesW = s.OwnerGrid.GetLength(0); + int tilesH = s.OwnerGrid.GetLength(1); + int minTX = Math.Max(0, (cx - r) / ts), maxTX = Math.Min(tilesW - 1, (cx + r) / ts); + int minTY = Math.Max(0, (cy - r) / ts), maxTY = Math.Min(tilesH - 1, (cy + r) / ts); + int r2 = r * r, gained = 0, tileScore = 0; + for (int tx = minTX; tx <= maxTX; tx++) + { + int gx = tx * ts + ts / 2; + int dx = gx - cx; int dx2 = dx * dx; + for (int ty = minTY; ty <= maxTY; ty++) + { + int gy = ty * ts + ts / 2; + int dy = gy - cy; + if (dx2 + dy * dy > r2) continue; + var prevOwner = s.OwnerGrid[tx, ty]; + if (prevOwner == shooterId) continue; // 自己已占有,无变更 + + // 若有前任拥有者且不是本次涂色者:为其扣分与减少 PaintTiles + if (prevOwner != null && s.Runtime.TryGetValue(prevOwner.Value, out var prevRt)) + { + if (prevRt.PaintTiles > 0) + { + int tilesBeforeLoss = Math.Max(0, prevRt.PaintTiles - 1); + int lossLevel = tilesBeforeLoss / ScoreConfig.TileDecayUnit; + double lossFactor = ExpDecay(lossLevel, ScoreConfig.TileExpLambda, ScoreConfig.TileMinFactor); + int loss = (int)Math.Round(ScoreConfig.TileBasePoints * lossFactor); + prevRt.Score = Math.Max(0, prevRt.Score - loss); // 不允许降为负数(可按需改为允许负分) + prevRt.PaintTiles = Math.Max(0, prevRt.PaintTiles - 1); + } + } + + // 归属变更给当前玩家并为其加分 + s.OwnerGrid[tx, ty] = shooterId; + int totalBeforeThis = paintTilesBefore + gained; + int level = totalBeforeThis / ScoreConfig.TileDecayUnit; + double factor = ExpDecay(level, ScoreConfig.TileExpLambda, ScoreConfig.TileMinFactor); + tileScore += (int)Math.Round(ScoreConfig.TileBasePoints * factor); + gained++; + } + } + return (gained, tileScore); + } + private bool TryPickup(GameState s, Guid playerId) + { + if (!s.Runtime.TryGetValue(playerId, out var rt)) return false; + bool changed = false; + for (int i = s.Items.Count - 1; i >= 0; i--) + { + var it = s.Items[i]; + int dx = rt.X - it.X, dy = rt.Y - it.Y; if (dx * dx + dy * dy <= (it.R + 10) * (it.R + 10)) + { + // 改为捡入背包,不立即生效 + var t = it.Type?.ToLowerInvariant() ?? ""; + if (!rt.Inventory.ContainsKey(t)) rt.Inventory[t] = 0; + rt.Inventory[t]++; + s.Items.RemoveAt(i); changed = true; + } + } + return changed; + } + + public IDictionary GetInventory(Guid gameId, string connectionId) + { + if (!_games.TryGetValue(gameId, out var state)) return new Dictionary(); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return new Dictionary(); + if (!state.Runtime.TryGetValue(player.Id, out var rt)) return new Dictionary(); + return new Dictionary(rt.Inventory); + } + + public (bool ok, int energy, IDictionary inventory) ActivateItem(Guid gameId, string connectionId, string type) + { + if (!_games.TryGetValue(gameId, out var state)) return (false, 0, new Dictionary()); + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return (false, 0, new Dictionary()); + if (!state.Runtime.TryGetValue(player.Id, out var rt)) return (false, 0, new Dictionary()); + var key = (type ?? string.Empty).ToLowerInvariant(); + if (!rt.Inventory.TryGetValue(key, out var count) || count <= 0) return (false, rt.Energy, new Dictionary(rt.Inventory)); + rt.Inventory[key] = count - 1; if (rt.Inventory[key] <= 0) rt.Inventory.Remove(key); + var now = DateTime.UtcNow; + switch (key) + { + case "energy": + // 绿色十字:5秒无限能量,同时立刻回满 + rt.Energy = 100; + rt.EnergyUnlimitedUntil = (rt.EnergyUnlimitedUntil != null && rt.EnergyUnlimitedUntil > now) + ? rt.EnergyUnlimitedUntil.Value.AddSeconds(5) + : now.AddSeconds(5); + break; + case "stealth": + // 红色五角星:5秒无敌(可叠加延长);不再授予隐身 + rt.InvincibleUntil = (rt.InvincibleUntil != null && rt.InvincibleUntil > now) + ? rt.InvincibleUntil.Value.AddSeconds(5) + : now.AddSeconds(5); + break; + case "speed": + // 橙色三角形:双倍移速,持续7秒(可叠加延长) + rt.SpeedUntil = (rt.SpeedUntil != null && rt.SpeedUntil > now) + ? rt.SpeedUntil.Value.AddSeconds(7) + : now.AddSeconds(7); + break; + case "noclip": + // 蓝色方块:7秒穿墙(可叠加延长) + rt.NoclipUntil = (rt.NoclipUntil != null && rt.NoclipUntil > now) + ? rt.NoclipUntil.Value.AddSeconds(7) + : now.AddSeconds(7); + break; // 期间可穿墙,炮弹也穿墙 + case "bigbang": + // 紫色六边形:持续Buff,时长 5 秒,可叠加延长 + rt.BigBangUntil = (rt.BigBangUntil != null && rt.BigBangUntil > now) + ? rt.BigBangUntil.Value.AddSeconds(5) + : now.AddSeconds(5); + break; + } + return (true, rt.Energy, new Dictionary(rt.Inventory)); + } + + public IEnumerable GetItems(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var s)) return Enumerable.Empty(); + return s.Items.Select(it => new { type = it.Type, x = it.X, y = it.Y, r = it.R }); + } + public IEnumerable GetWalls(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var s)) return Enumerable.Empty(); + return s.Walls.Select(w => new { x = w.X, y = w.Y, w = w.W, h = w.H }); + } + public IEnumerable GetStealthZones(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var s)) return Enumerable.Empty(); + return s.Zones.Select(z => new { x = z.X, y = z.Y, r = z.R }); + } + + public IEnumerable GetAreaStats(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var state)) return Enumerable.Empty(); + // 重新同步 PixelCount(容错:防止覆盖时负值或不同步) + foreach (var k in state.PixelCount.Keys.ToList()) if (state.PixelCount[k] < 0) state.PixelCount[k] = 0; + double total = state.PixelCount.Values.Sum(); + if (total <= 0) total = 1; + return state.Game.GamePlayers.Select(gp => new AreaStatDto + { + PlayerId = gp.PlayerId, + PlayerName = gp.Player.Name, + Color = gp.Player.Color, + AreaPercent = state.PixelCount.TryGetValue(gp.PlayerId, out var pc) ? pc * 100.0 / total : 0 + }); + } + + public IEnumerable GetScores(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var state)) return Enumerable.Empty(); + var list = new List(); + foreach (var gp in state.Game.GamePlayers) + { + var pid = gp.PlayerId; + state.Runtime.TryGetValue(pid, out var rt); + list.Add(new + { + playerId = pid, + name = gp.Player.Name, + color = gp.Player.Color, + score = rt?.Score ?? 0, + kills = rt?.Kills ?? 0, + paintTiles = rt?.PaintTiles ?? 0 + }); + } + return list; + } + + public Guid? GetGameIdByConnection(string connectionId) + { + if (_connectionToGame.TryGetValue(connectionId, out var gid)) return gid; else return null; + } + + public Guid? ResolveGameId(string input) + { + if (Guid.TryParse(input, out var gid)) + { + return _games.ContainsKey(gid) ? gid : null; + } + // 兼容使用房间“名称/代码”作为路由参数的情况(CreateGame 中 Code=Name) + var match = _games.Values.FirstOrDefault(g => string.Equals(g.Game.Name, input, StringComparison.OrdinalIgnoreCase)); + return match?.Game.Id; + } + + public PlayerDto? GetPlayerByConnection(string connectionId) + { + if (!_connectionToGame.TryGetValue(connectionId, out var gid)) return null; + if (!_games.TryGetValue(gid, out var state)) return null; + if (!state.PlayersByConnection.TryGetValue(connectionId, out var player)) return null; + return new PlayerDto { Id = player.Id, Name = player.Name, Color = player.Color, Score = 0 }; + } + public int GetRemainingSeconds(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var state) || state.StartedAt == null) return 0; + var elapsed = (int)(DateTime.UtcNow - state.StartedAt.Value).TotalSeconds; + var total = state.Game.DurationMinutes * 60; + var remain = Math.Max(0, total - elapsed); + if (remain == 0 && state.Game.Status == GameStatus.InProgress) + { + state.Game.Status = GameStatus.Completed; + } + return remain; + } + + public GameResultDto GetResult(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var state)) return new GameResultDto(); + // 计算面积占比(用于展示) + var areaMap = GetAreaStats(gameId).ToDictionary(a => a.PlayerId, a => a.AreaPercent); + // 组装分数榜(来自 runtime) + var entries = new List(); + foreach (var gp in state.Game.GamePlayers) + { + state.Runtime.TryGetValue(gp.PlayerId, out var rt); + entries.Add(new ScoreEntryDto + { + PlayerId = gp.PlayerId, + PlayerName = gp.Player.Name, + Color = gp.Player.Color, + Score = rt?.Score ?? 0, + Kills = rt?.Kills ?? 0, + PaintTiles = rt?.PaintTiles ?? 0, + AreaPercent = areaMap.TryGetValue(gp.PlayerId, out var ap1) ? ap1 : 0 + }); + } + var ordered = entries.OrderByDescending(e => e.Score).ToList(); + var winner = ordered.FirstOrDefault(); + return new GameResultDto + { + GameId = gameId, + WinnerPlayerId = winner?.PlayerId, + WinnerAreaPercent = winner != null ? (areaMap.TryGetValue(winner.PlayerId, out var ap2) ? ap2 : 0) : 0, + WinnerScore = winner?.Score ?? 0, + Players = ordered + }; + } + + public IEnumerable ListGames() + { + return _games.Values.Select(g => new GameDto + { + Id = g.Game.Id, + Name = g.Game.Name, + Code = g.Game.Name, + MaxPlayers = g.Game.MaxPlayers, + Status = g.Game.Status, + CurrentPlayers = g.Game.GamePlayers.Count, + DurationMinutes = g.Game.DurationMinutes, + CanvasWidth = g.Game.CanvasWidth, + CanvasHeight = g.Game.CanvasHeight, + HostPlayerName = g.HostPlayerId != null ? g.Game.GamePlayers.FirstOrDefault(x=>x.PlayerId==g.HostPlayerId)?.Player?.Name : null, + HasPassword = g.Game.Password != null, + Players = g.Game.GamePlayers.Select(gp => new PlayerDto { Id = gp.PlayerId, Name = gp.Player.Name, Color = gp.Player.Color, Score = 0 }).ToList() + }); + } + + // 清理长期空闲的等待中房间(无任何活动,未开始) + public IEnumerable CleanupIdleRooms(TimeSpan idleFor) + { + var now = DateTime.UtcNow; + var expired = _games.Where(kv => + kv.Value.Game.Status == GameStatus.Waiting + && kv.Value.StartedAt == null + && (now - kv.Value.LastActivityUtc) >= idleFor + ).Select(kv => kv.Key).ToList(); + foreach (var id in expired) + { + _games.TryRemove(id, out _); + } + return expired; + } + + public int GetPlayerCount(Guid gameId) + { + return _games.TryGetValue(gameId, out var state) ? state.Game.GamePlayers.Count : 0; + } + + public GameDto? GetGameSnapshot(Guid gameId) + { + if (!_games.TryGetValue(gameId, out var g)) return null; + return new GameDto + { + Id = g.Game.Id, + Name = g.Game.Name, + Code = g.Game.Name, + MaxPlayers = g.Game.MaxPlayers, + Status = g.Game.Status, + CurrentPlayers = g.Game.GamePlayers.Count, + DurationMinutes = g.Game.DurationMinutes, + CanvasWidth = g.Game.CanvasWidth, + CanvasHeight = g.Game.CanvasHeight, + HostPlayerName = g.HostPlayerId != null ? g.Game.GamePlayers.FirstOrDefault(x=>x.PlayerId==g.HostPlayerId)?.Player?.Name : null, + HasPassword = g.Game.Password != null, + Players = g.Game.GamePlayers.Select(gp => new PlayerDto { Id = gp.PlayerId, Name = gp.Player.Name, Color = gp.Player.Color, Score = 0 }).ToList() + }; + } +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Services/IGameManager.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Services/IGameManager.cs" new file mode 100644 index 0000000000000000000000000000000000000000..cae300886e050dfc66233d71357b3bb9a558a8a1 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/Services/IGameManager.cs" @@ -0,0 +1,52 @@ +using TerritoryGame.Application.Common; + +namespace TerritoryGame.Application.Services; + +public interface IGameManager +{ + Task CreateGameAsync(string name, int maxPlayers, int durationMinutes, string? password = null); + // 新增 password 参数用于校验密码房间 + Task AddPlayerAsync(Guid gameId, string playerName, string connectionId, string? password = null); + Task RemovePlayerAsync(Guid gameId, string connectionId); + Task StartGameAsync(Guid gameId); + Task ProcessDrawAsync(Guid gameId, string connectionId, int x, int y, int size, string color); + IEnumerable GetAreaStats(Guid gameId); + IEnumerable GetPlayerStates(Guid gameId); + (bool ok, object? moved, int? energy, bool itemsChanged) Move(Guid gameId, string connectionId, int forward, int rotate); + // 前端权威:由客户端上报绝对位置与角度 + (bool ok, object? moved, int? energy, bool itemsChanged) ClientSetTransform(Guid gameId, string connectionId, int x, int y, double angle); + (bool ok, int energy, IEnumerable? sector, IEnumerable? stats, string? color) Spray(Guid gameId, string connectionId, int radius, int angleWidthDeg, bool allowOverwrite); + // 炮弹(权威爆炸) + // 返回值新增 killFeed:每个元素包含 shooterId/shooterName、victimId/victimName、item(可能为 null) + (bool ok, int energy, IEnumerable? affected, object? explosion, IEnumerable? killed, string? color, IEnumerable? killFeed) FireCannon(Guid gameId, string connectionId); + // 地图与道具 + IEnumerable GetWalls(Guid gameId); + IEnumerable GetItems(Guid gameId); + IEnumerable GetStealthZones(Guid gameId); + // 背包:获取调用者当前道具库存 + IDictionary GetInventory(Guid gameId, string connectionId); + // 激活道具(消耗背包内一个指定类型并应用效果) + (bool ok, int energy, IDictionary inventory) ActivateItem(Guid gameId, string connectionId, string type); + // 计时器驱动:检查并刷新一批道具,返回是否新增 + bool CheckAndSpawnItems(Guid gameId); + // 获取调用者当前运行时状态(含持续效果时间戳) + object? GetSelfState(Guid gameId, string connectionId); + Guid? GetGameIdByConnection(string connectionId); + int GetRemainingSeconds(Guid gameId); + GameResultDto GetResult(Guid gameId); + IEnumerable ListGames(); + int GetPlayerCount(Guid gameId); + GameDto? GetGameSnapshot(Guid gameId); + // 允许通过字符串(可能是 Guid 或 房间号/名称)解析实际游戏 Id + Guid? ResolveGameId(string input); + // 通过连接获取玩家(用于客户端补偿获取自身信息) + PlayerDto? GetPlayerByConnection(string connectionId); + // 房主踢出指定玩家(返回:是否成功、被踢玩家Id、被踢玩家连接Id、房间是否被删除、调用者是否房主) + (bool success, Guid? removedPlayerId, string? removedConnectionId, bool gameDeleted, bool wasHostCaller, string? reason) KickPlayer(Guid gameId, string hostConnectionId, Guid targetPlayerId); + // 清理空闲房间(返回删除的房间Id) + IEnumerable CleanupIdleRooms(TimeSpan idleFor); + // 获取实时分数榜(得分/击杀/占地) + IEnumerable GetScores(Guid gameId); +} + +public record RemovePlayerResult(bool Success, bool GameDeleted, Guid? RemovedPlayerId, bool WasHost); diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/TerritoryGame.Application.csproj" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/TerritoryGame.Application.csproj" new file mode 100644 index 0000000000000000000000000000000000000000..c0c66db3a33466ee1187c064de7da6fc422e9a1f --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Application/TerritoryGame.Application.csproj" @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + net8.0 + enable + enable + + + diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Class1.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Class1.cs" new file mode 100644 index 0000000000000000000000000000000000000000..23fe78c8cfced99969de368cdae2afb2697b07a7 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Class1.cs" @@ -0,0 +1,6 @@ +namespace TerritoryGame.Domain; + +public class Class1 +{ + +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Model/Game.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Model/Game.cs" new file mode 100644 index 0000000000000000000000000000000000000000..c174d871edd7033f76526fa48905001daeb20011 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Model/Game.cs" @@ -0,0 +1,31 @@ +// Domain/Models/Game.cs +namespace TerritoryGame.Domain.Models; + +public class Game +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public GameStatus Status { get; set; } = GameStatus.Waiting; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime? StartedAt { get; set; } + public DateTime? EndedAt { get; set; } + public int MaxPlayers { get; set; } = 6; + public int DurationMinutes { get; set; } = 3; + public string? Password { get; set; } // 可选房间密码(纯文本暂存;生产应哈希) + + // 画布设置 + public int CanvasWidth { get; set; } = 800; + public int CanvasHeight { get; set; } = 600; + public string NeutralColor { get; set; } = "#CCCCCC"; + + // 导航属性 + public ICollection GamePlayers { get; set; } = new List(); + public ICollection Pixels { get; set; } = new List(); +} + +public enum GameStatus +{ + Waiting, + InProgress, + Completed +} \ No newline at end of file diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Model/GamePlayer.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Model/GamePlayer.cs" new file mode 100644 index 0000000000000000000000000000000000000000..cba51f31ed440229253d27c6db6be892ddbefa7d --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Model/GamePlayer.cs" @@ -0,0 +1,13 @@ +// Domain/Models/GamePlayer.cs +namespace TerritoryGame.Domain.Models; + +public class GamePlayer +{ + public Guid GameId { get; set; } + public Guid PlayerId { get; set; } + public int AreaOccupied { get; set; } // 占领的面积 + + // 导航属性 + public Game Game { get; set; } = null!; + public Player Player { get; set; } = null!; +} \ No newline at end of file diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Model/Pixel.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Model/Pixel.cs" new file mode 100644 index 0000000000000000000000000000000000000000..256eb4dd6491bb2f933f4967e3df80766f1100a4 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Model/Pixel.cs" @@ -0,0 +1,13 @@ +// Domain/Models/Pixel.cs +namespace TerritoryGame.Domain.Models; + +public class Pixel +{ + public Guid GameId { get; set; } + public int X { get; set; } + public int Y { get; set; } + public string? Color { get; set; } // 颜色值或null表示未涂色 + + // 导航属性 + public Game Game { get; set; } = null!; +} \ No newline at end of file diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Model/Player.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Model/Player.cs" new file mode 100644 index 0000000000000000000000000000000000000000..bf7187d40947fc4c24f771f226ad2dfa1334a162 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Model/Player.cs" @@ -0,0 +1,14 @@ +// Domain/Models/Player.cs +namespace TerritoryGame.Domain.Models; + +public class Player +{ + public Guid Id { get; set; } + public string ConnectionId { get; set; } = null!; // SignalR连接ID + public string Name { get; set; } = null!; + public string Color { get; set; } = null!; // 系统分配的颜色,格式如"#RRGGBB" + public int Score { get; set; } + + // 导航属性 + public ICollection GamePlayers { get; set; } = new List(); +} \ No newline at end of file diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Model/User.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Model/User.cs" new file mode 100644 index 0000000000000000000000000000000000000000..5be05ea65da9f0a7eca16d0d9602c5b4003ea718 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Model/User.cs" @@ -0,0 +1,9 @@ +namespace TerritoryGame.Domain.Models; + +public class User +{ + public Guid Id { get; set; } + public string Username { get; set; } = null!; + public string PasswordHash { get; set; } = null!; // {salt}.{hash} + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Services/ColorService.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Services/ColorService.cs" new file mode 100644 index 0000000000000000000000000000000000000000..8267be2affca0d014bfabbfef027f4f045d7f377 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/Services/ColorService.cs" @@ -0,0 +1,47 @@ +// Domain/Services/ColorService.cs +namespace TerritoryGame.Domain.Services; + +public static class ColorService +{ + // 预定义一组美观的、对比度足够的颜色 + private static readonly string[] PredefinedColors = + { + "#FF5252", "#FF4081", "#E040FB", "#7C4DFF", + "#536DFE", "#448AFF", "#40C4FF", "#18FFFF", + "#64FFDA", "#69F0AE", "#B2FF59", "#EEFF41", + "#FFFF00", "#FFD740", "#FFAB40", "#FF6E40" + }; + + private static readonly Random Random = new(); + private static readonly HashSet UsedColors = new(); + + public static string GetRandomColor() + { + lock (Random) + { + // 如果所有颜色都用过了,重置 + if (UsedColors.Count >= PredefinedColors.Length) + { + UsedColors.Clear(); + } + + // 随机选择一个未使用的颜色 + string color; + do + { + color = PredefinedColors[Random.Next(PredefinedColors.Length)]; + } while (UsedColors.Contains(color)); + + UsedColors.Add(color); + return color; + } + } + + public static void ReleaseColor(string color) + { + lock (UsedColors) + { + UsedColors.Remove(color); + } + } +} \ No newline at end of file diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/TerritoryGame.Domain.csproj" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/TerritoryGame.Domain.csproj" new file mode 100644 index 0000000000000000000000000000000000000000..fa71b7ae6a34999a3f96c40d9a0b870b311d11dd --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Domain/TerritoryGame.Domain.csproj" @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Class1.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Class1.cs" new file mode 100644 index 0000000000000000000000000000000000000000..23a9b3da37d7f9b9732d7fd3f44ea39d0c005d8b --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Class1.cs" @@ -0,0 +1,6 @@ +namespace TerritoryGame.Infrastructure; + +public class Class1 +{ + +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Data/TerritoryGameDbContext.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Data/TerritoryGameDbContext.cs" new file mode 100644 index 0000000000000000000000000000000000000000..5ec27680f84254a4826cfd2c21d131f9abe7c9b1 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Data/TerritoryGameDbContext.cs" @@ -0,0 +1,57 @@ +// Infrastructure/Data/TerritoryGameDbContext.cs +using Microsoft.EntityFrameworkCore; +using TerritoryGame.Domain.Models; + +namespace TerritoryGame.Infrastructure.Data; + +public class TerritoryGameDbContext : DbContext +{ + public TerritoryGameDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Players { get; set; } + public DbSet Games { get; set; } + public DbSet GamePlayers { get; set; } + public DbSet Pixels { get; set; } + public DbSet Users { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // 配置复合主键 + modelBuilder.Entity() + .HasKey(gp => new { gp.GameId, gp.PlayerId }); + + modelBuilder.Entity() + .HasKey(p => new { p.GameId, p.X, p.Y }); + + // 配置关系 - 设置为必需 + modelBuilder.Entity() + .HasOne(gp => gp.Game) + .WithMany(g => g.GamePlayers) + .HasForeignKey(gp => gp.GameId) + .IsRequired(); + + modelBuilder.Entity() + .HasOne(gp => gp.Player) + .WithMany(p => p.GamePlayers) + .HasForeignKey(gp => gp.PlayerId) + .IsRequired(); + + modelBuilder.Entity() + .HasOne(p => p.Game) + .WithMany(g => g.Pixels) + .HasForeignKey(p => p.GameId) + .IsRequired(); + + // 其他配置 + modelBuilder.Entity() + .Property(g => g.Status) + .HasConversion(); + + modelBuilder.Entity() + .HasIndex(u => u.Username) + .IsUnique(); + } +} \ No newline at end of file diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/DependencyInjection.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/DependencyInjection.cs" new file mode 100644 index 0000000000000000000000000000000000000000..9cbb2bc59ea071c204fb2da8486a310a63ab34b4 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/DependencyInjection.cs" @@ -0,0 +1,34 @@ +// Infrastructure/DependencyInjection.cs +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using StackExchange.Redis; +using TerritoryGame.Infrastructure.Data; + +namespace TerritoryGame.Infrastructure; + +public static class DependencyInjection +{ + // Infrastructure/DependencyInjection.cs + public static IServiceCollection AddInfrastructure( + this IServiceCollection services, + IConfiguration configuration) + { + // PostgreSQL配置 + services.AddDbContext(options => + options.UseNpgsql(configuration.GetConnectionString("DefaultConnection"))); + + // Redis配置 - 添加null检查 + var redisConnectionString = configuration.GetConnectionString("Redis"); + if (string.IsNullOrWhiteSpace(redisConnectionString)) + { + throw new ArgumentNullException(nameof(redisConnectionString), + "Redis connection string is not configured"); + } + + services.AddSingleton(_ => + ConnectionMultiplexer.Connect(redisConnectionString)); + + return services; + } +} \ No newline at end of file diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Migrations/20250815083643_InitCreate.Designer.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Migrations/20250815083643_InitCreate.Designer.cs" new file mode 100644 index 0000000000000000000000000000000000000000..ce7b59518741665e5ef1f8812992cfd0fb754bd1 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Migrations/20250815083643_InitCreate.Designer.cs" @@ -0,0 +1,179 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TerritoryGame.Infrastructure.Data; + +#nullable disable + +namespace TerritoryGame.Infrastructure.Migrations +{ + [DbContext(typeof(TerritoryGameDbContext))] + [Migration("20250815083643_InitCreate")] + partial class InitCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Game", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanvasHeight") + .HasColumnType("integer"); + + b.Property("CanvasWidth") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationMinutes") + .HasColumnType("integer"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxPlayers") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NeutralColor") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.GamePlayer", b => + { + b.Property("GameId") + .HasColumnType("uuid"); + + b.Property("PlayerId") + .HasColumnType("uuid"); + + b.Property("AreaOccupied") + .HasColumnType("integer"); + + b.HasKey("GameId", "PlayerId"); + + b.HasIndex("PlayerId"); + + b.ToTable("GamePlayers"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Pixel", b => + { + b.Property("GameId") + .HasColumnType("uuid"); + + b.Property("X") + .HasColumnType("integer"); + + b.Property("Y") + .HasColumnType("integer"); + + b.Property("Color") + .HasColumnType("text"); + + b.HasKey("GameId", "X", "Y"); + + b.ToTable("Pixels"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConnectionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Score") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.GamePlayer", b => + { + b.HasOne("TerritoryGame.Domain.Models.Game", "Game") + .WithMany("GamePlayers") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TerritoryGame.Domain.Models.Player", "Player") + .WithMany("GamePlayers") + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Pixel", b => + { + b.HasOne("TerritoryGame.Domain.Models.Game", "Game") + .WithMany("Pixels") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Game", b => + { + b.Navigation("GamePlayers"); + + b.Navigation("Pixels"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Player", b => + { + b.Navigation("GamePlayers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Migrations/20250815083643_InitCreate.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Migrations/20250815083643_InitCreate.cs" new file mode 100644 index 0000000000000000000000000000000000000000..e7dfdbf6b8701fad8006f7efa5385a966b4e886d --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Migrations/20250815083643_InitCreate.cs" @@ -0,0 +1,117 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TerritoryGame.Infrastructure.Migrations +{ + /// + public partial class InitCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Games", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Status = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + StartedAt = table.Column(type: "timestamp with time zone", nullable: true), + EndedAt = table.Column(type: "timestamp with time zone", nullable: true), + MaxPlayers = table.Column(type: "integer", nullable: false), + DurationMinutes = table.Column(type: "integer", nullable: false), + CanvasWidth = table.Column(type: "integer", nullable: false), + CanvasHeight = table.Column(type: "integer", nullable: false), + NeutralColor = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Games", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Players", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ConnectionId = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Color = table.Column(type: "text", nullable: false), + Score = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Players", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Pixels", + columns: table => new + { + GameId = table.Column(type: "uuid", nullable: false), + X = table.Column(type: "integer", nullable: false), + Y = table.Column(type: "integer", nullable: false), + Color = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Pixels", x => new { x.GameId, x.X, x.Y }); + table.ForeignKey( + name: "FK_Pixels_Games_GameId", + column: x => x.GameId, + principalTable: "Games", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "GamePlayers", + columns: table => new + { + GameId = table.Column(type: "uuid", nullable: false), + PlayerId = table.Column(type: "uuid", nullable: false), + AreaOccupied = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GamePlayers", x => new { x.GameId, x.PlayerId }); + table.ForeignKey( + name: "FK_GamePlayers_Games_GameId", + column: x => x.GameId, + principalTable: "Games", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_GamePlayers_Players_PlayerId", + column: x => x.PlayerId, + principalTable: "Players", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_GamePlayers_PlayerId", + table: "GamePlayers", + column: "PlayerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GamePlayers"); + + migrationBuilder.DropTable( + name: "Pixels"); + + migrationBuilder.DropTable( + name: "Players"); + + migrationBuilder.DropTable( + name: "Games"); + } + } +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Migrations/20250816150245_AddUsers.Designer.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Migrations/20250816150245_AddUsers.Designer.cs" new file mode 100644 index 0000000000000000000000000000000000000000..8daafa29d41566f94f2384867a05a6e306cdfffb --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Migrations/20250816150245_AddUsers.Designer.cs" @@ -0,0 +1,204 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TerritoryGame.Infrastructure.Data; + +#nullable disable + +namespace TerritoryGame.Infrastructure.Migrations +{ + [DbContext(typeof(TerritoryGameDbContext))] + [Migration("20250816150245_AddUsers")] + partial class AddUsers + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Game", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanvasHeight") + .HasColumnType("integer"); + + b.Property("CanvasWidth") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationMinutes") + .HasColumnType("integer"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxPlayers") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NeutralColor") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.GamePlayer", b => + { + b.Property("GameId") + .HasColumnType("uuid"); + + b.Property("PlayerId") + .HasColumnType("uuid"); + + b.Property("AreaOccupied") + .HasColumnType("integer"); + + b.HasKey("GameId", "PlayerId"); + + b.HasIndex("PlayerId"); + + b.ToTable("GamePlayers"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Pixel", b => + { + b.Property("GameId") + .HasColumnType("uuid"); + + b.Property("X") + .HasColumnType("integer"); + + b.Property("Y") + .HasColumnType("integer"); + + b.Property("Color") + .HasColumnType("text"); + + b.HasKey("GameId", "X", "Y"); + + b.ToTable("Pixels"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConnectionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Score") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.GamePlayer", b => + { + b.HasOne("TerritoryGame.Domain.Models.Game", "Game") + .WithMany("GamePlayers") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TerritoryGame.Domain.Models.Player", "Player") + .WithMany("GamePlayers") + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Pixel", b => + { + b.HasOne("TerritoryGame.Domain.Models.Game", "Game") + .WithMany("Pixels") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Game", b => + { + b.Navigation("GamePlayers"); + + b.Navigation("Pixels"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Player", b => + { + b.Navigation("GamePlayers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Migrations/20250816150245_AddUsers.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Migrations/20250816150245_AddUsers.cs" new file mode 100644 index 0000000000000000000000000000000000000000..96b6a0e6678c7ca95f1f68ad193ecdcf58e2a9e5 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Migrations/20250816150245_AddUsers.cs" @@ -0,0 +1,42 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TerritoryGame.Infrastructure.Migrations +{ + /// + public partial class AddUsers : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Username = table.Column(type: "text", nullable: false), + PasswordHash = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_Username", + table: "Users", + column: "Username", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Migrations/TerritoryGameDbContextModelSnapshot.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Migrations/TerritoryGameDbContextModelSnapshot.cs" new file mode 100644 index 0000000000000000000000000000000000000000..269197798dbf8e61cc533fd8efc682131e6bbdad --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Migrations/TerritoryGameDbContextModelSnapshot.cs" @@ -0,0 +1,201 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TerritoryGame.Infrastructure.Data; + +#nullable disable + +namespace TerritoryGame.Infrastructure.Migrations +{ + [DbContext(typeof(TerritoryGameDbContext))] + partial class TerritoryGameDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Game", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanvasHeight") + .HasColumnType("integer"); + + b.Property("CanvasWidth") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationMinutes") + .HasColumnType("integer"); + + b.Property("EndedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxPlayers") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NeutralColor") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Games"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.GamePlayer", b => + { + b.Property("GameId") + .HasColumnType("uuid"); + + b.Property("PlayerId") + .HasColumnType("uuid"); + + b.Property("AreaOccupied") + .HasColumnType("integer"); + + b.HasKey("GameId", "PlayerId"); + + b.HasIndex("PlayerId"); + + b.ToTable("GamePlayers"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Pixel", b => + { + b.Property("GameId") + .HasColumnType("uuid"); + + b.Property("X") + .HasColumnType("integer"); + + b.Property("Y") + .HasColumnType("integer"); + + b.Property("Color") + .HasColumnType("text"); + + b.HasKey("GameId", "X", "Y"); + + b.ToTable("Pixels"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConnectionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Score") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.GamePlayer", b => + { + b.HasOne("TerritoryGame.Domain.Models.Game", "Game") + .WithMany("GamePlayers") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TerritoryGame.Domain.Models.Player", "Player") + .WithMany("GamePlayers") + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Pixel", b => + { + b.HasOne("TerritoryGame.Domain.Models.Game", "Game") + .WithMany("Pixels") + .HasForeignKey("GameId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Game"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Game", b => + { + b.Navigation("GamePlayers"); + + b.Navigation("Pixels"); + }); + + modelBuilder.Entity("TerritoryGame.Domain.Models.Player", b => + { + b.Navigation("GamePlayers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Security/PasswordHasher.cs" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Security/PasswordHasher.cs" new file mode 100644 index 0000000000000000000000000000000000000000..120561dbb226db96e5ada7eb1aa3f2af50320fab --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/Security/PasswordHasher.cs" @@ -0,0 +1,32 @@ +using System.Security.Cryptography; +using System.Text; + +namespace TerritoryGame.Infrastructure.Security; + +public static class PasswordHasher +{ + public static string Hash(string password) + { + var saltBytes = RandomNumberGenerator.GetBytes(16); + var salt = Convert.ToBase64String(saltBytes); + var hash = Compute(password, salt); + return salt + "." + hash; + } + + public static bool Verify(string password, string stored) + { + var parts = stored.Split('.'); + if (parts.Length != 2) return false; + var salt = parts[0]; + var expected = parts[1]; + var actual = Compute(password, salt); + return CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(actual)); + } + + private static string Compute(string password, string salt) + { + using var sha256 = SHA256.Create(); + var bytes = Encoding.UTF8.GetBytes(password + salt); + return Convert.ToBase64String(sha256.ComputeHash(bytes)); + } +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/TerritoryGame.Infrastructure.csproj" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/TerritoryGame.Infrastructure.csproj" new file mode 100644 index 0000000000000000000000000000000000000000..9ed81c671dc3f5c942d58ce4e7e63acb161823e6 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.Infrastructure/TerritoryGame.Infrastructure.csproj" @@ -0,0 +1,24 @@ + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + net8.0 + enable + enable + + + diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.sln" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.sln" new file mode 100644 index 0000000000000000000000000000000000000000..bebba03082b585edcb61cd2623631f74570e3fe9 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/backend/src/TerritoryGame.sln" @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerritoryGame.API", "TerritoryGame.API\TerritoryGame.API.csproj", "{B94187C1-B78F-4C32-9733-88AE969266C5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerritoryGame.Application", "TerritoryGame.Application\TerritoryGame.Application.csproj", "{230A48D9-230E-43D3-836C-BC7F1852930C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerritoryGame.Domain", "TerritoryGame.Domain\TerritoryGame.Domain.csproj", "{9092F7DC-2A14-4356-A8AE-B576FFAE3B7E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerritoryGame.Infrastructure", "TerritoryGame.Infrastructure\TerritoryGame.Infrastructure.csproj", "{BD5F3259-81B2-4963-B220-4F9841B2F94B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B94187C1-B78F-4C32-9733-88AE969266C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B94187C1-B78F-4C32-9733-88AE969266C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B94187C1-B78F-4C32-9733-88AE969266C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B94187C1-B78F-4C32-9733-88AE969266C5}.Release|Any CPU.Build.0 = Release|Any CPU + {230A48D9-230E-43D3-836C-BC7F1852930C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {230A48D9-230E-43D3-836C-BC7F1852930C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {230A48D9-230E-43D3-836C-BC7F1852930C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {230A48D9-230E-43D3-836C-BC7F1852930C}.Release|Any CPU.Build.0 = Release|Any CPU + {9092F7DC-2A14-4356-A8AE-B576FFAE3B7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9092F7DC-2A14-4356-A8AE-B576FFAE3B7E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9092F7DC-2A14-4356-A8AE-B576FFAE3B7E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9092F7DC-2A14-4356-A8AE-B576FFAE3B7E}.Release|Any CPU.Build.0 = Release|Any CPU + {BD5F3259-81B2-4963-B220-4F9841B2F94B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD5F3259-81B2-4963-B220-4F9841B2F94B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD5F3259-81B2-4963-B220-4F9841B2F94B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD5F3259-81B2-4963-B220-4F9841B2F94B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/.editorconfig" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/.editorconfig" new file mode 100644 index 0000000000000000000000000000000000000000..3b510aa687ba5d3dbaec1b9c6989327f84261a21 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/.editorconfig" @@ -0,0 +1,8 @@ +[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +max_line_length = 100 diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/.gitattributes" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/.gitattributes" new file mode 100644 index 0000000000000000000000000000000000000000..6313b56c57848efce05faa7aa7e901ccfc2886ea --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/.gitattributes" @@ -0,0 +1 @@ +* text=auto eol=lf diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/.gitignore" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/.gitignore" new file mode 100644 index 0000000000000000000000000000000000000000..8ee54e8d343e466a213c8c30aa04be77126b170d --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/.gitignore" @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/.prettierrc.json" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/.prettierrc.json" new file mode 100644 index 0000000000000000000000000000000000000000..29a2402ef050746efe041b9e3393bf33796407c3 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/.prettierrc.json" @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "singleQuote": true, + "printWidth": 100 +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/eslint.config.js" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/eslint.config.js" new file mode 100644 index 0000000000000000000000000000000000000000..7807d8b33d25d86a60b25349e940aa9ae59c3d2e --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/eslint.config.js" @@ -0,0 +1,26 @@ +import { defineConfig, globalIgnores } from 'eslint/config' +import globals from 'globals' +import js from '@eslint/js' +import pluginVue from 'eslint-plugin-vue' +import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' + +export default defineConfig([ + { + name: 'app/files-to-lint', + files: ['**/*.{js,mjs,jsx,vue}'], + }, + + globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']), + + { + languageOptions: { + globals: { + ...globals.browser, + }, + }, + }, + + js.configs.recommended, + ...pluginVue.configs['flat/essential'], + skipFormatting, +]) diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/index.html" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/index.html" new file mode 100644 index 0000000000000000000000000000000000000000..d599d86183dd4a4d62aae7656babda4e156b3b84 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/index.html" @@ -0,0 +1,13 @@ + + + + + + + 鼠标大作战 + + +
+ + + diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/jsconfig.json" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/jsconfig.json" new file mode 100644 index 0000000000000000000000000000000000000000..5a1f2d222a302a174e710614c6d76531b7bda926 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/jsconfig.json" @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "paths": { + "@/*": ["./src/*"] + } + }, + "exclude": ["node_modules", "dist"] +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/package.json" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/package.json" new file mode 100644 index 0000000000000000000000000000000000000000..3a134bf6d7497c028b2b2831d20fd7a0dc046ef5 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/package.json" @@ -0,0 +1,35 @@ +{ + "name": "frontend", + "version": "0.0.0", + "private": true, + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint . --fix", + "format": "prettier --write src/" + }, + "dependencies": { + "@microsoft/signalr": "^9.0.6", + "axios": "^1.11.0", + "element-plus": "^2.10.7", + "pinia": "^3.0.3", + "vue": "^3.5.18", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "@eslint/js": "^9.31.0", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/eslint-config-prettier": "^10.2.0", + "eslint": "^9.31.0", + "eslint-plugin-vue": "~10.3.0", + "globals": "^16.3.0", + "prettier": "3.6.2", + "vite": "^7.0.6", + "vite-plugin-vue-devtools": "^8.0.0" + } +} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/pnpm-lock.yaml" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/pnpm-lock.yaml" new file mode 100644 index 0000000000000000000000000000000000000000..27b92250afe18de9adb751bba2c1f251f946a7a1 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/pnpm-lock.yaml" @@ -0,0 +1,3111 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@microsoft/signalr': + specifier: ^9.0.6 + version: 9.0.6 + axios: + specifier: ^1.11.0 + version: 1.11.0 + element-plus: + specifier: ^2.10.7 + version: 2.10.7(vue@3.5.18) + pinia: + specifier: ^3.0.3 + version: 3.0.3(vue@3.5.18) + vue: + specifier: ^3.5.18 + version: 3.5.18 + vue-router: + specifier: ^4.5.1 + version: 4.5.1(vue@3.5.18) + devDependencies: + '@eslint/js': + specifier: ^9.31.0 + version: 9.33.0 + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.1(vite@7.1.2)(vue@3.5.18) + '@vue/eslint-config-prettier': + specifier: ^10.2.0 + version: 10.2.0(eslint@9.33.0)(prettier@3.6.2) + eslint: + specifier: ^9.31.0 + version: 9.33.0 + eslint-plugin-vue: + specifier: ~10.3.0 + version: 10.3.0(eslint@9.33.0)(vue-eslint-parser@10.2.0(eslint@9.33.0)) + globals: + specifier: ^16.3.0 + version: 16.3.0 + prettier: + specifier: 3.6.2 + version: 3.6.2 + vite: + specifier: ^7.0.6 + version: 7.1.2 + vite-plugin-vue-devtools: + specifier: ^8.0.0 + version: 8.0.0(vite@7.1.2)(vue@3.5.18) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.3': + resolution: {integrity: sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.3': + resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.3': + resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.3': + resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-decorators@7.28.0': + resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.27.1': + resolution: {integrity: sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.0': + resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.3': + resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.2': + resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + engines: {node: '>=6.9.0'} + + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + + '@element-plus/icons-vue@2.3.2': + resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==} + peerDependencies: + vue: ^3.2.0 + + '@esbuild/aix-ppc64@0.25.9': + resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.9': + resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.9': + resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.9': + resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.9': + resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.9': + resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.9': + resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.9': + resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.9': + resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.9': + resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.9': + resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.9': + resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.9': + resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.9': + resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.9': + resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.9': + resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.9': + resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.9': + resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.9': + resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.9': + resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.9': + resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.9': + resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.9': + resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.9': + resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.9': + resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.9': + resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.33.0': + resolution: {integrity: sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.3': + resolution: {integrity: sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + + '@microsoft/signalr@9.0.6': + resolution: {integrity: sha512-DrhgzFWI9JE4RPTsHYRxh4yr+OhnwKz8bnJe7eIi7mLLjqhJpEb62CiUy/YbFvLqLzcGzlzz1QWgVAW0zyipMQ==} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rolldown/pluginutils@1.0.0-beta.29': + resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} + + '@rollup/rollup-android-arm-eabi@4.46.2': + resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.46.2': + resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.46.2': + resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.46.2': + resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.46.2': + resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.46.2': + resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.46.2': + resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.46.2': + resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.46.2': + resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.46.2': + resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.46.2': + resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.46.2': + resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.46.2': + resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.46.2': + resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.46.2': + resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.46.2': + resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.46.2': + resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.46.2': + resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==} + cpu: [x64] + os: [win32] + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@sxzz/popperjs-es@2.11.7': + resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/web-bluetooth@0.0.16': + resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==} + + '@vitejs/plugin-vue@6.0.1': + resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vue: ^3.2.25 + + '@vue/babel-helper-vue-transform-on@1.5.0': + resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==} + + '@vue/babel-plugin-jsx@1.5.0': + resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.5.0': + resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.18': + resolution: {integrity: sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==} + + '@vue/compiler-dom@3.5.18': + resolution: {integrity: sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==} + + '@vue/compiler-sfc@3.5.18': + resolution: {integrity: sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==} + + '@vue/compiler-ssr@3.5.18': + resolution: {integrity: sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.7': + resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==} + + '@vue/devtools-core@8.0.0': + resolution: {integrity: sha512-5bPtF0jAFnaGs4C/4+3vGRR5U+cf6Y8UWK0nJflutEDGepHxl5L9JRaPdHQYCUgrzUaf4cY4waNBEEGXrfcs3A==} + peerDependencies: + vue: ^3.0.0 + + '@vue/devtools-kit@7.7.7': + resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==} + + '@vue/devtools-kit@8.0.0': + resolution: {integrity: sha512-b11OeQODkE0bctdT0RhL684pEV2DPXJ80bjpywVCbFn1PxuL3bmMPDoJKjbMnnoWbrnUYXYzFfmMWBZAMhORkQ==} + + '@vue/devtools-shared@7.7.7': + resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} + + '@vue/devtools-shared@8.0.0': + resolution: {integrity: sha512-jrKnbjshQCiOAJanoeJjTU7WaCg0Dz2BUal6SaR6VM/P3hiFdX5Q6Pxl73ZMnrhCxNK9nAg5hvvRGqs+6dtU1g==} + + '@vue/eslint-config-prettier@10.2.0': + resolution: {integrity: sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw==} + peerDependencies: + eslint: '>= 8.21.0' + prettier: '>= 3.0.0' + + '@vue/reactivity@3.5.18': + resolution: {integrity: sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==} + + '@vue/runtime-core@3.5.18': + resolution: {integrity: sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==} + + '@vue/runtime-dom@3.5.18': + resolution: {integrity: sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==} + + '@vue/server-renderer@3.5.18': + resolution: {integrity: sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==} + peerDependencies: + vue: 3.5.18 + + '@vue/shared@3.5.18': + resolution: {integrity: sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==} + + '@vueuse/core@9.13.0': + resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==} + + '@vueuse/metadata@9.13.0': + resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==} + + '@vueuse/shared@9.13.0': + resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansis@4.1.0: + resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} + engines: {node: '>=14'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.11.0: + resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + birpc@2.5.0: + resolution: {integrity: sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + browserslist@4.25.2: + resolution: {integrity: sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001735: + resolution: {integrity: sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + default-browser-id@5.0.0: + resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + engines: {node: '>=18'} + + default-browser@5.2.1: + resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.201: + resolution: {integrity: sha512-ZG65vsrLClodGqywuigc+7m0gr4ISoTQttfVh7nfpLv0M7SIwF4WbFNEOywcqTiujs12AUeeXbFyQieDICAIxg==} + + element-plus@2.10.7: + resolution: {integrity: sha512-bL4yhepL8/0NEQA5+N2Q6ZVKLipIDkiQjK2mqtSmGh6CxJk1yaBMdG5HXfYkbk1htNcT3ULk9g23lzT323JGcA==} + peerDependencies: + vue: ^3.2.0 + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.9: + resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.4: + resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-vue@10.3.0: + resolution: {integrity: sha512-A0u9snqjCfYaPnqqOaH6MBLVWDUIN4trXn8J3x67uDcXvR7X6Ut8p16N+nYhMCQ9Y7edg2BIRGzfyZsY0IdqoQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 + vue-eslint-parser: ^10.0.0 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.33.0: + resolution: {integrity: sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + + execa@9.6.0: + resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} + engines: {node: ^18.19.0 || >=20.5.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-cookie@2.2.0: + resolution: {integrity: sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.3.0: + resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.1.5: + resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} + engines: {node: ^18 || >=20} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pinia@3.0.3: + resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-ms@9.2.0: + resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} + engines: {node: '>=18'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.46.2: + resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-applescript@7.0.0: + resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} + engines: {node: '>=18'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} + engines: {node: ^14.18.0 || >=16.0.0} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + unplugin-utils@0.2.5: + resolution: {integrity: sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==} + engines: {node: '>=18.12.0'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite-dev-rpc@1.1.0: + resolution: {integrity: sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 + + vite-hot-client@2.1.0: + resolution: {integrity: sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite-plugin-inspect@11.3.2: + resolution: {integrity: sha512-nzwvyFQg58XSMAmKVLr2uekAxNYvAbz1lyPmCAFVIBncCgN9S/HPM+2UM9Q9cvc4JEbC5ZBgwLAdaE2onmQuKg==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + vite-plugin-vue-devtools@8.0.0: + resolution: {integrity: sha512-9bWQig8UMu3nPbxX86NJv56aelpFYoBHxB5+pxuQz3pa3Tajc1ezRidj/0dnADA4/UHuVIfwIVYHOvMXYcPshg==} + engines: {node: '>=v14.21.3'} + peerDependencies: + vite: ^6.0.0 || ^7.0.0-0 + + vite-plugin-vue-inspector@5.3.2: + resolution: {integrity: sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==} + peerDependencies: + vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite@7.1.2: + resolution: {integrity: sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-eslint-parser@10.2.0: + resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + vue-router@4.5.1: + resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==} + peerDependencies: + vue: ^3.2.0 + + vue@3.5.18: + resolution: {integrity: sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors@2.1.1: + resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} + engines: {node: '>=18'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.0': {} + + '@babel/core@7.28.3': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) + '@babel/helpers': 7.28.3 + '@babel/parser': 7.28.3 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.2 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.3) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.27.1': + dependencies: + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.2 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.3': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 + + '@babel/parser@7.28.3': + dependencies: + '@babel/types': 7.28.2 + + '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.28.3) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.3) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 + + '@babel/traverse@7.28.3': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.3 + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.2': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@ctrl/tinycolor@3.6.1': {} + + '@element-plus/icons-vue@2.3.2(vue@3.5.18)': + dependencies: + vue: 3.5.18 + + '@esbuild/aix-ppc64@0.25.9': + optional: true + + '@esbuild/android-arm64@0.25.9': + optional: true + + '@esbuild/android-arm@0.25.9': + optional: true + + '@esbuild/android-x64@0.25.9': + optional: true + + '@esbuild/darwin-arm64@0.25.9': + optional: true + + '@esbuild/darwin-x64@0.25.9': + optional: true + + '@esbuild/freebsd-arm64@0.25.9': + optional: true + + '@esbuild/freebsd-x64@0.25.9': + optional: true + + '@esbuild/linux-arm64@0.25.9': + optional: true + + '@esbuild/linux-arm@0.25.9': + optional: true + + '@esbuild/linux-ia32@0.25.9': + optional: true + + '@esbuild/linux-loong64@0.25.9': + optional: true + + '@esbuild/linux-mips64el@0.25.9': + optional: true + + '@esbuild/linux-ppc64@0.25.9': + optional: true + + '@esbuild/linux-riscv64@0.25.9': + optional: true + + '@esbuild/linux-s390x@0.25.9': + optional: true + + '@esbuild/linux-x64@0.25.9': + optional: true + + '@esbuild/netbsd-arm64@0.25.9': + optional: true + + '@esbuild/netbsd-x64@0.25.9': + optional: true + + '@esbuild/openbsd-arm64@0.25.9': + optional: true + + '@esbuild/openbsd-x64@0.25.9': + optional: true + + '@esbuild/openharmony-arm64@0.25.9': + optional: true + + '@esbuild/sunos-x64@0.25.9': + optional: true + + '@esbuild/win32-arm64@0.25.9': + optional: true + + '@esbuild/win32-ia32@0.25.9': + optional: true + + '@esbuild/win32-x64@0.25.9': + optional: true + + '@eslint-community/eslint-utils@4.7.0(eslint@9.33.0)': + dependencies: + eslint: 9.33.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.1': {} + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.33.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.3': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.30 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.30': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@microsoft/signalr@9.0.6': + dependencies: + abort-controller: 3.0.0 + eventsource: 2.0.2 + fetch-cookie: 2.2.0 + node-fetch: 2.7.0 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - encoding + - utf-8-validate + + '@pkgr/core@0.2.9': {} + + '@polka/url@1.0.0-next.29': {} + + '@rolldown/pluginutils@1.0.0-beta.29': {} + + '@rollup/rollup-android-arm-eabi@4.46.2': + optional: true + + '@rollup/rollup-android-arm64@4.46.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.46.2': + optional: true + + '@rollup/rollup-darwin-x64@4.46.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.46.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.46.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.46.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.46.2': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.46.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.46.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.46.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.46.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.46.2': + optional: true + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@sxzz/popperjs-es@2.11.7': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.20 + + '@types/lodash@4.17.20': {} + + '@types/web-bluetooth@0.0.16': {} + + '@vitejs/plugin-vue@6.0.1(vite@7.1.2)(vue@3.5.18)': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.29 + vite: 7.1.2 + vue: 3.5.18 + + '@vue/babel-helper-vue-transform-on@1.5.0': {} + + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.28.3)': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + '@vue/babel-helper-vue-transform-on': 1.5.0 + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.28.3) + '@vue/shared': 3.5.18 + optionalDependencies: + '@babel/core': 7.28.3 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.28.3)': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/core': 7.28.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/parser': 7.28.3 + '@vue/compiler-sfc': 3.5.18 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.18': + dependencies: + '@babel/parser': 7.28.3 + '@vue/shared': 3.5.18 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.18': + dependencies: + '@vue/compiler-core': 3.5.18 + '@vue/shared': 3.5.18 + + '@vue/compiler-sfc@3.5.18': + dependencies: + '@babel/parser': 7.28.3 + '@vue/compiler-core': 3.5.18 + '@vue/compiler-dom': 3.5.18 + '@vue/compiler-ssr': 3.5.18 + '@vue/shared': 3.5.18 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.18': + dependencies: + '@vue/compiler-dom': 3.5.18 + '@vue/shared': 3.5.18 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.7': + dependencies: + '@vue/devtools-kit': 7.7.7 + + '@vue/devtools-core@8.0.0(vite@7.1.2)(vue@3.5.18)': + dependencies: + '@vue/devtools-kit': 8.0.0 + '@vue/devtools-shared': 8.0.0 + mitt: 3.0.1 + nanoid: 5.1.5 + pathe: 2.0.3 + vite-hot-client: 2.1.0(vite@7.1.2) + vue: 3.5.18 + transitivePeerDependencies: + - vite + + '@vue/devtools-kit@7.7.7': + dependencies: + '@vue/devtools-shared': 7.7.7 + birpc: 2.5.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-kit@8.0.0': + dependencies: + '@vue/devtools-shared': 8.0.0 + birpc: 2.5.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-shared@7.7.7': + dependencies: + rfdc: 1.4.1 + + '@vue/devtools-shared@8.0.0': + dependencies: + rfdc: 1.4.1 + + '@vue/eslint-config-prettier@10.2.0(eslint@9.33.0)(prettier@3.6.2)': + dependencies: + eslint: 9.33.0 + eslint-config-prettier: 10.1.8(eslint@9.33.0) + eslint-plugin-prettier: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.33.0))(eslint@9.33.0)(prettier@3.6.2) + prettier: 3.6.2 + transitivePeerDependencies: + - '@types/eslint' + + '@vue/reactivity@3.5.18': + dependencies: + '@vue/shared': 3.5.18 + + '@vue/runtime-core@3.5.18': + dependencies: + '@vue/reactivity': 3.5.18 + '@vue/shared': 3.5.18 + + '@vue/runtime-dom@3.5.18': + dependencies: + '@vue/reactivity': 3.5.18 + '@vue/runtime-core': 3.5.18 + '@vue/shared': 3.5.18 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.18(vue@3.5.18)': + dependencies: + '@vue/compiler-ssr': 3.5.18 + '@vue/shared': 3.5.18 + vue: 3.5.18 + + '@vue/shared@3.5.18': {} + + '@vueuse/core@9.13.0(vue@3.5.18)': + dependencies: + '@types/web-bluetooth': 0.0.16 + '@vueuse/metadata': 9.13.0 + '@vueuse/shared': 9.13.0(vue@3.5.18) + vue-demi: 0.14.10(vue@3.5.18) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@9.13.0': {} + + '@vueuse/shared@9.13.0(vue@3.5.18)': + dependencies: + vue-demi: 0.14.10(vue@3.5.18) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansis@4.1.0: {} + + argparse@2.0.1: {} + + async-validator@4.2.5: {} + + asynckit@0.4.0: {} + + axios@1.11.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + birpc@2.5.0: {} + + boolbase@1.0.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + browserslist@4.25.2: + dependencies: + caniuse-lite: 1.0.30001735 + electron-to-chromium: 1.5.201 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.2) + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.0.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001735: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.1.3: {} + + dayjs@1.11.13: {} + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + default-browser-id@5.0.0: {} + + default-browser@5.2.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.0 + + define-lazy-prop@3.0.0: {} + + delayed-stream@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.201: {} + + element-plus@2.10.7(vue@3.5.18): + dependencies: + '@ctrl/tinycolor': 3.6.1 + '@element-plus/icons-vue': 2.3.2(vue@3.5.18) + '@floating-ui/dom': 1.7.3 + '@popperjs/core': '@sxzz/popperjs-es@2.11.7' + '@types/lodash': 4.17.20 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 9.13.0(vue@3.5.18) + async-validator: 4.2.5 + dayjs: 1.11.13 + escape-html: 1.0.3 + lodash: 4.17.21 + lodash-es: 4.17.21 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.18 + transitivePeerDependencies: + - '@vue/composition-api' + + entities@4.5.0: {} + + error-stack-parser-es@1.0.5: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.25.9: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.9 + '@esbuild/android-arm': 0.25.9 + '@esbuild/android-arm64': 0.25.9 + '@esbuild/android-x64': 0.25.9 + '@esbuild/darwin-arm64': 0.25.9 + '@esbuild/darwin-x64': 0.25.9 + '@esbuild/freebsd-arm64': 0.25.9 + '@esbuild/freebsd-x64': 0.25.9 + '@esbuild/linux-arm': 0.25.9 + '@esbuild/linux-arm64': 0.25.9 + '@esbuild/linux-ia32': 0.25.9 + '@esbuild/linux-loong64': 0.25.9 + '@esbuild/linux-mips64el': 0.25.9 + '@esbuild/linux-ppc64': 0.25.9 + '@esbuild/linux-riscv64': 0.25.9 + '@esbuild/linux-s390x': 0.25.9 + '@esbuild/linux-x64': 0.25.9 + '@esbuild/netbsd-arm64': 0.25.9 + '@esbuild/netbsd-x64': 0.25.9 + '@esbuild/openbsd-arm64': 0.25.9 + '@esbuild/openbsd-x64': 0.25.9 + '@esbuild/openharmony-arm64': 0.25.9 + '@esbuild/sunos-x64': 0.25.9 + '@esbuild/win32-arm64': 0.25.9 + '@esbuild/win32-ia32': 0.25.9 + '@esbuild/win32-x64': 0.25.9 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.33.0): + dependencies: + eslint: 9.33.0 + + eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.33.0))(eslint@9.33.0)(prettier@3.6.2): + dependencies: + eslint: 9.33.0 + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@9.33.0) + + eslint-plugin-vue@10.3.0(eslint@9.33.0)(vue-eslint-parser@10.2.0(eslint@9.33.0)): + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0) + eslint: 9.33.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.1.2 + semver: 7.7.2 + vue-eslint-parser: 10.2.0(eslint@9.33.0) + xml-name-validator: 4.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.33.0: + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.33.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.33.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + 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.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + event-target-shim@5.0.1: {} + + eventsource@2.0.2: {} + + execa@9.6.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.2.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.1 + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fetch-cookie@2.2.0: + dependencies: + set-cookie-parser: 2.7.1 + tough-cookie: 4.1.4 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.3.0: {} + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hookable@5.5.3: {} + + human-signals@8.0.1: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-plain-obj@4.1.0: {} + + is-stream@4.0.1: {} + + is-unicode-supported@2.1.0: {} + + is-what@4.1.16: {} + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kolorist@1.8.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.21: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.17.21 + lodash-es: 4.17.21 + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + memoize-one@6.0.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + mitt@3.0.1: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + nanoid@5.1.5: {} + + natural-compare@1.4.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-releases@2.0.19: {} + + normalize-wheel-es@1.2.0: {} + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + ohash@2.0.11: {} + + open@10.2.0: + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-ms@4.0.0: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pinia@3.0.3(vue@3.5.18): + dependencies: + '@vue/devtools-api': 7.7.7 + vue: 3.5.18 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.6.2: {} + + pretty-ms@9.2.0: + dependencies: + parse-ms: 4.0.0 + + proxy-from-env@1.1.0: {} + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} + + querystringify@2.2.0: {} + + requires-port@1.0.0: {} + + resolve-from@4.0.0: {} + + rfdc@1.4.1: {} + + rollup@4.46.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.46.2 + '@rollup/rollup-android-arm64': 4.46.2 + '@rollup/rollup-darwin-arm64': 4.46.2 + '@rollup/rollup-darwin-x64': 4.46.2 + '@rollup/rollup-freebsd-arm64': 4.46.2 + '@rollup/rollup-freebsd-x64': 4.46.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.46.2 + '@rollup/rollup-linux-arm-musleabihf': 4.46.2 + '@rollup/rollup-linux-arm64-gnu': 4.46.2 + '@rollup/rollup-linux-arm64-musl': 4.46.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.46.2 + '@rollup/rollup-linux-ppc64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-musl': 4.46.2 + '@rollup/rollup-linux-s390x-gnu': 4.46.2 + '@rollup/rollup-linux-x64-gnu': 4.46.2 + '@rollup/rollup-linux-x64-musl': 4.46.2 + '@rollup/rollup-win32-arm64-msvc': 4.46.2 + '@rollup/rollup-win32-ia32-msvc': 4.46.2 + '@rollup/rollup-win32-x64-msvc': 4.46.2 + fsevents: 2.3.3 + + run-applescript@7.0.0: {} + + semver@6.3.1: {} + + semver@7.7.2: {} + + set-cookie-parser@2.7.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + sirv@3.0.1: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + speakingurl@14.0.1: {} + + strip-final-newline@4.0.0: {} + + strip-json-comments@3.1.1: {} + + superjson@2.2.2: + dependencies: + copy-anything: 3.0.5 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + synckit@0.11.11: + dependencies: + '@pkgr/core': 0.2.9 + + tinyglobby@0.2.14: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + totalist@3.0.1: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tr46@0.0.3: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + unicorn-magic@0.3.0: {} + + universalify@0.2.0: {} + + unplugin-utils@0.2.5: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + update-browserslist-db@1.1.3(browserslist@4.25.2): + dependencies: + browserslist: 4.25.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + util-deprecate@1.0.2: {} + + vite-dev-rpc@1.1.0(vite@7.1.2): + dependencies: + birpc: 2.5.0 + vite: 7.1.2 + vite-hot-client: 2.1.0(vite@7.1.2) + + vite-hot-client@2.1.0(vite@7.1.2): + dependencies: + vite: 7.1.2 + + vite-plugin-inspect@11.3.2(vite@7.1.2): + dependencies: + ansis: 4.1.0 + debug: 4.4.1 + error-stack-parser-es: 1.0.5 + ohash: 2.0.11 + open: 10.2.0 + perfect-debounce: 1.0.0 + sirv: 3.0.1 + unplugin-utils: 0.2.5 + vite: 7.1.2 + vite-dev-rpc: 1.1.0(vite@7.1.2) + transitivePeerDependencies: + - supports-color + + vite-plugin-vue-devtools@8.0.0(vite@7.1.2)(vue@3.5.18): + dependencies: + '@vue/devtools-core': 8.0.0(vite@7.1.2)(vue@3.5.18) + '@vue/devtools-kit': 8.0.0 + '@vue/devtools-shared': 8.0.0 + execa: 9.6.0 + sirv: 3.0.1 + vite: 7.1.2 + vite-plugin-inspect: 11.3.2(vite@7.1.2) + vite-plugin-vue-inspector: 5.3.2(vite@7.1.2) + transitivePeerDependencies: + - '@nuxt/kit' + - supports-color + - vue + + vite-plugin-vue-inspector@5.3.2(vite@7.1.2): + dependencies: + '@babel/core': 7.28.3 + '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.3) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.3) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.3) + '@vue/compiler-dom': 3.5.18 + kolorist: 1.8.0 + magic-string: 0.30.17 + vite: 7.1.2 + transitivePeerDependencies: + - supports-color + + vite@7.1.2: + dependencies: + esbuild: 0.25.9 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.46.2 + tinyglobby: 0.2.14 + optionalDependencies: + fsevents: 2.3.3 + + vue-demi@0.14.10(vue@3.5.18): + dependencies: + vue: 3.5.18 + + vue-eslint-parser@10.2.0(eslint@9.33.0): + dependencies: + debug: 4.4.1 + eslint: 9.33.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + vue-router@4.5.1(vue@3.5.18): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.18 + + vue@3.5.18: + dependencies: + '@vue/compiler-dom': 3.5.18 + '@vue/compiler-sfc': 3.5.18 + '@vue/runtime-dom': 3.5.18 + '@vue/server-renderer': 3.5.18(vue@3.5.18) + '@vue/shared': 3.5.18 + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + ws@7.5.10: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.0 + + xml-name-validator@4.0.0: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + yoctocolors@2.1.1: {} diff --git "a/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/vite.config.js" "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/vite.config.js" new file mode 100644 index 0000000000000000000000000000000000000000..4217010a3178372181948ce34c4d5045dfa18325 --- /dev/null +++ "b/\346\232\221\346\234\237\351\233\206\350\256\255/06\351\241\271\347\233\256\346\272\220\344\273\243\347\240\201/frontend/vite.config.js" @@ -0,0 +1,18 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueDevTools from 'vite-plugin-vue-devtools' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + vueDevTools(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + }, + }, +})