diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts index 3eaa308..c56cc12 100644 --- a/apps/client/src/scenes/game/GameManager.ts +++ b/apps/client/src/scenes/game/GameManager.ts @@ -101,7 +101,7 @@ gameMap: this.gameMap, onGameStart: this.setGameStart.bind(this), onGameEnd: this.lockInput.bind(this), - onBombPlaced: (payload) => { + onBombPlacedFromNetwork: (payload) => { const bombId = createBombIdFromPayload(payload); this.upsertBombFromNetwork(bombId, payload); }, diff --git a/apps/client/src/scenes/game/application/GameNetworkSync.ts b/apps/client/src/scenes/game/application/GameNetworkSync.ts index 8cb19a4..783e3ca 100644 --- a/apps/client/src/scenes/game/application/GameNetworkSync.ts +++ b/apps/client/src/scenes/game/application/GameNetworkSync.ts @@ -5,7 +5,7 @@ */ import { Container } from "pixi.js"; import type { - BombPlacedPayload, + BombNetworkPayload, CurrentPlayersPayload, GameStartPayload, NewPlayerPayload, @@ -27,7 +27,7 @@ gameMap: GameMapController; onGameStart: (startTime: number) => void; onGameEnd: () => void; - onBombPlaced: (payload: BombPlacedPayload) => void; + onBombPlacedFromNetwork: (payload: BombNetworkPayload) => void; }; /** ゲーム中のネットワークイベント購読と同期処理を管理する */ @@ -38,7 +38,7 @@ private gameMap: GameMapController; private onGameStart: (startTime: number) => void; private onGameEnd: () => void; - private onBombPlaced: (payload: BombPlacedPayload) => void; + private onBombPlacedFromNetwork: (payload: BombNetworkPayload) => void; private isBound = false; private debugLog = (message: string) => { @@ -99,18 +99,26 @@ this.onGameEnd(); }; - private handleBombPlaced = (payload: BombPlacedPayload) => { - this.onBombPlaced(payload); + private handleBombPlaced = (payload: BombNetworkPayload) => { + this.onBombPlacedFromNetwork(payload); }; - constructor({ worldContainer, players, myId, gameMap, onGameStart, onGameEnd, onBombPlaced }: GameNetworkSyncOptions) { + constructor({ + worldContainer, + players, + myId, + gameMap, + onGameStart, + onGameEnd, + onBombPlacedFromNetwork, + }: GameNetworkSyncOptions) { this.worldContainer = worldContainer; this.players = players; this.myId = myId; this.gameMap = gameMap; this.onGameStart = onGameStart; this.onGameEnd = onGameEnd; - this.onBombPlaced = onBombPlaced; + this.onBombPlacedFromNetwork = onBombPlacedFromNetwork; } public bind() { diff --git a/apps/server/src/network/handlers/game/registerGameHandlers.ts b/apps/server/src/network/handlers/game/registerGameHandlers.ts index 97052ea..bc551d4 100644 --- a/apps/server/src/network/handlers/game/registerGameHandlers.ts +++ b/apps/server/src/network/handlers/game/registerGameHandlers.ts @@ -3,7 +3,7 @@ * ゲーム関連イベントの受信ハンドラを登録する */ import { Server, Socket } from "socket.io"; -import { protocol } from "@repo/shared"; +import { config, createBombIdFromPayload, protocol } from "@repo/shared"; import { readyForGameCoordinator } from "@server/application/coordinators/readyForGameCoordinator"; import { startGameCoordinator } from "@server/application/coordinators/startGameCoordinator"; import type { @@ -20,6 +20,36 @@ import { createServerSocketOnBridge } from "@server/network/handlers/socketEventBridge"; import { createPayloadGuard } from "@server/network/handlers/payloadGuard"; import { createGameOutputAdapter } from "./createGameOutputAdapter"; +const roomBombDedupTable = new Map>(); + +const cleanupExpiredBombDedup = (roomId: string, nowMs: number) => { + const roomTable = roomBombDedupTable.get(roomId); + if (!roomTable) return; + + roomTable.forEach((expiresAtMs, bombId) => { + if (expiresAtMs <= nowMs) { + roomTable.delete(bombId); + } + }); + + if (roomTable.size === 0) { + roomBombDedupTable.delete(roomId); + } +}; + +const shouldBroadcastBombPlaced = (roomId: string, bombId: string, nowMs: number) => { + cleanupExpiredBombDedup(roomId, nowMs); + + const roomTable = roomBombDedupTable.get(roomId) ?? new Map(); + if (roomTable.has(bombId)) { + return false; + } + + const ttlMs = config.GAME_CONFIG.BOMB_FUSE_MS + config.GAME_CONFIG.BOMB_DEDUP_EXTRA_TTL_MS; + roomTable.set(bombId, nowMs + ttlMs); + roomBombDedupTable.set(roomId, roomTable); + return true; +}; /** ゲーム受信イベントごとの入力検証関数を保持するテーブル */ const gamePayloadValidators = { @@ -108,6 +138,12 @@ return; } + const nowMs = Date.now(); + const bombId = createBombIdFromPayload(data); + if (!shouldBroadcastBombPlaced(roomId, bombId, nowMs)) { + return; + } + common.emitToRoom(roomId, protocol.SocketEvents.BOMB_PLACED, data); }); }; diff --git a/packages/shared/src/config/README.md b/packages/shared/src/config/README.md index bce6cce..027f1af 100644 --- a/packages/shared/src/config/README.md +++ b/packages/shared/src/config/README.md @@ -24,6 +24,7 @@ - `GAME_CONFIG.BOMB_RADIUS_GRID`: 爆風半径(グリッド単位) - `GAME_CONFIG.BOMB_FUSE_MS`: 爆弾設置から爆発までの時間(ms) - `GAME_CONFIG.BOMB_COOLDOWN_MS`: 次の爆弾を置けるまでの待機時間(ms) +- `GAME_CONFIG.BOMB_DEDUP_EXTRA_TTL_MS`: サーバー側の爆弾重複排除で保持時間に加算する猶予(ms) - `GAME_CONFIG.TEAM_COUNT`: チーム総数 - `TEAM_NAMES`: `teamId` 順の表示名配列 - `validateTeamConfig`: チーム関連設定(件数整合性)を検証する関数 diff --git a/packages/shared/src/config/gameConfig.ts b/packages/shared/src/config/gameConfig.ts index 07bfc27..f81bf92 100644 --- a/packages/shared/src/config/gameConfig.ts +++ b/packages/shared/src/config/gameConfig.ts @@ -23,6 +23,7 @@ BOMB_RADIUS_GRID: 1.5, // 爆風半径(グリッド単位、円形当たり判定) BOMB_FUSE_MS: 1000, // 設置から爆発までの時間(ms) BOMB_COOLDOWN_MS: 1200, // 設置後に次の爆弾を置けるまでの待機時間(ms) + BOMB_DEDUP_EXTRA_TTL_MS: 1000, // 重複排除保持時間の追加分(ms) // チーム設定(クライアント/サーバー契約) TEAM_COUNT: 4,