Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | 102x 102x 102x 102x 9x 93x 23x 70x 102x 102x 102x 102x 102x 102x 102x 53x 53x 53x 102x 101x 35x 35x 66x 66x 66x 175x 175x 175x 175x 2x 2x 2x 2x 2x 66x 66x 64x 64x 102x 102x 100x 39x 36x 10x 10x 36x 5x 5x 102x 102x | /**
* useCombatTimer - Hook for managing combat round timer
*
* Korean: 전투 라운드 타이머 훅 (Combat Round Timer Hook)
*
* Manages countdown timer for combat rounds with:
* - Pause/resume support
* - Warning thresholds at 10s and 5s
* - Audio alerts for warnings
* - Time's up callback
*
* @module hooks/useCombatTimer
* @category Combat Hooks
*/
import { useEffect, useRef, useState } from "react";
import { useAudio } from "../audio/AudioProvider";
/**
* Timer warning level indicating urgency
*/
export type TimerWarningLevel = "none" | "warning" | "urgent";
/**
* Configuration for the combat timer hook
*/
export interface UseCombatTimerConfig {
/** Initial time in seconds */
readonly initialTime: number;
/** Whether the timer is paused */
readonly isPaused: boolean;
/** Callback when timer reaches 0 */
readonly onTimeUp: () => void;
/** Warning threshold in seconds (default: 10) */
readonly warningThreshold?: number;
/** Urgent warning threshold in seconds (default: 5) */
readonly urgentThreshold?: number;
/** Optional key to force timer reset (e.g., round number) */
readonly resetKey?: string;
}
/**
* Return value from useCombatTimer hook
*/
export interface UseCombatTimerReturn {
/** Current time remaining in seconds */
readonly timeRemaining: number;
/** Current warning level */
readonly warningLevel: TimerWarningLevel;
/** Whether timer has reached 0 */
readonly isTimeUp: boolean;
/** Formatted time string (MM:SS) */
readonly formattedTime: string;
}
/**
* Format seconds into MM:SS format
* @param seconds - Time in seconds
* @returns Formatted string (e.g., "03:45", "00:05")
*/
function formatTime(seconds: number): string {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return `${minutes.toString().padStart(2, "0")}:${remainingSeconds
.toString()
.padStart(2, "0")}`;
}
/**
* Get warning level based on time remaining
*/
function getWarningLevel(
timeRemaining: number,
warningThreshold: number,
urgentThreshold: number
): TimerWarningLevel {
if (timeRemaining <= urgentThreshold) {
return "urgent";
}
if (timeRemaining <= warningThreshold) {
return "warning";
}
return "none";
}
/**
* useCombatTimer Hook
*
* Manages combat round countdown timer with pause support and audio warnings.
*
* Features:
* - Counts down from initial time to 0
* - Pauses/resumes based on isPaused prop
* - Plays audio warning at warning threshold (default 10s)
* - Plays urgent audio warning at urgent threshold (default 5s)
* - Calls onTimeUp when timer reaches 0
* - Provides formatted time string (MM:SS)
* - Returns current warning level for UI styling
*
* Korean: 전투 라운드 타이머 관리 훅
*
* @example
* ```tsx
* const { timeRemaining, warningLevel, formattedTime } = useCombatTimer({
* initialTime: 180, // 3 minutes
* isPaused: false,
* onTimeUp: () => handleRoundEnd(),
* warningThreshold: 10,
* urgentThreshold: 5,
* });
* ```
*/
export function useCombatTimer(
config: UseCombatTimerConfig
): UseCombatTimerReturn {
const {
initialTime,
isPaused,
onTimeUp,
warningThreshold = 10,
urgentThreshold = 5,
resetKey,
} = config;
const audio = useAudio();
const [timeRemaining, setTimeRemaining] = useState(initialTime);
const [isTimeUp, setIsTimeUp] = useState(false);
const lastWarningRef = useRef<TimerWarningLevel>("none");
const intervalRef = useRef<NodeJS.Timeout | null>(null);
// Reset timer when initialTime or resetKey changes (new round)
useEffect(() => {
setTimeRemaining(initialTime);
setIsTimeUp(false);
lastWarningRef.current = "none";
}, [initialTime, resetKey]);
// Timer countdown logic
useEffect(() => {
// Don't run if paused or time is up
if (isPaused || isTimeUp) {
Iif (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
return;
}
// Track start time for precise elapsed time calculation
const startTimeRef = Date.now();
const startingTimeRemaining = timeRemaining;
// Start interval
intervalRef.current = setInterval(() => {
const elapsed = (Date.now() - startTimeRef) / 1000;
const next = Math.max(0, startingTimeRemaining - elapsed);
setTimeRemaining(next);
// Check if time just reached 0
if (next <= 0 && !isTimeUp) {
setIsTimeUp(true);
Eif (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
onTimeUp();
}
}, 100);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [isPaused, isTimeUp, onTimeUp]);
// Warning level calculation
const warningLevel = getWarningLevel(
timeRemaining,
warningThreshold,
urgentThreshold
);
// Audio warnings
useEffect(() => {
if (!audio.isAudioReady) return;
if (isPaused) return;
// Play warning sound at threshold
if (
warningLevel === "warning" &&
lastWarningRef.current === "none" &&
timeRemaining <= warningThreshold &&
timeRemaining > urgentThreshold
) {
audio.playSFX("attack_light"); // Placeholder - will be timer_warning_10s
lastWarningRef.current = "warning";
}
// Play urgent warning sound at urgent threshold
if (
warningLevel === "urgent" &&
lastWarningRef.current !== "urgent" &&
timeRemaining <= urgentThreshold &&
timeRemaining > 0
) {
audio.playSFX("attack_heavy"); // Placeholder - will be timer_warning_5s
lastWarningRef.current = "urgent";
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
warningLevel,
timeRemaining,
audio.isAudioReady,
isPaused,
warningThreshold,
urgentThreshold,
]); // audio.playSFX is stable after initialization, omitted to prevent unnecessary re-renders
// Format time for display
const formattedTime = formatTime(timeRemaining);
return {
timeRemaining,
warningLevel,
isTimeUp,
formattedTime,
};
}
|