216 lines
6.1 KiB
TypeScript
216 lines
6.1 KiB
TypeScript
export const ACTION_LAYERS = ["primary", "secondary", "tertiary"] as const;
|
||
|
||
export type ActionBindingLayer = typeof ACTION_LAYERS[number];
|
||
|
||
export const ACTION_CONTROLS = [
|
||
"face-top",
|
||
"face-right",
|
||
"face-bottom",
|
||
"face-left",
|
||
"r1",
|
||
"r2",
|
||
"dpad-left",
|
||
"dpad-right",
|
||
] as const;
|
||
|
||
export type ActionBindingControl = typeof ACTION_CONTROLS[number];
|
||
export type ActionBindingId = `${ActionBindingLayer}:${ActionBindingControl}`;
|
||
export type ActionBindings = Readonly<Record<ActionBindingId, string | null>>;
|
||
|
||
export interface ActionBindingControlDefinition {
|
||
readonly id: ActionBindingControl;
|
||
readonly label: string;
|
||
readonly gamepadLabel: string;
|
||
readonly keyboardLabel: string;
|
||
readonly gamepadToken: string;
|
||
readonly keyboardCode: string;
|
||
}
|
||
|
||
export const ACTION_LAYER_LABELS: Readonly<Record<ActionBindingLayer, string>> = Object.freeze({
|
||
primary: "Base",
|
||
secondary: "L1",
|
||
tertiary: "L2",
|
||
});
|
||
|
||
export const ACTION_CONTROL_DEFINITIONS: Readonly<Record<ActionBindingControl, ActionBindingControlDefinition>> = Object.freeze({
|
||
"face-top": {
|
||
id: "face-top",
|
||
label: "Triangle",
|
||
gamepadLabel: "△",
|
||
keyboardLabel: "3",
|
||
gamepadToken: "Button3",
|
||
keyboardCode: "Digit3",
|
||
},
|
||
"face-right": {
|
||
id: "face-right",
|
||
label: "Circle",
|
||
gamepadLabel: "○",
|
||
keyboardLabel: "4",
|
||
gamepadToken: "Button1",
|
||
keyboardCode: "Digit4",
|
||
},
|
||
"face-bottom": {
|
||
id: "face-bottom",
|
||
label: "Cross",
|
||
gamepadLabel: "×",
|
||
keyboardLabel: "1",
|
||
gamepadToken: "Button0",
|
||
keyboardCode: "Digit1",
|
||
},
|
||
"face-left": {
|
||
id: "face-left",
|
||
label: "Square",
|
||
gamepadLabel: "□",
|
||
keyboardLabel: "2",
|
||
gamepadToken: "Button2",
|
||
keyboardCode: "Digit2",
|
||
},
|
||
r1: {
|
||
id: "r1",
|
||
label: "R1",
|
||
gamepadLabel: "R1",
|
||
keyboardLabel: "5",
|
||
gamepadToken: "Button5",
|
||
keyboardCode: "Digit5",
|
||
},
|
||
r2: {
|
||
id: "r2",
|
||
label: "R2",
|
||
gamepadLabel: "R2",
|
||
keyboardLabel: "6",
|
||
gamepadToken: "Button7",
|
||
keyboardCode: "Digit6",
|
||
},
|
||
"dpad-left": {
|
||
id: "dpad-left",
|
||
label: "D-pad Left",
|
||
gamepadLabel: "←",
|
||
keyboardLabel: "7",
|
||
gamepadToken: "Button14",
|
||
keyboardCode: "Digit7",
|
||
},
|
||
"dpad-right": {
|
||
id: "dpad-right",
|
||
label: "D-pad Right",
|
||
gamepadLabel: "→",
|
||
keyboardLabel: "8",
|
||
gamepadToken: "Button15",
|
||
keyboardCode: "Digit8",
|
||
},
|
||
});
|
||
|
||
// Preserve the prototype's existing Cross, Square, Triangle progression for
|
||
// the first three abilities, then finish the face diamond before filling the
|
||
// remaining base and modifier slots.
|
||
const DEFAULT_ASSIGNMENT_CONTROLS: readonly ActionBindingControl[] = [
|
||
"face-bottom",
|
||
"face-left",
|
||
"face-top",
|
||
"face-right",
|
||
"r1",
|
||
"r2",
|
||
"dpad-left",
|
||
"dpad-right",
|
||
];
|
||
|
||
export function actionBindingId(
|
||
layer: ActionBindingLayer,
|
||
control: ActionBindingControl,
|
||
): ActionBindingId {
|
||
return `${layer}:${control}`;
|
||
}
|
||
|
||
export function emptyActionBindings(): Record<ActionBindingId, string | null> {
|
||
return Object.fromEntries(ACTION_LAYERS.flatMap((layer) => ACTION_CONTROLS.map((control) => [
|
||
actionBindingId(layer, control),
|
||
null,
|
||
]))) as Record<ActionBindingId, string | null>;
|
||
}
|
||
|
||
export function defaultActionBindings(abilityIds: readonly string[]): ActionBindings {
|
||
const bindings = emptyActionBindings();
|
||
const assignmentOrder = ACTION_LAYERS.flatMap((layer) => DEFAULT_ASSIGNMENT_CONTROLS.map((control) => (
|
||
actionBindingId(layer, control)
|
||
)));
|
||
abilityIds.slice(0, assignmentOrder.length).forEach((abilityId, index) => {
|
||
bindings[assignmentOrder[index]] = abilityId;
|
||
});
|
||
return bindings;
|
||
}
|
||
|
||
export function normalizeActionBindings(
|
||
value: unknown,
|
||
defaultAbilityIds: readonly string[],
|
||
allowedAbilityIds: readonly string[] = defaultAbilityIds,
|
||
): ActionBindings {
|
||
const defaults = defaultActionBindings(defaultAbilityIds);
|
||
if (!value || typeof value !== "object" || Array.isArray(value)) return defaults;
|
||
const source = value as Record<string, unknown>;
|
||
const allowed = new Set(allowedAbilityIds);
|
||
const bindings = { ...defaults };
|
||
for (const layer of ACTION_LAYERS) {
|
||
for (const control of ACTION_CONTROLS) {
|
||
const id = actionBindingId(layer, control);
|
||
if (!Object.prototype.hasOwnProperty.call(source, id)) continue;
|
||
const abilityId = source[id];
|
||
bindings[id] = abilityId === null
|
||
? null
|
||
: typeof abilityId === "string" && allowed.has(abilityId)
|
||
? abilityId
|
||
: null;
|
||
}
|
||
}
|
||
return bindings;
|
||
}
|
||
|
||
export function withActionBinding(
|
||
bindings: ActionBindings,
|
||
layer: ActionBindingLayer,
|
||
control: ActionBindingControl,
|
||
abilityId: string | null,
|
||
): ActionBindings {
|
||
return { ...bindings, [actionBindingId(layer, control)]: abilityId };
|
||
}
|
||
|
||
export function resetActionBindingLayer(
|
||
bindings: ActionBindings,
|
||
layer: ActionBindingLayer,
|
||
abilityIds: readonly string[],
|
||
): ActionBindings {
|
||
const defaults = defaultActionBindings(abilityIds);
|
||
const next = { ...bindings };
|
||
for (const control of ACTION_CONTROLS) {
|
||
const id = actionBindingId(layer, control);
|
||
next[id] = defaults[id];
|
||
}
|
||
return next;
|
||
}
|
||
|
||
export function actionLayerForModifiers(l1: boolean, l2: boolean): ActionBindingLayer {
|
||
if (l2) return "tertiary";
|
||
if (l1) return "secondary";
|
||
return "primary";
|
||
}
|
||
|
||
export function actionControlForControllerToken(token: string): ActionBindingControl | null {
|
||
return ACTION_CONTROLS.find((control) => ACTION_CONTROL_DEFINITIONS[control].gamepadToken === token) ?? null;
|
||
}
|
||
|
||
export function actionControlForKeyboardCode(code: string): ActionBindingControl | null {
|
||
return ACTION_CONTROLS.find((control) => ACTION_CONTROL_DEFINITIONS[control].keyboardCode === code) ?? null;
|
||
}
|
||
|
||
export function actionBindingChordLabel(
|
||
layer: ActionBindingLayer,
|
||
control: ActionBindingControl,
|
||
inputMode: "keyboard" | "gamepad" = "gamepad",
|
||
): string {
|
||
const definition = ACTION_CONTROL_DEFINITIONS[control];
|
||
if (inputMode === "keyboard") {
|
||
const modifier = layer === "secondary" ? "Shift + " : layer === "tertiary" ? "Alt + " : "";
|
||
return `${modifier}${definition.keyboardLabel}`;
|
||
}
|
||
const modifier = layer === "secondary" ? "L1 + " : layer === "tertiary" ? "L2 + " : "";
|
||
return `${modifier}${definition.label}`;
|
||
}
|