diff --git a/apps/client/src/scenes/game/GameManager.ts b/apps/client/src/scenes/game/GameManager.ts index 7e3b51a..6bff087 100644 --- a/apps/client/src/scenes/game/GameManager.ts +++ b/apps/client/src/scenes/game/GameManager.ts @@ -220,6 +220,7 @@ startCountdownSec: this.sessionFacade.getStartCountdownSec(), isInputEnabled: this.runtime.isInputEnabled(), teamPaintRates: this.runtime.getPaintRatesByTeam(), + miniMapTeamIds: this.runtime.getMiniMapTeamIds(), localBombHitCount: this.localBombHitCount, localPlayerPosition: this.runtime.getLocalPlayerPosition(), }; diff --git a/apps/client/src/scenes/game/GameScene.tsx b/apps/client/src/scenes/game/GameScene.tsx index ae6640e..8a079fc 100644 --- a/apps/client/src/scenes/game/GameScene.tsx +++ b/apps/client/src/scenes/game/GameScene.tsx @@ -19,6 +19,7 @@ startCountdownText, isInputEnabled, teamPaintRates, + miniMapTeamIds, localBombHitCount, localPlayerPosition, handleInput, @@ -31,6 +32,7 @@ startCountdownText={startCountdownText} isInputEnabled={isInputEnabled} teamPaintRates={teamPaintRates} + miniMapTeamIds={miniMapTeamIds} localBombHitCount={localBombHitCount} localPlayerPosition={localPlayerPosition} pixiContainerRef={pixiContainerRef} diff --git a/apps/client/src/scenes/game/GameView.tsx b/apps/client/src/scenes/game/GameView.tsx index dbd6967..e17cc44 100644 --- a/apps/client/src/scenes/game/GameView.tsx +++ b/apps/client/src/scenes/game/GameView.tsx @@ -27,6 +27,7 @@ startCountdownText: string | null; isInputEnabled: boolean; teamPaintRates: number[]; + miniMapTeamIds: number[]; localBombHitCount: number; localPlayerPosition: { x: number; y: number } | null; pixiContainerRef: React.RefObject; @@ -102,6 +103,7 @@ startCountdownText, isInputEnabled, teamPaintRates, + miniMapTeamIds, localBombHitCount, localPlayerPosition, pixiContainerRef, @@ -120,11 +122,14 @@
HP: {heartGauge}
+ -
{remainingSeconds === 60 && ( diff --git a/apps/client/src/scenes/game/application/runtime/GameSceneRuntime.ts b/apps/client/src/scenes/game/application/runtime/GameSceneRuntime.ts index c145d5b..89f0c5f 100644 --- a/apps/client/src/scenes/game/application/runtime/GameSceneRuntime.ts +++ b/apps/client/src/scenes/game/application/runtime/GameSceneRuntime.ts @@ -193,6 +193,16 @@ return this.gameMap.getPaintRatesByTeam(); } + /** ミニマップ描画用の全セルteamId配列を返す */ + public getMiniMapTeamIds(): number[] { + if (!this.gameMap) { + const totalCells = config.GAME_CONFIG.GRID_COLS * config.GAME_CONFIG.GRID_ROWS; + return new Array(totalCells).fill(-1); + } + + return this.gameMap.getAllCellTeamIds(); + } + /** ローカルプレイヤーの現在座標を返す */ public getLocalPlayerPosition(): { x: number; y: number } | null { const localPlayer = this.playerRepository.getById(this.myId); @@ -202,8 +212,8 @@ const position = localPlayer.getPosition(); return { - x: Math.round(position.x * 10) / 10, - y: Math.round(position.y * 10) / 10, + x: position.x, + y: position.y, }; } diff --git a/apps/client/src/scenes/game/application/ui/GameUiStateSyncService.ts b/apps/client/src/scenes/game/application/ui/GameUiStateSyncService.ts index e131a4c..16065fd 100644 --- a/apps/client/src/scenes/game/application/ui/GameUiStateSyncService.ts +++ b/apps/client/src/scenes/game/application/ui/GameUiStateSyncService.ts @@ -16,10 +16,25 @@ startCountdownSec: number; isInputEnabled: boolean; teamPaintRates: number[]; + miniMapTeamIds: number[]; localBombHitCount: number; localPlayerPosition: { x: number; y: number } | null; }; +const isSameMiniMapTeamIds = (a: number[], b: number[]): boolean => { + if (a.length !== b.length) { + return false; + } + + for (let index = 0; index < a.length; index += 1) { + if (a[index] !== b[index]) { + return false; + } + } + + return true; +}; + const isSameLocalPlayerPosition = ( a: { x: number; y: number } | null, b: { x: number; y: number } | null, @@ -91,6 +106,10 @@ this.lastState.isInputEnabled === snapshot.isInputEnabled && this.lastState.localBombHitCount === snapshot.localBombHitCount && isSamePaintRates(this.lastState.teamPaintRates, snapshot.teamPaintRates) && + isSameMiniMapTeamIds( + this.lastState.miniMapTeamIds, + snapshot.miniMapTeamIds, + ) && isSameLocalPlayerPosition( this.lastState.localPlayerPosition, snapshot.localPlayerPosition, diff --git a/apps/client/src/scenes/game/entities/map/GameMapController.ts b/apps/client/src/scenes/game/entities/map/GameMapController.ts index ff0f999..5d6733d 100644 --- a/apps/client/src/scenes/game/entities/map/GameMapController.ts +++ b/apps/client/src/scenes/game/entities/map/GameMapController.ts @@ -56,6 +56,11 @@ return this.model.getPaintRatesByTeam(config.GAME_CONFIG.TEAM_COUNT); } + /** 現在の全セルteamId配列を取得する */ + public getAllCellTeamIds(): number[] { + return this.model.getAllTeamIds(); + } + /** すべてのセルteamIdを描画色へ変換する */ private resolveAllCellColors(teamIds: number[]): Array { return teamIds.map((teamId) => this.resolveCellColor(teamId)); diff --git a/apps/client/src/scenes/game/hooks/useGameSceneController.ts b/apps/client/src/scenes/game/hooks/useGameSceneController.ts index 55b3cb1..f253f00 100644 --- a/apps/client/src/scenes/game/hooks/useGameSceneController.ts +++ b/apps/client/src/scenes/game/hooks/useGameSceneController.ts @@ -15,6 +15,9 @@ const DEFAULT_TEAM_PAINT_RATES = new Array( config.GAME_CONFIG.TEAM_COUNT, ).fill(0); +const DEFAULT_MINIMAP_TEAM_IDS = new Array( + config.GAME_CONFIG.GRID_COLS * config.GAME_CONFIG.GRID_ROWS, +).fill(-1); /** ゲーム画面の状態と入力ハンドラを提供するフック */ export const useGameSceneController = (myId: string | null) => { @@ -28,6 +31,9 @@ const [teamPaintRates, setTeamPaintRates] = useState( DEFAULT_TEAM_PAINT_RATES, ); + const [miniMapTeamIds, setMiniMapTeamIds] = useState( + DEFAULT_MINIMAP_TEAM_IDS, + ); const [localBombHitCount, setLocalBombHitCount] = useState(0); const [localPlayerPosition, setLocalPlayerPosition] = useState<{ x: number; @@ -56,6 +62,7 @@ ); setTeamPaintRates(state.teamPaintRates); + setMiniMapTeamIds(state.miniMapTeamIds); setLocalBombHitCount(state.localBombHitCount); setLocalPlayerPosition(state.localPlayerPosition); }); @@ -67,6 +74,7 @@ setStartCountdownText(null); setIsInputEnabled(false); setTeamPaintRates(DEFAULT_TEAM_PAINT_RATES); + setMiniMapTeamIds(DEFAULT_MINIMAP_TEAM_IDS); setLocalBombHitCount(0); setLocalPlayerPosition(null); }; @@ -86,6 +94,7 @@ startCountdownText, isInputEnabled, teamPaintRates, + miniMapTeamIds, localBombHitCount, localPlayerPosition, handleInput, diff --git a/apps/client/src/scenes/game/input/minimap/presentation/MiniMapPanel.styles.ts b/apps/client/src/scenes/game/input/minimap/presentation/MiniMapPanel.styles.ts index f73e59b..c97aa89 100644 --- a/apps/client/src/scenes/game/input/minimap/presentation/MiniMapPanel.styles.ts +++ b/apps/client/src/scenes/game/input/minimap/presentation/MiniMapPanel.styles.ts @@ -45,6 +45,15 @@ overflow: "hidden", }; +/** ミニマップ描画キャンバスのスタイル */ +export const MINIMAP_CANVAS_STYLE: CSSProperties = { + position: "absolute", + inset: 0, + width: "100%", + height: "100%", + imageRendering: "pixelated", +}; + /** ミニマップ内の現在地ドットスタイルを返す */ export const buildMiniMapDotStyle = ( leftPx: number, diff --git a/apps/client/src/scenes/game/input/minimap/presentation/MiniMapPanel.tsx b/apps/client/src/scenes/game/input/minimap/presentation/MiniMapPanel.tsx index feaec53..174ad61 100644 --- a/apps/client/src/scenes/game/input/minimap/presentation/MiniMapPanel.tsx +++ b/apps/client/src/scenes/game/input/minimap/presentation/MiniMapPanel.tsx @@ -3,10 +3,11 @@ * ミニマップの開閉操作と表示を担うプレゼンテーションコンポーネント * 全体マップ枠とローカルプレイヤー現在地のみを描画する */ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { config } from "@client/config"; import { buildMiniMapDotStyle, + MINIMAP_CANVAS_STYLE, buildMiniMapToggleButtonStyle, MINIMAP_FRAME_STYLE, MINIMAP_PANEL_ROOT_STYLE, @@ -16,6 +17,7 @@ /** ミニマップの入力プロパティ */ export type MiniMapPanelProps = { + miniMapTeamIds: number[]; localPlayerPosition: { x: number; y: number } | null; }; @@ -24,16 +26,56 @@ }; /** 全体マップ上のローカル位置を示すミニマップを描画する */ -export const MiniMapPanel = ({ localPlayerPosition }: MiniMapPanelProps) => { +export const MiniMapPanel = ({ + miniMapTeamIds, + localPlayerPosition, +}: MiniMapPanelProps) => { const [isOpen, setIsOpen] = useState(false); + const canvasRef = useRef(null); + + useEffect(() => { + if (!isOpen) { + return; + } + + const canvas = canvasRef.current; + if (!canvas) { + return; + } + + const context = canvas.getContext("2d"); + if (!context) { + return; + } + + const cols = config.GAME_CONFIG.GRID_COLS; + const rows = config.GAME_CONFIG.GRID_ROWS; + const totalCells = cols * rows; + const cellWidth = MINIMAP_FRAME_SIZE_PX / cols; + const cellHeight = MINIMAP_FRAME_SIZE_PX / rows; + + context.clearRect(0, 0, MINIMAP_FRAME_SIZE_PX, MINIMAP_FRAME_SIZE_PX); + + for (let index = 0; index < totalCells; index += 1) { + const teamId = miniMapTeamIds[index] ?? -1; + if (teamId < 0 || teamId >= config.GAME_CONFIG.TEAM_COLORS.length) { + continue; + } + + const col = index % cols; + const row = Math.floor(index / cols); + context.fillStyle = config.GAME_CONFIG.TEAM_COLORS[teamId] ?? "#000000"; + context.fillRect(col * cellWidth, row * cellHeight, cellWidth, cellHeight); + } + }, [isOpen, miniMapTeamIds]); const markerPosition = useMemo(() => { if (!localPlayerPosition) { return null; } - const width = config.GAME_CONFIG.MAP_WIDTH_PX; - const height = config.GAME_CONFIG.MAP_HEIGHT_PX; + const width = config.GAME_CONFIG.GRID_COLS; + const height = config.GAME_CONFIG.GRID_ROWS; if (width <= 0 || height <= 0) { return null; } @@ -63,6 +105,12 @@ {isOpen && (
+ {markerPosition && (