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
parent
e82cc0a016
commit
6ae7edeb56
|
|
@ -1,4 +1,5 @@
|
|||
import { AppRoutes } from './app/AppRoutes'
|
||||
import './App.css'
|
||||
import { useKeyboardInput } from './input/useKeyboardInput'
|
||||
import { usePoseInput } from './input/usePoseInput'
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue