mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-25 00:23:25 +08:00
Compare commits
No commits in common. "f7c6434abaf580ee46df45b8f83566aad2faa39b" and "9ec13e5314f03226f8371f5eb1f872172131c6d1" have entirely different histories.
f7c6434aba
...
9ec13e5314
1
.gitignore
vendored
1
.gitignore
vendored
@ -49,4 +49,3 @@ script/people.ts
|
||||
docs/
|
||||
user.ts
|
||||
.antlr
|
||||
graph.svg
|
12
.madgerc
12
.madgerc
@ -1,12 +0,0 @@
|
||||
{
|
||||
"fileExtensions": [
|
||||
"ts",
|
||||
"tsx"
|
||||
],
|
||||
"tsConfig": "./tsconfig.json",
|
||||
"detectiveOptions": {
|
||||
"ts": {
|
||||
"skipTypeImports": true
|
||||
}
|
||||
}
|
||||
}
|
@ -63,7 +63,6 @@
|
||||
"glob": "^11.0.1",
|
||||
"globals": "^15.14.0",
|
||||
"less": "^4.2.0",
|
||||
"madge": "^8.0.0",
|
||||
"postcss-preset-env": "^9.6.0",
|
||||
"rollup": "^3.29.4",
|
||||
"terser": "^5.31.6",
|
||||
|
@ -1,2 +1 @@
|
||||
export * from './keyCodes';
|
||||
export * from './types';
|
||||
|
@ -1,4 +0,0 @@
|
||||
export interface ResponseBase {
|
||||
code: number;
|
||||
message: string;
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "@motajs/legacy-client",
|
||||
"dependencies": {
|
||||
"@motajs/legacy-system": "workspace:*",
|
||||
"@motajs/legacy-ui": "workspace:*"
|
||||
"@motajs/common": "workspace:*"
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * as System from '@motajs/legacy-system';
|
||||
export * as UI from '@motajs/legacy-ui';
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "@motajs/legacy-system",
|
||||
"dependencies": {
|
||||
"@motajs/system-action": "workspace:*"
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './misc';
|
||||
export * from './storage';
|
@ -4,8 +4,6 @@
|
||||
"@motajs/render": "workspace:*",
|
||||
"@motajs/common": "workspace:*",
|
||||
"@motajs/client": "workspace:*",
|
||||
"@motajs/client-base": "workspace:*",
|
||||
"@motajs/legacy-system": "workspace:*",
|
||||
"@motajs/system-action": "workspace:*"
|
||||
"@motajs/client-base": "workspace:*"
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export { default as Box } from './box.vue';
|
||||
export { default as BoxAnimate } from './boxAnimate.vue';
|
||||
export { default as Column } from './colomn.vue';
|
||||
export { default as EnemyOne } from './enemyOne.vue';
|
||||
export { default as Minimap } from './minimap.vue';
|
||||
export { default as Scroll } from './scroll.vue';
|
@ -8,7 +8,7 @@ import { requireUniqueSymbol } from '../utils';
|
||||
import { MinimapDrawer, getArea } from '../tools/fly';
|
||||
import { useDrag, useWheel } from '../use';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { mainSetting } from '../preset/ui';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
|
||||
const props = defineProps<{
|
||||
action?: boolean;
|
||||
@ -168,7 +168,6 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
const hook = Mota.require('var', 'hook');
|
||||
hook.off('afterChangeFloor', onChange);
|
||||
hook.off('afterBattle', afterBattle);
|
||||
});
|
||||
|
@ -1,12 +1,7 @@
|
||||
export * as UI from './ui';
|
||||
export * as Components from './components';
|
||||
export * from './preset';
|
||||
export * from './tools';
|
||||
|
||||
export * from './animateController';
|
||||
export * from './controller';
|
||||
export * from './danmaku';
|
||||
export * from './mark';
|
||||
export * from './setting';
|
||||
export * from './use';
|
||||
export * from './utils';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { fixedUi } from './preset/ui';
|
||||
import { fixedUi } from '@/core/main/init/ui';
|
||||
import type { DamageEnemy } from '@/game/enemy/damage';
|
||||
import { tip } from './utils';
|
||||
import { ref, Ref } from 'vue';
|
||||
|
@ -1,43 +0,0 @@
|
||||
import { Danmaku } from '../danmaku';
|
||||
import { Component, h } from 'vue';
|
||||
import { mainSetting } from './ui';
|
||||
import { getIconHeight } from '../utils';
|
||||
import { BoxAnimate } from '../components';
|
||||
|
||||
// 图标类型
|
||||
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,6 +0,0 @@
|
||||
export * from './ui';
|
||||
export * from './settings';
|
||||
export * from './danmaku';
|
||||
export * from './fixed';
|
||||
export * from './hotkey';
|
||||
export * from './keyboard';
|
@ -1,364 +0,0 @@
|
||||
import { GameStorage, VirtualKey } from '@motajs/legacy-system';
|
||||
import {
|
||||
createSettingComponents,
|
||||
GameUi,
|
||||
isMobile,
|
||||
MotaSetting,
|
||||
triggerFullscreen,
|
||||
UI,
|
||||
UiController
|
||||
} from '@motajs/legacy-ui';
|
||||
import { bgmController, soundPlayer } from '@/module';
|
||||
import settingsText from '@/data/settings.json';
|
||||
|
||||
//#region legacy-ui
|
||||
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('hotkey', UI.Hotkey),
|
||||
new GameUi('toolEditor', UI.ToolEditor),
|
||||
new GameUi('virtualKey', 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) {
|
||||
core.closePanel();
|
||||
}
|
||||
});
|
||||
fixedUi.on('start', () => {
|
||||
fixed.style.display = 'block';
|
||||
});
|
||||
fixedUi.on('end', () => {
|
||||
fixed.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region legacy-setting
|
||||
|
||||
const COM = createSettingComponents();
|
||||
|
||||
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 === '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(
|
||||
'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(
|
||||
'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,308 +0,0 @@
|
||||
import { FunctionalComponent, reactive } from 'vue';
|
||||
import { EventEmitter } from '@motajs/legacy-common';
|
||||
import { has } from './utils';
|
||||
import { createSettingComponents } from './preset';
|
||||
|
||||
const COM = createSettingComponents();
|
||||
|
||||
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[];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { mainSetting } from '../preset/ui';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { downloadCanvasImage, has, tip } from '../utils';
|
||||
|
||||
type BFSFromString = `${FloorIds},${number},${number},${Dir}`;
|
||||
|
@ -46,10 +46,10 @@ import BookDetail from './bookDetail.vue';
|
||||
import { LeftOutlined } from '@ant-design/icons-vue';
|
||||
import { ToShowEnemy, detailInfo } from '../tools/book';
|
||||
import { getDetailedEnemy } from '../tools/fixed';
|
||||
import { GameUi } from '../controller';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { mainUi } from '../preset/ui';
|
||||
import { mainSetting } from '../preset/ui';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { isMobile } from '../use';
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -83,7 +83,7 @@ import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
|
||||
import EnemyCritical from '../panel/enemyCritical.vue';
|
||||
import EnemyTarget from '../panel/enemyTarget.vue';
|
||||
import { detailInfo } from '../tools/book';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
|
||||
const props = defineProps<{
|
||||
fromBook?: boolean;
|
||||
|
@ -9,8 +9,8 @@
|
||||
import { Animation, hyper, sleep } from 'mutate-animate';
|
||||
import { onMounted } from 'vue';
|
||||
import { has } from '../utils';
|
||||
import { GameUi } from '../controller';
|
||||
import { fixedUi } from '../preset/ui';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { fixedUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
|
@ -23,8 +23,8 @@ import { computed, onMounted, ref } from 'vue';
|
||||
import Box from '../components/box.vue';
|
||||
import list from '../data/achievement.json';
|
||||
import { AchievementType, getNowPoint, totalPoint } from '../tools/achievement';
|
||||
import { GameUi } from '../controller';
|
||||
import { fixedUi } from '../preset/ui';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { fixedUi } from '@/core/main/init/ui';
|
||||
|
||||
const height = window.innerHeight;
|
||||
|
||||
|
@ -28,9 +28,9 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, onUnmounted, reactive, watch } from 'vue';
|
||||
import { Danmaku } from '../danmaku';
|
||||
import { Danmaku } from '@/core/main/custom/danmaku';
|
||||
import { LikeFilled } from '@ant-design/icons-vue';
|
||||
import { mainSetting } from '../preset/ui';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
interface ElementMap {
|
||||
|
@ -158,17 +158,17 @@ import {
|
||||
SendOutlined,
|
||||
UpOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { Danmaku } from '../danmaku';
|
||||
import { GameUi } from '../controller';
|
||||
import { Danmaku } from '@/core/main/custom/danmaku';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { fixedUi } from '../preset/ui';
|
||||
import { fixedUi } from '@/core/main/init/ui';
|
||||
import { calStringSize, tip } from '../utils';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { stringifyCSS, parseCss, getIconHeight } from '../utils';
|
||||
import { logger, LogLevel } from '@motajs/common';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import BoxAnimate from '../components/boxAnimate.vue';
|
||||
import Scroll from '@/components/scroll.vue';
|
||||
import BoxAnimate from '@/components/boxAnimate.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
|
@ -22,9 +22,9 @@ import { computed, onUnmounted, ref } from 'vue';
|
||||
import desc from '../data/desc.json';
|
||||
import { splitText } from '../utils';
|
||||
import Colomn from '../components/colomn.vue';
|
||||
import { GameUi } from '../controller';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { mainUi } from '../preset/ui';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
|
@ -186,10 +186,10 @@ import BoxAnimate from '../components/boxAnimate.vue';
|
||||
import { has, tip, type } from '../utils';
|
||||
import { cancelGlobalDrag, isMobile, useDrag } from '../use';
|
||||
import { hyper } from 'mutate-animate';
|
||||
import { GameUi } from '../controller';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { getStatusLabel } from '../utils';
|
||||
import { mainUi } from '../preset/ui';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
|
@ -31,7 +31,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUpdated, Ref, ref, watch } from 'vue';
|
||||
import Box from '../components/box.vue';
|
||||
import { GameUi } from '../controller';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import type { DamageEnemy, EnemyInfo } from '@/game/enemy/damage';
|
||||
import { nextFrame } from '../utils';
|
||||
|
||||
|
@ -12,9 +12,9 @@
|
||||
import { getDetailedEnemy } from '../tools/fixed';
|
||||
import BookDetail from './bookDetail.vue';
|
||||
import { detailInfo } from '../tools/book';
|
||||
import { hovered } from '../preset/fixed';
|
||||
import { GameUi } from '../controller';
|
||||
import { mainUi } from '../preset/ui';
|
||||
import { hovered } from '@/core/main/init/fixed';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
|
@ -99,12 +99,12 @@ import {
|
||||
} from '@ant-design/icons-vue';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { tip } from '../utils';
|
||||
import { GameUi } from '../controller';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { createChangable } from '../tools/common';
|
||||
import { mainUi } from '../preset/ui';
|
||||
import { mainSetting } from '../preset/ui';
|
||||
import { GameStorage } from '@motajs/legacy-system';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { GameStorage } from '@/core/main/storage';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
|
@ -36,15 +36,15 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Hotkey } from '@motajs/system-action';
|
||||
import { GameUi } from '../controller';
|
||||
import { Hotkey } from '@/core/main/custom/hotkey';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import Column from '../components/colomn.vue';
|
||||
import { mainUi } from '../preset/ui';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { KeyCode, KeyCodeUtils } from '@motajs/client-base';
|
||||
import { generateBinary, keycode } from '../utils';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
|
||||
interface HotkeyKeys {
|
||||
index: number;
|
||||
|
@ -31,10 +31,10 @@ import {
|
||||
loadDefaultResource,
|
||||
LoadTask
|
||||
} from '@motajs/legacy-common';
|
||||
import { GameUi } from '../controller';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { formatSize } from '../utils';
|
||||
import { logger } from '@motajs/common';
|
||||
import { fixedUi } from '../preset/ui';
|
||||
import { fixedUi } from '@/core/main/init/ui';
|
||||
import { sleep } from 'mutate-animate';
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -56,8 +56,8 @@ import { MarkInfo, unmarkEnemy } from '../mark';
|
||||
import Box from '../components/box.vue';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import BoxAnimate from '../components/boxAnimate.vue';
|
||||
import { GameUi } from '../controller';
|
||||
import { fixedUi } from '../preset/ui';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { fixedUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
|
@ -73,20 +73,20 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onUnmounted, ref, shallowRef } from 'vue';
|
||||
import { mainSetting } from '../preset/ui';
|
||||
import {
|
||||
mainSetting,
|
||||
MotaSetting,
|
||||
MotaSettingItem,
|
||||
SettingDisplayer,
|
||||
SettingDisplayInfo
|
||||
} from '../setting';
|
||||
} from '@/core/main/setting';
|
||||
import { RightOutlined, LeftOutlined } from '@ant-design/icons-vue';
|
||||
import { splitText } from '../utils';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import { isMobile } from '../use';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { GameUi } from '../controller';
|
||||
import { mainUi } from '../preset/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
info?: MotaSetting;
|
||||
|
@ -172,9 +172,9 @@ import {
|
||||
import { splitText, tip } from '../utils';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import BoxAnimate from '../components/boxAnimate.vue';
|
||||
import { GameUi } from '../controller';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { mainUi } from '../preset/ui';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
|
@ -21,7 +21,7 @@ import { computed, ref } from 'vue';
|
||||
import skills from '../data/skill.json';
|
||||
import { has } from '../utils';
|
||||
import Column from '../components/colomn.vue';
|
||||
import { mainUi } from '../preset/ui';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
|
@ -84,9 +84,9 @@ import Scroll from '../components/scroll.vue';
|
||||
import { has, splitText, tip } from '../utils';
|
||||
import { isMobile } from '../use';
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { GameUi } from '../controller';
|
||||
import { mainUi } from '../preset/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import type { Chapter } from '@/plugin/game/skillTree';
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -67,10 +67,11 @@ import { sleep } from 'mutate-animate';
|
||||
import { doByInterval } from '../utils';
|
||||
import { triggerFullscreen } from '../utils';
|
||||
import { isMobile } from '../use';
|
||||
import { GameUi } from '../controller';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { mainUi } from '../preset/ui';
|
||||
import { mainSetting } from '../preset/ui';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import { CustomToolbar } from '@/core/main/custom/toolbar';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { mat4 } from 'gl-matrix';
|
||||
// todo: 改了
|
||||
import { bgmController } from '@/module';
|
||||
@ -322,6 +323,7 @@ onMounted(async () => {
|
||||
main = document.getElementById('start-main') as HTMLDivElement;
|
||||
start = document.getElementById('start') as HTMLDivElement;
|
||||
background = document.getElementById('background') as HTMLImageElement;
|
||||
CustomToolbar.closeAll();
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
|
586
packages/legacy-ui/src/ui/toolEditor.vue
Normal file
586
packages/legacy-ui/src/ui/toolEditor.vue
Normal file
@ -0,0 +1,586 @@
|
||||
<template>
|
||||
<div id="tools">
|
||||
<span class="button-text" @click="exit"><left-outlined /> 返回</span>
|
||||
</div>
|
||||
<div id="tool-editor">
|
||||
<div id="tool-left">
|
||||
<Scroll class="tool-list-scroll">
|
||||
<div id="tool-list">
|
||||
<div
|
||||
v-for="(item, i) of list"
|
||||
class="tool-list-item selectable"
|
||||
:selected="i === selected"
|
||||
@click="selected = i"
|
||||
>
|
||||
<span>{{ item.id }}</span>
|
||||
<a-button
|
||||
type="danger"
|
||||
class="tool-list-delete"
|
||||
@click.stop="deleteTool(item.id)"
|
||||
>删除</a-button
|
||||
>
|
||||
</div>
|
||||
<div id="tool-list-add">
|
||||
<div
|
||||
id="tool-add-div"
|
||||
@click="addingTool = true"
|
||||
v-if="!addingTool"
|
||||
>
|
||||
<PlusOutlined></PlusOutlined>
|
||||
<span>新增工具栏</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<a-input
|
||||
style="
|
||||
height: 100%;
|
||||
font-size: 80%;
|
||||
width: 100%;
|
||||
"
|
||||
v-model:value="addingToolId"
|
||||
@blur="addTool"
|
||||
></a-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Scroll>
|
||||
<a-divider
|
||||
type="vertical"
|
||||
dashed
|
||||
v-if="isMobile"
|
||||
style="height: 100%; border-color: #ddd4"
|
||||
></a-divider>
|
||||
<div id="tool-preview" v-if="!!bar && isMobile">
|
||||
<div id="tool-preview-container">
|
||||
<div class="tool-preview-item" v-for="item of bar.items">
|
||||
<component
|
||||
:is="CustomToolbar.info[item.type].show as any"
|
||||
:item="item"
|
||||
:toolbar="bar"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider
|
||||
class="divider"
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
:type="isMobile ? 'horizontal' : 'vertical'"
|
||||
></a-divider>
|
||||
<div id="tool-info">
|
||||
<div id="tool-detail" v-if="!!bar">
|
||||
<Scroll class="tool-item-list-scroll">
|
||||
<div class="tool-item-list">
|
||||
<div
|
||||
class="tool-item-list-item"
|
||||
v-for="item of bar.items"
|
||||
>
|
||||
<div
|
||||
class="tool-item-header"
|
||||
:folded="!unfolded[item.id]"
|
||||
@click="triggerFold(item.id)"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="tool-fold"
|
||||
:folded="!unfolded[item.id]"
|
||||
>
|
||||
<RightOutlined></RightOutlined>
|
||||
</span>
|
||||
<span class="tool-name">
|
||||
<span
|
||||
v-if="editId !== item.id"
|
||||
style="cursor: text"
|
||||
@click.stop="editName(item.id)"
|
||||
>
|
||||
{{ item.id }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<a-input
|
||||
@blur="editNameSuccess(item.id)"
|
||||
@click.stop=""
|
||||
v-model:value="editValue"
|
||||
class="tool-name-edit"
|
||||
></a-input>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<a-button
|
||||
type="danger"
|
||||
class="tool-item-delete"
|
||||
@click.stop="deleteItem(item)"
|
||||
>删除</a-button
|
||||
>
|
||||
</div>
|
||||
<div v-if="!!unfolded[item.id]">
|
||||
<component
|
||||
:is="CustomToolbar.info[item.type].editor"
|
||||
:item="item"
|
||||
:toolbar="bar"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tool-item-add">
|
||||
<div
|
||||
id="tool-item-add-div"
|
||||
v-if="!addingItem"
|
||||
@click="addingItem = true"
|
||||
>
|
||||
<PlusOutlined></PlusOutlined>
|
||||
<span>新增工具</span>
|
||||
</div>
|
||||
<div id="tool-item-add-type" v-else>
|
||||
<span>工具类型</span>
|
||||
<a-select
|
||||
v-model:value="addingType"
|
||||
style="
|
||||
width: 120px;
|
||||
height: 100%;
|
||||
font-size: 80%;
|
||||
background-color: #222;
|
||||
"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(
|
||||
info, type
|
||||
) of CustomToolbar.info"
|
||||
:value="type"
|
||||
>{{ info.name }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
<a-button
|
||||
type="primary"
|
||||
style="font-size: 80%; height: 100%"
|
||||
@click="addItem(true)"
|
||||
>确定</a-button
|
||||
>
|
||||
<a-button
|
||||
@click="addItem(false)"
|
||||
style="
|
||||
background-color: #222;
|
||||
font-size: 80%;
|
||||
height: 100%;
|
||||
"
|
||||
>取消</a-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Scroll>
|
||||
</div>
|
||||
<a-divider dashed v-if="!isMobile"></a-divider>
|
||||
<div id="tool-preview" v-if="!!bar && !isMobile">
|
||||
<div id="tool-preview-container">
|
||||
<div class="tool-preview-item" v-for="item of bar.items">
|
||||
<component
|
||||
:is="CustomToolbar.info[item.type].show as any"
|
||||
:item="item"
|
||||
:toolbar="bar"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { CustomToolbar } from '@/core/main/custom/toolbar';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { computed, onUnmounted, reactive, ref } from 'vue';
|
||||
import {
|
||||
PlusOutlined,
|
||||
RightOutlined,
|
||||
LeftOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import { isMobile } from '../use';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import { deleteWith, tip } from '../utils';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { ToolbarItemType } from '@/core/main/init/toolbar';
|
||||
|
||||
const props = defineProps<{
|
||||
ui: GameUi;
|
||||
num: number;
|
||||
}>();
|
||||
|
||||
const scale = mainSetting.getValue('ui.toolbarScale', 100) / 100;
|
||||
|
||||
const list = CustomToolbar.list;
|
||||
|
||||
const selected = ref(0);
|
||||
const bar = computed(() => list[selected.value]);
|
||||
|
||||
const unfolded = reactive<Record<string, boolean>>({});
|
||||
const editId = ref<string>();
|
||||
const editValue = ref<string>();
|
||||
|
||||
// 添加自定义工具
|
||||
const addingItem = ref(false);
|
||||
const addingType = ref<ToolbarItemType>('item');
|
||||
|
||||
// 添加自定义工具栏
|
||||
const addingTool = ref(false);
|
||||
const addingToolId = ref('');
|
||||
|
||||
/**
|
||||
* 编辑名称
|
||||
*/
|
||||
function editName(id: string) {
|
||||
editId.value = id;
|
||||
editValue.value = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑名称完成
|
||||
*/
|
||||
function editNameSuccess(id: string) {
|
||||
if (bar.value.items.some(v => v.id === editId.value)) {
|
||||
tip('error', '名称重复!');
|
||||
} else {
|
||||
const item = bar.value.items.find(v => v.id === id)!;
|
||||
item.id = editValue.value!;
|
||||
}
|
||||
editId.value = void 0;
|
||||
editValue.value = void 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除自定义工具
|
||||
*/
|
||||
function deleteItem(item: any) {
|
||||
Modal.confirm({
|
||||
title: '确定要删除这个自定义工具吗?',
|
||||
onOk() {
|
||||
deleteWith(bar.value.items, item);
|
||||
},
|
||||
onCancel() {}
|
||||
});
|
||||
}
|
||||
|
||||
function addItem(add: boolean) {
|
||||
if (add) {
|
||||
bar.value.add(
|
||||
// @ts-ignore
|
||||
CustomToolbar.info[addingType.value].onCreate({
|
||||
id: `tool-item-${bar.value.items.length}`,
|
||||
type: addingType.value
|
||||
})
|
||||
);
|
||||
}
|
||||
addingItem.value = false;
|
||||
addingType.value = 'item';
|
||||
}
|
||||
|
||||
/**
|
||||
* 更改折叠
|
||||
*/
|
||||
function triggerFold(id: string) {
|
||||
unfolded[id] = !unfolded[id];
|
||||
}
|
||||
|
||||
function exit() {
|
||||
mainUi.close(props.num);
|
||||
}
|
||||
|
||||
function deleteTool(id: string) {
|
||||
Modal.confirm({
|
||||
title: '确定要删除这个自定义工具栏吗?',
|
||||
onOk() {
|
||||
const index = CustomToolbar.list.findIndex(v => v.id === id);
|
||||
if (index !== -1) {
|
||||
CustomToolbar.list[index].closeAll();
|
||||
CustomToolbar.list.splice(index, 1);
|
||||
}
|
||||
selected.value = 0;
|
||||
},
|
||||
onCancel() {}
|
||||
});
|
||||
}
|
||||
|
||||
function addTool() {
|
||||
if (addingToolId.value === '') {
|
||||
addingToolId.value = '';
|
||||
addingTool.value = false;
|
||||
return;
|
||||
}
|
||||
if (CustomToolbar.list.some(v => v.id === addingToolId.value)) {
|
||||
tip('error', '工具栏名称重复!');
|
||||
return;
|
||||
} else {
|
||||
const bar = new CustomToolbar(addingToolId.value);
|
||||
}
|
||||
addingToolId.value = '';
|
||||
addingTool.value = false;
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
CustomToolbar.save();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#tools {
|
||||
width: 100%;
|
||||
font-family: 'normal';
|
||||
font-size: 3.2vh;
|
||||
height: 5vh;
|
||||
position: fixed;
|
||||
left: 10vw;
|
||||
top: 5vh;
|
||||
}
|
||||
|
||||
#tool-editor {
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'normal';
|
||||
font-size: 150%;
|
||||
user-select: none;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.tool-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tool-list-scroll {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#tool-left {
|
||||
flex-basis: 30%;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#tool-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#tool-list-add {
|
||||
margin-top: 5%;
|
||||
padding: 1% 3% 1% 3%;
|
||||
|
||||
#tool-add-div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
padding: 1% 4%;
|
||||
transition: background-color linear 0.1s;
|
||||
border-radius: 5px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#tool-add-div:hover {
|
||||
background-color: rgba(39, 251, 209, 0.316);
|
||||
}
|
||||
|
||||
#tool-add-div:active {
|
||||
background-color: rgba(39, 251, 209, 0.202);
|
||||
}
|
||||
}
|
||||
|
||||
.tool-list-delete {
|
||||
font-size: 80%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#tool-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
#tool-detail {
|
||||
height: 60%;
|
||||
|
||||
.tool-item-header {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tool-item-header[folded='false'] {
|
||||
border-bottom: 0.5px solid #888;
|
||||
}
|
||||
|
||||
.tool-item-list-scroll {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tool-item-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tool-item-list-item {
|
||||
border: 1px solid #ddd8;
|
||||
margin-bottom: 3%;
|
||||
background-color: #222;
|
||||
padding-left: 2%;
|
||||
}
|
||||
|
||||
.tool-fold {
|
||||
::v-deep(span) {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-fold[folded='false'] {
|
||||
::v-deep(span) {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.tool-name {
|
||||
margin-left: 3%;
|
||||
width: 40%;
|
||||
display: flex;
|
||||
|
||||
.tool-name-edit {
|
||||
width: 100%;
|
||||
font-size: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-item-delete {
|
||||
height: 100%;
|
||||
justify-self: end;
|
||||
font-size: 80%;
|
||||
padding: 2px 15px;
|
||||
}
|
||||
|
||||
#tool-item-add-div {
|
||||
padding: 1% 3% 1% 3%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
padding: 1% 4%;
|
||||
transition: background-color linear 0.1s;
|
||||
border-radius: 5px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#tool-item-add-div:hover {
|
||||
background-color: rgba(39, 251, 209, 0.316);
|
||||
}
|
||||
|
||||
#tool-item-add-div:active {
|
||||
background-color: rgba(39, 251, 209, 0.202);
|
||||
}
|
||||
|
||||
#tool-item-add-type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
padding: 1% 4%;
|
||||
justify-content: space-between;
|
||||
border-radius: 5px;
|
||||
background-color: rgba(39, 251, 209, 0.316);
|
||||
}
|
||||
}
|
||||
|
||||
#tool-preview {
|
||||
height: 40%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
|
||||
#tool-preview-container {
|
||||
width: 90%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border: 2px solid #ddd9;
|
||||
background-color: #0009;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tool-preview-item {
|
||||
display: flex;
|
||||
margin: v-bind('5 * scale + "px"');
|
||||
min-width: v-bind('50 * scale + "px"');
|
||||
min-height: v-bind('50 * scale + "px"');
|
||||
background-color: #222;
|
||||
border: 1.5px solid #ddd8;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.1s linear;
|
||||
|
||||
::v-deep(*) {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#tool-editor {
|
||||
padding-top: 15%;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#tool-left {
|
||||
width: 100%;
|
||||
flex-basis: 40%;
|
||||
max-height: 40vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.tool-list-scroll {
|
||||
height: 100%;
|
||||
flex-basis: 50%;
|
||||
}
|
||||
|
||||
#tool-preview {
|
||||
flex-basis: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#tool-info {
|
||||
width: 100%;
|
||||
flex-basis: 60%;
|
||||
|
||||
#tool-detail {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
138
packages/legacy-ui/src/ui/toolbar.vue
Normal file
138
packages/legacy-ui/src/ui/toolbar.vue
Normal file
@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<Box
|
||||
class="toolbar-container"
|
||||
:dragable="true"
|
||||
:resizable="true"
|
||||
v-model:left="box.x"
|
||||
v-model:top="box.y"
|
||||
v-model:height="box.height"
|
||||
v-model:width="box.width"
|
||||
:key="num"
|
||||
>
|
||||
<div class="toolbar">
|
||||
<div
|
||||
class="toolbar-item"
|
||||
v-for="item of bar.items"
|
||||
@click.stop="click"
|
||||
:noaction="!!item.noDefaultAction"
|
||||
>
|
||||
<component
|
||||
:is="CustomToolbar.info[item.type].show as any"
|
||||
:item="item"
|
||||
:toolbar="bar"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Box from '../components/box.vue';
|
||||
import { CustomToolbar } from '@/core/main/custom/toolbar';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { onUnmounted, reactive, watch } from 'vue';
|
||||
|
||||
interface BoxData {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
bar: CustomToolbar;
|
||||
}>();
|
||||
|
||||
const bar = props.bar;
|
||||
const scale = mainSetting.getValue('ui.toolbarScale', 100) / 100;
|
||||
|
||||
const box = reactive<BoxData>({
|
||||
x: bar.x,
|
||||
y: bar.y,
|
||||
width: bar.width,
|
||||
height: bar.height
|
||||
});
|
||||
|
||||
watch(box, ({ x, y, width, height }) => {
|
||||
bar.x = x;
|
||||
bar.y = y;
|
||||
bar.width = width;
|
||||
bar.height = height;
|
||||
});
|
||||
|
||||
function click() {
|
||||
// pass
|
||||
}
|
||||
|
||||
function posChange() {
|
||||
box.x = bar.x;
|
||||
box.y = bar.y;
|
||||
box.width = bar.width;
|
||||
box.height = bar.height;
|
||||
}
|
||||
|
||||
bar.on('posChange', posChange);
|
||||
|
||||
onUnmounted(() => {
|
||||
bar.off('posChange', posChange);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.toolbar-container {
|
||||
background-color: #0009;
|
||||
padding: v-bind('scale * 5 + "px"');
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.toolbar-item {
|
||||
display: flex;
|
||||
margin: v-bind('scale * 5 + "px"');
|
||||
min-width: v-bind('scale * 50 + "px"');
|
||||
min-height: v-bind('scale * 50 + "px"');
|
||||
background-color: #222;
|
||||
border: 1.5px solid #ddd8;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.1s linear;
|
||||
}
|
||||
|
||||
.toolbar-item[noaction='false'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar-item::v-deep(> *) {
|
||||
height: 100%;
|
||||
min-width: v-bind('scale * 50 + "px"');
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
text-overflow: clip;
|
||||
text-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar-item::v-deep(.button-text)[active='true'] {
|
||||
color: gold;
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.toolbar-item:hover[noaction='false'] {
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
.toolbar-item:active[noaction='false'] {
|
||||
background-color: #333;
|
||||
}
|
||||
</style>
|
@ -116,9 +116,9 @@ import { isMobile } from '../use';
|
||||
import { type, has } from '../utils';
|
||||
import { hyper } from 'mutate-animate';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { GameUi } from '../controller';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { mainUi } from '../preset/ui';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
|
@ -6,8 +6,9 @@ import { Ref, ref } from 'vue';
|
||||
import { EVENT_KEY_CODE_MAP, KeyCode } from '@motajs/client-base';
|
||||
import axios from 'axios';
|
||||
import { decompressFromBase64 } from 'lz-string';
|
||||
import { Keyboard, KeyboardEmits, isAssist } from '@motajs/system-action';
|
||||
import { fixedUi, mainUi } from './preset/ui';
|
||||
import { Keyboard, KeyboardEmits } from '@/core/main/custom/keyboard';
|
||||
import { fixedUi, mainUi } from '@/core/main/init/ui';
|
||||
import { isAssist } from '@/core/main/custom/hotkey';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
type CanParseCss = keyof {
|
||||
|
@ -4,7 +4,6 @@
|
||||
"@motajs/common": "workspace:*",
|
||||
"@motajs/render-core": "workspace:*",
|
||||
"@motajs/render-elements": "workspace:*",
|
||||
"@motajs/render-style": "workspace:*",
|
||||
"@motajs/system-action": "workspace:*"
|
||||
"@motajs/render-style": "workspace:*"
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { Animation, Ticker, Transition } from 'mutate-animate';
|
||||
import { ERenderItemEvent, RenderItem } from '@motajs/render-core';
|
||||
import { gameKey, Hotkey } from '@motajs/system-action';
|
||||
// todo: 改为 monorepo
|
||||
import { gameKey, Hotkey } from '@/core/main/custom/hotkey';
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "@motajs/system-action",
|
||||
"dependencies": {
|
||||
"@motajs/common": "workspace:*"
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export * from './hotkey';
|
||||
export * from './keyboard';
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "@motajs/system",
|
||||
"dependencies": {
|
||||
"@motajs/system-ui": "workspace:*",
|
||||
"@motajs/system-action": "workspace:*"
|
||||
"@motajs/system-ui": "workspace:*"
|
||||
}
|
||||
}
|
@ -1,2 +1 @@
|
||||
export * as Action from '@motajs/system-action';
|
||||
export * as UI from '@motajs/system-ui';
|
||||
|
1187
pnpm-lock.yaml
1187
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -26,7 +26,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { mainUi, fixedUi } from '@motajs/legacy-ui';
|
||||
import { mainUi, fixedUi } from './core/main/init/ui';
|
||||
|
||||
onMounted(() => {
|
||||
const { hook } = Mota.requireAll('var');
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { mat4 } from 'gl-matrix';
|
||||
import { logger } from '@motajs/common';
|
||||
import { WebGLColorArray, createProgram, isWebGL2Supported } from './webgl';
|
||||
import {
|
||||
ILayerRenderExtends,
|
||||
Layer,
|
||||
HeroRenderer,
|
||||
Sprite
|
||||
} from '@motajs/render';
|
||||
WebGLColorArray,
|
||||
createProgram,
|
||||
isWebGL2Supported
|
||||
} from './webgl';
|
||||
import { ILayerRenderExtends, Layer, HeroRenderer, Sprite } from '@motajs/render';
|
||||
|
||||
/**
|
||||
/**
|
||||
* 最大光源数量,必须设置,且光源数不能超过这个值,这个值决定了会预留多少的缓冲区,因此最好尽可能小,同时游戏过程中不可修改
|
||||
* 这个值越大,对显卡尤其是显存的要求会越大,不过考虑到各种设备的性能差异,不建议超过10
|
||||
*/
|
||||
@ -30,13 +29,7 @@ interface ShadowConfig {
|
||||
blur?: number;
|
||||
}
|
||||
|
||||
function addLightFromBlock(
|
||||
floors: FloorIds[],
|
||||
block: number,
|
||||
config: LightConfig,
|
||||
sc?: ShadowConfig,
|
||||
hero?: LightConfig
|
||||
) {
|
||||
function addLightFromBlock(floors: FloorIds[], block: number, config: LightConfig, sc?: ShadowConfig, hero?: LightConfig) {
|
||||
floors.forEach(v => {
|
||||
const shadow = new Shadow(v);
|
||||
shadow.background = [0, 0, 0, 0.2];
|
||||
@ -72,7 +65,7 @@ function addLightFromBlock(
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const hook = Mota.require('var', 'hook');
|
||||
@ -80,10 +73,7 @@ const hook = Mota.require('var', 'hook');
|
||||
hook.once('reset', () => {
|
||||
Shadow.init();
|
||||
addLightFromBlock(
|
||||
core.floorIds
|
||||
.slice(61, 70)
|
||||
.concat(core.floorIds.slice(72, 81))
|
||||
.concat(core.floorIds.slice(85, 107)),
|
||||
core.floorIds.slice(61, 70).concat(core.floorIds.slice(72, 81)).concat(core.floorIds.slice(85, 107)),
|
||||
103,
|
||||
{ decay: 50, r: 300, color: [0.9333, 0.6, 0.333, 0.3] },
|
||||
{ background: [0, 0, 0, 0.2] },
|
||||
@ -92,12 +82,7 @@ hook.once('reset', () => {
|
||||
addLightFromBlock(
|
||||
['MT50', 'MT60', 'MT61', 'MT72', 'MT73', 'MT74', 'MT75'],
|
||||
103,
|
||||
{
|
||||
decay: 20,
|
||||
r: 150,
|
||||
color: [0.9333, 0.6, 0.333, 0.3],
|
||||
noShelter: true
|
||||
},
|
||||
{ decay: 20, r: 150, color: [0.9333, 0.6, 0.333, 0.3], noShelter: true },
|
||||
{ background: [0, 0, 0, 0.3] }
|
||||
);
|
||||
Mota.rewrite(core.control, 'loadData', 'add', () => {
|
||||
@ -110,17 +95,17 @@ hook.once('reset', () => {
|
||||
hook.on('reset', () => {
|
||||
Shadow.update(true);
|
||||
LayerShadowExtends.shadowList.forEach(v => v.update());
|
||||
});
|
||||
})
|
||||
hook.on('setBlock', () => {
|
||||
Shadow.update(true);
|
||||
LayerShadowExtends.shadowList.forEach(v => v.update());
|
||||
});
|
||||
hook.on('changingFloor', floorId => {
|
||||
})
|
||||
hook.on('changingFloor', floorId => {
|
||||
Shadow.clearBuffer();
|
||||
Shadow.update(true);
|
||||
// setCanvasFilterByFloorId(floorId);
|
||||
LayerShadowExtends.shadowList.forEach(v => v.update());
|
||||
});
|
||||
})
|
||||
|
||||
// 深度测试着色器
|
||||
|
||||
@ -274,6 +259,7 @@ void main() {
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
interface ShadowProgram {
|
||||
depth: WebGLProgram;
|
||||
color: WebGLProgram;
|
||||
@ -398,8 +384,7 @@ export class Shadow {
|
||||
* @param nocache 是否不使用缓存
|
||||
*/
|
||||
calShadowInfo(nocache: boolean = false) {
|
||||
if (!nocache && this.cache && Shadow.cached.has(this.floorId))
|
||||
return this.cache;
|
||||
if (!nocache && this.cache && Shadow.cached.has(this.floorId)) return this.cache;
|
||||
Shadow.cached.add(this.floorId);
|
||||
Shadow.clearBuffer();
|
||||
|
||||
@ -417,81 +402,33 @@ export class Shadow {
|
||||
const t = (core._PY_ - (y + h)) * ratio;
|
||||
res.push(
|
||||
// 上边缘
|
||||
l,
|
||||
t,
|
||||
0,
|
||||
l,
|
||||
t,
|
||||
m,
|
||||
r,
|
||||
t,
|
||||
m,
|
||||
r,
|
||||
t,
|
||||
0,
|
||||
r,
|
||||
t,
|
||||
m,
|
||||
l,
|
||||
t,
|
||||
0,
|
||||
l, t, 0,
|
||||
l, t, m,
|
||||
r, t, m,
|
||||
r, t, 0,
|
||||
r, t, m,
|
||||
l, t, 0,
|
||||
// 右
|
||||
r,
|
||||
t,
|
||||
0,
|
||||
r,
|
||||
t,
|
||||
m,
|
||||
r,
|
||||
b,
|
||||
m,
|
||||
r,
|
||||
b,
|
||||
0,
|
||||
r,
|
||||
b,
|
||||
m,
|
||||
r,
|
||||
t,
|
||||
0,
|
||||
r, t, 0,
|
||||
r, t, m,
|
||||
r, b, m,
|
||||
r, b, 0,
|
||||
r, b, m,
|
||||
r, t, 0,
|
||||
// 下
|
||||
r,
|
||||
b,
|
||||
0,
|
||||
r,
|
||||
b,
|
||||
m,
|
||||
l,
|
||||
b,
|
||||
m,
|
||||
l,
|
||||
b,
|
||||
0,
|
||||
l,
|
||||
b,
|
||||
m,
|
||||
r,
|
||||
b,
|
||||
0,
|
||||
r, b, 0,
|
||||
r, b, m,
|
||||
l, b, m,
|
||||
l, b, 0,
|
||||
l, b, m,
|
||||
r, b, 0,
|
||||
// 左
|
||||
l,
|
||||
b,
|
||||
0,
|
||||
l,
|
||||
b,
|
||||
m,
|
||||
l,
|
||||
t,
|
||||
m,
|
||||
l,
|
||||
t,
|
||||
0,
|
||||
l,
|
||||
t,
|
||||
m,
|
||||
l,
|
||||
b,
|
||||
0
|
||||
l, b, 0,
|
||||
l, b, m,
|
||||
l, t, m,
|
||||
l, t, 0,
|
||||
l, t, m,
|
||||
l, b, 0
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -525,7 +462,7 @@ export class Shadow {
|
||||
removeLight(id: string) {
|
||||
const index = this.lights.findIndex(v => v.id === id);
|
||||
this.lights.splice(index, 1);
|
||||
delete this.originLightInfo[id];
|
||||
delete this.originLightInfo[id]
|
||||
this.followHero.delete(id);
|
||||
this.requestRefresh();
|
||||
}
|
||||
@ -572,7 +509,7 @@ export class Shadow {
|
||||
* @param nocache 是否不使用缓存
|
||||
* @param resize 是否resize canvas
|
||||
*/
|
||||
private refresh(nocache: boolean = false, resize: boolean = false) {
|
||||
private refresh(nocache: boolean = false, resize: boolean = false) {
|
||||
this.calShadowInfo(nocache);
|
||||
if (resize) {
|
||||
Shadow.resizeCanvas();
|
||||
@ -590,7 +527,7 @@ export class Shadow {
|
||||
|
||||
// depth test
|
||||
gl.useProgram(Shadow.program.depth);
|
||||
const lightProjection = Shadow.martix.projection;
|
||||
const lightProjection = Shadow.martix.projection
|
||||
|
||||
// 使用 3D 纹理存储深度信息
|
||||
const texture = Shadow.texture.depth;
|
||||
@ -608,23 +545,11 @@ export class Shadow {
|
||||
gl.enable(gl.DEPTH_TEST);
|
||||
gl.depthFunc(gl.LESS);
|
||||
this.lights.forEach((light, i) => {
|
||||
this.depthTest(
|
||||
lightProjection,
|
||||
light,
|
||||
i,
|
||||
texture,
|
||||
length,
|
||||
proj,
|
||||
view
|
||||
);
|
||||
this.depthTest(lightProjection, light, i, texture, length, proj, view);
|
||||
});
|
||||
|
||||
gl.disableVertexAttribArray(position);
|
||||
gl.bufferData(
|
||||
gl.ARRAY_BUFFER,
|
||||
new Float32Array(info.length),
|
||||
gl.STATIC_DRAW
|
||||
);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(info.length), gl.STATIC_DRAW);
|
||||
gl.vertexAttribPointer(position, 3, gl.FLOAT, false, 0, 0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
|
||||
@ -668,14 +593,13 @@ export class Shadow {
|
||||
const v = this.lights[i];
|
||||
data.push(
|
||||
// 坐标
|
||||
v.x * ratio,
|
||||
v.y * ratio,
|
||||
0,
|
||||
0 // 填充到 4 个分量以确保对齐
|
||||
v.x * ratio, v.y * ratio, 0, 0 // 填充到 4 个分量以确保对齐
|
||||
);
|
||||
} else {
|
||||
// 如果没有光源,添加填充以确保统一缓冲区大小保持一致
|
||||
data.push(0, 0, 0, 0);
|
||||
data.push(
|
||||
0, 0, 0, 0
|
||||
);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < MAX_LIGHT_NUM; i++) {
|
||||
@ -683,14 +607,13 @@ export class Shadow {
|
||||
const v = this.lights[i];
|
||||
data.push(
|
||||
// 颜色
|
||||
v.color[0],
|
||||
v.color[1],
|
||||
v.color[2],
|
||||
v.color[3] // 4 个分量的颜色
|
||||
v.color[0], v.color[1], v.color[2], v.color[3] // 4 个分量的颜色
|
||||
);
|
||||
} else {
|
||||
// 如果没有光源,添加填充以确保统一缓冲区大小保持一致
|
||||
data.push(0, 0, 0, 0);
|
||||
data.push(
|
||||
0, 0, 0, 0
|
||||
);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < MAX_LIGHT_NUM; i++) {
|
||||
@ -698,14 +621,13 @@ export class Shadow {
|
||||
const v = this.lights[i];
|
||||
data.push(
|
||||
// 半径、衰减半径、遮挡
|
||||
v.r * ratio,
|
||||
v.decay * ratio,
|
||||
v.noShelter ? 1 : 0,
|
||||
0 // 填充到 4 个分量
|
||||
v.r * ratio, v.decay * ratio, v.noShelter ? 1 : 0, 0 // 填充到 4 个分量
|
||||
);
|
||||
} else {
|
||||
// 如果没有光源,添加填充以确保统一缓冲区大小保持一致
|
||||
data.push(0, 0, 0, 0);
|
||||
data.push(
|
||||
0, 0, 0, 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -713,11 +635,7 @@ export class Shadow {
|
||||
|
||||
const lightsBuffer = Shadow.buffer.color.lights;
|
||||
gl.bindBuffer(gl.UNIFORM_BUFFER, lightsBuffer);
|
||||
gl.bufferData(
|
||||
gl.UNIFORM_BUFFER,
|
||||
new Float32Array(data),
|
||||
gl.DYNAMIC_DRAW
|
||||
);
|
||||
gl.bufferData(gl.UNIFORM_BUFFER, new Float32Array(data), gl.DYNAMIC_DRAW);
|
||||
gl.uniformBlockBinding(Shadow.program.color, blockIndex, 0);
|
||||
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, lightsBuffer);
|
||||
|
||||
@ -726,13 +644,7 @@ export class Shadow {
|
||||
const colorFramebuffer = Shadow.buffer.color.framebuffer;
|
||||
gl.enable(gl.DEPTH_TEST);
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, colorFramebuffer);
|
||||
gl.framebufferTexture2D(
|
||||
gl.FRAMEBUFFER,
|
||||
gl.COLOR_ATTACHMENT0,
|
||||
gl.TEXTURE_2D,
|
||||
colorTexture,
|
||||
0
|
||||
);
|
||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTexture, 0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, colorTexture);
|
||||
gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT);
|
||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||
@ -778,13 +690,7 @@ export class Shadow {
|
||||
const blurFramebuffer = Shadow.buffer.blur1.framebuffer;
|
||||
gl.bindTexture(gl.TEXTURE_2D, colorTexture);
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, blurFramebuffer);
|
||||
gl.framebufferTexture2D(
|
||||
gl.FRAMEBUFFER,
|
||||
gl.COLOR_ATTACHMENT0,
|
||||
gl.TEXTURE_2D,
|
||||
blurTexture,
|
||||
0
|
||||
);
|
||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, blurTexture, 0);
|
||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
|
||||
|
||||
@ -808,7 +714,7 @@ export class Shadow {
|
||||
gl.vertexAttribPointer(texBlur2, 2, gl.FLOAT, false, 0, 0);
|
||||
const blur2IndicesBuffer = Shadow.buffer.blur2.indices;
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, blur2IndicesBuffer);
|
||||
|
||||
|
||||
// Texture
|
||||
const blur2TextureLoc = Shadow.locations.blur2.u_texture;
|
||||
const blur2TextureSizeLoc = Shadow.locations.blur2.u_textureSize;
|
||||
@ -841,24 +747,13 @@ export class Shadow {
|
||||
const gl = Shadow.gl;
|
||||
const ratio = core.domStyle.scale * devicePixelRatio;
|
||||
const cameraMatrix = mat4.create();
|
||||
mat4.lookAt(
|
||||
cameraMatrix,
|
||||
[light.x * ratio, light.y * ratio, core._PX_ * 2 * ratio],
|
||||
[light.x * ratio, light.y * ratio, 0],
|
||||
[0, 1, 0]
|
||||
);
|
||||
mat4.lookAt(cameraMatrix, [light.x * ratio, light.y * ratio, core._PX_ * 2 * ratio], [light.x * ratio, light.y * ratio, 0], [0, 1, 0]);
|
||||
|
||||
const size = core._PX_ * ratio * 2;
|
||||
gl.viewport(0, 0, size, size);
|
||||
const framebuffer = Shadow.buffer.depth.framebuffer[index];
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
||||
gl.framebufferTextureLayer(
|
||||
gl.FRAMEBUFFER,
|
||||
gl.COLOR_ATTACHMENT0,
|
||||
texture,
|
||||
0,
|
||||
index
|
||||
);
|
||||
gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, texture, 0, index);
|
||||
gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT);
|
||||
|
||||
gl.uniformMatrix4fv(proj, false, lightProjection);
|
||||
@ -887,20 +782,12 @@ export class Shadow {
|
||||
gl.UNSIGNED_BYTE,
|
||||
null
|
||||
);
|
||||
gl.texParameteri(
|
||||
gl.TEXTURE_2D_ARRAY,
|
||||
gl.TEXTURE_MIN_FILTER,
|
||||
gl.NEAREST
|
||||
);
|
||||
gl.texParameteri(
|
||||
gl.TEXTURE_2D_ARRAY,
|
||||
gl.TEXTURE_MAG_FILTER,
|
||||
gl.NEAREST
|
||||
);
|
||||
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
return texture!;
|
||||
}
|
||||
|
||||
private static create2DTexture(size: number) {
|
||||
private static create2DTexture(size: number) {
|
||||
const gl = Shadow.gl;
|
||||
const texture = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
@ -1022,62 +909,48 @@ export class Shadow {
|
||||
u_texture: gl.getUniformLocation(blur2, 'u_texture')!,
|
||||
u_textureSize: gl.getUniformLocation(blur2, 'u_textureSize')!
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Matrix
|
||||
const lightProjection = mat4.create();
|
||||
mat4.perspective(lightProjection, FOVY, 1, 1, core._PX_ * ratio);
|
||||
this.martix = {
|
||||
projection: lightProjection
|
||||
};
|
||||
}
|
||||
|
||||
// Buffer
|
||||
const depthFramebuffers = Array(MAX_LIGHT_NUM)
|
||||
.fill(1)
|
||||
.map(v => gl.createFramebuffer()!);
|
||||
const depthFramebuffers = Array(MAX_LIGHT_NUM).fill(1).map(v => gl.createFramebuffer()!);
|
||||
this.buffer = {
|
||||
depth: {
|
||||
position: gl.createBuffer()!,
|
||||
framebuffer: depthFramebuffers
|
||||
},
|
||||
color: {
|
||||
position: this.initBuffer(
|
||||
new Float32Array([1, 1, -1, 1, 1, -1, -1, -1])
|
||||
),
|
||||
texcoord: this.initBuffer(
|
||||
new Float32Array([1, 1, 0, 1, 1, 0, 0, 0])
|
||||
),
|
||||
position: this.initBuffer(new Float32Array([1, 1, -1, 1, 1, -1, -1, -1])),
|
||||
texcoord: this.initBuffer(new Float32Array([1, 1, 0, 1, 1, 0, 0, 0])),
|
||||
indices: this.initIndicesBuffer(),
|
||||
framebuffer: gl.createFramebuffer()!,
|
||||
lights: gl.createBuffer()!
|
||||
},
|
||||
blur1: {
|
||||
position: this.initBuffer(
|
||||
new Float32Array([1, 1, -1, 1, 1, -1, -1, -1])
|
||||
),
|
||||
texcoord: this.initBuffer(
|
||||
new Float32Array([1, 1, 0, 1, 1, 0, 0, 0])
|
||||
),
|
||||
position: this.initBuffer(new Float32Array([1, 1, -1, 1, 1, -1, -1, -1])),
|
||||
texcoord: this.initBuffer(new Float32Array([1, 1, 0, 1, 1, 0, 0, 0])),
|
||||
indices: this.initIndicesBuffer(),
|
||||
framebuffer: gl.createFramebuffer()!
|
||||
},
|
||||
blur2: {
|
||||
position: this.initBuffer(
|
||||
new Float32Array([1, 1, -1, 1, 1, -1, -1, -1])
|
||||
),
|
||||
texcoord: this.initBuffer(
|
||||
new Float32Array([1, 1, 0, 1, 1, 0, 0, 0])
|
||||
),
|
||||
position: this.initBuffer(new Float32Array([1, 1, -1, 1, 1, -1, -1, -1])),
|
||||
texcoord: this.initBuffer(new Float32Array([1, 1, 0, 1, 1, 0, 0, 0])),
|
||||
indices: this.initIndicesBuffer()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Texture
|
||||
this.texture = {
|
||||
depth: this.create3DTexture(core._PX_, MAX_LIGHT_NUM),
|
||||
color: this.create2DTexture(core._PX_),
|
||||
blur: this.create2DTexture(core._PX_)
|
||||
};
|
||||
}
|
||||
this.resizeCanvas();
|
||||
}
|
||||
|
||||
@ -1088,7 +961,7 @@ export class Shadow {
|
||||
mat4.perspective(lightProjection, FOVY, 1, 1, core._PX_ * ratio);
|
||||
this.martix = {
|
||||
projection: lightProjection
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1381,8 +1254,8 @@ export class LayerShadowExtends implements ILayerRenderExtends {
|
||||
static shadowList: Set<LayerShadowExtends> = new Set();
|
||||
id: string = 'shadow';
|
||||
|
||||
layer!: Layer;
|
||||
hero!: HeroRenderer;
|
||||
layer!: Layer
|
||||
hero!: HeroRenderer
|
||||
sprite!: Sprite;
|
||||
|
||||
update() {
|
||||
@ -1392,19 +1265,19 @@ export class LayerShadowExtends implements ILayerRenderExtends {
|
||||
private onMoveTick = (x: number, y: number) => {
|
||||
const now = Shadow.now();
|
||||
if (!now) return;
|
||||
if (now.followHero.size === 0) return;
|
||||
if (now.followHero.size === 0) return;
|
||||
now.followHero.forEach(v => {
|
||||
now.modifyLight(v, {
|
||||
x: x * 32 + 16,
|
||||
y: y * 32 + 16
|
||||
});
|
||||
});
|
||||
now.requestRefresh();
|
||||
|
||||
now.requestRefresh();
|
||||
|
||||
this.layer.requestAfterFrame(() => {
|
||||
this.sprite.update(this.sprite);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private listen() {
|
||||
this.hero.on('moveTick', this.onMoveTick);
|
||||
@ -1424,15 +1297,9 @@ export class LayerShadowExtends implements ILayerRenderExtends {
|
||||
this.sprite = new Sprite('static', false);
|
||||
this.sprite.setHD(true);
|
||||
this.sprite.size(layer.width, layer.height);
|
||||
this.sprite.setRenderFn((canvas, transform) => {
|
||||
this.sprite.setRenderFn((canvas, transform) => {
|
||||
if (Shadow.map[core.status.floorId]) {
|
||||
canvas.ctx.drawImage(
|
||||
Shadow.canvas,
|
||||
0,
|
||||
0,
|
||||
layer.width,
|
||||
layer.height
|
||||
);
|
||||
canvas.ctx.drawImage(Shadow.canvas, 0, 0, layer.width, layer.height);
|
||||
}
|
||||
});
|
||||
|
107
src/core/index.ts
Normal file
107
src/core/index.ts
Normal file
@ -0,0 +1,107 @@
|
||||
// todo: 这个引入不加会报错,应该是循环引用导致的
|
||||
import '@/plugin/utils';
|
||||
import { Focus, GameUi, UiController } from './main/custom/ui';
|
||||
import { GameStorage } from './main/storage';
|
||||
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 { UI } from '@motajs/legacy-ui';
|
||||
import Box from '@/components/box.vue';
|
||||
import BoxAnimate from '@/components/boxAnimate.vue';
|
||||
import Colomn from '@/components/colomn.vue';
|
||||
import EnemyOne from '@/components/enemyOne.vue';
|
||||
import Scroll from '@/components/scroll.vue';
|
||||
import EnemyCritical from '@/panel/enemyCritical.vue';
|
||||
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 * 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', 'RenderUtils', utils);
|
||||
Mota.register('module', 'UI', UI);
|
||||
Mota.register('module', 'UIComponents', {
|
||||
Box,
|
||||
BoxAnimate,
|
||||
Colomn,
|
||||
EnemyOne,
|
||||
Scroll,
|
||||
EnemyCritical,
|
||||
EnemySpecial,
|
||||
EnemyTarget,
|
||||
Keyboard: KeyboardPanel
|
||||
});
|
||||
Mota.register('module', 'Shadow', Shadow);
|
||||
Mota.register('module', 'Effect', {});
|
||||
Mota.register('module', 'Render', Render);
|
||||
Mota.register('module', 'Action', {
|
||||
HeroKeyMover
|
||||
});
|
||||
Mota.register('module', 'Animation', Animation);
|
||||
|
||||
main.renderLoaded = true;
|
||||
Mota.require('var', 'hook').emit('renderLoaded');
|
21
src/core/interface.ts
Normal file
21
src/core/interface.ts
Normal file
@ -0,0 +1,21 @@
|
||||
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,8 +1,8 @@
|
||||
import { KeyCode } from '@motajs/client-base';
|
||||
import { Hotkey, HotkeyData } from '@motajs/system-action';
|
||||
import { Hotkey, HotkeyData } from '../custom/hotkey';
|
||||
import type { HeroMover, IMoveController } from '@/game/state/move';
|
||||
import { Ticker } from 'mutate-animate';
|
||||
import { mainScope } from '@motajs/legacy-ui';
|
||||
import { mainScope } from '../init/hotkey';
|
||||
|
||||
type MoveKey = Record<Dir, HotkeyData>;
|
||||
type MoveKeyConfig = Record<Dir, string>;
|
@ -1,10 +1,18 @@
|
||||
import BoxAnimate from '@/components/boxAnimate.vue';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { logger } from '@motajs/common';
|
||||
import { deleteWith, ensureArray, parseCss, tip } from '@motajs/legacy-ui';
|
||||
import { ResponseBase } from '@motajs/client-base';
|
||||
import { ResponseBase } from '@/core/interface';
|
||||
import {
|
||||
deleteWith,
|
||||
ensureArray,
|
||||
getIconHeight,
|
||||
parseCss,
|
||||
tip
|
||||
} from '@motajs/legacy-ui';
|
||||
import axios, { AxiosResponse, toFormData } from 'axios';
|
||||
import { VNode, h, shallowReactive } from 'vue';
|
||||
import { Component, VNode, h, shallowReactive } from 'vue';
|
||||
// /* @__PURE__ */ import { id, password } from '../../../../user';
|
||||
import { mainSetting } from '../setting';
|
||||
|
||||
type CSSObj = Partial<Record<CanParseCss, string>>;
|
||||
|
||||
@ -205,8 +213,8 @@ export class Danmaku extends EventEmitter<DanmakuEvent> {
|
||||
* @param stroke 描边颜色
|
||||
*/
|
||||
color(fill?: string, stroke?: string) {
|
||||
if (fill) this.textColor = fill;
|
||||
if (stroke) this.strokeColor = stroke;
|
||||
fill && (this.textColor = fill);
|
||||
stroke && (this.strokeColor = stroke);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -484,3 +492,41 @@ export class Danmaku extends EventEmitter<DanmakuEvent> {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@ -105,10 +105,10 @@ export class Keyboard extends EventEmitter<VirtualKeyboardEvent> {
|
||||
this.scopeStack.push(symbol);
|
||||
const ev: Listener<VirtualKeyEmitFn>[] = [];
|
||||
this.onEmitKey[symbol] = ev;
|
||||
// @ts-expect-error 无法推导
|
||||
// @ts-ignore
|
||||
this.events = ev;
|
||||
this.emit('scopeCreate', symbol);
|
||||
if (last) {
|
||||
if (!!last) {
|
||||
this.scopeAssist[symbol] = this.assist;
|
||||
}
|
||||
this.assist = 0;
|
||||
@ -133,7 +133,7 @@ export class Keyboard extends EventEmitter<VirtualKeyboardEvent> {
|
||||
if (!symbol) return;
|
||||
this.scope = symbol;
|
||||
this.assist = this.scopeAssist[symbol];
|
||||
// @ts-expect-error 无法推导
|
||||
// @ts-ignore
|
||||
this.events = this.onEmitKey[symbol];
|
||||
}
|
||||
|
||||
@ -189,7 +189,7 @@ export class Keyboard extends EventEmitter<VirtualKeyboardEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
export function generateKeyboardEvent(_key: KeyCode, assist: number) {
|
||||
export function generateKeyboardEvent(key: KeyCode, assist: number) {
|
||||
const { ctrl, alt, shift } = unwarpBinary(assist);
|
||||
const ev = new KeyboardEvent('keyup', {
|
||||
ctrlKey: ctrl,
|
386
src/core/main/custom/toolbar.ts
Normal file
386
src/core/main/custom/toolbar.ts
Normal file
@ -0,0 +1,386 @@
|
||||
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,5 +1,6 @@
|
||||
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;
|
||||
@ -124,7 +125,6 @@ type UiVBind = Record<string, any>;
|
||||
interface MountedVBind {
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
controller: UiController;
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
@ -132,12 +132,14 @@ export class GameUi extends EventEmitter<GameUiEvent> {
|
||||
static uiList: GameUi[] = [];
|
||||
|
||||
component: Component;
|
||||
hotkey?: Hotkey;
|
||||
id: string;
|
||||
symbol: symbol = Symbol();
|
||||
|
||||
constructor(id: string, component: Component) {
|
||||
constructor(id: string, component: Component, hotkey?: Hotkey) {
|
||||
super();
|
||||
this.component = component;
|
||||
this.hotkey = hotkey;
|
||||
this.id = id;
|
||||
GameUi.uiList.push(this);
|
||||
}
|
||||
@ -271,7 +273,6 @@ export class UiController extends Focus<IndexedGameUi> {
|
||||
const bind = {
|
||||
num,
|
||||
ui,
|
||||
controller: this,
|
||||
...(vBind ?? {})
|
||||
};
|
||||
const sui = ui.with(bind, vOn);
|
@ -18,7 +18,7 @@ const showFixed = debounce((block: Block) => {
|
||||
if (!enemy) return;
|
||||
fixedUi.open(
|
||||
'fixed',
|
||||
{ enemy, close, loc: [cx, cy], hovered },
|
||||
{ enemy, close, loc: [cx, cy] },
|
||||
{ close: closeFixed }
|
||||
);
|
||||
}, 200);
|
@ -1,5 +1,5 @@
|
||||
import { KeyCode } from '@motajs/client-base';
|
||||
import { gameKey, HotkeyJSON } from '@motajs/system-action';
|
||||
import { gameKey, HotkeyJSON } from '../custom/hotkey';
|
||||
import {
|
||||
openDanmakuPoster,
|
||||
tip,
|
||||
@ -9,7 +9,7 @@ import {
|
||||
} from '@motajs/legacy-ui';
|
||||
import { hovered } from './fixed';
|
||||
import { mainUi } from './ui';
|
||||
import { GameStorage } from '@motajs/legacy-system';
|
||||
import { GameStorage } from '../storage';
|
||||
|
||||
export const mainScope = Symbol.for('@key_main');
|
||||
|
3
src/core/main/init/index.ts
Normal file
3
src/core/main/init/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import './fixed';
|
||||
import './keyboard';
|
||||
import './hotkey';
|
@ -1,5 +1,5 @@
|
||||
import { KeyCode } from '@motajs/client-base';
|
||||
import { Keyboard } from '@motajs/system-action';
|
||||
import { Keyboard } from '../custom/keyboard';
|
||||
|
||||
const qweKey = new Keyboard('qwe'); // 字母键盘,A-Z
|
||||
const numKey = new Keyboard('num'); // 数字键盘,1-0
|
||||
@ -239,7 +239,7 @@ numKey
|
||||
y: 0,
|
||||
width: 45,
|
||||
height: 45,
|
||||
text: `<span style='font-size: 80%'>$<br />4</span>`
|
||||
text: `<span style='font-size: 80%'>\$<br />4</span>`
|
||||
})
|
||||
.add({
|
||||
key: KeyCode.Digit5,
|
@ -1,5 +1,5 @@
|
||||
import { Keyboard } from '@motajs/system-action';
|
||||
import KeyboardUI from '../panel/keyboard.vue';
|
||||
import { Keyboard } from '../custom/keyboard';
|
||||
import KeyboardUI from '@/panel/keyboard.vue';
|
||||
|
||||
interface VirtualKeyProps {
|
||||
keyboard: Keyboard;
|
@ -1,7 +1,7 @@
|
||||
import type { SettingComponent, SettingComponentProps } from '../setting';
|
||||
import { Button, InputNumber, Radio } from 'ant-design-vue';
|
||||
import { mainUi } from './ui';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { gameKey } from '../custom/hotkey';
|
||||
|
||||
interface Components {
|
||||
Default: SettingComponent;
|
||||
@ -28,7 +28,7 @@ export function createSettingComponents() {
|
||||
return com;
|
||||
}
|
||||
|
||||
function DefaultSetting(_props: SettingComponentProps) {
|
||||
function DefaultSetting(props: SettingComponentProps) {
|
||||
return (
|
||||
<div>
|
||||
<span> 未知的设置类型 </span>
|
||||
@ -149,7 +149,7 @@ function showSpecialSetting(id: string, vBind?: any) {
|
||||
mainUi.open(id, vBind);
|
||||
}
|
||||
|
||||
function HotkeySetting(_props: SettingComponentProps) {
|
||||
function HotkeySetting(props: SettingComponentProps) {
|
||||
return (
|
||||
<div style="display: flex; justify-content: center">
|
||||
<Button
|
||||
@ -168,7 +168,7 @@ function HotkeySetting(_props: SettingComponentProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function ToolbarEditor(_props: SettingComponentProps) {
|
||||
function ToolbarEditor(props: SettingComponentProps) {
|
||||
return (
|
||||
<div style="display: flex; justify-content: center">
|
||||
<Button
|
||||
@ -183,7 +183,7 @@ function ToolbarEditor(_props: SettingComponentProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function PerformanceSetting(_props: SettingComponentProps) {
|
||||
function PerformanceSetting(props: SettingComponentProps) {
|
||||
return (
|
||||
<div style="display: flex; justify-content: center">
|
||||
<Button
|
1071
src/core/main/init/toolbar.tsx
Normal file
1071
src/core/main/init/toolbar.tsx
Normal file
File diff suppressed because it is too large
Load Diff
74
src/core/main/init/ui.ts
Normal file
74
src/core/main/init/ui.ts
Normal file
@ -0,0 +1,74 @@
|
||||
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';
|
||||
});
|
||||
});
|
574
src/core/main/setting.ts
Normal file
574
src/core/main/setting.ts
Normal file
@ -0,0 +1,574 @@
|
||||
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();
|
||||
});
|
15
src/core/package.ts
Normal file
15
src/core/package.ts
Normal file
@ -0,0 +1,15 @@
|
||||
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);
|
88
src/data/achievement.json
Normal file
88
src/data/achievement.json
Normal file
@ -0,0 +1,88 @@
|
||||
{
|
||||
"normal": [
|
||||
{
|
||||
"name": "虚惊一场",
|
||||
"text": [
|
||||
"打完山洞门口的兽人后只剩一滴血"
|
||||
],
|
||||
"point": 30
|
||||
},
|
||||
{
|
||||
"name": "真能刷",
|
||||
"text": [
|
||||
"勇气之路的刷血怪刷到 <span style=\"color: gold\">15w</span> 以上的血"
|
||||
],
|
||||
"point": 30
|
||||
}
|
||||
],
|
||||
"challenge": [
|
||||
{
|
||||
"name": "逃出生天",
|
||||
"text": [
|
||||
"通过山路追逐战的困难难度"
|
||||
],
|
||||
"point": 20
|
||||
},
|
||||
{
|
||||
"name": "冰与火之舞",
|
||||
"text": [
|
||||
"完成第二章音游特殊战的困难难度"
|
||||
],
|
||||
"point": 50
|
||||
}
|
||||
],
|
||||
"explore": [
|
||||
{
|
||||
"name": "勇气巅峰",
|
||||
"text": [
|
||||
"第一章完成度达到100%"
|
||||
],
|
||||
"progress": "${Mota.Plugin.require('completion_r').getChapterCompletion(1)} / 100",
|
||||
"percent": true,
|
||||
"point": 50
|
||||
},
|
||||
{
|
||||
"name": "你是怎么办到的?!",
|
||||
"text": [
|
||||
"与山路上的若干个神秘木牌对话"
|
||||
],
|
||||
"progress": "${core.getLocalStorage('mountSign', 0)} / 5",
|
||||
"hide": "该探索成就需要你自己探索如何达成",
|
||||
"point": 25
|
||||
},
|
||||
{
|
||||
"name": "智慧之心",
|
||||
"text": [
|
||||
"第二章完成度达到100%"
|
||||
],
|
||||
"progress": "${Mota.Plugin.require('completion_r').getChapterCompletion(2)} / 100",
|
||||
"percent": true,
|
||||
"point": 50
|
||||
},
|
||||
{
|
||||
"name": "源头?",
|
||||
"text": [
|
||||
"在冰封雪原第一个山洞的水源处使用跳跃技能,并向前一步触发剧情"
|
||||
],
|
||||
"hide": "该探索成就需要你自己探索如何达成",
|
||||
"point": 30
|
||||
},
|
||||
{
|
||||
"name": "学坏了",
|
||||
"text": [
|
||||
"学习电摇嘲讽技能"
|
||||
],
|
||||
"hide": "该探索成就需要你自己探索如何达成",
|
||||
"point": 20
|
||||
},
|
||||
{
|
||||
"name": "满腹经纶",
|
||||
"text": [
|
||||
"把第二章中所有能学习的技能都学一遍"
|
||||
],
|
||||
"hide": "该探索成就需要你自己探索如何达成",
|
||||
"progress": "",
|
||||
"point": 50
|
||||
}
|
||||
]
|
||||
}
|
54
src/data/bgm.json
Normal file
54
src/data/bgm.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"title.opus": {
|
||||
"area": "未完工",
|
||||
"name": "川井宪次 - 破裂足音",
|
||||
"from": "《永远的七日之都》",
|
||||
"img": "/project/images/bg.webp",
|
||||
"desc": []
|
||||
},
|
||||
"cave.opus": {
|
||||
"area": "山洞",
|
||||
"name": "Faodail - Wren",
|
||||
"img": "",
|
||||
"desc": []
|
||||
},
|
||||
"grass.opus": {
|
||||
"area": "草地",
|
||||
"name": "大树 & 朱晨阳 - 大树与鹿",
|
||||
"img": "",
|
||||
"desc": []
|
||||
},
|
||||
"mount.opus": {
|
||||
"area": "山路",
|
||||
"name": "Epistra - Dream Of A Dream",
|
||||
"img": "",
|
||||
"desc": []
|
||||
},
|
||||
"escape.opus": {
|
||||
"area": "山路追逐",
|
||||
"name": "Gareth Coker - Escaping a Foul Presence",
|
||||
"from": "《奥日与精灵意志》",
|
||||
"img": "",
|
||||
"desc": []
|
||||
},
|
||||
"plot1.opus": {
|
||||
"area": "勇气之路",
|
||||
"name": "Mark Petrie & Danny McCarthy - Rags To Rings",
|
||||
"img": "",
|
||||
"desc": []
|
||||
},
|
||||
"tower.opus": {
|
||||
"area": "智慧之塔",
|
||||
"name": "Falcom Sound Team J.D.K. - A Light Illuminating The Depth",
|
||||
"from": "《英雄传说:零之轨迹》",
|
||||
"img": "",
|
||||
"desc": []
|
||||
},
|
||||
"beforeBoss.opus": {
|
||||
"area": "战前独白",
|
||||
"name": "Evan LE NY - Some Calm",
|
||||
"from": "《太空化学》",
|
||||
"img": "",
|
||||
"desc": []
|
||||
}
|
||||
}
|
563
src/data/desc.json
Normal file
563
src/data/desc.json
Normal file
@ -0,0 +1,563 @@
|
||||
{
|
||||
"tip": {
|
||||
"text": "注意事项",
|
||||
"condition": "true",
|
||||
"desc": [
|
||||
"这里显示本塔中需要注意的事项。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"1. <span style=\"color: yellow; font-weight: 700\">",
|
||||
"本百科全书字数很多,可以选择性地阅读。</span>不过本条目最好可以全部阅读一遍。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"2. 本百科全书的内容会<span style=\"color: gold\">随着游戏的推进而增加新内容</span>,",
|
||||
"同时每次增加新内容时都会有提示。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"3. <span style=\"color: gold\">背包中的系统设置同样非常重要,有些问题可以在那里找到原因</span>。",
|
||||
"例如当你获得技能时可能会发现开启不了技能,",
|
||||
"就是因为你打开了<span style=\"color: gold\">自动切换技能</span>的功能,在系统设置里面有说。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"4. <span style=\"color: yellow; font-weight: 700\">重要!!!</span>本塔没有考虑录像的二次播放性,",
|
||||
"这意味着如果你从头播放一个录像,播放完成后继续游玩,提交成绩后不能保证绿录像,请谨慎考虑。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"5. 本塔中<span style=\"color: gold\">几乎所有 ui 都可以纵向滚动</span>,如果发现显示不全,",
|
||||
"可以尝试上下拖动,就像浏览网页一样。电脑端还可以使用滚轮上下滚动。",
|
||||
"大部分可以纵向滚动的 ui 都会在右方有一个滚动条,也可以拖动它进行滚动,例如本百科全书的条目列表和",
|
||||
"条目说明都是可以通过上述方式滚动的。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"6. 本塔主要面向电脑端设计,",
|
||||
"<span style=\"color: gold\">建议使用电脑游玩以获得更好的游戏体验,同时使用约16:9的比例游玩更加合适",
|
||||
"</span>。但是手机依然可以游玩本塔,",
|
||||
"但部分操作可能不是很方便,ui 也可能不是很美观,不过依然可以完整体验本游戏。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"7. 对于手机端,可以点击<span style=\"color: gold\">右下角的难度文字</span>来切换工具栏至数字键。",
|
||||
"这样,你可以更加方便地进行使用技能等操作。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"8. 本塔中几乎所有 ui 在打开时都会有一个0.6s的动画,如果不想要,可以在开头捡的系统设置里面关闭(默认关闭)。",
|
||||
"同时,几乎所有 ui 的退出按钮都在左上角。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"9. 地图上显示的怪物临界有可能不准,当其与折线图有差异时,<span style=\"color: gold\">请以折线图为准</span>。"
|
||||
]
|
||||
},
|
||||
"about": {
|
||||
"text": "关于游戏",
|
||||
"condition": "true",
|
||||
"desc": [
|
||||
"使用样板:Vite 魔塔样板",
|
||||
"<br>",
|
||||
"样板版本:V2.10.0",
|
||||
"<br>",
|
||||
"游戏版本:V1.0.0-alpha",
|
||||
"<br>",
|
||||
"游戏作者:古祠",
|
||||
"<br>",
|
||||
"游戏开源地址:<a href=\"https://github.com/unanmed/HumanBreak\" target=\"_blank\">",
|
||||
"https://github.com/unanmed/HumanBreak</a>",
|
||||
"<br>",
|
||||
"本塔遵循MIT开源协议。<a href=\"LICENSE\" target=\"_blank\">查看开源协议</a>",
|
||||
"<br>",
|
||||
"音乐来源:网易云音乐等",
|
||||
"<br>",
|
||||
"素材来源:大素材库、爱给网、网站素材库等",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">特别说明:素材与音乐均来自网络,不得用于商业用途,仅用于参考与学习</span>",
|
||||
"<br>",
|
||||
"特别鸣谢(排名不分先后):",
|
||||
"<br>",
|
||||
"1. 无名甲烷菌(提供部分特殊属性与机制想法)",
|
||||
"<br>",
|
||||
"测试(排名不分先后):",
|
||||
"<br>",
|
||||
"1. 永葆一颗童心",
|
||||
"<br>",
|
||||
"2. 影法师",
|
||||
"<br>",
|
||||
"3. 夜战天明889",
|
||||
"<br>",
|
||||
"4. 霸道的老鼠"
|
||||
]
|
||||
},
|
||||
"tutorial": {
|
||||
"text": "新手教程",
|
||||
"condition": "true",
|
||||
"desc": [
|
||||
"本条目是魔塔游戏的新手教程,如果对魔塔有一定的了解,可以直接忽略。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"魔塔是一种固定数值rpg游戏,在打怪的时候,遵循<span style=\"color: gold\">我打你一下,你打我一下</span>",
|
||||
"的原则,造成的伤害是己方攻击减去对方防御,最后怪物的伤害便是你在战斗中失去的生命值。当然,为了游戏体验,",
|
||||
"战斗过程会被省略。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"宝石可以增加你的属性,在大部分魔塔中,红宝石增加攻击,蓝宝石增加防御,本塔也不例外。血瓶可以增加你的生命值。",
|
||||
"一般情况下,拾取宝物的优先级是<span style=\"color: gold\">红宝石 > 蓝宝石 > 血瓶</span>,",
|
||||
"但部分情况可能不是这样,这需要你自己的游玩经验等。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"本塔还拥有升级机制,升级时能够给你增加大量的属性,因此,一般情况下当你接近升级时,需要尽快打怪升级。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"然后是门。在魔塔中,很多门都不是必开的门,它们的作用一般是可以躲开怪物拿宝石,或者门里面有血瓶等。",
|
||||
"当你血量足够时,这些门可以不用开,不然可能会有必开的门无法开启导致卡关。对于钥匙,每种颜色的钥匙开对应颜色的门,",
|
||||
"价值是<span style=\"color: gold\">红 > 蓝 > 黄</span>。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"为了更加方便,本塔增加了宝石血瓶显示数据的功能,这样你可以清晰地知道每个宝石增加了多少属性。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"下面是勇士基础属性的说明:",
|
||||
"<br>",
|
||||
"<span style=\"color: lightgreen\">1. 生命值</span>:",
|
||||
"勇士的血量,当它归零时,游戏结束",
|
||||
"<br>",
|
||||
"<span style=\"color: lightcoral\">2. 攻击</span>:",
|
||||
"勇士的攻击,攻击越高,每回合对怪物造成的伤害越高",
|
||||
"<br>",
|
||||
"<span style=\"color: lightblue\">3. 防御</span>:",
|
||||
"勇士的防御,防御越高,怪物每回合对你造成的伤害越低",
|
||||
"<br>",
|
||||
"<span style=\"color: green\">4. 经验</span>:",
|
||||
"勇士的经验,到达一定值后会升级。本塔在状态栏中显示为距离升级剩余的经验",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">5. 金币</span>:",
|
||||
"勇士的金币,可以用于购买物品。本塔中在进入第二章后会有用",
|
||||
"<br>",
|
||||
"<span style=\"color: lightgreen\">6. 护盾</span>:",
|
||||
"勇士的护盾,用处是能够在战后减少同等数值的伤害,在本塔中可以使伤害变为负值。本塔中,在点开无上之盾技能后,",
|
||||
"智慧会充当护盾。更多信息可以查看“勇士属性”条目。"
|
||||
]
|
||||
},
|
||||
"noun": {
|
||||
"text": "名词解释",
|
||||
"condition": "true",
|
||||
"desc": [
|
||||
"本条目会解释诸如临界等魔塔术语,对魔塔有一定了解的可以直接忽略。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: lightcoral\">1. 临界</span>:",
|
||||
"在魔塔中,临界是一个非常重要的东西。首先,我们很容易可以得到,吃攻击时只有当减少了战斗回合数时怪物的伤害会减少,",
|
||||
"那么,吃攻击时怪物的减伤是不连续的。而<span style=\"color: gold\">距离下一次减少怪物的伤害需要加的攻击的量</span>",
|
||||
"便是临界。当我们吃一个攻击恰好使怪物伤害减少时,称为“踩临界”。一般情况下,踩临界的减伤要比吃防御要高,",
|
||||
"因此,当能踩到临界时,我们应当先踩临界,再吃防御。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: lightblue\">2. 加防</span>:",
|
||||
"加防指的是加防对怪物的减伤。在本塔中,会以“n防”的形式显示在怪物手册或其他地方。在本塔中,一般你不需要刻意计算",
|
||||
"临界与加防减伤,你可以在怪物手册中<span style=\"color: gold\">查看减伤折线图</span>,",
|
||||
"更多信息请查看“怪物手册”条目。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">3. 咸鱼</span>:",
|
||||
"一般来讲,开不必开的门,或者使用不必使用的道具被称为咸鱼,或者是咸门,咸道具。一般情况下,说“咸”便是指咸鱼。",
|
||||
"一般情况下,门后面有宝石且无法通过其他方式进入的都是必开门,而只有血瓶的都是咸鱼门。"
|
||||
]
|
||||
},
|
||||
"shortcut": {
|
||||
"text": "快捷键",
|
||||
"condition": "true",
|
||||
"desc": [
|
||||
"这里包含本塔中所有的快捷键。对于手机端,可以点击工具栏的难度的位置切换工具栏至数字键。",
|
||||
"下面会分为样板快捷键和本塔快捷键两类分别说明。可以ctrl+F进行搜索快捷键的功能。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"下面是样板中的所有快捷键:",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">X</span>:打开怪物手册",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">S</span>:打开存档界面",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">D</span>:打开读档界面",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">A或5</span>:读取自动存档",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">W或6</span>:撤销读取的自动存档",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">Q</span>:打开装备栏",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">T</span>:打开道具栏",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">G</span>:打开楼层传送器",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">Z或单击勇士</span>:勇士转向",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">空格或双击勇士或7</span>:轻按(拾取勇士周围的宝物但不移动勇士)",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">Esc</span>:打开游戏菜单",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">R</span>:打开录像回放菜单",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">N</span>:询问是否返回游戏主菜单",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">V</span>:打开快捷商店",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">B</span>:打开数据统计界面",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">Alt + 数字键</span>:快速换装",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">PgUp或PgDn</span>:浏览地图",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">P</span>:打开评论区",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"下面是本塔中新增的快捷键(不包括技能,技能快捷键请在查看技能界面中查看):",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">M</span>:快速标记怪物",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">J</span>:打开技能树",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">H</span>:打开百科全书",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">E</span>:查看鼠标位置怪物的特殊属性信息",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">C</span>:查看鼠标位置怪物的详细临界信息"
|
||||
]
|
||||
},
|
||||
"extraAttr": {
|
||||
"text": "勇士属性",
|
||||
"condition": "true",
|
||||
"desc": [
|
||||
"这里只对本塔中新增的勇士属性进行说明。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: lightblue\">1. 智慧</span>:",
|
||||
"智慧是该塔的核心属性之一。智慧可用于智慧加点,该功能会在进入第一章后开启。使用智慧可以点技能树。",
|
||||
"除此之外,智慧也有其它功能。例如点开无上之盾技能后智慧还可以充当护盾,第二章点开学习技能后可以使用智慧学习怪物技能等。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: lightgreen\">2. 生命回复</span>:",
|
||||
"生命回复指的是勇士每回合回复的生命值。当与怪物战斗时,勇士每回合都会回复对应量的生命值。因此,当吃攻击时,",
|
||||
"与怪物战斗的回合数可能会减少,导致生命回复的总回复量减少。不过大部分情况下不需要在意这一点,",
|
||||
"减少一回合并不会对吸的血造成很大的影响,除了一些特殊情况。",
|
||||
"该项会显示在状态栏的生命值右方偏下的位置。该项不会超过勇士防御的十分之一,如果真实值溢出,那么多余部分会忽略,",
|
||||
"当防御提高时,其值会一同改变",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: lightcoral\">3. 额外攻击</span>:",
|
||||
"额外攻击指的是勇士每回合的额外造成的伤害。一般情况下,当勇士破了怪物的防御时,该项便会起作用。",
|
||||
"额外攻击相当于魔攻,无法通过一般方式减免。当勇士攻击怪物时,每回合都会附加对应量的伤害,对坚固怪同样有效。",
|
||||
"额外攻击会显示在状态栏的攻击右方偏下的位置。"
|
||||
]
|
||||
},
|
||||
"statusBar": {
|
||||
"text": "状态栏",
|
||||
"condition": "true",
|
||||
"desc": [
|
||||
"在本塔中,状态栏与游戏画面是分开的。<span style=\"color: gold\">你可以自由拖动状态栏,也可以修改其大小</span>。",
|
||||
"具体方法如下:点击一下状态栏之后,左上角的拖拽图标会放大,此时你可以按住它拖动状态栏。",
|
||||
"你可以直接将鼠标放到状态栏的边框上,然后直接拖动以改变状态栏的大小。手机端可以先点击一下状态栏使边框",
|
||||
"变宽,然后拖动。电脑端点击状态栏也可以使边框变宽。如果你想折叠状态栏,完全可以拖动状态栏的下边框,",
|
||||
"然后直接拖动至上方,这时状态栏便会变成一条线,相当于折叠了状态栏",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">状态栏可以纵向滚动</span>,",
|
||||
"如果你发现状态栏显示不全,可以尝试拉大状态栏,或者纵向拖动状态栏,就像网页上下滚动一样。",
|
||||
"电脑端还可以使用滚轮上下滚动。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"如果你觉得状态栏有些碍事,你完全可以将其缩小,或者把它放到不碍事的地方。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"状态栏上面可能会有按钮,你可以直接点击。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"对状态栏布局的说明。",
|
||||
"<br>",
|
||||
"本塔的状态栏的布局较为灵活。它是横向的布局,在状态栏较宽时可以看到,属性会横向依次显示。按照显示顺序,",
|
||||
"状态栏显示项依次为:",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"1. <span style=\"color: gold\">楼层名</span>,点击后进入浏览地图界面",
|
||||
"<br>",
|
||||
"2. <span style=\"color: gold\">勇士等级</span>",
|
||||
"<br>",
|
||||
"3. <span style=\"color: gold\">当前开启的技能</span>",
|
||||
"<br>",
|
||||
"4. <span style=\"color: lightgreen\">当前勇士生命值</span>,右方偏下为每回合回复的生命值",
|
||||
",当点开治愈之泉技能时,右方偏上会显示距离增加生命回复剩余血瓶数",
|
||||
"<br>",
|
||||
"5. <span style=\"color: lightcoral\">当前勇士的攻击</span>,右方偏下为勇士的额外攻击",
|
||||
"<br>",
|
||||
"6. <span style=\"color: lightblue\">当前勇士的防御</span>,当有魔法防御时,右方偏下为勇士的魔法防御",
|
||||
"<br>",
|
||||
"7. <span style=\"color: lightgreen\">当前勇士的智慧</span>,可以用于智慧加点等",
|
||||
"<br>",
|
||||
"8. <span style=\"color: gold\">当前勇士的金币</span>",
|
||||
"<br>",
|
||||
"9. <span style=\"color: lightgreen\">当前勇士距离升级剩余经验数</span>",
|
||||
"<br>",
|
||||
"10. <span style=\"color: gold\">三色钥匙</span>",
|
||||
"<br>",
|
||||
"11. <span style=\"color: gold\">打开技能树</span>(进入第一章后开启)",
|
||||
"<br>",
|
||||
"12. <span style=\"color: gold\">查看勇士的技能</span>(进入第一章后开启)"
|
||||
]
|
||||
},
|
||||
"markEnemy": {
|
||||
"text": "标记怪物",
|
||||
"condition": "true",
|
||||
"desc": [
|
||||
"标记怪物可以使你能够更加方便地了解一个怪物的情况。",
|
||||
"<br>",
|
||||
"你可以通过以下两种方式标记怪物:",
|
||||
"<br>",
|
||||
"1. 打开怪物手册,选中怪物,进入怪物更多信息栏,点击标记怪物。",
|
||||
"<br>",
|
||||
"2. 将鼠标移动到你想要标记的怪物上面,<span style=\"color: gold\">",
|
||||
"按下M键</span>,即可标记怪物,注意浏览地图中不能用该方式标记。",
|
||||
"手机端暂时没有快速标记怪物的方式。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">当一个怪物被标记后,怪物会有以下行为</span>:",
|
||||
"<br>",
|
||||
"1. 当勇士恰好能打败怪物时,会进行提示",
|
||||
"<br>",
|
||||
"2. 当怪物的伤害恰好低于勇士生命值的2/3或1/3时,会进行提示",
|
||||
"<br>",
|
||||
"3. 当勇士恰好踩到怪物的临界时,会进行提示",
|
||||
"<br>",
|
||||
"4. 当怪物零伤时,会进行提示",
|
||||
"<br>",
|
||||
"5. 被标记的怪物会出现类似于状态栏的盒子,可以随意拖动和改变大小。你也可以选择关闭这个盒子,",
|
||||
"被关闭后可以通过重新标记来打开。这个盒子会显示标记的怪物的临界与伤害信息等,与状态栏一样,可以纵向滚动。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"这个功能可以用于标记boss或者较强的挡路怪,当这些怪能够攻击时你可以直接收到信息,不需要再时刻费心注意怪物的伤害。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">注意,标记的怪物是不计入存档的,同时标记的怪物只在本次游戏中有效,刷新页面后便会消失。</span>"
|
||||
]
|
||||
},
|
||||
"book": {
|
||||
"text": "怪物手册",
|
||||
"condition": "true",
|
||||
"desc": [
|
||||
"本塔的怪物手册功能很多,下面一一介绍。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"首先,你可以按X打开怪物手册。除此之外,将鼠标移动到怪物上也可以定点查看怪物的粗略信息。",
|
||||
"将鼠标移动到一个怪物上,按下<span style=\"color: gold\">",
|
||||
"E键</span>,可以查看该怪物的特殊属性信息。按下<span style=\"color: gold\">",
|
||||
"C键</span>,可以查看该怪物的详细临界信息。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"怪物手册打开的时候有一个0.6秒的动画,如果不想要可以在开头捡的系统设置里面关闭(默认关闭)。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"打开怪物手册后,怪物手册的布局与样板自带的类似。与样板不同的是,这里的怪物手册不再是翻页式结构。",
|
||||
"<span style=\"color: gold\">这里的怪物手册是滚动式结构</span>,",
|
||||
"你可以像浏览网页一样,用手指或鼠标上下滚动或者拖动右边的滚动条,电脑端还可以使用滚轮。",
|
||||
"对于电脑端,还可以使用键盘操作。上和下可以上下选择怪物,左和右可以向上或向下移动5个怪物。这些操作与样板都类似。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"点击一个怪物或者按下回车空格后,将进入怪物详细信息界面。这个界面分为多个栏,分别是特殊属性栏,详细临界栏,更多信息栏。",
|
||||
"进入怪物详细信息后默认在特殊属性栏,该栏可以查看怪物的特殊属性。",
|
||||
"注意特殊属性依然可以纵向滚动。在特殊属性下方,",
|
||||
"是怪物的临界表,可以粗略地查看怪物的临界信息。在下方,你可以点击详细临界信息进入详细临界栏。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"在详细临界栏中,怪物的伤害会以<span style=\"color: gold\">可视化折线图</span>的方式显示出来,",
|
||||
"从而你可以更为清晰地看出怪物减伤趋势。",
|
||||
"除了查看怪物伤害曲线,你还可以规划宝石。每个折线图下方都有一个滑动条,你可以拖动来模拟吃宝石。",
|
||||
"注意,拖动时,滑动条左边会显示当前的加攻或加防次数,这个数值指的是在勇士所在地图中需要吃的最弱的宝石数量。",
|
||||
"例如,当前勇士所在地图中最弱的宝石加2点攻击,加攻次数为3,那么勇士的攻击增加量就为6。",
|
||||
"勇士增加的攻击数值也会在下方显示。当加攻次数和加防次数改变时,折线图也会变化。",
|
||||
"当前状态下怪物的伤害以及减伤总量也会在下方显示。<span style=\"color: gold\">",
|
||||
"注意在此栏中无法通过点击屏幕回到怪物手册界面,更多信息请查看最后一段</span>。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"在特殊属性栏,点击下方的怪物更多信息可以进入更多信息栏。此栏中,你可以查看怪物描述。但这不是这一栏的核心功能。",
|
||||
"这一栏的核心功能是标记怪物。被标记的怪物会有一些非常方便的行为,这些行为可以在“",
|
||||
"<span style=\"color: gold\">标记怪物</span>”条目中查看。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"注意,在怪物详细信息中,除详细临界栏外均可以通过点击屏幕返回到怪物手册界面。",
|
||||
"如果你是电脑端,在任意栏目中<span style=\"color: gold\">按下X键</span>会退出怪物手册,返回游戏,",
|
||||
"<span style=\"color: gold\">按下回车(Enter)键</span>会回到怪物手册界面。"
|
||||
]
|
||||
},
|
||||
"fly": {
|
||||
"text": "楼层传送器",
|
||||
"condition": "true",
|
||||
"desc": [
|
||||
"楼传界面打开时会有一个0.6秒的动画,如果不想要可以在开头捡的系统设置里面关闭。(默认关闭)",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"本塔的楼层传送器是一个集<span style=\"color: gold\">分区、小地图、楼层传送、浏览地图</span>于一体的多功能楼传。",
|
||||
"<a href=\"https://unanmed.github.io/HumanBreak/maps/index.html\" target=\"_blank\">你也可以点击这里</a>查看所有区域的缩略图。",
|
||||
"下面是楼传的具体说明:",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"首先,对于电脑端,最左侧显示区域信息,手机端则在上方的左侧。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"然后,区域的右侧是小地图栏,这一栏会显示楼层的平面结构。你可以拖动,也可以使用滚轮或者双指放缩,当放缩到一定大小时,",
|
||||
"会显示地图的缩略图。直接点击地图也可以选中地图,再次点击会传送至目标地图。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"对于电脑端,最右侧是当前选中的地图的缩略图,手机则在下方,点击缩略图也可以传送。缩略图的下方是当前选中的地图名,",
|
||||
"左右各有两个按钮,表示后退10层、后退1层、前进1层、前进10层,与样板的楼传的按钮功能类似,对于小地图无法显示的单层,",
|
||||
"可以使用该功能到达。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"最下方是设置按钮,可以切换无边框模式,电脑端还可以切换传统按键模式,传统按键模式下按键遵循样板的楼传按键方式。",
|
||||
"对于非传统模式,<span style=\"color: gold\">上下左右</span>可以移动地图,",
|
||||
"<span style=\"color: gold\">PageUp和PageDown</span>可以前进1层或后退1层。"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"text": "道具栏与装备栏",
|
||||
"condition": "true",
|
||||
"desc": [
|
||||
"道具栏与装备栏打开时会有一个0.6秒的动画,如果不想要可以在开头捡的系统设置里面关闭。(默认关闭)",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"本塔的道具栏没有特别之处,这里不需要说明。主要是装备栏。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"本塔的装备栏手机和电脑端不同,电脑端比手机端多了一个勇士属性的显示。在装备栏的装备列表栏,",
|
||||
"<span style=\"color: gold\">上方有两个选择框与一个排序方式的选项</span>。",
|
||||
"这三个可以筛选你拥有的装备并进行排序,从而让你能够更清楚地知道哪个装备更强。",
|
||||
"第一个选择框可以筛选装备增加的属性,如果装备不增加选择的属性,那么会不显示。第二个选择框可以筛选增加的属性的方式,",
|
||||
"有数值增加和百分比增加两种。在这个选择框右边有一个图标,这个图标可以改变武器的排序方式,有升序和降序两种,默认为升序。",
|
||||
"例如,你拥有两个装备,分别增加10攻击和20攻击,三者你分别选择了攻击,数值,升序,那么增加10攻击的装备会排在上面,",
|
||||
"而增加20攻击的装备会排在下面。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"对于电脑端,如果你想装装备,<span style=\"color: gold\">可以直接拖动装备至装备孔</span>,",
|
||||
"也可以选中装备后再次点击。<span style=\"color: gold\">手机端暂时无法拖动装备</span>。当选中一个装备后,",
|
||||
"电脑端和手机端均会显示装备增加或减少的属性,注意有的装备可能<span style=\"color: gold\">不增加属性但是有特殊功能</span>。",
|
||||
"对于电脑端,还会直接在勇士属性栏显示增加或减少的属性。"
|
||||
]
|
||||
},
|
||||
"achievement": {
|
||||
"text": "成就",
|
||||
"condition": "true",
|
||||
"desc": [
|
||||
"成就系统是本塔的一个独立系统。它不会像勇士属性一样跟随存档变化,而是只要你完成了成就,那么就永远完成了,",
|
||||
"除非你清理了浏览器。每个成就都有成就点,<span style=\"color: gold\">成就点目前没有实际用途,",
|
||||
"只是一个收集要素,对游戏进程没有任何影响。</span>",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"成就分为三种,普通成就,挑战成就和探索成就。普通成就完成难度一般较低,挑战成就完成难度较高,",
|
||||
"而探索成就一般需要你自己探索如何完成。对于完成度类型的探索成就,它的完成度由到达过的地图与本章完成的成就数决定。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: gold\">调试模式下无法完成成就!</span>"
|
||||
]
|
||||
},
|
||||
"score": {
|
||||
"text": "计分方式",
|
||||
"condition": "true",
|
||||
"desc": [
|
||||
"第一章计分方式:血量 + 黄 * 5000 + 蓝 * 15000",
|
||||
"<br>",
|
||||
"第二章计分方式:血量 / 10 + 黄 * 2000 + 蓝 * 5000 + 红 * 10000"
|
||||
]
|
||||
},
|
||||
"skillTree": {
|
||||
"text": "技能树",
|
||||
"condition": "flags.chapter > 0",
|
||||
"desc": [
|
||||
"打开技能树可以点击状态栏的<span style=\"color: gold\">",
|
||||
"技能树按钮</span>(如果发现没有显示可以尝试上下滚动状态栏),还可以按",
|
||||
"<span style=\"color: gold\">快捷键J</span>打开。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"技能树是本塔的主要玩法之一。它可以让你使用智慧来学习技能,增加属性等。智慧在状态栏显示在防御的下一项,",
|
||||
"绿宝石可以增加勇士的智慧。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"打开技能树页面后,你可以在上方看到技能的名称与描述,下方会显示技能树,以及升级要求等。点击一个技能可以选中技能,",
|
||||
"再次点击可以升级技能。注意,前置技能栏可以上下滚动,因此如果发现显示不全,可以尝试上下滚动前置技能栏",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"注意,技能在点开之后是无法取消的,因此,加点时请慎重加点。注意,部分技能是必点技能,这些技能会在技能说明中明确指出,",
|
||||
"这些技能一般需要尽早点出。"
|
||||
]
|
||||
},
|
||||
"study": {
|
||||
"text": "学习",
|
||||
"condition": "Mota.Plugin.require('skillTree_g').getSkillLevel(11) > 0",
|
||||
"desc": [
|
||||
"本条目会详细说明学习的机制与所有可以被学习的技能被学习后的效果。当前已经学习的技能会以与状态栏类似的盒子展示出来。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"首先,学习技能消耗的智慧点会越来越多,初始消耗的智慧点为500,每学习一次增加250。",
|
||||
"学习的技能可以持续5场战斗,在技能树界面每升级一次增加3场,",
|
||||
"<span style=\"color: gold\">当前为${Mota.Plugin.require('skillTree_g').getSkillLevel(11) * 3 + 2}场</span>。",
|
||||
"学习后对应属性的值,例如抱团怪增加的属性百分比,会与被学习的怪物相同。学习界面可以使用背包中的道具或点击状态栏打开。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"下面会详细说明每一种可以被学习的技能被学习后的效果,没有列出的均不可学习。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: #fc3\">1. 致命一击</span>:勇士每5回合对怪物造成一次强力攻击。",
|
||||
"<br>",
|
||||
"<span style=\"color: #bbb0ff\">2. 恶毒</span>:勇士攻击无视怪物的防御。",
|
||||
"<br>",
|
||||
"<span style=\"color: #c0b088\">3. 坚固</span>:勇士防御不低于怪物的攻击-1。",
|
||||
"<br>",
|
||||
"<span style=\"color: #fe7\">4. n连击</span>:勇士每回合攻击n次",
|
||||
"<br>",
|
||||
"<span style=\"color: #b30000\">5. 饥渴</span>:勇士在战前吸取怪物一定量的攻击加载自己身上,",
|
||||
"同时减少怪物相应量的攻击,优先于怪物。",
|
||||
"<br>",
|
||||
"<span style=\"color: #fa4\">6. 抱团</span>:勇士周围每有一个拥有抱团属性的怪物,勇士的属性便增加一定值。",
|
||||
"相应地,拥有抱团属性的怪物也会受到勇士的加成。",
|
||||
"<br>",
|
||||
"<span style=\"color: #b0c0dd\">7. 勇气之刃</span>:勇士第一回合造成一定量的伤害,之后正常。",
|
||||
"<br>",
|
||||
"<span style=\"color: #ff00d2\">8. 勇气冲锋</span>:勇士首先发动冲锋,造成一定量的伤害,眩晕怪物5回合。",
|
||||
"学习该技能后,勇士无条件先手。",
|
||||
"<br>",
|
||||
"<span style=\"color: #bbb0ff\">9. 魔攻</span>:勇士攻击无视怪物的防御。",
|
||||
"<br>",
|
||||
"<span style=\"color: #b0b666\">10. 先攻</span>:勇士无条件先手。"
|
||||
]
|
||||
},
|
||||
"special1": {
|
||||
"text": "第一章怪物特技",
|
||||
"condition": "flags.chapter > 0",
|
||||
"desc": [
|
||||
"这里会展示第一章的怪物中需要特别说明的怪物特技。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: #c0b088\">1. 坚固</span>:",
|
||||
"在本塔中,额外攻击可以对坚固怪造成额外伤害。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: #80eed6\">2. 绝对防御</span>:",
|
||||
"该怪物一般可以用于刷血。该怪物可以使你每回合对怪物造成的伤害恰好为1,导致战斗回合数很高,因此可以刷血。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: #fc3\">3. 致命一击、勇气之刃、勇气冲锋</span>:",
|
||||
"造成的伤害为怪物每回合对勇士的伤害的一定倍数,而非攻击提高一定倍数。"
|
||||
]
|
||||
},
|
||||
"special2": {
|
||||
"text": "第二章怪物特技",
|
||||
"condition": "flags.chapter > 1",
|
||||
"desc": [
|
||||
"这里会展示第二章的怪物中需要特别说明的怪物特技。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: #f66\">1. 电摇嘲讽</span>:",
|
||||
"该特技会撞碎路上的所有地形和门,不需要消耗钥匙,拾取路上的所有道具,与路上的怪物战斗,最后与该怪物战斗。",
|
||||
"如果怪物所在位置可以被嘲讽,那么勇士会被继续嘲讽。如果在被嘲讽的路上可以被其他怪物嘲讽,则不会触发。",
|
||||
"如果一个点可以被多个怪物嘲讽,那么会优先选择最靠左上角的怪物。在地图上会标记出勇士的移动方向。",
|
||||
"<span style=\"color: gold\">在被嘲讽之前会自动存档。</span>",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"<span style=\"color: #d8a\">2. 永夜</span>、<span style=\"color: #ffd\">极昼</span>:",
|
||||
"战斗后会在本楼层中加减怪物与勇士的攻防,每个楼层会单独存储。例如你在1楼层增加了100点攻击,2楼层减少了100点攻击,",
|
||||
"那么当你从2楼层到1楼层时,攻击会增加200点,反之亦然。注意这里没有计算buff。"
|
||||
]
|
||||
}
|
||||
}
|
8
src/data/enemyBorder.json
Normal file
8
src/data/enemyBorder.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"redSwordsman": {
|
||||
"box-shadow": "0px 0px 8px white"
|
||||
},
|
||||
"E374": {
|
||||
"box-shadow": "0px 0px 8px gray"
|
||||
}
|
||||
}
|
101
src/data/logger.json
Normal file
101
src/data/logger.json
Normal file
@ -0,0 +1,101 @@
|
||||
{
|
||||
"error": {
|
||||
"1": "Unexpected error when posting danmaku. Error info: $1",
|
||||
"2": "Unexpected loading error in loading resource '$1/$2''. Error info: $3",
|
||||
"3": "Syntax error in parsing CSS: Unexpected ':'. Col: $1. CSS string: '$2'",
|
||||
"4": "Syntax error in parsing CSS: Unexpected ';'. Col: $1. CSS string: '$2'",
|
||||
"5": "Syntax error in parsing CSS: Missing property name after '-'. Col: $1. CSS string: '$2'",
|
||||
"6": "Syntax error in parsing CSS: Unexpected end of css, expecting ':'. Col: $1. CSS string: '$2'",
|
||||
"7": "Syntax error in parsing CSS: Unexpected end of css, expecting property value. Col: $1. CSS string: '$2'",
|
||||
"8": "Post danmaku with not allowed css. Info: $1",
|
||||
"9": "Cannot initialize shader program. Error info: $1",
|
||||
"10": "Cannot compile $1 shader. Error info: $2",
|
||||
"11": "Cache depth cannot larger than 31.",
|
||||
"12": "Cannot move while status is not 'moving'. Call 'readyMove' first.",
|
||||
"13": "Cannot compile $1 shader. Error info: $2",
|
||||
"14": "",
|
||||
"15": "",
|
||||
"16": "Cannot find log message for $1 code $2.",
|
||||
"17": "Cannot use shader program for shader element that does not belong to it.",
|
||||
"18": "Cannot delete shader program for shader element that does not belong to it.",
|
||||
"19": "Cannot create MotaRenderer instance for nonexistent canvas.",
|
||||
"20": "Cannot create render element for tag '$1', since there's no registration for it.",
|
||||
"21": "Incorrect render prop type is delivered. key: '$1', expected type: '$2', delivered type: '$3'",
|
||||
"22": "Incorrect props for custom tag. Please ensure you have delivered 'item' prop and other required props.",
|
||||
"23": "Cannot get reader when fetching '$1'.",
|
||||
"24": "Cannot decode source type of '$1', since there is no registered decoder for that type.",
|
||||
"25": "Unknown audio type. Header: '$1'",
|
||||
"26": "Uncaught error when fetching stream data from '$1'. Error info: $2.",
|
||||
"1101": "Shadow extension needs 'floor-hero' extension as dependency.",
|
||||
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency.",
|
||||
"1301": "Portal extension need 'floor-binder' extension as dependency.",
|
||||
"1401": "Halo extension needs 'floor-binder' extension as dependency."
|
||||
},
|
||||
"warn": {
|
||||
"1": "Resource with type of 'none' is loaded.",
|
||||
"2": "Repeat load of resource '$1/$2'.",
|
||||
"3": "Unknown danmaku tag: $1",
|
||||
"4": "Ignored a mismatched ']' in danmaku.",
|
||||
"5": "Repeat post danmaku.",
|
||||
"6": "Registered special danmaku element: $1.",
|
||||
"7": "Unknown special danmaku element: '$1'.",
|
||||
"8": "Incomplete render data is put. None will be filled to the lacked data.",
|
||||
"9": "Data transfered is partially (or totally) out of range. Overflowed data will be ignored.",
|
||||
"10": "Cannot resolve big image of enemy '$1;.",
|
||||
"11": "Cannot resolve material $1. Material not exists.",
|
||||
"12": "Cannot mark buffable with a non-number status. Key: '$1'.",
|
||||
"13": "Cannot set buff of non-number status. Key: '$1'.",
|
||||
"14": "Cannot add status of non-number status. Key: '$1'.",
|
||||
"15": "Cannot get item of a non-item block on loc: $1,$2,$3.",
|
||||
"16": "Override repeated state key: '$1'.",
|
||||
"17": "Floor-damage extension needs 'floor-binder' extension as dependency.",
|
||||
"18": "Uncaught error in posting like info for danmaku. Danmaku id: $1.",
|
||||
"19": "Repeat light id: '$1'.",
|
||||
"20": "Cannot apply animation to camera operation that does not belong to it.",
|
||||
"21": "Cannot apply transition to camera operation that does not belong to it.",
|
||||
"22": "There is already an active camera for delivered render item. Consider using 'Camera.for' or diable the active camera to avoid some exceptions.",
|
||||
"23": "Render item with id of '$1' has already exists. Please avoid repeat id since it may cause issues when use 'getElementById'.",
|
||||
"24": "Uniform block can only be used in glsl version es 300.",
|
||||
"25": "Cannot activate weather since there's no weather with id of '$1'.",
|
||||
"26": "Cannot set attribute when only element number specified. Use 'pointer' or 'pointerI' instead.",
|
||||
"27": "Cannot vertex attribute integer point when specified as float. Use 'set' or 'pointer' instead.",
|
||||
"28": "Redefinition of shader $1: '$2'",
|
||||
"29": "Cannot define new texture since texture index is larger than max texture count.",
|
||||
"30": "Cannot use indices named $1 since no definition for it. Please define it in advance.",
|
||||
"31": "Cannot use indices since the indices instance is not belong to the program.",
|
||||
"32": "Sub-image exceeds texture dimensions, auto adjusting size.",
|
||||
"33": "Cannot modify MotaOffscreenCanvas2D that is freezed.",
|
||||
"34": "Repeated render tag registration: '$1'.",
|
||||
"35": "Cannot append child on plain render item, please ensure you have overrided 'appendChild' method in your own element.",
|
||||
"36": "Cannot remove child on plain render item, please ensure you have overrided 'removeChild' method in your own element.",
|
||||
"37": "Cannot execute 'requestSort' on plain render item, please ensure you have overrided 'requestSort' method in your own element.",
|
||||
"38": "Using plain text in jsx is strongly not recommended, since you can hardly modify its attributes. Consider using Text element instead.",
|
||||
"39": "Plain text is not supported outside Text element.",
|
||||
"40": "Cannot return canvas that is not provided by this pool.",
|
||||
"41": "Width of text content components must be positive. receive: $1",
|
||||
"42": "Repeated Textbox id: '$1'.",
|
||||
"43": "Cannot set icon of '$1', since it does not exists. Please ensure you have delivered correct icon id or number.",
|
||||
"44": "Unexpected end when loading stream audio, reason: '$1'",
|
||||
"45": "Audio route with id of '$1' has already existed. New route will override old route.",
|
||||
"46": "Cannot pipe new StreamReader object when stream is loading.",
|
||||
"47": "Audio stream decoder for audio type '$1' has already existed.",
|
||||
"48": "Sample rate in stream audio must be constant.",
|
||||
"49": "Repeated patch for '$1', key: '$2'.",
|
||||
"50": "Unknown audio extension name: '$1'",
|
||||
"51": "Cannot decode sound '$1', since audio file may not supported by 2.b.",
|
||||
"52": "Cannot play sound '$1', since there is no added data named it.",
|
||||
"53": "Cannot $1 audio route '$2', since there is not added route named it.",
|
||||
"54": "Missing start tag for '$1', index: $2.",
|
||||
"55": "Unchildable tag '$1' should follow with param.",
|
||||
"56": "Method '$1' has been deprecated. Consider using '$2' instead.",
|
||||
"57": "Repeated UI controller on item '$1', new controller will not work.",
|
||||
"58": "Fail to set ellipse round rect, since length of 'ellipse' property should only be 2, 4, 6 or 8. delivered: $1",
|
||||
"59": "Unknown icon '$1' in parsing text content.",
|
||||
"60": "Repeated Tip id: '$1'.",
|
||||
"61": "Unexpected recursive call of $1.update in render function. Please ensure you have to do this, if you do, ignore this warn.",
|
||||
"62": "Recursive fallback fonts in '$1'.",
|
||||
"63": "Uncaught promise error in waiting box component. Error reason: $1",
|
||||
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency.",
|
||||
"1101": "Cannot add new effect to point effect instance, for there's no more reserve space for it. Please increase the max count of the instance."
|
||||
}
|
||||
}
|
25
src/data/resource.json
Normal file
25
src/data/resource.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"resource": [
|
||||
"zip.resource.zip",
|
||||
"zip.weather.zip",
|
||||
"zip.materials.zip",
|
||||
"materials.keyboard.png",
|
||||
"bgms.tower.opus",
|
||||
"bgms.cave.opus",
|
||||
"bgms.mount.opus",
|
||||
"bgms.towerBoss3.opus",
|
||||
"bgms.winter.opus",
|
||||
"bgms.title.opus",
|
||||
"bgms.road.opus",
|
||||
"bgms.beforeBoss.opus",
|
||||
"bgms.winterTown.opus",
|
||||
"bgms.towerBoss.opus",
|
||||
"bgms.grass.opus",
|
||||
"bgms.plot1.opus",
|
||||
"bgms.escape.opus",
|
||||
"bgms.towerBoss2.opus"
|
||||
],
|
||||
"stereoSE": [
|
||||
""
|
||||
]
|
||||
}
|
62
src/data/settings.json
Normal file
62
src/data/settings.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"screen": {
|
||||
"fullscreen": [
|
||||
"是否全屏进行游戏,全屏后按ESC退出全屏,开启后将不能通过按ESC开启系统设置菜单,",
|
||||
"请按下方的按钮打开。进入或退出全屏后请存读档一下,以解决一部分绘制问题。"
|
||||
],
|
||||
"halo": ["开启后,会在地图上显示范围光环。"],
|
||||
"itemDetail": ["是否在地图上显示宝石血瓶装备等增加的属性值"],
|
||||
"transition": [
|
||||
"是否展示当一个ui界面,如怪物手册等的打开与关闭时的动画。当此项开启时,",
|
||||
"所有界面被打开或关闭时都会展示动画,否则会直接展示出来"
|
||||
],
|
||||
"fontSize": [
|
||||
"在各种 ui 界面中显示的文字大小,范围为 2 - 48。注意,字体过大可能会引起 ui 布局发生错误"
|
||||
],
|
||||
"criticalGem": ["临界是否显示为在当前地图要吃的宝石数"]
|
||||
},
|
||||
"action": {
|
||||
"autoSkill": [
|
||||
"开启后,打怪物的时候会自动选择伤害最低的技能。同时显伤也会显示此状态下的伤害,",
|
||||
"临界也会考虑技能在内"
|
||||
],
|
||||
"fixed": [
|
||||
"开启后,当鼠标移动到怪物上时,会以盒子的形式展示该点的怪物信息。手机端此功能无效。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"注:当鼠标移动到怪物上时,经过200毫秒才会显示信息,防止误操作。"
|
||||
],
|
||||
"hotkey": ["设置游戏中会用到的一些快捷键"],
|
||||
"toolbar": [
|
||||
"允许你在工具栏上自定义按钮,包括使用道具、开关技能、按下某个按键等。",
|
||||
"推荐手机进行一些设置"
|
||||
]
|
||||
},
|
||||
"utils": {
|
||||
"betterLoad": [
|
||||
"<span style=\"color: yellow; font-weight: 700\">试验性功能</span>",
|
||||
"<br>",
|
||||
"开启后游戏将对加载进行优化,缩短进入游戏时的加载时长,而在游戏中对资源进行部分性按需加载,从而对加载进行优化。",
|
||||
"该设置不会影响你的正常游戏,但如果网络环境较差,可能会导致部分楼层转换时间明显变长。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"注:修改后刷新页面起效。"
|
||||
],
|
||||
"autoScale": [
|
||||
"开启后,每次进入游戏时会自动缩放游戏画面至合适值。该项只对电脑端有效。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"缩放原则如下:",
|
||||
"<br>",
|
||||
"1. 首先尝试缩放至最大缩放比例",
|
||||
"<br>",
|
||||
"2. 如果缩放后游戏画面高度高于页面高度的95%,那么缩小一个缩放比例,否则保持最大比例"
|
||||
]
|
||||
},
|
||||
"fx": {
|
||||
"paraLight": [
|
||||
"是否开启野外的平行光阴影,在野外将会显示平行光阴影,模拟太阳光,拥有不错的视觉效果"
|
||||
],
|
||||
"frag": ["开启后,在打败怪物后会触发怪物碎裂特效。"]
|
||||
}
|
||||
}
|
43
src/data/skill.json
Normal file
43
src/data/skill.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"none": {
|
||||
"text": "无",
|
||||
"opened": "true",
|
||||
"desc": [
|
||||
"当前未选择技能"
|
||||
]
|
||||
},
|
||||
"blade": {
|
||||
"text": "1:断灭之刃",
|
||||
"opened": "true",
|
||||
"desc": [
|
||||
"<span style=\"color: gold\">快捷键1</span>,开启后勇士攻击增加${level:2 * 10}%,",
|
||||
"同时防御减少${level:2 * 10}%。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"当前等级:${level:2}"
|
||||
]
|
||||
},
|
||||
"jump": {
|
||||
"text": "2:跳跃",
|
||||
"opened": "true",
|
||||
"desc": [
|
||||
"<span style=\"color: gold\">快捷键2</span>,消耗200点生命值,困难消耗400点,一个地图只能使用3次,",
|
||||
"如果前方为可通行的地面,则不能使用该技能,如果前方为怪物,则将怪物移至勇士视线上第一个不能通行的方块后",
|
||||
"如果前方为障碍物,则直接跳到该障碍物的后方。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"进入第二章后不再消耗生命值。"
|
||||
]
|
||||
},
|
||||
"shield": {
|
||||
"text": "3:铸剑为盾",
|
||||
"opened": "true",
|
||||
"desc": [
|
||||
"<span style=\"color: gold\">快捷键3</span>,开启后勇士防御增加${level:10 * 10}%,",
|
||||
"同时攻击减少${level:10 * 10}%。",
|
||||
"<br>",
|
||||
"<br>",
|
||||
"当前等级:${level:10}"
|
||||
]
|
||||
}
|
||||
}
|
37
src/data/tips.json
Normal file
37
src/data/tips.json
Normal file
@ -0,0 +1,37 @@
|
||||
[
|
||||
"按下C可以查看鼠标位置怪物临界",
|
||||
"按下E可以查看鼠标位置怪物的详细属性",
|
||||
"将鼠标移动到光环怪上以查看其产生的光环",
|
||||
"字体太大?试试在背包的系统设置里面调整字体大小吧!",
|
||||
"字体太小?试试在背包的系统设置里面调整字体大小吧!",
|
||||
"按键不合心意?试试在背包的系统设置里面自定义快捷键",
|
||||
"拖动状态栏左上角可以移动状态栏哦!",
|
||||
"拖动状态栏右下角可以缩放状态栏哦!",
|
||||
"按下M键,鼠标位置的怪物的信息就会被你看光啦!",
|
||||
"咱就是说,要不要试一下工具栏的最后一个按钮?",
|
||||
"要不要试试工具栏倒数第二个按钮呢?",
|
||||
"想自定义工具栏?去背包的系统设置看看吧!",
|
||||
"冷知识:临界界面可以拖动滚动条来查看减伤情况",
|
||||
"可以用滚轮或者双指缩放小地图!",
|
||||
"楼传的最左侧一栏可以选择区域!",
|
||||
"冷知识:装备栏左栏最上面可以修改装备排序",
|
||||
"冷冷冷知识:装备栏左栏最上面右侧可以更改顺序或倒序",
|
||||
"第一章使用跳跃技能可是要扣血的!要注意!",
|
||||
"按H查看本游戏的百科全书",
|
||||
"给别人炫耀一下自己的成就点吧!虽然不能记榜(",
|
||||
"抱团属性会在怪物右上角显示加成数量!",
|
||||
"乾坤挪移属性会在怪物左上角显示“乾”字!",
|
||||
"电脑端可以试试按F11全屏游玩!",
|
||||
"手机端要不试试横屏玩?",
|
||||
"不在楼梯边也可以使用楼传!",
|
||||
"技能树的右下角可以切换章节!",
|
||||
"开启自动切换技能就会自动帮你选择最优技能了!",
|
||||
"魔塔不仅有撤回,还有恢复,按W或6就可以了!",
|
||||
"觉得卡顿?可以去试着设置里面关闭一些特性!",
|
||||
"从第二章开始,怪物负伤害量不会超过其生命的1/4",
|
||||
"生命回复不会超过防御的十分之一",
|
||||
"不想看小贴士?设置里面可以关掉!",
|
||||
"不小心进入了追猎范围?读取自动存档撤回到进入前吧!",
|
||||
"不小心进入了电摇嘲讽范围?读取自动存档撤回到进入前吧!",
|
||||
"小地图出现卡顿?试试在背包中系统设置里把小地图懒更新打开吧!"
|
||||
]
|
@ -1,6 +1,7 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { cloneDeep, isNil } from 'lodash-es';
|
||||
import { ItemState } from './item';
|
||||
import { HeroSkill, NightSpecial } from '../mechanism/misc';
|
||||
|
||||
/**
|
||||
@ -342,3 +343,122 @@ 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;
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import type { Hero } from './hero';
|
||||
import { GameState, gameStates } from './state';
|
||||
import { loading } from '../game';
|
||||
|
||||
type EffectFn = () => void;
|
||||
type CanUseEffectFn = () => boolean;
|
||||
type EffectFn = (state: GameState, hero: Hero<any>) => void;
|
||||
type CanUseEffectFn = (state: GameState, hero: Hero<any>) => boolean;
|
||||
|
||||
interface ItemStateEvent {
|
||||
use: [];
|
||||
use: [hero: Hero<any>];
|
||||
}
|
||||
|
||||
export class ItemState<
|
||||
@ -54,6 +56,7 @@ export class ItemState<
|
||||
this.canUseItemEffect = item.canUseItemEffect;
|
||||
|
||||
this.compileFunction();
|
||||
this.compileEvent();
|
||||
}
|
||||
|
||||
private compileFunction() {
|
||||
@ -82,6 +85,48 @@ 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
|
||||
|
@ -11,7 +11,7 @@ import type {
|
||||
LayerMovingRenderable,
|
||||
LayerFloorBinder
|
||||
} from '@motajs/render';
|
||||
import type { HeroKeyMover } from '@/module/action/move';
|
||||
import type { HeroKeyMover } from '@/core/main/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,10 +661,7 @@ 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);
|
||||
@ -788,7 +785,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 } = core.utils.scan[direction];
|
||||
const { x: dx, y: dy } = core.utils.scan[direction];
|
||||
const { x: tdx } = core.utils.scan[toDir];
|
||||
|
||||
const promises = [...list].map(v => {
|
||||
|
127
src/game/state/state.ts
Normal file
127
src/game/state/state.ts
Normal file
@ -0,0 +1,127 @@
|
||||
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,95 +1,516 @@
|
||||
import type * as Client from '@motajs/client';
|
||||
import type * as ClientBase from '@motajs/client-base';
|
||||
import type * as Common from '@motajs/common';
|
||||
import type * as LegacyClient from '@motajs/legacy-client';
|
||||
import type * as LegacyCommon from '@motajs/legacy-common';
|
||||
import type * as LegacySystem from '@motajs/legacy-system';
|
||||
import type * as LegacyUI from '@motajs/legacy-ui';
|
||||
import type * as Render from '@motajs/render';
|
||||
import type * as RenderCore from '@motajs/render-core';
|
||||
import type * as RenderElements from '@motajs/render-elements';
|
||||
import type * as RenderStyle from '@motajs/render-style';
|
||||
import type * as RenderVue from '@motajs/render-vue';
|
||||
import type * as System from '@motajs/system';
|
||||
import type * as SystemAction from '@motajs/system-action';
|
||||
import type * as SystemUI from '@motajs/system-ui';
|
||||
import type {
|
||||
Disposable,
|
||||
EventEmitter,
|
||||
IndexedEventEmitter
|
||||
} from '@motajs/legacy-common';
|
||||
import type { loading } from './game';
|
||||
import type { Hotkey } from '@/core/main/custom/hotkey';
|
||||
import type { Keyboard } from '@/core/main/custom/keyboard';
|
||||
import type { CustomToolbar } from '@/core/main/custom/toolbar';
|
||||
import type { Focus, GameUi, UiController } from '@/core/main/custom/ui';
|
||||
import type { gameListener, hook } from './game';
|
||||
import type { MotaSetting, SettingDisplayer } from '@/core/main/setting';
|
||||
import type { GameStorage } from '@/core/main/storage';
|
||||
import type { DamageEnemy, EnemyCollection } from './enemy/damage';
|
||||
import type { specials } from './enemy/special';
|
||||
import type { KeyCode } from '@motajs/client-base';
|
||||
import type { Ref } from 'vue';
|
||||
import type * as battle from './enemy/battle';
|
||||
import type * as hero from './state/hero';
|
||||
import type * as damage from './enemy/damage';
|
||||
import type { Logger } from '@motajs/common';
|
||||
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 { BlockMover, HeroMover, ObjectMoverBase } from './state/move';
|
||||
import type * as Animation from 'mutate-animate';
|
||||
import type { WeatherController } from '@/module/weather/weather';
|
||||
|
||||
interface ClassInterface {
|
||||
// 渲染进程与游戏进程通用
|
||||
EventEmitter: typeof EventEmitter;
|
||||
IndexedEventEmitter: typeof IndexedEventEmitter;
|
||||
Disposable: typeof Disposable;
|
||||
// 定义于渲染进程
|
||||
GameStorage: typeof GameStorage;
|
||||
MotaSetting: typeof MotaSetting;
|
||||
SettingDisplayer: typeof SettingDisplayer;
|
||||
Focus: typeof Focus;
|
||||
GameUi: typeof GameUi;
|
||||
UiController: typeof UiController;
|
||||
Hotkey: typeof Hotkey;
|
||||
Keyboard: typeof Keyboard;
|
||||
CustomToolbar: typeof CustomToolbar;
|
||||
Danmaku: typeof Danmaku;
|
||||
// todo: 放到插件 ShaderEffect: typeof ShaderEffect;
|
||||
// 定义于游戏进程,渲染进程依然可用
|
||||
EnemyCollection: typeof EnemyCollection;
|
||||
DamageEnemy: typeof DamageEnemy;
|
||||
}
|
||||
|
||||
type _IBattle = typeof battle;
|
||||
type _IHero = typeof hero;
|
||||
type _IDamage = typeof damage;
|
||||
|
||||
interface FunctionInterface extends _IBattle, _IHero, _IDamage {
|
||||
// 定义于渲染进程,录像中会进行polyfill,但是不执行任何内容
|
||||
readyAllResource(): void;
|
||||
// 定义于游戏进程,渲染进程依然可用
|
||||
|
||||
// todo
|
||||
}
|
||||
|
||||
interface VariableInterface {
|
||||
// 定义于渲染进程,录像中会进行polyfill
|
||||
loading: typeof loading;
|
||||
hook: typeof hook;
|
||||
gameListener: typeof gameListener;
|
||||
mainSetting: MotaSetting;
|
||||
gameKey: Hotkey;
|
||||
mainUi: UiController;
|
||||
fixedUi: UiController;
|
||||
KeyCode: typeof KeyCode;
|
||||
// isMobile: boolean;
|
||||
settingStorage: GameStorage;
|
||||
status: Ref<boolean>;
|
||||
// 定义于游戏进程,渲染进程依然可用
|
||||
haloSpecials: number[];
|
||||
enemySpecials: typeof specials;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
interface ModuleInterface {
|
||||
'@motajs/client': typeof Client;
|
||||
'@motajs/client-base': typeof ClientBase;
|
||||
'@motajs/common': typeof Common;
|
||||
'@motajs/legacy-client': typeof LegacyClient;
|
||||
'@motajs/legacy-common': typeof LegacyCommon;
|
||||
'@motajs/legacy-system': typeof LegacySystem;
|
||||
'@motajs/legacy-ui': typeof LegacyUI;
|
||||
'@motajs/render': typeof Render;
|
||||
'@motajs/render-core': typeof RenderCore;
|
||||
'@motajs/render-elements': typeof RenderElements;
|
||||
'@motajs/render-style': typeof RenderStyle;
|
||||
'@motajs/render-vue': typeof RenderVue;
|
||||
'@motajs/system': typeof System;
|
||||
'@motajs/system-action': typeof SystemAction;
|
||||
'@motajs/system-ui': typeof SystemUI;
|
||||
Mechanism: {
|
||||
BluePalace: typeof misc.BluePalace;
|
||||
NightSpecial: typeof misc.NightSpecial;
|
||||
MiscData: typeof misc.MiscData;
|
||||
HeroSkill: typeof misc.HeroSkill;
|
||||
};
|
||||
Effect: {};
|
||||
Render: typeof Render;
|
||||
State: {
|
||||
ItemState: typeof ItemState;
|
||||
HeroMover: typeof HeroMover;
|
||||
BlockMover: typeof BlockMover;
|
||||
ObjectMoverBase: typeof ObjectMoverBase;
|
||||
heroMoveCollection: {
|
||||
mover: HeroMover;
|
||||
keyMover?: HeroKeyMover;
|
||||
};
|
||||
};
|
||||
Action: {
|
||||
HeroKeyMover: typeof HeroKeyMover;
|
||||
};
|
||||
Animation: typeof Animation;
|
||||
Weather: {
|
||||
controller: WeatherController;
|
||||
WeatherController: typeof WeatherController;
|
||||
};
|
||||
}
|
||||
|
||||
interface SystemInterfaceMap {
|
||||
class: ClassInterface;
|
||||
fn: FunctionInterface;
|
||||
var: VariableInterface;
|
||||
module: ModuleInterface;
|
||||
}
|
||||
|
||||
type InterfaceType = keyof SystemInterfaceMap;
|
||||
|
||||
interface PluginInterface {
|
||||
// 渲染进程定义的插件
|
||||
chase_r: typeof import('../plugin/chase');
|
||||
gameCanvas_r: typeof import('../plugin/fx/gameCanvas');
|
||||
frag_r: typeof import('../plugin/fx/frag');
|
||||
boss_r: typeof import('../plugin/boss');
|
||||
// 游戏进程定义的插件
|
||||
utils_g: typeof import('../plugin/game/utils');
|
||||
shop_g: typeof import('../plugin/game/shop');
|
||||
replay_g: typeof import('../plugin/game/replay');
|
||||
skillTree_g: typeof import('../plugin/game/skillTree');
|
||||
removeMap_g: typeof import('../plugin/game/removeMap');
|
||||
remainEnemy_g: typeof import('../plugin/game/enemy/remainEnemy');
|
||||
chase_g: typeof import('../plugin/game/chase');
|
||||
skill_g: typeof import('../plugin/game/skill');
|
||||
itemDetail_g: typeof import('../plugin/game/fx/itemDetail');
|
||||
checkBlock_g: typeof import('../plugin/game/enemy/checkblock');
|
||||
}
|
||||
|
||||
interface PackageInterface {
|
||||
axios: typeof import('axios');
|
||||
// 'chart.js': typeof import('chart.js');
|
||||
jszip: typeof import('jszip');
|
||||
lodash: typeof import('lodash-es');
|
||||
'lz-string': typeof import('lz-string');
|
||||
'mutate-animate': typeof import('mutate-animate');
|
||||
// vue: typeof import('vue');
|
||||
}
|
||||
|
||||
export interface IMota {
|
||||
rewrite: typeof rewrite;
|
||||
r: typeof r;
|
||||
rf: typeof rf;
|
||||
|
||||
/** 样板插件接口 */
|
||||
Plugin: IPlugin;
|
||||
/**
|
||||
* 样板使用的第三方库接口,可以直接获取到库的原有接口。
|
||||
* 接口在渲染进程中引入,在游戏进程中不会polyfill,因此在游戏进程中使用时,
|
||||
* 应先使用main.replayChecking进行检查,保证该值不存在时才进行使用,否则会引起录像出错
|
||||
*/
|
||||
// Package: IPackage;
|
||||
|
||||
/**
|
||||
* 获取一个样板接口
|
||||
* @param type 要获取的接口类型
|
||||
* @param key 接口名称
|
||||
*/
|
||||
require<K extends keyof ModuleInterface>(key: K): ModuleInterface[K];
|
||||
require<T extends InterfaceType, K extends keyof SystemInterfaceMap[T]>(
|
||||
type: T,
|
||||
key: K
|
||||
): SystemInterfaceMap[T][K];
|
||||
/**
|
||||
* 获取一个样板接口
|
||||
* @param type 要获取的接口类型
|
||||
* @param key 接口名称
|
||||
*/
|
||||
require<T = unknown>(key: string): T;
|
||||
require(type: InterfaceType, key: string): any;
|
||||
|
||||
/**
|
||||
* 获取一种接口的所有内容
|
||||
* @param type 要获取的接口类型
|
||||
*/
|
||||
requireAll<T extends InterfaceType>(type: T): SystemInterfaceMap[T];
|
||||
|
||||
/**
|
||||
* 注册一个样板接口
|
||||
* @param type 要注册的接口类型
|
||||
* @param key 接口名称
|
||||
* @param data 接口内容
|
||||
*/
|
||||
register<K extends keyof ModuleInterface>(
|
||||
register<T extends InterfaceType, K extends keyof SystemInterfaceMap[T]>(
|
||||
type: T,
|
||||
key: K,
|
||||
data: ModuleInterface[K]
|
||||
data: SystemInterfaceMap[T][K]
|
||||
): void;
|
||||
/**
|
||||
* 注册一个样板接口
|
||||
* @param type 要注册的接口类型
|
||||
* @param key 接口名称
|
||||
* @param data 接口内容
|
||||
*/
|
||||
register(key: string, data: unknown): void;
|
||||
register(type: InterfaceType, key: string, data: any): void;
|
||||
}
|
||||
|
||||
export interface IPlugin {
|
||||
inited: boolean;
|
||||
|
||||
/**
|
||||
* 初始化所有插件
|
||||
*/
|
||||
init(): void;
|
||||
|
||||
/**
|
||||
* 获取到一个插件的内容
|
||||
* @param plugin 要获取的插件
|
||||
*/
|
||||
require<K extends keyof PluginInterface>(plugin: K): PluginInterface[K];
|
||||
/**
|
||||
* 获取到一个插件的内容
|
||||
* @param plugin 要获取的插件
|
||||
*/
|
||||
require(plugin: string): any;
|
||||
|
||||
/**
|
||||
* 获取所有插件
|
||||
*/
|
||||
requireAll(): PluginInterface & { [x: string]: any };
|
||||
|
||||
/**
|
||||
* 注册一个插件
|
||||
* @param plugin 要注册的插件名
|
||||
* @param data 插件内容
|
||||
* @param init 插件的初始化函数,可选,初始化函数接受两个参数,分别是plugin和data,表示插件名称和内容
|
||||
*/
|
||||
register<K extends keyof PluginInterface>(
|
||||
plugin: K,
|
||||
data: PluginInterface[K],
|
||||
init?: (plugin: K, data: PluginInterface[K]) => void
|
||||
): void;
|
||||
/**
|
||||
* 注册一个插件
|
||||
* @param plugin 要注册的插件名
|
||||
* @param init 插件的初始化函数,初始化函数接受一个参数,表示插件名称,要求返回插件内容
|
||||
*/
|
||||
register<K extends keyof PluginInterface>(
|
||||
plugin: K,
|
||||
init: (plugin: K) => PluginInterface[K]
|
||||
): void;
|
||||
/**
|
||||
* 注册一个插件
|
||||
* @param plugin 要注册的插件名
|
||||
* @param data 插件内容
|
||||
* @param init 插件的初始化函数,可选,初始化函数接受两个参数,分别是plugin和data,表示插件名称和内容
|
||||
*/
|
||||
register<K extends string, D>(
|
||||
plugin: K,
|
||||
data: D,
|
||||
init?: (plugin: K, data: D) => void
|
||||
): void;
|
||||
/**
|
||||
* 注册一个插件
|
||||
* @param plugin 要注册的插件名
|
||||
* @param init 插件的初始化函数,初始化函数接受一个参数,表示插件名称,要求返回插件内容
|
||||
*/
|
||||
register<K extends string>(plugin: K, init: (plugin: K) => any): void;
|
||||
}
|
||||
|
||||
export interface IPackage {
|
||||
/**
|
||||
* 获取样板使用的第三方库
|
||||
* @param name 要获取的第三方库
|
||||
*/
|
||||
require<K extends keyof PackageInterface>(name: K): PackageInterface[K];
|
||||
|
||||
/**
|
||||
* 获取样板使用的所有第三方库
|
||||
*/
|
||||
requireAll(): PackageInterface;
|
||||
|
||||
register<K extends keyof PackageInterface>(
|
||||
name: K,
|
||||
data: PackageInterface[K]
|
||||
): void;
|
||||
}
|
||||
|
||||
interface IPluginData {
|
||||
/** 插件类型,content表示直接注册了内容,function表示注册了初始化函数,内容从其返回值获取 */
|
||||
type: 'content' | 'function';
|
||||
data: any;
|
||||
init?: (plugin: string, data?: any) => any;
|
||||
}
|
||||
|
||||
class MPlugin {
|
||||
private static plugins: Record<string, IPluginData> = {};
|
||||
private static pluginData: Record<string, any> = {};
|
||||
static inited = false;
|
||||
|
||||
constructor() {
|
||||
throw new Error(`System plugin class cannot be constructed.`);
|
||||
}
|
||||
|
||||
static init() {
|
||||
for (const [key, data] of Object.entries(this.plugins)) {
|
||||
if (data.type === 'content') {
|
||||
data.init?.(key, data.data);
|
||||
} else {
|
||||
data.data = data.init!(key);
|
||||
}
|
||||
this.pluginData[key] = data.data;
|
||||
}
|
||||
this.inited = true;
|
||||
}
|
||||
|
||||
static require(key: string) {
|
||||
if (!this.inited) {
|
||||
throw new Error(`Cannot access plugin '${key}' before initialize.`);
|
||||
}
|
||||
if (!(key in this.plugins)) {
|
||||
throw new Error(`Cannot resolve plugin require: key='${key}'`);
|
||||
}
|
||||
return this.plugins[key].data;
|
||||
}
|
||||
|
||||
static requireAll(): PluginInterface {
|
||||
return this.pluginData as PluginInterface;
|
||||
}
|
||||
|
||||
static register(key: string, data: any, init?: any) {
|
||||
if (typeof data === 'function') {
|
||||
this.plugins[key] = {
|
||||
type: 'function',
|
||||
init: data,
|
||||
data: void 0
|
||||
};
|
||||
} else {
|
||||
this.plugins[key] = {
|
||||
type: 'content',
|
||||
data,
|
||||
init
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MPackage {
|
||||
// @ts-ignore
|
||||
private static packages: PackageInterface = {};
|
||||
|
||||
constructor() {
|
||||
throw new Error(`System package class cannot be constructed.`);
|
||||
}
|
||||
|
||||
static require<K extends keyof PackageInterface>(
|
||||
name: K
|
||||
): PackageInterface[K] {
|
||||
return this.packages[name];
|
||||
}
|
||||
|
||||
static requireAll() {
|
||||
return this.packages;
|
||||
}
|
||||
|
||||
static register<K extends keyof PackageInterface>(
|
||||
name: K,
|
||||
data: PackageInterface[K]
|
||||
) {
|
||||
this.packages[name] = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 样板接口系统,通过 Mota 获取到样板的核心功能
|
||||
* 样板接口系统,通过 Mota 获取到样板的核心功能,不可实例化
|
||||
*/
|
||||
class Mota implements IMota {
|
||||
private modules: Record<string, any> = {};
|
||||
class Mota {
|
||||
private static classes: Record<string, any> = {};
|
||||
private static functions: Record<string, any> = {};
|
||||
private static variables: Record<string, any> = {};
|
||||
private static modules: Record<string, any> = {};
|
||||
|
||||
r = r;
|
||||
rf = rf;
|
||||
static rewrite = rewrite;
|
||||
static r = r;
|
||||
static rf = rf;
|
||||
static Plugin = MPlugin;
|
||||
// static Package = MPackage;
|
||||
|
||||
constructor() {
|
||||
throw new Error(`System interface class cannot be constructed.`);
|
||||
}
|
||||
|
||||
require(key: string): any {
|
||||
const data = this.modules[key];
|
||||
static require(type: InterfaceType, key: string): any {
|
||||
const data = this.getByType(type)[key];
|
||||
if (data) return data;
|
||||
else {
|
||||
throw new Error(`Cannot resolve module '${key}'`);
|
||||
throw new Error(
|
||||
`Cannot resolve require: type='${type}',key='${key}'`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
register(key: string, data: any) {
|
||||
if (key in this.modules) {
|
||||
console.warn(`模块注册重复: '${key}',已将其覆盖`);
|
||||
static requireAll<T extends InterfaceType>(type: T): SystemInterfaceMap[T] {
|
||||
return this.getByType(type) as SystemInterfaceMap[T];
|
||||
}
|
||||
|
||||
static register(type: InterfaceType, key: string, data: any) {
|
||||
const obj = this.getByType(type);
|
||||
if (key in obj) {
|
||||
console.warn(
|
||||
`重复的样板接口注册: type='${type}', key='${key}',已将其覆盖`
|
||||
);
|
||||
}
|
||||
this.modules[key] = data;
|
||||
obj[key] = data;
|
||||
}
|
||||
|
||||
private static getByType(type: InterfaceType) {
|
||||
return type === 'class'
|
||||
? this.classes
|
||||
: type === 'fn'
|
||||
? this.functions
|
||||
: type === 'var'
|
||||
? this.variables
|
||||
: this.modules;
|
||||
}
|
||||
}
|
||||
|
||||
type RewriteType = 'full' | 'front' | 'add';
|
||||
type _F<F> = F extends (...params: infer P) => infer R ? [P, R] : never;
|
||||
type _Func = (...params: any) => any;
|
||||
|
||||
/**
|
||||
* 全量复写或在函数前添加内容
|
||||
* @param base 函数所在对象
|
||||
* @param key 函数名称,即函数在base中叫什么
|
||||
* @param type 复写类型,full表示全量复写,front表示在原函数之前添加内容
|
||||
* @param re 复写函数,类型为full时表示将原函数完全覆盖,为front时表示将该函数添加到原函数之前
|
||||
* @param bind 原函数的调用对象,默认为base
|
||||
* @param rebind 复写函数的调用对象,默认为base
|
||||
*/
|
||||
function rewrite<
|
||||
O,
|
||||
K extends SelectKey<O, _Func>,
|
||||
R extends 'full' | 'front',
|
||||
T = O
|
||||
>(
|
||||
base: O,
|
||||
key: K,
|
||||
type: R,
|
||||
re: (
|
||||
this: T,
|
||||
...params: [..._F<O[K]>[0], ...any[]]
|
||||
) => R extends 'full' ? _F<O[K]>[1] : void,
|
||||
bind?: any,
|
||||
rebind?: T
|
||||
): (this: T, ...params: [..._F<O[K]>[0], ...any[]]) => _F<O[K]>[1];
|
||||
/**
|
||||
* 在函数后追加内容
|
||||
* @param base 函数所在对象
|
||||
* @param key 函数名称,即函数在base中叫什么
|
||||
* @param type 复写类型,add表示在函数后追加
|
||||
* @param re 复写函数,类型为add时表示在原函数后面追加复写函数,会在第一个参数中传入原函数的返回值,
|
||||
* 并要求复写函数必须有返回值,作为复写的最终返回值。
|
||||
* @param bind 原函数的调用对象,默认为`base`
|
||||
* @param rebind 复写函数的调用对象,默认为`base`
|
||||
*/
|
||||
function rewrite<O, K extends SelectKey<O, _Func>, T = O>(
|
||||
base: O,
|
||||
key: K,
|
||||
type: 'add',
|
||||
re: (
|
||||
this: T,
|
||||
...params: [_F<O[K]>[1], ..._F<O[K]>[0], ...any[]]
|
||||
) => _F<O[K]>[1],
|
||||
bind?: any,
|
||||
rebind?: T
|
||||
): (this: T, ...params: [..._F<O[K]>[0], ...any[]]) => _F<O[K]>[1];
|
||||
function rewrite<O, K extends SelectKey<O, _Func>, T = O>(
|
||||
base: O,
|
||||
key: K,
|
||||
type: RewriteType,
|
||||
re: (this: T, ...params: [..._F<O[K]>[0], ...any[]]) => _F<O[K]>[1],
|
||||
bind?: any,
|
||||
rebind?: T
|
||||
): (this: T, ...params: [..._F<O[K]>[0], ...any[]]) => _F<O[K]>[1] {
|
||||
const func = base[key];
|
||||
if (typeof func !== 'function') {
|
||||
throw new Error(
|
||||
`Cannot rewrite variable with type of '${typeof func}'.`
|
||||
);
|
||||
}
|
||||
if (type === 'full') {
|
||||
// @ts-ignore
|
||||
return (base[key] = re.bind(rebind ?? base));
|
||||
} else if (type === 'add') {
|
||||
const origin = base[key];
|
||||
function res(this: T, ...params: [..._F<O[K]>[0], ...any[]]) {
|
||||
const v = (origin as _Func).call(bind ?? base, ...params);
|
||||
// @ts-ignore
|
||||
const ret = re.call(rebind ?? base, v, ...params);
|
||||
return ret;
|
||||
}
|
||||
// @ts-ignore
|
||||
return (base[key] = res);
|
||||
} else {
|
||||
const origin = base[key];
|
||||
function res(this: T, ...params: [..._F<O[K]>[0], ...any[]]) {
|
||||
// @ts-ignore
|
||||
re.call(rebind ?? base, ...params);
|
||||
const ret = (origin as _Func).call(bind ?? base, ...params);
|
||||
return ret;
|
||||
}
|
||||
// @ts-ignore
|
||||
return (base[key] = res);
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,8 +526,6 @@ function r<T = undefined>(fn: (this: T) => void, thisArg?: T) {
|
||||
if (!main.replayChecking && main.mode === 'play') fn.call(thisArg as T);
|
||||
}
|
||||
|
||||
const empty = () => {};
|
||||
|
||||
/**
|
||||
* 将一个函数包裹成渲染进程函数,执行这个函数时将直接在渲染进程下执行。该函数与 {@link r} 函数的关系,
|
||||
* 与`call`和`bind`的关系类似。
|
||||
@ -122,8 +541,8 @@ function rf<F extends (...params: any) => any, T>(
|
||||
fn: F,
|
||||
thisArg?: T
|
||||
): (this: T, ...params: Parameters<F>) => ReturnType<F> | undefined {
|
||||
// @ts-expect-error 录像验证的时候不能执行任何操作,因此返回空函数
|
||||
if (main.replayChecking || main.mode === 'editor') return empty;
|
||||
// @ts-ignore
|
||||
if (main.replayChecking || main.mode === 'editor') return () => {};
|
||||
else {
|
||||
return (...params) => {
|
||||
return fn.call(thisArg, ...params);
|
||||
@ -137,4 +556,4 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
window.Mota = new Mota();
|
||||
window.Mota = Mota;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { createApp } from 'vue';
|
||||
import './game/index';
|
||||
import './core/index';
|
||||
import App from './App.vue';
|
||||
import './styles.less';
|
||||
import 'ant-design-vue/dist/antd.dark.css';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Patch, PatchClass } from '@motajs/legacy-common';
|
||||
import { audioPlayer, bgmController, soundPlayer } from '../audio';
|
||||
import { mainSetting } from '@motajs/legacy-ui';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
|
@ -1,6 +1,18 @@
|
||||
import { soundPlayer } from './audio';
|
||||
import { patchAll } from './fallback';
|
||||
import { create } from './render';
|
||||
import { RainWeather } from './weather/rain';
|
||||
import { WeatherController } from './weather/weather';
|
||||
|
||||
patchAll();
|
||||
Mota.register('module', 'Weather', {
|
||||
WeatherController,
|
||||
RainWeather
|
||||
});
|
||||
Mota.register('module', 'Audio', {
|
||||
soundPlayer
|
||||
});
|
||||
Mota.require('var', 'loading').once('coreInit', create);
|
||||
|
||||
export * from './weather';
|
||||
export * from './audio';
|
||||
|
@ -10,9 +10,9 @@ import {
|
||||
SetupComponentOptions,
|
||||
waitbox
|
||||
} from '../components';
|
||||
import { mainUi } from '@motajs/legacy-ui';
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { generateKeyboardEvent } from '@motajs/system-action';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { generateKeyboardEvent } from '@/core/main/custom/keyboard';
|
||||
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 '@motajs/system-action';
|
||||
import { generateKeyboardEvent } from '@motajs/system-action';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { generateKeyboardEvent } from '@/core/main/custom/keyboard';
|
||||
import { transitioned } from '../use';
|
||||
import { linear } from 'mutate-animate';
|
||||
import { KeyCode } from '@motajs/client-base';
|
||||
|
@ -5,17 +5,11 @@
|
||||
"@motajs/client-base": "workspace:*",
|
||||
"@motajs/common": "workspace:*",
|
||||
"@motajs/render": "workspace:*",
|
||||
"@motajs/render-core": "workspace:*",
|
||||
"@motajs/render-elements": "workspace:*",
|
||||
"@motajs/render-style": "workspace:*",
|
||||
"@motajs/render-vue": "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-system": "workspace:*"
|
||||
"@motajs/legacy-ui": "workspace:*"
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { MotaOffscreenCanvas2D } from '@motajs/render';
|
||||
import { mainSetting } from '@motajs/legacy-ui';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import {
|
||||
LayerGroupFloorBinder,
|
||||
ILayerGroupRenderExtends,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { mainSetting } from '@motajs/legacy-ui';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import {
|
||||
Damage,
|
||||
DamageRenderable,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { MotaOffscreenCanvas2D } from '@motajs/render';
|
||||
import { mainSetting, MotaSettingItem } from '@motajs/legacy-ui';
|
||||
import { mainSetting, MotaSettingItem } from '@/core/main/setting';
|
||||
import {
|
||||
LayerGroupFloorBinder,
|
||||
ILayerGroupRenderExtends,
|
||||
|
63
types/source/data.d.ts → src/source/data.d.ts
vendored
63
types/source/data.d.ts → src/source/data.d.ts
vendored
@ -1,3 +1,4 @@
|
||||
|
||||
type FloorIds =
|
||||
| 'empty'
|
||||
| 'MT0'
|
||||
@ -105,7 +106,7 @@ type FloorIds =
|
||||
| 'MT94'
|
||||
| 'MT95'
|
||||
| 'MT96'
|
||||
| 'MT97';
|
||||
| 'MT97'
|
||||
|
||||
type ImageIds =
|
||||
| 'IQ.png'
|
||||
@ -139,7 +140,7 @@ type ImageIds =
|
||||
| 'tower7.webp'
|
||||
| 'winskin.png'
|
||||
| 'winskin2.png'
|
||||
| 'winskin3.png';
|
||||
| 'winskin3.png'
|
||||
|
||||
type AnimationIds =
|
||||
| 'amazed'
|
||||
@ -163,7 +164,7 @@ type AnimationIds =
|
||||
| 'sweat'
|
||||
| 'sweat2'
|
||||
| 'sword'
|
||||
| 'zone';
|
||||
| 'zone'
|
||||
|
||||
type SoundIds =
|
||||
| '008-System08.opus'
|
||||
@ -205,7 +206,7 @@ type SoundIds =
|
||||
| 'shop.opus'
|
||||
| 'thunder.opus'
|
||||
| 'tree.opus'
|
||||
| 'zone.opus';
|
||||
| 'zone.opus'
|
||||
|
||||
type BgmIds =
|
||||
| 'beforeBoss.opus'
|
||||
@ -228,33 +229,35 @@ type BgmIds =
|
||||
| 'towerBoss2.opus'
|
||||
| 'towerBoss3.opus'
|
||||
| 'winter.opus'
|
||||
| 'winterTown.opus';
|
||||
| 'winterTown.opus'
|
||||
|
||||
type FontIds = 'normal' | 'FiraCode';
|
||||
type FontIds =
|
||||
| 'normal'
|
||||
| 'FiraCode'
|
||||
|
||||
interface NameMap {
|
||||
确定: 'confirm.opus';
|
||||
取消: 'cancel.opus';
|
||||
操作失败: 'error.opus';
|
||||
光标移动: 'cursor.opus';
|
||||
打开界面: 'open_ui.opus';
|
||||
读档: 'load.opus';
|
||||
存档: 'save.opus';
|
||||
获得道具: 'item.opus';
|
||||
回血: 'recovery.opus';
|
||||
炸弹: 'bomb.opus';
|
||||
飞行器: 'centerFly.opus';
|
||||
开关门: 'door.opus';
|
||||
上下楼: 'floor.opus';
|
||||
跳跃: 'jump.opus';
|
||||
破墙镐: 'pickaxe.opus';
|
||||
破冰镐: 'icePickaxe.opus';
|
||||
宝石: 'gem.opus';
|
||||
阻激夹域: 'zone.opus';
|
||||
穿脱装备: 'equip.opus';
|
||||
背景音乐: 'bgm.opus';
|
||||
攻击: 'attack.opus';
|
||||
背景图: 'bg.jpg';
|
||||
商店: 'shop.opus';
|
||||
领域: 'zone';
|
||||
'确定': 'confirm.opus';
|
||||
'取消': 'cancel.opus';
|
||||
'操作失败': 'error.opus';
|
||||
'光标移动': 'cursor.opus';
|
||||
'打开界面': 'open_ui.opus';
|
||||
'读档': 'load.opus';
|
||||
'存档': 'save.opus';
|
||||
'获得道具': 'item.opus';
|
||||
'回血': 'recovery.opus';
|
||||
'炸弹': 'bomb.opus';
|
||||
'飞行器': 'centerFly.opus';
|
||||
'开关门': 'door.opus';
|
||||
'上下楼': 'floor.opus';
|
||||
'跳跃': 'jump.opus';
|
||||
'破墙镐': 'pickaxe.opus';
|
||||
'破冰镐': 'icePickaxe.opus';
|
||||
'宝石': 'gem.opus';
|
||||
'阻激夹域': 'zone.opus';
|
||||
'穿脱装备': 'equip.opus';
|
||||
'背景音乐': 'bgm.opus';
|
||||
'攻击': 'attack.opus';
|
||||
'背景图': 'bg.jpg';
|
||||
'商店': 'shop.opus';
|
||||
'领域': 'zone';
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user