diff --git a/web/src/features/skillcheck/indicator.tsx b/web/src/features/skillcheck/indicator.tsx index f961faafa..86381f012 100644 --- a/web/src/features/skillcheck/indicator.tsx +++ b/web/src/features/skillcheck/indicator.tsx @@ -1,6 +1,5 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import type { SkillCheckProps } from '../../typings'; -import { useInterval } from '@mantine/hooks'; interface Props { angle: number; @@ -11,15 +10,58 @@ interface Props { handleComplete: (success: boolean) => void; } -const Indicator: React.FC = ({ angle, offset, multiplier, handleComplete, skillCheck, className }) => { +const BASE_DURATION_MS = 2000; + +const Indicator: React.FC = ({ + angle, + offset, + multiplier, + handleComplete, + skillCheck, + className, +}) => { const [indicatorAngle, setIndicatorAngle] = useState(-90); const [keyPressed, setKeyPressed] = useState(false); - const interval = useInterval( - () => - setIndicatorAngle((prevState) => { - return (prevState += multiplier); - }), - 1 + + const rafIdRef = useRef(null); + const startTimeRef = useRef(null); + const completedRef = useRef(false); + + const stopAnimation = () => { + if (rafIdRef.current !== null) { + cancelAnimationFrame(rafIdRef.current); + rafIdRef.current = null; + } + }; + + const animate = useCallback( + (time: number) => { + if (completedRef.current) return; + + if (startTimeRef.current === null) { + startTimeRef.current = time; + } + + const elapsed = time - startTimeRef.current; + + const speed = Math.max(multiplier || 0, 0.0001); + const duration = BASE_DURATION_MS / speed; + + const progress = Math.min(elapsed / duration, 1); + const newAngle = -90 + progress * 360; + + setIndicatorAngle(newAngle); + + if (newAngle + 90 >= 360) { + completedRef.current = true; + stopAnimation(); + handleComplete(false); + return; + } + + rafIdRef.current = requestAnimationFrame(animate); + }, + [multiplier, handleComplete] ); const keyHandler = useCallback( (e: KeyboardEvent) => { @@ -42,32 +84,43 @@ const Indicator: React.FC = ({ angle, offset, multiplier, handleComplete, useEffect(() => { setIndicatorAngle(-90); + startTimeRef.current = null; + completedRef.current = false; + window.addEventListener('keydown', keyHandler); - interval.start(); - }, [skillCheck]); + rafIdRef.current = requestAnimationFrame(animate); - useEffect(() => { - if (indicatorAngle + 90 >= 360) { - interval.stop(); - handleComplete(false); - } - }, [indicatorAngle]); + return () => { + stopAnimation(); + window.removeEventListener('keydown', keyHandler); + startTimeRef.current = null; + completedRef.current = true; + }; + }, [skillCheck, keyHandler, animate]); useEffect(() => { - if (!keyPressed) return; + if (!keyPressed || completedRef.current) return; if (skillCheck.keys && !skillCheck.keys?.includes(keyPressed)) return; - interval.stop(); - + stopAnimation(); window.removeEventListener('keydown', keyHandler); + completedRef.current = true; if (keyPressed !== skillCheck.key || indicatorAngle < angle || indicatorAngle > angle + offset) handleComplete(false); else handleComplete(true); setKeyPressed(false); - }, [keyPressed]); + }, [ + keyPressed, + angle, + offset, + indicatorAngle, + skillCheck, + keyHandler, + handleComplete, + ]); return ; };