add score pulse and floating delta on each hit
Score text pulses (scale 1.3→1) on each point gain. A +delta floater appears at a random offset near the score and fades upward over 0.8s. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>main
parent
0b134d1dbe
commit
d2630b1b64
|
|
@ -18,6 +18,31 @@
|
|||
color: #fff;
|
||||
text-shadow: 0 0 16px rgba(217, 70, 239, 0.6), 0 2px 4px rgba(0, 0, 0, 0.8);
|
||||
font-variant-numeric: tabular-nums;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.scorePulse {
|
||||
animation: score-beat 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes score-beat {
|
||||
0% { transform: scale(1.3); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.scoreDelta {
|
||||
position: absolute;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #00ffcc;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
|
||||
pointer-events: none;
|
||||
animation: delta-float 0.8s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes delta-float {
|
||||
0% { opacity: 1; transform: translateY(0); }
|
||||
100% { opacity: 0; transform: translateY(-30px); }
|
||||
}
|
||||
|
||||
.menuBtn {
|
||||
|
|
|
|||
|
|
@ -56,20 +56,35 @@ export function GamePage() {
|
|||
|
||||
const gameReady = calibrationStatus === 'ready'
|
||||
|
||||
// Score delta floater
|
||||
const [scoreDelta, setScoreDelta] = useState<{ value: number; key: number; dx: number; dy: number } | null>(null)
|
||||
const [scorePulse, setScorePulse] = useState(0)
|
||||
|
||||
// Scoring: base points + combo bonus (capped at 4x)
|
||||
const handleJudgment = useCallback(
|
||||
(j: 'perfect' | 'good' | 'miss', combo: number) => {
|
||||
let delta = 0
|
||||
if (j === 'perfect') {
|
||||
addPerfectHit()
|
||||
const multiplier = 1 + Math.min(combo, 30) / 10
|
||||
addScore(Math.round(1000 * multiplier))
|
||||
delta = Math.round(1000 * multiplier)
|
||||
} else if (j === 'good') {
|
||||
addGoodHit()
|
||||
const multiplier = 1 + Math.min(combo, 30) / 10
|
||||
addScore(Math.round(500 * multiplier))
|
||||
delta = Math.round(500 * multiplier)
|
||||
} else {
|
||||
addMiss()
|
||||
}
|
||||
if (delta > 0) {
|
||||
addScore(delta)
|
||||
setScoreDelta({
|
||||
value: delta,
|
||||
key: Date.now(),
|
||||
dx: 20 + Math.random() * 80,
|
||||
dy: -10 + Math.random() * 40,
|
||||
})
|
||||
setScorePulse(Date.now())
|
||||
}
|
||||
},
|
||||
[addPerfectHit, addGoodHit, addMiss, addScore],
|
||||
)
|
||||
|
|
@ -104,9 +119,22 @@ export function GamePage() {
|
|||
{error && <div className="status error">{error}</div>}
|
||||
|
||||
<div className={styles.scoreHud}>
|
||||
<span className={`${styles.scoreValue} font-title`}>
|
||||
<span
|
||||
key={scorePulse}
|
||||
className={`${styles.scoreValue} ${scorePulse ? styles.scorePulse : ''} font-title`}
|
||||
>
|
||||
{currentScore.toLocaleString()}
|
||||
</span>
|
||||
{scoreDelta && (
|
||||
<span
|
||||
key={scoreDelta.key}
|
||||
className={styles.scoreDelta}
|
||||
style={{ left: scoreDelta.dx, top: scoreDelta.dy }}
|
||||
onAnimationEnd={() => setScoreDelta(null)}
|
||||
>
|
||||
+{scoreDelta.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<video ref={videoRef} className="webcam-video" playsInline muted />
|
||||
|
|
|
|||
Loading…
Reference in New Issue