diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts index a0c8987..796697c 100644 --- a/apps/client/src/scenes/game/GameManager.ts +++ b/apps/client/src/scenes/game/GameManager.ts @@ -3,7 +3,7 @@ import { config } from "@repo/shared"; import type { playerTypes } from "@repo/shared"; import { LocalPlayerController, RemotePlayerController } from "./entities/player/PlayerController"; -import { GameMap } from "./entities/map/GameMap"; +import { GameMapController } from "./entities/map/GameMapController"; export class GameManager { private app: Application; @@ -11,7 +11,7 @@ private players: Record = {}; private myId: string; private container: HTMLDivElement; - private gameMap!: GameMap; + private gameMap!: GameMapController; private gameStartTime: number | null = null; // サーバーからゲーム開始通知(と開始時刻)を受け取った時に呼ぶ @@ -60,9 +60,9 @@ this.container.appendChild(this.app.canvas); // 背景マップの配置 - const gameMap = new GameMap(); + const gameMap = new GameMapController(); this.gameMap = gameMap; - this.worldContainer.addChild(gameMap); + this.worldContainer.addChild(gameMap.getDisplayObject()); this.app.stage.addChild(this.worldContainer); // ネットワークイベントの登録 diff --git a/apps/client/src/scenes/game/entities/map/GameMap.ts b/apps/client/src/scenes/game/entities/map/GameMap.ts deleted file mode 100644 index 95cb999..0000000 --- a/apps/client/src/scenes/game/entities/map/GameMap.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Container, Graphics } from "pixi.js"; -import { config } from "@repo/shared"; -import type { gridMapTypes } from "@repo/shared"; - -// 親クラスを Graphics から Container に変更し、レイヤー管理を可能にする -export class GameMap extends Container { - private bgGraphics: Graphics; - private gridGraphics: Graphics; - - // 400個(GRID_COLS * GRID_ROWS)のマス目描画用オブジェクトを保持する1次元配列 - private cells: Graphics[] = []; - - constructor() { - super(); - this.bgGraphics = new Graphics(); - this.gridGraphics = new Graphics(); - - // 描画順(追加した順に前面に描画される): 背景 -> マス目 -> グリッド線 - this.addChild(this.bgGraphics); - - // マス目の初期化と Container への追加 - this.initCells(); - - this.addChild(this.gridGraphics); - - // 背景と線の描画(静的なので1回だけ実行) - this.drawBaseMap(); - } - - // 設定値に基づき、空のマス目(Graphics)を400個生成して配列に格納する - private initCells() { - const { GRID_COLS, GRID_ROWS, GRID_CELL_SIZE } = config.GAME_CONFIG; - const totalCells = GRID_COLS * GRID_ROWS; - - for (let i = 0; i < totalCells; i++) { - const col = i % GRID_COLS; - const row = Math.floor(i / GRID_COLS); - - const cell = new Graphics(); - // マスの座標をあらかじめ設定しておく(描画の基準点になる) - cell.x = col * GRID_CELL_SIZE; - cell.y = row * GRID_CELL_SIZE; - - this.addChild(cell); - this.cells.push(cell); - } - } - - // 設定値参照によるマップ外観(背景・グリッド線)の組み立て処理 - private drawBaseMap() { - const { - MAP_WIDTH_PX, MAP_HEIGHT_PX, GRID_CELL_SIZE, - MAP_BG_COLOR, MAP_GRID_COLOR, MAP_BORDER_COLOR - } = config.GAME_CONFIG; - - // マップ全域背景レイヤー - this.bgGraphics.rect(0, 0, MAP_WIDTH_PX, MAP_HEIGHT_PX).fill(MAP_BG_COLOR); - - // 縦方向グリッド線 - for (let x = 0; x <= MAP_WIDTH_PX; x += GRID_CELL_SIZE) { - this.gridGraphics.moveTo(x, 0).lineTo(x, MAP_HEIGHT_PX).stroke({ width: 1, color: MAP_GRID_COLOR }); - } - // 横方向グリッド線 - for (let y = 0; y <= MAP_HEIGHT_PX; y += GRID_CELL_SIZE) { - this.gridGraphics.moveTo(0, y).lineTo(MAP_WIDTH_PX, y).stroke({ width: 1, color: MAP_GRID_COLOR }); - } - - // プレイ領域外枠 - this.gridGraphics.rect(0, 0, MAP_WIDTH_PX, MAP_HEIGHT_PX).stroke({ width: 5, color: MAP_BORDER_COLOR }); - } - - /** - * サーバー(またはテストロジック)から受け取った最新のマップ状態で色を更新する - */ - public updateMapState(state: gridMapTypes.MapState) { - const { GRID_CELL_SIZE, TEAM_COLORS } = config.GAME_CONFIG; - - for (let i = 0; i < state.gridColors.length; i++) { - const teamId = state.gridColors[i]; - const cell = this.cells[i]; - - // 一旦マスの描画をクリア - cell.clear(); - - // 塗布済み(-1以外)の場合のみ色を塗る - if (teamId !== -1) { - // 他の描画処理と同様に,文字列のカラーコードをPixiJS用の数値に変換 - const colorString = TEAM_COLORS[teamId] || '#FFFFFF'; - const hexColor = parseInt(colorString.replace("#", "0x"), 16); - - // cell.x, cell.y は設定済みなので、ローカル座標 (0,0) からサイズ分を塗りつぶす - cell.rect(0, 0, GRID_CELL_SIZE, GRID_CELL_SIZE).fill(hexColor); - } - } - } - - /** - * 差分データを受け取って指定のマスだけ色を更新する - */ - public updateCells(updates: gridMapTypes.CellUpdate[]) { - const { GRID_CELL_SIZE, TEAM_COLORS } = config.GAME_CONFIG; - - updates.forEach(({ index, teamId }) => { - const cell = this.cells[index]; - if (!cell) return; - - cell.clear(); - - if (teamId !== -1) { - const colorString = TEAM_COLORS[teamId] || '#FFFFFF'; - const hexColor = parseInt(colorString.replace("#", "0x"), 16); - cell.rect(0, 0, GRID_CELL_SIZE, GRID_CELL_SIZE).fill(hexColor); - } - }); - } -} \ No newline at end of file diff --git a/apps/client/src/scenes/game/entities/map/GameMapController.ts b/apps/client/src/scenes/game/entities/map/GameMapController.ts new file mode 100644 index 0000000..41ca4ef --- /dev/null +++ b/apps/client/src/scenes/game/entities/map/GameMapController.ts @@ -0,0 +1,49 @@ +/** + * GameMapController + * 外部からのマップ更新入力をModelとViewへ仲介するコントローラー + * 全体更新と差分更新を統一的に扱い,描画同期を提供する + */ +import type { gridMapTypes } from '@repo/shared'; +import type { Container } from 'pixi.js'; +import { GameMapModel } from './GameMapModel'; +import { GameMapView } from './GameMapView'; + +/** マップ更新の仲介責務を担うコントローラー */ +export class GameMapController { + private readonly model: GameMapModel; + private readonly view: GameMapView; + + /** モデルとビューを生成し初期同期する */ + constructor() { + this.model = new GameMapModel(); + this.view = new GameMapView(); + this.view.renderAll(this.model.getAllTeamIds()); + } + + /** 描画オブジェクトを取得する */ + public getDisplayObject(): Container { + return this.view.displayObject; + } + + /** 全体マップ状態を反映する */ + public updateMapState(state: gridMapTypes.MapState): void { + this.model.applyMapState(state); + this.view.renderAll(this.model.getAllTeamIds()); + } + + /** 差分セル更新を反映する */ + public updateCells(updates: gridMapTypes.CellUpdate[]): void { + this.model.applyUpdates(updates); + + updates.forEach(({ index }) => { + const teamId = this.model.getTeamId(index); + if (teamId === undefined) return; + this.view.renderCell(index, teamId); + }); + } + + /** 管理中の描画リソースを破棄する */ + public destroy(): void { + this.view.destroy(); + } +} \ No newline at end of file diff --git a/apps/client/src/scenes/game/entities/map/GameMapModel.ts b/apps/client/src/scenes/game/entities/map/GameMapModel.ts new file mode 100644 index 0000000..305f14d --- /dev/null +++ b/apps/client/src/scenes/game/entities/map/GameMapModel.ts @@ -0,0 +1,50 @@ +/** + * GameMapModel + * マップセルの色状態を管理する計算モデル + * 全体更新と差分更新を適用して描画入力用の状態を保持する + */ +import { config } from '@repo/shared'; +import type { gridMapTypes } from '@repo/shared'; + +/** マップセル状態の計算責務を担うモデル */ +export class GameMapModel { + private readonly cellTeamIds: number[]; + + /** 設定値に基づいて初期セル状態を構築する */ + constructor() { + const { GRID_COLS, GRID_ROWS } = config.GAME_CONFIG; + this.cellTeamIds = new Array(GRID_COLS * GRID_ROWS).fill(-1); + } + + /** 全体マップ状態を適用する */ + public applyMapState(state: gridMapTypes.MapState): void { + const maxLength = Math.min(this.cellTeamIds.length, state.gridColors.length); + for (let index = 0; index < maxLength; index++) { + this.cellTeamIds[index] = state.gridColors[index]; + } + } + + /** 差分セル更新を適用する */ + public applyUpdates(updates: gridMapTypes.CellUpdate[]): void { + updates.forEach(({ index, teamId }) => { + if (!this.isValidIndex(index)) return; + this.cellTeamIds[index] = teamId; + }); + } + + /** 指定セルのチームIDを取得する */ + public getTeamId(index: number): number | undefined { + if (!this.isValidIndex(index)) return undefined; + return this.cellTeamIds[index]; + } + + /** 描画用の全セル状態を取得する */ + public getAllTeamIds(): number[] { + return [...this.cellTeamIds]; + } + + /** セル添字が有効範囲内かを判定する */ + private isValidIndex(index: number): boolean { + return Number.isInteger(index) && index >= 0 && index < this.cellTeamIds.length; + } +} \ No newline at end of file diff --git a/apps/client/src/scenes/game/entities/map/GameMapView.ts b/apps/client/src/scenes/game/entities/map/GameMapView.ts new file mode 100644 index 0000000..5c5185b --- /dev/null +++ b/apps/client/src/scenes/game/entities/map/GameMapView.ts @@ -0,0 +1,105 @@ +/** + * GameMapView + * マップ背景,グリッド線,セル塗りの描画を担うビュー + * 計算済みセル状態を受けてPixi描画へ反映する + */ +import { Container, Graphics } from 'pixi.js'; +import { config } from '@repo/shared'; + +/** マップ描画責務を担うビュー */ +export class GameMapView { + public readonly displayObject: Container; + + private readonly bgGraphics: Graphics; + private readonly gridGraphics: Graphics; + private readonly cells: Graphics[] = []; + + /** 背景,グリッド線,セル描画オブジェクトを初期化する */ + constructor() { + this.displayObject = new Container(); + this.bgGraphics = new Graphics(); + this.gridGraphics = new Graphics(); + + this.displayObject.addChild(this.bgGraphics); + this.initCells(); + this.displayObject.addChild(this.gridGraphics); + this.drawBaseMap(); + } + + /** 全セル状態をまとめて描画へ反映する */ + public renderAll(teamIds: number[]): void { + const maxLength = Math.min(this.cells.length, teamIds.length); + for (let index = 0; index < maxLength; index++) { + this.renderCell(index, teamIds[index]); + } + } + + /** 指定セルの状態を描画へ反映する */ + public renderCell(index: number, teamId: number): void { + const cell = this.cells[index]; + if (!cell) return; + + const { GRID_CELL_SIZE } = config.GAME_CONFIG; + + // 対象セルをクリアしてから必要に応じて再塗布する + cell.clear(); + if (teamId === -1) return; + + const hexColor = this.toHexColor(teamId); + cell.rect(0, 0, GRID_CELL_SIZE, GRID_CELL_SIZE).fill(hexColor); + } + + /** 描画リソースを破棄する */ + public destroy(): void { + this.displayObject.destroy({ children: true }); + } + + /** 設定値に基づいてセル描画オブジェクトを初期化する */ + private initCells(): void { + const { GRID_COLS, GRID_ROWS, GRID_CELL_SIZE } = config.GAME_CONFIG; + const totalCells = GRID_COLS * GRID_ROWS; + + for (let index = 0; index < totalCells; index++) { + const col = index % GRID_COLS; + const row = Math.floor(index / GRID_COLS); + + const cell = new Graphics(); + cell.x = col * GRID_CELL_SIZE; + cell.y = row * GRID_CELL_SIZE; + + this.displayObject.addChild(cell); + this.cells.push(cell); + } + } + + /** マップ背景とグリッド線を描画する */ + private drawBaseMap(): void { + const { + MAP_WIDTH_PX, + MAP_HEIGHT_PX, + GRID_CELL_SIZE, + MAP_BG_COLOR, + MAP_GRID_COLOR, + MAP_BORDER_COLOR, + } = config.GAME_CONFIG; + + this.bgGraphics.rect(0, 0, MAP_WIDTH_PX, MAP_HEIGHT_PX).fill(MAP_BG_COLOR); + + for (let x = 0; x <= MAP_WIDTH_PX; x += GRID_CELL_SIZE) { + this.gridGraphics.moveTo(x, 0).lineTo(x, MAP_HEIGHT_PX).stroke({ width: 1, color: MAP_GRID_COLOR }); + } + + for (let y = 0; y <= MAP_HEIGHT_PX; y += GRID_CELL_SIZE) { + this.gridGraphics.moveTo(0, y).lineTo(MAP_WIDTH_PX, y).stroke({ width: 1, color: MAP_GRID_COLOR }); + } + + this.gridGraphics.rect(0, 0, MAP_WIDTH_PX, MAP_HEIGHT_PX).stroke({ width: 5, color: MAP_BORDER_COLOR }); + } + + /** チームIDから塗り色の16進数カラー値を取得する */ + private toHexColor(teamId: number): number { + const { TEAM_COLORS } = config.GAME_CONFIG; + const colorString = TEAM_COLORS[teamId] || '#FFFFFF'; + return parseInt(colorString.replace('#', '0x'), 16); + } +} \ No newline at end of file