mirror of
https://github.com/unanmed/HumanBreak.git
synced 2026-11-15 03:04:30 +08:00
refactor: src/core/main 全部移入 monorepo
This commit is contained in:
1 parent
9ec13e5314
commit
960dbd429a
79 files changed
+1872
-3616
No files matched your search
+1
-1
@@ -26,7 +26,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { mainUi, fixedUi } from './core/main/init/ui';
|
||||
import { mainUi, fixedUi } from '@motajs/legacy-ui';
|
||||
|
||||
onMounted(() => {
|
||||
const { hook } = Mota.requireAll('var');
|
||||
|
||||
+7
-57
@@ -1,35 +1,12 @@
|
||||
// todo: 这个引入不加会报错,应该是循环引用导致的
|
||||
import '@/plugin/utils';
|
||||
import { Focus, GameUi, UiController } from './main/custom/ui';
|
||||
import { GameStorage } from './main/storage';
|
||||
import * as LegacyUI from '@motajs/legacy-ui';
|
||||
import * as LegacySystem from '@motajs/legacy-system';
|
||||
import './main/init/';
|
||||
import './main/custom/toolbar';
|
||||
import { fixedUi, mainUi } from './main/init/ui';
|
||||
import {
|
||||
MotaSetting,
|
||||
SettingDisplayer,
|
||||
mainSetting,
|
||||
settingStorage
|
||||
} from './main/setting';
|
||||
import { KeyCode } from '@motajs/client-base';
|
||||
import '@/plugin';
|
||||
import './package';
|
||||
import { CustomToolbar } from './main/custom/toolbar';
|
||||
import {
|
||||
Hotkey,
|
||||
checkAssist,
|
||||
isAssist,
|
||||
unwarpBinary,
|
||||
gameKey
|
||||
} from './main/custom/hotkey';
|
||||
import { Keyboard, generateKeyboardEvent } from './main/custom/keyboard';
|
||||
import './main/layout';
|
||||
import { createSettingComponents } from './main/init/settings';
|
||||
import {
|
||||
createToolbarComponents,
|
||||
createToolbarEditorComponents
|
||||
} from './main/init/toolbar';
|
||||
import { VirtualKey } from './main/init/misc';
|
||||
import * as System from '@motajs/system';
|
||||
import { UI } from '@motajs/legacy-ui';
|
||||
import Box from '@/components/box.vue';
|
||||
import BoxAnimate from '@/components/boxAnimate.vue';
|
||||
@@ -41,47 +18,20 @@ import EnemySpecial from '@/panel/enemySpecial.vue';
|
||||
import EnemyTarget from '@/panel/enemyTarget.vue';
|
||||
import KeyboardPanel from '@/panel/keyboard.vue';
|
||||
import { logger } from '@motajs/common';
|
||||
import { Danmaku } from './main/custom/danmaku';
|
||||
import * as Shadow from './fx/shadow';
|
||||
import { Render } from '@motajs/client';
|
||||
import { HeroKeyMover } from './main/action/move';
|
||||
import { HeroKeyMover } from '../module/action/move';
|
||||
import * as Animation from 'mutate-animate';
|
||||
import '@/module';
|
||||
|
||||
// ----- 类注册
|
||||
Mota.register('class', 'CustomToolbar', CustomToolbar);
|
||||
Mota.register('class', 'Focus', Focus);
|
||||
Mota.register('class', 'GameStorage', GameStorage);
|
||||
Mota.register('class', 'GameUi', GameUi);
|
||||
Mota.register('class', 'Hotkey', Hotkey);
|
||||
Mota.register('class', 'Keyboard', Keyboard);
|
||||
Mota.register('class', 'MotaSetting', MotaSetting);
|
||||
Mota.register('class', 'SettingDisplayer', SettingDisplayer);
|
||||
Mota.register('class', 'UiController', UiController);
|
||||
Mota.register('class', 'Danmaku', Danmaku);
|
||||
// ----- 函数注册
|
||||
Mota.register('fn', 'unwrapBinary', unwarpBinary);
|
||||
Mota.register('fn', 'checkAssist', checkAssist);
|
||||
Mota.register('fn', 'isAssist', isAssist);
|
||||
Mota.register('fn', 'generateKeyboardEvent', generateKeyboardEvent);
|
||||
// ----- 变量注册
|
||||
Mota.register('var', 'mainUi', mainUi);
|
||||
Mota.register('var', 'fixedUi', fixedUi);
|
||||
Mota.register('var', 'gameKey', gameKey);
|
||||
Mota.register('var', 'mainSetting', mainSetting);
|
||||
Mota.register('var', 'KeyCode', KeyCode);
|
||||
Mota.register('var', 'settingStorage', settingStorage);
|
||||
Mota.register('var', 'status', status);
|
||||
Mota.register('var', 'logger', logger);
|
||||
// ----- 模块注册
|
||||
Mota.register('module', 'CustomComponents', {
|
||||
createSettingComponents,
|
||||
createToolbarComponents,
|
||||
createToolbarEditorComponents
|
||||
});
|
||||
Mota.register('module', 'MiscComponents', {
|
||||
VirtualKey
|
||||
});
|
||||
Mota.register('module', 'LegacyUI', LegacyUI);
|
||||
Mota.register('module', 'System', System);
|
||||
Mota.register('module', 'LegacySystem', LegacySystem);
|
||||
Mota.register('module', 'RenderUtils', utils);
|
||||
Mota.register('module', 'UI', UI);
|
||||
Mota.register('module', 'UIComponents', {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
export interface Undoable<T> {
|
||||
stack: T[];
|
||||
redoStack: T[];
|
||||
|
||||
/**
|
||||
* 撤销
|
||||
*/
|
||||
undo(): T | undefined;
|
||||
|
||||
/**
|
||||
* 重做
|
||||
*/
|
||||
redo(): T | undefined;
|
||||
}
|
||||
|
||||
export interface ResponseBase {
|
||||
code: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type CSSObj = Partial<Record<CanParseCss, string>>;
|
||||
@@ -1,532 +0,0 @@
|
||||
import BoxAnimate from '@/components/boxAnimate.vue';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { logger } from '@motajs/common';
|
||||
import { ResponseBase } from '@/core/interface';
|
||||
import {
|
||||
deleteWith,
|
||||
ensureArray,
|
||||
getIconHeight,
|
||||
parseCss,
|
||||
tip
|
||||
} from '@motajs/legacy-ui';
|
||||
import axios, { AxiosResponse, toFormData } from 'axios';
|
||||
import { Component, VNode, h, shallowReactive } from 'vue';
|
||||
// /* @__PURE__ */ import { id, password } from '../../../../user';
|
||||
import { mainSetting } from '../setting';
|
||||
|
||||
type CSSObj = Partial<Record<CanParseCss, string>>;
|
||||
|
||||
interface DanmakuResponse extends ResponseBase {
|
||||
total: number;
|
||||
list: DanmakuInfo[];
|
||||
}
|
||||
|
||||
interface DanmakuInfo {
|
||||
id: string;
|
||||
comment: string;
|
||||
tags: string;
|
||||
love: string;
|
||||
my_love_type: boolean;
|
||||
userid: string;
|
||||
deler: string;
|
||||
upload_time: string;
|
||||
tower_name: string;
|
||||
}
|
||||
|
||||
interface DanmakuPostInfo extends Partial<DanmakuContentInfo> {
|
||||
type: 1 | 2 | 3;
|
||||
towername: 'HumanBreak';
|
||||
id?: number;
|
||||
}
|
||||
|
||||
interface DanmakuContentInfo {
|
||||
comment: string;
|
||||
tags: string;
|
||||
}
|
||||
|
||||
interface PostDanmakuResponse extends ResponseBase {
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface PostLikeResponse extends ResponseBase {
|
||||
liked: boolean;
|
||||
}
|
||||
|
||||
interface DanmakuEvent {
|
||||
showStart: [danmaku: Danmaku];
|
||||
showEnd: [danmaku: Danmaku];
|
||||
like: [liked: boolean, danmaku: Danmaku];
|
||||
}
|
||||
|
||||
type SpecContentFn = (content: string, type: string) => VNode;
|
||||
|
||||
interface AllowedCSS {
|
||||
property: string;
|
||||
check: (value: string, prop: string) => true | string;
|
||||
}
|
||||
|
||||
const allowedCSS: Partial<Record<CanParseCss, AllowedCSS>> = {
|
||||
color: {
|
||||
property: 'color',
|
||||
check: () => true
|
||||
},
|
||||
backgroundColor: {
|
||||
property: 'backgroundColor',
|
||||
check: () => true
|
||||
},
|
||||
fontSize: {
|
||||
property: 'fontSize',
|
||||
check: value => {
|
||||
if (!/^\d+%$/.test(value)) {
|
||||
return '字体大小只能设置为百分格式';
|
||||
}
|
||||
if (parseInt(value) > 200) {
|
||||
return '字体最大只能为200%';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export class Danmaku extends EventEmitter<DanmakuEvent> {
|
||||
static backend: string = `/backend/tower/barrage.php`;
|
||||
static all: Set<Danmaku> = new Set();
|
||||
static allInPos: Partial<Record<FloorIds, Record<LocString, Danmaku[]>>> =
|
||||
{};
|
||||
|
||||
static showList: Danmaku[] = shallowReactive([]);
|
||||
static showMap: Map<number, Danmaku> = new Map();
|
||||
static specList: Record<string, SpecContentFn> = {};
|
||||
|
||||
static lastEditoredDanmaku?: Danmaku;
|
||||
|
||||
id: number = -1;
|
||||
text: string = '';
|
||||
x: number = 0;
|
||||
y: number = 0;
|
||||
floor?: FloorIds;
|
||||
showing: boolean = false;
|
||||
likedNum: number = 0;
|
||||
liked: boolean = false;
|
||||
|
||||
style: CSSObj = {};
|
||||
textColor: string = 'white';
|
||||
strokeColor: string = 'black';
|
||||
|
||||
private posted: boolean = false;
|
||||
vNode?: VNode;
|
||||
private posting: boolean = false;
|
||||
|
||||
/**
|
||||
* 发送弹幕
|
||||
* @returns 弹幕发送的 Axios Post 信息,为 Promise
|
||||
*/
|
||||
async post(): Promise<AxiosResponse<PostDanmakuResponse>> {
|
||||
if (this.posted || this.posting) {
|
||||
logger.warn(5);
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
const data: DanmakuPostInfo = {
|
||||
type: 2,
|
||||
towername: 'HumanBreak',
|
||||
...this.encode()
|
||||
};
|
||||
|
||||
this.posting = true;
|
||||
const form = toFormData(data);
|
||||
// /* @__PURE__ */ form.append('userid', id);
|
||||
// /* @__PURE__ */ form.append('password', password);
|
||||
|
||||
try {
|
||||
const res = await axios.post<PostDanmakuResponse>(
|
||||
Danmaku.backend,
|
||||
form
|
||||
);
|
||||
|
||||
this.id = res.data.id;
|
||||
this.posting = false;
|
||||
|
||||
if (res.data.code === 0) {
|
||||
this.posted = true;
|
||||
tip('success', '发送成功');
|
||||
this.addToList();
|
||||
} else {
|
||||
tip('error', res.data.message);
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
this.posted = false;
|
||||
this.posting = false;
|
||||
logger.error(1, String(e));
|
||||
return Promise.reject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将弹幕整合为可以发送的格式
|
||||
*/
|
||||
encode(): DanmakuContentInfo {
|
||||
const css = this.getEncodedCSS();
|
||||
return {
|
||||
comment: this.text,
|
||||
tags: JSON.stringify([
|
||||
`!css:${JSON.stringify(css)}`,
|
||||
`!pos:${this.x},${this.y},${this.floor}`
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析弹幕信息
|
||||
* @param info 要被解析的弹幕信息
|
||||
*/
|
||||
decode(info: DanmakuContentInfo) {
|
||||
this.text = info.comment;
|
||||
|
||||
ensureArray(JSON.parse(info.tags) as string[]).forEach(v => {
|
||||
if (v.startsWith('!css:')) {
|
||||
this.style = JSON.parse(v.slice(5));
|
||||
} else if (v.startsWith('!pos:')) {
|
||||
const [x, y, f] = v.slice(5).split(',');
|
||||
this.x = parseInt(x);
|
||||
this.y = parseInt(y);
|
||||
this.floor = f as FloorIds;
|
||||
} else {
|
||||
logger.warn(3, v);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getEncodedCSS() {
|
||||
const css = JSON.parse(JSON.stringify(this.style)) as CSSObj;
|
||||
if (!css.color) css.color = this.textColor;
|
||||
if (!css.textShadow)
|
||||
css.textShadow = `1px 1px 1px ${this.strokeColor}, 1px -1px 1px ${this.strokeColor}, -1px 1px 1px ${this.strokeColor}, -1px -1px 1px ${this.strokeColor}`;
|
||||
return { ...css, ...this.style };
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置文字的颜色
|
||||
* @param fill 填充颜色
|
||||
* @param stroke 描边颜色
|
||||
*/
|
||||
color(fill?: string, stroke?: string) {
|
||||
fill && (this.textColor = fill);
|
||||
stroke && (this.strokeColor = stroke);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个图标
|
||||
* @param icon 要显示的图标id
|
||||
*/
|
||||
addIcon(icon: AllIds) {
|
||||
this.text += `[i:${icon}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置这个弹幕整体的css信息
|
||||
* @param str css字符串
|
||||
* @param overwrite 是否完全覆写原来的css
|
||||
*/
|
||||
css(str: string, overwrite?: boolean): void;
|
||||
/**
|
||||
* 设置这个弹幕整体的css信息
|
||||
* @param str css对象,参考 CSSStyleDeclaration
|
||||
* @param overwrite 是否完全覆写原来的css
|
||||
*/
|
||||
css(obj: CSSObj, overwrite?: boolean): void;
|
||||
css(obj: string | CSSObj, overwrite: boolean = false) {
|
||||
const res = typeof obj === 'string' ? parseCss(obj) : obj;
|
||||
const allow = Danmaku.checkCSSAllow(res);
|
||||
if (allow.length === 0) {
|
||||
if (overwrite) this.style = res;
|
||||
else {
|
||||
this.style = { ...this.style, ...res };
|
||||
}
|
||||
} else {
|
||||
logger.error(8, allow.join(','));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将这个弹幕添加至弹幕列表
|
||||
*/
|
||||
addToList() {
|
||||
Danmaku.all.add(this);
|
||||
if (!this.floor) return;
|
||||
Danmaku.allInPos[this.floor] ??= {};
|
||||
Danmaku.allInPos[this.floor]![`${this.x},${this.y}`] ??= [];
|
||||
Danmaku.allInPos[this.floor]![`${this.x},${this.y}`].push(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析这个弹幕为 VNode
|
||||
*/
|
||||
parse() {
|
||||
let pointer = -1;
|
||||
let ignore = false;
|
||||
|
||||
let str = '';
|
||||
|
||||
let spec = false;
|
||||
let specType = '';
|
||||
let specTypeEnd = false;
|
||||
let specContent = '';
|
||||
|
||||
const children: VNode[] = [];
|
||||
|
||||
while (++pointer < this.text.length) {
|
||||
const char = this.text[pointer];
|
||||
|
||||
if (char === '\\' && !ignore) {
|
||||
ignore = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ignore) {
|
||||
str += char;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '[') {
|
||||
spec = true;
|
||||
children.push(h('span', str));
|
||||
str = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === ']') {
|
||||
if (!spec) {
|
||||
logger.warn(4);
|
||||
str += char;
|
||||
} else {
|
||||
spec = false;
|
||||
specTypeEnd = false;
|
||||
children.push(this.createSpecVNode(specType, specContent));
|
||||
specType = '';
|
||||
specContent = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (spec) {
|
||||
if (!specTypeEnd) {
|
||||
if (char !== ':') {
|
||||
specType += char;
|
||||
} else {
|
||||
specTypeEnd = true;
|
||||
}
|
||||
} else {
|
||||
specContent += char;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
str += char;
|
||||
}
|
||||
|
||||
if (str.length > 0) {
|
||||
children.push(h('span', str));
|
||||
}
|
||||
|
||||
return h(
|
||||
'span',
|
||||
{ class: 'danmaku', style: this.getEncodedCSS() },
|
||||
children
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本弹幕的VNode
|
||||
*/
|
||||
getVNode(nocache: boolean = false) {
|
||||
if (nocache) return (this.vNode = this.parse());
|
||||
return this.vNode ?? (this.vNode = this.parse());
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示这个弹幕
|
||||
*/
|
||||
show() {
|
||||
if (this.showing) return;
|
||||
this.showing = true;
|
||||
Danmaku.showList.push(this);
|
||||
Danmaku.showMap.set(this.id, this);
|
||||
this.emit('showStart', this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示结束这个弹幕
|
||||
*/
|
||||
showEnd() {
|
||||
if (!this.showing) return;
|
||||
this.showing = false;
|
||||
deleteWith(Danmaku.showList, this);
|
||||
Danmaku.showMap.delete(this.id);
|
||||
this.emit('showEnd', this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 点赞或取消点赞
|
||||
*/
|
||||
async triggerLike() {
|
||||
const post: DanmakuPostInfo = {
|
||||
type: 3,
|
||||
towername: 'HumanBreak',
|
||||
id: this.id
|
||||
};
|
||||
|
||||
const form = toFormData(post);
|
||||
// /* @__PURE__ */ form.append('userid', id);
|
||||
// /* @__PURE__ */ form.append('password', password);
|
||||
|
||||
const res = await axios.post<PostLikeResponse>(Danmaku.backend, form);
|
||||
if (res.data.code !== 0) {
|
||||
logger.warn(18, this.id.toString());
|
||||
tip('error', `Error ${res.data.code}. ${res.data.message}`);
|
||||
} else {
|
||||
tip('success', res.data.message);
|
||||
|
||||
if (res.data.liked) {
|
||||
this.liked = true;
|
||||
this.likedNum++;
|
||||
} else {
|
||||
this.liked = false;
|
||||
this.likedNum--;
|
||||
}
|
||||
this.emit('like', this.liked, this);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁这个弹幕
|
||||
*/
|
||||
destroy() {
|
||||
this.showEnd();
|
||||
Danmaku.all.delete(this);
|
||||
if (this.floor) {
|
||||
const floor = Danmaku.allInPos[this.floor];
|
||||
if (floor) {
|
||||
delete floor[`${this.x},${this.y}`];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createSpecVNode(type: string, content: string): VNode {
|
||||
if (Danmaku.specList[type]) {
|
||||
return Danmaku.specList[type](content, type);
|
||||
} else {
|
||||
logger.warn(7, type);
|
||||
}
|
||||
|
||||
return h('span');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查CSS内容是否符合发弹幕要求
|
||||
* @param css 要检查的CSS内容
|
||||
*/
|
||||
static checkCSSAllow(css: CSSObj) {
|
||||
const problem: string[] = [];
|
||||
for (const [key, value] of Object.entries(css)) {
|
||||
if (!allowedCSS[key as CanParseCss]) {
|
||||
problem.push(`不允许的CSS:${key}`);
|
||||
continue;
|
||||
} else {
|
||||
const res = allowedCSS[key as CanParseCss]!.check(value, key);
|
||||
if (res !== true) {
|
||||
problem.push(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取本塔所有弹幕
|
||||
*/
|
||||
static async fetch() {
|
||||
Danmaku.all.clear();
|
||||
Danmaku.allInPos = {};
|
||||
const form = toFormData({
|
||||
type: 1,
|
||||
towername: 'HumanBreak'
|
||||
});
|
||||
// /* @__PURE__ */ form.append('userid', id);
|
||||
// /* @__PURE__ */ form.append('password', password);
|
||||
const data = await axios.post<DanmakuResponse>(Danmaku.backend, form);
|
||||
|
||||
data.data.list.forEach(v => {
|
||||
const dan = new Danmaku();
|
||||
dan.id = parseInt(v.id);
|
||||
dan.likedNum = parseInt(v.love);
|
||||
dan.liked = v.my_love_type;
|
||||
dan.decode(v);
|
||||
dan.posted = true;
|
||||
dan.addToList();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示一个弹幕
|
||||
* @param dan 要显示的弹幕
|
||||
*/
|
||||
static show(dan: Danmaku) {
|
||||
dan.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册一个特殊显示内容
|
||||
* @param type 特殊内容类型
|
||||
* @param fn 特殊内容显示函数,返回VNode
|
||||
*/
|
||||
static registerSpecContent(type: string, fn: SpecContentFn) {
|
||||
if (this.specList[type]) {
|
||||
logger.warn(6, type);
|
||||
}
|
||||
this.specList[type] = fn;
|
||||
}
|
||||
}
|
||||
|
||||
// 图标类型
|
||||
Danmaku.registerSpecContent('i', content => {
|
||||
const height = getIconHeight(content as AllIds);
|
||||
|
||||
return h(BoxAnimate as Component, {
|
||||
id: content,
|
||||
noborder: true,
|
||||
noAnimate: true,
|
||||
width: 32,
|
||||
height
|
||||
});
|
||||
});
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
Danmaku.backend = `/danmaku`;
|
||||
}
|
||||
|
||||
Mota.require('var', 'hook').once('reset', () => {
|
||||
Danmaku.fetch();
|
||||
});
|
||||
|
||||
// 勇士移动后显示弹幕
|
||||
Mota.require('var', 'hook').on('moveOneStep', (x, y, floor) => {
|
||||
const enabled = mainSetting.getValue('ui.danmaku', true);
|
||||
if (!enabled) return;
|
||||
const f = Danmaku.allInPos[floor];
|
||||
if (f) {
|
||||
const danmaku = f[`${x},${y}`];
|
||||
if (danmaku) {
|
||||
danmaku.forEach(v => {
|
||||
setTimeout(() => {
|
||||
v.show();
|
||||
}, Math.random() * 1000);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,463 +0,0 @@
|
||||
import { KeyCode } from '@motajs/client-base';
|
||||
import {
|
||||
deleteWith,
|
||||
generateBinary,
|
||||
keycode,
|
||||
spliceBy
|
||||
} from '@motajs/legacy-ui';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
// todo: 按下加速节流触发
|
||||
|
||||
interface HotkeyEvent {
|
||||
set: [id: string, key: KeyCode, assist: number];
|
||||
emit: [key: KeyCode, assist: number, type: KeyEmitType];
|
||||
press: [key: KeyCode];
|
||||
release: [key: KeyCode];
|
||||
}
|
||||
|
||||
type KeyEmitType =
|
||||
| 'up'
|
||||
| 'down'
|
||||
| 'down-repeat'
|
||||
| 'down-throttle'
|
||||
// todo: | 'down-accelerate'
|
||||
| 'down-timeout';
|
||||
|
||||
type KeyEventType = 'up' | 'down';
|
||||
|
||||
interface AssistHoykey {
|
||||
ctrl: boolean;
|
||||
shift: boolean;
|
||||
alt: boolean;
|
||||
}
|
||||
|
||||
interface RegisterHotkeyData extends Partial<AssistHoykey> {
|
||||
id: string;
|
||||
name: string;
|
||||
defaults: KeyCode;
|
||||
}
|
||||
|
||||
export interface HotkeyData extends Required<RegisterHotkeyData> {
|
||||
key: KeyCode;
|
||||
emits: Map<symbol, HotkeyEmitData>;
|
||||
group?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id 此处的id包含数字后缀
|
||||
* @returns 返回 `@void` 时,表示此次触发没有包含副作用,不会致使 `preventDefault` 被执行
|
||||
*/
|
||||
type HotkeyFunc = (
|
||||
id: string,
|
||||
code: KeyCode,
|
||||
ev: KeyboardEvent
|
||||
) => void | '@void';
|
||||
|
||||
interface HotkeyEmitData {
|
||||
func: HotkeyFunc;
|
||||
onUp?: HotkeyFunc;
|
||||
config?: HotkeyEmitConfig;
|
||||
}
|
||||
|
||||
export interface HotkeyJSON {
|
||||
key: KeyCode;
|
||||
assist: number;
|
||||
}
|
||||
|
||||
export interface HotkeyEmitConfig {
|
||||
type: KeyEmitType;
|
||||
throttle?: number;
|
||||
accelerate?: number;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export class Hotkey extends EventEmitter<HotkeyEvent> {
|
||||
static list: Hotkey[];
|
||||
|
||||
id: string;
|
||||
name: string;
|
||||
data: Record<string, HotkeyData> = {};
|
||||
keyMap: Map<KeyCode, HotkeyData[]> = new Map();
|
||||
/** id to name */
|
||||
groupName: Record<string, string> = {
|
||||
none: '未分类按键'
|
||||
};
|
||||
/** id to keys */
|
||||
groups: Record<string, string[]> = {
|
||||
none: []
|
||||
};
|
||||
enabled: boolean = false;
|
||||
conditionMap: Map<symbol, () => boolean> = new Map();
|
||||
|
||||
/** 当前作用域 */
|
||||
scope: symbol = Symbol();
|
||||
private scopeStack: symbol[] = [];
|
||||
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) {
|
||||
super();
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册一个按键,id可以包含数字后缀,可以显示为同一个按键操作拥有多个按键可以触发
|
||||
* @param data 要注册的按键信息
|
||||
*/
|
||||
register(data: RegisterHotkeyData) {
|
||||
const d: HotkeyData = {
|
||||
...data,
|
||||
ctrl: !!data.ctrl,
|
||||
shift: !!data.shift,
|
||||
alt: !!data.alt,
|
||||
key: data.defaults,
|
||||
emits: new Map(),
|
||||
group: this.grouping
|
||||
};
|
||||
this.ensureMap(d.key);
|
||||
if (d.id in this.data) {
|
||||
console.warn(`已存在id为${d.id}的按键,已将其覆盖`);
|
||||
}
|
||||
this.data[d.id] = d;
|
||||
const arr = this.keyMap.get(d.key)!;
|
||||
arr.push(d);
|
||||
this.groups[this.grouping].push(d.id);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现一个按键按下时的操作
|
||||
* @param id 要实现的按键id,可以不包含数字后缀
|
||||
* @param func 按键按下时执行的函数
|
||||
* @param config 按键的配置信息
|
||||
*/
|
||||
realize(id: string, func: HotkeyFunc, config?: HotkeyEmitConfig) {
|
||||
const toSet = Object.values(this.data).filter(v => {
|
||||
const split = v.id.split('_');
|
||||
const last = !isNaN(Number(split.at(-1)));
|
||||
return v.id === id || (last && split.slice(0, -1).join('_') === id);
|
||||
});
|
||||
if (toSet.length === 0) {
|
||||
throw new Error(`Realize nonexistent key '${id}'.`);
|
||||
}
|
||||
for (const key of toSet) {
|
||||
const data = key.emits.get(this.scope);
|
||||
if (!data) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用一个symbol作为当前作用域,之后调用{@link realize}所实现的按键功能将会添加至此作用域
|
||||
* @param symbol 当前作用域的symbol
|
||||
*/
|
||||
use(symbol: symbol) {
|
||||
spliceBy(this.scopeStack, symbol);
|
||||
this.scopeStack.push(symbol);
|
||||
this.scope = symbol;
|
||||
this.conditionMap.set(symbol, () => true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放一个作用域,释放后作用域将退回至删除的作用域的上一级
|
||||
* @param symbol 要释放的作用域的symbol
|
||||
*/
|
||||
dispose(symbol: symbol = this.scopeStack.at(-1) ?? Symbol()) {
|
||||
for (const key of Object.values(this.data)) {
|
||||
key.emits.delete(symbol);
|
||||
}
|
||||
spliceBy(this.scopeStack, symbol);
|
||||
this.scope = this.scopeStack.at(-1) ?? Symbol();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一个按键信息
|
||||
* @param id 要设置的按键的id
|
||||
* @param key 要设置成的按键
|
||||
* @param assist 辅助按键,三位二进制数据,从低到高依次为`ctrl` `shift` `alt`
|
||||
* @param emit 是否触发set事件,当且仅当从fromJSON方法调用时为false
|
||||
*/
|
||||
set(id: string, key: KeyCode, assist: number, emit: boolean = true) {
|
||||
const { ctrl, shift, alt } = unwarpBinary(assist);
|
||||
const data = this.data[id];
|
||||
if (!data) return;
|
||||
const before = this.keyMap.get(data?.key ?? KeyCode.Unknown)! ?? [];
|
||||
deleteWith(before, data);
|
||||
this.ensureMap(key);
|
||||
const after = this.keyMap.get(key)!;
|
||||
after.push(data);
|
||||
data.key = key;
|
||||
data.ctrl = ctrl;
|
||||
data.shift = shift;
|
||||
data.alt = alt;
|
||||
if (emit) this.emit('set', id, key, assist);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发一个按键
|
||||
* @param key 要触发的按键
|
||||
* @param assist 辅助按键,三位二进制数据,从低到高依次为`ctrl` `shift` `alt`
|
||||
* @returns 是否有按键被触发
|
||||
*/
|
||||
emitKey(
|
||||
key: KeyCode,
|
||||
assist: number,
|
||||
type: KeyEventType,
|
||||
ev: KeyboardEvent
|
||||
): boolean {
|
||||
// 检查全局启用情况
|
||||
if (!this.enabled) return false;
|
||||
const when = this.conditionMap.get(this.scope)!;
|
||||
if (type === 'up') this.checkPressEnd(key);
|
||||
else if (type === 'down') this.checkPress(key);
|
||||
if (!when()) return false;
|
||||
const toEmit = this.keyMap.get(key);
|
||||
if (!toEmit) return false;
|
||||
|
||||
// 进行按键初始处理
|
||||
const { ctrl, shift, alt } = unwarpBinary(assist);
|
||||
|
||||
// 真正开始触发按键
|
||||
let emitted = false;
|
||||
|
||||
toEmit.forEach(v => {
|
||||
if (ctrl === v.ctrl && shift === v.shift && alt === v.alt) {
|
||||
const data = v.emits.get(this.scope);
|
||||
if (!data) return;
|
||||
|
||||
if (type === 'up' && data.onUp) {
|
||||
data.onUp(v.id, key, ev);
|
||||
emitted = true;
|
||||
return;
|
||||
}
|
||||
if (!this.canEmit(v.id, key, type, data)) return;
|
||||
const res = data.func(v.id, key, ev);
|
||||
if (res !== '@void') emitted = true;
|
||||
}
|
||||
});
|
||||
this.emit('emit', key, assist, type);
|
||||
|
||||
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 的按键将会加入此分组
|
||||
* @param id 组的id
|
||||
* @param name 组的名称
|
||||
*/
|
||||
group(id: string, name: string, keys?: RegisterHotkeyData[]) {
|
||||
this.grouping = id;
|
||||
this.groupName[id] = name;
|
||||
this.groups[id] ??= [];
|
||||
keys?.forEach(v => this.register(v));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用这个按键控制器
|
||||
*/
|
||||
enable() {
|
||||
this.enabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用这个按键控制器
|
||||
*/
|
||||
disable() {
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前作用域下,满足什么条件时触发按键
|
||||
* @param fn 条件函数
|
||||
*/
|
||||
when(fn: () => boolean) {
|
||||
this.conditionMap.set(this.scope, fn);
|
||||
return this;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const res: Record<string, HotkeyJSON> = {};
|
||||
for (const [key, data] of Object.entries(this.data)) {
|
||||
res[key] = {
|
||||
key: data.key,
|
||||
assist: generateBinary([data.ctrl, data.shift, data.alt])
|
||||
};
|
||||
}
|
||||
return JSON.stringify(res);
|
||||
}
|
||||
|
||||
fromJSON(data: string) {
|
||||
const json: Record<string, HotkeyJSON> = JSON.parse(data);
|
||||
for (const [key, data] of Object.entries(json)) {
|
||||
this.set(key, data.key, data.assist, false);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureMap(key: KeyCode) {
|
||||
if (!this.keyMap.has(key)) {
|
||||
this.keyMap.set(key, []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id获取hotkey实例
|
||||
* @param id 要获取的hotkey实例的id
|
||||
*/
|
||||
static get(id: string) {
|
||||
return this.list.find(v => v.id === id);
|
||||
}
|
||||
}
|
||||
|
||||
export function unwarpBinary(bin: number): AssistHoykey {
|
||||
return {
|
||||
ctrl: !!(bin & (1 << 0)),
|
||||
shift: !!(bin & (1 << 1)),
|
||||
alt: !!(bin & (1 << 2))
|
||||
};
|
||||
}
|
||||
|
||||
export function checkAssist(bin: number, key: KeyCode) {
|
||||
return (
|
||||
isAssist(key) &&
|
||||
!!(
|
||||
(1 << (key === KeyCode.Ctrl ? 0 : key === KeyCode.Shift ? 1 : 2)) &
|
||||
bin
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function isAssist(key: KeyCode) {
|
||||
return key === KeyCode.Ctrl || key === KeyCode.Shift || key === KeyCode.Alt;
|
||||
}
|
||||
|
||||
export const gameKey = new Hotkey('gameKey', '游戏按键');
|
||||
|
||||
// ----- Listening
|
||||
document.addEventListener('keyup', e => {
|
||||
const assist = generateBinary([e.ctrlKey, e.shiftKey, e.altKey]);
|
||||
const code = keycode(e.keyCode);
|
||||
if (gameKey.emitKey(code, assist, 'up', e)) {
|
||||
e.preventDefault();
|
||||
if (core.status.holdingKeys) {
|
||||
deleteWith(core.status.holdingKeys, e.keyCode);
|
||||
}
|
||||
} else {
|
||||
// polyfill样板
|
||||
if (main.dom.inputDiv.style.display == 'block') {
|
||||
if (e.keyCode == 13) {
|
||||
setTimeout(function () {
|
||||
main.dom.inputYes.click();
|
||||
}, 50);
|
||||
} else if (e.keyCode == 27) {
|
||||
setTimeout(function () {
|
||||
main.dom.inputNo.click();
|
||||
}, 50);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
core &&
|
||||
core.isPlaying &&
|
||||
core.status &&
|
||||
(core.isPlaying() || core.status.lockControl)
|
||||
)
|
||||
core.onkeyUp(e);
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
const assist = generateBinary([e.ctrlKey, e.shiftKey, e.altKey]);
|
||||
const code = keycode(e.keyCode);
|
||||
if (gameKey.emitKey(code, assist, 'down', e)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
@@ -1,201 +0,0 @@
|
||||
import { EventEmitter, Listener } from '@motajs/legacy-common';
|
||||
import { KeyCode } from '@motajs/client-base';
|
||||
import { gameKey } from './hotkey';
|
||||
import { unwarpBinary } from './hotkey';
|
||||
import { deleteWith, flipBinary } from '@motajs/legacy-ui';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { shallowReactive } from 'vue';
|
||||
|
||||
export interface KeyboardEmits {
|
||||
key: KeyCode;
|
||||
assist: number;
|
||||
}
|
||||
|
||||
interface KeyboardItem {
|
||||
key: KeyCode;
|
||||
text?: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface VirtualKeyEmit {
|
||||
preventDefault(): void;
|
||||
preventAssist(): void;
|
||||
}
|
||||
|
||||
type VirtualKeyEmitFn = (
|
||||
item: KeyboardItem,
|
||||
assist: number,
|
||||
index: number,
|
||||
ev: VirtualKeyEmit
|
||||
) => void;
|
||||
|
||||
interface VirtualKeyboardEvent {
|
||||
add: (item: KeyboardItem) => void;
|
||||
remove: (item: KeyboardItem) => void;
|
||||
extend: (extended: Keyboard) => void;
|
||||
emit: VirtualKeyEmitFn;
|
||||
scopeCreate: (scope: symbol) => void;
|
||||
scopeDispose: (scope: symbol) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 虚拟按键,同一个虚拟按键实例应当只能同时操作一个,但可以显示多个
|
||||
*/
|
||||
export class Keyboard extends EventEmitter<VirtualKeyboardEvent> {
|
||||
static list: Keyboard[] = [];
|
||||
|
||||
id: string;
|
||||
keys: KeyboardItem[] = [];
|
||||
assist: number = 0;
|
||||
fontSize: number = 18;
|
||||
|
||||
scope: symbol = Symbol();
|
||||
private scopeStack: symbol[] = [];
|
||||
private onEmitKey: Record<symbol, Listener<VirtualKeyEmitFn>[]> = {};
|
||||
private scopeAssist: Record<symbol, number> = {};
|
||||
|
||||
constructor(id: string) {
|
||||
super();
|
||||
this.id = id;
|
||||
Keyboard.list.push(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 给虚拟键盘添加一个按键
|
||||
* @param item 按键信息
|
||||
*/
|
||||
add(item: KeyboardItem) {
|
||||
const i = shallowReactive(item);
|
||||
this.keys.push(i);
|
||||
this.emit('add', i);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除一个按键
|
||||
* @param item 按键信息
|
||||
*/
|
||||
remove(item: KeyboardItem) {
|
||||
deleteWith(this.keys, item);
|
||||
this.emit('remove', item);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创造一个在某些辅助按键已经按下的情况下的作用域,这些被按下的辅助按键还可以被玩家手动取消
|
||||
* @param assist 辅助按键
|
||||
*/
|
||||
withAssist(assist: number) {
|
||||
const symbol = this.createScope();
|
||||
this.assist = assist;
|
||||
return symbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创造一个虚拟按键作用域,所有监听的事件与其他作用域不冲突
|
||||
* @returns 作用域的唯一标识符
|
||||
*/
|
||||
createScope() {
|
||||
const last = this.scopeStack.at(-1);
|
||||
const symbol = Symbol();
|
||||
this.scope = symbol;
|
||||
this.scopeStack.push(symbol);
|
||||
const ev: Listener<VirtualKeyEmitFn>[] = [];
|
||||
this.onEmitKey[symbol] = ev;
|
||||
// @ts-ignore
|
||||
this.events = ev;
|
||||
this.emit('scopeCreate', symbol);
|
||||
if (!!last) {
|
||||
this.scopeAssist[symbol] = this.assist;
|
||||
}
|
||||
this.assist = 0;
|
||||
this.scopeAssist[symbol] = 0;
|
||||
return symbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放一个作用域,同时删除其中的所有监听器
|
||||
*/
|
||||
disposeScope() {
|
||||
if (this.scopeStack.length === 0) {
|
||||
throw new Error(
|
||||
`Cannot dispose virtual key scope since there's no scope to be disposed.`
|
||||
);
|
||||
}
|
||||
const now = this.scopeStack.pop()!;
|
||||
delete this.onEmitKey[now];
|
||||
delete this.scopeAssist[now];
|
||||
const symbol = this.scopeStack.at(-1);
|
||||
this.emit('scopeDispose', now);
|
||||
if (!symbol) return;
|
||||
this.scope = symbol;
|
||||
this.assist = this.scopeAssist[symbol];
|
||||
// @ts-ignore
|
||||
this.events = this.onEmitKey[symbol];
|
||||
}
|
||||
|
||||
/**
|
||||
* 继承一个按键的按键信息
|
||||
* @param keyboard 要被继承的按键
|
||||
* @param offsetX 被继承的按键的横坐标偏移量
|
||||
* @param offsetY 被继承的按键的纵坐标偏移量
|
||||
*/
|
||||
extend(keyboard: Keyboard, offsetX: number = 0, offsetY: number = 0) {
|
||||
const toClone = cloneDeep(keyboard.keys);
|
||||
toClone.forEach(v => {
|
||||
v.x += offsetX;
|
||||
v.y += offsetY;
|
||||
});
|
||||
|
||||
this.keys.push(...toClone);
|
||||
this.emit('extend', keyboard);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发按键
|
||||
* @param key 要触发的按键
|
||||
*/
|
||||
emitKey(key: KeyboardItem, index: number) {
|
||||
let prevent = false;
|
||||
let preventAss = false;
|
||||
const preventDefault = () => (prevent = true);
|
||||
const preventAssist = () => (preventAss = true);
|
||||
this.emit('emit', key, this.assist, index, {
|
||||
preventDefault,
|
||||
preventAssist
|
||||
});
|
||||
|
||||
if (!preventAss) {
|
||||
if (key.key === KeyCode.Ctrl) {
|
||||
this.assist = flipBinary(this.assist, 0);
|
||||
} else if (key.key === KeyCode.Shift) {
|
||||
this.assist = flipBinary(this.assist, 1);
|
||||
} else if (key.key === KeyCode.Alt) {
|
||||
this.assist = flipBinary(this.assist, 2);
|
||||
}
|
||||
}
|
||||
if (!prevent) {
|
||||
const ev = generateKeyboardEvent(key.key, this.assist);
|
||||
gameKey.emitKey(key.key, this.assist, 'up', ev);
|
||||
}
|
||||
}
|
||||
|
||||
static get(id: string) {
|
||||
return this.list.find(v => v.id === id);
|
||||
}
|
||||
}
|
||||
|
||||
export function generateKeyboardEvent(key: KeyCode, assist: number) {
|
||||
const { ctrl, alt, shift } = unwarpBinary(assist);
|
||||
const ev = new KeyboardEvent('keyup', {
|
||||
ctrlKey: ctrl,
|
||||
shiftKey: shift,
|
||||
altKey: alt
|
||||
});
|
||||
|
||||
return ev;
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
import { EventEmitter } from '@motajs/legacy-common';
|
||||
import { deleteWith, has } from '@motajs/legacy-ui';
|
||||
import { Component, nextTick, reactive, shallowReactive } from 'vue';
|
||||
import { fixedUi } from '../init/ui';
|
||||
import { GameStorage } from '../storage';
|
||||
import type {
|
||||
CustomToolbarComponent,
|
||||
MiscToolbar,
|
||||
SettableItemData,
|
||||
ToolbarItemBase,
|
||||
ToolbarItemMap,
|
||||
ToolbarItemType
|
||||
} from '../init/toolbar';
|
||||
|
||||
interface CustomToolbarEvent {
|
||||
add: (item: ValueOf<ToolbarItemMap>) => void;
|
||||
delete: (item: ValueOf<ToolbarItemMap>) => void;
|
||||
set: (id: string, data: Partial<SettableItemData>) => void;
|
||||
emit: (id: string, item: ValueOf<ToolbarItemMap>) => void;
|
||||
posChange: (bar: CustomToolbar) => void;
|
||||
}
|
||||
|
||||
interface ToolbarSaveData {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
items: ValueOf<ToolbarItemMap>[];
|
||||
}
|
||||
|
||||
type ToolItemEmitFn<T extends ToolbarItemType> = (
|
||||
this: CustomToolbar,
|
||||
id: string,
|
||||
item: ToolbarItemMap[T]
|
||||
) => boolean;
|
||||
|
||||
interface RegisteredCustomToolInfo {
|
||||
name: string;
|
||||
onEmit: ToolItemEmitFn<ToolbarItemType>;
|
||||
show: CustomToolbarComponent;
|
||||
editor: CustomToolbarComponent;
|
||||
onCreate: (item: any) => ToolbarItemBase<ToolbarItemType>;
|
||||
}
|
||||
|
||||
type MiscEmitFn = (
|
||||
id: string,
|
||||
toolbar: CustomToolbar,
|
||||
item: MiscToolbar
|
||||
) => void;
|
||||
type ActivedFn = (info: MiscInfo) => boolean;
|
||||
|
||||
interface MiscInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
emit: MiscEmitFn;
|
||||
display: () => Component;
|
||||
activable?: boolean;
|
||||
actived?: ActivedFn;
|
||||
}
|
||||
|
||||
interface Misc {
|
||||
info: Record<string, MiscInfo>;
|
||||
|
||||
/**
|
||||
* 注册一个杂项工具
|
||||
* @param id 杂项工具的id
|
||||
* @param name 这个工具的名称
|
||||
* @param emit 触发这个杂项工具时执行的函数
|
||||
* @param display 这个工具的显示组件
|
||||
* @param activable 是否是可以被激活的工具,例如打开小地图后显示为激活状态
|
||||
*/
|
||||
register(
|
||||
this: Misc,
|
||||
id: string,
|
||||
name: string,
|
||||
emit: MiscEmitFn,
|
||||
display: () => Component
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 为一类杂项工具设置激活信息
|
||||
* @param id 杂项工具的id
|
||||
* @param activable 是否可激活
|
||||
* @param actived 获取当前是否激活的函数
|
||||
*/
|
||||
bindActivable(id: string, activable: boolean, actived?: ActivedFn): void;
|
||||
|
||||
/**
|
||||
* 刷新所有或指定的包含杂项工具的工具栏
|
||||
* @param id 指定包含这个杂项工具的工具栏刷新,例如填drag,则只会刷新包含drag杂项工具的工具栏
|
||||
*/
|
||||
requestRefresh(id?: string): void;
|
||||
}
|
||||
|
||||
const toolbarStorage = new GameStorage<Record<string, ToolbarSaveData>>(
|
||||
GameStorage.fromAuthor('AncTe', 'toolbar')
|
||||
);
|
||||
|
||||
const misc: Misc = {
|
||||
info: {},
|
||||
register(id, name, emit, display) {
|
||||
this.info[id] = { id, name, emit, display };
|
||||
},
|
||||
bindActivable(id, activable, actived) {
|
||||
this.info[id].activable = activable;
|
||||
this.info[id].actived = actived;
|
||||
},
|
||||
requestRefresh(id) {
|
||||
if (id) {
|
||||
CustomToolbar.list.forEach(v => {
|
||||
if (
|
||||
v.items.some(v => {
|
||||
return v.type === 'misc' && v.items?.includes(id);
|
||||
})
|
||||
) {
|
||||
v.refresh();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
CustomToolbar.list.forEach(v => {
|
||||
if (v.items.some(v => v.type === 'misc')) {
|
||||
v.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export class CustomToolbar extends EventEmitter<CustomToolbarEvent> {
|
||||
static num: number = 0;
|
||||
static list: CustomToolbar[] = shallowReactive([]);
|
||||
static info: Record<string, RegisteredCustomToolInfo> = {};
|
||||
|
||||
static misc: Misc = misc;
|
||||
|
||||
items: ValueOf<ToolbarItemMap>[] = reactive([]);
|
||||
num: number = CustomToolbar.num++;
|
||||
id: string;
|
||||
// ----- size
|
||||
x: number = 300;
|
||||
y: number = 300;
|
||||
width: number = 300;
|
||||
height: number = 70;
|
||||
// ----- other
|
||||
assistKey: number = 0;
|
||||
showIds: number[] = [];
|
||||
|
||||
constructor(id: string, noshow: boolean = false) {
|
||||
super();
|
||||
this.id = id;
|
||||
// 按比例设置初始大小
|
||||
const setting = Mota.require('var', 'mainSetting');
|
||||
const scale = setting.getValue('ui.toolbarScale', 100) / 100;
|
||||
this.width *= scale;
|
||||
this.height *= scale;
|
||||
this.x *= scale;
|
||||
this.y *= scale;
|
||||
|
||||
if (!noshow) this.show();
|
||||
CustomToolbar.list.push(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个自定义项
|
||||
* @param item 要添加的自定义工具栏项
|
||||
*/
|
||||
add<K extends ToolbarItemType>(item: ToolbarItemMap[K]) {
|
||||
const index = this.items.findIndex(v => v.id === item.id);
|
||||
if (index !== -1) {
|
||||
console.warn(`添加了id重复的自定义工具,已将其覆盖`);
|
||||
this.items[index] = item;
|
||||
} else {
|
||||
this.items.push(item);
|
||||
}
|
||||
this.emit('add', item);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个自定义项
|
||||
* @param id 要删除的项的id
|
||||
*/
|
||||
delete(id: string) {
|
||||
const index = this.items.findIndex(v => v.id === id);
|
||||
if (index === -1) return;
|
||||
const item = this.items[index];
|
||||
this.items.splice(index, 1);
|
||||
this.emit('delete', item);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一个项
|
||||
* @param id 要设置的项的id
|
||||
* @param item 要设置的属性内容
|
||||
*/
|
||||
set<T extends ToolbarItemType>(
|
||||
id: string,
|
||||
item: Partial<SettableItemData<T>>
|
||||
) {
|
||||
const toSet = this.items.find(v => v.id === id);
|
||||
if (!toSet) return;
|
||||
Object.assign(toSet, item);
|
||||
this.emit('set', id, item);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发一个自定义工具
|
||||
* @param id 要触发的自定义工具的id
|
||||
*/
|
||||
emitTool(id: string) {
|
||||
const item = this.items.find(v => v.id === id);
|
||||
if (!item) return this;
|
||||
this.emit('emit', id, item);
|
||||
const info = CustomToolbar.info[item.type];
|
||||
if (!info) {
|
||||
console.warn(`触发了未知的自定义工具类型:'${item.type}'`);
|
||||
return this;
|
||||
}
|
||||
const success = info.onEmit.call(this, id, item);
|
||||
if (!success) {
|
||||
console.warn(`触发自定义工具失败,id:'${id}',type:${item.type}`);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制刷新这个自定义工具栏的所有显示
|
||||
*/
|
||||
refresh(reopen: boolean = false) {
|
||||
if (reopen && this.showIds.length > 0) {
|
||||
this.closeAll();
|
||||
nextTick(() => {
|
||||
this.show();
|
||||
});
|
||||
} else {
|
||||
const items = this.items.splice(0);
|
||||
nextTick(() => {
|
||||
this.items.push(...items);
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
setPos(x?: number, y?: number) {
|
||||
has(x) && (this.x = x);
|
||||
has(y) && (this.y = y);
|
||||
this.emit('posChange', this);
|
||||
}
|
||||
|
||||
setSize(width?: number, height?: number) {
|
||||
has(width) && (this.width = width);
|
||||
has(height) && (this.height = height);
|
||||
this.emit('posChange', this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示这个自定义工具栏,可以显示多个,且内容互通
|
||||
* @param multi 是否允许显示多个,不填时,如果已经存在这个工具栏,那么将不会显示
|
||||
*/
|
||||
show(multi: boolean = false) {
|
||||
if (
|
||||
!multi &&
|
||||
this.showIds.some(v => fixedUi.stack.some(vv => vv.num === v))
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
const id = fixedUi.open('toolbar', { bar: this });
|
||||
this.showIds.push(id);
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭一个以此实例为基础显示的自定义工具栏
|
||||
* @param id 要关闭的id
|
||||
*/
|
||||
close(id: number) {
|
||||
fixedUi.close(id);
|
||||
deleteWith(this.showIds, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭这个自定义工具栏的所有显示
|
||||
*/
|
||||
closeAll() {
|
||||
this.showIds.forEach(v => fixedUi.close(v));
|
||||
this.showIds = [];
|
||||
}
|
||||
|
||||
static get(id: string) {
|
||||
return this.list.find(v => v.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册一类自定义工具
|
||||
* @param type 要注册的自定义工具类型
|
||||
* @param name 该类型的中文名
|
||||
* @param onEmit 当触发这个自定义工具的时候执行的函数
|
||||
* @param show 这个自定义工具在自定义工具栏的显示组件
|
||||
* @param editor 这个自定义工具在编辑时编辑组件
|
||||
* @param onCreate 当这个自定义工具在编辑器中被添加时,执行的初始化脚本
|
||||
*/
|
||||
static register<K extends ToolbarItemType>(
|
||||
type: K,
|
||||
name: string,
|
||||
onEmit: ToolItemEmitFn<K>,
|
||||
show: CustomToolbarComponent<K>,
|
||||
editor: CustomToolbarComponent<K>,
|
||||
onCreate: (item: any) => ToolbarItemMap[K]
|
||||
) {
|
||||
if (type in this.info) {
|
||||
console.warn(`已存在名为'${type}'的自定义工具类型,已将其覆盖!`);
|
||||
}
|
||||
const info: RegisteredCustomToolInfo = {
|
||||
name,
|
||||
onEmit: onEmit as ToolItemEmitFn<ToolbarItemType>,
|
||||
show: show as CustomToolbarComponent,
|
||||
editor: editor as CustomToolbarComponent,
|
||||
// @ts-ignore
|
||||
onCreate
|
||||
};
|
||||
this.info[type] = info;
|
||||
}
|
||||
|
||||
static save() {
|
||||
toolbarStorage.clear();
|
||||
const setting = Mota.require('var', 'mainSetting');
|
||||
const scale = setting.getValue('ui.toolbarScale', 100) / 100;
|
||||
this.list.forEach(v => {
|
||||
const toSave: ToolbarSaveData = {
|
||||
x: v.x,
|
||||
y: v.y,
|
||||
w: v.width / scale,
|
||||
h: v.height / scale,
|
||||
items: []
|
||||
};
|
||||
v.items.forEach(v => {
|
||||
toSave.items.push(v);
|
||||
});
|
||||
toolbarStorage.setValue(v.id, toSave);
|
||||
});
|
||||
toolbarStorage.write();
|
||||
}
|
||||
|
||||
static load() {
|
||||
toolbarStorage.read();
|
||||
for (const [key, value] of Object.entries(toolbarStorage.data)) {
|
||||
const bar = this.get(key) ?? new CustomToolbar(key);
|
||||
bar.x = value!.x;
|
||||
bar.y = value!.y;
|
||||
bar.width = value!.w;
|
||||
bar.height = value!.h;
|
||||
for (const item of value!.items) {
|
||||
bar.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static refreshAll(reopen: boolean = false): void {
|
||||
CustomToolbar.list.forEach(v => v.refresh(reopen));
|
||||
}
|
||||
|
||||
static showAll(): number[] {
|
||||
return CustomToolbar.list.map(v => v.show());
|
||||
}
|
||||
|
||||
static closeAll() {
|
||||
this.list.forEach(v => v.closeAll());
|
||||
}
|
||||
}
|
||||
|
||||
Mota.require('var', 'loading').once('coreInit', () => {
|
||||
CustomToolbar.load();
|
||||
CustomToolbar.closeAll();
|
||||
|
||||
window.addEventListener('beforeunload', e => {
|
||||
CustomToolbar.save();
|
||||
});
|
||||
window.addEventListener('blur', () => {
|
||||
CustomToolbar.save();
|
||||
});
|
||||
});
|
||||
Mota.require('var', 'hook').on('reset', () => {
|
||||
CustomToolbar.showAll();
|
||||
});
|
||||
@@ -1,337 +0,0 @@
|
||||
import { Component, shallowReactive } from 'vue';
|
||||
import { EventEmitter } from '@motajs/legacy-common';
|
||||
import { Hotkey } from './hotkey';
|
||||
|
||||
interface FocusEvent<T> {
|
||||
focus: (before: T | null, after: T) => void;
|
||||
unfocus: (before: T | null) => void;
|
||||
add: (item: T) => void;
|
||||
pop: (item: T | null) => void;
|
||||
splice: (spliced: T[]) => void;
|
||||
}
|
||||
|
||||
export class Focus<T = any> extends EventEmitter<FocusEvent<T>> {
|
||||
/** 显示列表 */
|
||||
stack: T[];
|
||||
focused: T | null = null;
|
||||
|
||||
/** 聚焦目标是否平等,在平等时,关闭聚焦目标不再会将其之后的目标全部删除,而是保留 */
|
||||
readonly equal: boolean;
|
||||
|
||||
constructor(react: boolean = false, equal: boolean = false) {
|
||||
super();
|
||||
this.stack = react ? shallowReactive([]) : [];
|
||||
this.equal = equal;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聚焦于一个目标
|
||||
* @param target 聚焦目标
|
||||
* @param add 如果聚焦目标不在显示列表里面,是否自动追加
|
||||
*/
|
||||
focus(target: T, add: boolean = false) {
|
||||
if (target === this.focused) return;
|
||||
const before = this.focused;
|
||||
if (!this.stack.includes(target)) {
|
||||
if (add) {
|
||||
this.add(target);
|
||||
this.focused = target;
|
||||
} else {
|
||||
console.warn(
|
||||
`聚焦于一个不存在的目标,同时没有传入自动追加的参数`,
|
||||
`聚焦目标:${target}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.focused = target;
|
||||
}
|
||||
this.emit('focus', before, this.focused);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消聚焦
|
||||
*/
|
||||
unfocus() {
|
||||
const before = this.focused;
|
||||
this.focused = null;
|
||||
this.emit('unfocus', before);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向显示列表中添加物品
|
||||
* @param item 添加的物品
|
||||
*/
|
||||
add(item: T) {
|
||||
this.stack.push(item);
|
||||
this.emit('add', item);
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出显示列表中的最后一个物品
|
||||
*/
|
||||
pop() {
|
||||
const item = this.stack.pop() ?? null;
|
||||
const last = this.stack.at(-1) ?? null;
|
||||
if (!last) this.unfocus();
|
||||
else this.focus(last);
|
||||
this.emit('pop', item);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从一个位置开始删除显示列表,如果平等,则只会删除一个,否则会将其之后的所有的目标全部删除
|
||||
* @param item 从哪开始删除,包括此项
|
||||
*/
|
||||
splice(item: T) {
|
||||
const index = this.stack.indexOf(item);
|
||||
this.spliceIndex(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据索引在显示列表中删除一项
|
||||
* @param index 要删除的项的索引
|
||||
*/
|
||||
spliceIndex(index: number) {
|
||||
if (index === -1) {
|
||||
this.emit('splice', []);
|
||||
return;
|
||||
}
|
||||
if (!this.stack[index]) return;
|
||||
const last = this.stack.at(-1) ?? null;
|
||||
if (!last) this.unfocus();
|
||||
else this.focus(last);
|
||||
this.emit(
|
||||
'splice',
|
||||
this.stack.splice(index, this.equal ? 1 : Infinity)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface GameUiEvent {
|
||||
close: () => void;
|
||||
open: () => void;
|
||||
}
|
||||
|
||||
interface ShowableGameUi {
|
||||
ui: GameUi;
|
||||
vOn?: UiVOn;
|
||||
vBind?: UiVBind;
|
||||
}
|
||||
|
||||
type UiVOn = Record<string, (param?: any) => void>;
|
||||
type UiVBind = Record<string, any>;
|
||||
|
||||
interface MountedVBind {
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
export class GameUi extends EventEmitter<GameUiEvent> {
|
||||
static uiList: GameUi[] = [];
|
||||
|
||||
component: Component;
|
||||
hotkey?: Hotkey;
|
||||
id: string;
|
||||
symbol: symbol = Symbol();
|
||||
|
||||
constructor(id: string, component: Component, hotkey?: Hotkey) {
|
||||
super();
|
||||
this.component = component;
|
||||
this.hotkey = hotkey;
|
||||
this.id = id;
|
||||
GameUi.uiList.push(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 v-on 和 v-bind 创建可以显示的 ui 组件
|
||||
* @param vOn 监听的事件
|
||||
* @param vBind 绑定的数据
|
||||
*/
|
||||
with(vBind?: UiVBind, vOn?: UiVOn): ShowableGameUi {
|
||||
return { ui: this, vOn, vBind };
|
||||
}
|
||||
}
|
||||
|
||||
interface IndexedGameUi extends ShowableGameUi {
|
||||
num: number;
|
||||
vBind?: MountedVBind;
|
||||
}
|
||||
|
||||
interface HoldOnController {
|
||||
end(noClosePanel?: boolean): void;
|
||||
}
|
||||
|
||||
export class UiController extends Focus<IndexedGameUi> {
|
||||
static list: UiController[] = [];
|
||||
list: Record<string, GameUi> = {};
|
||||
num: number = 0;
|
||||
show: 'end' | 'all' = 'all';
|
||||
|
||||
private hold: boolean = false;
|
||||
|
||||
constructor(equal?: boolean) {
|
||||
super(true, equal);
|
||||
UiController.list.push(this);
|
||||
this.on('splice', spliced => {
|
||||
spliced.forEach(v => {
|
||||
v.ui.emit('close');
|
||||
});
|
||||
if (this.stack.length === 0) {
|
||||
if (!this.hold) this.emit('end', false);
|
||||
this.hold = false;
|
||||
}
|
||||
});
|
||||
this.on('add', item => {
|
||||
if (this.stack.length === 1) {
|
||||
this.emit('start');
|
||||
}
|
||||
item.ui.emit('open');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置为仅显示最后一个ui
|
||||
*/
|
||||
showEnd() {
|
||||
this.show = 'end';
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置为显示所有ui
|
||||
*/
|
||||
showAll() {
|
||||
this.show = 'all';
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id获取到ui
|
||||
* @param id ui的id
|
||||
*/
|
||||
get(id: string) {
|
||||
return this.list[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂时保持下一次删除ui不会导致ui整体被关闭,引起ui背景闪烁。
|
||||
* 例如可以用于道具栏,打开道具时就应当 holdOn,然后通过道具使用钩子来判断接下来是否要隐藏 app:
|
||||
* ```txt
|
||||
* hold on -> close -> use item -> hook -> stack.length === 0 ? end() : no action
|
||||
* ```
|
||||
*/
|
||||
holdOn(): HoldOnController {
|
||||
this.hold = true;
|
||||
|
||||
return {
|
||||
end: (noClosePanel: boolean = false) => {
|
||||
this.emit('end', noClosePanel);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭一个ui,注意如果不是平等模式,在其之后的ui都会同时关闭掉
|
||||
* @param num 要关闭的ui的唯一标识符
|
||||
*/
|
||||
close(num: number) {
|
||||
const ui = this.stack.findIndex(v => v.num === num);
|
||||
this.spliceIndex(ui);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id关闭所有同id的ui,注意非平等模式下,会将第一个ui后的所有ui都关闭掉
|
||||
* @param id 要关闭的ui的id
|
||||
*/
|
||||
closeByName(id: string) {
|
||||
if (!this.equal) {
|
||||
const ui = this.stack.findIndex(v => v.ui.id === id);
|
||||
this.spliceIndex(ui);
|
||||
} else {
|
||||
let ui;
|
||||
while ((ui = this.stack.findIndex(v => v.ui.id === id)) !== -1) {
|
||||
this.spliceIndex(ui);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开一个新的ui
|
||||
* @param id 要打开的ui的id
|
||||
* @param vOn 监听的事件
|
||||
* @param vBind 绑定的数据
|
||||
* @returns ui的唯一标识符
|
||||
*/
|
||||
open(id: string, vBind?: UiVBind, vOn?: UiVOn) {
|
||||
if (core.isReplaying() && !this.equal) return -1;
|
||||
const ui = this.get(id);
|
||||
if (!ui) {
|
||||
console.warn(`Unknown UI: '${id}'.`);
|
||||
return -1;
|
||||
}
|
||||
const num = this.num++;
|
||||
const bind = {
|
||||
num,
|
||||
ui,
|
||||
...(vBind ?? {})
|
||||
};
|
||||
const sui = ui.with(bind, vOn);
|
||||
this.add({
|
||||
num,
|
||||
...sui
|
||||
} as IndexedGameUi);
|
||||
return num;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册一个ui
|
||||
* @param id ui的id
|
||||
* @param ui 对应的GameUi实例
|
||||
*/
|
||||
register(...ui: GameUi[]) {
|
||||
ui.forEach(v => {
|
||||
const id = v.id;
|
||||
if (id in this.list) {
|
||||
console.warn(`已存在id为'${id}'的ui,已将其覆盖`);
|
||||
}
|
||||
this.list[id] = v;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册一个ui
|
||||
* @param id 要取消注册的ui的id
|
||||
*/
|
||||
unregister(...id: string[]) {
|
||||
id.forEach(v => {
|
||||
delete this.list[v];
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ui的唯一标识符进行聚焦
|
||||
* @param num 要聚焦于的ui的唯一标识符
|
||||
*/
|
||||
focusByNum(num: number) {
|
||||
const ui = this.getByNum(num);
|
||||
if (!ui) return;
|
||||
this.focus(ui);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据唯一标识符获取对应的ui
|
||||
* @param num ui的唯一标识符
|
||||
*/
|
||||
getByNum(num: number) {
|
||||
return this.stack.find(v => v.num === num);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ui的唯一标识符来判断当前是否存在某个ui
|
||||
* @param id ui的唯一标识符
|
||||
*/
|
||||
hasName(id: string) {
|
||||
return this.stack.some(v => v.ui.id === id);
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { debounce } from 'lodash-es';
|
||||
import { fixedUi, mainUi } from './ui';
|
||||
import { ref } from 'vue';
|
||||
import { sleep } from 'mutate-animate';
|
||||
|
||||
const close = ref(false);
|
||||
|
||||
let cx = 0;
|
||||
let cy = 0;
|
||||
|
||||
/**
|
||||
* 显示定点查看
|
||||
*/
|
||||
const showFixed = debounce((block: Block) => {
|
||||
const e = core.material.enemys[block.event.id as EnemyIds];
|
||||
if (!e) return;
|
||||
const enemy = core.status.thisMap.enemy.get(block.x, block.y);
|
||||
if (!enemy) return;
|
||||
fixedUi.open(
|
||||
'fixed',
|
||||
{ enemy, close, loc: [cx, cy] },
|
||||
{ close: closeFixed }
|
||||
);
|
||||
}, 200);
|
||||
|
||||
/**
|
||||
* 关闭定点查看
|
||||
*/
|
||||
const closeFixed = () => {
|
||||
close.value = true;
|
||||
sleep(200).then(() => {
|
||||
fixedUi.closeByName('fixed');
|
||||
close.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// todo: 应当在这里实现查看临界与特殊属性的功能
|
||||
export let hovered: Block | null;
|
||||
|
||||
const { hook, gameListener } = Mota.requireAll('var');
|
||||
gameListener.on('hoverBlock', block => {
|
||||
closeFixed();
|
||||
hovered = block;
|
||||
});
|
||||
gameListener.on('leaveBlock', (_, __, leaveGame) => {
|
||||
showFixed.cancel();
|
||||
if (!leaveGame) closeFixed();
|
||||
hovered = null;
|
||||
});
|
||||
gameListener.on('mouseMove', e => {
|
||||
cx = e.clientX;
|
||||
cy = e.clientY;
|
||||
showFixed.cancel();
|
||||
if (hovered) {
|
||||
showFixed(hovered);
|
||||
}
|
||||
});
|
||||
|
||||
hook.once('mounted', () => {
|
||||
mainUi.on('start', () => {
|
||||
showFixed.cancel();
|
||||
closeFixed();
|
||||
});
|
||||
});
|
||||
@@ -1,589 +0,0 @@
|
||||
import { KeyCode } from '@motajs/client-base';
|
||||
import { gameKey, HotkeyJSON } from '../custom/hotkey';
|
||||
import {
|
||||
openDanmakuPoster,
|
||||
tip,
|
||||
hasMarkedEnemy,
|
||||
markEnemy,
|
||||
unmarkEnemy
|
||||
} from '@motajs/legacy-ui';
|
||||
import { hovered } from './fixed';
|
||||
import { mainUi } from './ui';
|
||||
import { GameStorage } from '../storage';
|
||||
|
||||
export const mainScope = Symbol.for('@key_main');
|
||||
|
||||
// todo: 读取上一个手动存档,存档至下一个存档栏
|
||||
// ----- Register
|
||||
gameKey
|
||||
// --------------------
|
||||
.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
|
||||
})
|
||||
// --------------------
|
||||
.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
|
||||
})
|
||||
.register({
|
||||
id: 'skillTree',
|
||||
name: '技能树',
|
||||
defaults: KeyCode.KeyJ
|
||||
})
|
||||
.register({
|
||||
id: 'desc',
|
||||
name: '百科全书',
|
||||
defaults: KeyCode.KeyH
|
||||
})
|
||||
// --------------------
|
||||
.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: 'mark',
|
||||
name: '标记怪物',
|
||||
defaults: KeyCode.KeyM
|
||||
})
|
||||
.register({
|
||||
id: 'special',
|
||||
name: '鼠标位置怪物属性',
|
||||
defaults: KeyCode.KeyE
|
||||
})
|
||||
.register({
|
||||
id: 'critical',
|
||||
name: '鼠标位置怪物临界',
|
||||
defaults: KeyCode.KeyC
|
||||
})
|
||||
.register({
|
||||
id: 'danmaku',
|
||||
name: '发送弹幕',
|
||||
defaults: KeyCode.KeyA,
|
||||
ctrl: true
|
||||
})
|
||||
.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
|
||||
})
|
||||
// --------------------
|
||||
.group('skill', '技能按键')
|
||||
.register({
|
||||
id: 'skill1',
|
||||
name: '断灭之刃',
|
||||
defaults: KeyCode.Digit1
|
||||
})
|
||||
.register({
|
||||
id: 'skill2',
|
||||
name: '跳跃',
|
||||
defaults: KeyCode.Digit2
|
||||
})
|
||||
.register({
|
||||
id: 'skill3',
|
||||
name: '铸剑为盾',
|
||||
defaults: KeyCode.Digit3
|
||||
})
|
||||
// --------------------
|
||||
.group('system', '系统按键')
|
||||
.register({
|
||||
id: 'restart',
|
||||
name: '回到开始界面',
|
||||
defaults: KeyCode.KeyN
|
||||
})
|
||||
.register({
|
||||
id: 'comment',
|
||||
name: '评论区',
|
||||
defaults: KeyCode.KeyP
|
||||
})
|
||||
.register({
|
||||
id: 'debug',
|
||||
name: '调试模式',
|
||||
defaults: KeyCode.F8
|
||||
})
|
||||
// --------------------
|
||||
.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
|
||||
})
|
||||
// --------------------
|
||||
.group('@ui_start', '开始界面')
|
||||
.register({
|
||||
id: '@start_up',
|
||||
name: '上移光标',
|
||||
defaults: KeyCode.UpArrow
|
||||
})
|
||||
.register({
|
||||
id: '@start_down',
|
||||
name: '下移光标',
|
||||
defaults: KeyCode.DownArrow
|
||||
})
|
||||
// --------------------
|
||||
.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
|
||||
})
|
||||
// --------------------
|
||||
.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
|
||||
})
|
||||
// --------------------
|
||||
.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
|
||||
})
|
||||
// --------------------
|
||||
.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
|
||||
})
|
||||
// --------------------
|
||||
.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
|
||||
});
|
||||
|
||||
gameKey.enable();
|
||||
gameKey.use(mainScope);
|
||||
|
||||
// ----- Realization
|
||||
|
||||
gameKey
|
||||
.when(
|
||||
() =>
|
||||
!core.status.lockControl && !core.isMoving() && !core.isReplaying()
|
||||
)
|
||||
.realize('book', () => {
|
||||
core.openBook(true);
|
||||
})
|
||||
.realize('save', () => {
|
||||
core.save(true);
|
||||
})
|
||||
.realize('load', () => {
|
||||
core.load(true);
|
||||
})
|
||||
.realize('toolbox', () => {
|
||||
core.openToolbox(true);
|
||||
})
|
||||
.realize('equipbox', () => {
|
||||
core.openEquipbox(true);
|
||||
})
|
||||
.realize('fly', () => {
|
||||
core.useFly(true);
|
||||
})
|
||||
.realize('menu', () => {
|
||||
core.openSettings(true);
|
||||
})
|
||||
.realize('replay', () => {
|
||||
core.ui._drawReplay();
|
||||
})
|
||||
.realize('shop', () => {
|
||||
core.openQuickShop(true);
|
||||
})
|
||||
.realize('statistics', () => {
|
||||
core.ui._drawStatistics();
|
||||
})
|
||||
.realize('viewMap', () => {
|
||||
core.ui._drawViewMaps();
|
||||
})
|
||||
.realize('skillTree', () => {
|
||||
core.useItem('skill1', true);
|
||||
})
|
||||
.realize('desc', () => {
|
||||
core.useItem('I560', true);
|
||||
})
|
||||
.realize('undo', () => {
|
||||
core.doSL('autoSave', 'load');
|
||||
})
|
||||
.realize('redo', () => {
|
||||
core.doSL('autoSave', 'reload');
|
||||
})
|
||||
.realize('turn', () => {
|
||||
core.turnHero();
|
||||
})
|
||||
.realize('getNext', () => {
|
||||
core.getNextItem();
|
||||
})
|
||||
.realize('mark', () => {
|
||||
const cls = hovered?.event.cls;
|
||||
if (cls === 'enemys' || cls === 'enemy48') {
|
||||
const id = hovered!.event.id as EnemyIds;
|
||||
if (hasMarkedEnemy(id)) unmarkEnemy(id);
|
||||
else markEnemy(id);
|
||||
}
|
||||
})
|
||||
.realize('special', () => {
|
||||
if (hovered) {
|
||||
const { x, y } = hovered;
|
||||
const enemy = core.status.thisMap.enemy.get(x, y);
|
||||
if (enemy) mainUi.open('fixedDetail', { panel: 'special' });
|
||||
}
|
||||
})
|
||||
.realize('critical', () => {
|
||||
if (hovered) {
|
||||
const { x, y } = hovered;
|
||||
const enemy = core.status.thisMap.enemy.get(x, y);
|
||||
if (enemy) mainUi.open('fixedDetail', { panel: 'critical' });
|
||||
}
|
||||
})
|
||||
.realize('danmaku', () => {
|
||||
openDanmakuPoster();
|
||||
})
|
||||
.realize('restart', () => {
|
||||
core.confirmRestart();
|
||||
})
|
||||
.realize('comment', () => {
|
||||
core.actions._clickGameInfo_openComments();
|
||||
})
|
||||
.realize('skill1', () => {
|
||||
const HeroSkill = Mota.require('module', 'Mechanism').HeroSkill;
|
||||
if (!HeroSkill.learnedSkill(HeroSkill.Blade)) return;
|
||||
if (HeroSkill.getAutoSkill()) {
|
||||
tip('error', '已开启自动切换技能!');
|
||||
return;
|
||||
}
|
||||
core.playSound('光标移动');
|
||||
HeroSkill.toggleSkill(HeroSkill.Blade);
|
||||
core.status.route.push('useSkill:Blade');
|
||||
core.updateStatusBar();
|
||||
})
|
||||
.realize('skill2', () => {
|
||||
const HeroSkill = Mota.require('module', 'Mechanism').HeroSkill;
|
||||
if (
|
||||
!flags.onChase &&
|
||||
!core.status.floorId.startsWith('tower') &&
|
||||
HeroSkill.learnedSkill(HeroSkill.Jump)
|
||||
) {
|
||||
Mota.Plugin.require('skill_g').jumpSkill();
|
||||
core.status.route.push('useSkill:Jump');
|
||||
} else {
|
||||
if (core.hasItem('pickaxe')) {
|
||||
core.useItem('pickaxe');
|
||||
}
|
||||
}
|
||||
})
|
||||
.realize('skill3', () => {
|
||||
const HeroSkill = Mota.require('module', 'Mechanism').HeroSkill;
|
||||
if (!HeroSkill.learnedSkill(HeroSkill.Shield)) return;
|
||||
if (HeroSkill.getAutoSkill()) {
|
||||
tip('error', '已开启自动切换技能!');
|
||||
return;
|
||||
}
|
||||
core.playSound('光标移动');
|
||||
HeroSkill.toggleSkill(HeroSkill.Shield);
|
||||
core.status.route.push('useSkill:Shield');
|
||||
core.updateStatusBar();
|
||||
})
|
||||
.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,3 +0,0 @@
|
||||
import './fixed';
|
||||
import './keyboard';
|
||||
import './hotkey';
|
||||
@@ -1,888 +0,0 @@
|
||||
import { KeyCode } from '@motajs/client-base';
|
||||
import { Keyboard } from '../custom/keyboard';
|
||||
|
||||
const qweKey = new Keyboard('qwe'); // 字母键盘,A-Z
|
||||
const numKey = new Keyboard('num'); // 数字键盘,1-0
|
||||
const charKey = new Keyboard('charKey'); // 符号键盘,!@<>等
|
||||
const fnKey = new Keyboard('fn'); // Fn键盘,F1-F12
|
||||
const assistKey = new Keyboard('assist'); // 辅助键盘,tab,capslk,shift,ctrl等
|
||||
const arrowKey = new Keyboard('arrow'); // 方向键
|
||||
const numpadKey = new Keyboard('numpad'); // 小键盘
|
||||
const mediaKey = new Keyboard('media'); // 媒体按键
|
||||
const toolKey = new Keyboard('tool'); // Home,End,Insert,Delete,PageUp,PageDn
|
||||
|
||||
const mainKey = new Keyboard('main'); // 主键盘,不包含小键盘和方向键
|
||||
const toolArrowKey = new Keyboard('toolArrow'); // 辅助键盘和方向键
|
||||
const miniKey = new Keyboard('mini'); // 小键盘,包含媒体按键
|
||||
const fullKey = new Keyboard('full'); // 全部键盘
|
||||
|
||||
qweKey
|
||||
.add({
|
||||
key: KeyCode.KeyQ,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyW,
|
||||
x: 50,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyE,
|
||||
x: 100,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyR,
|
||||
x: 150,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyT,
|
||||
x: 200,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyY,
|
||||
x: 250,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyU,
|
||||
x: 300,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyI,
|
||||
x: 350,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyO,
|
||||
x: 400,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyP,
|
||||
x: 450,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyA,
|
||||
x: 10,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyS,
|
||||
x: 60,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyD,
|
||||
x: 110,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyF,
|
||||
x: 160,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyG,
|
||||
x: 210,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyH,
|
||||
x: 260,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyJ,
|
||||
x: 310,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyK,
|
||||
x: 360,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyL,
|
||||
x: 410,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyZ,
|
||||
x: 40,
|
||||
y: 100,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyX,
|
||||
x: 90,
|
||||
y: 100,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyC,
|
||||
x: 140,
|
||||
y: 100,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyV,
|
||||
x: 190,
|
||||
y: 100,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyB,
|
||||
x: 240,
|
||||
y: 100,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyN,
|
||||
x: 290,
|
||||
y: 100,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.KeyM,
|
||||
x: 340,
|
||||
y: 100,
|
||||
width: 45,
|
||||
height: 45
|
||||
});
|
||||
|
||||
numKey
|
||||
.add({
|
||||
key: KeyCode.Backquote,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>~<br />\`</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Digit1,
|
||||
x: 50,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>!<br />1</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Digit2,
|
||||
x: 100,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>@<br />2</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Digit3,
|
||||
x: 150,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>#<br />3</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Digit4,
|
||||
x: 200,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>\$<br />4</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Digit5,
|
||||
x: 250,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>%<br />5</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Digit6,
|
||||
x: 300,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>^<br />6</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Digit7,
|
||||
x: 350,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>&<br />7</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Digit8,
|
||||
x: 400,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>*<br />8</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Digit9,
|
||||
x: 450,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>(<br />9</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Digit0,
|
||||
x: 500,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>)<br />0</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Minus,
|
||||
x: 550,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>—<br />-</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Equal,
|
||||
x: 600,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>+<br />=</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Backspace,
|
||||
x: 650,
|
||||
y: 0,
|
||||
width: 70,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 60%'>Backspace</span>`
|
||||
});
|
||||
|
||||
charKey
|
||||
.add({
|
||||
key: KeyCode.BracketLeft,
|
||||
x: 110,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>{<br />[</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.BracketRight,
|
||||
x: 160,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>}<br />]</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Backslash,
|
||||
x: 210,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>|<br />\\</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Semicolon,
|
||||
x: 70,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>:<br />;</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Quote,
|
||||
x: 120,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>"<br />'</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Comma,
|
||||
x: 0,
|
||||
y: 100,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'><<br />,</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Period,
|
||||
x: 50,
|
||||
y: 100,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>><br />.</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Slash,
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>?<br />/</span>`
|
||||
});
|
||||
|
||||
fnKey
|
||||
.add({
|
||||
key: KeyCode.Escape,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `Esc`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.F1,
|
||||
x: 50 + 25,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.F2,
|
||||
x: 100 + 25,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.F3,
|
||||
x: 150 + 25,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.F4,
|
||||
x: 200 + 25,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.F5,
|
||||
x: 250 + 50,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.F6,
|
||||
x: 300 + 50,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.F7,
|
||||
x: 350 + 50,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.F8,
|
||||
x: 400 + 50,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.F9,
|
||||
x: 450 + 75,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.F10,
|
||||
x: 500 + 75,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.F11,
|
||||
x: 550 + 75,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.F12,
|
||||
x: 600 + 75,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45
|
||||
});
|
||||
|
||||
assistKey
|
||||
.add({
|
||||
key: KeyCode.Tab,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 70,
|
||||
height: 45
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.CapsLock,
|
||||
x: 0,
|
||||
y: 50,
|
||||
width: 80,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>CapsLk</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Shift,
|
||||
x: 0,
|
||||
y: 100,
|
||||
width: 110,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>Shift</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Ctrl,
|
||||
x: 0,
|
||||
y: 150,
|
||||
width: 75,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>Ctrl</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Meta,
|
||||
x: 80,
|
||||
y: 150,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: /* html */ `<span style='font-size: 130%; display: flex; justify-content: center; align-items: center'>
|
||||
<svg style="width: 1em; height: 1em" viewbox="64 64 896 896" fill="currentColor">
|
||||
<path d="M523.8 191.4v288.9h382V128.1zm0 642.2l382 62.2v-352h-382zM120.1 480.2H443V201.9l-322.9 53.5zm0 290.4L443 823.2V543.8H120.1z" />
|
||||
</svg>
|
||||
</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Alt,
|
||||
x: 130,
|
||||
y: 150,
|
||||
width: 70,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>Alt</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Space,
|
||||
x: 205,
|
||||
y: 150,
|
||||
width: 320,
|
||||
height: 45,
|
||||
text: ``
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Alt,
|
||||
x: 530,
|
||||
y: 150,
|
||||
width: 70,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>Alt</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Meta,
|
||||
x: 605,
|
||||
y: 150,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: /* html */ `<span style='font-size: 130%; display: flex; justify-content: center; align-items: center'>
|
||||
<svg style="width: 1em; height: 1em" viewbox="64 64 896 896" fill="currentColor">
|
||||
<path d="M523.8 191.4v288.9h382V128.1zm0 642.2l382 62.2v-352h-382zM120.1 480.2H443V201.9l-322.9 53.5zm0 290.4L443 823.2V543.8H120.1z" />
|
||||
</svg>
|
||||
</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Ctrl,
|
||||
x: 655,
|
||||
y: 150,
|
||||
width: 65,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>Ctrl</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Shift,
|
||||
x: 615,
|
||||
y: 100,
|
||||
width: 105,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>Shift</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Enter,
|
||||
x: 635,
|
||||
y: 50,
|
||||
width: 85,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>Enter</span>`
|
||||
});
|
||||
|
||||
arrowKey
|
||||
.add({
|
||||
key: KeyCode.UpArrow,
|
||||
x: 50,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `↑`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.LeftArrow,
|
||||
x: 0,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `←`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.DownArrow,
|
||||
x: 50,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `↓`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.RightArrow,
|
||||
x: 100,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `→`
|
||||
});
|
||||
|
||||
toolKey
|
||||
.add({
|
||||
key: KeyCode.Insert,
|
||||
x: 0,
|
||||
y: 60,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 70%'>Insert</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Delete,
|
||||
x: 0,
|
||||
y: 110,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 70%'>Delete</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Home,
|
||||
x: 50,
|
||||
y: 60,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>Home</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.End,
|
||||
x: 50,
|
||||
y: 110,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>End</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.PageUp,
|
||||
x: 100,
|
||||
y: 60,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>PgUp</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.PageDown,
|
||||
x: 100,
|
||||
y: 110,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>PgDn</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.ScrollLock,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>ScrLk</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.PauseBreak,
|
||||
x: 100,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 70%'>Pause<br />Break</span>`
|
||||
});
|
||||
|
||||
numpadKey
|
||||
.add({
|
||||
key: KeyCode.NumLock,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 70%'>NumLk</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.NumpadDivide,
|
||||
x: 50,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `/`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.NumpadMultiply,
|
||||
x: 100,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `*`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.NumpadSubtract,
|
||||
x: 150,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `-`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.NumpadAdd,
|
||||
x: 150,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 95,
|
||||
text: `+`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Enter,
|
||||
x: 150,
|
||||
y: 150,
|
||||
width: 45,
|
||||
height: 95,
|
||||
text: `<span style='font-size: 80%'>Enter</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.NumpadDecimal,
|
||||
x: 100,
|
||||
y: 200,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `.`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Numpad0,
|
||||
x: 0,
|
||||
y: 200,
|
||||
width: 95,
|
||||
height: 45,
|
||||
text: `0`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Numpad7,
|
||||
x: 0,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `7`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Numpad8,
|
||||
x: 50,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `8`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Numpad9,
|
||||
x: 100,
|
||||
y: 50,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `9`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Numpad4,
|
||||
x: 0,
|
||||
y: 100,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `4`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Numpad5,
|
||||
x: 50,
|
||||
y: 100,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `5`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Numpad6,
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `6`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Numpad1,
|
||||
x: 0,
|
||||
y: 150,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `1`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Numpad2,
|
||||
x: 50,
|
||||
y: 150,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `2`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Numpad3,
|
||||
x: 100,
|
||||
y: 150,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `3`
|
||||
});
|
||||
|
||||
mediaKey
|
||||
.add({
|
||||
key: KeyCode.MediaPlayPause,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: /* html */ `<span style='font-size: 90%; display: flex; justify-content: center; align-items: center'>
|
||||
<svg style="width: 1em; height: 1em" viewbox="64 64 896 896" fill="currentColor">
|
||||
<path d="M715.8 493.5L335 165.1c-14.2-12.2-35-1.2-35 18.5v656.8c0 19.7 20.8 30.7 35 18.5l380.8-328.4c10.9-9.4 10.9-27.6 0-37z" />
|
||||
</svg>
|
||||
/
|
||||
<svg style="width: 1em; height: 1em" viewbox="64 64 896 896" fill="currentColor">
|
||||
<path d="M304 176h80v672h-80zm408 0h-64c-4.4 0-8 3.6-8 8v656c0 4.4 3.6 8 8 8h64c4.4 0 8-3.6 8-8V184c0-4.4-3.6-8-8-8z" />
|
||||
</svg>
|
||||
</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.MediaStop,
|
||||
x: 50,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: /* html */ `<span style='font-size: 80%; display: flex; justify-content: center; align-items: center'>
|
||||
<svg style="width: 1em; height: 1em" viewbox="64 64 896 896" fill="currentColor">
|
||||
<rect x="0" y="0" width="100%" height="100%" />
|
||||
</svg>
|
||||
</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.MediaTrackPrevious,
|
||||
x: 100,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: /* html */ `<span style='font-size: 110%; display: flex; justify-content: center; align-items: center'>
|
||||
<svg style="width: 1em; height: 1em" viewbox="64 64 896 896" fill="currentColor">
|
||||
<path d="M517.6 273.5L230.2 499.3a16.14 16.14 0 000 25.4l287.4 225.8c10.7 8.4 26.4.8 26.4-12.7V286.2c0-13.5-15.7-21.1-26.4-12.7zm320 0L550.2 499.3a16.14 16.14 0 000 25.4l287.4 225.8c10.7 8.4 26.4.8 26.4-12.7V286.2c0-13.5-15.7-21.1-26.4-12.7zm-620-25.5h-51.2c-3.5 0-6.4 2.7-6.4 6v516c0 3.3 2.9 6 6.4 6h51.2c3.5 0 6.4-2.7 6.4-6V254c0-3.3-2.9-6-6.4-6z" />
|
||||
</svg>
|
||||
</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.MediaTrackNext,
|
||||
x: 150,
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: /* html */ `<span style='font-size: 110%; display: flex; justify-content: center; align-items: center'>
|
||||
<svg style="width: 1em; height: 1em" viewbox="64 64 896 896" fill="currentColor">
|
||||
<path d="M793.8 499.3L506.4 273.5c-10.7-8.4-26.4-.8-26.4 12.7v451.6c0 13.5 15.7 21.1 26.4 12.7l287.4-225.8a16.14 16.14 0 000-25.4zm-320 0L186.4 273.5c-10.7-8.4-26.4-.8-26.4 12.7v451.5c0 13.5 15.7 21.1 26.4 12.7l287.4-225.8c4.1-3.2 6.2-8 6.2-12.7 0-4.6-2.1-9.4-6.2-12.6zM857.6 248h-51.2c-3.5 0-6.4 2.7-6.4 6v516c0 3.3 2.9 6 6.4 6h51.2c3.5 0 6.4-2.7 6.4-6V254c0-3.3-2.9-6-6.4-6z" />
|
||||
</svg>
|
||||
</span>`
|
||||
});
|
||||
|
||||
toolArrowKey.extend(arrowKey, 0, 210).extend(toolKey, 0, 0);
|
||||
miniKey.extend(numpadKey, 0, 0);
|
||||
|
||||
mainKey
|
||||
.extend(numKey, 0, 60)
|
||||
.extend(qweKey, 75, 110)
|
||||
.extend(charKey, 465, 110)
|
||||
.extend(fnKey, 0, 0)
|
||||
.extend(assistKey, 0, 110);
|
||||
|
||||
fullKey
|
||||
.extend(mainKey, 0, 0)
|
||||
.extend(toolArrowKey, 740, 0)
|
||||
.extend(miniKey, 905, 60)
|
||||
.extend(mediaKey, 905, 0);
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Keyboard } from '../custom/keyboard';
|
||||
import KeyboardUI from '@/panel/keyboard.vue';
|
||||
|
||||
interface VirtualKeyProps {
|
||||
keyboard: Keyboard;
|
||||
}
|
||||
|
||||
export function VirtualKey(props: VirtualKeyProps) {
|
||||
return (
|
||||
<KeyboardUI
|
||||
style="align-self: center"
|
||||
keyboard={props.keyboard}
|
||||
></KeyboardUI>
|
||||
);
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
import type { SettingComponent, SettingComponentProps } from '../setting';
|
||||
import { Button, InputNumber, Radio } from 'ant-design-vue';
|
||||
import { mainUi } from './ui';
|
||||
import { gameKey } from '../custom/hotkey';
|
||||
|
||||
interface Components {
|
||||
Default: SettingComponent;
|
||||
Boolean: SettingComponent;
|
||||
Number: SettingComponent;
|
||||
HotkeySetting: SettingComponent;
|
||||
ToolbarEditor: SettingComponent;
|
||||
Radio: (items: string[]) => SettingComponent;
|
||||
Performance: SettingComponent;
|
||||
}
|
||||
|
||||
export type { Components as SettingDisplayComponents };
|
||||
|
||||
export function createSettingComponents() {
|
||||
const com: Components = {
|
||||
Default: DefaultSetting,
|
||||
Boolean: BooleanSetting,
|
||||
Number: NumberSetting,
|
||||
HotkeySetting,
|
||||
ToolbarEditor,
|
||||
Radio: RadioSetting,
|
||||
Performance: PerformanceSetting
|
||||
};
|
||||
return com;
|
||||
}
|
||||
|
||||
function DefaultSetting(props: SettingComponentProps) {
|
||||
return (
|
||||
<div>
|
||||
<span> 未知的设置类型 </span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BooleanSetting(props: SettingComponentProps) {
|
||||
const { setting, displayer, item } = props;
|
||||
const changeValue = () => {
|
||||
setting.setValue(displayer.selectStack.join('.'), !item.value);
|
||||
displayer.update();
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="editor-boolean">
|
||||
<span>当前 {item.value ? '开启' : '关闭'}</span>
|
||||
<Button
|
||||
class="boolean-button"
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={changeValue}
|
||||
>
|
||||
{item.value ? '关闭' : '开启'}设置
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NumberSetting(props: SettingComponentProps) {
|
||||
const { setting, displayer, item } = props;
|
||||
const changeValue = (value: number) => {
|
||||
if (typeof value !== 'number') return;
|
||||
if (value < (item.step?.[0] ?? 0) || value > (item.step?.[1] ?? 100)) {
|
||||
return;
|
||||
}
|
||||
setting.setValue(displayer.selectStack.join('.'), Math.round(value));
|
||||
displayer.update();
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="editor-number">
|
||||
<div class="editor-number-input">
|
||||
<span> 修改设置: </span>
|
||||
<InputNumber
|
||||
class="number-input"
|
||||
size="large"
|
||||
// keyboard={true}
|
||||
min={item.step?.[0] ?? 0}
|
||||
max={item.step?.[1] ?? 100}
|
||||
step={item.step?.[2] ?? 1}
|
||||
value={item.value as number}
|
||||
onChange={value => changeValue(value as number)}
|
||||
></InputNumber>
|
||||
</div>
|
||||
<div class="editor-number-button">
|
||||
<Button
|
||||
class="number-button"
|
||||
type="primary"
|
||||
onClick={() =>
|
||||
changeValue(
|
||||
(item.value as number) - (item.step?.[2] ?? 1)
|
||||
)
|
||||
}
|
||||
>
|
||||
减少
|
||||
</Button>
|
||||
<Button
|
||||
class="number-button"
|
||||
type="primary"
|
||||
onClick={() =>
|
||||
changeValue(
|
||||
(item.value as number) + (item.step?.[2] ?? 1)
|
||||
)
|
||||
}
|
||||
>
|
||||
增加
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RadioSetting(items: string[]) {
|
||||
return (props: SettingComponentProps) => {
|
||||
const { setting, displayer, item } = props;
|
||||
|
||||
const changeValue = (value: number) => {
|
||||
if (isNaN(value)) return;
|
||||
setting.setValue(displayer.selectStack.join('.'), value);
|
||||
displayer.update();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{items.map((v, i) => {
|
||||
return (
|
||||
<Radio
|
||||
value={i}
|
||||
checked={i === item.value}
|
||||
onClick={() => changeValue(i)}
|
||||
>
|
||||
{v}
|
||||
</Radio>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function showSpecialSetting(id: string, vBind?: any) {
|
||||
const ui = mainUi.get(id);
|
||||
mainUi.showEnd();
|
||||
ui.once('close', () => {
|
||||
mainUi.showAll();
|
||||
});
|
||||
mainUi.open(id, vBind);
|
||||
}
|
||||
|
||||
function HotkeySetting(props: SettingComponentProps) {
|
||||
return (
|
||||
<div style="display: flex; justify-content: center">
|
||||
<Button
|
||||
style="font-size: 75%"
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={() =>
|
||||
showSpecialSetting('hotkey', {
|
||||
hotkey: gameKey
|
||||
})
|
||||
}
|
||||
>
|
||||
快捷键设置
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarEditor(props: SettingComponentProps) {
|
||||
return (
|
||||
<div style="display: flex; justify-content: center">
|
||||
<Button
|
||||
style="font-size: 75%"
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={() => showSpecialSetting('toolEditor')}
|
||||
>
|
||||
自定义工具栏
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PerformanceSetting(props: SettingComponentProps) {
|
||||
return (
|
||||
<div style="display: flex; justify-content: center">
|
||||
<Button
|
||||
style="font-size: 75%"
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={() => showSpecialSetting('performance')}
|
||||
>
|
||||
打开性能界面
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,74 +0,0 @@
|
||||
import { UI } from '@motajs/legacy-ui';
|
||||
import * as MiscUI from './misc';
|
||||
import { GameUi, UiController } from '../custom/ui';
|
||||
import { mainSetting } from '../setting';
|
||||
|
||||
export const mainUi = new UiController();
|
||||
mainUi.register(
|
||||
new GameUi('book', UI.Book),
|
||||
new GameUi('toolbox', UI.Toolbox),
|
||||
new GameUi('equipbox', UI.Equipbox),
|
||||
new GameUi('settings', UI.Settings),
|
||||
new GameUi('desc', UI.Desc),
|
||||
new GameUi('skill', UI.Skill),
|
||||
new GameUi('skillTree', UI.SkillTree),
|
||||
new GameUi('fly', UI.Fly),
|
||||
new GameUi('fixedDetail', UI.FixedDetail),
|
||||
new GameUi('shop', UI.Shop),
|
||||
new GameUi('achievement', UI.Achievement),
|
||||
new GameUi('bgm', UI.BgmList),
|
||||
new GameUi('hotkey', UI.Hotkey),
|
||||
new GameUi('toolEditor', UI.ToolEditor),
|
||||
new GameUi('virtualKey', MiscUI.VirtualKey)
|
||||
// todo: 把游戏主 div 加入到 mainUi 里面
|
||||
);
|
||||
mainUi.showAll();
|
||||
|
||||
export const fixedUi = new UiController(true);
|
||||
fixedUi.register(
|
||||
new GameUi('markedEnemy', UI.Marked),
|
||||
new GameUi('fixed', UI.Fixed),
|
||||
new GameUi('chapter', UI.Chapter),
|
||||
new GameUi('completeAchi', UI.CompleteAchi),
|
||||
new GameUi('start', UI.Start),
|
||||
new GameUi('toolbar', UI.Toolbar),
|
||||
new GameUi('load', UI.Load),
|
||||
new GameUi('danmaku', UI.Danmaku),
|
||||
new GameUi('danmakuEditor', UI.DanmakuEditor),
|
||||
new GameUi('tips', UI.Tips)
|
||||
);
|
||||
fixedUi.showAll();
|
||||
|
||||
const hook = Mota.require('var', 'hook');
|
||||
hook.once('mounted', () => {
|
||||
const ui = document.getElementById('ui-main')!;
|
||||
const fixed = document.getElementById('ui-fixed')!;
|
||||
|
||||
const blur = mainSetting.getSetting('screen.blur');
|
||||
|
||||
mainUi.on('start', () => {
|
||||
ui.style.display = 'flex';
|
||||
if (blur?.value) {
|
||||
ui.style.backdropFilter = 'blur(5px)';
|
||||
ui.style.backgroundColor = 'rgba(0,0,0,0.7333)';
|
||||
} else {
|
||||
ui.style.backdropFilter = 'none';
|
||||
ui.style.backgroundColor = 'rgba(0,0,0,0.85)';
|
||||
}
|
||||
core.lockControl();
|
||||
});
|
||||
mainUi.on('end', noClosePanel => {
|
||||
ui.style.display = 'none';
|
||||
if (!noClosePanel) {
|
||||
try {
|
||||
core.closePanel();
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
fixedUi.on('start', () => {
|
||||
fixed.style.display = 'block';
|
||||
});
|
||||
fixedUi.on('end', () => {
|
||||
fixed.style.display = 'none';
|
||||
});
|
||||
});
|
||||
@@ -1,574 +0,0 @@
|
||||
import { FunctionalComponent, reactive } from 'vue';
|
||||
import { EventEmitter } from '@motajs/legacy-common';
|
||||
import { GameStorage } from './storage';
|
||||
import { has, triggerFullscreen, isMobile } from '@motajs/legacy-ui';
|
||||
import { createSettingComponents } from './init/settings';
|
||||
import settingsText from '@/data/settings.json';
|
||||
import { CustomToolbar } from './custom/toolbar';
|
||||
import { fixedUi } from './init/ui';
|
||||
import { bgmController, soundPlayer } from '@/module';
|
||||
|
||||
export interface SettingComponentProps {
|
||||
item: MotaSettingItem;
|
||||
setting: MotaSetting;
|
||||
displayer: SettingDisplayer;
|
||||
}
|
||||
|
||||
export type SettingComponent = FunctionalComponent<SettingComponentProps>;
|
||||
type MotaSettingType = boolean | number | MotaSetting;
|
||||
|
||||
export interface MotaSettingItem<T extends MotaSettingType = MotaSettingType> {
|
||||
name: string;
|
||||
key: string;
|
||||
value: T;
|
||||
controller: SettingComponent;
|
||||
description?: string;
|
||||
defaults?: boolean | number;
|
||||
step?: [number, number, number];
|
||||
display?: (value: T) => string;
|
||||
}
|
||||
|
||||
interface SettingEvent {
|
||||
valueChange: <T extends boolean | number>(
|
||||
key: string,
|
||||
newValue: T,
|
||||
oldValue: T
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type SettingText = {
|
||||
[key: string]: string[] | SettingText;
|
||||
};
|
||||
|
||||
export interface SettingDisplayInfo {
|
||||
item: MotaSettingItem | null;
|
||||
list: Record<string, MotaSettingItem>;
|
||||
text: string[];
|
||||
}
|
||||
|
||||
const COM = createSettingComponents();
|
||||
|
||||
export class MotaSetting extends EventEmitter<SettingEvent> {
|
||||
static noStorage: string[] = [];
|
||||
|
||||
readonly list: Record<string, MotaSettingItem> = {};
|
||||
|
||||
/**
|
||||
* 重设设置
|
||||
* @param setting 设置信息
|
||||
*/
|
||||
reset(setting: Record<string, boolean | number>) {
|
||||
for (const [key, value] of Object.entries(setting)) {
|
||||
this.setValue(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册一个数字型设置
|
||||
* @param key 设置的键名
|
||||
* @param value 设置的值
|
||||
*/
|
||||
register(
|
||||
key: string,
|
||||
name: string,
|
||||
value: number,
|
||||
com?: SettingComponent,
|
||||
step?: [number, number, number]
|
||||
): this;
|
||||
/**
|
||||
* 注册一个非数字型设置
|
||||
* @param key 设置的键名
|
||||
* @param value 设置的值
|
||||
*/
|
||||
register(
|
||||
key: string,
|
||||
name: string,
|
||||
value: boolean | MotaSetting,
|
||||
com?: SettingComponent
|
||||
): this;
|
||||
register(
|
||||
key: string,
|
||||
name: string,
|
||||
value: MotaSettingType,
|
||||
com: SettingComponent = COM.Default,
|
||||
step: [number, number, number] = [0, 100, 1]
|
||||
) {
|
||||
const setting: MotaSettingItem = {
|
||||
name,
|
||||
value,
|
||||
key,
|
||||
controller: com
|
||||
};
|
||||
if (!(value instanceof MotaSetting)) setting.defaults = value;
|
||||
if (typeof value === 'number') setting.step = step;
|
||||
this.list[key] = setting;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个设置信息
|
||||
* @param key 要获取的设置的键
|
||||
*/
|
||||
getSetting(key: string): Readonly<MotaSettingItem | null> {
|
||||
const list = key.split('.');
|
||||
return this.getSettingBy(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一个设置的值
|
||||
* @param key 要设置的设置的键
|
||||
* @param value 要设置的值
|
||||
*/
|
||||
setValue(key: string, value: boolean | number) {
|
||||
const setting = this.getSettingBy(key.split('.'));
|
||||
if (typeof setting.value !== typeof value) {
|
||||
throw new Error(
|
||||
`Setting type mismatch on setting '${key}'.` +
|
||||
`Expected: ${typeof setting.value}. Recieve: ${typeof value}`
|
||||
);
|
||||
}
|
||||
const old = setting.value as boolean | number;
|
||||
setting.value = value;
|
||||
|
||||
this.emit('valueChange', key, value, old);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加一个设置的值
|
||||
* @param key 要改变的设置的值
|
||||
* @param value 值的增量
|
||||
*/
|
||||
addValue(key: string, value: number) {
|
||||
const setting = this.getSettingBy(key.split('.'));
|
||||
if (typeof setting.value !== 'number') {
|
||||
throw new Error(
|
||||
`Cannot execute addValue method on a non-number setting.` +
|
||||
`Type expected: number. See: ${typeof setting.value}`
|
||||
);
|
||||
}
|
||||
const old = setting.value as boolean | number;
|
||||
setting.value += value;
|
||||
this.emit('valueChange', key, old, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个设置的值,如果获取到的是一个MotaSetting实例,那么返回undefined
|
||||
* @param key 要获取的设置
|
||||
*/
|
||||
getValue(key: string): boolean | number | undefined;
|
||||
/**
|
||||
* 获取一个设置的值,如果获取到的是一个MotaSetting实例,那么返回defaultValue
|
||||
* @param key 要获取的设置
|
||||
* @param defaultValue 设置的默认值
|
||||
*/
|
||||
getValue<T extends boolean | number>(key: string, defaultValue: T): T;
|
||||
getValue<T extends boolean | number>(
|
||||
key: string,
|
||||
defaultValue?: T
|
||||
): T | undefined {
|
||||
const setting = this.getSetting(key);
|
||||
if (!has(setting) && !has(defaultValue)) return void 0;
|
||||
if (setting instanceof MotaSetting) {
|
||||
if (has(setting)) return defaultValue;
|
||||
return void 0;
|
||||
} else {
|
||||
return has(setting) ? (setting.value as T) : (defaultValue as T);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一个设置的值显示函数
|
||||
* @param key 要设置的设置的键
|
||||
* @param func 显示函数
|
||||
*/
|
||||
setDisplayFunc(key: string, func: (value: MotaSettingType) => string) {
|
||||
const setting = this.getSettingBy(key.split('.'));
|
||||
setting.display = func;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一个设置的修改部分组件
|
||||
* @param key 要设置的设置的键
|
||||
* @param com 设置修改部分的组件
|
||||
*/
|
||||
setValueController(key: string, com: SettingComponent) {
|
||||
const setting = this.getSettingBy(key.split('.'));
|
||||
setting.controller = com;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置一个设置的说明
|
||||
* @param key 要设置的设置的id
|
||||
* @param desc 设置的说明
|
||||
*/
|
||||
setDescription(key: string, desc: string) {
|
||||
const setting = this.getSettingBy(key.split('.'));
|
||||
setting.description = desc;
|
||||
return this;
|
||||
}
|
||||
|
||||
private getSettingBy(list: string[]) {
|
||||
let now: MotaSetting = this;
|
||||
|
||||
for (let i = 0; i < list.length - 1; i++) {
|
||||
const item = now.list[list[i]].value;
|
||||
if (!(item instanceof MotaSetting)) {
|
||||
throw new Error(
|
||||
`Cannot get setting. The parent isn't a MotaSetting instance.` +
|
||||
`Key: '${list.join('.')}'. Reading: '${list[i]}'`
|
||||
);
|
||||
}
|
||||
now = item;
|
||||
}
|
||||
|
||||
return now.list[list.at(-1)!] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
interface SettingDisplayerEvent {
|
||||
update: (stack: string[], display: SettingDisplayInfo[]) => void;
|
||||
}
|
||||
|
||||
export class SettingDisplayer extends EventEmitter<SettingDisplayerEvent> {
|
||||
setting: MotaSetting;
|
||||
/** 选项选中栈 */
|
||||
selectStack: string[] = [];
|
||||
displayInfo: SettingDisplayInfo[] = reactive([]);
|
||||
|
||||
constructor(setting: MotaSetting) {
|
||||
super();
|
||||
this.setting = setting;
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加选择项
|
||||
* @param key 下一个选择项
|
||||
*/
|
||||
add(key: string) {
|
||||
this.selectStack.push(...key.split('.'));
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 剪切后面的选择项
|
||||
* @param index 从哪开始剪切
|
||||
*/
|
||||
cut(index: number, noUpdate: boolean = false) {
|
||||
this.selectStack.splice(index, Infinity);
|
||||
if (!noUpdate) this.update();
|
||||
}
|
||||
|
||||
update() {
|
||||
const list = this.selectStack;
|
||||
let now = this.setting;
|
||||
this.displayInfo = [];
|
||||
|
||||
for (let i = 0; i < list.length - 1; i++) {
|
||||
const item = now.list[list[i]].value;
|
||||
if (!(item instanceof MotaSetting)) {
|
||||
throw new Error(
|
||||
`Cannot get setting. The parent isn't a MotaSetting instance.` +
|
||||
`Key: '${list.join('.')}'. Reading: '${list[i + 1]}'`
|
||||
);
|
||||
}
|
||||
|
||||
this.displayInfo.push({
|
||||
item: now.list[list[i]],
|
||||
text: [],
|
||||
list: now.list
|
||||
});
|
||||
|
||||
now = item;
|
||||
}
|
||||
|
||||
const last = now.list[list.at(-1)!];
|
||||
if (last) {
|
||||
const desc = last.description;
|
||||
const text = desc ? desc.split('\n') : ['请选择设置'];
|
||||
|
||||
this.displayInfo.push({
|
||||
item: last,
|
||||
text,
|
||||
list: now.list
|
||||
});
|
||||
if (last.value instanceof MotaSetting) {
|
||||
this.displayInfo.push({
|
||||
item: null,
|
||||
text: ['请选择设置'],
|
||||
list: (last.value as MotaSetting).list
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.displayInfo.push({
|
||||
item: null,
|
||||
text: ['请选择设置'],
|
||||
list: this.setting.list
|
||||
});
|
||||
}
|
||||
this.emit('update', this.selectStack, this.displayInfo);
|
||||
}
|
||||
}
|
||||
|
||||
export const mainSetting = new MotaSetting();
|
||||
// 添加不参与全局存储的设置
|
||||
MotaSetting.noStorage.push('action.autoSkill', 'screen.fullscreen');
|
||||
|
||||
const storage = new GameStorage(GameStorage.fromAuthor('AncTe', 'setting'));
|
||||
|
||||
export { storage as settingStorage };
|
||||
|
||||
// ----- 监听设置修改
|
||||
mainSetting.on('valueChange', (key, n, o) => {
|
||||
if (!MotaSetting.noStorage.includes(key)) {
|
||||
storage.setValue(key, n);
|
||||
}
|
||||
|
||||
const [root, setting] = key.split('.');
|
||||
|
||||
if (root === 'screen') {
|
||||
handleScreenSetting(setting, n, o);
|
||||
} else if (root === 'action') {
|
||||
handleActionSetting(setting, n, o);
|
||||
} else if (root === 'audio') {
|
||||
handleAudioSetting(setting, n, o);
|
||||
} else if (root === 'ui') {
|
||||
handleUiSetting(setting, n, o);
|
||||
}
|
||||
});
|
||||
|
||||
const root = document.getElementById('root') as HTMLDivElement;
|
||||
|
||||
function handleScreenSetting<T extends number | boolean>(
|
||||
key: string,
|
||||
n: T,
|
||||
_o: T
|
||||
) {
|
||||
if (key === 'fullscreen') {
|
||||
// 全屏
|
||||
triggerFullscreen(n as boolean);
|
||||
} else if (key === 'heroDetail') {
|
||||
// 勇士显伤
|
||||
core.drawHero();
|
||||
} else if (key === 'fontSize') {
|
||||
// 字体大小
|
||||
root.style.fontSize = `${n}px`;
|
||||
const absoluteSize = (n as number) * devicePixelRatio;
|
||||
storage.setValue('@@absoluteFontSize', absoluteSize);
|
||||
storage.write();
|
||||
} else if (key === 'fontSizeStatus') {
|
||||
// fontSize.value = n as number;
|
||||
}
|
||||
}
|
||||
|
||||
function handleActionSetting<T extends number | boolean>(
|
||||
key: string,
|
||||
n: T,
|
||||
_o: T
|
||||
) {
|
||||
if (key === 'autoSkill') {
|
||||
// 自动切换技能
|
||||
const HeroSkill = Mota.require('module', 'Mechanism').HeroSkill;
|
||||
HeroSkill.setAutoSkill(n as boolean);
|
||||
core.status.route.push(`set:autoSkill:${n}`)
|
||||
}
|
||||
}
|
||||
|
||||
function handleAudioSetting<T extends number | boolean>(
|
||||
key: string,
|
||||
n: T,
|
||||
_o: T
|
||||
) {
|
||||
if (key === 'bgmEnabled') {
|
||||
bgmController.setEnabled(n as boolean);
|
||||
core.checkBgm();
|
||||
} else if (key === 'bgmVolume') {
|
||||
bgmController.setVolume((n as number) / 100);
|
||||
} else if (key === 'soundEnabled') {
|
||||
soundPlayer.setEnabled(n as boolean);
|
||||
} else if (key === 'soundVolume') {
|
||||
soundPlayer.setVolume((n as number) / 100)
|
||||
}
|
||||
}
|
||||
|
||||
function handleUiSetting<T extends number | boolean>(key: string, n: T, o: T) {
|
||||
if (key === 'toolbarScale') {
|
||||
const scale = (n as number) / (o as number);
|
||||
CustomToolbar.list.forEach(v => {
|
||||
v.setSize(v.width * scale, v.height * scale);
|
||||
});
|
||||
CustomToolbar.refreshAll(true);
|
||||
} else if (key === 'danmaku') {
|
||||
if (n) {
|
||||
fixedUi.open('danmaku');
|
||||
} else {
|
||||
fixedUi.closeByName('danmaku');
|
||||
}
|
||||
} else if (key === 'tips') {
|
||||
if (n && core.isPlaying()) {
|
||||
fixedUi.open('tips');
|
||||
} else {
|
||||
fixedUi.closeByName('tips')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 游戏的所有设置项
|
||||
// todo: 虚拟键盘缩放,小地图楼传缩放
|
||||
mainSetting
|
||||
.register(
|
||||
'screen',
|
||||
'显示设置',
|
||||
new MotaSetting()
|
||||
.register('fullscreen', '全屏游戏', false, COM.Boolean)
|
||||
.register('halo', '光环显示', true, COM.Boolean)
|
||||
.register('itemDetail', '宝石血瓶显伤', true, COM.Boolean)
|
||||
.register('heroDetail', '勇士显伤', false, COM.Boolean)
|
||||
.register('transition', '界面动画', false, COM.Boolean)
|
||||
.register('fontSize', '字体大小', 16, COM.Number, [2, 48, 1])
|
||||
.register('fontSizeStatus', '状态栏字体', 16, COM.Number, [10, 300, 10])
|
||||
.register('smoothView', '平滑镜头', true, COM.Boolean)
|
||||
.register('criticalGem', '临界显示方式', false, COM.Boolean)
|
||||
.setDisplayFunc('criticalGem', value => (value ? '宝石数' : '攻击'))
|
||||
.register('keyScale', '虚拟键盘缩放', 100, COM.Number, [25, 5, 500])
|
||||
.register('blur', '背景虚化', !isMobile, COM.Boolean)
|
||||
)
|
||||
.register(
|
||||
'action',
|
||||
'操作设置',
|
||||
new MotaSetting()
|
||||
.register('autoSkill', '自动切换技能', true, COM.Boolean)
|
||||
.register('fixed', '定点查看', true, COM.Boolean)
|
||||
.register('hotkey', '快捷键', false, COM.HotkeySetting)
|
||||
.setDisplayFunc('hotkey', () => '')
|
||||
.register('toolbar', '自定义工具栏', false, COM.ToolbarEditor)
|
||||
.setDisplayFunc('toolbar', () => '')
|
||||
)
|
||||
.register(
|
||||
'audio',
|
||||
'音频设置',
|
||||
new MotaSetting()
|
||||
.register('bgmEnabled', '开启音乐', true, COM.Boolean)
|
||||
.register('bgmVolume', '音乐音量', 80, COM.Number, [0, 100, 5])
|
||||
.register('soundEnabled', '开启音效', true, COM.Boolean)
|
||||
.register('soundVolume', '音效音量', 80, COM.Number, [0, 100, 5])
|
||||
)
|
||||
.register(
|
||||
'utils',
|
||||
'系统设置',
|
||||
new MotaSetting()
|
||||
.register('betterLoad', '优化加载', true, COM.Boolean)
|
||||
.register('autoScale', '自动放缩', true, COM.Boolean)
|
||||
)
|
||||
.register(
|
||||
'fx',
|
||||
'特效设置',
|
||||
new MotaSetting()
|
||||
.register('paraLight', '野外阴影', true, COM.Boolean)
|
||||
.register('frag', '打怪特效', true, COM.Boolean)
|
||||
.register('portalParticle', '传送门特效', true, COM.Boolean)
|
||||
)
|
||||
.register(
|
||||
'ui',
|
||||
'ui设置',
|
||||
new MotaSetting()
|
||||
.register('mapScale', '小地图缩放', 100, COM.Number, [50, 1000, 50])
|
||||
.setDisplayFunc('mapScale', value => `${value}%`)
|
||||
.register('mapLazy', '小地图懒更新', false, COM.Boolean)
|
||||
.register('toolbarScale', '工具栏缩放', 100, COM.Number, [10, 500, 10])
|
||||
.setDisplayFunc('toolbarScale', value => `${value}%`)
|
||||
.register('bookScale', '怪物手册缩放', 100, COM.Number, [10, 500, 10])
|
||||
.setDisplayFunc('bookScale', value => `${value}%`)
|
||||
.register('danmaku', '显示弹幕', true, COM.Boolean)
|
||||
.register('danmakuSpeed', '弹幕速度', 60, COM.Number, [10, 1000, 5])
|
||||
.register('tips', '小贴士', true, COM.Boolean)
|
||||
);
|
||||
|
||||
const loading = Mota.require('var', 'loading');
|
||||
loading.once('coreInit', () => {
|
||||
mainSetting.reset({
|
||||
'screen.fullscreen': !!document.fullscreenElement,
|
||||
'screen.halo': !!storage.getValue('screen.showHalo', true),
|
||||
'screen.itemDetail': !!storage.getValue('screen.itemDetail', true),
|
||||
'screen.heroDetail': !!storage.getValue('screen.heroDetail', false),
|
||||
'screen.transition': !!storage.getValue('screen.transition', false),
|
||||
'screen.fontSize': storage.getValue('screen.fontSize', isMobile ? 9 : 16),
|
||||
'screen.smoothView': !!storage.getValue('screen.smoothView', true),
|
||||
'screen.criticalGem': !!storage.getValue('screen.criticalGem', false),
|
||||
'screen.fontSizeStatus': storage.getValue('screen.fontSizeStatus', 100),
|
||||
'action.fixed': !!storage.getValue('action.fixed', true),
|
||||
'audio.bgmEnabled': !!storage.getValue('audio.bgmEnabled', true),
|
||||
'audio.bgmVolume': storage.getValue('audio.bgmVolume', 80),
|
||||
'audio.soundEnabled': !!storage.getValue('audio.soundEnabled', true),
|
||||
'audio.soundVolume': storage.getValue('audio.soundVolume', 80),
|
||||
'utils.betterLoad': !!storage.getValue('utils.betterLoad', true),
|
||||
'utils.autoScale': !!storage.getValue('utils.autoScale', true),
|
||||
'fx.paraLight': !!storage.getValue('fx.paraLight', true),
|
||||
'fx.frag': !!storage.getValue('fx.frag', true),
|
||||
'fx.portalParticle': !!storage.getValue('fx.portalParticle', true),
|
||||
'ui.mapScale': storage.getValue(
|
||||
'ui.mapScale',
|
||||
isMobile ? 300 : Math.floor(window.innerWidth / 600) * 50
|
||||
),
|
||||
'ui.mapLazy': storage.getValue('ui.mapLazy', false),
|
||||
'ui.toolbarScale': storage.getValue(
|
||||
'ui.toolbarScale',
|
||||
isMobile ? 50 : Math.floor((window.innerWidth / 1700) * 10) * 10
|
||||
),
|
||||
'ui.bookScale': storage.getValue('ui.bookScale', isMobile ? 100 : 80),
|
||||
'ui.danmaku': storage.getValue('ui.danmaku', true),
|
||||
'ui.danmakuSpeed': storage.getValue(
|
||||
'ui.danmakuSpeed',
|
||||
Math.floor(window.innerWidth / 30) * 5
|
||||
),
|
||||
'ui.tips': storage.getValue('ui.tips', true)
|
||||
});
|
||||
});
|
||||
|
||||
interface SettingTextData {
|
||||
[x: string]: string[] | SettingTextData;
|
||||
}
|
||||
|
||||
function getSettingText(obj: SettingTextData, key?: string) {
|
||||
for (const [k, value] of Object.entries(obj)) {
|
||||
const setKey = key ? key + '.' + k : k;
|
||||
if (value instanceof Array) {
|
||||
mainSetting.setDescription(setKey, value.join('\n'));
|
||||
} else {
|
||||
getSettingText(value, setKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
getSettingText(settingsText);
|
||||
|
||||
mainSetting
|
||||
.setDescription('audio.bgmEnabled', `是否开启背景音乐`)
|
||||
.setDescription('audio.bgmVolume', `背景音乐的音量`)
|
||||
.setDescription('audio.soundEnabled', `是否开启音效`)
|
||||
.setDescription('audio.soundVolume', `音效的音量`)
|
||||
.setDescription('ui.mapScale', `楼传小地图的缩放,百分比格式`)
|
||||
.setDescription('ui.mapLazy', `是否启用小地图懒更新模式,此模式下剩余怪物数量不会实时更新而变成切换地图后更新,打开小地图时出现卡顿可以尝试开启此设置`)
|
||||
.setDescription('ui.toolbarScale', `自定义工具栏的缩放比例`)
|
||||
.setDescription('ui.bookScale', `怪物手册界面中每个怪物框体的高度缩放,最小值限定为 20% 屏幕高度`)
|
||||
.setDescription('ui.danmaku', '是否显示弹幕')
|
||||
.setDescription('ui.danmakuSpeed', '弹幕速度,刷新或开关弹幕显示后起效')
|
||||
.setDescription('ui.tips', `是否在游戏画面右上角常亮显示小贴士`)
|
||||
.setDescription('screen.fontSizeStatus', `修改状态栏的字体大小`)
|
||||
.setDescription('screen.blur', '打开任意ui界面时是否有背景虚化效果,移动端打开后可能会有掉帧或者发热现象。关闭ui后生效')
|
||||
.setDescription('fx.portalParticle', '是否启用苍蓝之殿的传送门粒子特效,启用后可能对性能及设备发热有所影响')
|
||||
|
||||
function setFontSize() {
|
||||
const absoluteSize = storage.getValue(
|
||||
'@@absoluteFontSize',
|
||||
16 * devicePixelRatio
|
||||
);
|
||||
const size = Math.round(absoluteSize / devicePixelRatio);
|
||||
mainSetting.setValue('screen.fontSize', size);
|
||||
}
|
||||
setFontSize();
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
setFontSize();
|
||||
});
|
||||
@@ -1,116 +0,0 @@
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
export class GameStorage<T extends object = any> {
|
||||
static list: GameStorage<any>[] = [];
|
||||
|
||||
key: string;
|
||||
data!: Partial<T>;
|
||||
|
||||
constructor(key: string) {
|
||||
this.key = key;
|
||||
this.read();
|
||||
GameStorage.list.push(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地存储读取
|
||||
*/
|
||||
read(): T {
|
||||
const data = localStorage.getItem(this.key) ?? '{}';
|
||||
return (this.data = JSON.parse(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入本地存储
|
||||
*/
|
||||
write() {
|
||||
localStorage.setItem(this.key, JSON.stringify(this.data));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置存储的值
|
||||
* @param key 存储的名称
|
||||
* @param value 存储的值
|
||||
*/
|
||||
setValue<K extends keyof T>(key: K, value: T[K]): void;
|
||||
setValue(key: string, value: any): void;
|
||||
setValue<K extends keyof T>(key: K, value: T[K]) {
|
||||
this.data[key] = value;
|
||||
}
|
||||
|
||||
getValue<K extends keyof T>(key: K): T[K] | null;
|
||||
getValue<K extends keyof T>(key: K, defaults: T[K]): T[K];
|
||||
getValue<T>(key: string, defaults?: T): T;
|
||||
getValue<K extends keyof T>(key: K, defaults?: T[K]) {
|
||||
if (!isNil(this.data[key])) return this.data[key];
|
||||
else {
|
||||
if (defaults !== void 0) {
|
||||
this.data[key] = defaults;
|
||||
return defaults;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return JSON.stringify(this.data);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.data = {};
|
||||
}
|
||||
|
||||
keys() {
|
||||
return Object.keys(this.data);
|
||||
}
|
||||
|
||||
values() {
|
||||
return Object.values(this.data);
|
||||
}
|
||||
|
||||
entries() {
|
||||
return Object.entries(this.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本游戏的存储键
|
||||
* @param key 存储名称
|
||||
*/
|
||||
static fromGame(key: string) {
|
||||
return `HumanBreak_${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取与作者相关联的存储键
|
||||
* @param author 作者名称
|
||||
* @param key 存储名称
|
||||
*/
|
||||
static fromAuthor(author: string, key: string) {
|
||||
return `${author}@${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据存储键获取对应的存储实例
|
||||
* @param key 存储键
|
||||
* @example Storage.get(Storage.fromAuthor('AncTe', 'setting'));
|
||||
*/
|
||||
static get(key: string) {
|
||||
return this.list.find(v => v.key === key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 与Symbol.for类似
|
||||
*/
|
||||
static for(key: string) {
|
||||
const s = this.get(key);
|
||||
if (s) return s;
|
||||
else return new GameStorage(key);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
GameStorage.list.forEach(v => v.write());
|
||||
});
|
||||
window.addEventListener('blur', () => {
|
||||
GameStorage.list.forEach(v => v.write());
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import * as axios from 'axios';
|
||||
import * as chart from 'chart.js';
|
||||
import jszip from 'jszip';
|
||||
import * as lodash from 'lodash-es';
|
||||
import * as lzstring from 'lz-string';
|
||||
import * as animate from 'mutate-animate';
|
||||
import * as vue from 'vue';
|
||||
|
||||
// Mota.Package.register('axios', axios);
|
||||
// Mota.Package.register('chart.js', chart);
|
||||
// Mota.Package.register('jszip', jszip);
|
||||
// Mota.Package.register('lodash', lodash);
|
||||
// Mota.Package.register('lz-string', lzstring);
|
||||
// Mota.Package.register('mutate-animate', animate);
|
||||
// Mota.Package.register('vue', vue);
|
||||
+1
-121
@@ -1,7 +1,6 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { cloneDeep, isNil } from 'lodash-es';
|
||||
import { ItemState } from './item';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { HeroSkill, NightSpecial } from '../mechanism/misc';
|
||||
|
||||
/**
|
||||
@@ -343,122 +342,3 @@ interface IHeroItem {
|
||||
*/
|
||||
hasItem(item: AllIdsOf<'items'>): boolean;
|
||||
}
|
||||
|
||||
interface HeroEvent {
|
||||
beforeMove: [dir: Dir2];
|
||||
afterMove: [dir: Dir2];
|
||||
beforeMoveDirectly: [x: number, y: number];
|
||||
afterMoveDirectly: [x: number, y: number];
|
||||
stateChange: [state: HeroState<any>];
|
||||
}
|
||||
|
||||
export class Hero<T extends object = IHeroStatusDefault>
|
||||
extends EventEmitter<HeroEvent>
|
||||
implements IHeroItem
|
||||
{
|
||||
x: number;
|
||||
y: number;
|
||||
floorId: FloorIds;
|
||||
dir: Dir2;
|
||||
|
||||
readonly items: Map<AllIdsOf<'items'>, number> = new Map();
|
||||
readonly id: string;
|
||||
|
||||
state: HeroState<T>;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
x: number,
|
||||
y: number,
|
||||
floorId: FloorIds,
|
||||
state: HeroState<T>
|
||||
) {
|
||||
super();
|
||||
this.id = id;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.floorId = floorId;
|
||||
this.state = state;
|
||||
this.dir = 'down';
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置勇士状态,效果为直接将一个状态替换为另一个状态,不应当经常使用,仅应当在不得不使用的时候使用
|
||||
* @param state 要设置为的勇士状态
|
||||
*/
|
||||
setState(state: HeroState<T>): void {
|
||||
this.state = state;
|
||||
this.emit('stateChange', state);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取勇士状态
|
||||
*/
|
||||
getState(): HeroState<T> {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 见 {@link HeroState.refreshStatus}
|
||||
*/
|
||||
refreshState(key?: keyof T) {
|
||||
return this.state.refreshStatus(key);
|
||||
}
|
||||
|
||||
setItem(item: AllIdsOf<'items'>, value: number): boolean {
|
||||
this.items.set(item, value < 0 ? 0 : value);
|
||||
return true;
|
||||
}
|
||||
|
||||
addItem(item: AllIdsOf<'items'>, value: number): boolean {
|
||||
return this.setItem(item, (this.items.get(item) ?? 0) + value);
|
||||
}
|
||||
|
||||
useItem(item: AllIdsOf<'items'>): boolean {
|
||||
const state = ItemState.item(item);
|
||||
return !!state?.use(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得一个物品
|
||||
* @param item 物品id
|
||||
* @param num 获得的数量
|
||||
*/
|
||||
getItem(item: AllIdsOf<'items'>, num: number): boolean;
|
||||
/**
|
||||
* 获得一个物品
|
||||
* @param item 物品id,填写坐标时无效
|
||||
* @param x 物品所在x坐标
|
||||
* @param y 物品所在y坐标
|
||||
* @param floorId 物品所在楼层
|
||||
* @param num 获得的数量
|
||||
*/
|
||||
getItem(x: number, y: number, floorId?: FloorIds, num?: number): boolean;
|
||||
getItem(
|
||||
item: AllIdsOf<'items'> | number,
|
||||
y: number,
|
||||
floorId: FloorIds = this.floorId,
|
||||
num: number = 1
|
||||
): boolean {
|
||||
if (!isNil(floorId) && typeof item === 'number') {
|
||||
// 如果指定了坐标
|
||||
const block = core.getBlock(item as number, y, floorId);
|
||||
const id = block.event.id as AllIdsOf<'items'>;
|
||||
const cls = core.material.items[id]?.cls;
|
||||
if (cls === void 0) {
|
||||
logger.warn(15, item.toString(), y.toString(), floorId);
|
||||
return false;
|
||||
}
|
||||
return this.addItem(id, num!);
|
||||
}
|
||||
return this.addItem(item as AllIdsOf<'items'>, num!);
|
||||
}
|
||||
|
||||
itemCount(item: AllIdsOf<'items'>): number {
|
||||
return this.items.get(item) ?? 0;
|
||||
}
|
||||
|
||||
hasItem(item: AllIdsOf<'items'>): boolean {
|
||||
return this.itemCount(item) > 0;
|
||||
}
|
||||
}
|
||||
+3
-48
@@ -1,13 +1,11 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import type { Hero } from './hero';
|
||||
import { GameState, gameStates } from './state';
|
||||
import { loading } from '../game';
|
||||
|
||||
type EffectFn = (state: GameState, hero: Hero<any>) => void;
|
||||
type CanUseEffectFn = (state: GameState, hero: Hero<any>) => boolean;
|
||||
type EffectFn = () => void;
|
||||
type CanUseEffectFn = () => boolean;
|
||||
|
||||
interface ItemStateEvent {
|
||||
use: [hero: Hero<any>];
|
||||
use: [];
|
||||
}
|
||||
|
||||
export class ItemState<
|
||||
@@ -56,7 +54,6 @@ export class ItemState<
|
||||
this.canUseItemEffect = item.canUseItemEffect;
|
||||
|
||||
this.compileFunction();
|
||||
this.compileEvent();
|
||||
}
|
||||
|
||||
private compileFunction() {
|
||||
@@ -85,48 +82,6 @@ export class ItemState<
|
||||
}
|
||||
}
|
||||
|
||||
private compileEvent() {
|
||||
// todo
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用这个物品
|
||||
* @param hero 使用物品的勇士
|
||||
*/
|
||||
use(hero: Hero<any>): boolean {
|
||||
if (!this.canUse(hero)) return false;
|
||||
if (!gameStates.now) return false;
|
||||
const state = gameStates.now;
|
||||
this.useItemEffectFn?.(state, hero);
|
||||
if (this.useItemEvent) core.insertAction(this.useItemEvent);
|
||||
if (!this.noRoute) {
|
||||
core.status.route.push(`item:${this.id}`);
|
||||
}
|
||||
|
||||
hero.addItem(this.id, -1);
|
||||
this.emit('use', hero);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否可以使用一个物品
|
||||
* @param hero 使用物品的勇士
|
||||
*/
|
||||
canUse(hero: Hero<any>, num: number = 1): boolean {
|
||||
if (num <= 0) return false;
|
||||
if (hero.itemCount(this.id) < num) return false;
|
||||
if (!gameStates.now) return false;
|
||||
return !!this.canUseItemEffectFn?.(gameStates.now, hero);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记当前物品为不进入录像,也就是录像中不会使用该道具
|
||||
*/
|
||||
markNoRoute() {
|
||||
this.noRoute = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个道具的信息
|
||||
* @param id 要获取的道具id
|
||||
|
||||
+17
-14
@@ -11,7 +11,7 @@ import type {
|
||||
LayerMovingRenderable,
|
||||
LayerFloorBinder
|
||||
} from '@motajs/render';
|
||||
import type { HeroKeyMover } from '@/core/main/action/move';
|
||||
import type { HeroKeyMover } from '@/module/action/move';
|
||||
import { BluePalace, MiscData } from '../mechanism/misc';
|
||||
import { sleep } from 'mutate-animate';
|
||||
|
||||
@@ -293,7 +293,7 @@ export class BlockMover extends ObjectMoverBase {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async onMoveStart(controller: IMoveController): Promise<void> {
|
||||
protected async onMoveStart(_controller: IMoveController): Promise<void> {
|
||||
const adapter = BlockMover.adapter;
|
||||
if (adapter) {
|
||||
const list = adapter.items;
|
||||
@@ -332,7 +332,7 @@ export class BlockMover extends ObjectMoverBase {
|
||||
}
|
||||
}
|
||||
|
||||
protected async onMoveEnd(controller: IMoveController): Promise<void> {
|
||||
protected async onMoveEnd(_controller: IMoveController): Promise<void> {
|
||||
if (this.renderable) {
|
||||
this.layerItems.forEach(v => {
|
||||
v.moving.delete(this.renderable!);
|
||||
@@ -349,7 +349,7 @@ export class BlockMover extends ObjectMoverBase {
|
||||
|
||||
protected async onStepStart(
|
||||
step: MoveStepDir,
|
||||
controller: IMoveController
|
||||
_controller: IMoveController
|
||||
): Promise<BlockMoveCode> {
|
||||
await this.moveAnimate(step);
|
||||
const { x: dx, y: dy } = core.utils.scan2[this.moveDir];
|
||||
@@ -360,17 +360,17 @@ export class BlockMover extends ObjectMoverBase {
|
||||
}
|
||||
|
||||
protected async onStepEnd(
|
||||
step: MoveStepDir,
|
||||
code: BlockMoveCode,
|
||||
controller: IMoveController
|
||||
_step: MoveStepDir,
|
||||
_code: BlockMoveCode,
|
||||
_controller: IMoveController
|
||||
): Promise<void> {}
|
||||
|
||||
protected onSetMoveSpeed(
|
||||
speed: number,
|
||||
controller: IMoveController
|
||||
_speed: number,
|
||||
_controller: IMoveController
|
||||
): void {}
|
||||
|
||||
private moveAnimate(step: MoveStepDir) {
|
||||
private moveAnimate(_step: MoveStepDir) {
|
||||
const layer = this.layerItems[0];
|
||||
if (!layer) return;
|
||||
if (!this.renderable) return;
|
||||
@@ -521,7 +521,7 @@ export class HeroMover extends ObjectMoverBase {
|
||||
}
|
||||
|
||||
protected async onStepStart(
|
||||
step: MoveStepDir,
|
||||
_step: MoveStepDir,
|
||||
controller: IMoveController
|
||||
): Promise<HeroMoveCode> {
|
||||
const showDir = toDir(this.faceDir);
|
||||
@@ -594,7 +594,7 @@ export class HeroMover extends ObjectMoverBase {
|
||||
}
|
||||
|
||||
protected async onStepEnd(
|
||||
step: MoveStepDir,
|
||||
_step: MoveStepDir,
|
||||
code: HeroMoveCode,
|
||||
controller: IMoveController
|
||||
): Promise<void> {
|
||||
@@ -661,7 +661,10 @@ export class HeroMover extends ObjectMoverBase {
|
||||
}
|
||||
}
|
||||
|
||||
protected onSetMoveSpeed(speed: number, controller: IMoveController): void {
|
||||
protected onSetMoveSpeed(
|
||||
speed: number,
|
||||
_controller: IMoveController
|
||||
): void {
|
||||
const adapter = HeroMover.adapter;
|
||||
if (!adapter) return;
|
||||
adapter.sync('setMoveSpeed', speed);
|
||||
@@ -785,7 +788,7 @@ export class HeroMover extends ObjectMoverBase {
|
||||
const list = adapter.items;
|
||||
const { x: tx, y: ty, dir: toDir } = data;
|
||||
const { x, y, direction } = core.status.hero.loc;
|
||||
const { x: dx, y: dy } = core.utils.scan[direction];
|
||||
const { x: dx } = core.utils.scan[direction];
|
||||
const { x: tdx } = core.utils.scan[toDir];
|
||||
|
||||
const promises = [...list].map(v => {
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import { Undoable } from '@/core/interface';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
type ToJSONFunction<T> = (data: T) => string;
|
||||
type FromJSONFunction<T> = (data: string) => T;
|
||||
|
||||
export class GameState {
|
||||
state: Map<string, any> = new Map();
|
||||
|
||||
private static states: Set<string> = new Set();
|
||||
private static toJSONFn: Map<string, ToJSONFunction<any>> = new Map();
|
||||
private static fromJSONFn: Map<string, FromJSONFunction<any>> = new Map();
|
||||
|
||||
/**
|
||||
* 序列化游戏状态,可直接用于存储等操作
|
||||
*/
|
||||
toJSON() {
|
||||
const obj: Record<string, string> = {};
|
||||
this.state.forEach((v, k) => {
|
||||
const to = GameState.toJSONFn.get(k);
|
||||
if (to) obj[k] = to(v);
|
||||
else obj[k] = JSON.stringify(v);
|
||||
});
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某个游戏状态
|
||||
* @param key 要获取的状态名称
|
||||
*/
|
||||
get<T>(key: string): T | undefined {
|
||||
return this.state.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某个游戏状态
|
||||
* @param key 要设置的状态名称
|
||||
* @param data 状态数据
|
||||
*/
|
||||
set(key: string, data: any) {
|
||||
this.state.set(key, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册一个新的状态,如果重复则会覆盖
|
||||
* @param key 状态名称
|
||||
* @param toJSON 状态的序列化函数,传入状态数据,要求返回序列化后的字符串,
|
||||
* 不填则表示使用JSON.stringify进行序列化
|
||||
* @param fromJSON 状态的反序列化函数,传入序列化后的字符串,要求返回反序列化的状态数据,
|
||||
* 不填表示使用JSON.parse进行反序列化
|
||||
*/
|
||||
static register<T>(
|
||||
key: string,
|
||||
toJSON?: ToJSONFunction<T>,
|
||||
fromJSON?: FromJSONFunction<T>
|
||||
) {
|
||||
if (this.states.has(key)) {
|
||||
logger.warn(16, key);
|
||||
}
|
||||
|
||||
if (toJSON) {
|
||||
this.toJSONFn.set(key, toJSON);
|
||||
} else {
|
||||
this.toJSONFn.delete(key);
|
||||
}
|
||||
if (fromJSON) {
|
||||
this.fromJSONFn.set(key, fromJSON);
|
||||
} else {
|
||||
this.fromJSONFn.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从序列化字符串读取游戏状态
|
||||
* @param json 序列化字符串
|
||||
*/
|
||||
static fromJSON(json: string) {
|
||||
const obj: Record<string, string> = JSON.parse(json);
|
||||
const state = new GameState();
|
||||
for (const [key, data] of Object.entries(obj)) {
|
||||
const from = this.fromJSONFn.get(key);
|
||||
if (from) state.set(key, from(data));
|
||||
else state.set(key, JSON.parse(data));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
interface StateStoreEvent {
|
||||
undo: [state: GameState];
|
||||
redo: [state: GameState];
|
||||
change: [before: GameState | undefined, now: GameState];
|
||||
}
|
||||
|
||||
class StateStore
|
||||
extends EventEmitter<StateStoreEvent>
|
||||
implements Undoable<GameState>
|
||||
{
|
||||
now?: GameState;
|
||||
stack: GameState[] = [];
|
||||
redoStack: GameState[] = [];
|
||||
|
||||
undo(): GameState | undefined {
|
||||
const state = this.stack.pop();
|
||||
if (!state) return void 0;
|
||||
this.redoStack.push(state);
|
||||
this.emit('undo', state);
|
||||
return state;
|
||||
}
|
||||
|
||||
redo(): GameState | undefined {
|
||||
const state = this.redoStack.pop();
|
||||
if (!state) return void 0;
|
||||
this.stack.push(state);
|
||||
this.emit('redo', state);
|
||||
return state;
|
||||
}
|
||||
|
||||
use(state: GameState) {
|
||||
const before = this.now;
|
||||
this.now = state;
|
||||
this.emit('change', before, state);
|
||||
}
|
||||
}
|
||||
|
||||
export const gameStates = new StateStore();
|
||||
+1
-1
@@ -23,7 +23,7 @@ import type { Danmaku } from '@/core/main/custom/danmaku';
|
||||
import type * as misc from './mechanism/misc';
|
||||
import type { Render } from '@motajs/client';
|
||||
import type { ItemState } from './state/item';
|
||||
import type { HeroKeyMover } from '@/core/main/action/move';
|
||||
import type { HeroKeyMover } from '@/module/action/move';
|
||||
import type { BlockMover, HeroMover, ObjectMoverBase } from './state/move';
|
||||
import type * as Animation from 'mutate-animate';
|
||||
import type { WeatherController } from '@/module/weather/weather';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { KeyCode } from '@motajs/client-base';
|
||||
import { Hotkey, HotkeyData } from '../custom/hotkey';
|
||||
import { Hotkey, HotkeyData } from '@motajs/system-action';
|
||||
import type { HeroMover, IMoveController } from '@/game/state/move';
|
||||
import { Ticker } from 'mutate-animate';
|
||||
import { mainScope } from '../init/hotkey';
|
||||
import { mainScope } from '@motajs/legacy-ui';
|
||||
|
||||
type MoveKey = Record<Dir, HotkeyData>;
|
||||
type MoveKeyConfig = Record<Dir, string>;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Patch, PatchClass } from '@motajs/legacy-common';
|
||||
import { audioPlayer, bgmController, soundPlayer } from '../audio';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { mainSetting } from '@motajs/legacy-ui';
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
SetupComponentOptions,
|
||||
waitbox
|
||||
} from '../components';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { generateKeyboardEvent } from '@/core/main/custom/keyboard';
|
||||
import { mainUi } from '@motajs/legacy-ui';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { generateKeyboardEvent } from '@motajs/system-action';
|
||||
import { getVitualKeyOnce } from '@motajs/legacy-ui';
|
||||
import { getAllSavesData, getSaveData } from '@/module/utils';
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
getVitualKeyOnce,
|
||||
openDanmakuPoster
|
||||
} from '@motajs/legacy-ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { generateKeyboardEvent } from '@/core/main/custom/keyboard';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { generateKeyboardEvent } from '@motajs/system-action';
|
||||
import { transitioned } from '../use';
|
||||
import { linear } from 'mutate-animate';
|
||||
import { KeyCode } from '@motajs/client-base';
|
||||
|
||||
+3
-1
@@ -7,9 +7,11 @@
|
||||
"@motajs/render": "workspace:*",
|
||||
"@motajs/system": "workspace:*",
|
||||
"@motajs/system-ui": "workspace:*",
|
||||
"@motajs/system-action": "workspace:*",
|
||||
"@motajs/legacy-common": "workspace:*",
|
||||
"@motajs/legacy-client": "workspace:*",
|
||||
"@motajs/legacy-data": "workspace:*",
|
||||
"@motajs/legacy-ui": "workspace:*"
|
||||
"@motajs/legacy-ui": "workspace:*",
|
||||
"@motajs/legacy-system": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { MotaOffscreenCanvas2D } from '@motajs/render';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { mainSetting } from '@motajs/legacy-ui';
|
||||
import {
|
||||
LayerGroupFloorBinder,
|
||||
ILayerGroupRenderExtends,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { mainSetting } from '@motajs/legacy-ui';
|
||||
import {
|
||||
Damage,
|
||||
DamageRenderable,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { MotaOffscreenCanvas2D } from '@motajs/render';
|
||||
import { mainSetting, MotaSettingItem } from '@/core/main/setting';
|
||||
import { mainSetting, MotaSettingItem } from '@motajs/legacy-ui';
|
||||
import {
|
||||
LayerGroupFloorBinder,
|
||||
ILayerGroupRenderExtends,
|
||||
|
||||
Reference in new issue
Block a user