Controller: subtitle line actions, Button Style, keyboard-only mode pause, Alt+C fixes (#316)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
YashaNoosandClaude Opus 5.5 authored and GitHub committed 2026-11-02 21:41:46 -08:00
1 parent 6d0f37ac21
commit b99409bbcd
60 files changed
+1900 -219

No files matched your search

@@ -0,0 +1,112 @@
import { DPAD_AXIS_INDEX, DPAD_BUTTON_INDEX } from '../handlers/gamepad-controller.js';
import type {
ControllerDpadFallback,
ResolvedControllerAxisBinding,
ResolvedControllerConfig,
ResolvedControllerDiscreteBinding,
} from '../../types';
type ControllerBindings = ResolvedControllerConfig['bindings'];
type ControllerBindingActionId = keyof ControllerBindings;
/** What the checker needs from an Alt+C row definition. */
export type ControllerBindingIssueDefinition = {
id: ControllerBindingActionId;
label: string;
bindingType: 'discrete' | 'axis';
};
/**
* Rows that do nothing in SubMiner: no action reads `rightStickHorizontal`, so it cannot
* clash with anything.
*/
const INACTIVE_BINDINGS: ReadonlySet<ControllerBindingActionId> = new Set(['rightStickHorizontal']);
/** Row key of a stick binding's D-pad row in Alt+C. */
export function getControllerDpadRowKey(actionId: ControllerBindingActionId): string {
return `${actionId}:dpad`;
}
const buttonKey = (buttonIndex: number) => `button:${buttonIndex}`;
const axisKey = (axisIndex: number, direction: 'negative' | 'positive') =>
`axis:${axisIndex}:${direction}`;
function discreteInputs(binding: ResolvedControllerDiscreteBinding): string[] {
if (binding.kind === 'none') return [];
if ('buttonIndex' in binding) return [buttonKey(binding.buttonIndex)];
return [axisKey(binding.axisIndex, binding.direction)];
}
function stickInputs(binding: ResolvedControllerAxisBinding): string[] {
if (binding.kind === 'none') return [];
return [axisKey(binding.axisIndex, 'negative'), axisKey(binding.axisIndex, 'positive')];
}
/** The D-pad is read from buttons 12-15 and, on some raw pads, from a hat axis. */
function dpadInputs(fallback: ControllerDpadFallback): string[] {
if (fallback === 'horizontal') {
return [
buttonKey(DPAD_BUTTON_INDEX.left),
buttonKey(DPAD_BUTTON_INDEX.right),
axisKey(DPAD_AXIS_INDEX.horizontal, 'negative'),
axisKey(DPAD_AXIS_INDEX.horizontal, 'positive'),
];
}
if (fallback === 'vertical') {
return [
buttonKey(DPAD_BUTTON_INDEX.up),
buttonKey(DPAD_BUTTON_INDEX.down),
axisKey(DPAD_AXIS_INDEX.vertical, 'negative'),
axisKey(DPAD_AXIS_INDEX.vertical, 'positive'),
];
}
return [];
}
/**
* Warnings for the main Alt+C list, keyed by row (the action id, or its D-pad row key):
* a row warns when another row uses one of its inputs. Every such pair fires together at
* some point, since the situations nest (always, then keyboard-only mode, then popup open),
* so a shared input is always worth pointing out. Dialog layouts are checked separately:
* they pause these actions while open.
*/
export function findControllerBindingIssues(
definitions: readonly ControllerBindingIssueDefinition[],
bindings: ControllerBindings,
): Partial<Record<string, string>> {
const rows: { key: string; label: string; inputs: string[] }[] = [];
for (const definition of definitions) {
if (INACTIVE_BINDINGS.has(definition.id)) continue;
const binding = bindings[definition.id];
if (!binding) continue;
if (definition.bindingType === 'axis') {
const axisBinding = binding as ResolvedControllerAxisBinding;
rows.push({
key: definition.id,
label: `${definition.label} (Stick)`,
inputs: stickInputs(axisBinding),
});
rows.push({
key: getControllerDpadRowKey(definition.id),
label: `${definition.label} (D-pad)`,
inputs: dpadInputs(axisBinding.kind === 'none' ? 'none' : axisBinding.dpadFallback),
});
} else {
rows.push({
key: definition.id,
label: definition.label,
inputs: discreteInputs(binding as ResolvedControllerDiscreteBinding),
});
}
}
const issues: Partial<Record<string, string>> = {};
for (const row of rows) {
const inputs = new Set(row.inputs);
const clashes = rows
.filter((other) => other !== row && other.inputs.some((input) => inputs.has(input)))
.map((other) => other.label);
if (clashes.length > 0) issues[row.key] = `Same as ${clashes.join(', ')}`;
}
return issues;
}