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) <noreply@anthropic.com>
main
name 2026-04-11 19:22:46 -07:00
parent e82cc0a016
commit 6ae7edeb56
3 changed files with 108 additions and 90 deletions

View File

@ -1,4 +1,5 @@
import { AppRoutes } from './app/AppRoutes'
import './App.css'
import { useKeyboardInput } from './input/useKeyboardInput'
import { usePoseInput } from './input/usePoseInput'

View File

@ -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);
}

View File

@ -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<ArrowGameHandle>(null)
const videoRef = useRef<HTMLVideoElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const arCanvasRef = useRef<HTMLCanvasElement>(null)
const [settings] = useState<PoseSettings>(loadSettings)
const [chartNotes, setChartNotes] = useState<ChartNote[] | undefined>(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 (
<div className={styles.root}>
<div className={styles.inner}>
<p className={`${styles.label} font-caption`}>NOW PLAYING</p>
<h1 className={`${styles.songTitle} font-display`}>
{resolvedSong.title}
</h1>
<p className={`${styles.artist} font-body`}>{resolvedSong.artist}</p>
{loading && <div className="status">Loading pose detection...</div>}
{error && <div className="status error">{error}</div>}
<div className={styles.placeholder}>
<p className="font-caption">GAME WILL GO HERE</p>
</div>
<video ref={videoRef} className="webcam-video" playsInline muted />
<canvas ref={canvasRef} className="pose-overlay" />
<canvas ref={arCanvasRef} className="ar-overlay" />
<button className={`${styles.finishBtn} font-title`} onClick={handleFinish}>
FINISH PLAYING
</button>
</div>
{(calibrationStatus === 'framing' || calibrationStatus === 'paused') && (
<div className="framing-line" />
)}
<GestureOverlay
gesture={gesture}
calibrationStatus={calibrationStatus}
framingHint={framingHint}
/>
{glow && (
<div
key={glow.key}
className="judgment-glow"
style={{ '--glow-color': glow.color } as React.CSSProperties}
onAnimationEnd={() => setGlow(null)}
/>
)}
<ArrowGame
ref={arrowGameRef}
active={gameReady}
chartNotes={chartNotes}
audioUrl={resolvedSong.audioUrl}
onJudgment={handleJudgmentWithGlow}
onSongEnd={handleSongEnd}
/>
</div>
)
}