aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/app/game/[slug]/components/PlayingView.tsx7
-rw-r--r--src/app/game/[slug]/hooks/useGameEngine.ts49
-rw-r--r--src/app/game/[slug]/page.tsx3
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}
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage