diff --git a/apps/client/src/entities/GameMap.ts b/apps/client/src/entities/GameMap.ts index 79577ff..90c6cbe 100644 --- a/apps/client/src/entities/GameMap.ts +++ b/apps/client/src/entities/GameMap.ts @@ -1,30 +1,117 @@ -import { Graphics } from "pixi.js"; +// apps/client/src/game/map/GameMap.ts (パスは適宜読み替えてください) +import { Container, Graphics } from "pixi.js"; import { GAME_CONFIG } from "@repo/shared/src/config/gameConfig"; +import type { MapState, CellUpdate } from "@repo/shared/src/types/map"; -// マップ背景・グリッド・外枠の一括描画オブジェクト -export class GameMap extends Graphics { +// 親クラスを 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.drawMap(); + this.bgGraphics = new Graphics(); + this.gridGraphics = new Graphics(); + + // 描画順(追加した順に前面に描画される): 背景 -> マス目 -> グリッド線 + this.addChild(this.bgGraphics); + + // マス目の初期化と Container への追加 + this.initCells(); + + this.addChild(this.gridGraphics); + + // 背景と線の描画(静的なので1回だけ実行) + this.drawBaseMap(); } - // 設定値参照によるマップ外観組み立て処理 - private drawMap() { - const { MAP_WIDTH, MAP_HEIGHT } = GAME_CONFIG; + // 設定値に基づき、空のマス目(Graphics)を400個生成して配列に格納する + private initCells() { + const { GRID_COLS, GRID_ROWS, GRID_CELL_SIZE } = 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, MAP_HEIGHT, GRID_CELL_SIZE, + MAP_BG_COLOR, MAP_GRID_COLOR, MAP_BORDER_COLOR + } = GAME_CONFIG; // マップ全域背景レイヤー - this.rect(0, 0, MAP_WIDTH, MAP_HEIGHT).fill(0x111111); + this.bgGraphics.rect(0, 0, MAP_WIDTH, MAP_HEIGHT).fill(MAP_BG_COLOR); - // 100px 間隔縦方向グリッド線 - for (let x = 0; x <= MAP_WIDTH; x += 100) { - this.moveTo(x, 0).lineTo(x, MAP_HEIGHT).stroke({ width: 1, color: 0x333333 }); + // 縦方向グリッド線 + for (let x = 0; x <= MAP_WIDTH; x += GRID_CELL_SIZE) { + this.gridGraphics.moveTo(x, 0).lineTo(x, MAP_HEIGHT).stroke({ width: 1, color: MAP_GRID_COLOR }); } - // 100px 間隔横方向グリッド線 - for (let y = 0; y <= MAP_HEIGHT; y += 100) { - this.moveTo(0, y).lineTo(MAP_WIDTH, y).stroke({ width: 1, color: 0x333333 }); + // 横方向グリッド線 + for (let y = 0; y <= MAP_HEIGHT; y += GRID_CELL_SIZE) { + this.gridGraphics.moveTo(0, y).lineTo(MAP_WIDTH, y).stroke({ width: 1, color: MAP_GRID_COLOR }); } // プレイ領域外枠 - this.rect(0, 0, MAP_WIDTH, MAP_HEIGHT).stroke({ width: 5, color: 0xff4444 }); + this.gridGraphics.rect(0, 0, MAP_WIDTH, MAP_HEIGHT).stroke({ width: 5, color: MAP_BORDER_COLOR }); + } + + /** + * サーバー(またはテストロジック)から受け取った最新のマップ状態で色を更新する + */ + public updateMapState(state: MapState) { + const { GRID_CELL_SIZE, TEAM_COLORS } = 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) { + // Player.ts と同様に、文字列のカラーコードを 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: CellUpdate[]) { + const { GRID_CELL_SIZE, TEAM_COLORS } = 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/managers/GameManager.ts b/apps/client/src/managers/GameManager.ts index 0611b10..e62c924 100644 --- a/apps/client/src/managers/GameManager.ts +++ b/apps/client/src/managers/GameManager.ts @@ -12,6 +12,7 @@ private players: Record = {}; private myId: string; private container: HTMLDivElement; + private gameMap!: GameMap; // 入力と状態管理 private joystickInput = { x: 0, y: 0 }; @@ -44,6 +45,7 @@ // 背景マップの配置 const gameMap = new GameMap(); + this.gameMap = gameMap; this.worldContainer.addChild(gameMap); this.app.stage.addChild(this.worldContainer); @@ -100,6 +102,10 @@ delete this.players[id]; } }); + + socketClient.onUpdateMapCells((updates) => { + this.gameMap.updateCells(updates); + }); } /** @@ -149,9 +155,6 @@ this.players = {}; // イベント購読の解除 - socketClient.socket.off("current_players"); - socketClient.socket.off("new_player"); - socketClient.socket.off("update_player"); - socketClient.socket.off("remove_player"); + socketClient.removeAllListeners(); } } \ No newline at end of file diff --git a/apps/client/src/network/SocketClient.ts b/apps/client/src/network/SocketClient.ts index bdd00cd..8112676 100644 --- a/apps/client/src/network/SocketClient.ts +++ b/apps/client/src/network/SocketClient.ts @@ -1,6 +1,7 @@ import { io, Socket } from "socket.io-client"; import { SocketEvents } from "@repo/shared/src/protocol/events"; import type { PlayerData } from "@repo/shared/src/types/player"; +import type { CellUpdate } from "@repo/shared/src/types/map"; /** * サーバー WebSocket 通信管理クラス @@ -67,6 +68,13 @@ } /** + * マス目の差分更新イベント購読 + */ + onUpdateMapCells(callback: (updates: CellUpdate[]) => void) { + this.socket.on(SocketEvents.UPDATE_MAP_CELLS, callback); + } + + /** * ルーム入室リクエスト送信 * @param roomId 入室先のID * @param playerName 表示名 @@ -102,6 +110,17 @@ readyForGame() { this.socket.emit(SocketEvents.READY_FOR_GAME); } + + /** + * 全てのゲームプレイ関連イベントを解除(一括解除用) + */ + removeAllListeners() { + this.socket.off(SocketEvents.CURRENT_PLAYERS); + this.socket.off(SocketEvents.NEW_PLAYER); + this.socket.off(SocketEvents.UPDATE_PLAYER); + this.socket.off(SocketEvents.REMOVE_PLAYER); + this.socket.off(SocketEvents.UPDATE_MAP_CELLS); + } } // シングルトン利用向け共有インスタンス diff --git "a/docs/01_Env/ENV_01_\347\222\260\345\242\203\346\247\213\347\257\211\343\203\273\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.txt" "b/docs/01_Env/ENV_01_\347\222\260\345\242\203\346\247\213\347\257\211\343\203\273\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.txt" index 2521b83..66d1fc0 100644 --- "a/docs/01_Env/ENV_01_\347\222\260\345\242\203\346\247\213\347\257\211\343\203\273\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.txt" +++ "b/docs/01_Env/ENV_01_\347\222\260\345\242\203\346\247\213\347\257\211\343\203\273\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.txt" @@ -56,7 +56,8 @@ │ └── server/ # 【権限】バックエンド (Node.js) │ ├── src/ │ │ ├── entities/ # サーバー側エンティティ (Player) - │ │ ├── managers/ # 管理クラス (GameManager) + │ │ ├── handlers/ # 処理ハンドラ (GameHandler, RoomHandler) + │ │ ├── managers/ # 管理クラス (GameManager, RoomManager) │ │ ├── network/ # WebSocket処理 (SocketManager) │ │ └── index.ts # エントリーポイント │ ├── tsconfig.json @@ -67,7 +68,7 @@ ├── src/ │ ├── config/ # 共通定数(gameConfig.ts) │ ├── protocol/ # 通信イベント定義(events.ts) - │ ├── types/ # 型定義(payloads.ts, player.ts, room.ts) + │ ├── types/ # 型定義(map.ts, payloads.ts, player.ts, room.ts) │ └── index.ts # エントリーポイント (tsupビルド対象) ├── tsup.config.ts # ビルド設定 └── package.json diff --git a/packages/shared/src/config/gameConfig.ts b/packages/shared/src/config/gameConfig.ts index 8193228..b2fbf6f 100644 --- a/packages/shared/src/config/gameConfig.ts +++ b/packages/shared/src/config/gameConfig.ts @@ -3,9 +3,14 @@ SCREEN_WIDTH: 1280, SCREEN_HEIGHT: 720, - // マップサイズ設定 - MAP_WIDTH: 2000, - MAP_HEIGHT: 2000, + // グリッド(マス)設定を新設 + GRID_CELL_SIZE: 100, // 1マスのサイズ(px) + GRID_COLS: 20, // 横のマス数 + GRID_ROWS: 20, // 縦のマス数 + + // マップサイズはグリッド設定から自動計算させる(ハードコーディングを避ける) + get MAP_WIDTH() { return this.GRID_COLS * this.GRID_CELL_SIZE; }, + get MAP_HEIGHT() { return this.GRID_ROWS * this.GRID_CELL_SIZE; }, // ルーム収容人数設定 MAX_PLAYERS_PER_ROOM: 4, @@ -21,5 +26,10 @@ // チームカラー設定 // teamId インデックス順カラー配列 - TEAM_COLORS: ['#FF4B4B', '#4B4BFF', '#4BFF4B', '#FFD700'], + TEAM_COLORS: ['#FF4B4B', '#4B4BFF', '#4BFF4B', '#FFD700'], + + // マップ描画用のカラー設定 + MAP_BG_COLOR: 0x111111, // 何も塗っていないマス(背景)の色 + MAP_GRID_COLOR: 0x333333, // グリッド線の色 + MAP_BORDER_COLOR: 0xff4444, // プレイ領域外枠の色 } as const; \ No newline at end of file diff --git a/packages/shared/src/logic/gridMap.ts b/packages/shared/src/logic/gridMap.ts new file mode 100644 index 0000000..10dd3b5 --- /dev/null +++ b/packages/shared/src/logic/gridMap.ts @@ -0,0 +1,20 @@ +import { GAME_CONFIG } from "../config/gameConfig"; + +/** + * ピクセル座標からグリッドの1次元配列インデックスを取得する(中心点判定) + */ +export function getGridIndexFromPosition(x: number, y: number): number | null { + const { GRID_CELL_SIZE, GRID_COLS, GRID_ROWS } = GAME_CONFIG; + + // 座標がどのマス(列・行)に属するか計算 + const col = Math.floor(x / GRID_CELL_SIZE); + const row = Math.floor(y / GRID_CELL_SIZE); + + // マップ外の場合は null を返す + if (col < 0 || col >= GRID_COLS || row < 0 || row >= GRID_ROWS) { + return null; + } + + // 1次元配列のインデックスに変換 (row * 幅 + col) + return row * GRID_COLS + col; +} \ No newline at end of file diff --git a/packages/shared/src/protocol/events.ts b/packages/shared/src/protocol/events.ts index 17ebae2..a69f60f 100644 --- a/packages/shared/src/protocol/events.ts +++ b/packages/shared/src/protocol/events.ts @@ -15,5 +15,6 @@ NEW_PLAYER: "new_player", UPDATE_PLAYER: "update_player", REMOVE_PLAYER: "remove_player", - MOVE: "move" + MOVE: "move", + UPDATE_MAP_CELLS: "update_map_cells", } as const; diff --git a/packages/shared/src/types/map.ts b/packages/shared/src/types/map.ts new file mode 100644 index 0000000..d85090e --- /dev/null +++ b/packages/shared/src/types/map.ts @@ -0,0 +1,8 @@ +export interface MapState { + gridColors: number[]; +} + +export interface CellUpdate { + index: number; + teamId: number; +} \ No newline at end of file