From 0aab42f0e9046b434911663c25cf25fb090fc418 Mon Sep 17 00:00:00 2001 From: Rune Harlyk Date: Tue, 14 Oct 2025 19:41:40 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=AE=20Maps=20controller=20buttons=20to?= =?UTF-8?q?=20modes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/lib/stores/gamepad.ts | 90 ++++++++++++++++------- app/src/routes/controller/Controls.svelte | 30 ++++++-- 2 files changed, 90 insertions(+), 30 deletions(-) diff --git a/app/src/lib/stores/gamepad.ts b/app/src/lib/stores/gamepad.ts index 5b82f7f..d22c899 100644 --- a/app/src/lib/stores/gamepad.ts +++ b/app/src/lib/stores/gamepad.ts @@ -5,43 +5,83 @@ export type GamepadState = { gamepads: Gamepad[] } +const DEADZONE = 0.15 +const dz = (x: number) => { + const a = Math.abs(x) + if (a < DEADZONE) return 0 + return ((a - DEADZONE) / (1 - DEADZONE)) * Math.sign(x) +} + +let raf = 0 +let running = false + export const gamepads = readable({ available: false, gamepads: [] }, set => { const update = () => { - const hasGamepadAPI = 'getGamepads' in navigator - if (!hasGamepadAPI) { - set({ available: false, gamepads: [] }) - return - } - - const gps = navigator.getGamepads?.() ?? [] - const validGamepads = gps.filter(Boolean) as Gamepad[] - set({ - available: true, - gamepads: validGamepads - }) + const pads = navigator.getGamepads?.() ?? [] + const list = Array.from(pads) + .map(p => p || null) + .filter(Boolean) as Gamepad[] + set({ available: 'getGamepads' in navigator, gamepads: list }) raf = requestAnimationFrame(update) } - window.addEventListener('gamepadconnected', update) - window.addEventListener('gamepaddisconnected', update) - let raf = requestAnimationFrame(update) + const onConnect = () => update() + const onDisconnect = () => update() + const onVis = () => { + if (document.hidden) { + running = false + cancelAnimationFrame(raf) + } else if (!running) { + running = true + raf = requestAnimationFrame(update) + } + } + + window.addEventListener('gamepadconnected', onConnect) + window.addEventListener('gamepaddisconnected', onDisconnect) + document.addEventListener('visibilitychange', onVis) + + running = true + raf = requestAnimationFrame(update) return () => { + running = false cancelAnimationFrame(raf) - window.removeEventListener('gamepadconnected', update) - window.removeEventListener('gamepaddisconnected', update) + window.removeEventListener('gamepadconnected', onConnect) + window.removeEventListener('gamepaddisconnected', onDisconnect) + document.removeEventListener('visibilitychange', onVis) } }) -export const gamepad = derived(gamepads, $gamepads => - $gamepads.available && $gamepads.gamepads.length > 0 ? $gamepads.gamepads[0] : null +export const gamepad = derived(gamepads, s => + s.available && s.gamepads.length ? s.gamepads[0] : null ) -export const gamepadAxes = derived(gamepad, $gamepad => $gamepad?.axes ?? [0, 0, 0, 0]) +export const hasGamepad = derived(gamepads, s => s.available && s.gamepads.length > 0) -export const gamepadButtons = derived(gamepad, $gamepad => $gamepad?.buttons ?? []) +export const gamepadAxes = derived(gamepad, g => (g ? g.axes.map(dz) : [0, 0, 0, 0])) -export const hasGamepad = derived( - gamepads, - $gamepads => $gamepads.available && $gamepads.gamepads.length > 0 -) +type ButtonEdge = { pressed: boolean; value: number; justPressed: boolean; justReleased: boolean } +const prev = new Map() + +export const gamepadButtons = derived(gamepad, g => g?.buttons ?? []) + +export const gamepadButtonsEdges = derived(gamepad, g => { + if (!g) return [] as ButtonEdge[] + const p = prev.get(g.index) || [] + const out = g.buttons.map((b, i): ButtonEdge => { + const pr = p[i] || { pressed: false, value: 0 } + const pressed = !!b.pressed || b.value > 0.5 + return { + pressed, + value: b.value, + justPressed: pressed && !pr.pressed, + justReleased: !pressed && pr.pressed + } + }) + prev.set( + g.index, + out.map(x => ({ pressed: x.pressed, value: x.value })) + ) + return out +}) diff --git a/app/src/routes/controller/Controls.svelte b/app/src/routes/controller/Controls.svelte index ec3ac08..79b00be 100644 --- a/app/src/routes/controller/Controls.svelte +++ b/app/src/routes/controller/Controls.svelte @@ -15,7 +15,12 @@ } from '$lib/stores' import type { vector } from '$lib/types/models' import { VerticalSlider } from '$lib/components/input' - import { gamepadAxes, hasGamepad } from '$lib/stores/gamepad' + import { + gamepadAxes, + gamepadButtons, + gamepadButtonsEdges, + hasGamepad + } from '$lib/stores/gamepad' import { notifications } from '$lib/components/toasts/notifications' let throttle = new throttler() @@ -37,10 +42,25 @@ }) // TODO React to button press - // $effect(() => { - // if ($gamepadButtons.length === 0) return - // - // }) + $effect(() => { + if (!$hasGamepad) return + const b = $gamepadButtonsEdges + if (!b.length) return + if (b[0]?.justPressed) mode.set(5) + if (b[1]?.justPressed) mode.set(4) + if (b[2]?.justPressed) mode.set(3) + if (b[3]?.justPressed) mode.set(0) + if (b[12]?.justPressed) + input.update(inputData => { + inputData['height'] = Math.min(inputData.height + 0.1, 1) + return inputData + }) + if (b[13]?.justPressed) + input.update(inputData => { + inputData['height'] = Math.min(inputData.height - 0.1, 1) + return inputData + }) + }) onMount(() => { left = nipplejs.create({