mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-11-10 16:46:06 -08:00
Controller: layouts for the media timing review and duplicate card dialogs (#313)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
696462a82e
commit
6d0f37ac21
50 files changed
+4555
-46
No files matched your search
@@ -0,0 +1,157 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { CORE_DEFAULT_CONFIG } from '../../config/definitions/defaults-core';
|
||||
import {
|
||||
CONTROLLER_FIELD_GROUPING_ACTIONS,
|
||||
CONTROLLER_FIELD_GROUPING_SCREEN,
|
||||
type ControllerFieldGroupingStep,
|
||||
} from '../../shared/controller-field-grouping';
|
||||
import { findControllerScreenBindingIssues } from '../../shared/controller-review';
|
||||
import type { ControllerFieldGroupingAction, ResolvedControllerConfig } from '../../types';
|
||||
import {
|
||||
createControllerComboScreenLayout,
|
||||
formatControllerScreenHints,
|
||||
getControllerReviewInputContext,
|
||||
} from './controller-screen-layout.js';
|
||||
import type { GamepadLike } from './gamepad-controller.js';
|
||||
|
||||
const CONFIG: ResolvedControllerConfig = { ...CORE_DEFAULT_CONFIG.controller, enabled: true };
|
||||
const { repeatDelayMs, repeatIntervalMs } = CONFIG;
|
||||
|
||||
// Standard-layout indices; faces follow config.buttonIndices (Xbox positions by default).
|
||||
const BUTTON = { A: 0, B: 1, X: 2, Y: 3, UP: 12, DOWN: 13, LEFT: 14, RIGHT: 15 };
|
||||
type ButtonName = keyof typeof BUTTON;
|
||||
|
||||
function pad(pressed: ButtonName[] = []): GamepadLike {
|
||||
const buttons = Array.from({ length: 17 }, () => ({ value: 0, pressed: false }));
|
||||
for (const name of pressed) buttons[BUTTON[name]] = { value: 1, pressed: true };
|
||||
return { id: 'pad', index: 0, connected: true, mapping: 'standard', axes: [0, 0, 0, 0], buttons };
|
||||
}
|
||||
|
||||
/** The field grouping layout driving a fake dialog that follows the default flow. */
|
||||
function createFieldGroupingLayout(config = CONFIG) {
|
||||
const actions: ControllerFieldGroupingAction[] = [];
|
||||
const state: { step: ControllerFieldGroupingStep } = { step: 'select' };
|
||||
const layout = createControllerComboScreenLayout({
|
||||
screen: CONTROLLER_FIELD_GROUPING_SCREEN,
|
||||
getBindings: (resolved) => resolved.fieldGroupingBindings,
|
||||
getStep: () => state.step,
|
||||
dispatch: (action) => {
|
||||
actions.push(action);
|
||||
if (action === 'continue') state.step = 'preview';
|
||||
if (action === 'back') state.step = 'select';
|
||||
},
|
||||
});
|
||||
layout.activate(pad(), config, 0, { latchHeld: true });
|
||||
return { layout, actions, state };
|
||||
}
|
||||
|
||||
test('the default duplicate card layout binds every action but preview Cancel, without conflicts', () => {
|
||||
for (const action of CONTROLLER_FIELD_GROUPING_ACTIONS) {
|
||||
assert.equal(
|
||||
CONFIG.fieldGroupingBindings[action.id]?.kind,
|
||||
action.id === 'cancelMerge' ? 'none' : 'combo',
|
||||
action.id,
|
||||
);
|
||||
}
|
||||
for (const mapping of ['standard', ''] as const) {
|
||||
for (const faceButtonLabels of ['xbox', 'nintendo'] as const) {
|
||||
const context = getControllerReviewInputContext({ ...CONFIG, faceButtonLabels }, mapping);
|
||||
assert.deepEqual(
|
||||
findControllerScreenBindingIssues(
|
||||
CONTROLLER_FIELD_GROUPING_SCREEN,
|
||||
CONFIG.fieldGroupingBindings,
|
||||
context,
|
||||
),
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('duplicate card hints read like the agreed layout', () => {
|
||||
const hints = formatControllerScreenHints(
|
||||
CONTROLLER_FIELD_GROUPING_SCREEN,
|
||||
CONFIG.fieldGroupingBindings,
|
||||
CONFIG,
|
||||
'standard',
|
||||
);
|
||||
assert.deepEqual(hints, {
|
||||
selectOriginal: '◀',
|
||||
selectNew: '▶',
|
||||
toggleDeleteDuplicate: 'X',
|
||||
continue: 'A',
|
||||
cancel: 'B',
|
||||
confirmMerge: 'A',
|
||||
back: 'B',
|
||||
cancelMerge: null,
|
||||
toggleView: 'Y',
|
||||
scrollUp: '▲',
|
||||
scrollDown: '▼',
|
||||
});
|
||||
});
|
||||
|
||||
test('the D-pad picks the card and X toggles delete on the first step', () => {
|
||||
const { layout, actions } = createFieldGroupingLayout();
|
||||
|
||||
layout.poll(pad(['RIGHT']), CONFIG, 10);
|
||||
layout.poll(pad(['RIGHT']), CONFIG, 2_000);
|
||||
layout.poll(pad(), CONFIG, 2_010);
|
||||
layout.poll(pad(['LEFT']), CONFIG, 2_020);
|
||||
layout.poll(pad(['X']), CONFIG, 2_030);
|
||||
// Up/down scroll only on the preview.
|
||||
layout.poll(pad(['DOWN']), CONFIG, 2_040);
|
||||
|
||||
assert.deepEqual(actions, ['selectNew', 'selectOriginal', 'toggleDeleteDuplicate']);
|
||||
});
|
||||
|
||||
test('B on the preview goes back; holding it does not also cancel', () => {
|
||||
const { layout, actions, state } = createFieldGroupingLayout();
|
||||
|
||||
layout.poll(pad(['A']), CONFIG, 10);
|
||||
assert.equal(state.step, 'preview');
|
||||
// A still held after the step change does not confirm the merge.
|
||||
layout.poll(pad(['A']), CONFIG, 20);
|
||||
layout.poll(pad(), CONFIG, 30);
|
||||
layout.poll(pad(['B']), CONFIG, 40);
|
||||
assert.equal(state.step, 'select');
|
||||
// B held across the step change is not a fresh press on the first step.
|
||||
layout.poll(pad(['B']), CONFIG, 1_000);
|
||||
layout.poll(pad(), CONFIG, 1_010);
|
||||
layout.poll(pad(['B']), CONFIG, 1_020);
|
||||
|
||||
assert.deepEqual(actions, ['continue', 'back', 'cancel']);
|
||||
});
|
||||
|
||||
test('preview scrolling repeats while the D-pad is held', () => {
|
||||
const { layout, actions, state } = createFieldGroupingLayout();
|
||||
state.step = 'preview';
|
||||
|
||||
layout.poll(pad(['DOWN']), CONFIG, 0);
|
||||
layout.poll(pad(['DOWN']), CONFIG, repeatDelayMs - 1);
|
||||
layout.poll(pad(['DOWN']), CONFIG, repeatDelayMs);
|
||||
layout.poll(pad(['DOWN']), CONFIG, repeatDelayMs + repeatIntervalMs);
|
||||
layout.poll(pad(), CONFIG, repeatDelayMs + repeatIntervalMs + 1);
|
||||
layout.poll(pad(['Y']), CONFIG, 5_000);
|
||||
layout.poll(pad(['Y']), CONFIG, 6_000);
|
||||
|
||||
assert.deepEqual(actions, ['scrollDown', 'scrollDown', 'scrollDown', 'toggleView']);
|
||||
});
|
||||
|
||||
test('remapped duplicate card bindings replace the defaults', () => {
|
||||
const config: ResolvedControllerConfig = {
|
||||
...CONFIG,
|
||||
fieldGroupingBindings: {
|
||||
...CONFIG.fieldGroupingBindings,
|
||||
back: { kind: 'none' },
|
||||
cancelMerge: { kind: 'combo', press: { kind: 'face', letter: 'b' }, hold: null },
|
||||
},
|
||||
};
|
||||
const { layout, actions, state } = createFieldGroupingLayout(config);
|
||||
state.step = 'preview';
|
||||
|
||||
layout.poll(pad(['B']), config, 10);
|
||||
|
||||
assert.deepEqual(actions, ['cancelMerge']);
|
||||
});
|
||||
Reference in new issue
Block a user