diff options
| -rw-r--r-- | src/app/game/[slug]/components/PlayingView.tsx | 7 | ||||
| -rw-r--r-- | src/app/game/[slug]/hooks/useGameEngine.ts | 49 | ||||
| -rw-r--r-- | src/app/game/[slug]/page.tsx | 3 |
3 files changed, 50 insertions, 9 deletions
diff --git a/src/app/game/[slug]/components/PlayingView.tsx b/src/app/game/[slug]/components/PlayingView.tsx index 446670a..0b44a08 100644 --- a/src/app/game/[slug]/components/PlayingView.tsx +++ b/src/app/game/[slug]/components/PlayingView.tsx @@ -52,6 +52,7 @@ interface PlayingViewProps { lineRemainingMs: number; currentLineTime: number; intermissionData: { pct: number; remainingMs: number }; + endingIntermissionData: { canSkip: boolean; remainingMs: number }; wrongChar: boolean; clearShowing: boolean; comboAnimKey: number; @@ -75,6 +76,7 @@ export default function PlayingView({ lineRemainingMs, currentLineTime, intermissionData, + endingIntermissionData, wrongChar, clearShowing, comboAnimKey, @@ -179,6 +181,11 @@ export default function PlayingView({ </LineTimingMeta> )} </LineTimingRow> + {endingIntermissionData.canSkip && ( + <div style={{ fontSize: 12, color: "rgba(255,255,255,0.6)", marginTop: 8, textAlign: "center" }}> + Press Space to skip long intermissions + </div> + )} <LineTimingBar> <LineTimingFill $pct={lineTimingPct} /> </LineTimingBar> diff --git a/src/app/game/[slug]/hooks/useGameEngine.ts b/src/app/game/[slug]/hooks/useGameEngine.ts index b2bd126..42fc2c1 100644 --- a/src/app/game/[slug]/hooks/useGameEngine.ts +++ b/src/app/game/[slug]/hooks/useGameEngine.ts @@ -84,6 +84,7 @@ export interface GameEngineResult { skipBacking: boolean; isVideo: boolean; intermissionData: { pct: number; remainingMs: number }; + endingIntermissionData: { canSkip: boolean; remainingMs: number }; // handlers handleStart: () => void; @@ -291,6 +292,22 @@ export function useGameEngine(): GameEngineResult { return { pct, remainingMs }; }, [gameLines, currentMs, offset]); // eslint-disable-line react-hooks/exhaustive-deps + const endingIntermissionData = useMemo(() => { + const lastIdx = gameLines.length - 1; + if (lastIdx < 0 || phase !== "playing") return { canSkip: false, remainingMs: 0 }; + + const isAtOrPastLastLine = g.displayedLineIdx >= lastIdx; + if (!isAtOrPastLastLine) return { canSkip: false, remainingMs: 0 }; + + const lastLine = gameLines[lastIdx]; + const inEndingIntermission = g.lineCompleted || (lastLine?.content.trim() ?? "") === ""; + if (!inEndingIntermission) return { canSkip: false, remainingMs: 0 }; + + const mediaCurrentMs = Math.max(0, currentMs - offset); + const remainingMs = Math.max(0, duration - mediaCurrentMs); + return { canSkip: remainingMs > 5000, remainingMs }; + }, [gameLines, phase, g.displayedLineIdx, g.lineCompleted, currentMs, duration, offset]); + useEffect(() => { const idx = g.displayedLineIdx; if (idx < 0 || !gameLines[idx]) { @@ -506,16 +523,31 @@ export function useGameEngine(): GameEngineResult { const handler = (e: KeyboardEvent) => { if (e.key === " ") { const idx = gRef.current.displayedLineIdx; - if (idx < 0 && gameLines.length > 0) { + const media = isVideo ? videoRef.current : audioRef.current; + + if (idx < 0 && gameLines.length > 0 && media) { const firstMs = gameLines[0]?.millisecond ?? 0; - const media = isVideo ? videoRef.current : audioRef.current; - if (media) { - const currentMsLocal = media.currentTime * 1000 + offsetRef.current; - const intermissionRemaining = Math.max(0, firstMs - currentMsLocal); - if (intermissionRemaining > 5000) { + const currentMsLocal = media.currentTime * 1000 + offsetRef.current; + const intermissionRemaining = Math.max(0, firstMs - currentMsLocal); + if (intermissionRemaining > 5000) { + e.preventDefault(); + const targetMs = firstMs - 3000; + media.currentTime = Math.max(0, (targetMs - offsetRef.current) / 1000); + setCurrentMs(media.currentTime * 1000 + offsetRef.current); + return; + } + } + + if (idx >= gameLines.length - 1 && gameLines.length > 0 && media) { + const currentLine = gameLines[idx]; + const canSkipEndingIntermission = + gRef.current.lineCompleted || (currentLine?.content.trim() ?? "") === ""; + + if (canSkipEndingIntermission && Number.isFinite(media.duration) && media.duration > 0) { + const remainingToEndMs = Math.max(0, media.duration * 1000 - media.currentTime * 1000); + if (remainingToEndMs > 5000) { e.preventDefault(); - const targetMs = firstMs - 3000; - media.currentTime = Math.max(0, (targetMs - offsetRef.current) / 1000); + media.currentTime = Math.max(0, media.duration - 0.05); setCurrentMs(media.currentTime * 1000 + offsetRef.current); return; } @@ -581,6 +613,7 @@ export function useGameEngine(): GameEngineResult { skipBacking, isVideo, intermissionData, + endingIntermissionData, // handlers handleStart, diff --git a/src/app/game/[slug]/page.tsx b/src/app/game/[slug]/page.tsx index d9f431f..57ae6b2 100644 --- a/src/app/game/[slug]/page.tsx +++ b/src/app/game/[slug]/page.tsx @@ -24,7 +24,7 @@ function GameInner() { g, gameLines, accuracy, wpm, // timing currentMs, duration, progressPct, lineTimingPct, lineRemainingMs, - currentLineTime, intermissionData, + currentLineTime, intermissionData, endingIntermissionData, // display wrongChar, clearShowing, comboAnimKey, wrapSpaceIndicators, // settings @@ -103,6 +103,7 @@ function GameInner() { lineRemainingMs={lineRemainingMs} currentLineTime={currentLineTime} intermissionData={intermissionData} + endingIntermissionData={endingIntermissionData} wrongChar={wrongChar} clearShowing={clearShowing} comboAnimKey={comboAnimKey} |
