mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-08-07 21:58:27 +02:00
Initial commit
This commit is contained in:
98
src/modules/mkb/definitions.ts
Normal file
98
src/modules/mkb/definitions.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
export const GamepadKey: {[index: string | number]: string | number} = {};
|
||||
GamepadKey[GamepadKey.A = 0] = 'A';
|
||||
GamepadKey[GamepadKey.B = 1] = 'B';
|
||||
GamepadKey[GamepadKey.X = 2] = 'X';
|
||||
GamepadKey[GamepadKey.Y = 3] = 'Y';
|
||||
GamepadKey[GamepadKey.LB = 4] = 'LB';
|
||||
GamepadKey[GamepadKey.RB = 5] = 'RB';
|
||||
GamepadKey[GamepadKey.LT = 6] = 'LT';
|
||||
GamepadKey[GamepadKey.RT = 7] = 'RT';
|
||||
GamepadKey[GamepadKey.SELECT = 8] = 'SELECT';
|
||||
GamepadKey[GamepadKey.START = 9] = 'START';
|
||||
GamepadKey[GamepadKey.L3 = 10] = 'L3';
|
||||
GamepadKey[GamepadKey.R3 = 11] = 'R3';
|
||||
GamepadKey[GamepadKey.UP = 12] = 'UP';
|
||||
GamepadKey[GamepadKey.DOWN = 13] = 'DOWN';
|
||||
GamepadKey[GamepadKey.LEFT = 14] = 'LEFT';
|
||||
GamepadKey[GamepadKey.RIGHT = 15] = 'RIGHT';
|
||||
GamepadKey[GamepadKey.HOME = 16] = 'HOME';
|
||||
|
||||
GamepadKey[GamepadKey.LS_UP = 100] = 'LS_UP';
|
||||
GamepadKey[GamepadKey.LS_DOWN = 101] = 'LS_DOWN';
|
||||
GamepadKey[GamepadKey.LS_LEFT = 102] = 'LS_LEFT';
|
||||
GamepadKey[GamepadKey.LS_RIGHT = 103] = 'LS_RIGHT';
|
||||
GamepadKey[GamepadKey.RS_UP = 200] = 'RS_UP';
|
||||
GamepadKey[GamepadKey.RS_DOWN = 201] = 'RS_DOWN';
|
||||
GamepadKey[GamepadKey.RS_LEFT = 202] = 'RS_LEFT';
|
||||
GamepadKey[GamepadKey.RS_RIGHT = 203] = 'RS_RIGHT';
|
||||
|
||||
|
||||
export const GamepadKeyName: {[index: string | number]: string[]} = {
|
||||
[GamepadKey.A]: ['A', '⇓'],
|
||||
[GamepadKey.B]: ['B', '⇒'],
|
||||
[GamepadKey.X]: ['X', '⇐'],
|
||||
[GamepadKey.Y]: ['Y', '⇑'],
|
||||
|
||||
[GamepadKey.LB]: ['LB', '↘'],
|
||||
[GamepadKey.RB]: ['RB', '↙'],
|
||||
[GamepadKey.LT]: ['LT', '↖'],
|
||||
[GamepadKey.RT]: ['RT', '↗'],
|
||||
|
||||
[GamepadKey.SELECT]: ['Select', '⇺'],
|
||||
[GamepadKey.START]: ['Start', '⇻'],
|
||||
[GamepadKey.HOME]: ['Home', ''],
|
||||
|
||||
[GamepadKey.UP]: ['D-Pad Up', '≻'],
|
||||
[GamepadKey.DOWN]: ['D-Pad Down', '≽'],
|
||||
[GamepadKey.LEFT]: ['D-Pad Left', '≺'],
|
||||
[GamepadKey.RIGHT]: ['D-Pad Right', '≼'],
|
||||
|
||||
[GamepadKey.L3]: ['L3', '↺'],
|
||||
[GamepadKey.LS_UP]: ['Left Stick Up', '↾'],
|
||||
[GamepadKey.LS_DOWN]: ['Left Stick Down', '⇂'],
|
||||
[GamepadKey.LS_LEFT]: ['Left Stick Left', '↼'],
|
||||
[GamepadKey.LS_RIGHT]: ['Left Stick Right', '⇀'],
|
||||
|
||||
[GamepadKey.R3]: ['R3', '↻'],
|
||||
[GamepadKey.RS_UP]: ['Right Stick Up', '↿'],
|
||||
[GamepadKey.RS_DOWN]: ['Right Stick Down', '⇃'],
|
||||
[GamepadKey.RS_LEFT]: ['Right Stick Left', '↽'],
|
||||
[GamepadKey.RS_RIGHT]: ['Right Stick Right', '⇁'],
|
||||
};
|
||||
|
||||
|
||||
export enum GamepadStick {
|
||||
LEFT = 0,
|
||||
RIGHT = 1,
|
||||
};
|
||||
|
||||
export enum MouseButtonCode {
|
||||
LEFT_CLICK = 'Mouse0',
|
||||
RIGHT_CLICK = 'Mouse2',
|
||||
MIDDLE_CLICK = 'Mouse1',
|
||||
};
|
||||
|
||||
export const MouseMapTo: {[index: string | number]: string | number} = {};
|
||||
MouseMapTo[MouseMapTo.OFF = 0] = 'OFF';
|
||||
MouseMapTo[MouseMapTo.LS = 1] = 'LS';
|
||||
MouseMapTo[MouseMapTo.RS = 2] = 'RS';
|
||||
|
||||
|
||||
export enum WheelCode {
|
||||
SCROLL_UP = 'ScrollUp',
|
||||
SCROLL_DOWN = 'ScrollDown',
|
||||
SCROLL_LEFT = 'ScrollLeft',
|
||||
SCROLL_RIGHT = 'ScrollRight',
|
||||
};
|
||||
|
||||
export enum MkbPresetKey {
|
||||
MOUSE_MAP_TO = 'map_to',
|
||||
|
||||
MOUSE_SENSITIVITY_X = 'sensitivity_x',
|
||||
MOUSE_SENSITIVITY_Y = 'sensitivity_y',
|
||||
|
||||
MOUSE_DEADZONE_COUNTERWEIGHT = 'deadzone_counterweight',
|
||||
|
||||
MOUSE_STICK_DECAY_STRENGTH = 'stick_decay_strength',
|
||||
MOUSE_STICK_DECAY_MIN = 'stick_decay_min',
|
||||
}
|
67
src/modules/mkb/key-helper.ts
Normal file
67
src/modules/mkb/key-helper.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { MouseButtonCode, WheelCode } from "./definitions";
|
||||
|
||||
export class KeyHelper {
|
||||
static #NON_PRINTABLE_KEYS = {
|
||||
'Backquote': '`',
|
||||
|
||||
// Mouse buttons
|
||||
[MouseButtonCode.LEFT_CLICK]: 'Left Click',
|
||||
[MouseButtonCode.RIGHT_CLICK]: 'Right Click',
|
||||
[MouseButtonCode.MIDDLE_CLICK]: 'Middle Click',
|
||||
|
||||
[WheelCode.SCROLL_UP]: 'Scroll Up',
|
||||
[WheelCode.SCROLL_DOWN]: 'Scroll Down',
|
||||
[WheelCode.SCROLL_LEFT]: 'Scroll Left',
|
||||
[WheelCode.SCROLL_RIGHT]: 'Scroll Right',
|
||||
};
|
||||
|
||||
static getKeyFromEvent(e: Event) {
|
||||
let code;
|
||||
let name;
|
||||
|
||||
if (e instanceof KeyboardEvent) {
|
||||
code = e.code;
|
||||
} else if (e instanceof MouseEvent) {
|
||||
code = 'Mouse' + e.button;
|
||||
} else if (e instanceof WheelEvent) {
|
||||
if (e.deltaY < 0) {
|
||||
code = WheelCode.SCROLL_UP;
|
||||
} else if (e.deltaY > 0) {
|
||||
code = WheelCode.SCROLL_DOWN;
|
||||
} else if (e.deltaX < 0) {
|
||||
code = WheelCode.SCROLL_LEFT;
|
||||
} else {
|
||||
code = WheelCode.SCROLL_RIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
if (code) {
|
||||
name = KeyHelper.codeToKeyName(code);
|
||||
}
|
||||
|
||||
return code ? {code, name} : null;
|
||||
}
|
||||
|
||||
static codeToKeyName(code: string) {
|
||||
return (
|
||||
// @ts-ignore
|
||||
KeyHelper.#NON_PRINTABLE_KEYS[code]
|
||||
||
|
||||
(code.startsWith('Key') && code.substring(3))
|
||||
||
|
||||
(code.startsWith('Digit') && code.substring(5))
|
||||
||
|
||||
(code.startsWith('Numpad') && ('Numpad ' + code.substring(6)))
|
||||
||
|
||||
(code.startsWith('Arrow') && ('Arrow ' + code.substring(5)))
|
||||
||
|
||||
(code.endsWith('Lock') && (code.replace('Lock', ' Lock')))
|
||||
||
|
||||
(code.endsWith('Left') && ('Left ' + code.replace('Left', '')))
|
||||
||
|
||||
(code.endsWith('Right') && ('Right ' + code.replace('Right', '')))
|
||||
||
|
||||
code
|
||||
);
|
||||
}
|
||||
}
|
467
src/modules/mkb/mkb-handler.ts
Normal file
467
src/modules/mkb/mkb-handler.ts
Normal file
@@ -0,0 +1,467 @@
|
||||
import { MkbPreset } from "./mkb-preset";
|
||||
import { GamepadKey, MkbPresetKey, GamepadStick, MouseMapTo } from "./definitions";
|
||||
import { createButton, Icon, ButtonStyle, CE } from "../../utils/html";
|
||||
import { BxEvent } from "../bx-event";
|
||||
import { PrefKey, getPref } from "../preferences";
|
||||
import { Toast } from "../../utils/toast";
|
||||
import { t } from "../translation";
|
||||
import { LocalDb } from "../../utils/local-db";
|
||||
import { KeyHelper } from "./key-helper";
|
||||
|
||||
/*
|
||||
This class uses some code from Yuzu emulator to handle mouse's movements
|
||||
Source: https://github.com/yuzu-emu/yuzu-mainline/blob/master/src/input_common/drivers/mouse.cpp
|
||||
*/
|
||||
export class MkbHandler {
|
||||
static #instance: MkbHandler;
|
||||
static get INSTANCE() {
|
||||
if (!MkbHandler.#instance) {
|
||||
MkbHandler.#instance = new MkbHandler();
|
||||
}
|
||||
|
||||
return MkbHandler.#instance;
|
||||
}
|
||||
|
||||
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);
|
||||
|
||||
static get DEFAULT_PANNING_SENSITIVITY() { return 0.0010; }
|
||||
static get DEFAULT_STICK_SENSITIVITY() { return 0.0006; }
|
||||
static get DEFAULT_DEADZONE_COUNTERWEIGHT() { return 0.01; }
|
||||
static get MAXIMUM_STICK_RANGE() { return 1.1; }
|
||||
|
||||
static VIRTUAL_GAMEPAD_ID = 'Xbox 360 Controller';
|
||||
|
||||
#VIRTUAL_GAMEPAD = {
|
||||
id: MkbHandler.VIRTUAL_GAMEPAD_ID,
|
||||
index: 3,
|
||||
connected: false,
|
||||
hapticActuators: null,
|
||||
mapping: 'standard',
|
||||
|
||||
axes: [0, 0, 0, 0],
|
||||
buttons: new Array(17).fill(null).map(() => ({pressed: false, value: 0})),
|
||||
timestamp: performance.now(),
|
||||
|
||||
vibrationActuator: null,
|
||||
};
|
||||
#nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator);
|
||||
|
||||
#enabled = false;
|
||||
|
||||
#prevWheelCode = null;
|
||||
#wheelStoppedTimeout?: number | null;
|
||||
|
||||
#detectMouseStoppedTimeout?: number | null;
|
||||
#allowStickDecaying = false;
|
||||
|
||||
#$message?: HTMLElement;
|
||||
|
||||
#STICK_MAP: {[index: keyof typeof GamepadKey]: (number | number[])[]};
|
||||
#LEFT_STICK_X: number[] = [];
|
||||
#LEFT_STICK_Y: number[] = [];
|
||||
#RIGHT_STICK_X: number[] = [];
|
||||
#RIGHT_STICK_Y: number[] = [];
|
||||
|
||||
constructor() {
|
||||
this.#STICK_MAP = {
|
||||
[GamepadKey.LS_LEFT]: [this.#LEFT_STICK_X, 0, -1],
|
||||
[GamepadKey.LS_RIGHT]: [this.#LEFT_STICK_X, 0, 1],
|
||||
[GamepadKey.LS_UP]: [this.#LEFT_STICK_Y, 1, -1],
|
||||
[GamepadKey.LS_DOWN]: [this.#LEFT_STICK_Y, 1, 1],
|
||||
|
||||
[GamepadKey.RS_LEFT]: [this.#RIGHT_STICK_X, 2, -1],
|
||||
[GamepadKey.RS_RIGHT]: [this.#RIGHT_STICK_X, 2, 1],
|
||||
[GamepadKey.RS_UP]: [this.#RIGHT_STICK_Y, 3, -1],
|
||||
[GamepadKey.RS_DOWN]: [this.#RIGHT_STICK_Y, 3, 1],
|
||||
};
|
||||
}
|
||||
|
||||
#patchedGetGamepads = () => {
|
||||
const gamepads = this.#nativeGetGamepads() || [];
|
||||
(gamepads as any)[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD;
|
||||
|
||||
return gamepads;
|
||||
}
|
||||
|
||||
#getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD;
|
||||
|
||||
#updateStick(stick: GamepadStick, x: number, y: number) {
|
||||
const virtualGamepad = this.#getVirtualGamepad();
|
||||
virtualGamepad.axes[stick * 2] = x;
|
||||
virtualGamepad.axes[stick * 2 + 1] = y;
|
||||
|
||||
virtualGamepad.timestamp = performance.now();
|
||||
}
|
||||
|
||||
#getStickAxes(stick: GamepadStick) {
|
||||
const virtualGamepad = this.#getVirtualGamepad();
|
||||
return {
|
||||
x: virtualGamepad.axes[stick * 2],
|
||||
y: virtualGamepad.axes[stick * 2 + 1],
|
||||
};
|
||||
}
|
||||
|
||||
#vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2);
|
||||
|
||||
#disableContextMenu = (e: Event) => e.preventDefault();
|
||||
|
||||
#resetGamepad = () => {
|
||||
const gamepad = this.#getVirtualGamepad();
|
||||
|
||||
// Reset axes
|
||||
gamepad.axes = [0, 0, 0, 0];
|
||||
|
||||
// Reset buttons
|
||||
for (const button of gamepad.buttons) {
|
||||
button.pressed = false;
|
||||
button.value = 0;
|
||||
}
|
||||
|
||||
gamepad.timestamp = performance.now();
|
||||
}
|
||||
|
||||
#pressButton = (buttonIndex: number, pressed: boolean) => {
|
||||
const virtualGamepad = this.#getVirtualGamepad();
|
||||
|
||||
if (buttonIndex >= 100) {
|
||||
let [valueArr, axisIndex, fullValue] = this.#STICK_MAP[buttonIndex];
|
||||
valueArr = valueArr as number[];
|
||||
axisIndex = axisIndex as number;
|
||||
|
||||
// Remove old index of the array
|
||||
for (let i = valueArr.length - 1; i >= 0; i--) {
|
||||
if (valueArr[i] === buttonIndex) {
|
||||
valueArr.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
pressed && valueArr.push(buttonIndex);
|
||||
|
||||
let value;
|
||||
if (valueArr.length) {
|
||||
// Get value of the last key of the axis
|
||||
value = this.#STICK_MAP[valueArr[valueArr.length - 1]][2] as number;
|
||||
} else {
|
||||
value = 0;
|
||||
}
|
||||
|
||||
virtualGamepad.axes[axisIndex] = value;
|
||||
} else {
|
||||
virtualGamepad.buttons[buttonIndex].pressed = pressed;
|
||||
virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0;
|
||||
}
|
||||
|
||||
virtualGamepad.timestamp = performance.now();
|
||||
}
|
||||
|
||||
#onKeyboardEvent = (e: KeyboardEvent) => {
|
||||
const isKeyDown = e.type === 'keydown';
|
||||
|
||||
// Toggle MKB feature
|
||||
if (isKeyDown && e.code === 'F8') {
|
||||
e.preventDefault();
|
||||
this.toggle();
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code];
|
||||
if (typeof buttonIndex === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore repeating keys
|
||||
if (e.repeat) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
this.#pressButton(buttonIndex, isKeyDown);
|
||||
}
|
||||
|
||||
#onMouseEvent = e => {
|
||||
const isMouseDown = e.type === 'mousedown';
|
||||
const key = KeyHelper.getKeyFromEvent(e);
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code];
|
||||
if (typeof buttonIndex === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
this.#pressButton(buttonIndex, isMouseDown);
|
||||
}
|
||||
|
||||
#onWheelEvent = (e: WheelEvent) => {
|
||||
const key = KeyHelper.getKeyFromEvent(e);
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code];
|
||||
if (typeof buttonIndex === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) {
|
||||
this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout);
|
||||
this.#pressButton(buttonIndex, true);
|
||||
}
|
||||
|
||||
this.#wheelStoppedTimeout = setTimeout(e => {
|
||||
this.#prevWheelCode = null;
|
||||
this.#pressButton(buttonIndex, false);
|
||||
}, 20);
|
||||
}
|
||||
|
||||
#decayStick = () => {
|
||||
if (!this.#allowStickDecaying) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO];
|
||||
if (mouseMapTo === MouseMapTo.OFF) {
|
||||
return;
|
||||
}
|
||||
|
||||
const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
|
||||
|
||||
const virtualGamepad = this.#getVirtualGamepad();
|
||||
let { x, y } = this.#getStickAxes(analog);
|
||||
const length = this.#vectorLength(x, y);
|
||||
|
||||
const clampedLength = Math.min(1.0, length);
|
||||
const decayStrength = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH];
|
||||
const decay = 1 - clampedLength * clampedLength * decayStrength;
|
||||
const minDecay = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_STICK_DECAY_MIN];
|
||||
const clampedDecay = Math.min(1 - minDecay, decay);
|
||||
|
||||
x *= clampedDecay;
|
||||
y *= clampedDecay;
|
||||
|
||||
const deadzoneCounterweight = 20 * MkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT;
|
||||
if (Math.abs(x) <= deadzoneCounterweight && Math.abs(y) <= deadzoneCounterweight) {
|
||||
x = 0;
|
||||
y = 0;
|
||||
}
|
||||
|
||||
if (this.#allowStickDecaying) {
|
||||
this.#updateStick(analog, x, y);
|
||||
|
||||
(x !== 0 || y !== 0) && requestAnimationFrame(this.#decayStick);
|
||||
}
|
||||
}
|
||||
|
||||
#onMouseStopped = (e: MouseEvent) => {
|
||||
this.#allowStickDecaying = true;
|
||||
requestAnimationFrame(this.#decayStick);
|
||||
}
|
||||
|
||||
#onMouseMoveEvent = (e: MouseEvent) => {
|
||||
// TODO: optimize this
|
||||
const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO];
|
||||
if (mouseMapTo === MouseMapTo.OFF) {
|
||||
// Ignore mouse movements
|
||||
return;
|
||||
}
|
||||
|
||||
this.#allowStickDecaying = false;
|
||||
this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout);
|
||||
this.#detectMouseStoppedTimeout = setTimeout(this.#onMouseStopped.bind(this, e), 100);
|
||||
|
||||
const deltaX = e.movementX;
|
||||
const deltaY = e.movementY;
|
||||
|
||||
const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT];
|
||||
|
||||
let x = deltaX * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X];
|
||||
let y = deltaY * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y];
|
||||
|
||||
let length = this.#vectorLength(x, y);
|
||||
if (length !== 0 && length < deadzoneCounterweight) {
|
||||
x *= deadzoneCounterweight / length;
|
||||
y *= deadzoneCounterweight / length;
|
||||
} else if (length > MkbHandler.MAXIMUM_STICK_RANGE) {
|
||||
x *= MkbHandler.MAXIMUM_STICK_RANGE / length;
|
||||
y *= MkbHandler.MAXIMUM_STICK_RANGE / length;
|
||||
}
|
||||
|
||||
const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
|
||||
this.#updateStick(analog, x, y);
|
||||
}
|
||||
|
||||
toggle = () => {
|
||||
this.#enabled = !this.#enabled;
|
||||
this.#enabled ? document.pointerLockElement && this.start() : this.stop();
|
||||
|
||||
Toast.show(t('mouse-and-keyboard'), t(this.#enabled ? 'enabled' : 'disabled'), {instant: true});
|
||||
|
||||
if (this.#enabled) {
|
||||
!document.pointerLockElement && this.#waitForPointerLock(true);
|
||||
} else {
|
||||
this.#waitForPointerLock(false);
|
||||
document.pointerLockElement && document.exitPointerLock();
|
||||
}
|
||||
}
|
||||
|
||||
#getCurrentPreset = () => {
|
||||
return new Promise(resolve => {
|
||||
const presetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
|
||||
LocalDb.INSTANCE.getPreset(presetId).then(preset => {
|
||||
resolve(preset ? preset : MkbPreset.DEFAULT_PRESET);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
refreshPresetData = () => {
|
||||
this.#getCurrentPreset().then(preset => {
|
||||
this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset.data);
|
||||
this.#resetGamepad();
|
||||
});
|
||||
}
|
||||
|
||||
#onPointerLockChange = e => {
|
||||
if (this.#enabled && !document.pointerLockElement) {
|
||||
this.stop();
|
||||
this.#waitForPointerLock(true);
|
||||
}
|
||||
}
|
||||
|
||||
#onPointerLockError = e => {
|
||||
console.log(e);
|
||||
this.stop();
|
||||
}
|
||||
|
||||
#onActivatePointerLock = () => {
|
||||
if (!document.pointerLockElement) {
|
||||
document.body.requestPointerLock();
|
||||
}
|
||||
|
||||
this.#waitForPointerLock(false);
|
||||
this.start();
|
||||
}
|
||||
|
||||
#waitForPointerLock = (wait: boolean) => {
|
||||
this.#$message && this.#$message.classList.toggle('bx-gone', !wait);
|
||||
}
|
||||
|
||||
#onStreamMenuShown = () => {
|
||||
this.#enabled && this.#waitForPointerLock(false);
|
||||
}
|
||||
|
||||
#onStreamMenuHidden = () => {
|
||||
this.#enabled && this.#waitForPointerLock(true);
|
||||
}
|
||||
|
||||
init = () => {
|
||||
this.refreshPresetData();
|
||||
this.#enabled = true;
|
||||
|
||||
window.addEventListener('keydown', this.#onKeyboardEvent);
|
||||
|
||||
document.addEventListener('pointerlockchange', this.#onPointerLockChange);
|
||||
document.addEventListener('pointerlockerror', this.#onPointerLockError);
|
||||
|
||||
this.#$message = CE('div', {'class': 'bx-mkb-pointer-lock-msg bx-gone'},
|
||||
createButton({
|
||||
icon: Icon.MOUSE_SETTINGS,
|
||||
style: ButtonStyle.PRIMARY as number,
|
||||
onClick: e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
showStreamSettings('mkb');
|
||||
},
|
||||
}),
|
||||
CE('div', {},
|
||||
CE('p', {}, t('mkb-click-to-activate')),
|
||||
CE('p', {}, t<any>('press-key-to-toggle-mkb')({key: 'F8'})),
|
||||
),
|
||||
);
|
||||
|
||||
this.#$message.addEventListener('click', this.#onActivatePointerLock);
|
||||
document.documentElement.appendChild(this.#$message);
|
||||
|
||||
window.addEventListener(BxEvent.STREAM_MENU_SHOWN, this.#onStreamMenuShown);
|
||||
window.addEventListener(BxEvent.STREAM_MENU_HIDDEN, this.#onStreamMenuHidden);
|
||||
|
||||
this.#waitForPointerLock(true);
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
this.#enabled = false;
|
||||
this.stop();
|
||||
|
||||
this.#waitForPointerLock(false);
|
||||
document.pointerLockElement && document.exitPointerLock();
|
||||
|
||||
window.removeEventListener('keydown', this.#onKeyboardEvent);
|
||||
|
||||
document.removeEventListener('pointerlockchange', this.#onPointerLockChange);
|
||||
document.removeEventListener('pointerlockerror', this.#onPointerLockError);
|
||||
|
||||
window.removeEventListener(BxEvent.STREAM_MENU_SHOWN, this.#onStreamMenuShown);
|
||||
window.removeEventListener(BxEvent.STREAM_MENU_HIDDEN, this.#onStreamMenuHidden);
|
||||
}
|
||||
|
||||
start = () => {
|
||||
window.navigator.getGamepads = this.#patchedGetGamepads;
|
||||
|
||||
this.#resetGamepad();
|
||||
|
||||
window.addEventListener('keyup', this.#onKeyboardEvent);
|
||||
|
||||
window.addEventListener('mousemove', this.#onMouseMoveEvent);
|
||||
window.addEventListener('mousedown', this.#onMouseEvent);
|
||||
window.addEventListener('mouseup', this.#onMouseEvent);
|
||||
window.addEventListener('wheel', this.#onWheelEvent);
|
||||
window.addEventListener('contextmenu', this.#disableContextMenu);
|
||||
|
||||
// Dispatch "gamepadconnected" event
|
||||
const virtualGamepad = this.#getVirtualGamepad();
|
||||
virtualGamepad.connected = true;
|
||||
virtualGamepad.timestamp = performance.now();
|
||||
|
||||
BxEvent.dispatch(window, 'gamepadconnected', {
|
||||
gamepad: virtualGamepad,
|
||||
});
|
||||
}
|
||||
|
||||
stop = () => {
|
||||
|
||||
// Dispatch "gamepaddisconnected" event
|
||||
const virtualGamepad = this.#getVirtualGamepad();
|
||||
virtualGamepad.connected = false;
|
||||
virtualGamepad.timestamp = performance.now();
|
||||
|
||||
BxEvent.dispatch(window, 'gamepaddisconnected', {
|
||||
gamepad: virtualGamepad,
|
||||
});
|
||||
|
||||
window.navigator.getGamepads = this.#nativeGetGamepads;
|
||||
|
||||
this.#resetGamepad();
|
||||
|
||||
window.removeEventListener('keyup', this.#onKeyboardEvent);
|
||||
|
||||
window.removeEventListener('mousemove', this.#onMouseMoveEvent);
|
||||
window.removeEventListener('mousedown', this.#onMouseEvent);
|
||||
window.removeEventListener('mouseup', this.#onMouseEvent);
|
||||
window.removeEventListener('wheel', this.#onWheelEvent);
|
||||
window.removeEventListener('contextmenu', this.#disableContextMenu);
|
||||
}
|
||||
|
||||
static setupEvents() {
|
||||
window.addEventListener(BxEvent.STREAM_PLAYING, e => {
|
||||
// Enable MKB
|
||||
if (getPref(PrefKey.MKB_ENABLED) && (!ENABLE_NATIVE_MKB_BETA || !window.NATIVE_MKB_TITLES.includes(GAME_PRODUCT_ID))) {
|
||||
console.log('Emulate MKB');
|
||||
MkbHandler.INSTANCE.init();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
163
src/modules/mkb/mkb-preset.ts
Normal file
163
src/modules/mkb/mkb-preset.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { t } from "../translation";
|
||||
import { SettingElementType } from "../settings";
|
||||
import { GamepadKey, MouseButtonCode, MouseMapTo, MkbPresetKey } from "./definitions";
|
||||
import { MkbHandler } from "./mkb-handler";
|
||||
|
||||
|
||||
export class MkbPreset {
|
||||
static MOUSE_SETTINGS = {
|
||||
[MkbPresetKey.MOUSE_MAP_TO]: {
|
||||
label: t('map-mouse-to'),
|
||||
type: SettingElementType.OPTIONS,
|
||||
default: MouseMapTo[MouseMapTo.RS],
|
||||
options: {
|
||||
[MouseMapTo[MouseMapTo.RS]]: t('right-stick'),
|
||||
[MouseMapTo[MouseMapTo.LS]]: t('left-stick'),
|
||||
[MouseMapTo[MouseMapTo.OFF]]: t('off'),
|
||||
},
|
||||
},
|
||||
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: {
|
||||
label: t('horizontal-sensitivity'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 50,
|
||||
min: 1,
|
||||
max: 200,
|
||||
|
||||
params: {
|
||||
suffix: '%',
|
||||
exactTicks: 20,
|
||||
},
|
||||
},
|
||||
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_X]: {
|
||||
label: t('vertical-sensitivity'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 50,
|
||||
min: 1,
|
||||
max: 200,
|
||||
|
||||
params: {
|
||||
suffix: '%',
|
||||
exactTicks: 20,
|
||||
},
|
||||
},
|
||||
|
||||
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: {
|
||||
label: t('deadzone-counterweight'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 20,
|
||||
min: 1,
|
||||
max: 100,
|
||||
|
||||
params: {
|
||||
suffix: '%',
|
||||
exactTicks: 10,
|
||||
},
|
||||
},
|
||||
|
||||
[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH]: {
|
||||
label: t('stick-decay-strength'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 100,
|
||||
min: 10,
|
||||
max: 100,
|
||||
|
||||
params: {
|
||||
suffix: '%',
|
||||
exactTicks: 10,
|
||||
},
|
||||
},
|
||||
|
||||
[MkbPresetKey.MOUSE_STICK_DECAY_MIN]: {
|
||||
label: t('stick-decay-minimum'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 10,
|
||||
min: 1,
|
||||
max: 10,
|
||||
|
||||
params: {
|
||||
suffix: '%',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
static DEFAULT_PRESET = {
|
||||
'mapping': {
|
||||
// Use "e.code" value from https://keyjs.dev
|
||||
[GamepadKey.UP]: ['ArrowUp'],
|
||||
[GamepadKey.DOWN]: ['ArrowDown'],
|
||||
[GamepadKey.LEFT]: ['ArrowLeft'],
|
||||
[GamepadKey.RIGHT]: ['ArrowRight'],
|
||||
|
||||
[GamepadKey.LS_UP]: ['KeyW'],
|
||||
[GamepadKey.LS_DOWN]: ['KeyS'],
|
||||
[GamepadKey.LS_LEFT]: ['KeyA'],
|
||||
[GamepadKey.LS_RIGHT]: ['KeyD'],
|
||||
|
||||
[GamepadKey.RS_UP]: ['KeyI'],
|
||||
[GamepadKey.RS_DOWN]: ['KeyK'],
|
||||
[GamepadKey.RS_LEFT]: ['KeyJ'],
|
||||
[GamepadKey.RS_RIGHT]: ['KeyL'],
|
||||
|
||||
[GamepadKey.A]: ['Space', 'KeyE'],
|
||||
[GamepadKey.X]: ['KeyR'],
|
||||
[GamepadKey.B]: ['ControlLeft', 'Backspace'],
|
||||
[GamepadKey.Y]: ['KeyV'],
|
||||
|
||||
[GamepadKey.START]: ['Enter'],
|
||||
[GamepadKey.SELECT]: ['Tab'],
|
||||
|
||||
[GamepadKey.LB]: ['KeyC', 'KeyG'],
|
||||
[GamepadKey.RB]: ['KeyQ'],
|
||||
|
||||
[GamepadKey.HOME]: ['Backquote'],
|
||||
|
||||
[GamepadKey.RT]: [MouseButtonCode.LEFT_CLICK],
|
||||
[GamepadKey.LT]: [MouseButtonCode.RIGHT_CLICK],
|
||||
|
||||
[GamepadKey.L3]: ['ShiftLeft'],
|
||||
[GamepadKey.R3]: ['KeyF'],
|
||||
},
|
||||
|
||||
'mouse': {
|
||||
[MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo[MouseMapTo.RS],
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_X]: 50,
|
||||
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: 50,
|
||||
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20,
|
||||
[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH]: 18,
|
||||
[MkbPresetKey.MOUSE_STICK_DECAY_MIN]: 6,
|
||||
},
|
||||
};
|
||||
|
||||
static convert(preset) {
|
||||
const obj = {
|
||||
'mapping': {},
|
||||
'mouse': Object.assign({}, preset.mouse),
|
||||
};
|
||||
|
||||
for (const buttonIndex in preset.mapping) {
|
||||
for (const keyName of preset.mapping[buttonIndex]) {
|
||||
obj.mapping[keyName] = parseInt(buttonIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-calculate mouse's sensitivities
|
||||
const mouse = obj.mouse;
|
||||
mouse[MkbPresetKey.MOUSE_SENSITIVITY_X] *= MkbHandler.DEFAULT_PANNING_SENSITIVITY;
|
||||
mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y] *= MkbHandler.DEFAULT_PANNING_SENSITIVITY;
|
||||
mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT] *= MkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT;
|
||||
mouse[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH] *= 0.01;
|
||||
mouse[MkbPresetKey.MOUSE_STICK_DECAY_MIN] *= 0.01;
|
||||
|
||||
const mouseMapTo = MouseMapTo[mouse[MkbPresetKey.MOUSE_MAP_TO]];
|
||||
if (typeof mouseMapTo !== 'undefined') {
|
||||
mouse[MkbPresetKey.MOUSE_MAP_TO] = mouseMapTo;
|
||||
} else {
|
||||
mouse[MkbPresetKey.MOUSE_MAP_TO] = MkbPreset.MOUSE_SETTINGS[MkbPresetKey.MOUSE_MAP_TO].default;
|
||||
}
|
||||
|
||||
console.log(obj);
|
||||
return obj;
|
||||
}
|
||||
}
|
531
src/modules/mkb/mkb-remapper.ts
Normal file
531
src/modules/mkb/mkb-remapper.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
import { GamepadKey } from "./definitions";
|
||||
import { CE, createButton, ButtonStyle } from "../../utils/html";
|
||||
import { t } from "../translation";
|
||||
import { Dialog } from "../dialog";
|
||||
import { getPref, setPref, PrefKey } from "../preferences";
|
||||
import { MkbPresetKey, GamepadKeyName } from "./definitions";
|
||||
import { KeyHelper } from "./key-helper";
|
||||
import { MkbPreset } from "./mkb-preset";
|
||||
import { MkbHandler } from "./mkb-handler";
|
||||
import { LocalDb } from "../../utils/local-db";
|
||||
import { Icon } from "../../utils/html";
|
||||
import { SettingElement } from "../settings";
|
||||
|
||||
type MkbRemapperElements = {
|
||||
wrapper: HTMLElement | null,
|
||||
presetsSelect: HTMLSelectElement | null,
|
||||
activateButton: HTMLButtonElement | null,
|
||||
currentBindingKey: HTMLElement | null,
|
||||
|
||||
allKeyElements: HTMLElement[],
|
||||
allMouseElements: {[key in MkbPresetKey]?: HTMLElement},
|
||||
}
|
||||
|
||||
export class MkbRemapper {
|
||||
get #BUTTON_ORDERS() {
|
||||
return [
|
||||
GamepadKey.UP,
|
||||
GamepadKey.DOWN,
|
||||
GamepadKey.LEFT,
|
||||
GamepadKey.RIGHT,
|
||||
|
||||
GamepadKey.A,
|
||||
GamepadKey.B,
|
||||
GamepadKey.X,
|
||||
GamepadKey.Y,
|
||||
|
||||
GamepadKey.LB,
|
||||
GamepadKey.RB,
|
||||
GamepadKey.LT,
|
||||
GamepadKey.RT,
|
||||
|
||||
GamepadKey.SELECT,
|
||||
GamepadKey.START,
|
||||
GamepadKey.HOME,
|
||||
|
||||
GamepadKey.L3,
|
||||
GamepadKey.LS_UP,
|
||||
GamepadKey.LS_DOWN,
|
||||
GamepadKey.LS_LEFT,
|
||||
GamepadKey.LS_RIGHT,
|
||||
|
||||
GamepadKey.R3,
|
||||
GamepadKey.RS_UP,
|
||||
GamepadKey.RS_DOWN,
|
||||
GamepadKey.RS_LEFT,
|
||||
GamepadKey.RS_RIGHT,
|
||||
];
|
||||
};
|
||||
|
||||
static #instance: MkbRemapper;
|
||||
static get INSTANCE() {
|
||||
if (!MkbRemapper.#instance) {
|
||||
MkbRemapper.#instance = new MkbRemapper();
|
||||
}
|
||||
|
||||
return MkbRemapper.#instance;
|
||||
};
|
||||
|
||||
#STATE = {
|
||||
currentPresetId: 0,
|
||||
presets: [],
|
||||
|
||||
editingPresetData: {},
|
||||
|
||||
isEditing: false,
|
||||
};
|
||||
|
||||
#$: MkbRemapperElements = {
|
||||
wrapper: null,
|
||||
presetsSelect: null,
|
||||
activateButton: null,
|
||||
|
||||
currentBindingKey: null,
|
||||
|
||||
allKeyElements: [],
|
||||
allMouseElements: {},
|
||||
};
|
||||
|
||||
bindingDialog: Dialog;
|
||||
|
||||
constructor() {
|
||||
this.#STATE.currentPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
|
||||
|
||||
this.bindingDialog = new Dialog({
|
||||
className: 'bx-binding-dialog',
|
||||
content: CE('div', {},
|
||||
CE('p', {}, t('press-to-bind')),
|
||||
CE('i', {}, t('press-esc-to-cancel')),
|
||||
),
|
||||
hideCloseButton: true,
|
||||
});
|
||||
}
|
||||
|
||||
#clearEventListeners = () => {
|
||||
window.removeEventListener('keydown', this.#onKeyDown);
|
||||
window.removeEventListener('mousedown', this.#onMouseDown);
|
||||
window.removeEventListener('wheel', this.#onWheel);
|
||||
};
|
||||
|
||||
#bindKey = ($elm: HTMLElement, key: any) => {
|
||||
const buttonIndex = parseInt($elm.getAttribute('data-button-index')!);
|
||||
const keySlot = parseInt($elm.getAttribute('data-key-slot')!);
|
||||
|
||||
// Ignore if bind the save key to the same element
|
||||
if ($elm.getAttribute('data-key-code') === key.code) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unbind duplicated keys
|
||||
for (const $otherElm of this.#$.allKeyElements) {
|
||||
if ($otherElm.getAttribute('data-key-code') === key.code) {
|
||||
this.#unbindKey($otherElm);
|
||||
}
|
||||
}
|
||||
|
||||
this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = key.code;
|
||||
$elm.textContent = key.name;
|
||||
$elm.setAttribute('data-key-code', key.code);
|
||||
}
|
||||
|
||||
#unbindKey = ($elm: HTMLElement) => {
|
||||
const buttonIndex = parseInt($elm.getAttribute('data-button-index')!);
|
||||
const keySlot = parseInt($elm.getAttribute('data-key-slot')!);
|
||||
|
||||
// Remove key from preset
|
||||
this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = null;
|
||||
$elm.textContent = '';
|
||||
$elm.removeAttribute('data-key-code');
|
||||
}
|
||||
|
||||
#onWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
this.#clearEventListeners();
|
||||
|
||||
this.#bindKey(this.#$.currentBindingKey!, KeyHelper.getKeyFromEvent(e));
|
||||
setTimeout(() => this.bindingDialog.hide(), 200);
|
||||
};
|
||||
|
||||
#onMouseDown = e => {
|
||||
e.preventDefault();
|
||||
this.#clearEventListeners();
|
||||
|
||||
this.#bindKey(this.#$.currentBindingKey!, KeyHelper.getKeyFromEvent(e));
|
||||
setTimeout(() => this.bindingDialog.hide(), 200);
|
||||
};
|
||||
|
||||
#onKeyDown = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.#clearEventListeners();
|
||||
|
||||
if (e.code !== 'Escape') {
|
||||
this.#bindKey(this.#$.currentBindingKey!, KeyHelper.getKeyFromEvent(e));
|
||||
}
|
||||
|
||||
setTimeout(() => this.bindingDialog.hide(), 200);
|
||||
};
|
||||
|
||||
#onBindingKey = e => {
|
||||
if (!this.#STATE.isEditing || e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(e);
|
||||
|
||||
this.#$.currentBindingKey = e.target;
|
||||
|
||||
window.addEventListener('keydown', this.#onKeyDown);
|
||||
window.addEventListener('mousedown', this.#onMouseDown);
|
||||
window.addEventListener('wheel', this.#onWheel);
|
||||
|
||||
this.bindingDialog.show({title: e.target.getAttribute('data-prompt')});
|
||||
};
|
||||
|
||||
#onContextMenu = (e: Event) => {
|
||||
e.preventDefault();
|
||||
if (!this.#STATE.isEditing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#unbindKey(e.target as HTMLElement);
|
||||
};
|
||||
|
||||
#getPreset = (presetId: string) => {
|
||||
return this.#STATE.presets[presetId];
|
||||
}
|
||||
|
||||
#getCurrentPreset = () => {
|
||||
return this.#getPreset(this.#STATE.currentPresetId);
|
||||
}
|
||||
|
||||
#switchPreset = presetId => {
|
||||
presetId = parseInt(presetId);
|
||||
|
||||
this.#STATE.currentPresetId = presetId;
|
||||
const presetData = this.#getCurrentPreset().data;
|
||||
|
||||
for (const $elm of this.#$.allKeyElements) {
|
||||
const buttonIndex = $elm.getAttribute('data-button-index');
|
||||
const keySlot = $elm.getAttribute('data-key-slot');
|
||||
|
||||
const buttonKeys = presetData.mapping[buttonIndex];
|
||||
if (buttonKeys && buttonKeys[keySlot]) {
|
||||
$elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]);
|
||||
$elm.setAttribute('data-key-code', buttonKeys[keySlot]);
|
||||
} else {
|
||||
$elm.textContent = '';
|
||||
$elm.removeAttribute('data-key-code');
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in this.#$.allMouseElements) {
|
||||
const $elm = this.#$.allMouseElements[key as MkbPresetKey]!;
|
||||
let value = presetData.mouse[key];
|
||||
if (typeof value === 'undefined') {
|
||||
value = MkbPreset.MOUSE_SETTINGS[key as MkbPresetKey].default;
|
||||
}
|
||||
|
||||
'setValue' in $elm && ($elm as any).setValue(value);
|
||||
}
|
||||
|
||||
// Update state of Activate button
|
||||
const activated = getPref(PrefKey.MKB_DEFAULT_PRESET_ID) === this.#STATE.currentPresetId;
|
||||
this.#$.activateButton!.disabled = activated;
|
||||
this.#$.activateButton!.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
|
||||
}
|
||||
|
||||
#refresh() {
|
||||
// Clear presets select
|
||||
while (this.#$.presetsSelect!.firstChild) {
|
||||
this.#$.presetsSelect!.removeChild(this.#$.presetsSelect!.firstChild);
|
||||
}
|
||||
|
||||
LocalDb.INSTANCE.getPresets()
|
||||
.then(presets => {
|
||||
this.#STATE.presets = presets;
|
||||
const $fragment = document.createDocumentFragment();
|
||||
|
||||
let defaultPresetId;
|
||||
if (this.#STATE.currentPresetId === 0) {
|
||||
this.#STATE.currentPresetId = parseInt(Object.keys(presets)[0]);
|
||||
|
||||
defaultPresetId = this.#STATE.currentPresetId;
|
||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, defaultPresetId);
|
||||
MkbHandler.INSTANCE.refreshPresetData();
|
||||
} else {
|
||||
defaultPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
|
||||
}
|
||||
|
||||
for (let id in presets) {
|
||||
id = parseInt(id);
|
||||
|
||||
const preset = presets[id];
|
||||
let name = preset.name;
|
||||
if (id === defaultPresetId) {
|
||||
name = `🎮 ` + name;
|
||||
}
|
||||
|
||||
const $options = CE('option', {value: id}, name);
|
||||
$options.selected = id === this.#STATE.currentPresetId;
|
||||
|
||||
$fragment.appendChild($options);
|
||||
};
|
||||
|
||||
this.#$.presetsSelect!.appendChild($fragment);
|
||||
|
||||
// Update state of Activate button
|
||||
const activated = defaultPresetId === this.#STATE.currentPresetId;
|
||||
this.#$.activateButton!.disabled = activated;
|
||||
this.#$.activateButton!.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
|
||||
|
||||
!this.#STATE.isEditing && this.#switchPreset(this.#STATE.currentPresetId);
|
||||
});
|
||||
}
|
||||
|
||||
#toggleEditing = (force?: boolean) => {
|
||||
this.#STATE.isEditing = typeof force !== 'undefined' ? force : !this.#STATE.isEditing;
|
||||
this.#$.wrapper!.classList.toggle('bx-editing', this.#STATE.isEditing);
|
||||
|
||||
if (this.#STATE.isEditing) {
|
||||
this.#STATE.editingPresetData = structuredClone(this.#getCurrentPreset().data);
|
||||
} else {
|
||||
this.#STATE.editingPresetData = {};
|
||||
}
|
||||
|
||||
|
||||
const childElements = this.#$.wrapper!.querySelectorAll('select, button, input');
|
||||
for (const $elm of Array.from(childElements)) {
|
||||
if ($elm.parentElement!.parentElement!.classList.contains('bx-mkb-action-buttons')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let disable = !this.#STATE.isEditing;
|
||||
|
||||
if ($elm.parentElement!.classList.contains('bx-mkb-preset-tools')) {
|
||||
disable = !disable;
|
||||
}
|
||||
|
||||
($elm as HTMLButtonElement).disabled = disable;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.#$.wrapper = CE('div', {'class': 'bx-mkb-settings'});
|
||||
|
||||
this.#$.presetsSelect = CE<HTMLSelectElement>('select', {});
|
||||
this.#$.presetsSelect!.addEventListener('change', e => {
|
||||
this.#switchPreset((e.target as HTMLSelectElement).value);
|
||||
});
|
||||
|
||||
const promptNewName = (value?: string) => {
|
||||
let newName: string | null = '';
|
||||
while (!newName) {
|
||||
newName = prompt(t('prompt-preset-name'), value);
|
||||
if (newName === null) {
|
||||
return false;
|
||||
}
|
||||
newName = newName.trim();
|
||||
}
|
||||
|
||||
return newName ? newName : false;
|
||||
};
|
||||
|
||||
const $header = CE('div', {'class': 'bx-mkb-preset-tools'},
|
||||
this.#$.presetsSelect,
|
||||
// Rename button
|
||||
createButton({
|
||||
title: t('rename'),
|
||||
icon: Icon.CURSOR_TEXT,
|
||||
onClick: e => {
|
||||
const preset = this.#getCurrentPreset();
|
||||
|
||||
let newName = promptNewName(preset.name);
|
||||
if (!newName || newName === preset.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update preset with new name
|
||||
preset.name = newName;
|
||||
LocalDb.INSTANCE.updatePreset(preset).then(id => this.#refresh());
|
||||
},
|
||||
}),
|
||||
|
||||
// New button
|
||||
createButton({
|
||||
icon: Icon.NEW,
|
||||
title: t('new'),
|
||||
onClick: e => {
|
||||
let newName = promptNewName('');
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new preset selected name
|
||||
LocalDb.INSTANCE.newPreset(newName, MkbPreset.DEFAULT_PRESET).then(id => {
|
||||
this.#STATE.currentPresetId = id;
|
||||
this.#refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
// Copy button
|
||||
createButton({
|
||||
icon: Icon.COPY,
|
||||
title: t('copy'),
|
||||
onClick: e => {
|
||||
const preset = this.#getCurrentPreset();
|
||||
|
||||
let newName = promptNewName(`${preset.name} (2)`);
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new preset selected name
|
||||
LocalDb.INSTANCE.newPreset(newName, preset.data).then(id => {
|
||||
this.#STATE.currentPresetId = id;
|
||||
this.#refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
// Delete button
|
||||
createButton({
|
||||
icon: Icon.TRASH,
|
||||
style: ButtonStyle.DANGER,
|
||||
title: t('delete'),
|
||||
onClick: e => {
|
||||
if (!confirm(t('confirm-delete-preset'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
LocalDb.INSTANCE.deletePreset(this.#STATE.currentPresetId).then(id => {
|
||||
this.#STATE.currentPresetId = 0;
|
||||
this.#refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
this.#$.wrapper!.appendChild($header);
|
||||
|
||||
const $rows = CE('div', {'class': 'bx-mkb-settings-rows'},
|
||||
CE('i', {'class': 'bx-mkb-note'}, t('right-click-to-unbind')),
|
||||
);
|
||||
|
||||
// Render keys
|
||||
const keysPerButton = 2;
|
||||
for (const buttonIndex of this.#BUTTON_ORDERS) {
|
||||
const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex];
|
||||
|
||||
let $elm;
|
||||
const $fragment = document.createDocumentFragment();
|
||||
for (let i = 0; i < keysPerButton; i++) {
|
||||
$elm = CE('button', {
|
||||
'data-prompt': buttonPrompt,
|
||||
'data-button-index': buttonIndex,
|
||||
'data-key-slot': i,
|
||||
}, ' ');
|
||||
|
||||
$elm.addEventListener('mouseup', this.#onBindingKey);
|
||||
$elm.addEventListener('contextmenu', this.#onContextMenu);
|
||||
|
||||
$fragment.appendChild($elm);
|
||||
this.#$.allKeyElements.push($elm);
|
||||
}
|
||||
|
||||
const $keyRow = CE('div', {'class': 'bx-mkb-key-row'},
|
||||
CE('label', {'title': buttonName}, buttonPrompt),
|
||||
$fragment,
|
||||
);
|
||||
|
||||
$rows.appendChild($keyRow);
|
||||
}
|
||||
|
||||
$rows.appendChild(CE('i', {'class': 'bx-mkb-note'}, t('mkb-adjust-ingame-settings')),);
|
||||
|
||||
// Render mouse settings
|
||||
const $mouseSettings = document.createDocumentFragment();
|
||||
for (const key in MkbPreset.MOUSE_SETTINGS) {
|
||||
const setting = MkbPreset.MOUSE_SETTINGS[key as MkbPresetKey];
|
||||
const value = setting.default;
|
||||
|
||||
let $elm;
|
||||
const onChange = (e, value) => {
|
||||
this.#STATE.editingPresetData.mouse[key] = value;
|
||||
};
|
||||
const $row = CE('div', {'class': 'bx-quick-settings-row'},
|
||||
CE('label', {'for': `bx_setting_${key}`}, setting.label),
|
||||
$elm = SettingElement.render(setting.type, key, setting, value, onChange, setting.params),
|
||||
);
|
||||
|
||||
$mouseSettings.appendChild($row);
|
||||
this.#$.allMouseElements[key as MkbPresetKey] = $elm;
|
||||
}
|
||||
|
||||
$rows.appendChild($mouseSettings);
|
||||
this.#$.wrapper!.appendChild($rows);
|
||||
|
||||
// Render action buttons
|
||||
const $actionButtons = CE('div', {'class': 'bx-mkb-action-buttons'},
|
||||
CE('div', {},
|
||||
// Edit button
|
||||
createButton({
|
||||
label: t('edit'),
|
||||
onClick: e => this.#toggleEditing(true),
|
||||
}),
|
||||
|
||||
// Activate button
|
||||
this.#$.activateButton = createButton({
|
||||
label: t('activate'),
|
||||
style: ButtonStyle.PRIMARY,
|
||||
onClick: e => {
|
||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.#STATE.currentPresetId);
|
||||
MkbHandler.INSTANCE.refreshPresetData();
|
||||
|
||||
this.#refresh();
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
||||
CE('div', {},
|
||||
// Cancel button
|
||||
createButton({
|
||||
label: t('cancel'),
|
||||
style: ButtonStyle.GHOST,
|
||||
onClick: e => {
|
||||
// Restore preset
|
||||
this.#switchPreset(this.#STATE.currentPresetId);
|
||||
this.#toggleEditing(false);
|
||||
},
|
||||
}),
|
||||
|
||||
// Save button
|
||||
createButton({
|
||||
label: t('save'),
|
||||
style: ButtonStyle.PRIMARY,
|
||||
onClick: e => {
|
||||
const updatedPreset = structuredClone(this.#getCurrentPreset());
|
||||
updatedPreset.data = this.#STATE.editingPresetData;
|
||||
|
||||
LocalDb.INSTANCE.updatePreset(updatedPreset).then(id => {
|
||||
// If this is the default preset => refresh preset data
|
||||
if (id === getPref(PrefKey.MKB_DEFAULT_PRESET_ID)) {
|
||||
MkbHandler.INSTANCE.refreshPresetData();
|
||||
}
|
||||
|
||||
this.#toggleEditing(false);
|
||||
this.#refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
this.#$.wrapper!.appendChild($actionButtons);
|
||||
|
||||
this.#toggleEditing(false);
|
||||
this.#refresh();
|
||||
return this.#$.wrapper;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user