diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts index 856de44..ea1fa27 100644 --- a/apps/client/src/scenes/game/GameManager.ts +++ b/apps/client/src/scenes/game/GameManager.ts @@ -12,14 +12,14 @@ import { socketManager } from "@client/network/SocketManager"; import { AppearanceResolver } from "./application/AppearanceResolver"; import { BombManager } from "./entities/bomb/BombManager"; -import { GameTimer } from "./application/GameTimer"; import { GameNetworkSync } from "./application/GameNetworkSync"; import { GameLoop } from "./application/GameLoop"; import { BombHitContextProvider } from "./application/BombHitContextProvider"; import { BombHitOrchestrator } from "./application/BombHitOrchestrator"; import { PlayerDeathPolicy } from "./application/PlayerDeathPolicy"; import { PlayerHitEffectOrchestrator } from "./application/PlayerHitEffectOrchestrator"; -import { InputGate } from "./application/lifecycle/InputGate"; +import { SceneLifecycleState } from "./application/lifecycle/SceneLifecycleState"; +import { GameSessionFacade } from "./application/lifecycle/GameSessionFacade"; import { HitReportPolicy } from "./application/combat/HitReportPolicy"; import { GameSceneOrchestrator } from "./application/orchestrators/GameSceneOrchestrator"; import type { BombHitEvaluationResult } from "./application/BombHitOrchestrator"; @@ -32,7 +32,7 @@ private players: GamePlayers = {}; private myId: string; private container: HTMLDivElement; - private timer = new GameTimer(); + private sessionFacade = new GameSessionFacade(); private appearanceResolver = new AppearanceResolver(); private bombManager: BombManager | null = null; private bombHitOrchestrator: BombHitOrchestrator | null = null; @@ -40,29 +40,29 @@ private gameLoop: GameLoop | null = null; private playerDeathPolicy!: PlayerDeathPolicy; private playerHitEffectOrchestrator!: PlayerHitEffectOrchestrator; - private inputGate: InputGate; + private lifecycleState = new SceneLifecycleState(); private hitReportPolicy = new HitReportPolicy(); // サーバーからゲーム開始通知(と開始時刻)を受け取った時に呼ぶ public setGameStart(startTime: number) { - this.timer.setGameStart(startTime); + this.sessionFacade.setGameStart(startTime); } public getStartCountdownSec(): number { - return this.timer.getPreStartRemainingSec(); + return this.sessionFacade.getStartCountdownSec(); } // 現在の残り秒数を取得する public getRemainingTime(): number { - return this.timer.getRemainingTime(); + return this.sessionFacade.getRemainingTime(); } public isInputEnabled(): boolean { - return this.inputGate.canAcceptInput(); + return this.sessionFacade.canAcceptInput(); } public placeBomb(): string | null { - if (!this.inputGate.canAcceptInput()) return null; + if (!this.sessionFacade.canAcceptInput()) return null; if (!this.bombManager) return null; const placed = this.bombManager.placeBomb(); if (!placed) return null; @@ -81,12 +81,10 @@ // 入力と状態管理 private joystickInput = { x: 0, y: 0 }; - private isInitialized = false; - private isDestroyed = false; public lockInput(): () => void { this.joystickInput = { x: 0, y: 0 }; - return this.inputGate.lockInput(); + return this.sessionFacade.lockInput(); } constructor(container: HTMLDivElement, myId: string) { @@ -95,9 +93,6 @@ this.app = new Application(); this.worldContainer = new Container(); this.worldContainer.sortableChildren = true; - this.inputGate = new InputGate({ - isStartedProvider: () => this.timer.isStarted(), - }); this.initializeHitSubsystem(); } @@ -127,7 +122,7 @@ }); // 初期化完了前に destroy() が呼ばれていたら、ここで処理を中断して破棄する - if (this.isDestroyed) { + if (this.lifecycleState.shouldAbortInit()) { this.app.destroy(true, { children: true }); return; } @@ -141,14 +136,14 @@ // メインループの登録 this.app.ticker.add(this.tick); - this.isInitialized = true; + this.lifecycleState.markInitialized(); } /** * React側からジョイスティックの入力を受け取る */ public setJoystickInput(x: number, y: number) { - this.joystickInput = this.inputGate.sanitizeJoystickInput({ x, y }); + this.joystickInput = this.sessionFacade.sanitizeJoystickInput({ x, y }); } /** @@ -179,7 +174,7 @@ players: this.players, myId: this.myId, appearanceResolver: this.appearanceResolver, - getElapsedMs: () => this.timer.getElapsedMs(), + getElapsedMs: () => this.sessionFacade.getElapsedMs(), getJoystickInput: () => this.joystickInput, onGameStart: this.setGameStart.bind(this), onGameEnd: this.lockInput.bind(this), @@ -224,8 +219,8 @@ * クリーンアップ処理(コンポーネントアンマウント時) */ public destroy() { - this.isDestroyed = true; - if (this.isInitialized) { + this.lifecycleState.markDestroyed(); + if (this.lifecycleState.shouldDestroyApp()) { this.app.destroy(true, { children: true }); } this.bombManager?.destroy(); @@ -234,7 +229,7 @@ this.bombHitOrchestrator = null; this.playerDeathPolicy.dispose(); this.hitReportPolicy.clear(); - this.inputGate.reset(); + this.sessionFacade.reset(); this.players = {}; this.joystickInput = { x: 0, y: 0 }; diff --git a/apps/client/src/scenes/game/application/lifecycle/GameSessionFacade.ts b/apps/client/src/scenes/game/application/lifecycle/GameSessionFacade.ts new file mode 100644 index 0000000..7a5debe --- /dev/null +++ b/apps/client/src/scenes/game/application/lifecycle/GameSessionFacade.ts @@ -0,0 +1,62 @@ +/** + * GameSessionFacade + * ゲーム進行状態と入力可否の問い合わせを仲介する + * タイマーと入力ゲートの操作窓口を統一する + */ +import { GameTimer } from "@client/scenes/game/application/GameTimer"; +import { + InputGate, + type JoystickInput, +} from "./InputGate"; + +/** ゲーム進行状態と入力可否の窓口を提供する */ +export class GameSessionFacade { + private readonly timer = new GameTimer(); + private readonly inputGate: InputGate; + + constructor() { + this.inputGate = new InputGate({ + isStartedProvider: () => this.timer.isStarted(), + }); + } + + /** サーバー同期のゲーム開始時刻を設定する */ + public setGameStart(startTime: number): void { + this.timer.setGameStart(startTime); + } + + /** ゲーム開始前カウントダウンの残り秒数を返す */ + public getStartCountdownSec(): number { + return this.timer.getPreStartRemainingSec(); + } + + /** ゲーム残り時間を返す */ + public getRemainingTime(): number { + return this.timer.getRemainingTime(); + } + + /** 経過ミリ秒を返す */ + public getElapsedMs(): number { + return this.timer.getElapsedMs(); + } + + /** 現在入力を受け付け可能かを返す */ + public canAcceptInput(): boolean { + return this.inputGate.canAcceptInput(); + } + + /** 入力ロックを取得し,解除関数を返す */ + public lockInput(): () => void { + return this.inputGate.lockInput(); + } + + /** 入力可否に応じてジョイスティック入力を正規化して返す */ + public sanitizeJoystickInput(input: JoystickInput): JoystickInput { + return this.inputGate.sanitizeJoystickInput(input); + } + + /** セッション関連の内部状態を初期化する */ + public reset(): void { + this.inputGate.reset(); + } +} \ No newline at end of file diff --git a/apps/client/src/scenes/game/application/lifecycle/SceneLifecycleState.ts b/apps/client/src/scenes/game/application/lifecycle/SceneLifecycleState.ts new file mode 100644 index 0000000..f5d8bf7 --- /dev/null +++ b/apps/client/src/scenes/game/application/lifecycle/SceneLifecycleState.ts @@ -0,0 +1,31 @@ +/** + * SceneLifecycleState + * ゲームシーンの初期化状態と破棄状態を管理する + * init と destroy の分岐判定を一元化する + */ + +/** ゲームシーンのライフサイクル状態を管理する */ +export class SceneLifecycleState { + private isInitialized = false; + private isDestroyed = false; + + /** 初期化完了前に破棄要求があったかを返す */ + public shouldAbortInit(): boolean { + return this.isDestroyed; + } + + /** 初期化完了を記録する */ + public markInitialized(): void { + this.isInitialized = true; + } + + /** 破棄要求を記録する */ + public markDestroyed(): void { + this.isDestroyed = true; + } + + /** 破棄処理で Pixi を破棄すべきかを返す */ + public shouldDestroyApp(): boolean { + return this.isInitialized; + } +} \ No newline at end of file