feat: 按键触发方式

This commit is contained in:
unanmed 2024-08-21 14:28:05 +08:00
parent b0f9e9d17d
commit fdd4563f45
12 changed files with 271 additions and 158 deletions

View File

@ -5,7 +5,6 @@ import { GameStorage } from './main/storage';
import './main/init/'; import './main/init/';
import './main/custom/toolbar'; import './main/custom/toolbar';
import { fixedUi, mainUi } from './main/init/ui'; import { fixedUi, mainUi } from './main/init/ui';
import { gameKey } from './main/init/hotkey';
import { import {
MotaSetting, MotaSetting,
SettingDisplayer, SettingDisplayer,
@ -22,7 +21,8 @@ import {
Hotkey, Hotkey,
checkAssist, checkAssist,
isAssist, isAssist,
unwarpBinary unwarpBinary,
gameKey
} from './main/custom/hotkey'; } from './main/custom/hotkey';
import { Keyboard, generateKeyboardEvent } from './main/custom/keyboard'; import { Keyboard, generateKeyboardEvent } from './main/custom/keyboard';
import './main/layout'; import './main/layout';

View File

@ -1,22 +1,27 @@
import { KeyCode } from '@/plugin/keyCodes'; import { KeyCode } from '@/plugin/keyCodes';
import { deleteWith, generateBinary, keycode, spliceBy } from '@/plugin/utils'; import { deleteWith, generateBinary, keycode, spliceBy } from '@/plugin/utils';
import { EventEmitter } from '../../common/eventEmitter'; import { EventEmitter } from 'eventemitter3';
import { isNil } from 'lodash-es';
// todo: 按下时触发,长按(单次/连续)触发,按下连续触发,按下节流触发,按下加速节流触发 // todo: 按下时触发,长按(单次/连续)触发,按下连续触发,按下节流触发,按下加速节流触发
interface HotkeyEvent { interface HotkeyEvent {
set: (id: string, key: KeyCode, assist: number) => void; set: [id: string, key: KeyCode, assist: number];
emit: (key: KeyCode, assist: number, type: KeyEmitType) => void; emit: [key: KeyCode, assist: number, type: KeyEmitType];
press: [key: KeyCode];
release: [key: KeyCode];
} }
type KeyEmitType = type KeyEmitType =
| 'down'
| 'up' | 'up'
| 'down'
| 'down-repeat' | 'down-repeat'
| 'down-throttle' | 'down-throttle'
| 'down-accelerate' // todo: | 'down-accelerate'
| 'down-timeout'; | 'down-timeout';
type KeyEventType = 'up' | 'down';
interface AssistHoykey { interface AssistHoykey {
ctrl: boolean; ctrl: boolean;
shift: boolean; shift: boolean;
@ -27,17 +32,17 @@ interface RegisterHotkeyData extends Partial<AssistHoykey> {
id: string; id: string;
name: string; name: string;
defaults: KeyCode; defaults: KeyCode;
type?: KeyEmitType;
} }
interface HotkeyData extends Required<RegisterHotkeyData> { interface HotkeyData extends Required<RegisterHotkeyData> {
key: KeyCode; key: KeyCode;
func: Map<symbol, HotkeyFunc>; emits: Map<symbol, HotkeyEmitData>;
group?: string; group?: string;
} }
/** /**
* @param id id包含数字后缀 * @param id id包含数字后缀
* @returns `@void` 使 `preventDefault`
*/ */
type HotkeyFunc = ( type HotkeyFunc = (
id: string, id: string,
@ -45,6 +50,12 @@ type HotkeyFunc = (
ev: KeyboardEvent ev: KeyboardEvent
) => void | '@void'; ) => void | '@void';
interface HotkeyEmitData {
func: HotkeyFunc;
onUp?: HotkeyFunc;
config?: HotkeyEmitConfig;
}
export interface HotkeyJSON { export interface HotkeyJSON {
key: KeyCode; key: KeyCode;
assist: number; assist: number;
@ -64,8 +75,6 @@ export class Hotkey extends EventEmitter<HotkeyEvent> {
name: string; name: string;
data: Record<string, HotkeyData> = {}; data: Record<string, HotkeyData> = {};
keyMap: Map<KeyCode, HotkeyData[]> = new Map(); keyMap: Map<KeyCode, HotkeyData[]> = new Map();
/** 每个指令的配置信息 */
configData: Map<string, HotkeyEmitConfig> = new Map();
/** id to name */ /** id to name */
groupName: Record<string, string> = { groupName: Record<string, string> = {
none: '未分类按键' none: '未分类按键'
@ -81,6 +90,13 @@ export class Hotkey extends EventEmitter<HotkeyEvent> {
private scopeStack: symbol[] = []; private scopeStack: symbol[] = [];
private grouping: string = 'none'; private grouping: string = 'none';
/** 当前正在按下的按键 */
private pressed: Set<KeyCode> = new Set();
/** 按键按下时的时间 */
private pressTime: Map<KeyCode, number> = new Map();
/** 按键节流时间 */
private throttleMap: Map<KeyCode, number> = new Map();
constructor(id: string, name: string) { constructor(id: string, name: string) {
super(); super();
this.id = id; this.id = id;
@ -98,9 +114,8 @@ export class Hotkey extends EventEmitter<HotkeyEvent> {
shift: !!data.shift, shift: !!data.shift,
alt: !!data.alt, alt: !!data.alt,
key: data.defaults, key: data.defaults,
func: new Map(), emits: new Map(),
group: this.grouping, group: this.grouping
type: data.type ?? 'up'
}; };
this.ensureMap(d.key); this.ensureMap(d.key);
if (d.id in this.data) { if (d.id in this.data) {
@ -117,7 +132,7 @@ export class Hotkey extends EventEmitter<HotkeyEvent> {
* *
* @param id id * @param id id
* @param func * @param func
* @param config * @param config
*/ */
realize(id: string, func: HotkeyFunc, config?: HotkeyEmitConfig) { realize(id: string, func: HotkeyFunc, config?: HotkeyEmitConfig) {
const toSet = Object.values(this.data).filter(v => { const toSet = Object.values(this.data).filter(v => {
@ -129,12 +144,24 @@ export class Hotkey extends EventEmitter<HotkeyEvent> {
throw new Error(`Realize nonexistent key '${id}'.`); throw new Error(`Realize nonexistent key '${id}'.`);
} }
for (const key of toSet) { for (const key of toSet) {
if (!key.func.has(this.scope)) { const data = key.emits.get(this.scope);
throw new Error( if (!data) {
`Cannot access using scope. Call use before calling realize.` key.emits.set(this.scope, { func, config });
); } else {
// 同时注册抬起和按下
const dataType = data.config?.type ?? 'up';
const configType = config?.type ?? 'up';
if (dataType === 'up' && configType !== 'up') {
data.onUp = func;
data.config ??= { type: configType };
data.config.type = configType;
} else if (dataType !== 'up' && configType === 'up') {
data.onUp = func;
} else {
data.config = config;
data.func = func;
}
} }
key.func.set(this.scope, func);
} }
return this; return this;
} }
@ -148,9 +175,6 @@ export class Hotkey extends EventEmitter<HotkeyEvent> {
this.scopeStack.push(symbol); this.scopeStack.push(symbol);
this.scope = symbol; this.scope = symbol;
this.conditionMap.set(symbol, () => true); this.conditionMap.set(symbol, () => true);
for (const key of Object.values(this.data)) {
key.func.set(symbol, () => '@void');
}
} }
/** /**
@ -159,7 +183,7 @@ export class Hotkey extends EventEmitter<HotkeyEvent> {
*/ */
dispose(symbol: symbol = this.scopeStack.at(-1) ?? Symbol()) { dispose(symbol: symbol = this.scopeStack.at(-1) ?? Symbol()) {
for (const key of Object.values(this.data)) { for (const key of Object.values(this.data)) {
key.func.delete(symbol); key.emits.delete(symbol);
} }
spliceBy(this.scopeStack, symbol); spliceBy(this.scopeStack, symbol);
this.scope = this.scopeStack.at(-1) ?? Symbol(); this.scope = this.scopeStack.at(-1) ?? Symbol();
@ -196,31 +220,106 @@ export class Hotkey extends EventEmitter<HotkeyEvent> {
emitKey( emitKey(
key: KeyCode, key: KeyCode,
assist: number, assist: number,
type: KeyEmitType, type: KeyEventType,
ev: KeyboardEvent ev: KeyboardEvent
): boolean { ): boolean {
// 检查全局启用情况
if (!this.enabled) return false; if (!this.enabled) return false;
const when = this.conditionMap.get(this.scope)!; const when = this.conditionMap.get(this.scope)!;
if (!when()) return false; if (!when()) return false;
const toEmit = this.keyMap.get(key); const toEmit = this.keyMap.get(key);
if (!toEmit) return false; if (!toEmit) return false;
// 进行按键初始处理
const { ctrl, shift, alt } = unwarpBinary(assist); const { ctrl, shift, alt } = unwarpBinary(assist);
// 真正开始触发按键
let emitted = false; let emitted = false;
toEmit.forEach(v => { toEmit.forEach(v => {
if (type !== v.type) return;
if (ctrl === v.ctrl && shift === v.shift && alt === v.alt) { if (ctrl === v.ctrl && shift === v.shift && alt === v.alt) {
const func = v.func.get(this.scope); const data = v.emits.get(this.scope);
if (!func) { if (!data) return;
throw new Error(`Emit unknown scope keys.`); if (type === 'up' && data.onUp) {
data.onUp(v.id, key, ev);
return;
} }
const res = func(v.id, key, ev); if (!this.canEmit(v.id, key, type, data)) return;
const res = data.func(v.id, key, ev);
if (res !== '@void') emitted = true; if (res !== '@void') emitted = true;
} }
}); });
this.emit('emit', key, assist, type); this.emit('emit', key, assist, type);
if (type === 'down') this.checkPress(key);
else this.checkPressEnd(key);
return emitted; return emitted;
} }
/**
*
* @param keyCode
*/
private checkPress(keyCode: KeyCode) {
if (this.pressed.has(keyCode)) return;
this.pressed.add(keyCode);
this.pressTime.set(keyCode, Date.now());
this.emit('press', keyCode);
}
/**
*
* @param keyCode
*/
private checkPressEnd(keyCode: KeyCode) {
if (!this.pressed.has(keyCode)) return;
this.pressed.delete(keyCode);
this.pressTime.delete(keyCode);
this.emit('release', keyCode);
}
/**
*
* @param id id
* @param keyCode
*/
private canEmit(
_id: string,
keyCode: KeyCode,
type: KeyEventType,
data: HotkeyEmitData
) {
const config = data?.config;
// 这时默认为抬起触发,始终可触发
if (type === 'up') {
if (!config || config.type === 'up') return true;
else return false;
}
if (!config) return false;
// 按下单次触发
if (config.type === 'down') return !this.pressed.has(keyCode);
// 按下重复触发
if (config.type === 'down-repeat') return true;
if (config.type === 'down-timeout') {
const time = this.pressTime.get(keyCode);
if (isNil(time) || isNil(config.timeout)) return false;
return Date.now() - time >= config.timeout;
}
if (config.type === 'down-throttle') {
const thorttleTime = this.throttleMap.get(keyCode);
if (isNil(config.throttle)) return false;
if (isNil(thorttleTime)) {
this.throttleMap.set(keyCode, Date.now());
return true;
}
if (Date.now() - thorttleTime >= config.throttle) {
this.throttleMap.set(keyCode, Date.now());
return true;
}
return false;
}
}
/** /**
* register * register
* @param id id * @param id id
@ -290,22 +389,6 @@ export class Hotkey extends EventEmitter<HotkeyEvent> {
} }
} }
// todo
/**
*
*/
export class HotkeyController {
/** 所有按下的按键 */
private pressed: Set<KeyCode> = new Set();
/** 当前控制器管理的热键实例 */
hotkey: Hotkey;
constructor(hotkey: Hotkey) {
this.hotkey = hotkey;
}
}
export function unwarpBinary(bin: number): AssistHoykey { export function unwarpBinary(bin: number): AssistHoykey {
return { return {
ctrl: !!(bin & (1 << 0)), ctrl: !!(bin & (1 << 0)),

View File

@ -1,6 +1,6 @@
import { EventEmitter, Listener } from '@/core/common/eventEmitter'; import { EventEmitter, Listener } from '@/core/common/eventEmitter';
import { KeyCode } from '@/plugin/keyCodes'; import { KeyCode } from '@/plugin/keyCodes';
import { gameKey } from '../init/hotkey'; import { gameKey } from './hotkey';
import { unwarpBinary } from './hotkey'; import { unwarpBinary } from './hotkey';
import { deleteWith, flipBinary } from '@/plugin/utils'; import { deleteWith, flipBinary } from '@/plugin/utils';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';

View File

@ -15,25 +15,21 @@ gameKey
.group('game', '游戏按键') .group('game', '游戏按键')
.register({ .register({
id: 'moveUp', id: 'moveUp',
type: 'down',
name: '上移', name: '上移',
defaults: KeyCode.UpArrow defaults: KeyCode.UpArrow
}) })
.register({ .register({
id: 'moveDown', id: 'moveDown',
type: 'down',
name: '下移', name: '下移',
defaults: KeyCode.DownArrow defaults: KeyCode.DownArrow
}) })
.register({ .register({
id: 'moveLeft', id: 'moveLeft',
type: 'down',
name: '左移', name: '左移',
defaults: KeyCode.LeftArrow defaults: KeyCode.LeftArrow
}) })
.register({ .register({
id: 'moveRight', id: 'moveRight',
type: 'down',
name: '右移', name: '右移',
defaults: KeyCode.RightArrow defaults: KeyCode.RightArrow
}) })
@ -293,78 +289,66 @@ gameKey
.register({ .register({
id: '@start_up', id: '@start_up',
name: '上移光标', name: '上移光标',
defaults: KeyCode.UpArrow, defaults: KeyCode.UpArrow
type: 'down'
}) })
.register({ .register({
id: '@start_down', id: '@start_down',
name: '下移光标', name: '下移光标',
defaults: KeyCode.DownArrow, defaults: KeyCode.DownArrow
type: 'down'
}) })
// -------------------- // --------------------
.group('@ui_book', '怪物手册') .group('@ui_book', '怪物手册')
.register({ .register({
id: '@book_up', id: '@book_up',
name: '上移光标', name: '上移光标',
defaults: KeyCode.UpArrow, defaults: KeyCode.UpArrow
type: 'down'
}) })
.register({ .register({
id: '@book_down', id: '@book_down',
name: '下移光标', name: '下移光标',
defaults: KeyCode.DownArrow, defaults: KeyCode.DownArrow
type: 'down'
}) })
.register({ .register({
id: '@book_pageDown_1', id: '@book_pageDown_1',
name: '下移5个怪物_1', name: '下移5个怪物_1',
defaults: KeyCode.RightArrow, defaults: KeyCode.RightArrow
type: 'down'
}) })
.register({ .register({
id: '@book_pageDown_2', id: '@book_pageDown_2',
name: '下移5个怪物_2', name: '下移5个怪物_2',
defaults: KeyCode.PageDown, defaults: KeyCode.PageDown
type: 'down'
}) })
.register({ .register({
id: '@book_pageUp_1', id: '@book_pageUp_1',
name: '上移5个怪物_1', name: '上移5个怪物_1',
defaults: KeyCode.LeftArrow, defaults: KeyCode.LeftArrow
type: 'down'
}) })
.register({ .register({
id: '@book_pageUp_2', id: '@book_pageUp_2',
name: '上移5个怪物_2', name: '上移5个怪物_2',
defaults: KeyCode.PageUp, defaults: KeyCode.PageUp
type: 'down'
}) })
// -------------------- // --------------------
.group('@ui_toolbox', '道具栏') .group('@ui_toolbox', '道具栏')
.register({ .register({
id: '@toolbox_right', id: '@toolbox_right',
name: '光标右移', name: '光标右移',
defaults: KeyCode.RightArrow, defaults: KeyCode.RightArrow
type: 'down'
}) })
.register({ .register({
id: '@toolbox_left', id: '@toolbox_left',
name: '光标左移', name: '光标左移',
defaults: KeyCode.LeftArrow, defaults: KeyCode.LeftArrow
type: 'down'
}) })
.register({ .register({
id: '@toolbox_up', id: '@toolbox_up',
name: '光标上移', name: '光标上移',
defaults: KeyCode.UpArrow, defaults: KeyCode.UpArrow
type: 'down'
}) })
.register({ .register({
id: '@toolbox_down', id: '@toolbox_down',
name: '光标下移', name: '光标下移',
defaults: KeyCode.DownArrow, defaults: KeyCode.DownArrow
type: 'down'
}) })
// -------------------- // --------------------
.group('@ui_shop', '商店') .group('@ui_shop', '商店')
@ -381,14 +365,12 @@ gameKey
.register({ .register({
id: '@shop_add', id: '@shop_add',
name: '增加购买量', name: '增加购买量',
defaults: KeyCode.RightArrow, defaults: KeyCode.RightArrow
type: 'down'
}) })
.register({ .register({
id: '@shop_min', id: '@shop_min',
name: '减少购买量', name: '减少购买量',
defaults: KeyCode.LeftArrow, defaults: KeyCode.LeftArrow
type: 'down'
}) })
// -------------------- // --------------------
.group('@ui_fly', '楼层传送') .group('@ui_fly', '楼层传送')

View File

@ -1,2 +1,3 @@
import './fixed'; import './fixed';
import './keyboard'; import './keyboard';
import './hotkey';

View File

@ -1,7 +1,7 @@
import type { SettingComponent, SettingComponentProps } from '../setting'; import type { SettingComponent, SettingComponentProps } from '../setting';
import { Button, InputNumber, Radio } from 'ant-design-vue'; import { Button, InputNumber, Radio } from 'ant-design-vue';
import { mainUi } from './ui'; import { mainUi } from './ui';
import { gameKey } from './hotkey'; import { gameKey } from '../custom/hotkey';
interface Components { interface Components {
Default: SettingComponent; Default: SettingComponent;

View File

@ -17,7 +17,7 @@ import {
} from 'ant-design-vue'; } from 'ant-design-vue';
import { MotaSettingItem, mainSetting } from '../setting'; import { MotaSettingItem, mainSetting } from '../setting';
import Minimap from '@/components/minimap.vue'; import Minimap from '@/components/minimap.vue';
import { gameKey } from './hotkey'; import { gameKey } from '../custom/hotkey';
import { FunctionalComponent, StyleValue, h } from 'vue'; import { FunctionalComponent, StyleValue, h } from 'vue';
import { mainUi } from './ui'; import { mainUi } from './ui';
import { isMobile } from '@/plugin/use'; import { isMobile } from '@/plugin/use';

View File

@ -1,6 +1,5 @@
import type { RenderAdapter } from '@/core/render/adapter'; import type { RenderAdapter } from '@/core/render/adapter';
import type { HeroRenderer } from '@/core/render/preset/hero'; import type { HeroRenderer } from '@/core/render/preset/hero';
import { backDir } from './utils';
interface Adapters { interface Adapters {
'hero-adapter'?: RenderAdapter<HeroRenderer>; 'hero-adapter'?: RenderAdapter<HeroRenderer>;
@ -9,7 +8,7 @@ interface Adapters {
const adapters: Adapters = {}; const adapters: Adapters = {};
export function init() { export function init() {
if (!main.replayChecking) { if (!main.replayChecking && main.mode === 'play') {
const Adapter = Mota.require('module', 'Render').RenderAdapter; const Adapter = Mota.require('module', 'Render').RenderAdapter;
const hero = Adapter.get<HeroRenderer>('hero-adapter'); const hero = Adapter.get<HeroRenderer>('hero-adapter');

View File

@ -151,34 +151,50 @@ function checkScroll() {
setTimeout(() => { setTimeout(() => {
gameKey.use(props.ui.symbol); gameKey.use(props.ui.symbol);
gameKey gameKey
.realize('@book_up', () => { .realize(
if (selected.value > 0) { '@book_up',
selected.value--; () => {
} if (selected.value > 0) {
checkScroll(); selected.value--;
}) }
.realize('@book_down', () => { checkScroll();
if (selected.value < enemy.length - 1) { },
selected.value++; { type: 'down-repeat' }
} )
checkScroll(); .realize(
}) '@book_down',
.realize('@book_pageDown', () => { () => {
if (selected.value <= 4) { if (selected.value < enemy.length - 1) {
selected.value = 0; selected.value++;
} else { }
selected.value -= 5; checkScroll();
} },
checkScroll(); { type: 'down-repeat' }
}) )
.realize('@book_pageUp', () => { .realize(
if (selected.value >= enemy.length - 5) { '@book_pageDown',
selected.value = enemy.length - 1; () => {
} else { if (selected.value <= 4) {
selected.value += 5; selected.value = 0;
} } else {
checkScroll(); selected.value -= 5;
}) }
checkScroll();
},
{ type: 'down-repeat' }
)
.realize(
'@book_pageUp',
() => {
if (selected.value >= enemy.length - 5) {
selected.value = enemy.length - 1;
} else {
selected.value += 5;
}
checkScroll();
},
{ type: 'down-repeat' }
)
.realize('exit', () => { .realize('exit', () => {
exit(); exit();
}) })

View File

@ -289,12 +289,20 @@ gameKey
selected.value++; selected.value++;
} }
}) })
.realize('@shop_add', () => { .realize(
count.value++; '@shop_add',
}) () => {
.realize('@shop_min', () => { count.value++;
count.value--; },
}) { type: 'down-repeat' }
)
.realize(
'@shop_min',
() => {
count.value--;
},
{ type: 'down-repeat' }
)
.realize('exit', () => { .realize('exit', () => {
exit(); exit();
}) })

View File

@ -192,20 +192,28 @@ function movein(button: HTMLElement, i: number) {
gameKey.use(props.ui.symbol); gameKey.use(props.ui.symbol);
gameKey gameKey
.realize('@start_up', () => { .realize(
const i = toshow.indexOf(selected.value); '@start_up',
const next = toshow[i + 1]; () => {
if (!next) return; const i = toshow.indexOf(selected.value);
selected.value = next; const next = toshow[i + 1];
setCursor(buttons[toshow.length - i - 2], toshow.length - i - 2); if (!next) return;
}) selected.value = next;
.realize('@start_down', () => { setCursor(buttons[toshow.length - i - 2], toshow.length - i - 2);
const i = toshow.indexOf(selected.value); },
const next = toshow[i - 1]; { type: 'down-repeat' }
if (!next) return; )
selected.value = next; .realize(
setCursor(buttons[toshow.length - i], toshow.length - i); '@start_down',
}) () => {
const i = toshow.indexOf(selected.value);
const next = toshow[i - 1];
if (!next) return;
selected.value = next;
setCursor(buttons[toshow.length - i], toshow.length - i);
},
{ type: 'down-repeat' }
)
.realize('confirm', () => { .realize('confirm', () => {
clickStartButton(selected.value); clickStartButton(selected.value);
}); });

View File

@ -205,35 +205,51 @@ async function toEquip() {
gameKey.use(props.ui.symbol); gameKey.use(props.ui.symbol);
gameKey gameKey
.realize('@toolbox_right', () => { .realize(
const constants = items.constants.length; '@toolbox_right',
if (mode.value === 'tools') { () => {
if (index.value >= constants) { const constants = items.constants.length;
index.value = constants - 1; if (mode.value === 'tools') {
if (index.value >= constants) {
index.value = constants - 1;
}
mode.value = 'constants';
} }
mode.value = 'constants'; },
} { type: 'down-repeat' }
}) )
.realize('@toolbox_left', () => { .realize(
const constants = items.tools.length; '@toolbox_left',
if (mode.value === 'constants') { () => {
if (index.value >= constants) { const constants = items.tools.length;
index.value = constants - 1; if (mode.value === 'constants') {
if (index.value >= constants) {
index.value = constants - 1;
}
mode.value = 'tools';
} }
mode.value = 'tools'; },
} { type: 'down-repeat' }
}) )
.realize('@toolbox_up', () => { .realize(
if (index.value > 0) { '@toolbox_up',
index.value--; () => {
} if (index.value > 0) {
}) index.value--;
.realize('@toolbox_down', () => { }
const total = items[mode.value].length; },
if (index.value < total - 1) { { type: 'down-repeat' }
index.value++; )
} .realize(
}) '@toolbox_down',
() => {
const total = items[mode.value].length;
if (index.value < total - 1) {
index.value++;
}
},
{ type: 'down-repeat' }
)
.realize('exit', () => { .realize('exit', () => {
exit(); exit();
}) })