Release Healer Man 0.1.9

This commit is contained in:
phenom committed 2026-08-30 14:16:56 -04:00
1 parent c245a199b6
commit d060296ade
74 files changed
+5901 -334

No files matched your search

+120 -27
View File
@@ -25,6 +25,8 @@ const INITIAL_REPEAT_MS = 280;
const REPEAT_MS = 90;
const AXIS_THRESHOLD = 0.55;
const AXIS_STEPS = 128;
const GAMEPAD_DISCOVERY_INTERVAL_MS = 500;
const REPEATABLE_BUTTON_TOKENS = new Set(["Button12", "Button13", "Button14", "Button15"]);
const listeners = new Set<TokenListener>();
const movementListeners = new Set<MovementListener>();
@@ -39,26 +41,38 @@ let stopService: (() => void) | null = null;
function connectedGamepad(): Gamepad | null {
const gamepads = navigator.getGamepads?.();
if (!gamepads) return null;
return Array.from(gamepads).find((gamepad): gamepad is Gamepad => gamepad !== null) ?? null;
for (let index = 0; index < gamepads.length; index += 1) {
const gamepad = gamepads[index];
if (gamepad) return gamepad;
}
return null;
}
function tokensFor(gamepad: Gamepad, tokens: Set<string>): void {
tokens.clear();
gamepad.buttons.forEach((button, index) => {
for (let index = 0; index < gamepad.buttons.length; index += 1) {
const button = gamepad.buttons[index];
if (button.pressed || button.value > 0.65) tokens.add(`Button${index}`);
});
gamepad.axes.forEach((value, index) => {
}
for (let index = 0; index < gamepad.axes.length; index += 1) {
const value = gamepad.axes[index];
if (value <= -AXIS_THRESHOLD) tokens.add(`Axis${index}-`);
if (value >= AXIS_THRESHOLD) tokens.add(`Axis${index}+`);
});
}
}
function canRepeat(token: string): boolean {
return ["Button12", "Button13", "Button14", "Button15"].includes(token)
return REPEATABLE_BUTTON_TOKENS.has(token)
|| token.startsWith("Axis0")
|| token.startsWith("Axis1");
}
function tokenSetsMatch(left: ReadonlySet<string>, right: ReadonlySet<string>): boolean {
if (left.size !== right.size) return false;
for (const token of left) if (!right.has(token)) return false;
return true;
}
function isLayerModifier(token: string): boolean {
return token === "Button4" || token === "Button6";
}
@@ -114,14 +128,17 @@ function normalizedAxis(value: number): number {
}
export function setExternalControllerMovement(next: ControllerMovement): void {
const normalized = {
moveX: normalizedAxis(next.moveX),
moveY: normalizedAxis(next.moveY),
lookX: normalizedAxis(next.lookX),
lookY: normalizedAxis(next.lookY),
};
if (Object.keys(normalized).every((key) => normalized[key as keyof ControllerMovement] === movement[key as keyof ControllerMovement])) return;
movement = normalized;
const moveX = normalizedAxis(next.moveX);
const moveY = normalizedAxis(next.moveY);
const lookX = normalizedAxis(next.lookX);
const lookY = normalizedAxis(next.lookY);
if (
moveX === movement.moveX
&& moveY === movement.moveY
&& lookX === movement.lookX
&& lookY === movement.lookY
) return;
movement = { moveX, moveY, lookX, lookY };
for (const listener of movementListeners) listener(movement);
}
@@ -141,6 +158,8 @@ export function resetControllerState(): void {
export function startControllerInput(): () => void {
if (stopService) return stopService;
const webGamepadPolling = !Capacitor.isNativePlatform();
const onNativeToken = (event: Event) => {
const detail = (event as CustomEvent<ControllerTokenEvent>).detail;
if (!detail?.token) return;
@@ -157,25 +176,63 @@ export function startControllerInput(): () => void {
if (detail) setExternalControllerMovement(detail);
};
const onReset = () => resetControllerState();
const onVisibility = () => {
if (document.visibilityState !== "visible") resetControllerState();
};
window.addEventListener(NATIVE_TOKEN_EVENT, onNativeToken);
window.addEventListener(NATIVE_MOTION_EVENT, onNativeMotion);
window.addEventListener(NATIVE_RESET_EVENT, onReset);
document.addEventListener("visibilitychange", onVisibility);
let frame = 0;
let discoveryTimer: ReturnType<typeof setTimeout> | null = null;
let hadGamepad = false;
let activeGamepadIndex: number | null = null;
const canPollGamepads = () => document.visibilityState === "visible"
&& (typeof document.hasFocus !== "function" || document.hasFocus());
const cancelScheduledPoll = () => {
if (frame) cancelAnimationFrame(frame);
frame = 0;
if (discoveryTimer !== null) clearTimeout(discoveryTimer);
discoveryTimer = null;
};
const suspendPolling = () => {
cancelScheduledPoll();
if (hadGamepad) resetControllerState();
hadGamepad = false;
activeGamepadIndex = null;
};
const scheduleFrame = () => {
if (!webGamepadPolling || frame || !canPollGamepads()) return;
if (discoveryTimer !== null) clearTimeout(discoveryTimer);
discoveryTimer = null;
frame = requestAnimationFrame(poll);
};
const scheduleDiscovery = () => {
if (!webGamepadPolling || frame || discoveryTimer !== null || !canPollGamepads()) return;
discoveryTimer = setTimeout(() => {
discoveryTimer = null;
scheduleFrame();
}, GAMEPAD_DISCOVERY_INTERVAL_MS);
};
const poll = (now: number) => {
const gamepad = typeof document.hasFocus === "function" && !document.hasFocus()
? null
: connectedGamepad();
frame = 0;
if (!canPollGamepads()) {
suspendPolling();
return;
}
const gamepad = connectedGamepad();
if (!gamepad) {
if (hadGamepad) resetControllerState();
hadGamepad = false;
activeGamepadIndex = null;
scheduleDiscovery();
} else {
hadGamepad = true;
activeGamepadIndex = gamepad.index;
setExternalControllerMovement({
moveX: gamepad.axes[0] ?? 0,
moveY: gamepad.axes[1] ?? 0,
@@ -183,9 +240,11 @@ export function startControllerInput(): () => void {
lookY: gamepad.axes[3] ?? 0,
});
tokensFor(gamepad, currentTokens);
for (const transition of orderedControllerTokenTransitions(previousTokens, currentTokens)) {
emitControllerToken(transition);
if (transition.pressed) repeatAt.set(transition.token, now + INITIAL_REPEAT_MS);
if (!tokenSetsMatch(previousTokens, currentTokens)) {
for (const transition of orderedControllerTokenTransitions(previousTokens, currentTokens)) {
emitControllerToken(transition);
if (transition.pressed) repeatAt.set(transition.token, now + INITIAL_REPEAT_MS);
}
}
for (const token of currentTokens) {
if (!previousTokens.has(token) || !canRepeat(token)) continue;
@@ -199,17 +258,51 @@ export function startControllerInput(): () => void {
const scratch = previousTokens;
previousTokens = currentTokens;
currentTokens = scratch;
scheduleFrame();
}
frame = requestAnimationFrame(poll);
};
if (!Capacitor.isNativePlatform()) frame = requestAnimationFrame(poll);
const onGamepadConnected = () => scheduleFrame();
const onGamepadDisconnected = (event: Event) => {
const disconnectedIndex = (event as GamepadEvent).gamepad?.index;
if (disconnectedIndex === undefined || disconnectedIndex === activeGamepadIndex) {
resetControllerState();
hadGamepad = false;
activeGamepadIndex = null;
}
scheduleFrame();
};
const onVisibility = () => {
if (document.visibilityState === "visible") scheduleFrame();
else {
suspendPolling();
if (!webGamepadPolling) resetControllerState();
}
};
const onFocus = () => scheduleFrame();
const onBlur = () => suspendPolling();
document.addEventListener("visibilitychange", onVisibility);
if (webGamepadPolling) {
window.addEventListener("gamepadconnected", onGamepadConnected);
window.addEventListener("gamepaddisconnected", onGamepadDisconnected);
window.addEventListener("focus", onFocus);
window.addEventListener("blur", onBlur);
scheduleFrame();
}
stopService = () => {
window.removeEventListener(NATIVE_TOKEN_EVENT, onNativeToken);
window.removeEventListener(NATIVE_MOTION_EVENT, onNativeMotion);
window.removeEventListener(NATIVE_RESET_EVENT, onReset);
document.removeEventListener("visibilitychange", onVisibility);
if (frame) cancelAnimationFrame(frame);
if (webGamepadPolling) {
window.removeEventListener("gamepadconnected", onGamepadConnected);
window.removeEventListener("gamepaddisconnected", onGamepadDisconnected);
window.removeEventListener("focus", onFocus);
window.removeEventListener("blur", onBlur);
}
cancelScheduledPoll();
resetControllerState();
stopService = null;
};