Newer
Older
PixelPaintWar / apps / client / src / scenes / game / input / GameInputOverlay.tsx
/**
 * GameInputOverlay
 * ゲーム入力UIレイヤーを構成する
 * ジョイスティック層と爆弾ボタン層を分離して配置する
 */
import { useEffect, useMemo, useState } from "react";
import { config } from "@client/config";
import { JoystickInputPresenter } from "./joystick/JoystickInputPresenter";
import { BombButton } from "./bomb/BombButton";

/** 入力UIレイヤーの入力プロパティ */
type GameInputOverlayProps = {
  onJoystickInput: (x: number, y: number) => void;
  onPlaceBomb: () => boolean;
};

const UI_LAYER_STYLE: React.CSSProperties = {
  position: "absolute",
  zIndex: 20,
  width: "100%",
  height: "100%",
};

const COOLDOWN_TICK_MS = 50;

/** 入力UIレイヤーを描画する */
export const GameInputOverlay = ({
  onJoystickInput,
  onPlaceBomb,
}: GameInputOverlayProps) => {
  const bombCooldownMs = config.GAME_CONFIG.BOMB_COOLDOWN_MS;
  const [lastBombPressedAt, setLastBombPressedAt] = useState<number | null>(
    null,
  );
  const [nowMs, setNowMs] = useState(() => Date.now());

  useEffect(() => {
    if (lastBombPressedAt === null) {
      return;
    }

    const timerId = window.setInterval(() => {
      setNowMs(Date.now());
    }, COOLDOWN_TICK_MS);

    return () => {
      window.clearInterval(timerId);
    };
  }, [lastBombPressedAt]);

  const cooldownState = useMemo(() => {
    if (bombCooldownMs <= 0) {
      return {
        progress: 1,
        isReady: true,
        remainingSecText: null,
      };
    }

    if (lastBombPressedAt === null) {
      return {
        progress: 1,
        isReady: true,
        remainingSecText: null,
      };
    }

    const elapsed = nowMs - lastBombPressedAt;
    const clampedElapsed = Math.max(0, Math.min(elapsed, bombCooldownMs));
    const progress = clampedElapsed / bombCooldownMs;
    const remainingMs = Math.max(0, bombCooldownMs - clampedElapsed);
    const isReady = remainingMs === 0;

    return {
      progress,
      isReady,
      remainingSecText: isReady ? null : String(Math.ceil(remainingMs / 1000)),
    };
  }, [bombCooldownMs, lastBombPressedAt, nowMs]);

  const handlePressBomb = () => {
    if (!cooldownState.isReady) {
      return;
    }

    const placed = onPlaceBomb();
    if (!placed) {
      return;
    }

    setLastBombPressedAt(Date.now());
    setNowMs(Date.now());
  };

  return (
    <div style={UI_LAYER_STYLE}>
      <JoystickInputPresenter onInput={onJoystickInput} />
      <BombButton
        onPress={handlePressBomb}
        cooldownProgress={cooldownState.progress}
        isReady={cooldownState.isReady}
        remainingSecText={cooldownState.remainingSecText}
      />
    </div>
  );
};