feat: add bark battle browser prototype

This commit is contained in:
2026-05-11 18:01:55 +08:00
parent bf72c2e48d
commit 2b046656dc
32 changed files with 2244 additions and 18 deletions

View File

@@ -0,0 +1,137 @@
import { useEffect, useRef, useState } from 'react';
import {
type BarkBattleConfig,
DEFAULT_BARK_BATTLE_CONFIG,
} from '../application/BarkBattleConfig';
import { BarkBattleController } from '../application/BarkBattleController';
import { BarkBattleHud } from './BarkBattleHud';
import { BarkBattleResultPanel } from './BarkBattleResultPanel';
type BarkBattleRuntimeShellProps = {
title?: string;
};
const DEBUG_CONFIG_FIELDS: Array<{
key: keyof Pick<
BarkBattleConfig,
| 'roundDurationMs'
| 'countdownMs'
| 'drawThreshold'
| 'barkThreshold'
| 'minBarkGapMs'
| 'balanceFactor'
| 'opponentBasePower'
>;
label: string;
min: number;
max: number;
step: number;
}> = [
{ key: 'roundDurationMs', label: '局长(ms)', min: 1000, max: 60000, step: 1000 },
{ key: 'countdownMs', label: '倒计时(ms)', min: 0, max: 5000, step: 500 },
{ key: 'drawThreshold', label: '平局阈值', min: 0, max: 40, step: 1 },
{ key: 'barkThreshold', label: '叫声阈值', min: 0.1, max: 1, step: 0.05 },
{ key: 'minBarkGapMs', label: '叫声间隔(ms)', min: 100, max: 1200, step: 50 },
{ key: 'balanceFactor', label: '拉锯速度', min: 5, max: 80, step: 1 },
{ key: 'opponentBasePower', label: '对手基础力', min: 0, max: 1, step: 0.05 },
];
export function BarkBattleRuntimeShell({ title = '汪汪声浪大作战' }: BarkBattleRuntimeShellProps) {
const [config, setConfig] = useState(DEFAULT_BARK_BATTLE_CONFIG);
const controllerRef = useRef<BarkBattleController | null>(null);
if (!controllerRef.current) {
controllerRef.current = new BarkBattleController(config);
}
const controller = controllerRef.current;
const [snapshot, setSnapshot] = useState(() => controller.getSnapshot());
const [particleText, setParticleText] = useState('');
const heldRef = useRef(false);
useEffect(() => {
controller.updateConfig(config);
setSnapshot(controller.getSnapshot());
}, [config, controller]);
useEffect(() => {
const timer = window.setInterval(() => {
controller.tick(100);
if (heldRef.current) {
controller.submitMockSample(0.88);
} else {
controller.submitMockSample(0.12);
}
setSnapshot(controller.getSnapshot());
}, 100);
return () => window.clearInterval(timer);
}, [controller]);
const restart = () => {
heldRef.current = false;
controller.restart();
setParticleText('');
setSnapshot(controller.getSnapshot());
};
const startMock = () => {
controller.startWithMockInput();
setSnapshot(controller.getSnapshot());
};
const finishNow = () => {
heldRef.current = false;
controller.finishNow();
setSnapshot(controller.getSnapshot());
};
const bark = () => {
heldRef.current = true;
setParticleText('汪!');
window.setTimeout(() => setParticleText(''), 680);
};
return (
<main className="bark-battle-runtime" aria-label={title}>
<BarkBattleHud
snapshot={snapshot}
onStartMicrophone={startMock}
onMockBark={bark}
onMockQuiet={() => {
heldRef.current = false;
}}
onRestart={restart}
/>
<aside className="bark-battle-debug-panel" aria-label="调试面板">
<header>
<strong></strong>
<span>{snapshot.phase}</span>
</header>
<div className="bark-battle-debug-panel__controls">
<button type="button" onClick={startMock}></button>
<button type="button" onClick={finishNow}></button>
<button type="button" onClick={restart}></button>
</div>
{DEBUG_CONFIG_FIELDS.map((field) => (
<label key={field.key}>
<span>{field.label}</span>
<input
aria-label={field.label}
type="range"
min={field.min}
max={field.max}
step={field.step}
value={config[field.key]}
onChange={(event) => {
const value = Number(event.currentTarget.value);
setConfig((current) => ({ ...current, [field.key]: value }));
}}
/>
<output>{config[field.key]}</output>
</label>
))}
</aside>
{particleText ? <div className="bark-battle-particles">{particleText}</div> : null}
{snapshot.result ? <BarkBattleResultPanel result={snapshot.result} onRestart={restart} /> : null}
</main>
);
}