refactor: 按键系统的部分实现

This commit is contained in:
AncTe committed 2026-10-24 18:25:06 +08:00
1 parent 3f7afb635e
commit 88c4f5112c
14 files changed
+158 -561

No files matched your search

@@ -1,4 +1,4 @@
export * from './choices';
export * from '../../../../src/client/ui/choices';
export * from './floorSelect';
export * from './icons';
export * from './input';
@@ -17,7 +17,7 @@ import { linear } from 'mutate-animate';
import { Background, Selection } from './misc';
import { GameUI, IUIController, SetupComponentOptions } from '@motajs/system';
import { KeyCode } from '@motajs/client-base';
import { IGameUIPropsBase } from '../types';
import { IGameUIPropsBase } from '../../../client-modules/src/types';
export interface InputProps extends DefaultProps, Partial<TextContentProps> {
/** 输入框的提示内容 */
@@ -142,7 +142,7 @@ export const Input = defineComponent<InputProps, InputEmits, keyof InputEmits>(
} else {
ele = document.createElement('input');
}
// See file://./../../../../../src/styles.less
// See src/styles.less
ele.classList.add('motajs-input-element');
listenInput();
};
@@ -15,7 +15,7 @@ import { clamp } from 'lodash-es';
// @ts-expect-error render/renderer 按 D-54 留在实现层,本文件已移入系统层,按 D-59 暂以标注记录,不反向引用、不解耦
import { using } from '../renderer';
import { cosh, CurveMode } from '@motajs/animate';
import { IGameUIPropsBase } from '../types';
import { IGameUIPropsBase } from '../../../client-modules/src/types';
interface ProgressProps extends DefaultProps {
/** 进度条的位置 */
+1
View File
@@ -5,6 +5,7 @@ export * from './layout';
export * from './map';
export * from './material';
export * from './save';
export * from './use';
export * from './shared';
export * from './types';
@@ -0,0 +1,71 @@
import { IHotkeyEmitInfo, IHotkeySystem } from '@motajs/system';
import { IHotkeyUsing } from './types';
import { getCurrentInstance, onMounted, onUnmounted } from 'vue';
import { logger } from '@motajs/common';
class HotkeyUsing implements IHotkeyUsing {
/** 作用域 Promise */
private readonly scopePromise: Promise<symbol>;
/** 当前是否已经挂载 */
private mounted: boolean = false;
/** 当前按键作用域 */
private keyScope: symbol | null = null;
/** 当前的按键实现对象 */
private impl: Map<string, (info: IHotkeyEmitInfo) => void> = new Map();
constructor(
readonly system: IHotkeySystem,
clearImpl: boolean
) {
const { promise, resolve } = Promise.withResolvers<symbol>();
this.scopePromise = promise;
onMounted(() => {
this.mounted = true;
const scope = system.enterScope(clearImpl);
this.keyScope = scope;
resolve(scope);
for (const [command, func] of this.impl) {
system.implement(command, func);
}
this.impl.clear();
});
onUnmounted(() => {
if (this.keyScope) {
system.leaveScope(this.keyScope);
} else {
logger.warn(211);
}
});
}
scope(): Promise<symbol> {
return this.scopePromise;
}
implement(command: string, func: (info: IHotkeyEmitInfo) => void): this {
if (this.mounted) {
this.system.implement(command, func);
} else {
this.impl.set(command, func);
}
return this;
}
}
/**
* 生成仅在当前组件的生命周期中生效的按键指令对象,必须在 Vue 组件中调用,否则会返回 `null`
* @param system 按键系统
* @param clearImpl 是否清空上一个作用域的按键实现,默认清空。不清空时,上一个作用域的按键实现依旧在新作用域生效,
* 若清空,则不会生效,只有当当前的作用域被释放后才会继续生效。
*/
export function useKey(
system: IHotkeySystem,
clearImpl: boolean = true
): IHotkeyUsing | null {
const instance = getCurrentInstance();
if (!instance) return null;
return new HotkeyUsing(system, clearImpl);
}
@@ -0,0 +1,2 @@
export * from './hotkey';
export * from './types';
@@ -0,0 +1,15 @@
import { IHotkeyEmitInfo } from '@motajs/system';
export interface IHotkeyUsing {
/**
* 获取本次 `using` 所使用的按键作用域
*/
scope(): Promise<symbol>;
/**
* 实现按键指令
* @param command 按键指令
* @param func 按键指令触发时执行的函数
*/
implement(command: string, func: (info: IHotkeyEmitInfo) => void): this;
}
@@ -1,526 +0,0 @@
import { KeyCode } from '@motajs/client-base';
import { gameKey, HotkeyJSON } from '@motajs/system';
import { GameStorage } from '@motajs/legacy-system';
export const mainScope = Symbol.for('@key_main');
// todo: 读取上一个手动存档,存档至下一个存档栏
// ----- Register
gameKey
//#region 游戏按键
.group('game', '游戏按键')
.register({
id: 'moveUp',
name: '上移',
defaults: KeyCode.UpArrow
})
.register({
id: 'moveDown',
name: '下移',
defaults: KeyCode.DownArrow
})
.register({
id: 'moveLeft',
name: '左移',
defaults: KeyCode.LeftArrow
})
.register({
id: 'moveRight',
name: '右移',
defaults: KeyCode.RightArrow
})
//#region ui界面
.group('ui', 'ui界面')
.register({
id: 'book',
name: '怪物手册',
defaults: KeyCode.KeyX
})
.register({
id: 'save',
name: '存档界面',
defaults: KeyCode.KeyS
})
.register({
id: 'load',
name: '读档界面',
defaults: KeyCode.KeyD
})
.register({
id: 'toolbox',
name: '道具栏',
defaults: KeyCode.KeyT
})
.register({
id: 'equipbox',
name: '装备栏',
defaults: KeyCode.KeyQ
})
.register({
id: 'fly',
name: '楼层传送',
defaults: KeyCode.KeyG
})
.register({
id: 'menu',
name: '菜单',
defaults: KeyCode.Escape
})
.register({
id: 'replay',
name: '录像回放',
defaults: KeyCode.KeyR
})
.register({
id: 'shop',
name: '快捷商店',
defaults: KeyCode.KeyV
})
.register({
id: 'statistics',
name: '数据统计',
defaults: KeyCode.KeyB
})
.register({
id: 'viewMap_1',
name: '浏览地图_1',
defaults: KeyCode.PageUp
})
.register({
id: 'viewMap_2',
name: '浏览地图_2',
defaults: KeyCode.PageDown
})
//#region 功能按键
.group('function', '功能按键')
.register({
id: 'undo_1',
name: '回退_1',
defaults: KeyCode.KeyA
})
.register({
id: 'undo_2',
name: '回退_2',
defaults: KeyCode.Digit5
})
.register({
id: 'redo_1',
name: '恢复_1',
defaults: KeyCode.KeyW
})
.register({
id: 'redo_2',
name: '恢复_2',
defaults: KeyCode.Digit6
})
.register({
id: 'turn',
name: '勇士转向',
defaults: KeyCode.KeyZ
})
.register({
id: 'getNext_1',
name: '轻按_1',
defaults: KeyCode.Space
})
.register({
id: 'getNext_2',
name: '轻按_2',
defaults: KeyCode.Digit7
})
.register({
id: 'quickEquip_1',
name: '切换/保存套装_1',
defaults: KeyCode.Digit1,
alt: true
})
.register({
id: 'quickEquip_2',
name: '切换/保存套装_2',
defaults: KeyCode.Digit2,
alt: true
})
.register({
id: 'quickEquip_3',
name: '切换/保存套装_3',
defaults: KeyCode.Digit3,
alt: true
})
.register({
id: 'quickEquip_4',
name: '切换/保存套装_4',
defaults: KeyCode.Digit4,
alt: true
})
.register({
id: 'quickEquip_5',
name: '切换/保存套装_5',
defaults: KeyCode.Digit5,
alt: true
})
.register({
id: 'quickEquip_6',
name: '切换/保存套装_6',
defaults: KeyCode.Digit6,
alt: true
})
.register({
id: 'quickEquip_7',
name: '切换/保存套装_7',
defaults: KeyCode.Digit7,
alt: true
})
.register({
id: 'quickEquip_8',
name: '切换/保存套装_8',
defaults: KeyCode.Digit8,
alt: true
})
.register({
id: 'quickEquip_9',
name: '切换/保存套装_9',
defaults: KeyCode.Digit9,
alt: true
})
.register({
id: 'quickEquip_0',
name: '切换/保存套装_0',
defaults: KeyCode.Digit0,
alt: true
})
//#region 系统按键
.group('system', '系统按键')
.register({
id: 'restart',
name: '回到开始界面',
defaults: KeyCode.KeyN
})
.register({
id: 'comment',
name: '评论区',
defaults: KeyCode.KeyP
})
.register({
id: 'debug',
name: '调试模式',
defaults: KeyCode.F8
})
//#region 通用按键
.group('general', '通用按键')
.register({
id: 'exit_1',
name: '退出ui界面_1',
defaults: KeyCode.KeyX
})
.register({
id: 'exit_2',
name: '退出ui界面_2',
defaults: KeyCode.Escape
})
.register({
id: 'confirm_1',
name: '确认_1',
defaults: KeyCode.Enter
})
.register({
id: 'confirm_2',
name: '确认_2',
defaults: KeyCode.Space
})
.register({
id: 'confirm_3',
name: '确认_3',
defaults: KeyCode.KeyC
})
//#region 开始界面
.group('@ui_start', '开始界面')
.register({
id: '@start_up',
name: '上移光标',
defaults: KeyCode.UpArrow
})
.register({
id: '@start_down',
name: '下移光标',
defaults: KeyCode.DownArrow
})
//#region 怪物手册
.group('@ui_book', '怪物手册')
.register({
id: '@book_up',
name: '上移光标',
defaults: KeyCode.UpArrow
})
.register({
id: '@book_down',
name: '下移光标',
defaults: KeyCode.DownArrow
})
.register({
id: '@book_pageDown_1',
name: '下移5个怪物_1',
defaults: KeyCode.RightArrow
})
.register({
id: '@book_pageDown_2',
name: '下移5个怪物_2',
defaults: KeyCode.PageDown
})
.register({
id: '@book_pageUp_1',
name: '上移5个怪物_1',
defaults: KeyCode.LeftArrow
})
.register({
id: '@book_pageUp_2',
name: '上移5个怪物_2',
defaults: KeyCode.PageUp
})
//#region 道具栏
.group('@ui_toolbox', '道具栏')
.register({
id: '@toolbox_right',
name: '光标右移',
defaults: KeyCode.RightArrow
})
.register({
id: '@toolbox_left',
name: '光标左移',
defaults: KeyCode.LeftArrow
})
.register({
id: '@toolbox_up',
name: '光标上移',
defaults: KeyCode.UpArrow
})
.register({
id: '@toolbox_down',
name: '光标下移',
defaults: KeyCode.DownArrow
})
//#region 商店
.group('@ui_shop', '商店')
.register({
id: '@shop_up',
name: '上移光标',
defaults: KeyCode.UpArrow
})
.register({
id: '@shop_down',
name: '下移光标',
defaults: KeyCode.DownArrow
})
.register({
id: '@shop_add',
name: '增加购买量',
defaults: KeyCode.RightArrow
})
.register({
id: '@shop_min',
name: '减少购买量',
defaults: KeyCode.LeftArrow
})
//#region 楼层传送
.group('@ui_fly', '楼层传送')
.register({
id: '@fly_left',
name: '左移地图',
defaults: KeyCode.LeftArrow
})
.register({
id: '@fly_right',
name: '右移地图',
defaults: KeyCode.RightArrow
})
.register({
id: '@fly_up',
name: '上移地图',
defaults: KeyCode.UpArrow
})
.register({
id: '@fly_down',
name: '下移地图',
defaults: KeyCode.DownArrow
})
.register({
id: '@fly_last',
name: '上一张地图',
defaults: KeyCode.PageDown
})
.register({
id: '@fly_next',
name: '下一张地图',
defaults: KeyCode.PageUp
})
//#region 传统楼传
.group('@ui_fly_tradition', '楼层传送-传统按键')
.register({
id: '@fly_down_t',
name: '上一张地图',
defaults: KeyCode.DownArrow
})
.register({
id: '@fly_up_t',
name: '下一张地图',
defaults: KeyCode.UpArrow
})
.register({
id: '@fly_left_t_1',
name: '前10张地图_1',
defaults: KeyCode.LeftArrow
})
.register({
id: '@fly_left_t_2',
name: '前10张地图_2',
defaults: KeyCode.PageDown
})
.register({
id: '@fly_right_t_1',
name: '后10张地图_1',
defaults: KeyCode.RightArrow
})
.register({
id: '@fly_right_t_2',
name: '后10张地图_2',
defaults: KeyCode.PageUp
})
// #region 存档界面
.group('@ui_save', '存档界面')
.register({
id: '@save_pageUp',
name: '向后翻页',
defaults: KeyCode.PageUp
})
.register({
id: '@save_pageDown',
name: '向前翻页',
defaults: KeyCode.PageDown
})
.register({
id: '@save_up',
name: '选择框向上',
defaults: KeyCode.UpArrow
})
.register({
id: '@save_down',
name: '选择框向下',
defaults: KeyCode.DownArrow
})
.register({
id: '@save_left',
name: '选择框向左',
defaults: KeyCode.LeftArrow
})
.register({
id: '@save_right',
name: '选择框向右',
defaults: KeyCode.RightArrow
})
//#region 浏览地图
.group('@ui_viewMap', '浏览地图')
.register({
id: '@viewMap_up_1',
name: '下一层地图_1',
defaults: KeyCode.UpArrow
})
.register({
id: '@viewMap_up_2',
name: '下一层地图_2',
defaults: KeyCode.PageUp
})
.register({
id: '@viewMap_down_1',
name: '上一层地图_1',
defaults: KeyCode.DownArrow
})
.register({
id: '@viewMap_down_2',
name: '上一层地图_2',
defaults: KeyCode.PageDown
})
.register({
id: '@viewMap_up_ten',
name: '下十层地图',
defaults: KeyCode.UpArrow,
ctrl: true
})
.register({
id: '@viewMap_down_ten',
name: '上十层地图',
defaults: KeyCode.DownArrow,
ctrl: true
})
.register({
id: '@viewMap_book',
name: '怪物手册',
defaults: KeyCode.KeyX
})
.register({
id: '@viewMap_fly',
name: '传送至',
defaults: KeyCode.KeyG
})
.register({
id: '@viewMap_reset',
name: '重置视角',
defaults: KeyCode.KeyR
});
// #endregion
gameKey.enable();
gameKey.use(mainScope);
//#region 按键实现
gameKey
.when(
() =>
!core.status.lockControl && !core.isMoving() && !core.isReplaying()
)
.realize('book', () => {
core.openBook(true);
})
.realize('toolbox', () => {
core.openToolbox(true);
})
.realize('equipbox', () => {
core.openEquipbox(true);
})
.realize('fly', () => {
core.useFly(true);
})
.realize('shop', () => {
core.openQuickShop(true);
})
.realize('undo', () => {
core.doSL('autoSave', 'load');
})
.realize('redo', () => {
core.doSL('autoSave', 'reload');
})
.realize('turn', () => {
core.turnHero();
})
.realize('getNext', () => {
core.getNextItem();
})
.realize('restart', () => {
core.confirmRestart();
})
.realize('comment', () => {
core.actions._clickGameInfo_openComments();
})
.realize('debug', () => {
core.debug();
});
// ----- Storage
const keyStorage = new GameStorage<Record<string, HotkeyJSON>>(
GameStorage.fromAuthor('AncTe', 'gameKey')
);
keyStorage.data = {};
keyStorage.read();
gameKey.on('set', (id, key, assist) => {
keyStorage.setValue(id, { key, assist });
});
gameKey.fromJSON(keyStorage.toJSON());
@@ -1,2 +1 @@
export * from './move';
export * from './hotkey';
+2
View File
@@ -301,6 +301,8 @@
"208": "Duplicate hotkey command group name: $1, old one will be overwritten.",
"209": "Cannot load key combinations for hotkey command '$1' since no command named it.",
"210": "Cannot add duplicate hotkey command '$1' to group '$2'.",
"211": "Expected a hotkey scope stored for a vue component, but no scope was found. This seems like a internal bug of engine, please report.",
"212": "Duplicate implements for hotkey command '$1', old one will be overwritten. This probably caused by unclearing implementation config, please check.",
"1001": "Event(setBlock): Unknown tile '$1' when setting block."
}
}
+25 -2
View File
@@ -141,9 +141,29 @@ export class HotkeySystem implements IHotkeySystem, IExcitable<number> {
//#region 作用域
enterScope(): symbol {
/**
* 获取下一个作用域的实现映射
* @param clear 是否清空作用域实现
*/
private nextScopeImpl(
clear: boolean
): Map<IHotkeyCommand, (info: IHotkeyEmitInfo) => void> {
const now = this.getScope();
if (!now) return new Map();
if (clear) {
return new Map();
} else {
return new Map(now.implements);
}
}
enterScope(clearImpl: boolean = true): symbol {
const symbol = Symbol();
this.scopes.push({ symbol, condition: null, implements: new Map() });
this.scopes.push({
symbol,
condition: null,
implements: this.nextScopeImpl(clearImpl)
});
this.holdingCmds.clear();
this.waitTimeout.clear();
return symbol;
@@ -191,6 +211,9 @@ export class HotkeySystem implements IHotkeySystem, IExcitable<number> {
logger.warn(202, command);
return this;
}
if (scope.implements.has(obj)) {
logger.warn(212, command);
}
scope.implements.set(obj, func);
return this;
}
+2 -1
View File
@@ -159,8 +159,9 @@ export interface IHotkeySystem {
/**
* 进入新的按键作用域
* @param clearImpl 是否清空上一个作用域的按键实现,默认清空
*/
enterScope(): symbol;
enterScope(clearImpl?: boolean): symbol;
/**
* 离开按键作用域
+9 -1
View File
@@ -10,7 +10,7 @@ export function initializeHotkey(state: IClientCore) {
//#region 通用按键
hotkey.setGroup('common', '通用按键', [
system.addCommand('@common/ok', '确认', {
system.addCommand('@common/confirm', '确认', {
defaults: [[KeyCode.Enter], [KeyCode.Space], [KeyCode.KeyC]],
priority: 0
}),
@@ -25,6 +25,14 @@ export function initializeHotkey(state: IClientCore) {
system.addCommand('@common/select-down', '下移选择光标', {
defaults: [[KeyCode.DownArrow]],
priority: 0
}),
system.addCommand('@common/select-left', '左移选择光标', {
defaults: [[KeyCode.LeftArrow]],
priority: 0
}),
system.addCommand('@common/select-right', '右移选择光标', {
defaults: [[KeyCode.RightArrow]],
priority: 0
})
]);
@@ -1,17 +1,18 @@
import { ElementLocator, Font } from '@motajs/render';
import { computed, defineComponent, reactive, ref } from 'vue';
import { Background, Selection } from './misc';
import { TextContent, TextContentProps } from './textbox';
import { TextAlign } from './textboxTyper';
import { Page, PageExpose } from './page';
import { Background, Selection } from '@user/client-base';
import {
TextContent,
TextContentProps,
TextAlign,
Page,
PageExpose
} from '@user/client-base';
import { GameUI, IUIController, SetupComponentOptions } from '@motajs/system';
// @ts-expect-error render/use.ts 按 D-54 留在实现层,本文件已移入系统层,按 D-68 暂以标注记录,不反向引用、不解耦
import { useKey } from '../use';
import { sleep } from 'mutate-animate';
import { DefaultProps } from '@motajs/render-vue';
import { IGameUIPropsBase } from '../types';
import { IGameUIPropsBase, useKey } from '@user/client-base';
export interface ConfirmBoxProps extends DefaultProps, TextContentProps {
export interface ConfirmBoxProps extends IGameUIPropsBase, TextContentProps {
/** 确认框的提示文本内容 */
text: string;
/** 确认框对话框的宽度 */
@@ -36,8 +37,8 @@ export interface ConfirmBoxProps extends DefaultProps, TextContentProps {
color?: CanvasStyle;
/** 对话框边框颜色,当未设置 winskin 时生效 */
border?: CanvasStyle;
/** 按键作用域,如果需要同作用域按键,那么需要传入 */
scope?: symbol;
/** 是否使用全新的按键作用域,此时选择框的按键作用域不会包含原本的按键指令实现,默认会使用全新作用域 */
freshScope?: boolean;
}
export type ConfirmBoxEmits = {
@@ -59,7 +60,7 @@ const confirmBoxProps = {
'defaultYes',
'color',
'border',
'scope'
'freshScope'
],
emits: ['no', 'yes']
} satisfies SetupComponentOptions<
@@ -149,13 +150,13 @@ export const ConfirmBox = defineComponent<
noSize.value = [width, height];
};
const [key] = useKey(false, props.scope);
key.realize('confirm', () => {
const key = useKey(props.state.hotkey.system, props.freshScope ?? true)!;
key.implement('@common/confirm', () => {
if (selected.value) emit('yes');
else emit('no');
});
key.realize('moveLeft', () => void (selected.value = true));
key.realize('moveRight', () => void (selected.value = false));
key.implement('@common/select-left', () => void (selected.value = true));
key.implement('@common/select-right', () => void (selected.value = false));
return () => (
<container loc={loc.value}>
@@ -215,7 +216,7 @@ export type ChoiceItem<T extends ChoiceKey = ChoiceKey> = [
text: string
];
export interface ChoicesProps extends DefaultProps, TextContentProps {
export interface ChoicesProps extends IGameUIPropsBase, TextContentProps {
/** 选项数组 */
choices: ChoiceItem[];
/** 选择框对话框的位置 */
@@ -248,8 +249,8 @@ export interface ChoicesProps extends DefaultProps, TextContentProps {
interval?: number;
/** 默认选中的选项索引 */
selected?: number;
/** 按键作用域,如果需要同作用域按键,那么需要传入,例如系统设置 UI */
scope?: symbol;
/** 是否使用全新的按键作用域,此时选择框的按键作用域不会包含原本的按键指令实现,默认会使用全新作用域 */
freshScope?: boolean;
}
export type ChoicesEmits = {
@@ -274,7 +275,7 @@ const choicesProps = {
'pad',
'interval',
'selected',
'scope'
'freshScope'
],
emits: ['choose']
} satisfies SetupComponentOptions<
@@ -473,8 +474,8 @@ export const Choices = defineComponent<
selected.value = 0;
};
const [key] = useKey(false, props.scope);
key.realize('moveUp', () => {
const key = useKey(props.state.hotkey.system, props.freshScope)!;
key.implement('moveUp', () => {
if (selected.value === 0) {
if (pageCom.value?.now() !== 0) {
pageCom.value?.movePage(-1);
@@ -484,7 +485,7 @@ export const Choices = defineComponent<
selected.value--;
}
});
key.realize('moveDown', () => {
key.implement('moveDown', () => {
const page = pageCom.value?.now() ?? 0;
if (selected.value === choiceCountPerPage.value - 1) {
if (page < pages.value - 1) {
@@ -498,9 +499,9 @@ export const Choices = defineComponent<
}
}
});
key.realize('moveLeft', () => pageCom.value?.movePage(-1));
key.realize('moveRight', () => pageCom.value?.movePage(1));
key.realize('confirm', () => {
key.implement('moveLeft', () => pageCom.value?.movePage(-1));
key.implement('moveRight', () => pageCom.value?.movePage(1));
key.implement('confirm', () => {
const page = pageCom.value?.now() ?? 0;
const index = page * choiceCountPerPage.value + selected.value;
emit('choose', props.choices[index][0]);