diff --git a/apps/server/eslint-rules/prefer-relative-for-local-server-path.mjs b/apps/server/eslint-rules/prefer-relative-for-local-server-path.mjs new file mode 100644 index 0000000..c73da95 --- /dev/null +++ b/apps/server/eslint-rules/prefer-relative-for-local-server-path.mjs @@ -0,0 +1,77 @@ +import path from 'node:path'; + +const SERVER_ALIAS_PREFIX = '@server/'; + +const toPosixPath = (value) => value.split(path.sep).join('/'); + +const toRelativeImportPath = (fromDir, targetPath) => { + const relativePath = path.relative(fromDir, targetPath); + const posixRelativePath = toPosixPath(relativePath); + + if (!posixRelativePath || posixRelativePath.startsWith('..')) { + return null; + } + + return posixRelativePath.startsWith('.') ? posixRelativePath : `./${posixRelativePath}`; +}; + +export default { + meta: { + type: 'suggestion', + docs: { + description: + 'Warn when @server alias is used even though the import can be written as a same-level relative path.', + }, + schema: [], + messages: { + preferRelative: + 'この import は相対参照で書けます。{{suggestion}} を使ってください(@server は遡りが必要な場合のみ使用)。', + }, + }, + create(context) { + const filename = context.filename; + if (!filename || filename === '') { + return {}; + } + + const srcRoot = path.resolve(process.cwd(), 'src'); + const absoluteFilename = path.resolve(filename); + + if (!absoluteFilename.startsWith(`${srcRoot}${path.sep}`)) { + return {}; + } + + const fromDirectory = path.dirname(absoluteFilename); + + return { + ImportDeclaration(node) { + const rawImportPath = node.source?.value; + if (typeof rawImportPath !== 'string') { + return; + } + + if (!rawImportPath.startsWith(SERVER_ALIAS_PREFIX)) { + return; + } + + const aliasedSubPath = rawImportPath.slice(SERVER_ALIAS_PREFIX.length); + if (!aliasedSubPath) { + return; + } + + const targetPath = path.resolve(srcRoot, aliasedSubPath); + const suggestion = toRelativeImportPath(fromDirectory, targetPath); + + if (!suggestion) { + return; + } + + context.report({ + node: node.source, + messageId: 'preferRelative', + data: { suggestion }, + }); + }, + }; + }, +}; diff --git a/apps/server/eslint.config.mjs b/apps/server/eslint.config.mjs new file mode 100644 index 0000000..f2adfcd --- /dev/null +++ b/apps/server/eslint.config.mjs @@ -0,0 +1,26 @@ +import tsParser from '@typescript-eslint/parser'; +import preferRelativeForLocalServerPath from './eslint-rules/prefer-relative-for-local-server-path.mjs'; + +export default [ + { + ignores: ['dist/**', 'node_modules/**'], + }, + { + files: ['src/**/*.ts'], + languageOptions: { + parser: tsParser, + ecmaVersion: 'latest', + sourceType: 'module', + }, + plugins: { + local: { + rules: { + 'prefer-relative-for-local-server-path': preferRelativeForLocalServerPath, + }, + }, + }, + rules: { + 'local/prefer-relative-for-local-server-path': 'warn', + }, + }, +]; \ No newline at end of file diff --git a/apps/server/package.json b/apps/server/package.json index 632e620..903089c 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -5,7 +5,8 @@ "main": "dist/index.js", "scripts": { "dev": "tsx watch src/index.ts", - "build": "tsc", + "build": "tsc && tsc-alias", + "lint": "eslint src --ext .ts", "start": "node dist/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, @@ -20,6 +21,9 @@ "devDependencies": { "@types/node": "^24.10.11", "@types/socket.io": "^3.0.2", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^10.0.2", + "tsc-alias": "^1.8.16", "tsx": "^4.21.0", "typescript": "^5.9.3" } diff --git a/apps/server/src/domains/game/GameHandler.ts b/apps/server/src/domains/game/GameHandler.ts index 2a45b4e..d1409db 100644 --- a/apps/server/src/domains/game/GameHandler.ts +++ b/apps/server/src/domains/game/GameHandler.ts @@ -1,99 +1,34 @@ import { Server, Socket } from "socket.io"; import { GameManager } from "./GameManager"; -import { RoomManager } from "../room/RoomManager"; -import { protocol, roomConsts } from "@repo/shared"; +import { RoomManager } from "@server/domains/room/RoomManager"; +import { protocol } from "@repo/shared"; import type { playerTypes } from "@repo/shared"; +import { onPing } from "./handlers/onPing"; +import { onStartGame } from "./handlers/onStartGame"; +import { onReadyForGame } from "./handlers/onReadyForGame"; +import { onMove } from "./handlers/onMove"; +import { onDisconnect } from "./handlers/onDisconnect"; export const registerGameHandlers = (io: Server, socket: Socket, gameManager: GameManager, roomManager: RoomManager) => { // クライアントから送られてきた時刻をそのまま返しつつ、サーバーの現在時刻も添える socket.on(protocol.SocketEvents.PING, (clientTime: number) => { - socket.emit(protocol.SocketEvents.PONG, { clientTime, serverTime: Date.now() }); + onPing(socket, clientTime); }); // ゲーム開始要求処理 socket.on(protocol.SocketEvents.START_GAME, () => { - const room = roomManager.getRoomByOwnerId(socket.id); - if (!room) { - console.log("[GameHandler] START_GAME ignored (no room)", { socketId: socket.id }); - return; - } - - if (room.status === roomConsts.RoomPhase.PLAYING) { - console.log("[GameHandler] START_GAME ignored (already playing)", { roomId: room.roomId }); - return; - } - - console.log("[GameHandler] START_GAME accepted", { - roomId: room.roomId, - ownerId: socket.id, - totalPlayers: room.players.length - }); - - if (room) { - room.status = roomConsts.RoomPhase.PLAYING; - - const playerIds = room.players.map((p: { id: string }) => p.id); - - // 同ルーム全プレイヤーのゲーム管理登録 - room.players.forEach((p: { id: string }) => { - gameManager.addPlayer(p.id); - }); - - // 20Hzのゲームループを開始し、毎フレームの送信処理を定義 - gameManager.startGameLoop( - room.roomId, - playerIds, - (tickData) => { - // 1. 各プレイヤーの最新座標をクライアントに送信 - tickData.players.forEach((playerData) => { - io.to(room.roomId).emit(protocol.SocketEvents.UPDATE_PLAYER, playerData); - }); - - // 2. 差分があれば、ルーム内の全員に一斉送信 - if (tickData.cellUpdates.length > 0) { - io.to(room.roomId).emit(protocol.SocketEvents.UPDATE_MAP_CELLS, tickData.cellUpdates); - } - }, - - () => { - // 3分経過時に GameLoop から呼ばれる処理 - console.log(`[GameHandler] ルーム ${room.roomId} のゲームが終了しました (3分経過)`); - io.to(room.roomId).emit(protocol.SocketEvents.GAME_END); // クライアントへ終了通知 - room.status = roomConsts.RoomPhase.WAITING; // ルーム状態を待機に戻す - } - ); - - // GameManagerから開始時刻を取得し、GAME_STARTイベントにデータを乗せて送る - const startTime = gameManager.getRoomStartTime(room.roomId) || Date.now(); - io.to(room.roomId).emit(protocol.SocketEvents.GAME_START, { startTime }); - } + onStartGame(io, gameManager, roomManager, socket.id); }); // 画面準備完了通知受信時初期データ返却 socket.on(protocol.SocketEvents.READY_FOR_GAME, () => { - const allPlayers = gameManager.getAllPlayers(); - socket.emit(protocol.SocketEvents.CURRENT_PLAYERS, allPlayers); - console.log("[GameHandler] READY_FOR_GAME received", { socketId: socket.id, totalPlayers: allPlayers.length }); - - // 準備が完了したクライアントに対して、改めて開始時刻を個別に教える - // Socket.ioの仕様上、socket.roomsには自身のIDと参加中のルームIDが含まれるため、そこからルームIDを特定する - const roomId = Array.from(socket.rooms).find(room => room !== socket.id); - if (roomId) { - const startTime = gameManager.getRoomStartTime(roomId); - if (startTime) { - // io.to() による全員への一斉送信ではなく、socket.emit() でこの本人にだけ送る - socket.emit(protocol.SocketEvents.GAME_START, { startTime }); - console.log("[GameHandler] GAME_START sent to ready client", { socketId: socket.id, roomId, startTime }); - } - } else { - console.log("[GameHandler] READY_FOR_GAME missing roomId", { socketId: socket.id }); - } + onReadyForGame(socket, gameManager); }); // ゲームプレイ中イベント群 socket.on(protocol.SocketEvents.MOVE, (data: playerTypes.MovePayload) => { - gameManager.movePlayer(socket.id, data.x, data.y); + onMove(gameManager, socket.id, data); }); }; @@ -102,9 +37,5 @@ * 切断時のゲームクリーンアップ処理 */ export const handleGameDisconnect = (io: Server, gameManager: GameManager, playerId: string) => { - // ゲームからの除外処理 - gameManager.removePlayer(playerId); - // 全体にプレイヤー削除を通知 - io.emit(protocol.SocketEvents.REMOVE_PLAYER, playerId); - console.log("[GameHandler] player removed", { playerId }); + onDisconnect(io, gameManager, playerId); }; \ No newline at end of file diff --git a/apps/server/src/domains/game/GameManager.ts b/apps/server/src/domains/game/GameManager.ts index 332ef67..97ef4cf 100644 --- a/apps/server/src/domains/game/GameManager.ts +++ b/apps/server/src/domains/game/GameManager.ts @@ -1,68 +1,42 @@ -import { Player } from "./entities/Player.js"; -import { config } from "@repo/shared"; -import { MapStore } from "./states/MapStore"; -import { gridMapLogic } from "@repo/shared"; -import type { gridMapTypes } from "@repo/shared"; import { GameLoop, type TickData } from "./GameLoop"; +import type { gridMapTypes } from "@repo/shared"; +import { Player } from "./entities/Player.js"; +import { PlayerRegistry } from "./application/services/PlayerRegistry"; +import { GameSessionService } from "./application/services/GameSessionService"; // プレイヤー集合の生成・更新・参照管理クラス export class GameManager { - private players: Map; - private mapStore: MapStore; - private gameLoops: Map; // NodeJS.Timeout から変更 - private roomStartTimes: Map; // ルームごとのゲーム開始時間を保持する + private playerRegistry: PlayerRegistry; + private gameSessionService: GameSessionService; constructor() { - this.players = new Map(); - this.mapStore = new MapStore(); - this.gameLoops = new Map(); - this.roomStartTimes = new Map(); + this.playerRegistry = new PlayerRegistry(); + this.gameSessionService = new GameSessionService(this.playerRegistry.getPlayersRef()); } // 外部(GameHandlerなど)から開始時刻を取得できるようにする getRoomStartTime(roomId: string): number | undefined { - return this.roomStartTimes.get(roomId); + return this.gameSessionService.getRoomStartTime(roomId); } // 新規プレイヤー登録と初期位置設定処理 addPlayer(id: string): Player { - const player = new Player(id); - player.x = config.GAME_CONFIG.GRID_COLS / 2; - player.y = config.GAME_CONFIG.GRID_ROWS / 2; - this.players.set(id, player); - console.log("[GameManager] player added", { playerId: id, totalPlayers: this.players.size }); - return player; + return this.playerRegistry.addPlayer(id); } // プレイヤー登録解除処理 removePlayer(id: string) { - const existed = this.players.delete(id); - if (existed) { - console.log("[GameManager] player removed", { playerId: id, totalPlayers: this.players.size }); - } else { - console.log("[GameManager] player remove ignored (not found)", { playerId: id }); - } + this.playerRegistry.removePlayer(id); } // 指定IDプレイヤー参照取得 getPlayer(id: string) { - return this.players.get(id); + return this.playerRegistry.getPlayer(id); } // 指定プレイヤー座標更新処理 movePlayer(id: string, x: number, y: number) { - const player = this.players.get(id); - if (player) { - console.log(`Move Request -> ID:${id.slice(0,4)} x:${Math.round(x)} y:${Math.round(y)}`); - if (typeof x !== "number" || typeof y !== "number" || isNaN(x) || isNaN(y)) { - console.log("⚠️ 無効なデータなので無視しました"); - return; - } - player.x = x; - player.y = y; - } else { - console.log("[GameManager] move ignored (player not found)", { playerId: id }); - } + this.playerRegistry.movePlayer(id, x, y); } /** @@ -77,67 +51,23 @@ onTick: (data: TickData) => void, onGameEnd: () => void ) { - if (this.gameLoops.has(roomId)) { - console.log("[GameManager] startGameLoop ignored (already running)", { roomId }); - return; - } - - const tickRate = config.GAME_CONFIG.PLAYER_POSITION_UPDATE_MS; - - // ループ開始時に、このルームの開始時刻を記憶する - this.roomStartTimes.set(roomId, Date.now()); - - // GameLoopインスタンスを生成し、参照を渡す - const loop = new GameLoop( - roomId, - tickRate, - playerIds, - this.players, - this.mapStore, - onTick, - () => { - // GameLoopが終了した時の処理 - this.roomStartTimes.delete(roomId); - this.gameLoops.delete(roomId); - onGameEnd(); // GameHandlerへ終了を伝える - } - ); - - loop.start(); - this.gameLoops.set(roomId, loop); - console.log("[GameManager] game loop started", { roomId, playerCount: playerIds.length }); + this.gameSessionService.startGameLoop(roomId, playerIds, onTick, onGameEnd); } /** * ゲームループを停止する */ stopGameLoop(roomId: string) { - const loop = this.gameLoops.get(roomId); - if (loop) { - loop.stop(); - this.gameLoops.delete(roomId); - this.roomStartTimes.delete(roomId); // 停止時も忘れずクリア - console.log("[GameManager] game loop stopped", { roomId }); - } else { - console.log("[GameManager] stopGameLoop ignored (not running)", { roomId }); - } + this.gameSessionService.stopGameLoop(roomId); } // 登録中全プレイヤー配列取得 getAllPlayers() { - return Array.from(this.players.values()); + return this.playerRegistry.getAllPlayers(); } // 【一時的】移動したプレイヤーの足元を塗り、差分を返すメソッド public paintAndGetUpdates(playerId: string): gridMapTypes.CellUpdate[] { - const player = this.players.get(playerId); - if (!player) return []; - - const gridIndex = gridMapLogic.getGridIndexFromPosition(player.x, player.y); - if (gridIndex !== null) { - this.mapStore.paintCell(gridIndex, player.teamId); - } - - return this.mapStore.getAndClearUpdates(); + return this.gameSessionService.paintAndGetUpdates(playerId); } } \ No newline at end of file diff --git a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts new file mode 100644 index 0000000..56ae47e --- /dev/null +++ b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts @@ -0,0 +1,25 @@ +import type { TickData } from "../../GameLoop"; + +export interface StartGamePort { + addPlayer(id: string): void; + startGameLoop( + roomId: string, + playerIds: string[], + onTick: (data: TickData) => void, + onGameEnd: () => void + ): void; + getRoomStartTime(roomId: string): number | undefined; +} + +export interface ReadyForGamePort { + getAllPlayers(): unknown[]; + getRoomStartTime(roomId: string): number | undefined; +} + +export interface MovePlayerPort { + movePlayer(id: string, x: number, y: number): void; +} + +export interface DisconnectPlayerPort { + removePlayer(id: string): void; +} diff --git a/apps/server/src/domains/game/application/services/GameSessionService.ts b/apps/server/src/domains/game/application/services/GameSessionService.ts new file mode 100644 index 0000000..ab33e56 --- /dev/null +++ b/apps/server/src/domains/game/application/services/GameSessionService.ts @@ -0,0 +1,78 @@ +import { config, gridMapLogic } from "@repo/shared"; +import type { gridMapTypes } from "@repo/shared"; +import { GameLoop, type TickData } from "../../GameLoop"; +import { Player } from "../../entities/Player.js"; +import { MapStore } from "../../states/MapStore"; + +export class GameSessionService { + private mapStore: MapStore; + private gameLoops: Map; + private roomStartTimes: Map; + + constructor(private players: Map) { + this.mapStore = new MapStore(); + this.gameLoops = new Map(); + this.roomStartTimes = new Map(); + } + + public getRoomStartTime(roomId: string): number | undefined { + return this.roomStartTimes.get(roomId); + } + + public startGameLoop( + roomId: string, + playerIds: string[], + onTick: (data: TickData) => void, + onGameEnd: () => void + ) { + if (this.gameLoops.has(roomId)) { + console.log("[GameManager] startGameLoop ignored (already running)", { roomId }); + return; + } + + const tickRate = config.GAME_CONFIG.PLAYER_POSITION_UPDATE_MS; + this.roomStartTimes.set(roomId, Date.now()); + + const loop = new GameLoop( + roomId, + tickRate, + playerIds, + this.players, + this.mapStore, + onTick, + () => { + this.roomStartTimes.delete(roomId); + this.gameLoops.delete(roomId); + onGameEnd(); + } + ); + + loop.start(); + this.gameLoops.set(roomId, loop); + console.log("[GameManager] game loop started", { roomId, playerCount: playerIds.length }); + } + + public stopGameLoop(roomId: string) { + const loop = this.gameLoops.get(roomId); + if (loop) { + loop.stop(); + this.gameLoops.delete(roomId); + this.roomStartTimes.delete(roomId); + console.log("[GameManager] game loop stopped", { roomId }); + } else { + console.log("[GameManager] stopGameLoop ignored (not running)", { roomId }); + } + } + + public paintAndGetUpdates(playerId: string): gridMapTypes.CellUpdate[] { + const player = this.players.get(playerId); + if (!player) return []; + + const gridIndex = gridMapLogic.getGridIndexFromPosition(player.x, player.y); + if (gridIndex !== null) { + this.mapStore.paintCell(gridIndex, player.teamId); + } + + return this.mapStore.getAndClearUpdates(); + } +} diff --git a/apps/server/src/domains/game/application/services/PlayerRegistry.ts b/apps/server/src/domains/game/application/services/PlayerRegistry.ts new file mode 100644 index 0000000..5233ce4 --- /dev/null +++ b/apps/server/src/domains/game/application/services/PlayerRegistry.ts @@ -0,0 +1,55 @@ +import { config } from "@repo/shared"; +import { Player } from "../../entities/Player.js"; + +export class PlayerRegistry { + private players: Map; + + constructor() { + this.players = new Map(); + } + + public addPlayer(id: string): Player { + const player = new Player(id); + player.x = config.GAME_CONFIG.GRID_COLS / 2; + player.y = config.GAME_CONFIG.GRID_ROWS / 2; + this.players.set(id, player); + console.log("[GameManager] player added", { playerId: id, totalPlayers: this.players.size }); + return player; + } + + public removePlayer(id: string) { + const existed = this.players.delete(id); + if (existed) { + console.log("[GameManager] player removed", { playerId: id, totalPlayers: this.players.size }); + } else { + console.log("[GameManager] player remove ignored (not found)", { playerId: id }); + } + } + + public getPlayer(id: string): Player | undefined { + return this.players.get(id); + } + + public movePlayer(id: string, x: number, y: number) { + const player = this.players.get(id); + if (player) { + console.log(`Move Request -> ID:${id.slice(0, 4)} x:${Math.round(x)} y:${Math.round(y)}`); + if (typeof x !== "number" || typeof y !== "number" || isNaN(x) || isNaN(y)) { + console.log("⚠️ 無効なデータなので無視しました"); + return; + } + player.x = x; + player.y = y; + } else { + console.log("[GameManager] move ignored (player not found)", { playerId: id }); + } + } + + public getAllPlayers(): Player[] { + return Array.from(this.players.values()); + } + + public getPlayersRef(): Map { + return this.players; + } +} diff --git a/apps/server/src/domains/game/application/useCases/disconnectUseCase.ts b/apps/server/src/domains/game/application/useCases/disconnectUseCase.ts new file mode 100644 index 0000000..1db69d0 --- /dev/null +++ b/apps/server/src/domains/game/application/useCases/disconnectUseCase.ts @@ -0,0 +1,20 @@ +import { protocol } from "@repo/shared"; +import type { DisconnectPlayerPort } from "../ports/gameUseCasePorts"; + +type EmitToAll = (event: string, payload?: unknown) => void; + +type DisconnectUseCaseParams = { + gameManager: DisconnectPlayerPort; + playerId: string; + emitToAll: EmitToAll; +}; + +export const disconnectUseCase = ({ + gameManager, + playerId, + emitToAll, +}: DisconnectUseCaseParams) => { + gameManager.removePlayer(playerId); + emitToAll(protocol.SocketEvents.REMOVE_PLAYER, playerId); + console.log("[GameHandler] player removed", { playerId }); +}; diff --git a/apps/server/src/domains/game/application/useCases/movePlayerUseCase.ts b/apps/server/src/domains/game/application/useCases/movePlayerUseCase.ts new file mode 100644 index 0000000..9bcece1 --- /dev/null +++ b/apps/server/src/domains/game/application/useCases/movePlayerUseCase.ts @@ -0,0 +1,16 @@ +import type { playerTypes } from "@repo/shared"; +import type { MovePlayerPort } from "../ports/gameUseCasePorts"; + +type MovePlayerUseCaseParams = { + gameManager: MovePlayerPort; + playerId: string; + move: playerTypes.MovePayload; +}; + +export const movePlayerUseCase = ({ + gameManager, + playerId, + move, +}: MovePlayerUseCaseParams) => { + gameManager.movePlayer(playerId, move.x, move.y); +}; diff --git a/apps/server/src/domains/game/application/useCases/pingUseCase.ts b/apps/server/src/domains/game/application/useCases/pingUseCase.ts new file mode 100644 index 0000000..0ef9f54 --- /dev/null +++ b/apps/server/src/domains/game/application/useCases/pingUseCase.ts @@ -0,0 +1,18 @@ +import { protocol } from "@repo/shared"; + +type EmitToSocket = (event: string, payload?: unknown) => void; + +type PingUseCaseParams = { + clientTime: number; + emitToSocket: EmitToSocket; +}; + +export const pingUseCase = ({ + clientTime, + emitToSocket, +}: PingUseCaseParams) => { + emitToSocket(protocol.SocketEvents.PONG, { + clientTime, + serverTime: Date.now(), + }); +}; diff --git a/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts new file mode 100644 index 0000000..92ab796 --- /dev/null +++ b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts @@ -0,0 +1,43 @@ +import { protocol } from "@repo/shared"; +import type { ReadyForGamePort } from "../ports/gameUseCasePorts"; + +type EmitToSocket = (event: string, payload?: unknown) => void; + +type ReadyForGameUseCaseParams = { + socketId: string; + roomId?: string; + gameManager: ReadyForGamePort; + emitToSocket: EmitToSocket; +}; + +export const readyForGameUseCase = ({ + socketId, + roomId, + gameManager, + emitToSocket, +}: ReadyForGameUseCaseParams) => { + const allPlayers = gameManager.getAllPlayers(); + emitToSocket(protocol.SocketEvents.CURRENT_PLAYERS, allPlayers); + + console.log("[GameHandler] READY_FOR_GAME received", { + socketId, + totalPlayers: allPlayers.length, + }); + + if (!roomId) { + console.log("[GameHandler] READY_FOR_GAME missing roomId", { socketId }); + return; + } + + const startTime = gameManager.getRoomStartTime(roomId); + if (!startTime) { + return; + } + + emitToSocket(protocol.SocketEvents.GAME_START, { startTime }); + console.log("[GameHandler] GAME_START sent to ready client", { + socketId, + roomId, + startTime, + }); +}; diff --git a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts new file mode 100644 index 0000000..41b2772 --- /dev/null +++ b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts @@ -0,0 +1,66 @@ +import { protocol, roomConsts } from "@repo/shared"; +import { RoomManager } from "@server/domains/room/RoomManager"; +import type { StartGamePort } from "../ports/gameUseCasePorts"; + +type EmitToRoom = (roomId: string, event: string, payload?: unknown) => void; + +type StartGameUseCaseParams = { + ownerId: string; + gameManager: StartGamePort; + roomManager: RoomManager; + emitToRoom: EmitToRoom; +}; + +export const startGameUseCase = ({ + ownerId, + gameManager, + roomManager, + emitToRoom, +}: StartGameUseCaseParams) => { + const room = roomManager.getRoomByOwnerId(ownerId); + if (!room) { + console.log("[GameHandler] START_GAME ignored (no room)", { socketId: ownerId }); + return; + } + + if (room.status === roomConsts.RoomPhase.PLAYING) { + console.log("[GameHandler] START_GAME ignored (already playing)", { roomId: room.roomId }); + return; + } + + console.log("[GameHandler] START_GAME accepted", { + roomId: room.roomId, + ownerId, + totalPlayers: room.players.length, + }); + + room.status = roomConsts.RoomPhase.PLAYING; + + const playerIds = room.players.map((p: { id: string }) => p.id); + + room.players.forEach((p: { id: string }) => { + gameManager.addPlayer(p.id); + }); + + gameManager.startGameLoop( + room.roomId, + playerIds, + (tickData) => { + tickData.players.forEach((playerData) => { + emitToRoom(room.roomId, protocol.SocketEvents.UPDATE_PLAYER, playerData); + }); + + if (tickData.cellUpdates.length > 0) { + emitToRoom(room.roomId, protocol.SocketEvents.UPDATE_MAP_CELLS, tickData.cellUpdates); + } + }, + () => { + console.log(`[GameHandler] ルーム ${room.roomId} のゲームが終了しました (3分経過)`); + emitToRoom(room.roomId, protocol.SocketEvents.GAME_END); + room.status = roomConsts.RoomPhase.WAITING; + } + ); + + const startTime = gameManager.getRoomStartTime(room.roomId) || Date.now(); + emitToRoom(room.roomId, protocol.SocketEvents.GAME_START, { startTime }); +}; diff --git a/apps/server/src/domains/game/handlers/onDisconnect.ts b/apps/server/src/domains/game/handlers/onDisconnect.ts new file mode 100644 index 0000000..5555397 --- /dev/null +++ b/apps/server/src/domains/game/handlers/onDisconnect.ts @@ -0,0 +1,22 @@ +import { Server } from "socket.io"; +import { GameManager } from "@server/domains/game/GameManager"; +import { disconnectUseCase } from "@server/domains/game/application/useCases/disconnectUseCase"; + +export const onDisconnect = ( + io: Server, + gameManager: GameManager, + playerId: string +) => { + disconnectUseCase({ + gameManager, + playerId, + emitToAll: (event, payload) => { + if (payload === undefined) { + io.emit(event); + return; + } + + io.emit(event, payload); + }, + }); +}; diff --git a/apps/server/src/domains/game/handlers/onMove.ts b/apps/server/src/domains/game/handlers/onMove.ts new file mode 100644 index 0000000..b07d713 --- /dev/null +++ b/apps/server/src/domains/game/handlers/onMove.ts @@ -0,0 +1,15 @@ +import { GameManager } from "@server/domains/game/GameManager"; +import type { playerTypes } from "@repo/shared"; +import { movePlayerUseCase } from "@server/domains/game/application/useCases/movePlayerUseCase"; + +export const onMove = ( + gameManager: GameManager, + playerId: string, + data: playerTypes.MovePayload +) => { + movePlayerUseCase({ + gameManager, + playerId, + move: data, + }); +}; diff --git a/apps/server/src/domains/game/handlers/onPing.ts b/apps/server/src/domains/game/handlers/onPing.ts new file mode 100644 index 0000000..99c8f88 --- /dev/null +++ b/apps/server/src/domains/game/handlers/onPing.ts @@ -0,0 +1,16 @@ +import { Socket } from "socket.io"; +import { pingUseCase } from "@server/domains/game/application/useCases/pingUseCase"; + +export const onPing = (socket: Socket, clientTime: number) => { + pingUseCase({ + clientTime, + emitToSocket: (event, payload) => { + if (payload === undefined) { + socket.emit(event); + return; + } + + socket.emit(event, payload); + }, + }); +}; diff --git a/apps/server/src/domains/game/handlers/onReadyForGame.ts b/apps/server/src/domains/game/handlers/onReadyForGame.ts new file mode 100644 index 0000000..6bece4f --- /dev/null +++ b/apps/server/src/domains/game/handlers/onReadyForGame.ts @@ -0,0 +1,24 @@ +import { Socket } from "socket.io"; +import { GameManager } from "@server/domains/game/GameManager"; +import { readyForGameUseCase } from "@server/domains/game/application/useCases/readyForGameUseCase"; + +export const onReadyForGame = ( + socket: Socket, + gameManager: GameManager +) => { + const roomId = Array.from(socket.rooms).find((room) => room !== socket.id); + + readyForGameUseCase({ + socketId: socket.id, + roomId, + gameManager, + emitToSocket: (event, payload) => { + if (payload === undefined) { + socket.emit(event); + return; + } + + socket.emit(event, payload); + }, + }); +}; diff --git a/apps/server/src/domains/game/handlers/onStartGame.ts b/apps/server/src/domains/game/handlers/onStartGame.ts new file mode 100644 index 0000000..528caaa --- /dev/null +++ b/apps/server/src/domains/game/handlers/onStartGame.ts @@ -0,0 +1,25 @@ +import { Server } from "socket.io"; +import { GameManager } from "@server/domains/game/GameManager"; +import { RoomManager } from "@server/domains/room/RoomManager"; +import { startGameUseCase } from "@server/domains/game/application/useCases/startGameUseCase"; + +export const onStartGame = ( + io: Server, + gameManager: GameManager, + roomManager: RoomManager, + ownerId: string +) => { + startGameUseCase({ + ownerId, + gameManager, + roomManager, + emitToRoom: (roomId, event, payload) => { + if (payload === undefined) { + io.to(roomId).emit(event); + return; + } + + io.to(roomId).emit(event, payload); + }, + }); +}; diff --git a/apps/server/src/domains/room/RoomHandler.ts b/apps/server/src/domains/room/RoomHandler.ts index 91f4dec..756cb63 100644 --- a/apps/server/src/domains/room/RoomHandler.ts +++ b/apps/server/src/domains/room/RoomHandler.ts @@ -2,24 +2,28 @@ import { RoomManager } from "./RoomManager"; import { protocol } from "@repo/shared"; import type { roomTypes } from "@repo/shared"; +import { joinRoomUseCase } from "./application/useCases/joinRoomUseCase"; +import { roomDisconnectUseCase } from "./application/useCases/roomDisconnectUseCase"; export const registerRoomHandlers = (io: Server, socket: Socket, roomManager: RoomManager) => { socket.on(protocol.SocketEvents.JOIN_ROOM, (data: roomTypes.JoinRoomPayload) => { - const { roomId, playerName } = data; - console.log("[RoomHandler] JOIN_ROOM received", { roomId, socketId: socket.id, playerName }); + const { roomId } = data; socket.join(roomId); - // RoomManagerにデータ操作を依頼 - const room = roomManager.addPlayerToRoom(roomId, socket.id, playerName); + joinRoomUseCase({ + roomManager, + socketId: socket.id, + data, + emitToRoom: (targetRoomId, event, payload) => { + if (payload === undefined) { + io.to(targetRoomId).emit(event); + return; + } - // ルーム内全員向け最新状態配信 - io.to(roomId).emit(protocol.SocketEvents.ROOM_UPDATE, room); - console.log("[RoomHandler] ROOM_UPDATE emitted", { - roomId, - ownerId: room.ownerId, - totalPlayers: room.players.length + io.to(targetRoomId).emit(event, payload); + }, }); }); @@ -29,20 +33,16 @@ * 切断時のルームクリーンアップ処理 */ export const handleRoomDisconnect = (io: Server, socket: Socket, roomManager: RoomManager) => { - // ルームからの除外処理 - const updatedRooms = roomManager.removePlayer(socket.id); - console.log("[RoomHandler] disconnect cleanup", { + roomDisconnectUseCase({ + roomManager, socketId: socket.id, - updatedRoomCount: updatedRooms.length - }); - - // 更新があったルーム(オーナー変更など)にのみ通知を飛ばす - updatedRooms.forEach(room => { - io.to(room.roomId).emit(protocol.SocketEvents.ROOM_UPDATE, room); - console.log("[RoomHandler] ROOM_UPDATE emitted", { - roomId: room.roomId, - ownerId: room.ownerId, - totalPlayers: room.players.length - }); + emitToRoom: (roomId, event, payload) => { + if (payload === undefined) { + io.to(roomId).emit(event); + return; + } + + io.to(roomId).emit(event, payload); + }, }); }; \ No newline at end of file diff --git a/apps/server/src/domains/room/application/ports/roomUseCasePorts.ts b/apps/server/src/domains/room/application/ports/roomUseCasePorts.ts new file mode 100644 index 0000000..37d7416 --- /dev/null +++ b/apps/server/src/domains/room/application/ports/roomUseCasePorts.ts @@ -0,0 +1,9 @@ +import type { roomTypes } from "@repo/shared"; + +export interface JoinRoomPort { + addPlayerToRoom(roomId: string, socketId: string, playerName: string): roomTypes.Room; +} + +export interface DisconnectRoomPort { + removePlayer(socketId: string): roomTypes.Room[]; +} diff --git a/apps/server/src/domains/room/application/useCases/joinRoomUseCase.ts b/apps/server/src/domains/room/application/useCases/joinRoomUseCase.ts new file mode 100644 index 0000000..2123b35 --- /dev/null +++ b/apps/server/src/domains/room/application/useCases/joinRoomUseCase.ts @@ -0,0 +1,31 @@ +import { protocol } from "@repo/shared"; +import type { roomTypes } from "@repo/shared"; +import type { JoinRoomPort } from "../ports/roomUseCasePorts"; + +type EmitToRoom = (roomId: string, event: string, payload?: unknown) => void; + +type JoinRoomUseCaseParams = { + roomManager: JoinRoomPort; + socketId: string; + data: roomTypes.JoinRoomPayload; + emitToRoom: EmitToRoom; +}; + +export const joinRoomUseCase = ({ + roomManager, + socketId, + data, + emitToRoom, +}: JoinRoomUseCaseParams) => { + const { roomId, playerName } = data; + console.log("[RoomHandler] JOIN_ROOM received", { roomId, socketId, playerName }); + + const room = roomManager.addPlayerToRoom(roomId, socketId, playerName); + + emitToRoom(roomId, protocol.SocketEvents.ROOM_UPDATE, room); + console.log("[RoomHandler] ROOM_UPDATE emitted", { + roomId, + ownerId: room.ownerId, + totalPlayers: room.players.length, + }); +}; diff --git a/apps/server/src/domains/room/application/useCases/roomDisconnectUseCase.ts b/apps/server/src/domains/room/application/useCases/roomDisconnectUseCase.ts new file mode 100644 index 0000000..be33326 --- /dev/null +++ b/apps/server/src/domains/room/application/useCases/roomDisconnectUseCase.ts @@ -0,0 +1,31 @@ +import { protocol } from "@repo/shared"; +import type { DisconnectRoomPort } from "../ports/roomUseCasePorts"; + +type EmitToRoom = (roomId: string, event: string, payload?: unknown) => void; + +type RoomDisconnectUseCaseParams = { + roomManager: DisconnectRoomPort; + socketId: string; + emitToRoom: EmitToRoom; +}; + +export const roomDisconnectUseCase = ({ + roomManager, + socketId, + emitToRoom, +}: RoomDisconnectUseCaseParams) => { + const updatedRooms = roomManager.removePlayer(socketId); + console.log("[RoomHandler] disconnect cleanup", { + socketId, + updatedRoomCount: updatedRooms.length, + }); + + updatedRooms.forEach((room) => { + emitToRoom(room.roomId, protocol.SocketEvents.ROOM_UPDATE, room); + console.log("[RoomHandler] ROOM_UPDATE emitted", { + roomId: room.roomId, + ownerId: room.ownerId, + totalPlayers: room.players.length, + }); + }); +}; diff --git a/apps/server/src/network/SocketManager.ts b/apps/server/src/network/SocketManager.ts index 0aa661c..01bff7d 100644 --- a/apps/server/src/network/SocketManager.ts +++ b/apps/server/src/network/SocketManager.ts @@ -1,9 +1,9 @@ import { Server, Socket } from "socket.io"; -import { GameManager } from "../domains/game/GameManager"; -import { RoomManager } from "../domains/room/RoomManager"; +import { GameManager } from "@server/domains/game/GameManager"; +import { RoomManager } from "@server/domains/room/RoomManager"; import { protocol } from "@repo/shared"; -import { registerRoomHandlers, handleRoomDisconnect } from "../domains/room/RoomHandler"; -import { registerGameHandlers, handleGameDisconnect } from "../domains/game/GameHandler"; +import { registerRoomHandlers, handleRoomDisconnect } from "@server/domains/room/RoomHandler"; +import { registerGameHandlers, handleGameDisconnect } from "@server/domains/game/GameHandler"; export class SocketManager { private io: Server; diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 26213dc..0ecadc8 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -10,8 +10,10 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "baseUrl": "." - /* "paths" は削除します(pnpm のワークスペース解決を使用するため) */ + "baseUrl": ".", + "paths": { + "@server/*": ["src/*"] + } }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94b00f1..37621b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,15 @@ '@types/socket.io': specifier: ^3.0.2 version: 3.0.2 + '@typescript-eslint/parser': + specifier: ^8.56.1 + version: 8.56.1(eslint@10.0.2)(typescript@5.9.3) + eslint: + specifier: ^10.0.2 + version: 10.0.2 + tsc-alias: + specifier: ^1.8.16 + version: 1.8.16 tsx: specifier: ^4.21.0 version: 4.21.0 @@ -421,6 +430,18 @@ '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@pixi/app@7.4.3': resolution: {integrity: sha512-opyWMuO0Ir8pf1DYUR++wAA6ZfNU+nIX2z95R2OD172HbcdhB4/HD7leLIIAny/LciEdMqlWEBhXK7N93YWbdg==} peerDependencies: @@ -840,9 +861,17 @@ any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -855,10 +884,18 @@ resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + brace-expansion@5.0.3: resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==} engines: {node: 18 || 20 || >=22} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -885,6 +922,10 @@ caniuse-lite@1.0.30001769: resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -893,6 +934,10 @@ resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -937,6 +982,10 @@ resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1037,12 +1086,19 @@ fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1056,6 +1112,10 @@ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1096,10 +1156,18 @@ gifuct-js@2.1.2: resolution: {integrity: sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1120,6 +1188,10 @@ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1128,6 +1200,10 @@ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1208,6 +1284,14 @@ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -1226,6 +1310,10 @@ ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mylas@2.1.14: + resolution: {integrity: sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==} + engines: {node: '>=16.0.0'} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -1244,6 +1332,10 @@ node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1275,6 +1367,10 @@ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1284,6 +1380,10 @@ picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} @@ -1298,6 +1398,10 @@ pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -1338,6 +1442,13 @@ resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} engines: {node: '>=0.6'} + queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -1360,6 +1471,10 @@ resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -1371,11 +1486,18 @@ resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rollup@4.57.1: resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -1412,6 +1534,10 @@ resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + socket.io-adapter@2.5.6: resolution: {integrity: sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==} @@ -1458,6 +1584,10 @@ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -1485,6 +1615,11 @@ '@swc/wasm': optional: true + tsc-alias@1.8.16: + resolution: {integrity: sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==} + engines: {node: '>=16.20.2'} + hasBin: true + tsup@8.5.1: resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} engines: {node: '>=18'} @@ -1886,6 +2021,18 @@ '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + '@pixi/app@7.4.3(@pixi/core@7.4.3)(@pixi/display@7.4.3(@pixi/core@7.4.3))': dependencies: '@pixi/core': 7.4.3 @@ -2269,18 +2416,31 @@ any-promise@1.3.0: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + arg@4.1.3: {} + array-union@2.1.0: {} + balanced-match@4.0.4: {} base64id@2.0.0: {} baseline-browser-mapping@2.9.19: {} + binary-extensions@2.3.0: {} + brace-expansion@5.0.3: dependencies: balanced-match: 4.0.4 + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.19 @@ -2308,12 +2468,26 @@ caniuse-lite@1.0.30001769: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 commander@4.1.1: {} + commander@9.5.0: {} + confbox@0.1.8: {} consola@3.4.2: {} @@ -2345,6 +2519,10 @@ diff@4.0.4: {} + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2498,10 +2676,22 @@ fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -2510,6 +2700,10 @@ dependencies: flat-cache: 4.0.1 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -2561,10 +2755,23 @@ dependencies: js-binary-schema-parser: 2.0.3 + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + gopd@1.2.0: {} has-symbols@1.1.0: {} @@ -2577,12 +2784,18 @@ imurmurhash@0.1.4: {} + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-number@7.0.0: {} + isexe@2.0.0: {} ismobilejs@1.1.1: {} @@ -2642,6 +2855,13 @@ math-intrinsics@1.1.0: {} + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + mime-db@1.52.0: {} mime-types@2.1.35: @@ -2661,6 +2881,8 @@ ms@2.1.3: {} + mylas@2.1.14: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -2675,6 +2897,8 @@ node-releases@2.0.27: {} + normalize-path@3.0.0: {} + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -2702,12 +2926,16 @@ path-key@3.1.1: {} + path-type@4.0.0: {} + pathe@2.0.3: {} performance-now@2.1.0: {} picocolors@1.1.1: {} + picomatch@2.3.1: {} + picomatch@4.0.3: {} pirates@4.0.7: {} @@ -2731,6 +2959,10 @@ mlly: 1.8.0 pathe: 2.0.3 + plimit-lit@1.6.1: + dependencies: + queue-lit: 1.5.2 + postcss-load-config@6.0.1(postcss@8.5.6)(tsx@4.21.0): dependencies: lilconfig: 3.1.3 @@ -2760,6 +2992,10 @@ dependencies: side-channel: 1.1.0 + queue-lit@1.5.2: {} + + queue-microtask@1.2.3: {} + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -2779,12 +3015,18 @@ dependencies: loose-envify: 1.4.0 + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + readdirp@4.1.2: {} resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} + reusify@1.1.0: {} + rollup@4.57.1: dependencies: '@types/estree': 1.0.8 @@ -2816,6 +3058,10 @@ '@rollup/rollup-win32-x64-msvc': 4.57.1 fsevents: 2.3.3 + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -2858,6 +3104,8 @@ side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + slash@3.0.0: {} + socket.io-adapter@2.5.6: dependencies: debug: 4.4.3 @@ -2930,6 +3178,10 @@ fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + tree-kill@1.2.2: {} ts-api-utils@2.4.0(typescript@5.9.3): @@ -2956,6 +3208,16 @@ v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + tsc-alias@1.8.16: + dependencies: + chokidar: 3.6.0 + commander: 9.5.0 + get-tsconfig: 4.13.6 + globby: 11.1.0 + mylas: 2.1.14 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 + tsup@8.5.1(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.3)