diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts index fe3d470..5cd39fb 100644 --- a/apps/client/src/scenes/game/GameManager.ts +++ b/apps/client/src/scenes/game/GameManager.ts @@ -8,21 +8,15 @@ BombPlacedAckPayload, BombPlacedPayload, } from "@repo/shared"; -import { config } from "@client/config"; import { socketManager } from "@client/network/SocketManager"; import { AppearanceResolver } from "./application/AppearanceResolver"; -import { GameMapController } from "./entities/map/GameMapController"; 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 { HitReportPolicy } from "./application/combat/HitReportPolicy"; -import type { BombHitEvaluationResult } from "./application/BombHitOrchestrator"; +import { SceneLifecycleState } from "./application/lifecycle/SceneLifecycleState"; +import { GameSessionFacade } from "./application/lifecycle/GameSessionFacade"; +import { CombatLifecycleFacade } from "./application/combat/CombatLifecycleFacade"; +import { GameSceneOrchestrator } from "./application/orchestrators/GameSceneOrchestrator"; import type { GamePlayers } from "./application/game.types"; /** ゲームシーンの実行ライフサイクルを管理するマネージャー */ @@ -32,38 +26,34 @@ private players: GamePlayers = {}; private myId: string; private container: HTMLDivElement; - private gameMap!: GameMapController; - private timer = new GameTimer(); + private sessionFacade = new GameSessionFacade(); private appearanceResolver = new AppearanceResolver(); private bombManager: BombManager | null = null; - private bombHitOrchestrator: BombHitOrchestrator | null = null; private networkSync: GameNetworkSync | null = null; private gameLoop: GameLoop | null = null; - private playerDeathPolicy!: PlayerDeathPolicy; - private playerHitEffectOrchestrator!: PlayerHitEffectOrchestrator; - private inputGate: InputGate; - private hitReportPolicy = new HitReportPolicy(); + private combatFacade: CombatLifecycleFacade; + private lifecycleState = new SceneLifecycleState(); // サーバーからゲーム開始通知(と開始時刻)を受け取った時に呼ぶ 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; @@ -82,12 +72,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) { @@ -96,23 +84,13 @@ this.app = new Application(); this.worldContainer = new Container(); this.worldContainer.sortableChildren = true; - this.inputGate = new InputGate({ - isStartedProvider: () => this.timer.isStarted(), - }); - this.initializeHitSubsystem(); - } - - /** 被弾時の入力制御と演出発火のサブシステムを初期化する */ - private initializeHitSubsystem(): void { - this.playerDeathPolicy = new PlayerDeathPolicy({ - myId: this.myId, - hitStunMs: config.GAME_CONFIG.PLAYER_HIT_STUN_MS, - acquireInputLock: this.lockInput.bind(this), - }); - this.playerHitEffectOrchestrator = new PlayerHitEffectOrchestrator({ + this.combatFacade = new CombatLifecycleFacade({ players: this.players, - blinkDurationMs: config.GAME_CONFIG.PLAYER_HIT_EFFECT.BLINK_DURATION_MS, - dedupWindowMs: config.GAME_CONFIG.PLAYER_HIT_EFFECT.DEDUP_WINDOW_MS, + myId: this.myId, + acquireInputLock: this.lockInput.bind(this), + onSendBombHitReport: (bombId) => { + socketManager.game.sendBombHitReport({ bombId }); + }, }); } @@ -128,31 +106,28 @@ }); // 初期化完了前に destroy() が呼ばれていたら、ここで処理を中断して破棄する - if (this.isDestroyed) { + if (this.lifecycleState.shouldAbortInit()) { this.app.destroy(true, { children: true }); return; } this.container.appendChild(this.app.canvas); - this.initializeWorld(); - this.initializeNetworkSync(); - this.initializeBombSubsystem(); - this.initializeGameLoop(); + this.initializeSceneSubsystems(); // サーバーへゲーム準備完了を通知 socketManager.game.readyForGame(); // メインループの登録 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 }); } /** @@ -162,22 +137,16 @@ this.gameLoop?.tick(ticker); }; - /** 背景マップとワールド描画コンテナを初期化する */ - private initializeWorld(): void { - const gameMap = new GameMapController(this.appearanceResolver); - this.gameMap = gameMap; - this.worldContainer.addChild(gameMap.getDisplayObject()); - this.app.stage.addChild(this.worldContainer); - } - - /** ネットワーク購読を初期化してバインドする */ - private initializeNetworkSync(): void { - this.networkSync = new GameNetworkSync({ + /** ゲームシーンのサブシステムを初期化して配線する */ + private initializeSceneSubsystems(): void { + const orchestrator = new GameSceneOrchestrator({ + app: this.app, worldContainer: this.worldContainer, players: this.players, myId: this.myId, - gameMap: this.gameMap, appearanceResolver: this.appearanceResolver, + getElapsedMs: () => this.sessionFacade.getElapsedMs(), + getJoystickInput: () => this.joystickInput, onGameStart: this.setGameStart.bind(this), onGameEnd: this.lockInput.bind(this), onBombPlacedFromOthers: (payload) => { @@ -187,82 +156,31 @@ this.applyPlacedBombAck(payload); }, onPlayerDeadFromNetwork: (payload) => { - this.playerDeathPolicy.applyPlayerDeadEvent(payload); - this.playerHitEffectOrchestrator.handleNetworkPlayerDead(payload.playerId, this.myId); + this.combatFacade.handleNetworkPlayerDead(payload); }, - }); - this.networkSync.bind(); - } - - /** 爆弾管理と当たり判定橋渡しを初期化する */ - private initializeBombSubsystem(): void { - const bombHitContextProvider = new BombHitContextProvider({ - players: this.players, - myId: this.myId, - }); - this.bombHitOrchestrator = new BombHitOrchestrator({ - contextProvider: bombHitContextProvider, - }); - - this.bombManager = new BombManager({ - worldContainer: this.worldContainer, - players: this.players, - myId: this.myId, - getElapsedMs: () => this.timer.getElapsedMs(), - appearanceResolver: this.appearanceResolver, onBombExploded: (payload) => { - const result = this.bombHitOrchestrator?.handleBombExploded(payload); - this.handleBombHitEvaluation(result, payload.bombId); + this.combatFacade.handleBombExploded(payload); }, }); - } - /** ゲームループを初期化する */ - private initializeGameLoop(): void { - if (!this.bombManager) { - return; - } - - this.gameLoop = new GameLoop({ - app: this.app, - worldContainer: this.worldContainer, - players: this.players, - myId: this.myId, - getJoystickInput: () => this.joystickInput, - bombManager: this.bombManager, - }); - } - - /** 爆弾当たり判定の評価結果を受け取り,後続処理へ接続する */ - private handleBombHitEvaluation( - result: BombHitEvaluationResult | undefined, - bombId: string, - ): void { - if (!this.hitReportPolicy.shouldSendReport(result, bombId)) { - return; - } - - this.playerDeathPolicy.applyLocalHitStun(); - this.playerHitEffectOrchestrator.handleLocalBombHit(this.myId); - - socketManager.game.sendBombHitReport({ bombId }); + const initializedScene = orchestrator.initialize(); + this.networkSync = initializedScene.networkSync; + this.bombManager = initializedScene.bombManager; + this.gameLoop = initializedScene.gameLoop; } /** * クリーンアップ処理(コンポーネントアンマウント時) */ public destroy() { - this.isDestroyed = true; - if (this.isInitialized) { + this.lifecycleState.markDestroyed(); + if (this.lifecycleState.shouldDestroyApp()) { this.app.destroy(true, { children: true }); } this.bombManager?.destroy(); this.bombManager = null; - this.bombHitOrchestrator?.clear(); - this.bombHitOrchestrator = null; - this.playerDeathPolicy.dispose(); - this.hitReportPolicy.clear(); - this.inputGate.reset(); + this.combatFacade.dispose(); + this.sessionFacade.reset(); this.players = {}; this.joystickInput = { x: 0, y: 0 }; diff --git a/apps/client/src/scenes/game/application/GameLoop.ts b/apps/client/src/scenes/game/application/GameLoop.ts index 8973ac4..1e23d4e 100644 --- a/apps/client/src/scenes/game/application/GameLoop.ts +++ b/apps/client/src/scenes/game/application/GameLoop.ts @@ -13,6 +13,7 @@ import { CameraStep } from "./loopSteps/CameraStep"; import { BombStep } from "./loopSteps/BombStep"; import { resolveFrameDelta } from "./loopSteps/frameDelta"; +import { SocketPlayerMoveSender } from "./network/PlayerMoveSender"; type GameLoopOptions = { app: Application; @@ -40,7 +41,9 @@ this.players = players; this.myId = myId; this.inputStep = new InputStep({ getJoystickInput }); - this.simulationStep = new SimulationStep(); + this.simulationStep = new SimulationStep({ + moveSender: new SocketPlayerMoveSender(), + }); this.bombStep = new BombStep({ bombManager }); this.cameraStep = new CameraStep(); } diff --git a/apps/client/src/scenes/game/application/combat/CombatLifecycleFacade.ts b/apps/client/src/scenes/game/application/combat/CombatLifecycleFacade.ts new file mode 100644 index 0000000..2fb08fc --- /dev/null +++ b/apps/client/src/scenes/game/application/combat/CombatLifecycleFacade.ts @@ -0,0 +1,83 @@ +/** + * CombatLifecycleFacade + * 被弾判定から硬直,演出,報告送信までの戦闘ライフサイクルを管理する + * ゲームマネージャーから被弾関連の責務を分離する + */ +import { config } from "@client/config"; +import type { PlayerDeadPayload } from "@repo/shared"; +import type { BombExplodedPayload } from "@client/scenes/game/entities/bomb/BombManager"; +import { BombHitContextProvider } from "@client/scenes/game/application/BombHitContextProvider"; +import { BombHitOrchestrator } from "@client/scenes/game/application/BombHitOrchestrator"; +import { PlayerDeathPolicy } from "@client/scenes/game/application/PlayerDeathPolicy"; +import { PlayerHitEffectOrchestrator } from "@client/scenes/game/application/PlayerHitEffectOrchestrator"; +import type { GamePlayers } from "@client/scenes/game/application/game.types"; +import { HitReportPolicy } from "./HitReportPolicy"; + +/** CombatLifecycleFacade の初期化入力 */ +export type CombatLifecycleFacadeOptions = { + players: GamePlayers; + myId: string; + acquireInputLock: () => () => void; + onSendBombHitReport: (bombId: string) => void; +}; + +/** 被弾関連ライフサイクルの制御を担当する */ +export class CombatLifecycleFacade { + private readonly myId: string; + private readonly onSendBombHitReport: (bombId: string) => void; + private readonly bombHitOrchestrator: BombHitOrchestrator; + private readonly playerDeathPolicy: PlayerDeathPolicy; + private readonly playerHitEffectOrchestrator: PlayerHitEffectOrchestrator; + private readonly hitReportPolicy = new HitReportPolicy(); + + constructor({ + players, + myId, + acquireInputLock, + onSendBombHitReport, + }: CombatLifecycleFacadeOptions) { + this.myId = myId; + this.onSendBombHitReport = onSendBombHitReport; + this.bombHitOrchestrator = new BombHitOrchestrator({ + contextProvider: new BombHitContextProvider({ + players, + myId, + }), + }); + this.playerDeathPolicy = new PlayerDeathPolicy({ + myId, + hitStunMs: config.GAME_CONFIG.PLAYER_HIT_STUN_MS, + acquireInputLock, + }); + this.playerHitEffectOrchestrator = new PlayerHitEffectOrchestrator({ + players, + blinkDurationMs: config.GAME_CONFIG.PLAYER_HIT_EFFECT.BLINK_DURATION_MS, + dedupWindowMs: config.GAME_CONFIG.PLAYER_HIT_EFFECT.DEDUP_WINDOW_MS, + }); + } + + /** 爆弾爆発時の判定と後続処理を実行する */ + public handleBombExploded(payload: BombExplodedPayload): void { + const result = this.bombHitOrchestrator.handleBombExploded(payload); + if (!this.hitReportPolicy.shouldSendReport(result, payload.bombId)) { + return; + } + + this.playerDeathPolicy.applyLocalHitStun(); + this.playerHitEffectOrchestrator.handleLocalBombHit(this.myId); + this.onSendBombHitReport(payload.bombId); + } + + /** ネットワーク被弾通知を適用する */ + public handleNetworkPlayerDead(payload: PlayerDeadPayload): void { + this.playerDeathPolicy.applyPlayerDeadEvent(payload); + this.playerHitEffectOrchestrator.handleNetworkPlayerDead(payload.playerId, this.myId); + } + + /** 管理中リソースを破棄する */ + public dispose(): void { + this.bombHitOrchestrator.clear(); + this.playerDeathPolicy.dispose(); + this.hitReportPolicy.clear(); + } +} \ No newline at end of file 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 diff --git a/apps/client/src/scenes/game/application/loopSteps/SimulationStep.ts b/apps/client/src/scenes/game/application/loopSteps/SimulationStep.ts index 750a396..92b13b3 100644 --- a/apps/client/src/scenes/game/application/loopSteps/SimulationStep.ts +++ b/apps/client/src/scenes/game/application/loopSteps/SimulationStep.ts @@ -4,10 +4,16 @@ * ローカル更新とリモート補間更新を順に実行する */ import { config } from "@client/config"; -import { socketManager } from "@client/network/SocketManager"; import { LocalPlayerController, RemotePlayerController } from "@client/scenes/game/entities/player/PlayerController"; +import type { MoveSender } from "@client/scenes/game/application/network/PlayerMoveSender"; import type { GamePlayers } from "../game.types"; +/** SimulationStep の初期化入力 */ +type SimulationStepOptions = { + moveSender: MoveSender; + nowMsProvider?: () => number; +}; + type SimulationStepParams = { me: LocalPlayerController; players: GamePlayers; @@ -17,9 +23,16 @@ /** シミュレーション段の更新処理を担うステップ */ export class SimulationStep { + private readonly moveSender: MoveSender; + private readonly nowMsProvider: () => number; private lastPositionSentTime = 0; private wasMoving = false; + constructor({ moveSender, nowMsProvider = () => performance.now() }: SimulationStepOptions) { + this.moveSender = moveSender; + this.nowMsProvider = nowMsProvider; + } + public run({ me, players, deltaSeconds, isMoving }: SimulationStepParams) { this.runLocalSimulation({ me, isMoving }); this.runRemoteSimulation({ players, deltaSeconds }); @@ -29,16 +42,16 @@ if (isMoving) { me.tick(); - const now = performance.now(); + const now = this.nowMsProvider(); if (now - this.lastPositionSentTime >= config.GAME_CONFIG.PLAYER_POSITION_UPDATE_MS) { const position = me.getPosition(); - socketManager.game.sendMove(position.x, position.y); + this.moveSender.sendMove(position.x, position.y); this.lastPositionSentTime = now; } } else if (this.wasMoving) { me.tick(); const position = me.getPosition(); - socketManager.game.sendMove(position.x, position.y); + this.moveSender.sendMove(position.x, position.y); } else { me.tick(); } diff --git a/apps/client/src/scenes/game/application/network/PlayerMoveSender.ts b/apps/client/src/scenes/game/application/network/PlayerMoveSender.ts new file mode 100644 index 0000000..9efa0c6 --- /dev/null +++ b/apps/client/src/scenes/game/application/network/PlayerMoveSender.ts @@ -0,0 +1,19 @@ +/** + * PlayerMoveSender + * ローカルプレイヤー移動の送信責務を提供する + * シミュレーション層から通信実装を分離する + */ +import { socketManager } from "@client/network/SocketManager"; + +/** 移動送信のインターフェース型 */ +export type MoveSender = { + sendMove: (x: number, y: number) => void; +}; + +/** ソケット経由で移動送信を行う実装 */ +export class SocketPlayerMoveSender implements MoveSender { + /** 指定座標をサーバーへ送信する */ + public sendMove(x: number, y: number): void { + socketManager.game.sendMove(x, y); + } +} \ No newline at end of file diff --git a/apps/client/src/scenes/game/application/orchestrators/GameSceneOrchestrator.ts b/apps/client/src/scenes/game/application/orchestrators/GameSceneOrchestrator.ts new file mode 100644 index 0000000..427b399 --- /dev/null +++ b/apps/client/src/scenes/game/application/orchestrators/GameSceneOrchestrator.ts @@ -0,0 +1,153 @@ +/** + * GameSceneOrchestrator + * ゲームシーン初期化時のサブシステム配線を担当する + * ワールド,ネットワーク,爆弾,ループの生成順序を統制する + */ +import { Application, Container } from "pixi.js"; +import type { + BombPlacedAckPayload, + BombPlacedPayload, + PlayerDeadPayload, +} from "@repo/shared"; +import { AppearanceResolver } from "@client/scenes/game/application/AppearanceResolver"; +import { GameMapController } from "@client/scenes/game/entities/map/GameMapController"; +import { GameNetworkSync } from "@client/scenes/game/application/GameNetworkSync"; +import { BombManager, type BombExplodedPayload } from "@client/scenes/game/entities/bomb/BombManager"; +import { GameLoop } from "@client/scenes/game/application/GameLoop"; +import type { GamePlayers } from "@client/scenes/game/application/game.types"; + +/** GameSceneOrchestrator の初期化入力 */ +export type GameSceneOrchestratorOptions = { + app: Application; + worldContainer: Container; + players: GamePlayers; + myId: string; + appearanceResolver: AppearanceResolver; + getElapsedMs: () => number; + getJoystickInput: () => { x: number; y: number }; + onGameStart: (startTime: number) => void; + onGameEnd: () => void; + onBombPlacedFromOthers: (payload: BombPlacedPayload) => void; + onBombPlacedAckFromNetwork: (payload: BombPlacedAckPayload) => void; + onPlayerDeadFromNetwork: (payload: PlayerDeadPayload) => void; + onBombExploded: (payload: BombExplodedPayload) => void; +}; + +/** 初期化済みサブシステム参照の戻り値型 */ +export type InitializedGameScene = { + gameMap: GameMapController; + networkSync: GameNetworkSync; + bombManager: BombManager; + gameLoop: GameLoop; +}; + +/** ゲームシーン初期化配線を担当する */ +export class GameSceneOrchestrator { + private readonly app: Application; + private readonly worldContainer: Container; + private readonly players: GamePlayers; + private readonly myId: string; + private readonly appearanceResolver: AppearanceResolver; + private readonly getElapsedMs: () => number; + private readonly getJoystickInput: () => { x: number; y: number }; + private readonly onGameStart: (startTime: number) => void; + private readonly onGameEnd: () => void; + private readonly onBombPlacedFromOthers: (payload: BombPlacedPayload) => void; + private readonly onBombPlacedAckFromNetwork: (payload: BombPlacedAckPayload) => void; + private readonly onPlayerDeadFromNetwork: (payload: PlayerDeadPayload) => void; + private readonly onBombExploded: (payload: BombExplodedPayload) => void; + + constructor({ + app, + worldContainer, + players, + myId, + appearanceResolver, + getElapsedMs, + getJoystickInput, + onGameStart, + onGameEnd, + onBombPlacedFromOthers, + onBombPlacedAckFromNetwork, + onPlayerDeadFromNetwork, + onBombExploded, + }: GameSceneOrchestratorOptions) { + this.app = app; + this.worldContainer = worldContainer; + this.players = players; + this.myId = myId; + this.appearanceResolver = appearanceResolver; + this.getElapsedMs = getElapsedMs; + this.getJoystickInput = getJoystickInput; + this.onGameStart = onGameStart; + this.onGameEnd = onGameEnd; + this.onBombPlacedFromOthers = onBombPlacedFromOthers; + this.onBombPlacedAckFromNetwork = onBombPlacedAckFromNetwork; + this.onPlayerDeadFromNetwork = onPlayerDeadFromNetwork; + this.onBombExploded = onBombExploded; + } + + /** シーン配線を順序どおり初期化し,参照を返す */ + public initialize(): InitializedGameScene { + const gameMap = this.initializeWorld(); + const networkSync = this.initializeNetworkSync(gameMap); + const bombManager = this.initializeBombSubsystem(); + const gameLoop = this.initializeGameLoop(bombManager); + return { + gameMap, + networkSync, + bombManager, + gameLoop, + }; + } + + /** 背景マップとワールド描画コンテナを初期化する */ + private initializeWorld(): GameMapController { + const gameMap = new GameMapController(this.appearanceResolver); + this.worldContainer.addChild(gameMap.getDisplayObject()); + this.app.stage.addChild(this.worldContainer); + return gameMap; + } + + /** ネットワーク購読を初期化してバインドする */ + private initializeNetworkSync(gameMap: GameMapController): GameNetworkSync { + const networkSync = new GameNetworkSync({ + worldContainer: this.worldContainer, + players: this.players, + myId: this.myId, + gameMap, + appearanceResolver: this.appearanceResolver, + onGameStart: this.onGameStart, + onGameEnd: this.onGameEnd, + onBombPlacedFromOthers: this.onBombPlacedFromOthers, + onBombPlacedAckFromNetwork: this.onBombPlacedAckFromNetwork, + onPlayerDeadFromNetwork: this.onPlayerDeadFromNetwork, + }); + networkSync.bind(); + return networkSync; + } + + /** 爆弾サブシステムを初期化する */ + private initializeBombSubsystem(): BombManager { + return new BombManager({ + worldContainer: this.worldContainer, + players: this.players, + myId: this.myId, + getElapsedMs: this.getElapsedMs, + appearanceResolver: this.appearanceResolver, + onBombExploded: this.onBombExploded, + }); + } + + /** ゲームループを初期化する */ + private initializeGameLoop(bombManager: BombManager): GameLoop { + return new GameLoop({ + app: this.app, + worldContainer: this.worldContainer, + players: this.players, + myId: this.myId, + getJoystickInput: this.getJoystickInput, + bombManager, + }); + } +} \ No newline at end of file