feat(web): implement haptic feedback at critical moments in replay viewer
Adds mobile haptic feedback (50ms vibration pulse) at critical moments during replay playback per plan §16.18: - Added opt-in toggle in Accessibility panel (checked by default) - Triggers vibration on: - Combat deaths (bot_died events) - Core captures (core_captured events) - Win probability shifts >15% - Imports hapticPulse, isHapticEnabled, setHapticEnabled from ambient.ts - Integrated into onTurnChange callback for real-time feedback Closes: bf-2m3wm Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
13d5a9f17d
commit
88d2fa161f
1 changed files with 46 additions and 0 deletions
|
|
@ -29,6 +29,7 @@ import { THEATER_STYLES, TheaterMode } from '../components/theater';
|
|||
import { setActiveReplay } from '../components/pip-registry';
|
||||
import { getPipMatchId, restorePip } from '../components/pip';
|
||||
import { fetchReplayFromUrl } from '../lib/replay-data';
|
||||
import { hapticPulse, isHapticEnabled, setHapticEnabled } from '../lib/ambient';
|
||||
|
||||
const loadReplayViewer = () => import('../replay-viewer');
|
||||
|
||||
|
|
@ -220,6 +221,10 @@ function initReplayViewerWithClass(ReplayViewerClass: any, initialUrl?: string):
|
|||
<input type="checkbox" id="reduced-motion-toggle">
|
||||
Reduced motion
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="haptic-toggle" checked>
|
||||
Haptic feedback (mobile)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -1496,6 +1501,7 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void {
|
|||
const shapesToggle = document.getElementById('shapes-toggle') as HTMLInputElement;
|
||||
const highContrastToggle = document.getElementById('high-contrast-toggle') as HTMLInputElement;
|
||||
const reducedMotionToggle = document.getElementById('reduced-motion-toggle') as HTMLInputElement;
|
||||
const hapticToggle = document.getElementById('haptic-toggle') as HTMLInputElement;
|
||||
|
||||
function updateAccessibility(): void {
|
||||
viewer.setAccessibility({
|
||||
|
|
@ -1511,6 +1517,12 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void {
|
|||
highContrastToggle.addEventListener('change', updateAccessibility);
|
||||
reducedMotionToggle.addEventListener('change', updateAccessibility);
|
||||
|
||||
// Haptic feedback toggle (§16.18)
|
||||
hapticToggle.checked = isHapticEnabled();
|
||||
hapticToggle.addEventListener('change', () => {
|
||||
setHapticEnabled(hapticToggle.checked);
|
||||
});
|
||||
|
||||
// Initialize accessibility from system preferences
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
reducedMotionToggle.checked = true;
|
||||
|
|
@ -1536,6 +1548,40 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void {
|
|||
viewer.refreshWinProbSparkline();
|
||||
updateAnnotationOverlay();
|
||||
updateTranscript();
|
||||
|
||||
// Haptic feedback at critical moments (§16.18)
|
||||
if (!isHapticEnabled()) return;
|
||||
const turn = viewer.getTurn();
|
||||
const replay = viewer.getReplay() as Replay | null;
|
||||
if (!replay || !replay.turns[turn]) return;
|
||||
|
||||
const turnData = replay.turns[turn];
|
||||
const events = turnData.events ?? [];
|
||||
let hasCriticalEvent = false;
|
||||
|
||||
// Check for combat deaths and core captures
|
||||
for (const event of events) {
|
||||
if (event.type === 'bot_died' || event.type === 'core_captured') {
|
||||
hasCriticalEvent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for win probability shift >15%
|
||||
if (!hasCriticalEvent && replay.win_prob && turn > 0) {
|
||||
const prevProbs = replay.win_prob[turn - 1];
|
||||
const currProbs = replay.win_prob[turn];
|
||||
if (prevProbs && currProbs && prevProbs.length >= 2 && currProbs.length >= 2) {
|
||||
const delta = Math.abs(currProbs[0] - prevProbs[0]);
|
||||
if (delta > 0.15) {
|
||||
hasCriticalEvent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCriticalEvent) {
|
||||
hapticPulse(50);
|
||||
}
|
||||
};
|
||||
viewer.onDebugChange = (debug: Record<number, DebugInfo> | null) => {
|
||||
updateDebugDisplay(debug);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue