feat: add bark battle debug feedback
This commit is contained in:
@@ -46,9 +46,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bark-battle-dog {
|
.bark-battle-dog {
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
animation: barkBattleDogPulse 420ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bark-battle-dog__body {
|
.bark-battle-dog__body {
|
||||||
@@ -61,11 +63,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bark-battle-dog__label,
|
.bark-battle-dog__label,
|
||||||
|
.bark-battle-dog__burst,
|
||||||
.bark-battle-vs {
|
.bark-battle-vs {
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bark-battle-dog__burst {
|
||||||
|
position: absolute;
|
||||||
|
top: -18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
color: #1f1147;
|
||||||
|
background: #facc15;
|
||||||
|
box-shadow: 0 0 22px rgba(250, 204, 21, 0.72);
|
||||||
|
animation: barkBattleBurst 640ms ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bark-battle-dog--opponent .bark-battle-dog__burst {
|
||||||
|
color: #fff7ed;
|
||||||
|
background: #7c3aed;
|
||||||
|
box-shadow: 0 0 22px rgba(124, 58, 237, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
.bark-battle-vs {
|
.bark-battle-vs {
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 10px 18px;
|
padding: 10px 18px;
|
||||||
@@ -176,6 +196,27 @@
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bark-battle-debug-metrics,
|
||||||
|
.bark-battle-debug-events {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bark-battle-debug-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bark-battle-debug-events {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
padding-left: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
.bark-battle-debug-panel__controls button {
|
.bark-battle-debug-panel__controls button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -186,6 +227,18 @@
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes barkBattleDogPulse {
|
||||||
|
from { transform: scale(1); }
|
||||||
|
45% { transform: scale(1.08); }
|
||||||
|
to { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes barkBattleBurst {
|
||||||
|
from { transform: translateY(18px) scale(0.72); opacity: 0; }
|
||||||
|
35% { opacity: 1; }
|
||||||
|
to { transform: translateY(-38px) scale(1.16); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes barkBattleParticlePop {
|
@keyframes barkBattleParticlePop {
|
||||||
from { transform: translateY(28px) scale(0.7); opacity: 0; }
|
from { transform: translateY(28px) scale(0.7); opacity: 0; }
|
||||||
42% { opacity: 1; }
|
42% { opacity: 1; }
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import type { BarkBattleSnapshot } from '../domain/BarkBattleTypes';
|
|||||||
|
|
||||||
type BarkBattleHudProps = {
|
type BarkBattleHudProps = {
|
||||||
snapshot: BarkBattleSnapshot;
|
snapshot: BarkBattleSnapshot;
|
||||||
|
playerPulseKey?: number;
|
||||||
|
opponentPulseKey?: number;
|
||||||
onStartMicrophone?: () => void;
|
onStartMicrophone?: () => void;
|
||||||
onMockBark?: () => void;
|
onMockBark?: () => void;
|
||||||
onMockQuiet?: () => void;
|
onMockQuiet?: () => void;
|
||||||
@@ -24,6 +26,8 @@ const failureText = {
|
|||||||
|
|
||||||
export function BarkBattleHud({
|
export function BarkBattleHud({
|
||||||
snapshot,
|
snapshot,
|
||||||
|
playerPulseKey = 0,
|
||||||
|
opponentPulseKey = 0,
|
||||||
onStartMicrophone,
|
onStartMicrophone,
|
||||||
onMockBark,
|
onMockBark,
|
||||||
onMockQuiet,
|
onMockQuiet,
|
||||||
@@ -61,12 +65,14 @@ export function BarkBattleHud({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bark-battle-arena" aria-label="竖屏声浪竞技场">
|
<div className="bark-battle-arena" aria-label="竖屏声浪竞技场">
|
||||||
<div className="bark-battle-dog bark-battle-dog--player" aria-label="玩家狗狗背对屏幕">
|
<div key={`player-${playerPulseKey}`} className="bark-battle-dog bark-battle-dog--player" aria-label="玩家狗狗背对屏幕">
|
||||||
|
<span className="bark-battle-dog__burst" aria-hidden="true">汪</span>
|
||||||
<span className="bark-battle-dog__body">🐕</span>
|
<span className="bark-battle-dog__body">🐕</span>
|
||||||
<span className="bark-battle-dog__label">你 · {snapshot.player.barkCount}</span>
|
<span className="bark-battle-dog__label">你 · {snapshot.player.barkCount}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bark-battle-vs">VS</div>
|
<div className="bark-battle-vs">VS</div>
|
||||||
<div className="bark-battle-dog bark-battle-dog--opponent" aria-label="对手狗狗面向屏幕">
|
<div key={`opponent-${opponentPulseKey}`} className="bark-battle-dog bark-battle-dog--opponent" aria-label="对手狗狗面向屏幕">
|
||||||
|
<span className="bark-battle-dog__burst" aria-hidden="true">反击</span>
|
||||||
<span className="bark-battle-dog__body">🐶</span>
|
<span className="bark-battle-dog__body">🐶</span>
|
||||||
<span className="bark-battle-dog__label">对手 · {snapshot.opponent.barkCount}</span>
|
<span className="bark-battle-dog__label">对手 · {snapshot.opponent.barkCount}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type BarkBattleConfig,
|
type BarkBattleConfig,
|
||||||
@@ -12,6 +12,11 @@ type BarkBattleRuntimeShellProps = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DebugEvent = {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
const DEBUG_CONFIG_FIELDS: Array<{
|
const DEBUG_CONFIG_FIELDS: Array<{
|
||||||
key: keyof Pick<
|
key: keyof Pick<
|
||||||
BarkBattleConfig,
|
BarkBattleConfig,
|
||||||
@@ -46,12 +51,39 @@ export function BarkBattleRuntimeShell({ title = '汪汪声浪大作战' }: Bark
|
|||||||
const controller = controllerRef.current;
|
const controller = controllerRef.current;
|
||||||
const [snapshot, setSnapshot] = useState(() => controller.getSnapshot());
|
const [snapshot, setSnapshot] = useState(() => controller.getSnapshot());
|
||||||
const [particleText, setParticleText] = useState('');
|
const [particleText, setParticleText] = useState('');
|
||||||
|
const [playerPulseKey, setPlayerPulseKey] = useState(0);
|
||||||
|
const [opponentPulseKey, setOpponentPulseKey] = useState(0);
|
||||||
|
const [debugEvents, setDebugEvents] = useState<DebugEvent[]>([]);
|
||||||
const heldRef = useRef(false);
|
const heldRef = useRef(false);
|
||||||
|
const lastPlayerBarkCountRef = useRef(0);
|
||||||
|
const lastOpponentPowerRef = useRef(0);
|
||||||
|
const debugEventIdRef = useRef(0);
|
||||||
|
|
||||||
|
const appendDebugEvent = useCallback((text: string) => {
|
||||||
|
debugEventIdRef.current += 1;
|
||||||
|
const event = { id: debugEventIdRef.current, text };
|
||||||
|
setDebugEvents((current) => [event, ...current].slice(0, 5));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const syncSnapshot = useCallback(() => {
|
||||||
|
const nextSnapshot = controller.getSnapshot();
|
||||||
|
if (nextSnapshot.player.barkCount > lastPlayerBarkCountRef.current) {
|
||||||
|
setPlayerPulseKey((current) => current + 1);
|
||||||
|
appendDebugEvent(`玩家叫声触发 #${nextSnapshot.player.barkCount} · 能量 ${Math.round(nextSnapshot.energy)}`);
|
||||||
|
}
|
||||||
|
if (nextSnapshot.phase === 'playing' && Math.abs(nextSnapshot.opponent.power - lastOpponentPowerRef.current) >= 0.08) {
|
||||||
|
setOpponentPulseKey((current) => current + 1);
|
||||||
|
appendDebugEvent(`对手反击强度 ${(nextSnapshot.opponent.power * 100).toFixed(0)}%`);
|
||||||
|
}
|
||||||
|
lastPlayerBarkCountRef.current = nextSnapshot.player.barkCount;
|
||||||
|
lastOpponentPowerRef.current = nextSnapshot.opponent.power;
|
||||||
|
setSnapshot(nextSnapshot);
|
||||||
|
}, [appendDebugEvent, controller]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
controller.updateConfig(config);
|
controller.updateConfig(config);
|
||||||
setSnapshot(controller.getSnapshot());
|
syncSnapshot();
|
||||||
}, [config, controller]);
|
}, [config, controller, syncSnapshot]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
@@ -61,31 +93,38 @@ export function BarkBattleRuntimeShell({ title = '汪汪声浪大作战' }: Bark
|
|||||||
} else {
|
} else {
|
||||||
controller.submitMockSample(0.12);
|
controller.submitMockSample(0.12);
|
||||||
}
|
}
|
||||||
setSnapshot(controller.getSnapshot());
|
syncSnapshot();
|
||||||
}, 100);
|
}, 100);
|
||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
}, [controller]);
|
}, [controller, syncSnapshot]);
|
||||||
|
|
||||||
const restart = () => {
|
const restart = () => {
|
||||||
heldRef.current = false;
|
heldRef.current = false;
|
||||||
controller.restart();
|
controller.restart();
|
||||||
setParticleText('');
|
setParticleText('');
|
||||||
setSnapshot(controller.getSnapshot());
|
setDebugEvents([]);
|
||||||
|
lastPlayerBarkCountRef.current = 0;
|
||||||
|
lastOpponentPowerRef.current = 0;
|
||||||
|
syncSnapshot();
|
||||||
};
|
};
|
||||||
|
|
||||||
const startMock = () => {
|
const startMock = () => {
|
||||||
controller.startWithMockInput();
|
controller.startWithMockInput();
|
||||||
setSnapshot(controller.getSnapshot());
|
appendDebugEvent('开始 mock 对局');
|
||||||
|
syncSnapshot();
|
||||||
};
|
};
|
||||||
|
|
||||||
const finishNow = () => {
|
const finishNow = () => {
|
||||||
heldRef.current = false;
|
heldRef.current = false;
|
||||||
controller.finishNow();
|
controller.finishNow();
|
||||||
setSnapshot(controller.getSnapshot());
|
appendDebugEvent('人工结束对局');
|
||||||
|
syncSnapshot();
|
||||||
};
|
};
|
||||||
|
|
||||||
const bark = () => {
|
const bark = () => {
|
||||||
heldRef.current = true;
|
heldRef.current = true;
|
||||||
|
setPlayerPulseKey((current) => current + 1);
|
||||||
|
appendDebugEvent('按下模拟叫声按钮');
|
||||||
setParticleText('汪!');
|
setParticleText('汪!');
|
||||||
window.setTimeout(() => setParticleText(''), 680);
|
window.setTimeout(() => setParticleText(''), 680);
|
||||||
};
|
};
|
||||||
@@ -94,6 +133,8 @@ export function BarkBattleRuntimeShell({ title = '汪汪声浪大作战' }: Bark
|
|||||||
<main className="bark-battle-runtime" aria-label={title}>
|
<main className="bark-battle-runtime" aria-label={title}>
|
||||||
<BarkBattleHud
|
<BarkBattleHud
|
||||||
snapshot={snapshot}
|
snapshot={snapshot}
|
||||||
|
playerPulseKey={playerPulseKey}
|
||||||
|
opponentPulseKey={opponentPulseKey}
|
||||||
onStartMicrophone={startMock}
|
onStartMicrophone={startMock}
|
||||||
onMockBark={bark}
|
onMockBark={bark}
|
||||||
onMockQuiet={() => {
|
onMockQuiet={() => {
|
||||||
@@ -111,6 +152,15 @@ export function BarkBattleRuntimeShell({ title = '汪汪声浪大作战' }: Bark
|
|||||||
<button type="button" onClick={finishNow}>结束</button>
|
<button type="button" onClick={finishNow}>结束</button>
|
||||||
<button type="button" onClick={restart}>重置</button>
|
<button type="button" onClick={restart}>重置</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="bark-battle-debug-metrics" aria-label="触发反馈">
|
||||||
|
<span>玩家触发:{snapshot.player.barkCount}</span>
|
||||||
|
<span>玩家强度:{(snapshot.player.power * 100).toFixed(0)}%</span>
|
||||||
|
<span>对手强度:{(snapshot.opponent.power * 100).toFixed(0)}%</span>
|
||||||
|
<span>能量:{Math.round(snapshot.energy)}</span>
|
||||||
|
</div>
|
||||||
|
<ol className="bark-battle-debug-events" aria-label="触发日志">
|
||||||
|
{debugEvents.length ? debugEvents.map((event) => <li key={event.id}>{event.text}</li>) : <li>等待输入触发</li>}
|
||||||
|
</ol>
|
||||||
{DEBUG_CONFIG_FIELDS.map((field) => (
|
{DEBUG_CONFIG_FIELDS.map((field) => (
|
||||||
<label key={field.key}>
|
<label key={field.key}>
|
||||||
<span>{field.label}</span>
|
<span>{field.label}</span>
|
||||||
|
|||||||
@@ -15,10 +15,15 @@ describe('BarkBattleRuntimeShell 调试面板', () => {
|
|||||||
expect(screen.getByRole('button', { name: '结束' })).toBeTruthy();
|
expect(screen.getByRole('button', { name: '结束' })).toBeTruthy();
|
||||||
expect(screen.getByRole('button', { name: '重置' })).toBeTruthy();
|
expect(screen.getByRole('button', { name: '重置' })).toBeTruthy();
|
||||||
expect(screen.getByLabelText('叫声阈值')).toBeTruthy();
|
expect(screen.getByLabelText('叫声阈值')).toBeTruthy();
|
||||||
|
expect(screen.getByLabelText('触发反馈')).toBeTruthy();
|
||||||
|
expect(screen.getByLabelText('触发日志')).toBeTruthy();
|
||||||
|
|
||||||
await userEvent.click(screen.getByRole('button', { name: '开始' }));
|
await userEvent.click(screen.getByRole('button', { name: '开始' }));
|
||||||
expect(screen.getByText(/countdown|playing/u)).toBeTruthy();
|
expect(screen.getByText(/countdown|playing/u)).toBeTruthy();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: '模拟叫声' }));
|
||||||
|
expect(screen.getAllByText('按下模拟叫声按钮').length).toBeGreaterThan(0);
|
||||||
|
|
||||||
await userEvent.click(screen.getByRole('button', { name: '结束' }));
|
await userEvent.click(screen.getByRole('button', { name: '结束' }));
|
||||||
expect(screen.getByRole('dialog', { name: '对战结算' })).toBeTruthy();
|
expect(screen.getByRole('dialog', { name: '对战结算' })).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user