feat: 跟进 2.A 的更改

This commit is contained in:
unanmed 2024-04-20 12:27:38 +08:00
parent 0dfb7e4b99
commit 86e6e76286
38 changed files with 851 additions and 493 deletions

View File

@ -8,4 +8,5 @@ public/project/maps.js
public/_server/**/*.js
script/**/*.js
public/editor.html
keyCodes.ts
keyCodes.ts
src/core/main/setting.ts

2
components.d.ts vendored
View File

@ -10,7 +10,6 @@ declare module '@vue/runtime-core' {
AButton: typeof import('ant-design-vue/es')['Button']
ADivider: typeof import('ant-design-vue/es')['Divider']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AProgress: typeof import('ant-design-vue/es')['Progress']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
@ -18,7 +17,6 @@ declare module '@vue/runtime-core' {
ASwitch: typeof import('ant-design-vue/es')['Switch']
Box: typeof import('./src/components/box.vue')['default']
BoxAnimate: typeof import('./src/components/boxAnimate.vue')['default']
Changable: typeof import('./src/components/changable.vue')['default']
Colomn: typeof import('./src/components/colomn.vue')['default']
EnemyOne: typeof import('./src/components/enemyOne.vue')['default']
Scroll: typeof import('./src/components/scroll.vue')['default']

View File

@ -602,6 +602,7 @@ core.prototype._afterLoadResources = function (callback) {
// if (core.plugin._afterLoadResources) core.plugin._afterLoadResources();
core.showStartAnimate();
Mota.require('var', 'hook').emit('load');
if (callback) callback();
};

View File

@ -4421,24 +4421,31 @@ events.prototype._checkLvUp_check = function () {
};
////// 尝试使用道具 //////
events.prototype.tryUseItem = function (itemId) {
events.prototype.tryUseItem = function (itemId, noRoute, callback) {
if (itemId == 'book') {
core.ui.closePanel();
return core.openBook(false);
core.openBook(false);
callback();
return;
}
if (itemId == 'fly') {
core.ui.closePanel();
return core.useFly(false);
core.useFly(false);
callback();
return;
}
if (itemId == 'centerFly') {
core.ui.closePanel();
return core.ui._drawCenterFly();
core.ui._drawCenterFly();
callback();
return;
}
if (core.canUseItem(itemId)) {
core.ui.closePanel();
core.useItem(itemId);
core.useItem(itemId, noRoute, callback);
} else {
core.playSound('操作失败');
core.drawTip('当前无法使用' + core.material.items[itemId].name, itemId);
}
callback();
};

View File

@ -48,6 +48,7 @@ function show(index: number) {
position: fixed;
overflow: visible;
display: block;
font-size: 80%;
font-family: 'normal';
}
@ -61,7 +62,6 @@ function show(index: number) {
top: 0;
position: fixed;
background-color: #000b;
backdrop-filter: blur(5px);
z-index: 1;
}

View File

@ -43,7 +43,7 @@
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, onUpdated, ref, useSlots, watch } from 'vue';
import { onMounted, onUnmounted, onUpdated, ref, watch } from 'vue';
import { DragOutlined } from '@ant-design/icons-vue';
import { isMobile, useDrag, cancelGlobalDrag } from '../plugin/use';
import { has } from '../plugin/utils';
@ -231,7 +231,6 @@ onUnmounted(() => {
top: 50px;
display: flex;
overflow: visible;
font-family: 'normal';
}
.box-main {

View File

@ -19,11 +19,14 @@
class="special-text"
v-if="has(enemy.special) && enemy.special.length > 0"
>
<span
v-for="(text, i) in enemy.showSpecial"
:style="{ color: text[2] }"
>&nbsp;{{ text[0] }}&nbsp;</span
>
<template v-for="(text, i) in enemy.showSpecial">
<span
v-if="i < (isMobile ? 1 : 2)"
:style="{ color: text[2] }"
>&nbsp;{{ text[0] }}&nbsp;</span
>
<span v-if="i === (isMobile ? 1 : 2)">...</span>
</template>
</div>
<div class="special-text" v-else>无属性</div>
</div>
@ -220,12 +223,12 @@ function enter() {
@media screen and (max-width: 600px) {
.rightbar {
width: 80%;
font-size: 85%;
font-size: 110%;
}
.leftbar {
width: 20%;
font-size: 80%;
font-size: 100%;
}
.enemy-container {

View File

@ -80,8 +80,8 @@ export class BgmController
this.playing = true;
if (!this.disable) {
this.setTransitionAnimate(id, 1);
if (this.now) this.setTransitionAnimate(this.now, 0, when);
this.setTransitionAnimate(id, 1, when);
if (this.now) this.setTransitionAnimate(this.now, 0);
}
if (!noStack) {

View File

@ -1,6 +1,5 @@
import { has } from '@/plugin/utils';
import { AudioParamOf, AudioPlayer } from './audio';
import resource from '@/data/resource.json';
import { ResourceController } from '../loader/controller';
// todo: 立体声,可设置音源位置
@ -24,7 +23,6 @@ export class SoundEffect extends AudioPlayer {
gain: GainNode = AudioPlayer.ac.createGain();
panner: PannerNode | null = null;
merger: ChannelMergerNode | null = null;
set volumn(value: number) {
this.gain.gain.value = value * SoundEffect.volume;
@ -63,9 +61,7 @@ export class SoundEffect extends AudioPlayer {
* 线
* ```txt
* source -> gain -> destination
* source -> panner -> gain --> destination
* source -> merger -> panner -> gain -> destination
*
* source -> panner -> gain -> destination
* ```
* @param stereo
*/
@ -74,20 +70,10 @@ export class SoundEffect extends AudioPlayer {
const ac = AudioPlayer.ac;
if (!channel) return;
this.panner = null;
this.merger = null;
if (stereo) {
this.panner = ac.createPanner();
this.panner.connect(this.gain);
if (channel === 1) {
this.merger = ac.createChannelMerger();
this.merger.connect(this.panner);
this.baseNode = [
{ node: this.merger, channel: 0 },
{ node: this.merger, channel: 1 }
];
} else {
this.baseNode = [{ node: this.panner }];
}
this.baseNode = [{ node: this.panner }];
} else {
this.baseNode = [{ node: this.gain }];
}
@ -136,15 +122,18 @@ export class SoundEffect extends AudioPlayer {
* @param source
* @param listener
*/
setPanner(source: Partial<Panner>, listener: Partial<Listener>) {
setPanner(source?: Partial<Panner>, listener?: Partial<Listener>) {
if (!this.panner) return;
console.log(2);
for (const [key, value] of Object.entries(source)) {
this.panner[key as keyof Panner].value = value;
if (source) {
for (const [key, value] of Object.entries(source)) {
this.panner[key as keyof Panner].value = value;
}
}
const l = AudioPlayer.ac.listener;
for (const [key, value] of Object.entries(listener)) {
l[key as keyof Listener].value = value;
if (listener) {
const l = AudioPlayer.ac.listener;
for (const [key, value] of Object.entries(listener)) {
l[key as keyof Listener].value = value;
}
}
}
}
@ -161,7 +150,6 @@ export class SoundController extends ResourceController<
* @param data ArrayBuffer信息AudioBuffer
*/
add(uri: string, data: ArrayBuffer) {
const stereo = resource.stereoSE.includes(uri);
const se = new SoundEffect(data, true);
if (this.list[uri]) {
console.warn(`Repeated sound effect: '${uri}'.`);
@ -176,6 +164,7 @@ export class SoundController extends ResourceController<
*/
play(sound: SoundIds, end?: () => void): number {
const se = this.get(sound);
if (!se) return -1;
const index = se.playSE();
if (!has(index)) return -1;
this.seIndex[index] = se;

View File

@ -1,13 +1,5 @@
import { BgmController, bgm } from './audio/bgm';
import { SoundController, SoundEffect, sound } from './audio/sound';
import { readyAllResource } from './loader/load';
import {
Resource,
ResourceStore,
ZippedResource,
resource,
zipResource
} from './loader/resource';
import { Focus, GameUi, UiController } from './main/custom/ui';
import { GameStorage } from './main/storage';
import './main/init/';
@ -20,19 +12,51 @@ import {
mainSetting,
settingStorage
} from './main/setting';
import { KeyCode } from '@/plugin/keyCodes';
import { KeyCode, ScanCode } from '@/plugin/keyCodes';
import { status } from '@/plugin/ui/statusBar';
import './plugin';
import './package';
import { AudioPlayer } from './audio/audio';
import { CustomToolbar } from './main/custom/toolbar';
import { Hotkey } from './main/custom/hotkey';
import { Keyboard } from './main/custom/keyboard';
import {
Hotkey,
checkAssist,
isAssist,
unwarpBinary
} from './main/custom/hotkey';
import { Keyboard, generateKeyboardEvent } from './main/custom/keyboard';
import './main/layout';
function ready() {
readyAllResource();
}
import { MComponent, m } from './main/layout';
import { createSettingComponents } from './main/init/settings';
import {
createToolbarComponents,
createToolbarEditorComponents
} from './main/init/toolbar';
import { VirtualKey } from './main/init/misc';
import * as utils from '@/plugin/utils';
import * as use from '@/plugin/use';
import * as mark from '@/plugin/mark';
import * as keyCodes from '@/plugin/keyCodes';
import { addAnimate, removeAnimate } from '@/plugin/animateController';
import * as bookTools from '@/plugin/ui/book';
import * as commonTools from '@/plugin/ui/common';
import * as equipboxTools from '@/plugin/ui/equipbox';
import * as fixedTools from '@/plugin/ui/fixed';
import * as flyTools from '@/plugin/ui/fly';
import * as statusBarTools from '@/plugin/ui/statusBar';
import * as toolboxTools from '@/plugin/ui/toolbox';
import * as UI from '@ui/index';
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 { MCGenerator } from './main/layout';
import { ResourceController } from './loader/controller';
// ----- 类注册
Mota.register('class', 'AudioPlayer', AudioPlayer);
@ -44,15 +68,20 @@ Mota.register('class', 'GameUi', GameUi);
Mota.register('class', 'Hotkey', Hotkey);
Mota.register('class', 'Keyboard', Keyboard);
Mota.register('class', 'MotaSetting', MotaSetting);
Mota.register('class', 'Resource', Resource);
Mota.register('class', 'ResourceStore', ResourceStore);
Mota.register('class', 'SettingDisplayer', SettingDisplayer);
Mota.register('class', 'SoundController', SoundController);
Mota.register('class', 'SoundEffect', SoundEffect);
Mota.register('class', 'UiController', UiController);
Mota.register('class', 'ZippedResource', ZippedResource);
Mota.register('class', 'MComponent', MComponent);
Mota.register('class', 'ResourceController', ResourceController);
// ----- 函数注册
Mota.register('fn', 'm', m);
Mota.register('fn', 'unwrapBinary', unwarpBinary);
Mota.register('fn', 'checkAssist', checkAssist);
Mota.register('fn', 'isAssist', isAssist);
Mota.register('fn', 'generateKeyboardEvent', generateKeyboardEvent);
Mota.register('fn', 'addAnimate', addAnimate);
Mota.register('fn', 'removeAnimate', removeAnimate);
// ----- 变量注册
Mota.register('var', 'mainUi', mainUi);
Mota.register('var', 'fixedUi', fixedUi);
@ -61,11 +90,43 @@ Mota.register('var', 'sound', sound);
Mota.register('var', 'gameKey', gameKey);
Mota.register('var', 'mainSetting', mainSetting);
Mota.register('var', 'KeyCode', KeyCode);
Mota.register('var', 'resource', resource);
Mota.register('var', 'zipResource', zipResource);
Mota.register('var', 'settingStorage', settingStorage);
Mota.register('var', 'status', status);
// ----- 模块注册
Mota.register('module', 'CustomComponents', {
createSettingComponents,
createToolbarComponents,
createToolbarEditorComponents
});
Mota.register('module', 'MiscComponents', {
VirtualKey
});
Mota.register('module', 'RenderUtils', utils);
Mota.register('module', 'Use', use);
Mota.register('module', 'Mark', mark);
Mota.register('module', 'KeyCodes', keyCodes);
Mota.register('module', 'UITools', {
book: bookTools,
common: commonTools,
equipbox: equipboxTools,
fixed: fixedTools,
fly: flyTools,
statusBar: statusBarTools,
toolbox: toolboxTools
});
Mota.register('module', 'UI', UI);
Mota.register('module', 'UIComponents', {
Box,
BoxAnimate,
Colomn,
EnemyOne,
Scroll,
EnemyCritical,
EnemySpecial,
EnemyTarget,
Keyboard: KeyboardPanel
});
Mota.register('module', 'MCGenerator', MCGenerator);
ready();
main.renderLoaded = true;
Mota.require('var', 'hook').emit('renderLoaded');

View File

@ -137,7 +137,7 @@ export class Hotkey extends EventEmitter<HotkeyEvent> {
* 退
* @param symbol symbol
*/
dispose(symbol: symbol) {
dispose(symbol: symbol = this.scopeStack.at(-1) ?? Symbol()) {
for (const key of Object.values(this.data)) {
key.func.delete(symbol);
}

View File

@ -116,6 +116,14 @@ export class CustomToolbar extends EventEmitter<CustomToolbarEvent> {
constructor(id: string) {
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;
this.show();
CustomToolbar.list.push(this);
}
@ -188,22 +196,31 @@ export class CustomToolbar extends EventEmitter<CustomToolbarEvent> {
/**
*
*/
refresh() {
const items = this.items.splice(0);
nextTick(() => {
this.items.push(...items);
});
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);
}
/**
@ -269,12 +286,14 @@ export class CustomToolbar extends EventEmitter<CustomToolbarEvent> {
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,
h: v.height,
w: v.width / scale,
h: v.height / scale,
items: []
};
v.items.forEach(v => {
@ -288,7 +307,7 @@ export class CustomToolbar extends EventEmitter<CustomToolbarEvent> {
static load() {
toolbarStorage.read();
for (const [key, value] of Object.entries(toolbarStorage.data)) {
const bar = new CustomToolbar(key);
const bar = this.get(key) ?? new CustomToolbar(key);
bar.x = value.x;
bar.y = value.y;
bar.width = value.w;
@ -299,6 +318,10 @@ export class CustomToolbar extends EventEmitter<CustomToolbarEvent> {
}
}
static refreshAll(reopen: boolean = false): void {
CustomToolbar.list.forEach(v => v.refresh(reopen));
}
static showAll(): number[] {
return CustomToolbar.list.map(v => v.show());
}
@ -376,16 +399,16 @@ CustomToolbar.register(
}
);
window.addEventListener('beforeunload', () => {
CustomToolbar.save();
});
window.addEventListener('blur', () => {
CustomToolbar.save();
});
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();

View File

@ -9,8 +9,6 @@ interface FocusEvent<T> extends EmitableEvent {
unfocus: (before: T | null) => void;
add: (item: T) => void;
pop: (item: T | null) => void;
register: (item: T[]) => void;
unregister: (item: T[]) => void;
splice: (spliced: T[]) => void;
}
@ -163,7 +161,7 @@ interface IndexedGameUi extends ShowableGameUi {
}
interface HoldOnController {
end(): void;
end(noClosePanel?: boolean): void;
}
export class UiController extends Focus<IndexedGameUi> {
@ -182,7 +180,7 @@ export class UiController extends Focus<IndexedGameUi> {
v.ui.emit('close');
});
if (this.stack.length === 0) {
if (!this.hold) this.emit('end');
if (!this.hold) this.emit('end', false);
this.hold = false;
}
});
@ -227,8 +225,8 @@ export class UiController extends Focus<IndexedGameUi> {
this.hold = true;
return {
end: () => {
this.emit('end');
end: (noClosePanel: boolean = false) => {
this.emit('end', noClosePanel);
}
};
}

View File

@ -9,7 +9,8 @@ interface Components {
Number: SettingComponent;
HotkeySetting: SettingComponent;
ToolbarEditor: SettingComponent;
RadioSetting: (items: string[]) => SettingComponent;
Radio: (items: string[]) => SettingComponent;
Performance: SettingComponent;
}
export type { Components as SettingDisplayComponents };
@ -21,7 +22,8 @@ export function createSettingComponents() {
Number: NumberSetting,
HotkeySetting,
ToolbarEditor,
RadioSetting
Radio: RadioSetting,
Performance: PerformanceSetting
};
return com;
}
@ -63,7 +65,7 @@ function NumberSetting(props: SettingComponentProps) {
if (value < (item.step?.[0] ?? 0) || value > (item.step?.[1] ?? 100)) {
return;
}
setting.setValue(displayer.selectStack.join('.'), value);
setting.setValue(displayer.selectStack.join('.'), Math.round(value));
displayer.update();
};
@ -180,3 +182,18 @@ function ToolbarEditor(props: SettingComponentProps) {
</div>
);
}
function PerformanceSetting(props: SettingComponentProps) {
return (
<div style="display: flex; justify-content: center">
<Button
style="font-size: 75%"
type="primary"
size="large"
onClick={() => showSpecialSetting('performance')}
>
</Button>
</div>
);
}

View File

@ -8,6 +8,7 @@ import { checkAssist } from '../custom/hotkey';
import { getVitualKeyOnce } from '@/plugin/utils';
import { cloneDeep } from 'lodash-es';
import { Select, SelectOption } from 'ant-design-vue';
import { mainSetting } from '../setting';
// todo: 新增更改设置的ToolItem
@ -53,15 +54,18 @@ function KeyTool(props: CustomToolbarProps<'hotkey'>) {
function ItemTool(props: CustomToolbarProps<'item'>) {
const { item, toolbar } = props;
const scale = mainSetting.getValue('ui.toolbarScale', 100) / 100;
return (
<div
style="display: flex; justify-content: center; width: 50px"
style={`display: flex; justify-content: center; width: ${
50 * scale
}px`}
onClick={() => toolbar.emitTool(item.id)}
>
<BoxAnimate
noborder={true}
width={50}
height={50}
width={50 * scale}
height={50 * scale}
id={item.item}
></BoxAnimate>
</div>

View File

@ -1,6 +1,7 @@
import * as UI from '@ui/.';
import * as MiscUI from './misc';
import { GameUi, UiController } from '../custom/ui';
import { mainSetting } from '../setting';
export const mainUi = new UiController();
mainUi.register(
@ -35,20 +36,34 @@ fixedUi.register(
);
fixedUi.showAll();
let loaded = false;
let mounted = false;
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', () => {
mainUi.on('end', noClosePanel => {
ui.style.display = 'none';
try {
core.closePanel();
} catch {}
if (!noClosePanel) {
try {
core.closePanel();
} catch {}
}
});
fixedUi.on('start', () => {
fixed.style.display = 'block';
@ -57,6 +72,15 @@ hook.once('mounted', () => {
fixed.style.display = 'none';
});
// todo: 暂时先这么搞,之后重写加载界面,需要改成先显示加载界面,加载完毕后再打开这个界面
fixedUi.open('start');
if (loaded && !mounted) {
fixedUi.open('start');
}
mounted = true;
});
hook.once('load', () => {
if (mounted) {
// todo: 暂时先这么搞,之后重写加载界面,需要改成先显示加载界面,加载完毕后再打开这个界面
fixedUi.open('start');
}
loaded = true;
});

View File

@ -5,10 +5,12 @@ import {
VNode,
VNodeChild,
defineComponent,
h,
h as hVue,
isVNode,
onMounted
} from 'vue';
import BoxAnimate from '@/components/boxAnimate.vue';
import { ensureArray } from '@/plugin/utils';
interface VForRenderer {
type: '@v-for';
@ -18,7 +20,7 @@ interface VForRenderer {
interface MotaComponent extends MotaComponentConfig {
type: string;
children: MComponent[] | MComponent;
children: (MComponent | MotaComponent | VNode)[];
}
interface MotaComponentConfig {
@ -32,7 +34,7 @@ interface MotaComponentConfig {
velse?: boolean;
}
type OnSetupFunction = (props: Record<string, any>) => void;
type OnSetupFunction = (props: Record<string, any>, ctx: SetupContext) => void;
type SetupFunction = (
props: Record<string, any>,
ctx: SetupContext
@ -43,6 +45,7 @@ type RetFunction = (
) => VNodeChild | VNodeChild[];
type OnMountedFunction = (
props: Record<string, any>,
ctx: SetupContext,
canvas: HTMLCanvasElement[]
) => void;
@ -51,6 +54,12 @@ type NonComponentConfig = Omit<
'innerText' | 'component' | 'slots' | 'dComponent'
>;
type MComponentChildren =
| (MComponent | MotaComponent | VNode)[]
| MComponent
| MotaComponent
| VNode;
export class MComponent {
static mountNum: number = 0;
@ -88,7 +97,7 @@ export class MComponent {
* @param children
* @param config {@link MComponent.h}
*/
div(children?: MComponent[] | MComponent, config?: NonComponentConfig) {
div(children?: MComponentChildren, config?: NonComponentConfig) {
return this.h('div', children, config);
}
@ -97,7 +106,7 @@ export class MComponent {
* @param children
* @param config {@link MComponent.h}
*/
span(children?: MComponent[] | MComponent, config?: NonComponentConfig) {
span(children?: MComponentChildren, config?: NonComponentConfig) {
return this.h('span', children, config);
}
@ -124,7 +133,7 @@ export class MComponent {
*/
com(
component: Component | MComponent,
config: Omit<MotaComponentConfig, 'innerText' | 'component'>
config?: Omit<MotaComponentConfig, 'innerText' | 'component'>
) {
return this.h(component, [], config);
}
@ -136,11 +145,7 @@ export class MComponent {
* MComponent.vNode函数生成
*/
vfor<T>(items: T[] | (() => T[]), map: (value: T, index: number) => VNode) {
this.content.push({
type: '@v-for',
items,
map
});
this.content.push(MCGenerator.vfor(items, map));
return this;
}
@ -180,32 +185,10 @@ export class MComponent {
*/
h(
type: string | Component | MComponent,
children?: MComponent[] | MComponent,
children?: MComponentChildren,
config: MotaComponentConfig = {}
): this {
if (typeof type === 'string') {
this.content.push({
type,
children: children ?? [],
props: config.props,
innerText: config.innerText,
slots: config.slots,
vif: config.vif,
velse: config.velse,
component: config.component
});
} else {
this.content.push({
type: 'component',
children: children ?? [],
props: config.props,
innerText: config.innerText,
slots: config.slots,
vif: config.vif,
velse: config.velse,
component: type
});
}
this.content.push(MCGenerator.h(type, children, config));
return this;
}
@ -253,11 +236,12 @@ export class MComponent {
return defineComponent(
(props, ctx) => {
const mountNum = MComponent.mountNum++;
this.onSetupFn?.(props);
this.onSetupFn?.(props, ctx);
onMounted(() => {
this.onMountedFn?.(
props,
ctx,
Array.from(
document.getElementsByClassName(
`--mota-component-canvas-${mountNum}`
@ -269,8 +253,6 @@ export class MComponent {
if (this.retFn) return () => this.retFn!(props, ctx);
else {
return () => {
console.log(ctx.slots.default);
const vNodes = MComponent.vNode(
this.content,
mountNum
@ -312,12 +294,19 @@ export class MComponent {
* @param children VNode的内容列表
* @param mount id
*/
static vNode(children: (MotaComponent | VForRenderer)[], mount?: number) {
static vNode(
children: (MotaComponent | VForRenderer | VNode)[],
mount?: number
) {
const mountNum = mount ?? this.mountNum++;
const res: VNode[] = [];
const vifRes: Map<number, boolean> = new Map();
children.forEach((v, i) => {
if (isVNode(v)) {
res.push(v);
return;
}
if (v.type === '@v-for') {
const node = v as VForRenderer;
const items =
@ -346,7 +335,7 @@ export class MComponent {
);
}
if (v.dComponent) {
res.push(h(v.dComponent(), props, v.slots));
res.push(hVue(v.dComponent(), props, v.slots));
} else {
if (v.component instanceof MComponent) {
res.push(
@ -356,12 +345,12 @@ export class MComponent {
)
);
} else {
res.push(h(v.component!, props, v.slots));
res.push(hVue(v.component!, props, v.slots));
}
}
} else if (v.type === 'text') {
res.push(
h(
hVue(
'span',
typeof v.innerText === 'function'
? v.innerText()
@ -372,15 +361,17 @@ export class MComponent {
const cls = `--mota-component-canvas-${mountNum}`;
const mix = !!props.class ? cls + ' ' + props.class : cls;
props.class = mix;
res.push(h('canvas', props, node.slots));
res.push(hVue('canvas', props, node.slots));
} else {
// 这个时候不可能会有插槽,只会有子内容,因此直接渲染子内容
const content = [node.children].flat(2);
const content = node.children;
const vn = this.vNode(
content.map(v => v.content).flat(),
content
.map(v => (v instanceof MComponent ? v.content : v))
.flat(),
mountNum
);
res.push(h(v.type, props, vn));
res.push(hVue(v.type, props, vn));
}
}
});
@ -406,7 +397,7 @@ export class MComponent {
* @param props props
*/
static prop(component: Component, props: Record<string, any>) {
return h(component, props);
return hVue(component, props);
}
}
@ -418,18 +409,101 @@ export function m() {
return new MComponent();
}
/**
* VNode
* @param id id
* @param width
* @param height
* @param noBoarder
*/
export function icon(
id: AllIds,
width?: number,
height?: number,
noBoarder?: number
) {
return h(BoxAnimate, { id, width, height, noBoarder });
export namespace MCGenerator {
export function h(
type: string | Component | MComponent,
children?: MComponentChildren,
config: MotaComponentConfig = {}
): MotaComponent {
if (typeof type === 'string') {
return {
type,
children: ensureArray(children ?? []),
props: config.props,
innerText: config.innerText,
slots: config.slots,
vif: config.vif,
velse: config.velse,
component: config.component
};
} else {
return {
type: 'component',
children: ensureArray(children ?? []),
props: config.props,
innerText: config.innerText,
slots: config.slots,
vif: config.vif,
velse: config.velse,
component: type
};
}
}
export function div(
children?: MComponentChildren,
config?: NonComponentConfig
): MotaComponent {
return h('div', children, config);
}
export function span(
children?: MComponentChildren,
config?: NonComponentConfig
): MotaComponent {
return h('span', children, config);
}
export function canvas(config?: NonComponentConfig): MotaComponent {
return h('canvas', [], config);
}
export function text(
text: string | (() => string),
config: NonComponentConfig = {}
): MotaComponent {
return h('text', [], { ...config, innerText: text });
}
export function com(
component: Component | MComponent,
config: Omit<MotaComponentConfig, 'innerText' | 'component'>
): MotaComponent {
return h(component, [], config);
}
/**
* VNode
* @param id id
* @param width
* @param height
* @param noBoarder
*/
export function icon(
id: AllIds,
width?: number,
height?: number,
noBoarder?: number
): VNode {
return hVue(BoxAnimate, { id, width, height, noBoarder });
}
export function vfor<T>(
items: T[] | (() => T[]),
map: (value: T, index: number) => VNode
): VForRenderer {
return {
type: '@v-for',
items,
map
};
}
/**
*
* @param value
*/
export function f<T>(value: T): () => T {
return () => value;
}
}

View File

@ -7,6 +7,8 @@ import { bgm } from '../audio/bgm';
import { SoundEffect } from '../audio/sound';
import settingsText from '@/data/settings.json';
import { isMobile } from '@/plugin/use';
import { fontSize } from '@/plugin/ui/statusBar';
import { CustomToolbar } from './custom/toolbar';
export interface SettingComponentProps {
item: MotaSettingItem;
@ -334,6 +336,8 @@ mainSetting.on('valueChange', (key, n, o) => {
handleActionSetting(setting, n, o);
} else if (root === 'audio') {
handleAudioSetting(setting, n, o);
} else if (root === 'ui') {
handleUiSetting(setting, n, o);
}
});
@ -363,6 +367,11 @@ function handleScreenSetting<T extends number | boolean>(
} 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;
}
}
@ -394,6 +403,16 @@ function handleAudioSetting<T extends number | boolean>(
}
}
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);
}
}
// ----- 游戏的所有设置项
// todo: 虚拟键盘缩放,小地图楼传缩放
mainSetting
@ -407,11 +426,13 @@ mainSetting
.register('heroDetail', '勇士显伤', false, COM.Boolean)
.register('transition', '界面动画', false, COM.Boolean)
.register('antiAlias', '抗锯齿', false, COM.Boolean)
.register('fontSize', '字体大小', 16, COM.Number, [8, 28, 1])
.register('fontSize', '字体大小', 16, COM.Number, [2, 48, 1])
.register('fontSizeStatus', '状态栏字体', 16, COM.Number, [2, 48, 1])
.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',
@ -450,13 +471,13 @@ mainSetting
.register(
'ui',
'ui设置',
new MotaSetting().register(
'mapScale',
'小地图楼传缩放',
100,
COM.Number,
[50, 1000, 50]
)
new MotaSetting()
.register('mapScale', '小地图缩放', 100, COM.Number, [50, 1000, 50])
.setDisplayFunc('mapScale', value => `${value}%`)
.register('toolbarScale', '工具栏缩放', 100, COM.Number, [10, 500, 10])
.setDisplayFunc('toolbarScale', value => `${value}%`)
.register('bookScale', '怪物手册缩放', 100, COM.Number, [10, 500, 10])
.setDisplayFunc('bookScale', value => `${value}%`)
);
const loading = Mota.require('var', 'loading');
@ -471,6 +492,7 @@ loading.once('coreInit', () => {
'screen.fontSize': storage.getValue('screen.fontSize', 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),
@ -483,7 +505,12 @@ loading.once('coreInit', () => {
'ui.mapScale': storage.getValue(
'ui.mapScale',
isMobile ? 300 : Math.floor(window.innerWidth / 600) * 50
)
),
'ui.toolbarScale': storage.getValue(
'ui.toolbarScale',
isMobile ? 50 : Math.floor((window.innerWidth / 1700) * 10) * 10
),
'ui.bookScale': storage.getValue('ui.bookScale', isMobile ? 100 : 80),
});
});
@ -515,4 +542,22 @@ mainSetting
.setDescription('audio.bgmVolume', `背景音乐的音量`)
.setDescription('audio.soundEnabled', `是否开启音效`)
.setDescription('audio.soundVolume', `音效的音量`)
.setDescription('ui.mapScale', `楼传小地图的缩放,百分比格式`);
.setDescription('ui.mapScale', `楼传小地图的缩放,百分比格式`)
.setDescription('ui.toolbarScale', `自定义工具栏的缩放比例`)
.setDescription('ui.bookScale', `怪物手册界面中每个怪物框体的高度缩放,最小值限定为 20% 屏幕高度`)
.setDescription('screen.fontSizeStatus', `修改状态栏的字体大小`)
.setDescription('screen.blur', '打开任意ui界面时是否有背景虚化效果移动端打开后可能会有掉帧或者发热现象。关闭ui后生效');
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();
});

View File

@ -4,6 +4,7 @@ import { loading } from '../game';
export interface CurrentEnemy {
enemy: DamageEnemy;
// 这个是干啥的?
onMapEnemy: DamageEnemy[];
}
@ -16,155 +17,64 @@ export function getEnemy(
return v.x === x && v.y === y;
});
if (!enemy) {
throw new Error(
`Get null when getting enemy on '${x},${y}' in '${floorId}'`
);
return null;
}
return enemy;
}
function init() {
core.enemys.canBattle = function canBattle(
x: number,
x: number | DamageEnemy,
y: number,
floorId: FloorIds = core.status.floorId
) {
const enemy = getEnemy(x, y, floorId);
const enemy = typeof x === 'number' ? getEnemy(x, y, floorId) : x;
if (!enemy) {
throw new Error(
`Cannot get enemy on x:${x}, y:${y}, floor: ${floorId}`
);
}
const { damage } = enemy.calDamage();
return damage < core.status.hero.hp;
};
core.events.battle = function battle(
x: number,
x: number | DamageEnemy,
y: number,
force: boolean = false,
callback?: () => void
) {
core.saveAndStopAutomaticRoute();
const enemy = getEnemy(x, y);
const isLoc = typeof x === 'number';
const enemy = isLoc ? getEnemy(x, y) : x;
if (!enemy) {
throw new Error(
`Cannot battle with enemy since no enemy on ${x},${y}`
);
}
// 非强制战斗
// @ts-ignore
if (!core.canBattle(x, y) && !force && !core.status.event.id) {
core.stopSound();
core.playSound('操作失败');
core.drawTip('你打不过此怪物!', enemy.id);
core.drawTip('你打不过此怪物!', enemy!.id);
return core.clearContinueAutomaticRoute(callback);
}
// 自动存档
if (!core.status.event.id) core.autosave(true);
// 战前事件
// 战后事件
core.afterBattle(enemy, x, y);
core.afterBattle(enemy, isLoc ? x : enemy.x, y);
callback?.();
};
core.events.afterBattle = function afterBattle(
enemy: DamageEnemy,
x?: number,
y?: number
) {
const floorId = core.status.floorId;
const special = enemy.info.special;
const getFacedId = (enemy: DamageEnemy) => {
const e = enemy.enemy;
// 播放战斗动画
let animate: AnimationIds = 'hand';
// 检查当前装备是否存在攻击动画
const equipId = core.getEquip(0);
if (equipId && (core.material.items[equipId].equip || {}).animate)
animate = core.material.items[equipId].equip.animate;
// 检查该动画是否存在SE如果不存在则使用默认音效
if (!core.material.animates[animate]?.se) core.playSound('attack.mp3');
// 战斗伤害
const info = enemy.calDamage(core.status.hero);
const damage = info.damage;
// 判定是否致死
if (damage >= core.status.hero.hp) {
core.status.hero.hp = 0;
core.updateStatusBar(false, true);
core.events.lose('战斗失败');
return;
}
// 扣减体力值并记录统计数据
core.status.hero.hp -= damage;
core.status.hero.statistics.battleDamage += damage;
core.status.hero.statistics.battle++;
// 智慧之源
if (special.includes(14) && flags.hard === 2) {
core.addFlag(
'inte_' + floorId,
Math.ceil((core.status.hero.mdef / 10) * 0.3) * 10
);
core.status.hero.mdef -=
Math.ceil((core.status.hero.mdef / 10) * 0.3) * 10;
}
// 极昼永夜
if (special.includes(22)) {
flags[`night_${floorId}`] ??= 0;
flags[`night_${floorId}`] -= enemy.enemy.night!;
}
if (special.includes(23)) {
flags[`night_${floorId}`] ??= 0;
flags[`night_${floorId}`] += enemy.enemy.day;
}
// if (core.plugin.skillTree.getSkillLevel(11) > 0) {
// core.plugin.study.declineStudiedSkill();
// }
// 如果是融化怪,需要特殊标记一下
if (special.includes(25) && has(x) && has(y)) {
flags[`melt_${floorId}`] ??= {};
flags[`melt_${floorId}`][`${x},${y}`] = enemy.enemy.melt;
}
// 获得金币
const money = enemy.enemy.money;
core.status.hero.money += money;
core.status.hero.statistics.money += money;
// 获得经验
const exp = enemy.enemy.exp;
core.status.hero.exp += exp;
core.status.hero.statistics.exp += exp;
const hint =
'打败 ' + enemy.enemy.name + ',金币+' + money + ',经验+' + exp;
core.drawTip(hint, enemy.id);
if (core.getFlag('bladeOn') && core.getFlag('blade')) {
core.setFlag('blade', false);
}
if (core.getFlag('shieldOn') && core.getFlag('shield')) {
core.setFlag('shield', false);
}
// 事件的处理
const todo: MotaEvent = [];
// 战后事件
if (has(core.status.floorId)) {
const loc = `${x},${y}` as LocString;
todo.push(
...(core.floors[core.status.floorId].afterBattle[loc] ?? [])
);
}
todo.push(...(enemy.enemy.afterBattle ?? []));
// 如果事件不为空,将其插入
if (todo.length > 0) core.insertAction(todo, x, y);
if (has(x) && has(y)) {
core.drawAnimate(animate, x, y);
core.removeBlock(x, y);
} else core.drawHeroAnimate(animate);
// 如果已有事件正在处理中
if (core.status.event.id == null) core.continueAutomaticRoute();
else core.clearContinueAutomaticRoute();
if (e.displayIdInBook) return e.displayIdInBook;
if (e.faceIds) return e.faceIds.down;
return e.id;
};
core.enemys.getCurrentEnemys = function getCurrentEnemys(
@ -176,7 +86,8 @@ function init() {
ensureFloorDamage(floorId);
const floor = core.status.maps[floorId];
floor.enemy.list.forEach(v => {
if (!(v.id in used)) {
const id = getFacedId(v);
if (!(id in used)) {
const e = new DamageEnemy(v.enemy);
e.calAttribute();
e.getRealInfo();
@ -186,9 +97,9 @@ function init() {
onMapEnemy: [v]
};
enemys.push(curr);
used[v.id] = curr.onMapEnemy;
used[id] = curr.onMapEnemy;
} else {
used[v.id].push(v);
used[id].push(v);
}
});
@ -207,7 +118,7 @@ function init() {
const enemy = getEnemy(data.x, data.y);
beforeBattle.push(...(floor.beforeBattle[loc] ?? []));
beforeBattle.push(...(enemy.enemy.beforeBattle ?? []));
beforeBattle.push(...(enemy!.enemy.beforeBattle ?? []));
if (beforeBattle.length > 0) {
beforeBattle.push({ type: 'battle', x: data.x, y: data.y });
@ -251,5 +162,22 @@ loading.once('coreInit', init);
declare global {
interface Enemys {
getCurrentEnemys(floorId: FloorIds): CurrentEnemy[];
canBattle(enemy: DamageEnemy, _?: number, floorId?: FloorIds): boolean;
canBattle(x: number, y: number, floorId?: FloorIds): boolean;
}
interface Events {
battle(
enemy: DamageEnemy,
_?: number,
force?: boolean,
callback?: () => void
): void;
battle(
x: number,
y?: number,
force?: boolean,
callback?: () => void
): void;
}
}

View File

@ -16,15 +16,15 @@ interface HaloType {
};
}
interface EnemyInfo {
interface EnemyInfo extends Partial<Enemy> {
atk: number;
def: number;
hp: number;
special: number[];
damageDecline: number;
atkBuff: number;
defBuff: number;
hpBuff: number;
atkBuff_: number;
defBuff_: number;
hpBuff_: number;
enemy: Enemy;
x?: number;
y?: number;
@ -65,7 +65,7 @@ interface CriticalDamageDelta extends Omit<DamageDelta, 'info'> {
type HaloFn = (info: EnemyInfo, enemy: Enemy) => void;
/** 光环属性 */
export const haloSpecials: number[] = [8, 21, 25, 26, 27];
export const haloSpecials: Set<number> = new Set([8, 21, 25, 26, 27]);
export class EnemyCollection implements RangeCollection<DamageEnemy> {
floorId: FloorIds;
@ -302,7 +302,7 @@ export class DamageEnemy<T extends EnemyIds = EnemyIds> {
info!: EnemyInfo;
/** 向其他怪提供过的光环 */
providedHalo: number[] = [];
providedHalo: Set<number> = new Set();
/**
* 0 -> -> 1 -> -> 2 -> provide inject
@ -334,16 +334,23 @@ export class DamageEnemy<T extends EnemyIds = EnemyIds> {
def: enemy.def,
special: enemy.special.slice(),
damageDecline: 0,
atkBuff: 0,
defBuff: 0,
hpBuff: 0,
atkBuff_: 0,
defBuff_: 0,
hpBuff_: 0,
enemy: this.enemy,
x: this.x,
y: this.y,
floorId: this.floorId
};
for (const [key, value] of Object.entries(enemy)) {
if (!(key in this.info) && has(value)) {
// @ts-ignore
this.info[key] = value;
}
}
this.progress = 0;
this.providedHalo = [];
this.providedHalo.clear();
}
/**
@ -370,8 +377,8 @@ export class DamageEnemy<T extends EnemyIds = EnemyIds> {
for (const [loc, per] of Object.entries(flags[`melt_${floorId}`])) {
const [mx, my] = loc.split(',').map(v => parseInt(v));
if (Math.abs(mx - this.x) <= 1 && Math.abs(my - this.y) <= 1) {
info.atkBuff += per as number;
info.defBuff += per as number;
info.atkBuff_ += per as number;
info.defBuff_ += per as number;
}
}
}
@ -392,9 +399,9 @@ export class DamageEnemy<T extends EnemyIds = EnemyIds> {
// 此时已经inject光环因此直接计算真实属性
const info = this.info;
info.atk = Math.floor(info.atk * (info.atkBuff / 100 + 1));
info.def = Math.floor(info.def * (info.defBuff / 100 + 1));
info.hp = Math.floor(info.hp * (info.hpBuff / 100 + 1));
info.atk = Math.floor(info.atk * (info.atkBuff_ / 100 + 1));
info.def = Math.floor(info.def * (info.defBuff_ / 100 + 1));
info.hp = Math.floor(info.hp * (info.hpBuff_ / 100 + 1));
return this.info;
}
@ -404,7 +411,7 @@ export class DamageEnemy<T extends EnemyIds = EnemyIds> {
if (!has(this.x) || !has(this.y)) return [];
const special = this.info.special ?? this.enemy.special;
const filter = special.filter(v => {
return haloSpecials.includes(v) && !this.providedHalo.includes(v);
return haloSpecials.has(v) && !this.providedHalo.has(v);
});
if (filter.length === 0) return [];
const collection = this.col ?? core.status.maps[this.floorId].enemy;
@ -448,11 +455,11 @@ export class DamageEnemy<T extends EnemyIds = EnemyIds> {
e.special.includes(8) &&
(e.x !== this.x || this.y !== e.y)
) {
e.atkBuff += enemy.together ?? 0;
e.defBuff += enemy.together ?? 0;
e.atkBuff_ += enemy.together ?? 0;
e.defBuff_ += enemy.together ?? 0;
}
});
this.providedHalo.push(8);
this.providedHalo.add(8);
}
// 冰封光环
@ -460,7 +467,7 @@ export class DamageEnemy<T extends EnemyIds = EnemyIds> {
square7.push(e => {
e.damageDecline += this.enemy.iceHalo ?? 0;
});
this.providedHalo.push(21);
this.providedHalo.add(21);
col.haloList.push({
type: 'square',
data: { x: this.x, y: this.y, d: 7 },
@ -472,9 +479,9 @@ export class DamageEnemy<T extends EnemyIds = EnemyIds> {
// 冰封之核
if (special.includes(26)) {
square5.push(e => {
e.defBuff += this.enemy.iceCore ?? 0;
e.defBuff_ += this.enemy.iceCore ?? 0;
});
this.providedHalo.push(26);
this.providedHalo.add(26);
col.haloList.push({
type: 'square',
data: { x: this.x, y: this.y, d: 5 },
@ -486,9 +493,9 @@ export class DamageEnemy<T extends EnemyIds = EnemyIds> {
// 火焰之核
if (special.includes(27)) {
square5.push(e => {
e.atkBuff += this.enemy.fireCore ?? 0;
e.atkBuff_ += this.enemy.fireCore ?? 0;
});
this.providedHalo.push(27);
this.providedHalo.add(27);
col.haloList.push({
type: 'square',
data: { x: this.x, y: this.y, d: 5 },
@ -617,7 +624,7 @@ export class DamageEnemy<T extends EnemyIds = EnemyIds> {
) {
damage[loc] ??= { damage: 0, type: new Set() };
damage[loc].damage += dam;
damage[loc].type.add(type);
if (type) damage[loc].type.add(type);
}
private calEnemyDamageOf(hero: Partial<HeroStatus>, enemy: EnemyInfo) {
@ -830,6 +837,9 @@ const skills: [unlock: string, condition: string][] = [
['shieldOn', 'shield']
];
const haloValue: Map<number, SelectKey<Enemy, number | undefined>[]> =
new Map();
/**
*
* @param info
@ -946,13 +956,6 @@ export function getSingleEnemy(id: EnemyIds) {
}
declare global {
interface PluginDeclaration {
damage: {
Enemy: typeof DamageEnemy;
Collection: typeof EnemyCollection;
};
}
interface Floor {
enemy: EnemyCollection;
}

View File

@ -84,14 +84,16 @@ export interface GameEvent extends EmitableEvent {
mounted: () => void;
/** Emitted in plugin/ui.js */
statusBarUpdate: () => void;
/** Emitted in libs/events.js */
afterGetItem: (
itemId: AllIdsOf<'items'>,
x: number,
y: number,
isGentleClick: boolean
) => void;
afterOpenDoor: (doorId: AllIdsOf<'animates'>, x: number, y: number) => void;
/** Emitted in core/index.ts */
renderLoaded: () => void;
// /** Emitted in libs/events.js */
// afterGetItem: (
// itemId: AllIdsOf<'items'>,
// x: number,
// y: number,
// isGentleClick: boolean
// ) => void;
// afterOpenDoor: (doorId: AllIdsOf<'animates'>, x: number, y: number) => void;
}
export const hook = new EventEmitter<GameEvent>();
@ -112,7 +114,7 @@ class GameListener extends EventEmitter<ListenerEvent> {
constructor() {
super();
if (main.replayChecking) return;
if (!!window.core) {
this.init();
} else {
@ -131,14 +133,19 @@ class GameListener extends EventEmitter<ListenerEvent> {
const getBlockLoc = (px: number, py: number, size: number) => {
return [
Math.floor(((px * 32) / size - core.bigmap.offsetX) / 32),
Math.floor(((py * 32) / size - core.bigmap.offsetY) / 32)
Math.floor(((px * 32) / size + core.bigmap.offsetX) / 32),
Math.floor(((py * 32) / size + core.bigmap.offsetY) / 32)
];
};
// hover & leave & mouseMove
data.addEventListener('mousemove', e => {
if (core.status.lockControl || !core.isPlaying()) return;
if (
core.status.lockControl ||
!core.isPlaying() ||
!core.status.floorId
)
return;
this.emit('mouseMove', e);
const {
x: px,
@ -164,7 +171,12 @@ class GameListener extends EventEmitter<ListenerEvent> {
}
});
data.addEventListener('mouseleave', e => {
if (core.status.lockControl || !core.isPlaying()) return;
if (
core.status.lockControl ||
!core.isPlaying() ||
!core.status.floorId
)
return;
const blocks = core.getMapBlocksObj();
const lastBlock = blocks[`${lastHoverX},${lastHoverY}`];
if (!!lastBlock) {
@ -175,7 +187,12 @@ class GameListener extends EventEmitter<ListenerEvent> {
});
// click
data.addEventListener('click', e => {
if (core.status.lockControl || !core.isPlaying()) return;
if (
core.status.lockControl ||
!core.isPlaying() ||
!core.status.floorId
)
return;
const {
x: px,
y: py,

View File

@ -18,11 +18,7 @@ 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,
SettingStorage
} from '@/core/main/setting';
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';
@ -87,7 +83,7 @@ interface VariableInterface {
sound: SoundController;
resource: ResourceStore<Exclude<ResourceType, 'zip'>>;
zipResource: ResourceStore<'zip'>;
settingStorage: GameStorage<SettingStorage>;
settingStorage: GameStorage;
status: Ref<boolean>;
// 定义于游戏进程,渲染进程依然可用
haloSpecials: number[];

View File

@ -8,7 +8,7 @@
* Inspired somewhat from https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
* But these are "more general", as they should work across browsers & OS`s.
*/
export enum KeyCode {
export enum KeyCode {
DependsOnKbLayout = -1,
/**

View File

@ -1,3 +1,4 @@
import { ref } from 'vue';
export const status = ref(false);
export const fontSize = ref(100);

View File

@ -71,6 +71,11 @@ type Enemy<I extends EnemyIds = EnemyIds> = {
*/
displayIdInBook: EnemyIds;
/**
*
*/
faceIds: Record<Dir, EnemyIds>;
/**
*
*/

26
src/types/event.d.ts vendored
View File

@ -99,22 +99,6 @@ interface Events extends EventData {
*/
trigger(x: number, y: number, callback?: () => void): void;
/**
*
* @example core.battle('greenSlime'); // 和从天而降的绿头怪战斗(如果打得过)
* @param id id
* @param x
* @param y
* @param force true表示强制战斗
* @param callback
*/
battle(
x: number,
y: number,
force: boolean = false,
callback?: () => void
): void;
/**
*
* @example core.openDoor(0, 0, true, core.jumpHero); // 打开左上角的门,需要钥匙,然后主角原地跳跃半秒
@ -428,6 +412,7 @@ interface Events extends EventData {
): void;
/**
* @deprecated
*
* @example core.setEnemy('greenSlime', 'def', 0); // 把绿头怪的防御设为0
* @param id id
@ -447,6 +432,7 @@ interface Events extends EventData {
): void;
/**
* @deprecated
*
* @param x
* @param y
@ -469,6 +455,7 @@ interface Events extends EventData {
): void;
/**
* @deprecated
*
* @param x
* @param y
@ -483,6 +470,7 @@ interface Events extends EventData {
): void;
/**
* @deprecated
*
* @param fromX
* @param fromY
@ -756,7 +744,11 @@ interface Events extends EventData {
* @example core.tryUseItem('pickaxe'); // 尝试使用破墙镐
* @param itemId id
*/
tryUseItem(itemId: ItemIdOf<'tools' | 'constants'>): void;
tryUseItem(
itemId: ItemIdOf<'tools' | 'constants'>,
noRoute?: boolean,
callback?: () => void
): void;
_sys_battle(data: Block, callback?: () => void): void;

View File

@ -50,6 +50,8 @@ import { getDetailedEnemy } from '../plugin/ui/fixed';
import { GameUi } from '@/core/main/custom/ui';
import { gameKey } from '@/core/main/init/hotkey';
import { mainUi } from '@/core/main/init/ui';
import { mainSetting } from '@/core/main/setting';
import { isMobile } from '@/plugin/use';
const props = defineProps<{
num: number;
@ -70,6 +72,14 @@ const drag = ref(false);
const detail = ref(false);
const selected = ref(0);
const settingScale = mainSetting.getValue('ui.bookScale', 100) / 100;
const scale = isMobile
? Math.max(settingScale * 15, 20)
: Math.max(
(window.innerWidth / window.innerHeight) * 15 * settingScale,
20
);
/**
* 选择怪物展示详细信息
* @param enemy 选择的怪物
@ -121,11 +131,13 @@ async function exit() {
const hold = mainUi.holdOn();
mainUi.close(props.num);
if (core.events.recoverEvents(core.status.event.interval)) {
hold.end(true);
return;
} else if (has(core.status.event.ui)) {
core.status.boxAnimateObjs = [];
// @ts-ignore
core.ui._drawViewMaps(core.status.event.ui);
hold.end(true);
} else hold.end();
}
@ -141,42 +153,44 @@ function checkScroll() {
}
//
gameKey.use(props.ui.symbol);
gameKey
.realize('@book_up', () => {
if (selected.value > 0) {
selected.value--;
}
checkScroll();
})
.realize('@book_down', () => {
if (selected.value < enemy.length - 1) {
selected.value++;
}
checkScroll();
})
.realize('@book_pageDown', () => {
if (selected.value <= 4) {
selected.value = 0;
} else {
selected.value -= 5;
}
checkScroll();
})
.realize('@book_pageUp', () => {
if (selected.value >= enemy.length - 5) {
selected.value = enemy.length - 1;
} else {
selected.value += 5;
}
checkScroll();
})
.realize('exit', () => {
exit();
})
.realize('confirm', () => {
select(toShow[selected.value], selected.value);
});
setTimeout(() => {
gameKey.use(props.ui.symbol);
gameKey
.realize('@book_up', () => {
if (selected.value > 0) {
selected.value--;
}
checkScroll();
})
.realize('@book_down', () => {
if (selected.value < enemy.length - 1) {
selected.value++;
}
checkScroll();
})
.realize('@book_pageDown', () => {
if (selected.value <= 4) {
selected.value = 0;
} else {
selected.value -= 5;
}
checkScroll();
})
.realize('@book_pageUp', () => {
if (selected.value >= enemy.length - 5) {
selected.value = enemy.length - 1;
} else {
selected.value += 5;
}
checkScroll();
})
.realize('exit', () => {
exit();
})
.realize('confirm', () => {
select(toShow[selected.value], selected.value);
});
}, 0);
onUnmounted(() => {
gameKey.dispose(props.ui.symbol);
@ -188,7 +202,6 @@ onUnmounted(() => {
user-select: none;
width: 80%;
height: 100%;
font-family: 'normal';
overflow: hidden;
transition: opacity 0.6s linear;
display: flex;
@ -208,25 +221,28 @@ onUnmounted(() => {
display: flex;
justify-content: center;
align-items: center;
font-family: 'normal';
}
.enemy {
display: flex;
flex-direction: column;
height: 20vh;
height: v-bind('scale + "vh"');
width: 100%;
padding: 0 1% 0 1%;
}
@media screen and (max-width: 600px) {
#tools {
transform: translateY(-50%);
}
#book {
width: 100%;
padding: 5%;
}
.enemy {
height: 15vh;
height: v-bind('scale * 2 / 3 + "vh"');
}
}
</style>

View File

@ -1,6 +1,11 @@
<!-- 怪物详细信息 -->
<template>
<div id="detail">
<div id="tools">
<span id="back" class="button-text tools" @click="close"
><left-outlined />返回</span
>
</div>
<div id="info" :style="{ top: `${top}px` }">
<EnemyOne :enemy="enemy!"></EnemyOne>
<a-divider
@ -172,7 +177,7 @@ onUnmounted(() => {
flex-direction: column;
align-items: center;
width: 72%;
height: 90%;
height: 100%;
transition: all 0.6s ease;
user-select: none;
}
@ -208,6 +213,15 @@ onUnmounted(() => {
opacity: 0;
}
#tools {
position: fixed;
height: 6%;
font-size: 3.2vh;
width: 100%;
left: 5%;
top: 5%;
}
@media screen and (max-width: 600px) {
#detail {
width: 100%;
@ -220,7 +234,11 @@ onUnmounted(() => {
font-size: 4vw;
bottom: 5%;
left: 5vw;
width: 90vw;
width: 80vw;
}
#info {
transform: translateY(10%);
}
}
</style>

View File

@ -701,14 +701,9 @@ onUnmounted(() => {
}
@media screen and (max-width: 600px) {
#equipbox {
padding: 5%;
}
#equipbox-main {
height: 90vh;
flex-direction: column-reverse;
font-size: 100%;
font-size: 225%;
}
#equip-now-div {
@ -721,7 +716,11 @@ onUnmounted(() => {
}
#equip-list {
flex-basis: 50%;
flex-basis: 45%;
#filter #sort-type {
font-size: 150%;
}
}
.divider {

View File

@ -56,15 +56,15 @@
:type="isMobile ? 'horizontal' : 'vertical'"
></a-divider>
<div id="fly-right">
<canvas id="fly-thumbnail" @click="fly"></canvas>
<canvas id="fly-thumbnail" @click="fly" @wheel="wheel"></canvas>
<div id="fly-tools">
<double-left-outlined
@click="changeFloorByDelta(-10)"
class="button-text"
class="button-text fly-button"
/>
<left-outlined
@click="changeFloorByDelta(-1)"
class="button-text"
class="button-text fly-button"
/>
<span
class="changable"
@ -74,11 +74,11 @@
>
<right-outlined
@click="changeFloorByDelta(1)"
class="button-text"
class="button-text fly-button"
/>
<double-right-outlined
@click="changeFloorByDelta(10)"
class="button-text"
class="button-text fly-button"
/>
</div>
</div>
@ -472,6 +472,10 @@ function click(e: MouseEvent) {
}
}
function wheel(ev: WheelEvent) {
changeFloorByDelta(-Math.sign(ev.deltaY));
}
function changeAreaByFloor(id: FloorIds) {
nowArea.value = Object.keys(area).find(v => area[v].includes(id))!;
}
@ -725,6 +729,7 @@ onUnmounted(() => {
flex-direction: column;
align-items: center;
justify-content: space-around;
position: relative;
}
#fly-tools {
@ -734,11 +739,17 @@ onUnmounted(() => {
flex-direction: row;
justify-content: space-around;
align-items: center;
position: absolute;
bottom: 0;
width: 100%;
background-color: #0004;
}
#fly-thumbnail {
width: 35vw;
height: 35vw;
max-height: 75vh;
max-width: 75vh;
border: 0.1vw solid #ddd4;
}
@ -766,10 +777,27 @@ onUnmounted(() => {
background-color: #ddd4;
}
.fly-button {
padding: 3%;
border-radius: 8px;
background-color: rgba(0, 0, 0, 0.301);
border: 1px dashed #ddda;
filter: drop-shadow(0px 0px 16px black);
}
#fly-now {
text-wrap: nowrap;
white-space: nowrap;
max-width: 50%;
text-overflow: ellipsis;
overflow: hidden;
text-shadow: 1px 1px 1px black, 1px -1px 1px black, -1px 1px 1px black,
-1px -1px 1px black;
}
@media screen and (max-width: 600px) {
#fly {
padding: 5%;
font-size: 100%;
font-size: 225%;
}
#fly-main {

View File

@ -242,7 +242,7 @@ onUnmounted(() => {
flex-direction: column;
width: 50%;
text-overflow: clip;
align-items: end;
align-items: flex-end;
text-align: end;
.hotkey-one-set-item {

View File

@ -108,7 +108,7 @@ function update() {
info.damage = enemy.enemy.calDamage().damage;
const critical = enemy.enemy.calCritical()[0];
info.critical = critical?.atkDelta ?? 0;
info.criticalDam = critical.delta ?? 0;
info.criticalDam = critical?.delta ?? 0;
info.defDamage = enemy.enemy.calDefDamage(ratio).delta;
}

View File

@ -46,15 +46,19 @@
</TransitionGroup>
</div>
<div class="setting-info">
<div
class="info-text"
v-html="splitText(display.at(-1)?.text ?? ['请选择设置'])"
></div>
<Scroll class="info-text-scroll">
<div
class="info-text"
v-html="
splitText(display.at(-1)?.text ?? ['请选择设置'])
"
></div>
</Scroll>
<a-divider class="info-divider" dashed></a-divider>
<div class="info-editor" v-if="!!selectedItem">
<div class="editor-custom">
<component
:is="selectedItem.controller"
:is="(selectedItem.controller as any)"
:item="selectedItem"
:displayer="displayer"
:setting="setting"
@ -68,16 +72,14 @@
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, shallowRef } from 'vue';
import { computed, onUnmounted, ref, shallowRef } from 'vue';
import {
mainSetting,
MotaSetting,
MotaSettingItem,
SettingDisplayer,
SettingDisplayInfo,
SettingText
SettingDisplayInfo
} from '../core/main/setting';
import settingText from '../data/settings.json';
import { RightOutlined, LeftOutlined } from '@ant-design/icons-vue';
import { splitText } from '../plugin/utils';
import Scroll from '../components/scroll.vue';
@ -88,13 +90,11 @@ import { mainUi } from '@/core/main/init/ui';
const props = defineProps<{
info?: MotaSetting;
text?: SettingText;
num: number;
ui: GameUi;
}>();
const setting = props.info ?? mainSetting;
const text = props.text ?? (settingText as SettingText);
const display = shallowRef<SettingDisplayInfo[]>([]);
const selectedItem = computed(() => display.value.at(-1)?.item);
const update = ref(false);
@ -312,6 +312,11 @@ onUnmounted(() => {
.info-text {
font-size: 85%;
min-height: 30%;
max-height: 50%;
}
.info-text-scroll {
max-height: 50%;
}
}
@ -321,7 +326,7 @@ onUnmounted(() => {
}
.setting-main {
font-size: 120%;
font-size: 225%;
.setting-container {
flex-direction: column;

View File

@ -460,7 +460,7 @@ onUnmounted(() => {
#shop {
width: 90vw;
padding-top: 5vh;
font-size: 100%;
font-size: 225%;
}
#item-list {

View File

@ -130,13 +130,18 @@
</template>
<script lang="ts" setup>
import { onMounted, ref, shallowReactive, watch } from 'vue';
import { onMounted, onUnmounted, ref, shallowReactive, watch } from 'vue';
import Box from '../components/box.vue';
import Scroll from '../components/scroll.vue';
import { status } from '../plugin/ui/statusBar';
import { isMobile } from '../plugin/use';
import { has } from '../plugin/utils';
import { fontSize } from '../plugin/ui/statusBar';
import { has } from '@/plugin/utils';
let main: HTMLDivElement;
const items = core.flags.statusBarItems;
const icons = core.statusBar.icons;
const skillTree = Mota.Plugin.require('skillTree_g');
const width = ref(
@ -148,6 +153,7 @@ const format = core.formatBigNumber;
watch(width, n => (updateStatus.value = !updateStatus.value));
watch(height, n => (updateStatus.value = !updateStatus.value));
watch(fontSize, n => (main.style.fontSize = `${isMobile ? n * 1.5 : n}%`));
const hero = shallowReactive<Partial<HeroStatus>>({});
const keys = shallowReactive<number[]>([]);
@ -230,8 +236,24 @@ function viewMap() {
function openStudy() {}
function resize() {
requestAnimationFrame(() => {
main.style.fontSize = `${
isMobile ? fontSize.value * 1.5 : fontSize.value
}%`;
});
}
onMounted(() => {
update();
main = document.getElementById('status-main') as HTMLDivElement;
window.addEventListener('resize', resize);
resize();
});
onUnmounted(() => {
window.removeEventListener('resize', resize);
});
</script>
@ -241,14 +263,16 @@ onMounted(() => {
width: 100%;
height: 100%;
padding: 1vh 0;
font-size: v-bind(fontSize);
}
.status-item {
position: relative;
display: flex;
flex-direction: row;
max-width: 17.5vw;
font-size: 200%;
width: 100%;
margin-bottom: 1vh;
margin-bottom: 14px;
text-shadow: 3px 2px 3px #000, 0px 0px 3px #111;
display: flex;
flex-direction: row;
@ -266,6 +290,10 @@ onMounted(() => {
margin-left: 10%;
}
.status-value {
transform: translateY(2px);
}
#status-header {
width: 100%;
display: flex;

View File

@ -3,41 +3,64 @@
<span class="button-text" @click="exit"><left-outlined /> 返回</span>
</div>
<div id="tool-editor">
<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-left">
<Scroll class="tool-list-scroll">
<div id="tool-list">
<div
id="tool-add-div"
@click="addingTool = true"
v-if="!addingTool"
v-for="(item, i) of list"
class="tool-list-item selectable"
:selected="i === selected"
@click="selected = i"
>
<PlusOutlined></PlusOutlined>&nbsp;&nbsp;
<span>新增工具栏</span>
<span>{{ item.id }}</span>
<a-button
type="danger"
class="tool-list-delete"
@click.stop="deleteTool(item.id)"
>删除</a-button
>
</div>
<div v-else>
<a-input
style="height: 100%; font-size: 80%; width: 100%"
v-model:value="addingToolId"
@blur="addTool"
></a-input>
<div id="tool-list-add">
<div
id="tool-add-div"
@click="addingTool = true"
v-if="!addingTool"
>
<PlusOutlined></PlusOutlined>&nbsp;&nbsp;
<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>
</Scroll>
</div>
<a-divider
class="divider"
dashed
@ -151,8 +174,8 @@
</div>
</Scroll>
</div>
<a-divider dashed></a-divider>
<div id="tool-preview" v-if="!!bar">
<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
@ -181,12 +204,15 @@ import { isMobile } from '@/plugin/use';
import Scroll from '@/components/scroll.vue';
import { deleteWith, tip } from '@/plugin/utils';
import { Modal } from 'ant-design-vue';
import { mainSetting } from '@/core/main/setting';
const props = defineProps<{
ui: GameUi;
num: number;
}>();
const scale = mainSetting.getValue('ui.toolbarScale', 100) / 100;
const list = CustomToolbar.list;
const selected = ref(0);
@ -331,7 +357,13 @@ onUnmounted(() => {
.tool-list-scroll {
height: 100%;
width: 30%;
width: 100%;
}
#tool-left {
flex-basis: 30%;
display: flex;
height: 100%;
}
#tool-list {
@ -478,7 +510,7 @@ onUnmounted(() => {
height: 40%;
display: flex;
justify-content: center;
align-items: start;
align-items: flex-start;
#tool-preview-container {
width: 90%;
@ -491,9 +523,9 @@ onUnmounted(() => {
.tool-preview-item {
display: flex;
margin: 5px;
min-width: 50px;
height: 50px;
margin: v-bind('5 * scale + "px"');
min-width: v-bind('50 * scale + "px"');
height: v-bind('50 * scale + "px"');
background-color: #222;
border: 1.5px solid #ddd8;
justify-content: center;
@ -509,4 +541,45 @@ onUnmounted(() => {
.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>

View File

@ -28,6 +28,7 @@
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 {
@ -44,6 +45,7 @@ const props = defineProps<{
}>();
const bar = props.bar;
const scale = mainSetting.getValue('ui.toolbarScale', 100) / 100;
const box = reactive<BoxData>({
x: bar.x,
@ -80,7 +82,7 @@ onUnmounted(() => {
<style lang="less" scoped>
.toolbar-container {
background-color: #0009;
padding: 5px;
padding: v-bind('scale * 5 + "px"');
}
.toolbar {
@ -93,9 +95,9 @@ onUnmounted(() => {
.toolbar-item {
display: flex;
margin: 5px;
min-width: 50px;
height: 50px;
margin: v-bind('scale * 5 + "px"');
min-width: v-bind('scale * 50 + "px"');
height: v-bind('scale * 50 + "px"');
cursor: pointer;
background-color: #222;
border: 1.5px solid #ddd8;
@ -106,7 +108,7 @@ onUnmounted(() => {
.toolbar-item::v-deep(> *) {
height: 100%;
min-width: 50px;
min-width: v-bind('scale * 50 + "px"');
display: flex;
justify-content: center;
align-items: center;

View File

@ -182,9 +182,9 @@ function use(id: ShowItemIds) {
const hold = mainUi.holdOn();
exit();
nextTick(() => {
core.useItem(id, false, () => {
core.tryUseItem(id, false, () => {
if (mainUi.stack.length === 0) {
hold.end();
hold.end(core.status.event.id !== 'toolbox');
}
});
});
@ -377,31 +377,23 @@ onUnmounted(() => {
align-items: center;
width: 100%;
height: 100%;
flex-basis: 50%;
#desc-text {
margin-top: 2vh;
margin-left: 0.5vw;
width: 100%;
height: 100%;
max-height: 100%;
}
}
}
@media screen and (max-width: 600px) {
#toolbox {
padding: 5%;
}
#tools {
span {
margin: 0;
}
}
#toolbox-main {
flex-direction: column-reverse;
height: 100%;
font-size: 100%;
height: 90%;
font-size: 225%;
margin-top: 10%;
}
.item-list {
@ -418,5 +410,16 @@ onUnmounted(() => {
display: flex;
flex-direction: column-reverse;
}
#detail {
flex-basis: 30%;
#desc {
#desc-text {
max-height: 10vh;
height: 10vh;
}
}
}
}
</style>