From 6ae7edeb5611bb755667f8bc6abf25c3a85e036c Mon Sep 17 00:00:00 2001 From: name Date: Sat, 11 Apr 2026 19:22:46 -0700 Subject: [PATCH] wire real game engine into GamePage Replace placeholder GamePage with full webcam + pose detection + ArrowGame. Loads SSC chart for the selected song, runs pose loop for step detection, tracks judgments in the store, and navigates to end-of-game when the song finishes. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/App.tsx | 1 + src/pages/GamePage.module.css | 68 +----------------- src/pages/GamePage.tsx | 129 +++++++++++++++++++++++++++------- 3 files changed, 108 insertions(+), 90 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8230013..ac54932 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { AppRoutes } from './app/AppRoutes' +import './App.css' import { useKeyboardInput } from './input/useKeyboardInput' import { usePoseInput } from './input/usePoseInput' diff --git a/src/pages/GamePage.module.css b/src/pages/GamePage.module.css index 157e6fb..251a9b5 100644 --- a/src/pages/GamePage.module.css +++ b/src/pages/GamePage.module.css @@ -1,68 +1,6 @@ .root { - height: 100vh; + position: fixed; + inset: 0; + background: #000; overflow: hidden; - display: flex; - align-items: center; - justify-content: center; - background: #0a0a0a; - color: white; - padding: 2rem; -} - -.inner { - text-align: center; - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; -} - -.label { - color: #888; - margin: 0; -} - -.songTitle { - font-size: var(--font-size-display-lg); - margin: 0; - color: #d946ef; -} - -.artist { - color: #aaa; - font-size: 1.1rem; - margin: 0; -} - -.placeholder { - margin: 2rem 0; - width: 400px; - height: 200px; - border: 1px dashed rgba(255, 255, 255, 0.2); - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - color: #555; -} - -.finishBtn { - padding: 14px 40px; - border: none; - border-radius: 8px; - font-size: 1.1rem; - cursor: pointer; - background: linear-gradient(135deg, #d946ef, #a855f7); - color: white; - box-shadow: 0 0 20px rgba(217, 70, 239, 0.4); - transition: transform 0.15s, box-shadow 0.15s; -} - -.finishBtn:hover { - transform: scale(1.05); - box-shadow: 0 0 30px rgba(217, 70, 239, 0.6); -} - -.finishBtn:active { - transform: scale(0.97); } diff --git a/src/pages/GamePage.tsx b/src/pages/GamePage.tsx index 9e6b5ff..3d8e6e8 100644 --- a/src/pages/GamePage.tsx +++ b/src/pages/GamePage.tsx @@ -1,46 +1,125 @@ +import { useCallback, useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' import { ROUTE_PATHS } from '../app/route-paths' import { SONGS } from '../features/song-select/song-data' import { useGameStatsStore } from '../stores/game-stats/store' +import { ArrowGame } from '../game/ArrowGame' +import type { ArrowGameHandle } from '../game/ArrowGame' +import { loadSSC } from '../game/sscParser' +import type { ChartNote } from '../game/sscParser' +import { usePoseLoop } from '../pose/usePoseLoop' +import { GestureOverlay } from '../components/GestureOverlay' +import type { PoseSettings } from '../types' +import { loadSettings } from '../types' import styles from './GamePage.module.css' export function GamePage() { const navigate = useNavigate() const song = useGameStatsStore((state) => state.currentSong) - const setCurrentSong = useGameStatsStore((state) => state.setCurrentSong) - const setScore = useGameStatsStore((state) => state.setScore) - const setJudgments = useGameStatsStore((state) => state.setJudgments) + const addPerfectHit = useGameStatsStore((state) => state.addPerfectHit) + const addGoodHit = useGameStatsStore((state) => state.addGoodHit) + const addMiss = useGameStatsStore((state) => state.addMiss) + const addScore = useGameStatsStore((state) => state.addScore) const resolvedSong = song ?? SONGS[0] - const handleFinish = () => { - setCurrentSong(resolvedSong) - setScore(87500) - setJudgments({ - perfectHits: 30, - goodHits: 12, - misses: 3, - }) + const arrowGameRef = useRef(null) + const videoRef = useRef(null) + const canvasRef = useRef(null) + const arCanvasRef = useRef(null) + + const [settings] = useState(loadSettings) + const [chartNotes, setChartNotes] = useState(undefined) + + // Load chart + useEffect(() => { + loadSSC(resolvedSong.chartUrl).then((parsed) => { + const diff = + parsed.difficulties.find((d) => d.difficulty === 'Beginner') || + parsed.difficulties.reduce((a, b) => (a.meter < b.meter ? a : b)) + if (diff) setChartNotes(diff.notes) + }).catch(() => {}) + }, [resolvedSong.chartUrl]) + + // Pose detection + const { loading, error, calibrationStatus, framingHint, gesture } = usePoseLoop( + videoRef, + canvasRef, + arCanvasRef, + settings, + null, + { onStep: (direction) => arrowGameRef.current?.handleStep(direction) }, + ) + + const gameReady = calibrationStatus === 'ready' + + // Judgment handling + const handleJudgment = useCallback( + (j: 'perfect' | 'good' | 'miss') => { + if (j === 'perfect') { + addPerfectHit() + addScore(1000) + } else if (j === 'good') { + addGoodHit() + addScore(500) + } else { + addMiss() + } + }, + [addPerfectHit, addGoodHit, addMiss, addScore], + ) + + const handleSongEnd = useCallback(() => { navigate(ROUTE_PATHS.endOfGame) - } + }, [navigate]) + + // Glow effect + const [glow, setGlow] = useState<{ color: string; key: number } | null>(null) + const handleJudgmentWithGlow = useCallback( + (j: 'perfect' | 'good' | 'miss') => { + const colors = { perfect: '#00ff6a', good: '#ffe600', miss: '#ff2222' } + setGlow({ color: colors[j], key: Date.now() }) + handleJudgment(j) + }, + [handleJudgment], + ) return (
-
-

NOW PLAYING

-

- {resolvedSong.title} -

-

{resolvedSong.artist}

+ {loading &&
Loading pose detection...
} + {error &&
{error}
} -
-

GAME WILL GO HERE

-
+
+ {(calibrationStatus === 'framing' || calibrationStatus === 'paused') && ( +
+ )} + + + + {glow && ( +
setGlow(null)} + /> + )} + +
) }