Newer
Older
PixelPaintWar / apps / client / src / scenes / game / input / hooks / useCooldownClock.ts
/**
 * useCooldownClock
 * クールダウン経過率と残り秒表示を計算するフック
 * トリガー時刻を保持し一定間隔で再計算する
 */
import { useCallback, useEffect, useMemo, useState } from "react";
import {
  SYSTEM_TIME_PROVIDER,
  type TimeProvider,
} from "@client/scenes/game/application/time/TimeProvider";

const COOLDOWN_TICK_MS = 50;

/** クールダウンUI描画に必要な状態 */
export type CooldownState = {
  progress: number;
  isReady: boolean;
  remainingSecText: string | null;
};

const READY_STATE: CooldownState = {
  progress: 1,
  isReady: true,
  remainingSecText: null,
};

/** useCooldownClock の依存注入オプション */
export type UseCooldownClockOptions = {
  timeProvider?: TimeProvider;
};

/** クールダウン状態とトリガー操作を提供するフック */
export const useCooldownClock = (
  cooldownMs: number,
  options?: UseCooldownClockOptions,
) => {
  const timeProvider = options?.timeProvider ?? SYSTEM_TIME_PROVIDER;
  const getNow = useCallback(() => timeProvider.now(), [timeProvider]);
  const [lastTriggeredAt, setLastTriggeredAt] = useState<number | null>(null);
  const [nowMs, setNowMs] = useState(() => getNow());

  useEffect(() => {
    if (lastTriggeredAt === null || cooldownMs <= 0) {
      return;
    }

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

    return () => {
      window.clearInterval(timerId);
    };
  }, [cooldownMs, getNow, lastTriggeredAt]);

  const cooldownState = useMemo<CooldownState>(() => {
    if (cooldownMs <= 0 || lastTriggeredAt === null) {
      return READY_STATE;
    }

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

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

  const markTriggered = useCallback(() => {
    const now = getNow();
    setLastTriggeredAt(now);
    setNowMs(now);
  }, [getNow]);

  return {
    cooldownState,
    markTriggered,
  };
};