diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts index 90b6e2a..06b088d 100644 --- a/apps/client/src/scenes/game/GameManager.ts +++ b/apps/client/src/scenes/game/GameManager.ts @@ -57,6 +57,10 @@ return this.timer.getRemainingTime(); } + public isInputEnabled(): boolean { + return this.canAcceptInput(); + } + public placeBomb(): string | null { if (!this.canAcceptInput()) return null; if (!this.bombManager) return null; diff --git a/apps/client/src/scenes/game/GameScene.tsx b/apps/client/src/scenes/game/GameScene.tsx index 84663e1..e5e7876 100644 --- a/apps/client/src/scenes/game/GameScene.tsx +++ b/apps/client/src/scenes/game/GameScene.tsx @@ -17,6 +17,7 @@ pixiContainerRef, timeLeft, startCountdownText, + isInputEnabled, handleInput, handlePlaceBomb, } = useGameSceneController(myId); @@ -25,6 +26,7 @@ ; onJoystickInput: (x: number, y: number) => void; onPlaceBomb: () => boolean; @@ -70,6 +71,7 @@ export const GameView = ({ timeLeft, startCountdownText, + isInputEnabled, pixiContainerRef, onJoystickInput, onPlaceBomb, @@ -88,6 +90,7 @@ {/* 入力UI レイヤー */} diff --git a/apps/client/src/scenes/game/application/GameNetworkSync.ts b/apps/client/src/scenes/game/application/GameNetworkSync.ts index 578f5ec..08d62ef 100644 --- a/apps/client/src/scenes/game/application/GameNetworkSync.ts +++ b/apps/client/src/scenes/game/application/GameNetworkSync.ts @@ -41,6 +41,75 @@ unbind: () => void; }; +type SubscriptionHandlers = { + handleCurrentPlayers: (serverPlayers: CurrentPlayersPayload) => void; + handleNewPlayer: (player: NewPlayerPayload) => void; + handleGameStart: (data: GameStartPayload) => void; + handlePlayerUpdates: (players: UpdatePlayersPayload) => void; + handleRemovePlayer: (id: RemovePlayerPayload) => void; + handleUpdateMapCells: (updates: UpdateMapCellsPayload) => void; + handleGameEnd: () => void; + handleBombPlaced: (payload: BombPlacedPayload) => void; + handleBombPlacedAck: (payload: BombPlacedAckPayload) => void; + handlePlayerDead: (payload: PlayerDeadPayload) => void; +}; + +const createSocketSubscriptions = ({ + handleCurrentPlayers, + handleNewPlayer, + handleGameStart, + handlePlayerUpdates, + handleRemovePlayer, + handleUpdateMapCells, + handleGameEnd, + handleBombPlaced, + handleBombPlacedAck, + handlePlayerDead, +}: SubscriptionHandlers): SocketSubscription[] => { + return [ + { + bind: () => socketManager.game.onCurrentPlayers(handleCurrentPlayers), + unbind: () => socketManager.game.offCurrentPlayers(handleCurrentPlayers), + }, + { + bind: () => socketManager.game.onNewPlayer(handleNewPlayer), + unbind: () => socketManager.game.offNewPlayer(handleNewPlayer), + }, + { + bind: () => socketManager.game.onGameStart(handleGameStart), + unbind: () => socketManager.game.offGameStart(handleGameStart), + }, + { + bind: () => socketManager.game.onUpdatePlayers(handlePlayerUpdates), + unbind: () => socketManager.game.offUpdatePlayers(handlePlayerUpdates), + }, + { + bind: () => socketManager.game.onRemovePlayer(handleRemovePlayer), + unbind: () => socketManager.game.offRemovePlayer(handleRemovePlayer), + }, + { + bind: () => socketManager.game.onUpdateMapCells(handleUpdateMapCells), + unbind: () => socketManager.game.offUpdateMapCells(handleUpdateMapCells), + }, + { + bind: () => socketManager.game.onGameEnd(handleGameEnd), + unbind: () => socketManager.game.offGameEnd(handleGameEnd), + }, + { + bind: () => socketManager.game.onBombPlaced(handleBombPlaced), + unbind: () => socketManager.game.offBombPlaced(handleBombPlaced), + }, + { + bind: () => socketManager.game.onBombPlacedAck(handleBombPlacedAck), + unbind: () => socketManager.game.offBombPlacedAck(handleBombPlacedAck), + }, + { + bind: () => socketManager.game.onPlayerDead(handlePlayerDead), + unbind: () => socketManager.game.offPlayerDead(handlePlayerDead), + }, + ]; +}; + /** ゲーム中のネットワークイベント購読と同期処理を管理する */ export class GameNetworkSync { private worldContainer: Container; @@ -148,48 +217,18 @@ this.onBombPlacedFromOthers = onBombPlacedFromOthers; this.onBombPlacedAckFromNetwork = onBombPlacedAckFromNetwork; this.onPlayerDeadFromNetwork = onPlayerDeadFromNetwork; - this.socketSubscriptions = [ - { - bind: () => socketManager.game.onCurrentPlayers(this.handleCurrentPlayers), - unbind: () => socketManager.game.offCurrentPlayers(this.handleCurrentPlayers), - }, - { - bind: () => socketManager.game.onNewPlayer(this.handleNewPlayer), - unbind: () => socketManager.game.offNewPlayer(this.handleNewPlayer), - }, - { - bind: () => socketManager.game.onGameStart(this.handleGameStart), - unbind: () => socketManager.game.offGameStart(this.handleGameStart), - }, - { - bind: () => socketManager.game.onUpdatePlayers(this.handlePlayerUpdates), - unbind: () => socketManager.game.offUpdatePlayers(this.handlePlayerUpdates), - }, - { - bind: () => socketManager.game.onRemovePlayer(this.handleRemovePlayer), - unbind: () => socketManager.game.offRemovePlayer(this.handleRemovePlayer), - }, - { - bind: () => socketManager.game.onUpdateMapCells(this.handleUpdateMapCells), - unbind: () => socketManager.game.offUpdateMapCells(this.handleUpdateMapCells), - }, - { - bind: () => socketManager.game.onGameEnd(this.handleGameEnd), - unbind: () => socketManager.game.offGameEnd(this.handleGameEnd), - }, - { - bind: () => socketManager.game.onBombPlaced(this.handleBombPlaced), - unbind: () => socketManager.game.offBombPlaced(this.handleBombPlaced), - }, - { - bind: () => socketManager.game.onBombPlacedAck(this.handleBombPlacedAck), - unbind: () => socketManager.game.offBombPlacedAck(this.handleBombPlacedAck), - }, - { - bind: () => socketManager.game.onPlayerDead(this.handlePlayerDead), - unbind: () => socketManager.game.offPlayerDead(this.handlePlayerDead), - }, - ]; + this.socketSubscriptions = createSocketSubscriptions({ + handleCurrentPlayers: this.handleCurrentPlayers, + handleNewPlayer: this.handleNewPlayer, + handleGameStart: this.handleGameStart, + handlePlayerUpdates: this.handlePlayerUpdates, + handleRemovePlayer: this.handleRemovePlayer, + handleUpdateMapCells: this.handleUpdateMapCells, + handleGameEnd: this.handleGameEnd, + handleBombPlaced: this.handleBombPlaced, + handleBombPlacedAck: this.handleBombPlacedAck, + handlePlayerDead: this.handlePlayerDead, + }); } public bind() { diff --git a/apps/client/src/scenes/game/hooks/useGameSceneController.ts b/apps/client/src/scenes/game/hooks/useGameSceneController.ts index 67bc2fe..3e40bd6 100644 --- a/apps/client/src/scenes/game/hooks/useGameSceneController.ts +++ b/apps/client/src/scenes/game/hooks/useGameSceneController.ts @@ -26,6 +26,7 @@ const [startCountdownText, setStartCountdownText] = useState( null, ); + const [isInputEnabled, setIsInputEnabled] = useState(false); useEffect(() => { if (!pixiContainerRef.current || !myId) return; @@ -52,6 +53,11 @@ setStartCountdownText((prev) => prev === nextCountdown ? prev : nextCountdown, ); + + const nextInputEnabled = manager.isInputEnabled(); + setIsInputEnabled((prev) => + prev === nextInputEnabled ? prev : nextInputEnabled, + ); }, config.GAME_CONFIG.TIMER_DISPLAY_UPDATE_MS); return () => { @@ -60,6 +66,7 @@ inputManagerRef.current = null; clearInterval(timerInterval); setStartCountdownText(null); + setIsInputEnabled(false); }; }, [myId]); @@ -75,6 +82,7 @@ pixiContainerRef, timeLeft, startCountdownText, + isInputEnabled, handleInput, handlePlaceBomb, }; diff --git a/apps/client/src/scenes/game/input/GameInputOverlay.tsx b/apps/client/src/scenes/game/input/GameInputOverlay.tsx index 09c2626..b1e7bb3 100644 --- a/apps/client/src/scenes/game/input/GameInputOverlay.tsx +++ b/apps/client/src/scenes/game/input/GameInputOverlay.tsx @@ -10,6 +10,7 @@ /** 入力UIレイヤーの入力プロパティ */ type GameInputOverlayProps = { + isInputEnabled: boolean; onJoystickInput: (x: number, y: number) => void; onPlaceBomb: () => boolean; }; @@ -25,6 +26,7 @@ /** 入力UIレイヤーを描画する */ export const GameInputOverlay = ({ + isInputEnabled, onJoystickInput, onPlaceBomb, }: GameInputOverlayProps) => { @@ -79,7 +81,7 @@ }, [bombCooldownMs, lastBombPressedAt, nowMs]); const handlePressBomb = () => { - if (!cooldownState.isReady) { + if (!isInputEnabled || !cooldownState.isReady) { return; } @@ -94,11 +96,14 @@ return (
- +
diff --git a/apps/client/src/scenes/game/input/joystick/JoystickInputPresenter.tsx b/apps/client/src/scenes/game/input/joystick/JoystickInputPresenter.tsx index 0d1d096..cc49572 100644 --- a/apps/client/src/scenes/game/input/joystick/JoystickInputPresenter.tsx +++ b/apps/client/src/scenes/game/input/joystick/JoystickInputPresenter.tsx @@ -11,6 +11,7 @@ export const JoystickInputPresenter = ({ onInput, maxDist, + isEnabled = true, }: UseJoystickInputPresenterProps) => { const { isMoving, @@ -24,11 +25,11 @@ return (
{/* 入力イベントをコントローラーへ渡し,描画用状態をViewへ渡す */} diff --git a/apps/client/src/scenes/game/input/joystick/common/joystick.types.ts b/apps/client/src/scenes/game/input/joystick/common/joystick.types.ts index b1c89db..d86c755 100644 --- a/apps/client/src/scenes/game/input/joystick/common/joystick.types.ts +++ b/apps/client/src/scenes/game/input/joystick/common/joystick.types.ts @@ -52,6 +52,7 @@ export type UseJoystickInputPresenterProps = { onInput: (moveX: number, moveY: number) => void; maxDist?: number; + isEnabled?: boolean; }; /** JoystickView に渡す描画状態型 */ diff --git a/apps/server/src/domains/game/application/useCases/reportBombHitUseCase.ts b/apps/server/src/domains/game/application/useCases/reportBombHitUseCase.ts index f65ccdf..5bbe2e4 100644 --- a/apps/server/src/domains/game/application/useCases/reportBombHitUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/reportBombHitUseCase.ts @@ -7,7 +7,7 @@ BombHitReportValidationPort, ReportBombHitInput, } from "../ports/gameUseCasePorts"; -import { createBombHitReportDedupeKey } from "@server/domains/game/entities/bomb/bombHitReport"; +import { shouldPublishPlayerDeadFromBombHit } from "./reportBombHitValidation"; type ReportBombHitUseCaseParams = { roomId: string; @@ -16,18 +16,6 @@ output: BombHitOutputPort; }; -/** 受信した被弾報告を処理対象にすべきか判定する */ -const shouldPublishPlayerDeadFromBombHit = ( - validation: BombHitReportValidationPort, - input: ReportBombHitInput, -): boolean => { - const dedupeKey = createBombHitReportDedupeKey( - input.socketId, - input.payload.bombId, - ); - return validation.shouldBroadcastBombHitReport(dedupeKey, input.nowMs); -}; - /** 被弾報告を死亡通知へ変換して配信する */ const publishPlayerDeadFromBombHit = ( roomId: string, diff --git a/apps/server/src/domains/game/application/useCases/reportBombHitValidation.ts b/apps/server/src/domains/game/application/useCases/reportBombHitValidation.ts new file mode 100644 index 0000000..7dfd691 --- /dev/null +++ b/apps/server/src/domains/game/application/useCases/reportBombHitValidation.ts @@ -0,0 +1,17 @@ +import type { + BombHitReportValidationPort, + ReportBombHitInput, +} from "../ports/gameUseCasePorts"; +import { createBombHitReportDedupeKey } from "@server/domains/game/entities/bomb/bombHitReport"; + +/** 受信した被弾報告を処理対象にすべきか判定する */ +export const shouldPublishPlayerDeadFromBombHit = ( + validation: BombHitReportValidationPort, + input: ReportBombHitInput, +): boolean => { + const dedupeKey = createBombHitReportDedupeKey( + input.socketId, + input.payload.bombId, + ); + return validation.shouldBroadcastBombHitReport(dedupeKey, input.nowMs); +};