diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 2f72af5..2af8ecd 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -20,7 +20,7 @@ onMounted(() => {
-
+
diff --git a/frontend/src/components/NavBar.vue b/frontend/src/components/NavBar.vue index 6d3cd6b..e40ea69 100644 --- a/frontend/src/components/NavBar.vue +++ b/frontend/src/components/NavBar.vue @@ -1,10 +1,11 @@ diff --git a/frontend/src/game/FightScene.ts b/frontend/src/game/FightScene.ts index 320c5fe..6d02aa9 100644 --- a/frontend/src/game/FightScene.ts +++ b/frontend/src/game/FightScene.ts @@ -8,7 +8,7 @@ import { ARENA_THEMES, spriteAnims, CHALLENGE_THEMED, TIER_ULTIMATES, CREATOR_MO import { CREATOR_ULTIMATE_CHANCE, CREATOR_THEMED_MOVE_RATIO, CRIT_THEMED_RATIO, CHOREOGRAPHY_THEMED_RATIO, CHOREOGRAPHY_GENERIC_CUTOFF, - TIER_ULTIMATE_CHANCES, isPerfMode, perfParticles, + TIER_ULTIMATE_CHANCES, isPerfMode, perfParticles, isReducedMotion, haptic, } from './fight/config' import { spawnSparks as _spawnSparks, spawnBulletHoles as _spawnBulletHoles, spawnExhaust as _spawnExhaust } from './fight/particles' import { glitchRGB as _glitchRGB, scanlineGlitch as _scanlineGlitch, vhsTracking as _vhsTracking, dimensionalShift as _dimensionalShift } from './fight/effects' @@ -168,6 +168,13 @@ export async function createFightScene(config: FightSceneConfig) { texFilter: 'nearest', }) + // Wrap k.shake to support reduced motion + haptic feedback + const _origShake = k.shake.bind(k) + k.shake = ((intensity: number) => { + if (intensity >= 3) haptic(Math.min(100, intensity * 8)) + if (!isReducedMotion()) _origShake(intensity) + }) as typeof k.shake + const colorsA = getBotColors(botA.seed) const colorsB = getBotColors(botB.seed) diff --git a/frontend/src/game/fight/config.ts b/frontend/src/game/fight/config.ts index 6bb8d04..2e9d539 100644 --- a/frontend/src/game/fight/config.ts +++ b/frontend/src/game/fight/config.ts @@ -29,11 +29,24 @@ export function setPerfMode(on: boolean): void { try { localStorage.setItem(PERF_KEY, on ? '1' : '0') } catch {} } -/** Scale a particle count down in performance mode (40%) */ +/** Scale a particle count down in performance mode (40%) or reduced motion (20%) */ export function perfParticles(count: number): number { + if (_reducedMotion) return Math.max(1, Math.round(count * 0.2)) return _perfMode ? Math.max(1, Math.round(count * 0.4)) : count } +// --- Reduced motion --- +const _reducedMotion = typeof window !== 'undefined' + ? window.matchMedia('(prefers-reduced-motion: reduce)').matches + : false + +export function isReducedMotion(): boolean { return _reducedMotion } + +// --- Haptic feedback --- +export function haptic(ms: number = 50): void { + try { navigator?.vibrate?.(ms) } catch { /* no vibration support */ } +} + // --- Choreography selection probabilities --- export const CREATOR_ULTIMATE_CHANCE = 0.5 // Creator ultimate chance (non-crit); 1.0 on crits export const CREATOR_THEMED_MOVE_RATIO = 0.6 // Creator uses creator-themed moves 60% of the time diff --git a/frontend/src/style.css b/frontend/src/style.css index 57a17fe..e8b14ca 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -186,3 +186,14 @@ .tier-4 { color: var(--color-neon-cyan); text-shadow: 0 0 8px rgba(0, 240, 255, 0.3); } .tier-5 { color: var(--color-neon-purple); text-shadow: 0 0 10px rgba(184, 61, 255, 0.5); } .tier-6 { color: var(--color-neon-pink); text-shadow: 0 0 12px rgba(255, 45, 123, 0.6); } + +/* Safe area for bottom nav (iPhone notch/home indicator) */ +.safe-area-bottom { padding-bottom: env(safe-area-inset-bottom, 0px); } + +/* Reduced motion: skip animations */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +}