diff --git a/apps/client/src/scenes/game/application/GameNetworkSync.ts b/apps/client/src/scenes/game/application/GameNetworkSync.ts index 129ddf9..fa34c80 100644 --- a/apps/client/src/scenes/game/application/GameNetworkSync.ts +++ b/apps/client/src/scenes/game/application/GameNetworkSync.ts @@ -7,21 +7,18 @@ import type { BombPlacedAckPayload, BombPlacedPayload, - CurrentPlayersPayload, GameStartPayload, - NewPlayerPayload, PlayerDeadPayload, - RemovePlayerPayload, - UpdateMapCellsPayload, - UpdatePlayersPayload, } from "@repo/shared"; import { AppearanceResolver } from "./AppearanceResolver"; -import { LocalPlayerController, RemotePlayerController } from "@client/scenes/game/entities/player/PlayerController"; import { GameMapController } from "@client/scenes/game/entities/map/GameMapController"; import { createNetworkSubscriptions, type SocketSubscriptionDictionary, } from "./network/NetworkSubscriptions"; +import { PlayerSyncHandler } from "./network/handlers/PlayerSyncHandler"; +import { MapSyncHandler } from "./network/handlers/MapSyncHandler"; +import { CombatSyncHandler } from "./network/handlers/CombatSyncHandler"; import type { GamePlayers } from "./game.types"; const ENABLE_DEBUG_LOG = import.meta.env.DEV; @@ -41,16 +38,11 @@ /** ゲーム中のネットワークイベント購読と同期処理を管理する */ export class GameNetworkSync { - private worldContainer: Container; - private players: GamePlayers; - private myId: string; - private gameMap: GameMapController; - private appearanceResolver: AppearanceResolver; + private playerSyncHandler: PlayerSyncHandler; + private mapSyncHandler: MapSyncHandler; + private combatSyncHandler: CombatSyncHandler; private onGameStart: (startTime: number) => void; private onGameEnd: () => void; - private onBombPlacedFromOthers: (payload: BombPlacedPayload) => void; - private onBombPlacedAckFromNetwork: (payload: BombPlacedAckPayload) => void; - private onPlayerDeadFromNetwork: (payload: PlayerDeadPayload) => void; private socketSubscriptions: SocketSubscriptionDictionary; private isBound = false; @@ -62,22 +54,6 @@ console.log(message); }; - private handleCurrentPlayers = (serverPlayers: CurrentPlayersPayload) => { - serverPlayers.forEach((p) => { - const playerController = p.id === this.myId - ? new LocalPlayerController(p, this.appearanceResolver) - : new RemotePlayerController(p, this.appearanceResolver); - this.worldContainer.addChild(playerController.getDisplayObject()); - this.players[p.id] = playerController; - }); - }; - - private handleNewPlayer = (p: NewPlayerPayload) => { - const playerController = new RemotePlayerController(p, this.appearanceResolver); - this.worldContainer.addChild(playerController.getDisplayObject()); - this.players[p.id] = playerController; - }; - private handleGameStart = (data: GameStartPayload) => { if (data && data.startTime) { this.onGameStart(data.startTime); @@ -85,45 +61,10 @@ } }; - private handlePlayerUpdates = (changedPlayers: UpdatePlayersPayload) => { - // UPDATE_PLAYERS は差分のみ届くため,対象IDだけ上書き更新する - changedPlayers.forEach((playerData) => { - const target = this.players[playerData.id]; - if (target && target instanceof RemotePlayerController) { - target.applyRemoteUpdate({ x: playerData.x, y: playerData.y }); - } - }); - }; - - private handleRemovePlayer = (id: RemovePlayerPayload) => { - const target = this.players[id]; - if (target) { - this.worldContainer.removeChild(target.getDisplayObject()); - target.destroy(); - delete this.players[id]; - } - }; - - private handleUpdateMapCells = (updates: UpdateMapCellsPayload) => { - this.gameMap.updateCells(updates); - }; - private handleGameEnd = () => { this.onGameEnd(); }; - private handleBombPlaced = (payload: BombPlacedPayload) => { - this.onBombPlacedFromOthers(payload); - }; - - private handleBombPlacedAck = (payload: BombPlacedAckPayload) => { - this.onBombPlacedAckFromNetwork(payload); - }; - - private handlePlayerDead = (payload: PlayerDeadPayload) => { - this.onPlayerDeadFromNetwork(payload); - }; - constructor({ worldContainer, players, @@ -136,27 +77,33 @@ onBombPlacedAckFromNetwork, onPlayerDeadFromNetwork, }: GameNetworkSyncOptions) { - this.worldContainer = worldContainer; - this.players = players; - this.myId = myId; - this.gameMap = gameMap; - this.appearanceResolver = appearanceResolver; + this.playerSyncHandler = new PlayerSyncHandler({ + worldContainer, + players, + myId, + appearanceResolver, + }); + this.mapSyncHandler = new MapSyncHandler({ + gameMap, + }); + this.combatSyncHandler = new CombatSyncHandler({ + onBombPlacedFromOthers, + onBombPlacedAckFromNetwork, + onPlayerDeadFromNetwork, + }); this.onGameStart = onGameStart; this.onGameEnd = onGameEnd; - this.onBombPlacedFromOthers = onBombPlacedFromOthers; - this.onBombPlacedAckFromNetwork = onBombPlacedAckFromNetwork; - this.onPlayerDeadFromNetwork = onPlayerDeadFromNetwork; this.socketSubscriptions = createNetworkSubscriptions({ - onCurrentPlayers: this.handleCurrentPlayers, - onNewPlayer: this.handleNewPlayer, + onCurrentPlayers: this.playerSyncHandler.handleCurrentPlayers, + onNewPlayer: this.playerSyncHandler.handleNewPlayer, onGameStart: this.handleGameStart, - onUpdatePlayers: this.handlePlayerUpdates, - onRemovePlayer: this.handleRemovePlayer, - onUpdateMapCells: this.handleUpdateMapCells, + onUpdatePlayers: this.playerSyncHandler.handlePlayerUpdates, + onRemovePlayer: this.playerSyncHandler.handleRemovePlayer, + onUpdateMapCells: this.mapSyncHandler.handleUpdateMapCells, onGameEnd: this.handleGameEnd, - onBombPlaced: this.handleBombPlaced, - onBombPlacedAck: this.handleBombPlacedAck, - onPlayerDead: this.handlePlayerDead, + onBombPlaced: this.combatSyncHandler.handleBombPlaced, + onBombPlacedAck: this.combatSyncHandler.handleBombPlacedAck, + onPlayerDead: this.combatSyncHandler.handlePlayerDead, }); } diff --git a/apps/client/src/scenes/game/application/network/handlers/CombatSyncHandler.ts b/apps/client/src/scenes/game/application/network/handlers/CombatSyncHandler.ts new file mode 100644 index 0000000..03e58fe --- /dev/null +++ b/apps/client/src/scenes/game/application/network/handlers/CombatSyncHandler.ts @@ -0,0 +1,49 @@ +/** + * CombatSyncHandler + * 戦闘関連イベントの受信処理を担当する + * 爆弾設置と被弾通知を上位へ橋渡しする + */ +import type { + BombPlacedAckPayload, + BombPlacedPayload, + PlayerDeadPayload, +} from "@repo/shared"; + +/** CombatSyncHandler の初期化入力 */ +export type CombatSyncHandlerOptions = { + onBombPlacedFromOthers: (payload: BombPlacedPayload) => void; + onBombPlacedAckFromNetwork: (payload: BombPlacedAckPayload) => void; + onPlayerDeadFromNetwork: (payload: PlayerDeadPayload) => void; +}; + +/** 戦闘関連イベントの橋渡しを担当する */ +export class CombatSyncHandler { + private readonly onBombPlacedFromOthers: (payload: BombPlacedPayload) => void; + private readonly onBombPlacedAckFromNetwork: (payload: BombPlacedAckPayload) => void; + private readonly onPlayerDeadFromNetwork: (payload: PlayerDeadPayload) => void; + + constructor({ + onBombPlacedFromOthers, + onBombPlacedAckFromNetwork, + onPlayerDeadFromNetwork, + }: CombatSyncHandlerOptions) { + this.onBombPlacedFromOthers = onBombPlacedFromOthers; + this.onBombPlacedAckFromNetwork = onBombPlacedAckFromNetwork; + this.onPlayerDeadFromNetwork = onPlayerDeadFromNetwork; + } + + /** 他プレイヤーの爆弾設置イベントを橋渡しする */ + public handleBombPlaced = (payload: BombPlacedPayload): void => { + this.onBombPlacedFromOthers(payload); + }; + + /** 設置ACKイベントを橋渡しする */ + public handleBombPlacedAck = (payload: BombPlacedAckPayload): void => { + this.onBombPlacedAckFromNetwork(payload); + }; + + /** 被弾通知イベントを橋渡しする */ + public handlePlayerDead = (payload: PlayerDeadPayload): void => { + this.onPlayerDeadFromNetwork(payload); + }; +} \ No newline at end of file diff --git a/apps/client/src/scenes/game/application/network/handlers/MapSyncHandler.ts b/apps/client/src/scenes/game/application/network/handlers/MapSyncHandler.ts new file mode 100644 index 0000000..2e5f871 --- /dev/null +++ b/apps/client/src/scenes/game/application/network/handlers/MapSyncHandler.ts @@ -0,0 +1,26 @@ +/** + * MapSyncHandler + * マップ同期イベントの受信処理を担当する + * セル更新データをマップへ適用する + */ +import type { UpdateMapCellsPayload } from "@repo/shared"; +import { GameMapController } from "@client/scenes/game/entities/map/GameMapController"; + +/** MapSyncHandler の初期化入力 */ +export type MapSyncHandlerOptions = { + gameMap: GameMapController; +}; + +/** マップ更新イベントの適用を担当する */ +export class MapSyncHandler { + private readonly gameMap: GameMapController; + + constructor({ gameMap }: MapSyncHandlerOptions) { + this.gameMap = gameMap; + } + + /** マップセル更新を適用する */ + public handleUpdateMapCells = (updates: UpdateMapCellsPayload): void => { + this.gameMap.updateCells(updates); + }; +} \ No newline at end of file diff --git a/apps/client/src/scenes/game/application/network/handlers/PlayerSyncHandler.ts b/apps/client/src/scenes/game/application/network/handlers/PlayerSyncHandler.ts new file mode 100644 index 0000000..a1556db --- /dev/null +++ b/apps/client/src/scenes/game/application/network/handlers/PlayerSyncHandler.ts @@ -0,0 +1,81 @@ +/** + * PlayerSyncHandler + * プレイヤー同期イベントの受信処理を担当する + * 生成,更新,削除の適用を一元管理する + */ +import { Container } from "pixi.js"; +import type { + CurrentPlayersPayload, + NewPlayerPayload, + RemovePlayerPayload, + UpdatePlayersPayload, +} from "@repo/shared"; +import { + LocalPlayerController, + RemotePlayerController, +} from "@client/scenes/game/entities/player/PlayerController"; +import { AppearanceResolver } from "@client/scenes/game/application/AppearanceResolver"; +import type { GamePlayers } from "@client/scenes/game/application/game.types"; + +/** PlayerSyncHandler の初期化入力 */ +export type PlayerSyncHandlerOptions = { + worldContainer: Container; + players: GamePlayers; + myId: string; + appearanceResolver: AppearanceResolver; +}; + +/** プレイヤー関連の同期イベント適用を担当する */ +export class PlayerSyncHandler { + private readonly worldContainer: Container; + private readonly players: GamePlayers; + private readonly myId: string; + private readonly appearanceResolver: AppearanceResolver; + + constructor({ worldContainer, players, myId, appearanceResolver }: PlayerSyncHandlerOptions) { + this.worldContainer = worldContainer; + this.players = players; + this.myId = myId; + this.appearanceResolver = appearanceResolver; + } + + /** 初期プレイヤー一覧を生成して反映する */ + public handleCurrentPlayers = (serverPlayers: CurrentPlayersPayload): void => { + serverPlayers.forEach((player) => { + const playerController = player.id === this.myId + ? new LocalPlayerController(player, this.appearanceResolver) + : new RemotePlayerController(player, this.appearanceResolver); + this.worldContainer.addChild(playerController.getDisplayObject()); + this.players[player.id] = playerController; + }); + }; + + /** 新規参加プレイヤーを生成して反映する */ + public handleNewPlayer = (payload: NewPlayerPayload): void => { + const playerController = new RemotePlayerController(payload, this.appearanceResolver); + this.worldContainer.addChild(playerController.getDisplayObject()); + this.players[payload.id] = playerController; + }; + + /** プレイヤー差分更新を反映する */ + public handlePlayerUpdates = (changedPlayers: UpdatePlayersPayload): void => { + changedPlayers.forEach((playerData) => { + const target = this.players[playerData.id]; + if (target && target instanceof RemotePlayerController) { + target.applyRemoteUpdate({ x: playerData.x, y: playerData.y }); + } + }); + }; + + /** 退出プレイヤーを削除する */ + public handleRemovePlayer = (id: RemovePlayerPayload): void => { + const target = this.players[id]; + if (!target) { + return; + } + + this.worldContainer.removeChild(target.getDisplayObject()); + target.destroy(); + delete this.players[id]; + }; +} \ No newline at end of file