Newer
Older
PixelPaintWar / apps / client / src / scenes / game / input / minimap / presentation / MiniMapPanel.tsx
/**
 * MiniMapPanel
 * ミニマップの開閉操作と表示を担うプレゼンテーションコンポーネント
 * 全体マップ枠とローカルプレイヤー現在地のみを描画する
 */
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,
} from "./MiniMapPanel.styles";

const MINIMAP_FRAME_SIZE_PX = 128;

/** ミニマップの入力プロパティ */
export type MiniMapPanelProps = {
  miniMapTeamIds: number[];
  localPlayerPosition: { x: number; y: number } | null;
};

const clamp = (value: number, min: number, max: number): number => {
  return Math.max(min, Math.min(value, max));
};

/** 全体マップ上のローカル位置を示すミニマップを描画する */
export const MiniMapPanel = ({
  miniMapTeamIds,
  localPlayerPosition,
}: MiniMapPanelProps) => {
  const [isOpen, setIsOpen] = useState(false);
  const canvasRef = useRef<HTMLCanvasElement | null>(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.GRID_COLS;
    const height = config.GAME_CONFIG.GRID_ROWS;
    if (width <= 0 || height <= 0) {
      return null;
    }

    const normalizedX = clamp(localPlayerPosition.x / width, 0, 1);
    const normalizedY = clamp(localPlayerPosition.y / height, 0, 1);

    return {
      leftPx: normalizedX * MINIMAP_FRAME_SIZE_PX,
      topPx: normalizedY * MINIMAP_FRAME_SIZE_PX,
    };
  }, [localPlayerPosition]);

  const buttonStyle = buildMiniMapToggleButtonStyle(isOpen);

  return (
    <div style={MINIMAP_PANEL_ROOT_STYLE}>
      <button
        type="button"
        style={buttonStyle}
        onClick={() => {
          setIsOpen((prev) => !prev);
        }}
      >
        {isOpen ? "閉じる" : "ミニマップ"}
      </button>

      {isOpen && (
        <div style={MINIMAP_FRAME_STYLE}>
          <canvas
            ref={canvasRef}
            width={MINIMAP_FRAME_SIZE_PX}
            height={MINIMAP_FRAME_SIZE_PX}
            style={MINIMAP_CANVAS_STYLE}
          />
          {markerPosition && (
            <div
              style={buildMiniMapDotStyle(
                markerPosition.leftPx,
                markerPosition.topPx,
              )}
            />
          )}
        </div>
      )}
    </div>
  );
};