diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts index 36cfdb4..29b2433 100644 --- a/apps/client/src/scenes/game/GameManager.ts +++ b/apps/client/src/scenes/game/GameManager.ts @@ -4,10 +4,6 @@ * マップ,ネットワーク同期,ゲームループを統合する */ import { Application, Container, Ticker } from "pixi.js"; -import type { - BombPlacedAckPayload, - BombPlacedPayload, -} from "@repo/shared"; import { AppearanceResolver } from "./application/AppearanceResolver"; import { BombManager } from "./entities/bomb/BombManager"; import { GameNetworkSync } from "./application/GameNetworkSync"; @@ -16,10 +12,29 @@ 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 { SocketGameActionSender } from "./application/network/GameActionSender"; +import { + GameSceneOrchestrator, + type GameSceneFactoryOptions, +} from "./application/orchestrators/GameSceneOrchestrator"; +import { + SocketGameActionSender, + type GameActionSender, +} from "./application/network/GameActionSender"; +import { + SocketPlayerMoveSender, + type MoveSender, +} from "./application/network/PlayerMoveSender"; import type { GamePlayers } from "./application/game.types"; +/** GameManager の依存注入オプション型 */ +export type GameManagerDependencies = { + sessionFacade?: GameSessionFacade; + lifecycleState?: SceneLifecycleState; + gameActionSender?: GameActionSender; + moveSender?: MoveSender; + sceneFactories?: GameSceneFactoryOptions; +}; + /** ゲームシーンの実行ライフサイクルを管理するマネージャー */ export class GameManager { private app: Application; @@ -27,20 +42,17 @@ private players: GamePlayers = {}; private myId: string; private container: HTMLDivElement; - private sessionFacade = new GameSessionFacade(); + private sessionFacade: GameSessionFacade; private appearanceResolver = new AppearanceResolver(); private bombManager: BombManager | null = null; private networkSync: GameNetworkSync | null = null; private gameLoop: GameLoop | null = null; private gameEventFacade: GameEventFacade; private combatFacade: CombatLifecycleFacade; - private lifecycleState = new SceneLifecycleState(); - private gameActionSender = new SocketGameActionSender(); - - // サーバーからゲーム開始通知(と開始時刻)を受け取った時に呼ぶ - public setGameStart(startTime: number) { - this.gameEventFacade.handleGameStart(startTime); - } + private lifecycleState: SceneLifecycleState; + private gameActionSender: GameActionSender; + private moveSender: MoveSender; + private sceneFactories?: GameSceneFactoryOptions; public getStartCountdownSec(): number { return this.sessionFacade.getStartCountdownSec(); @@ -65,14 +77,6 @@ return placed.tempBombId; } - public applyPlacedBombFromOthers(payload: BombPlacedPayload): void { - this.gameEventFacade.handleBombPlacedFromOthers(payload); - } - - public applyPlacedBombAck(payload: BombPlacedAckPayload): void { - this.gameEventFacade.handleBombPlacedAck(payload); - } - // 入力と状態管理 private joystickInput = { x: 0, y: 0 }; @@ -81,9 +85,18 @@ return this.sessionFacade.lockInput(); } - constructor(container: HTMLDivElement, myId: string) { + constructor( + container: HTMLDivElement, + myId: string, + dependencies: GameManagerDependencies = {}, + ) { this.container = container; // 明示的に代入 this.myId = myId; + this.sessionFacade = dependencies.sessionFacade ?? new GameSessionFacade(); + this.lifecycleState = dependencies.lifecycleState ?? new SceneLifecycleState(); + this.gameActionSender = dependencies.gameActionSender ?? new SocketGameActionSender(); + this.moveSender = dependencies.moveSender ?? new SocketPlayerMoveSender(); + this.sceneFactories = dependencies.sceneFactories; this.app = new Application(); this.worldContainer = new Container(); this.worldContainer.sortableChildren = true; @@ -156,6 +169,7 @@ appearanceResolver: this.appearanceResolver, getElapsedMs: () => this.sessionFacade.getElapsedMs(), getJoystickInput: () => this.joystickInput, + moveSender: this.moveSender, onGameStart: this.gameEventFacade.handleGameStart.bind(this.gameEventFacade), onGameEnd: this.lockInput.bind(this), onBombPlacedFromOthers: (payload) => { @@ -170,6 +184,7 @@ onBombExploded: (payload) => { this.combatFacade.handleBombExploded(payload); }, + factories: this.sceneFactories, }); const initializedScene = orchestrator.initialize(); diff --git a/apps/client/src/scenes/game/application/GameLoop.ts b/apps/client/src/scenes/game/application/GameLoop.ts index 1e23d4e..1ac43ba 100644 --- a/apps/client/src/scenes/game/application/GameLoop.ts +++ b/apps/client/src/scenes/game/application/GameLoop.ts @@ -13,7 +13,7 @@ import { CameraStep } from "./loopSteps/CameraStep"; import { BombStep } from "./loopSteps/BombStep"; import { resolveFrameDelta } from "./loopSteps/frameDelta"; -import { SocketPlayerMoveSender } from "./network/PlayerMoveSender"; +import type { MoveSender } from "./network/PlayerMoveSender"; type GameLoopOptions = { app: Application; @@ -22,6 +22,7 @@ myId: string; getJoystickInput: () => { x: number; y: number }; bombManager: BombManager; + moveSender: MoveSender; }; /** ゲームのフレーム更新順序を管理するループ制御クラス */ @@ -35,14 +36,14 @@ private bombStep: BombStep; private cameraStep: CameraStep; - constructor({ app, worldContainer, players, myId, getJoystickInput, bombManager }: GameLoopOptions) { + constructor({ app, worldContainer, players, myId, getJoystickInput, bombManager, moveSender }: GameLoopOptions) { this.app = app; this.worldContainer = worldContainer; this.players = players; this.myId = myId; this.inputStep = new InputStep({ getJoystickInput }); this.simulationStep = new SimulationStep({ - moveSender: new SocketPlayerMoveSender(), + moveSender, }); this.bombStep = new BombStep({ bombManager }); this.cameraStep = new CameraStep(); diff --git a/apps/client/src/scenes/game/application/orchestrators/GameSceneOrchestrator.ts b/apps/client/src/scenes/game/application/orchestrators/GameSceneOrchestrator.ts index 427b399..8e43682 100644 --- a/apps/client/src/scenes/game/application/orchestrators/GameSceneOrchestrator.ts +++ b/apps/client/src/scenes/game/application/orchestrators/GameSceneOrchestrator.ts @@ -14,8 +14,51 @@ 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 { MoveSender } from "@client/scenes/game/application/network/PlayerMoveSender"; import type { GamePlayers } from "@client/scenes/game/application/game.types"; +/** GameNetworkSync 生成入力型 */ +export type CreateNetworkSyncOptions = { + worldContainer: Container; + players: GamePlayers; + myId: string; + gameMap: GameMapController; + appearanceResolver: AppearanceResolver; + onGameStart: (startTime: number) => void; + onGameEnd: () => void; + onBombPlacedFromOthers: (payload: BombPlacedPayload) => void; + onBombPlacedAckFromNetwork: (payload: BombPlacedAckPayload) => void; + onPlayerDeadFromNetwork: (payload: PlayerDeadPayload) => void; +}; + +/** BombManager 生成入力型 */ +export type CreateBombManagerOptions = { + worldContainer: Container; + players: GamePlayers; + myId: string; + getElapsedMs: () => number; + appearanceResolver: AppearanceResolver; + onBombExploded: (payload: BombExplodedPayload) => void; +}; + +/** GameLoop 生成入力型 */ +export type CreateGameLoopOptions = { + app: Application; + worldContainer: Container; + players: GamePlayers; + myId: string; + getJoystickInput: () => { x: number; y: number }; + bombManager: BombManager; + moveSender: MoveSender; +}; + +/** サブシステム生成関数群の注入型 */ +export type GameSceneFactoryOptions = { + createNetworkSync?: (options: CreateNetworkSyncOptions) => GameNetworkSync; + createBombManager?: (options: CreateBombManagerOptions) => BombManager; + createGameLoop?: (options: CreateGameLoopOptions) => GameLoop; +}; + /** GameSceneOrchestrator の初期化入力 */ export type GameSceneOrchestratorOptions = { app: Application; @@ -25,12 +68,14 @@ appearanceResolver: AppearanceResolver; getElapsedMs: () => number; getJoystickInput: () => { x: number; y: number }; + moveSender: MoveSender; onGameStart: (startTime: number) => void; onGameEnd: () => void; onBombPlacedFromOthers: (payload: BombPlacedPayload) => void; onBombPlacedAckFromNetwork: (payload: BombPlacedAckPayload) => void; onPlayerDeadFromNetwork: (payload: PlayerDeadPayload) => void; onBombExploded: (payload: BombExplodedPayload) => void; + factories?: GameSceneFactoryOptions; }; /** 初期化済みサブシステム参照の戻り値型 */ @@ -50,12 +95,16 @@ private readonly appearanceResolver: AppearanceResolver; private readonly getElapsedMs: () => number; private readonly getJoystickInput: () => { x: number; y: number }; + private readonly moveSender: MoveSender; 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; + private readonly createNetworkSync: (options: CreateNetworkSyncOptions) => GameNetworkSync; + private readonly createBombManager: (options: CreateBombManagerOptions) => BombManager; + private readonly createGameLoop: (options: CreateGameLoopOptions) => GameLoop; constructor({ app, @@ -65,12 +114,14 @@ appearanceResolver, getElapsedMs, getJoystickInput, + moveSender, onGameStart, onGameEnd, onBombPlacedFromOthers, onBombPlacedAckFromNetwork, onPlayerDeadFromNetwork, onBombExploded, + factories, }: GameSceneOrchestratorOptions) { this.app = app; this.worldContainer = worldContainer; @@ -79,12 +130,16 @@ this.appearanceResolver = appearanceResolver; this.getElapsedMs = getElapsedMs; this.getJoystickInput = getJoystickInput; + this.moveSender = moveSender; this.onGameStart = onGameStart; this.onGameEnd = onGameEnd; this.onBombPlacedFromOthers = onBombPlacedFromOthers; this.onBombPlacedAckFromNetwork = onBombPlacedAckFromNetwork; this.onPlayerDeadFromNetwork = onPlayerDeadFromNetwork; this.onBombExploded = onBombExploded; + this.createNetworkSync = factories?.createNetworkSync ?? ((options) => new GameNetworkSync(options)); + this.createBombManager = factories?.createBombManager ?? ((options) => new BombManager(options)); + this.createGameLoop = factories?.createGameLoop ?? ((options) => new GameLoop(options)); } /** シーン配線を順序どおり初期化し,参照を返す */ @@ -111,7 +166,7 @@ /** ネットワーク購読を初期化してバインドする */ private initializeNetworkSync(gameMap: GameMapController): GameNetworkSync { - const networkSync = new GameNetworkSync({ + const networkSync = this.createNetworkSync({ worldContainer: this.worldContainer, players: this.players, myId: this.myId, @@ -129,7 +184,7 @@ /** 爆弾サブシステムを初期化する */ private initializeBombSubsystem(): BombManager { - return new BombManager({ + return this.createBombManager({ worldContainer: this.worldContainer, players: this.players, myId: this.myId, @@ -141,13 +196,14 @@ /** ゲームループを初期化する */ private initializeGameLoop(bombManager: BombManager): GameLoop { - return new GameLoop({ + return this.createGameLoop({ app: this.app, worldContainer: this.worldContainer, players: this.players, myId: this.myId, getJoystickInput: this.getJoystickInput, bombManager, + moveSender: this.moveSender, }); } } \ No newline at end of file