mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-10-08 20:01:47 +08:00
chore: 删除样板不需要的 UI
This commit is contained in:
parent
6c75fa507c
commit
fbecb5f8e4
@ -1,6 +1,5 @@
|
|||||||
import { KeyCode } from '@motajs/client-base';
|
import { KeyCode } from '@motajs/client-base';
|
||||||
import { gameKey, HotkeyJSON } from '@motajs/system-action';
|
import { gameKey, HotkeyJSON } from '@motajs/system-action';
|
||||||
import { hovered, mainUi, openDanmakuPoster } from '@motajs/legacy-ui';
|
|
||||||
import { GameStorage } from '@motajs/legacy-system';
|
import { GameStorage } from '@motajs/legacy-system';
|
||||||
|
|
||||||
export const mainScope = Symbol.for('@key_main');
|
export const mainScope = Symbol.for('@key_main');
|
||||||
@ -92,16 +91,6 @@ gameKey
|
|||||||
name: '浏览地图_2',
|
name: '浏览地图_2',
|
||||||
defaults: KeyCode.PageDown
|
defaults: KeyCode.PageDown
|
||||||
})
|
})
|
||||||
.register({
|
|
||||||
id: 'skillTree',
|
|
||||||
name: '技能树',
|
|
||||||
defaults: KeyCode.KeyJ
|
|
||||||
})
|
|
||||||
.register({
|
|
||||||
id: 'desc',
|
|
||||||
name: '百科全书',
|
|
||||||
defaults: KeyCode.KeyH
|
|
||||||
})
|
|
||||||
//#region 功能按键
|
//#region 功能按键
|
||||||
.group('function', '功能按键')
|
.group('function', '功能按键')
|
||||||
.register({
|
.register({
|
||||||
@ -139,27 +128,6 @@ gameKey
|
|||||||
name: '轻按_2',
|
name: '轻按_2',
|
||||||
defaults: KeyCode.Digit7
|
defaults: KeyCode.Digit7
|
||||||
})
|
})
|
||||||
.register({
|
|
||||||
id: 'mark',
|
|
||||||
name: '标记怪物',
|
|
||||||
defaults: KeyCode.KeyM
|
|
||||||
})
|
|
||||||
.register({
|
|
||||||
id: 'special',
|
|
||||||
name: '鼠标位置怪物属性',
|
|
||||||
defaults: KeyCode.KeyE
|
|
||||||
})
|
|
||||||
.register({
|
|
||||||
id: 'critical',
|
|
||||||
name: '鼠标位置怪物临界',
|
|
||||||
defaults: KeyCode.KeyC
|
|
||||||
})
|
|
||||||
.register({
|
|
||||||
id: 'danmaku',
|
|
||||||
name: '发送弹幕',
|
|
||||||
defaults: KeyCode.KeyA,
|
|
||||||
ctrl: true
|
|
||||||
})
|
|
||||||
.register({
|
.register({
|
||||||
id: 'quickEquip_1',
|
id: 'quickEquip_1',
|
||||||
name: '切换/保存套装_1',
|
name: '切换/保存套装_1',
|
||||||
@ -524,12 +492,6 @@ gameKey
|
|||||||
.realize('shop', () => {
|
.realize('shop', () => {
|
||||||
core.openQuickShop(true);
|
core.openQuickShop(true);
|
||||||
})
|
})
|
||||||
.realize('skillTree', () => {
|
|
||||||
core.useItem('skill1', true);
|
|
||||||
})
|
|
||||||
.realize('desc', () => {
|
|
||||||
core.useItem('I560', true);
|
|
||||||
})
|
|
||||||
.realize('undo', () => {
|
.realize('undo', () => {
|
||||||
core.doSL('autoSave', 'load');
|
core.doSL('autoSave', 'load');
|
||||||
})
|
})
|
||||||
@ -542,31 +504,6 @@ gameKey
|
|||||||
.realize('getNext', () => {
|
.realize('getNext', () => {
|
||||||
core.getNextItem();
|
core.getNextItem();
|
||||||
})
|
})
|
||||||
.realize('mark', () => {
|
|
||||||
const cls = hovered?.event.cls;
|
|
||||||
if (cls === 'enemys' || cls === 'enemy48') {
|
|
||||||
// const id = hovered!.event.id as EnemyIds;
|
|
||||||
// if (hasMarkedEnemy(id)) unmarkEnemy(id);
|
|
||||||
// else markEnemy(id);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.realize('special', () => {
|
|
||||||
if (hovered) {
|
|
||||||
const { x, y } = hovered;
|
|
||||||
const enemy = core.status.thisMap.enemy.get(x, y);
|
|
||||||
if (enemy) mainUi.open('fixedDetail', { panel: 'special' });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.realize('critical', () => {
|
|
||||||
if (hovered) {
|
|
||||||
const { x, y } = hovered;
|
|
||||||
const enemy = core.status.thisMap.enemy.get(x, y);
|
|
||||||
if (enemy) mainUi.open('fixedDetail', { panel: 'critical' });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.realize('danmaku', () => {
|
|
||||||
openDanmakuPoster();
|
|
||||||
})
|
|
||||||
.realize('restart', () => {
|
.realize('restart', () => {
|
||||||
core.confirmRestart();
|
core.confirmRestart();
|
||||||
})
|
})
|
||||||
|
@ -17,7 +17,8 @@ export function patchAudio() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
patch.add('playBgm', function (bgm, startTime) {
|
patch.add('playBgm', function (bgm, startTime) {
|
||||||
play(bgm, startTime);
|
const name = core.getMappedName(bgm) as BgmIds;
|
||||||
|
play(name, startTime);
|
||||||
});
|
});
|
||||||
patch.add('pauseBgm', function () {
|
patch.add('pauseBgm', function () {
|
||||||
pause();
|
pause();
|
||||||
|
@ -13,12 +13,13 @@ import { createWeather } from './weather';
|
|||||||
export function createGameRenderer() {
|
export function createGameRenderer() {
|
||||||
const App = defineComponent(_props => {
|
const App = defineComponent(_props => {
|
||||||
return () => (
|
return () => (
|
||||||
<container width={MAIN_WIDTH} height={MAIN_HEIGHT}>
|
<container noanti width={MAIN_WIDTH} height={MAIN_HEIGHT}>
|
||||||
{sceneController.render()}
|
{sceneController.render()}
|
||||||
</container>
|
</container>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mainRenderer.setAntiAliasing(false);
|
||||||
mainRenderer.hide();
|
mainRenderer.hide();
|
||||||
createApp(App).mount(mainRenderer);
|
createApp(App).mount(mainRenderer);
|
||||||
|
|
||||||
|
@ -1,140 +0,0 @@
|
|||||||
import { logger } from '@motajs/common';
|
|
||||||
import { MotaOffscreenCanvas2D } from '@motajs/render';
|
|
||||||
import { mainSetting } from '@motajs/legacy-ui';
|
|
||||||
import { Sprite, Transform } from '@motajs/render';
|
|
||||||
import { gameListener, hook } from '@user/data-base';
|
|
||||||
import {
|
|
||||||
ILayerGroupRenderExtends,
|
|
||||||
LayerGroup,
|
|
||||||
LayerGroupFloorBinder
|
|
||||||
} from '../elements';
|
|
||||||
|
|
||||||
export class LayerGroupHalo implements ILayerGroupRenderExtends {
|
|
||||||
id: string = 'halo';
|
|
||||||
|
|
||||||
group!: LayerGroup;
|
|
||||||
binder!: LayerGroupFloorBinder;
|
|
||||||
halo!: Halo;
|
|
||||||
|
|
||||||
static sprites: Set<Halo> = new Set();
|
|
||||||
|
|
||||||
awake(group: LayerGroup): void {
|
|
||||||
this.group = group;
|
|
||||||
const ex = group.getExtends('floor-binder');
|
|
||||||
if (ex instanceof LayerGroupFloorBinder) {
|
|
||||||
this.binder = ex;
|
|
||||||
this.halo = new Halo();
|
|
||||||
this.halo.setHD(true);
|
|
||||||
this.halo.size(group.width, group.height);
|
|
||||||
this.halo.setZIndex(75);
|
|
||||||
this.halo.binder = ex;
|
|
||||||
group.appendChild(this.halo);
|
|
||||||
LayerGroupHalo.sprites.add(this.halo);
|
|
||||||
} else {
|
|
||||||
logger.error(1401);
|
|
||||||
group.removeExtends('halo');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onDestroy(_group: LayerGroup): void {
|
|
||||||
this.halo?.destroy();
|
|
||||||
LayerGroupHalo.sprites.delete(this.halo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const haloColor: Record<number, string[]> = {
|
|
||||||
21: ['cyan'],
|
|
||||||
25: ['purple'],
|
|
||||||
26: ['blue'],
|
|
||||||
27: ['red'],
|
|
||||||
31: ['#3CFF49'],
|
|
||||||
29: ['#51E9FF'],
|
|
||||||
32: ['#fff966']
|
|
||||||
};
|
|
||||||
|
|
||||||
class Halo extends Sprite {
|
|
||||||
/** 单元格大小 */
|
|
||||||
cellSize: number = 32;
|
|
||||||
/** 当前楼层,用于获取有哪些光环 */
|
|
||||||
binder!: LayerGroupFloorBinder;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super('static', true);
|
|
||||||
|
|
||||||
this.setRenderFn((canvas, transform) => {
|
|
||||||
this.drawHalo(canvas, transform);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
drawHalo(canvas: MotaOffscreenCanvas2D, _transform: Transform) {
|
|
||||||
if (!mainSetting.getValue('screen.halo', true)) return;
|
|
||||||
const floorId = this.binder.getFloor();
|
|
||||||
if (!floorId) return;
|
|
||||||
const col = core.status.maps[floorId].enemy;
|
|
||||||
if (!col) return;
|
|
||||||
const [dx, dy] = col.translation;
|
|
||||||
const list = col.haloList.concat(
|
|
||||||
Object.keys(flags[`melt_${floorId}`] ?? {}).map(v => {
|
|
||||||
const [x, y] = v.split(',').map(v => parseInt(v));
|
|
||||||
return {
|
|
||||||
type: 'square',
|
|
||||||
data: {
|
|
||||||
x: x + dx,
|
|
||||||
y: y + dy,
|
|
||||||
d: 3
|
|
||||||
},
|
|
||||||
special: 25
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const { ctx } = canvas;
|
|
||||||
const cell = this.cellSize;
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
for (const halo of list) {
|
|
||||||
if (halo.type === 'square') {
|
|
||||||
const { x, y, d } = halo.data;
|
|
||||||
let [color, border] = haloColor[halo.special];
|
|
||||||
let alpha = 0.1;
|
|
||||||
let borderAlpha = 0.6;
|
|
||||||
const { mouseX, mouseY } = gameListener;
|
|
||||||
if (mouseX === halo.from?.x && mouseY === halo.from?.y) {
|
|
||||||
alpha = 0.3;
|
|
||||||
borderAlpha = 0.8;
|
|
||||||
color = '#ff0';
|
|
||||||
border = '#ff0';
|
|
||||||
}
|
|
||||||
const r = Math.floor(d / 2);
|
|
||||||
const left = x - r;
|
|
||||||
const top = y - r;
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
ctx.strokeStyle = border ?? color;
|
|
||||||
ctx.globalAlpha = alpha;
|
|
||||||
ctx.fillRect(left * cell, top * cell, d * cell, d * cell);
|
|
||||||
ctx.globalAlpha = borderAlpha;
|
|
||||||
ctx.strokeRect(left * cell, top * cell, d * cell, d * cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHalo(block: Block) {
|
|
||||||
if (block.event.cls === 'enemys' || block.event.cls === 'enemy48') {
|
|
||||||
LayerGroupHalo.sprites.forEach(v => {
|
|
||||||
const floor = v.binder.getFloor();
|
|
||||||
if (floor === core.status.floorId) {
|
|
||||||
v.update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hook.on('enemyExtract', col => {
|
|
||||||
LayerGroupHalo.sprites.forEach(v => {
|
|
||||||
const floor = v.binder.getFloor();
|
|
||||||
if (col.floorId === floor) {
|
|
||||||
v.update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
gameListener.on('hoverBlock', updateHalo);
|
|
||||||
gameListener.on('leaveBlock', updateHalo);
|
|
@ -38,7 +38,6 @@ import { ReplayingStatus } from './toolbar';
|
|||||||
import { getHeroStatusOn } from '@user/data-state';
|
import { getHeroStatusOn } from '@user/data-state';
|
||||||
import { hook } from '@user/data-base';
|
import { hook } from '@user/data-base';
|
||||||
import { FloorDamageExtends, FloorItemDetail } from '../elements';
|
import { FloorDamageExtends, FloorItemDetail } from '../elements';
|
||||||
import { LayerGroupHalo } from '../legacy/halo';
|
|
||||||
import { FloorChange } from '../legacy/fallback';
|
import { FloorChange } from '../legacy/fallback';
|
||||||
import { mainUIController } from './controller';
|
import { mainUIController } from './controller';
|
||||||
import {
|
import {
|
||||||
@ -57,7 +56,6 @@ const MainScene = defineComponent(() => {
|
|||||||
const layerGroupExtends: ILayerGroupRenderExtends[] = [
|
const layerGroupExtends: ILayerGroupRenderExtends[] = [
|
||||||
new FloorDamageExtends(),
|
new FloorDamageExtends(),
|
||||||
new FloorItemDetail(),
|
new FloorItemDetail(),
|
||||||
new LayerGroupHalo(),
|
|
||||||
new LayerGroupAnimate(),
|
new LayerGroupAnimate(),
|
||||||
new FloorViewport()
|
new FloorViewport()
|
||||||
];
|
];
|
||||||
@ -252,7 +250,12 @@ const MainScene = defineComponent(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<container id="main-scene" width={MAIN_WIDTH} height={MAIN_HEIGHT}>
|
<container
|
||||||
|
id="main-scene"
|
||||||
|
width={MAIN_WIDTH}
|
||||||
|
height={MAIN_HEIGHT}
|
||||||
|
noanti
|
||||||
|
>
|
||||||
<LeftStatusBar
|
<LeftStatusBar
|
||||||
loc={[0, 0, STATUS_BAR_WIDTH, STATUS_BAR_HEIGHT]}
|
loc={[0, 0, STATUS_BAR_WIDTH, STATUS_BAR_HEIGHT]}
|
||||||
status={leftStatus}
|
status={leftStatus}
|
||||||
@ -269,6 +272,7 @@ const MainScene = defineComponent(() => {
|
|||||||
onClick={clickMap}
|
onClick={clickMap}
|
||||||
onDown={downMap}
|
onDown={downMap}
|
||||||
onMove={moveMap}
|
onMove={moveMap}
|
||||||
|
noanti
|
||||||
>
|
>
|
||||||
<layer-group id="layer-main" ex={layerGroupExtends} ref={map}>
|
<layer-group id="layer-main" ex={layerGroupExtends} ref={map}>
|
||||||
<layer layer="bg" zIndex={10}></layer>
|
<layer layer="bg" zIndex={10}></layer>
|
||||||
|
@ -127,7 +127,7 @@ export const MainSettings = defineComponent<MainSettingsProps>(props => {
|
|||||||
choices={choices}
|
choices={choices}
|
||||||
width={POP_BOX_WIDTH}
|
width={POP_BOX_WIDTH}
|
||||||
onChoose={choose}
|
onChoose={choose}
|
||||||
maxHeight={MAIN_HEIGHT - 64}
|
maxHeight={MAIN_HEIGHT - 32}
|
||||||
interval={8}
|
interval={8}
|
||||||
scope={scope}
|
scope={scope}
|
||||||
/>
|
/>
|
||||||
|
@ -31,7 +31,6 @@ import {
|
|||||||
LayerGroup,
|
LayerGroup,
|
||||||
LayerGroupFloorBinder
|
LayerGroupFloorBinder
|
||||||
} from '../elements';
|
} from '../elements';
|
||||||
import { LayerGroupHalo } from '../legacy/halo';
|
|
||||||
import { Font } from '@motajs/render-style';
|
import { Font } from '@motajs/render-style';
|
||||||
import { clamp, mean } from 'lodash-es';
|
import { clamp, mean } from 'lodash-es';
|
||||||
import { calculateStatisticsOne, StatisticsDataOneFloor } from './statistics';
|
import { calculateStatisticsOne, StatisticsDataOneFloor } from './statistics';
|
||||||
@ -65,7 +64,6 @@ export const ViewMap = defineComponent<ViewMapProps>(props => {
|
|||||||
const layerGroupExtends: ILayerGroupRenderExtends[] = [
|
const layerGroupExtends: ILayerGroupRenderExtends[] = [
|
||||||
new FloorDamageExtends(),
|
new FloorDamageExtends(),
|
||||||
new FloorItemDetail(),
|
new FloorItemDetail(),
|
||||||
new LayerGroupHalo(),
|
|
||||||
new LayerGroupAnimate()
|
new LayerGroupAnimate()
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -2,5 +2,4 @@ export { default as Box } from './box.vue';
|
|||||||
export { default as BoxAnimate } from './boxAnimate.vue';
|
export { default as BoxAnimate } from './boxAnimate.vue';
|
||||||
export { default as Column } from './colomn.vue';
|
export { default as Column } from './colomn.vue';
|
||||||
export { default as EnemyOne } from './enemyOne.vue';
|
export { default as EnemyOne } from './enemyOne.vue';
|
||||||
export { default as Minimap } from './minimap.vue';
|
|
||||||
export { default as Scroll } from './scroll.vue';
|
export { default as Scroll } from './scroll.vue';
|
||||||
|
@ -1,176 +0,0 @@
|
|||||||
<template>
|
|
||||||
<canvas :id="`minimap-${id}`"></canvas>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { onMounted, onUnmounted } from 'vue';
|
|
||||||
import { MinimapDrawer, getArea } from '../tools/fly';
|
|
||||||
import { useDrag, useWheel, requireUniqueSymbol } from '../use';
|
|
||||||
import { debounce } from 'lodash-es';
|
|
||||||
import { mainSetting } from '../preset/settingIns';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
action?: boolean;
|
|
||||||
scale?: number;
|
|
||||||
noBorder?: boolean;
|
|
||||||
showInfo?: boolean;
|
|
||||||
autoLocate?: boolean;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const area = getArea();
|
|
||||||
const id = requireUniqueSymbol().toFixed(0);
|
|
||||||
const setting = mainSetting.getSetting('ui.mapLazy')!;
|
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement;
|
|
||||||
let drawer: MinimapDrawer;
|
|
||||||
|
|
||||||
function onChange(floor: FloorIds) {
|
|
||||||
drawer.nowFloor = floor;
|
|
||||||
drawer.nowArea =
|
|
||||||
Object.keys(area).find(v => area[v].includes(core.status.floorId)) ??
|
|
||||||
'';
|
|
||||||
drawer.drawedThumbnail = {};
|
|
||||||
if (props.autoLocate) {
|
|
||||||
drawer.locateMap(drawer.nowFloor);
|
|
||||||
}
|
|
||||||
drawer.drawMap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let scale = props.scale ?? 3;
|
|
||||||
|
|
||||||
let lastX = 0;
|
|
||||||
let lastY = 0;
|
|
||||||
|
|
||||||
let moved = false;
|
|
||||||
let startX = 0;
|
|
||||||
let startY = 0;
|
|
||||||
|
|
||||||
let touchScale = false;
|
|
||||||
let lastDis = 0;
|
|
||||||
|
|
||||||
let lastScale = scale;
|
|
||||||
const changeScale = debounce((s: number) => {
|
|
||||||
canvas.style.transform = '';
|
|
||||||
drawer.drawedThumbnail = {};
|
|
||||||
drawer.scale = s;
|
|
||||||
drawer.drawMap();
|
|
||||||
lastScale = s;
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
function resize(delta: number) {
|
|
||||||
drawer.ox *= delta;
|
|
||||||
drawer.oy *= delta;
|
|
||||||
scale = delta * scale;
|
|
||||||
changeScale(scale);
|
|
||||||
canvas.style.transform = `scale(${scale / lastScale})`;
|
|
||||||
drawer.thumbnailLoc = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function drag(x: number, y: number) {
|
|
||||||
if (touchScale) return;
|
|
||||||
const dx = x - lastX;
|
|
||||||
const dy = y - lastY;
|
|
||||||
drawer.ox += dx;
|
|
||||||
drawer.oy += dy;
|
|
||||||
lastX = x;
|
|
||||||
lastY = y;
|
|
||||||
drawer.checkMoveThumbnail();
|
|
||||||
drawer.drawToTarget();
|
|
||||||
if (Math.abs(x - startX) > 10 || Math.abs(y - startY) > 10) moved = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function touchdown(e: TouchEvent) {
|
|
||||||
if (e.touches.length >= 2) {
|
|
||||||
touchScale = true;
|
|
||||||
lastDis = Math.sqrt(
|
|
||||||
(e.touches[0].clientX - e.touches[1].clientX) ** 2 +
|
|
||||||
(e.touches[0].clientY - e.touches[1].clientY) ** 2
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function touchup(e: TouchEvent) {
|
|
||||||
if (e.touches.length < 2) touchScale = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function touchmove(e: TouchEvent) {
|
|
||||||
if (!touchScale) return;
|
|
||||||
const dis = Math.sqrt(
|
|
||||||
(e.touches[0].clientX - e.touches[1].clientX) ** 2 +
|
|
||||||
(e.touches[0].clientY - e.touches[1].clientY) ** 2
|
|
||||||
);
|
|
||||||
resize(dis / lastDis);
|
|
||||||
lastDis = dis;
|
|
||||||
}
|
|
||||||
|
|
||||||
function afterBattle() {
|
|
||||||
if (!setting.value) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
drawer.drawedThumbnail = {};
|
|
||||||
drawer.drawMap();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const width = props.width ?? 300;
|
|
||||||
const height = props.height ?? 300;
|
|
||||||
canvas = document.getElementById(`minimap-${id}`) as HTMLCanvasElement;
|
|
||||||
canvas.style.width = `${width}px`;
|
|
||||||
canvas.style.height = `${height}px`;
|
|
||||||
canvas.width = width * devicePixelRatio;
|
|
||||||
canvas.height = height * devicePixelRatio;
|
|
||||||
drawer = new MinimapDrawer(canvas);
|
|
||||||
|
|
||||||
drawer.scale = props.scale ?? 3;
|
|
||||||
drawer.noBorder = props.noBorder ?? false;
|
|
||||||
drawer.showInfo = props.showInfo ?? false;
|
|
||||||
|
|
||||||
if (props.autoLocate) {
|
|
||||||
drawer.locateMap(drawer.nowFloor);
|
|
||||||
}
|
|
||||||
drawer.drawMap();
|
|
||||||
|
|
||||||
const { hook } = Mota.require('@user/data-base');
|
|
||||||
hook.on('afterChangeFloor', onChange);
|
|
||||||
hook.on('afterBattle', afterBattle);
|
|
||||||
|
|
||||||
if (props.action) {
|
|
||||||
useDrag(
|
|
||||||
canvas,
|
|
||||||
drag,
|
|
||||||
(x, y) => {
|
|
||||||
lastX = x;
|
|
||||||
lastY = y;
|
|
||||||
startX = x;
|
|
||||||
startY = y;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
moved = false;
|
|
||||||
}, 50);
|
|
||||||
},
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
useWheel(canvas, (x, y) => {
|
|
||||||
const delta = -Math.sign(y) * 0.1 + 1;
|
|
||||||
resize(delta);
|
|
||||||
});
|
|
||||||
|
|
||||||
canvas.addEventListener('touchstart', touchdown);
|
|
||||||
canvas.addEventListener('touchend', touchup);
|
|
||||||
canvas.addEventListener('touchmove', touchmove);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
const { hook } = Mota.require('@user/data-base');
|
|
||||||
hook.off('afterChangeFloor', onChange);
|
|
||||||
hook.off('afterBattle', afterBattle);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
|
@ -1,488 +0,0 @@
|
|||||||
import { EventEmitter } from 'eventemitter3';
|
|
||||||
import { logger } from '@motajs/common';
|
|
||||||
import { ResponseBase } from '@motajs/client-base';
|
|
||||||
import axios, { AxiosResponse, toFormData } from 'axios';
|
|
||||||
import { VNode, h, shallowReactive } from 'vue';
|
|
||||||
import { ensureArray, parseCss } from './utils';
|
|
||||||
import { deleteWith } from '@motajs/legacy-common';
|
|
||||||
import { tip } from './use';
|
|
||||||
// /* @__PURE__ */ import { id, password } from '../../../../user';
|
|
||||||
|
|
||||||
type CSSObj = Partial<Record<CanParseCss, string>>;
|
|
||||||
|
|
||||||
interface DanmakuResponse extends ResponseBase {
|
|
||||||
total: number;
|
|
||||||
list: DanmakuInfo[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DanmakuInfo {
|
|
||||||
id: string;
|
|
||||||
comment: string;
|
|
||||||
tags: string;
|
|
||||||
love: string;
|
|
||||||
my_love_type: boolean;
|
|
||||||
userid: string;
|
|
||||||
deler: string;
|
|
||||||
upload_time: string;
|
|
||||||
tower_name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DanmakuPostInfo extends Partial<DanmakuContentInfo> {
|
|
||||||
type: 1 | 2 | 3;
|
|
||||||
towername: 'HumanBreak';
|
|
||||||
id?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DanmakuContentInfo {
|
|
||||||
comment: string;
|
|
||||||
tags: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PostDanmakuResponse extends ResponseBase {
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PostLikeResponse extends ResponseBase {
|
|
||||||
liked: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DanmakuEvent {
|
|
||||||
showStart: [danmaku: Danmaku];
|
|
||||||
showEnd: [danmaku: Danmaku];
|
|
||||||
like: [liked: boolean, danmaku: Danmaku];
|
|
||||||
}
|
|
||||||
|
|
||||||
type SpecContentFn = (content: string, type: string) => VNode;
|
|
||||||
|
|
||||||
interface AllowedCSS {
|
|
||||||
property: string;
|
|
||||||
check: (value: string, prop: string) => true | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowedCSS: Partial<Record<CanParseCss, AllowedCSS>> = {
|
|
||||||
color: {
|
|
||||||
property: 'color',
|
|
||||||
check: () => true
|
|
||||||
},
|
|
||||||
backgroundColor: {
|
|
||||||
property: 'backgroundColor',
|
|
||||||
check: () => true
|
|
||||||
},
|
|
||||||
fontSize: {
|
|
||||||
property: 'fontSize',
|
|
||||||
check: value => {
|
|
||||||
if (!/^\d+%$/.test(value)) {
|
|
||||||
return '字体大小只能设置为百分格式';
|
|
||||||
}
|
|
||||||
if (parseInt(value) > 200) {
|
|
||||||
return '字体最大只能为200%';
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Danmaku extends EventEmitter<DanmakuEvent> {
|
|
||||||
static backend: string = `/backend/tower/barrage.php`;
|
|
||||||
static all: Set<Danmaku> = new Set();
|
|
||||||
static allInPos: Partial<Record<FloorIds, Record<LocString, Danmaku[]>>> =
|
|
||||||
{};
|
|
||||||
|
|
||||||
static showList: Danmaku[] = shallowReactive([]);
|
|
||||||
static showMap: Map<number, Danmaku> = new Map();
|
|
||||||
static specList: Record<string, SpecContentFn> = {};
|
|
||||||
|
|
||||||
static lastEditoredDanmaku?: Danmaku;
|
|
||||||
|
|
||||||
id: number = -1;
|
|
||||||
text: string = '';
|
|
||||||
x: number = 0;
|
|
||||||
y: number = 0;
|
|
||||||
floor?: FloorIds;
|
|
||||||
showing: boolean = false;
|
|
||||||
likedNum: number = 0;
|
|
||||||
liked: boolean = false;
|
|
||||||
|
|
||||||
style: CSSObj = {};
|
|
||||||
textColor: string = 'white';
|
|
||||||
strokeColor: string = 'black';
|
|
||||||
|
|
||||||
private posted: boolean = false;
|
|
||||||
vNode?: VNode;
|
|
||||||
private posting: boolean = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送弹幕
|
|
||||||
* @returns 弹幕发送的 Axios Post 信息,为 Promise
|
|
||||||
*/
|
|
||||||
async post(): Promise<AxiosResponse<PostDanmakuResponse>> {
|
|
||||||
if (this.posted || this.posting) {
|
|
||||||
logger.warn(5);
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: DanmakuPostInfo = {
|
|
||||||
type: 2,
|
|
||||||
towername: 'HumanBreak',
|
|
||||||
...this.encode()
|
|
||||||
};
|
|
||||||
|
|
||||||
this.posting = true;
|
|
||||||
const form = toFormData(data);
|
|
||||||
// /* @__PURE__ */ form.append('userid', id);
|
|
||||||
// /* @__PURE__ */ form.append('password', password);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await axios.post<PostDanmakuResponse>(
|
|
||||||
Danmaku.backend,
|
|
||||||
form
|
|
||||||
);
|
|
||||||
|
|
||||||
this.id = res.data.id;
|
|
||||||
this.posting = false;
|
|
||||||
|
|
||||||
if (res.data.code === 0) {
|
|
||||||
this.posted = true;
|
|
||||||
tip('success', '发送成功');
|
|
||||||
this.addToList();
|
|
||||||
} else {
|
|
||||||
tip('error', res.data.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
|
||||||
this.posted = false;
|
|
||||||
this.posting = false;
|
|
||||||
logger.error(1, String(e));
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将弹幕整合为可以发送的格式
|
|
||||||
*/
|
|
||||||
encode(): DanmakuContentInfo {
|
|
||||||
const css = this.getEncodedCSS();
|
|
||||||
return {
|
|
||||||
comment: this.text,
|
|
||||||
tags: JSON.stringify([
|
|
||||||
`!css:${JSON.stringify(css)}`,
|
|
||||||
`!pos:${this.x},${this.y},${this.floor}`
|
|
||||||
])
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析弹幕信息
|
|
||||||
* @param info 要被解析的弹幕信息
|
|
||||||
*/
|
|
||||||
decode(info: DanmakuContentInfo) {
|
|
||||||
this.text = info.comment;
|
|
||||||
|
|
||||||
ensureArray(JSON.parse(info.tags) as string[]).forEach(v => {
|
|
||||||
if (v.startsWith('!css:')) {
|
|
||||||
this.style = JSON.parse(v.slice(5));
|
|
||||||
} else if (v.startsWith('!pos:')) {
|
|
||||||
const [x, y, f] = v.slice(5).split(',');
|
|
||||||
this.x = parseInt(x);
|
|
||||||
this.y = parseInt(y);
|
|
||||||
this.floor = f as FloorIds;
|
|
||||||
} else {
|
|
||||||
logger.warn(3, v);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getEncodedCSS() {
|
|
||||||
const css = JSON.parse(JSON.stringify(this.style)) as CSSObj;
|
|
||||||
if (!css.color) css.color = this.textColor;
|
|
||||||
if (!css.textShadow)
|
|
||||||
css.textShadow = `1px 1px 1px ${this.strokeColor}, 1px -1px 1px ${this.strokeColor}, -1px 1px 1px ${this.strokeColor}, -1px -1px 1px ${this.strokeColor}`;
|
|
||||||
return { ...css, ...this.style };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置文字的颜色
|
|
||||||
* @param fill 填充颜色
|
|
||||||
* @param stroke 描边颜色
|
|
||||||
*/
|
|
||||||
color(fill?: string, stroke?: string) {
|
|
||||||
if (fill) this.textColor = fill;
|
|
||||||
if (stroke) this.strokeColor = stroke;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加一个图标
|
|
||||||
* @param icon 要显示的图标id
|
|
||||||
*/
|
|
||||||
addIcon(icon: AllIds) {
|
|
||||||
this.text += `[i:${icon}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置这个弹幕整体的css信息
|
|
||||||
* @param str css字符串
|
|
||||||
* @param overwrite 是否完全覆写原来的css
|
|
||||||
*/
|
|
||||||
css(str: string, overwrite?: boolean): void;
|
|
||||||
/**
|
|
||||||
* 设置这个弹幕整体的css信息
|
|
||||||
* @param str css对象,参考 CSSStyleDeclaration
|
|
||||||
* @param overwrite 是否完全覆写原来的css
|
|
||||||
*/
|
|
||||||
css(obj: CSSObj, overwrite?: boolean): void;
|
|
||||||
css(obj: string | CSSObj, overwrite: boolean = false) {
|
|
||||||
const res = typeof obj === 'string' ? parseCss(obj) : obj;
|
|
||||||
const allow = Danmaku.checkCSSAllow(res);
|
|
||||||
if (allow.length === 0) {
|
|
||||||
if (overwrite) this.style = res;
|
|
||||||
else {
|
|
||||||
this.style = { ...this.style, ...res };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.error(8, allow.join(','));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将这个弹幕添加至弹幕列表
|
|
||||||
*/
|
|
||||||
addToList() {
|
|
||||||
Danmaku.all.add(this);
|
|
||||||
if (!this.floor) return;
|
|
||||||
Danmaku.allInPos[this.floor] ??= {};
|
|
||||||
Danmaku.allInPos[this.floor]![`${this.x},${this.y}`] ??= [];
|
|
||||||
Danmaku.allInPos[this.floor]![`${this.x},${this.y}`].push(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析这个弹幕为 VNode
|
|
||||||
*/
|
|
||||||
parse() {
|
|
||||||
let pointer = -1;
|
|
||||||
let ignore = false;
|
|
||||||
|
|
||||||
let str = '';
|
|
||||||
|
|
||||||
let spec = false;
|
|
||||||
let specType = '';
|
|
||||||
let specTypeEnd = false;
|
|
||||||
let specContent = '';
|
|
||||||
|
|
||||||
const children: VNode[] = [];
|
|
||||||
|
|
||||||
while (++pointer < this.text.length) {
|
|
||||||
const char = this.text[pointer];
|
|
||||||
|
|
||||||
if (char === '\\' && !ignore) {
|
|
||||||
ignore = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ignore) {
|
|
||||||
str += char;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === '[') {
|
|
||||||
spec = true;
|
|
||||||
children.push(h('span', str));
|
|
||||||
str = '';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char === ']') {
|
|
||||||
if (!spec) {
|
|
||||||
logger.warn(4);
|
|
||||||
str += char;
|
|
||||||
} else {
|
|
||||||
spec = false;
|
|
||||||
specTypeEnd = false;
|
|
||||||
children.push(this.createSpecVNode(specType, specContent));
|
|
||||||
specType = '';
|
|
||||||
specContent = '';
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spec) {
|
|
||||||
if (!specTypeEnd) {
|
|
||||||
if (char !== ':') {
|
|
||||||
specType += char;
|
|
||||||
} else {
|
|
||||||
specTypeEnd = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
specContent += char;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
str += char;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (str.length > 0) {
|
|
||||||
children.push(h('span', str));
|
|
||||||
}
|
|
||||||
|
|
||||||
return h(
|
|
||||||
'span',
|
|
||||||
{ class: 'danmaku', style: this.getEncodedCSS() },
|
|
||||||
children
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取本弹幕的VNode
|
|
||||||
*/
|
|
||||||
getVNode(nocache: boolean = false) {
|
|
||||||
if (nocache) return (this.vNode = this.parse());
|
|
||||||
return this.vNode ?? (this.vNode = this.parse());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示这个弹幕
|
|
||||||
*/
|
|
||||||
show() {
|
|
||||||
if (this.showing) return;
|
|
||||||
this.showing = true;
|
|
||||||
Danmaku.showList.push(this);
|
|
||||||
Danmaku.showMap.set(this.id, this);
|
|
||||||
this.emit('showStart', this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示结束这个弹幕
|
|
||||||
*/
|
|
||||||
showEnd() {
|
|
||||||
if (!this.showing) return;
|
|
||||||
this.showing = false;
|
|
||||||
deleteWith(Danmaku.showList, this);
|
|
||||||
Danmaku.showMap.delete(this.id);
|
|
||||||
this.emit('showEnd', this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 点赞或取消点赞
|
|
||||||
*/
|
|
||||||
async triggerLike() {
|
|
||||||
const post: DanmakuPostInfo = {
|
|
||||||
type: 3,
|
|
||||||
towername: 'HumanBreak',
|
|
||||||
id: this.id
|
|
||||||
};
|
|
||||||
|
|
||||||
const form = toFormData(post);
|
|
||||||
// /* @__PURE__ */ form.append('userid', id);
|
|
||||||
// /* @__PURE__ */ form.append('password', password);
|
|
||||||
|
|
||||||
const res = await axios.post<PostLikeResponse>(Danmaku.backend, form);
|
|
||||||
if (res.data.code !== 0) {
|
|
||||||
logger.warn(18, this.id.toString());
|
|
||||||
tip('error', `Error ${res.data.code}. ${res.data.message}`);
|
|
||||||
} else {
|
|
||||||
tip('success', res.data.message);
|
|
||||||
|
|
||||||
if (res.data.liked) {
|
|
||||||
this.liked = true;
|
|
||||||
this.likedNum++;
|
|
||||||
} else {
|
|
||||||
this.liked = false;
|
|
||||||
this.likedNum--;
|
|
||||||
}
|
|
||||||
this.emit('like', this.liked, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 销毁这个弹幕
|
|
||||||
*/
|
|
||||||
destroy() {
|
|
||||||
this.showEnd();
|
|
||||||
Danmaku.all.delete(this);
|
|
||||||
if (this.floor) {
|
|
||||||
const floor = Danmaku.allInPos[this.floor];
|
|
||||||
if (floor) {
|
|
||||||
delete floor[`${this.x},${this.y}`];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private createSpecVNode(type: string, content: string): VNode {
|
|
||||||
if (Danmaku.specList[type]) {
|
|
||||||
return Danmaku.specList[type](content, type);
|
|
||||||
} else {
|
|
||||||
logger.warn(7, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
return h('span');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查CSS内容是否符合发弹幕要求
|
|
||||||
* @param css 要检查的CSS内容
|
|
||||||
*/
|
|
||||||
static checkCSSAllow(css: CSSObj) {
|
|
||||||
const problem: string[] = [];
|
|
||||||
for (const [key, value] of Object.entries(css)) {
|
|
||||||
if (!allowedCSS[key as CanParseCss]) {
|
|
||||||
problem.push(`不允许的CSS:${key}`);
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
const res = allowedCSS[key as CanParseCss]!.check(value, key);
|
|
||||||
if (res !== true) {
|
|
||||||
problem.push(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return problem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 拉取本塔所有弹幕
|
|
||||||
*/
|
|
||||||
static async fetch() {
|
|
||||||
Danmaku.all.clear();
|
|
||||||
Danmaku.allInPos = {};
|
|
||||||
const form = toFormData({
|
|
||||||
type: 1,
|
|
||||||
towername: 'HumanBreak'
|
|
||||||
});
|
|
||||||
// /* @__PURE__ */ form.append('userid', id);
|
|
||||||
// /* @__PURE__ */ form.append('password', password);
|
|
||||||
const data = await axios.post<DanmakuResponse>(Danmaku.backend, form);
|
|
||||||
|
|
||||||
data.data.list.forEach(v => {
|
|
||||||
const dan = new Danmaku();
|
|
||||||
dan.id = parseInt(v.id);
|
|
||||||
dan.likedNum = parseInt(v.love);
|
|
||||||
dan.liked = v.my_love_type;
|
|
||||||
dan.decode(v);
|
|
||||||
dan.posted = true;
|
|
||||||
dan.addToList();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示一个弹幕
|
|
||||||
* @param dan 要显示的弹幕
|
|
||||||
*/
|
|
||||||
static show(dan: Danmaku) {
|
|
||||||
dan.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注册一个特殊显示内容
|
|
||||||
* @param type 特殊内容类型
|
|
||||||
* @param fn 特殊内容显示函数,返回VNode
|
|
||||||
*/
|
|
||||||
static registerSpecContent(type: string, fn: SpecContentFn) {
|
|
||||||
if (this.specList[type]) {
|
|
||||||
logger.warn(6, type);
|
|
||||||
}
|
|
||||||
this.specList[type] = fn;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
{
|
|
||||||
"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.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.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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,523 +0,0 @@
|
|||||||
{
|
|
||||||
"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>",
|
|
||||||
"注意,技能在点开之后是无法取消的,因此,加点时请慎重加点。注意,部分技能是必点技能,这些技能会在技能说明中明确指出,",
|
|
||||||
"这些技能一般需要尽早点出。"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"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。"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
{
|
|
||||||
"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}"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
[
|
|
||||||
"按下C可以查看鼠标位置怪物临界",
|
|
||||||
"按下E可以查看鼠标位置怪物的详细属性",
|
|
||||||
"将鼠标移动到光环怪上以查看其产生的光环",
|
|
||||||
"字体太大?试试在背包的系统设置里面调整字体大小吧!",
|
|
||||||
"字体太小?试试在背包的系统设置里面调整字体大小吧!",
|
|
||||||
"按键不合心意?试试在背包的系统设置里面自定义快捷键",
|
|
||||||
"拖动状态栏左上角可以移动状态栏哦!",
|
|
||||||
"拖动状态栏右下角可以缩放状态栏哦!",
|
|
||||||
"按下M键,鼠标位置的怪物的信息就会被你看光啦!",
|
|
||||||
"咱就是说,要不要试一下工具栏的最后一个按钮?",
|
|
||||||
"要不要试试工具栏倒数第二个按钮呢?",
|
|
||||||
"想自定义工具栏?去背包的系统设置看看吧!",
|
|
||||||
"冷知识:临界界面可以拖动滚动条来查看减伤情况",
|
|
||||||
"可以用滚轮或者双指缩放小地图!",
|
|
||||||
"楼传的最左侧一栏可以选择区域!",
|
|
||||||
"冷知识:装备栏左栏最上面可以修改装备排序",
|
|
||||||
"冷冷冷知识:装备栏左栏最上面右侧可以更改顺序或倒序",
|
|
||||||
"第一章使用跳跃技能可是要扣血的!要注意!",
|
|
||||||
"按H查看本游戏的百科全书",
|
|
||||||
"给别人炫耀一下自己的成就点吧!虽然不能记榜(",
|
|
||||||
"抱团属性会在怪物右上角显示加成数量!",
|
|
||||||
"乾坤挪移属性会在怪物左上角显示“乾”字!",
|
|
||||||
"电脑端可以试试按F11全屏游玩!",
|
|
||||||
"手机端要不试试横屏玩?",
|
|
||||||
"不在楼梯边也可以使用楼传!",
|
|
||||||
"技能树的右下角可以切换章节!",
|
|
||||||
"开启自动切换技能就会自动帮你选择最优技能了!",
|
|
||||||
"魔塔不仅有撤回,还有恢复,按W或6就可以了!",
|
|
||||||
"觉得卡顿?可以去试着设置里面关闭一些特性!",
|
|
||||||
"从第二章开始,怪物负伤害量不会超过其生命的1/4",
|
|
||||||
"生命回复不会超过防御的十分之一",
|
|
||||||
"不想看小贴士?设置里面可以关掉!",
|
|
||||||
"不小心进入了追猎范围?读取自动存档撤回到进入前吧!",
|
|
||||||
"不小心进入了电摇嘲讽范围?读取自动存档撤回到进入前吧!",
|
|
||||||
"小地图出现卡顿?试试在背包中系统设置里把小地图懒更新打开吧!"
|
|
||||||
]
|
|
@ -1 +0,0 @@
|
|||||||
export * from './webgl';
|
|
@ -1,288 +0,0 @@
|
|||||||
import { ensureArray } from '../utils';
|
|
||||||
import { sleep } from 'mutate-animate';
|
|
||||||
import { logger } from '@motajs/common';
|
|
||||||
import { tip } from '../use';
|
|
||||||
|
|
||||||
const { gl, gl2 } = checkSupport();
|
|
||||||
|
|
||||||
function checkSupport() {
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const canvas2 = document.createElement('canvas');
|
|
||||||
const gl = canvas.getContext('webgl');
|
|
||||||
const gl2 = canvas2.getContext('webgl2');
|
|
||||||
if (!gl) {
|
|
||||||
sleep(3000).then(() => {
|
|
||||||
tip(
|
|
||||||
'warning',
|
|
||||||
`您的浏览器不支持WebGL,大部分效果将会无法显示,请更新你的浏览器`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!gl2) {
|
|
||||||
sleep(3000).then(() => {
|
|
||||||
tip(
|
|
||||||
'warning',
|
|
||||||
`您的浏览器不支持WebGL2,大部分效果将会无法显示,请更新你的浏览器`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return { gl: !!gl, gl2: !!gl2 };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isWebGLSupported() {
|
|
||||||
return gl;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isWebGL2Supported() {
|
|
||||||
return gl2;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WebGLColorArray = [number, number, number, number];
|
|
||||||
|
|
||||||
interface WebGLShaderInfo {
|
|
||||||
vertex: WebGLShader;
|
|
||||||
fragment: WebGLShader;
|
|
||||||
}
|
|
||||||
|
|
||||||
type UniformBinderNum = 1 | 2 | 3 | 4;
|
|
||||||
type UniformBinderType = 'f' | 'i';
|
|
||||||
type UniformFunc<
|
|
||||||
N extends UniformBinderNum,
|
|
||||||
T extends UniformBinderType,
|
|
||||||
V extends 'v' | ''
|
|
||||||
> = `uniform${N}${T}${V}`;
|
|
||||||
|
|
||||||
type UniformBinderValue<N extends UniformBinderNum> = N extends 1
|
|
||||||
? number
|
|
||||||
: N extends 2
|
|
||||||
? [number, number]
|
|
||||||
: N extends 3
|
|
||||||
? [number, number, number]
|
|
||||||
: [number, number, number, number];
|
|
||||||
|
|
||||||
interface UniformBinder<
|
|
||||||
N extends UniformBinderNum,
|
|
||||||
T extends UniformBinderType,
|
|
||||||
V extends 'v' | ''
|
|
||||||
> {
|
|
||||||
value: UniformBinderValue<N>;
|
|
||||||
set(value: UniformBinderValue<N>): void;
|
|
||||||
get(): UniformBinderValue<N>;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class WebGLBase {
|
|
||||||
abstract canvas: HTMLCanvasElement;
|
|
||||||
abstract gl: WebGLRenderingContext | WebGL2RenderingContext;
|
|
||||||
|
|
||||||
background: WebGLColorArray = [0, 0, 0, 0];
|
|
||||||
|
|
||||||
vsSource: string = '';
|
|
||||||
fsSource: string = '';
|
|
||||||
|
|
||||||
program: WebGLProgram | null = null;
|
|
||||||
shader: WebGLShaderInfo | null = null;
|
|
||||||
|
|
||||||
resetCanvas() {
|
|
||||||
this.gl.clearColor(...this.background);
|
|
||||||
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSize(width: number, height: number) {
|
|
||||||
this.canvas.width = width;
|
|
||||||
this.canvas.height = height;
|
|
||||||
}
|
|
||||||
|
|
||||||
compile() {
|
|
||||||
const gl = this.gl;
|
|
||||||
gl.deleteProgram(this.program);
|
|
||||||
gl.deleteShader(this.shader?.vertex ?? null);
|
|
||||||
gl.deleteShader(this.shader?.fragment ?? null);
|
|
||||||
|
|
||||||
this.program = this.createProgram();
|
|
||||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
|
||||||
gl.useProgram(this.program);
|
|
||||||
}
|
|
||||||
|
|
||||||
vs(vs: string) {
|
|
||||||
this.vsSource = vs;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs(fs: string) {
|
|
||||||
this.fsSource = fs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建一个全局变量绑定器,用于操作全局变量
|
|
||||||
* @param uniform 全局变量的变量名
|
|
||||||
* @param num 变量的元素数量,float和int视为1,vec2 vec3 vec4分别视为 2 3 4
|
|
||||||
* @param type 数据类型,可以填'f',表示浮点型,或者填'i',表示整型
|
|
||||||
* @param vector 是否为向量,可以填'v',表示是向量,或者填'',表示不是向量
|
|
||||||
* @returns 一个uniform绑定器,用于操作全局变量uniform
|
|
||||||
*/
|
|
||||||
createUniformBinder<
|
|
||||||
N extends UniformBinderNum,
|
|
||||||
T extends UniformBinderType,
|
|
||||||
V extends 'v' | ''
|
|
||||||
>(uniform: string, num: N, type: T, vector: V): UniformBinder<N, T, V> {
|
|
||||||
if (!this.program) {
|
|
||||||
throw new Error(
|
|
||||||
`Uniform binder should be use when the program initialized.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const suffix = `${num}${type}${vector ? 'v' : ''}`;
|
|
||||||
const func = `uniform${suffix}` as UniformFunc<N, T, V>;
|
|
||||||
const value = (
|
|
||||||
num === 1 ? 0 : Array(num).fill(0)
|
|
||||||
) as UniformBinderValue<N>;
|
|
||||||
|
|
||||||
const loc = this.gl.getUniformLocation(this.program, uniform);
|
|
||||||
const gl = this.gl;
|
|
||||||
|
|
||||||
return {
|
|
||||||
value,
|
|
||||||
set(value) {
|
|
||||||
this.value = value;
|
|
||||||
let v;
|
|
||||||
if (vector === 'v') {
|
|
||||||
let _v = ensureArray(value);
|
|
||||||
if (type === 'f') {
|
|
||||||
v = new Float32Array(_v);
|
|
||||||
} else {
|
|
||||||
v = new Int32Array(_v);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
v = ensureArray(value);
|
|
||||||
}
|
|
||||||
// 对uniform赋值
|
|
||||||
if (vector === 'v') {
|
|
||||||
// @ts-ignore
|
|
||||||
gl[func](loc, v);
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
gl[func](loc, ...v);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
get() {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected createProgram() {
|
|
||||||
const gl = this.gl;
|
|
||||||
const vs = this.loadShader(gl.VERTEX_SHADER, this.vsSource);
|
|
||||||
const fs = this.loadShader(gl.FRAGMENT_SHADER, this.fsSource);
|
|
||||||
|
|
||||||
this.shader = {
|
|
||||||
vertex: vs,
|
|
||||||
fragment: fs
|
|
||||||
};
|
|
||||||
|
|
||||||
const program = gl.createProgram()!;
|
|
||||||
gl.attachShader(program, vs);
|
|
||||||
gl.attachShader(program, fs);
|
|
||||||
gl.linkProgram(program);
|
|
||||||
|
|
||||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
||||||
logger.error(9, gl.getProgramInfoLog(program) ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected loadShader(type: number, source: string) {
|
|
||||||
const gl = this.gl;
|
|
||||||
const shader = gl.createShader(type)!;
|
|
||||||
gl.shaderSource(shader, source);
|
|
||||||
gl.compileShader(shader);
|
|
||||||
|
|
||||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
||||||
throw new Error(
|
|
||||||
`Cannot compile ${
|
|
||||||
type === gl.VERTEX_SHADER ? 'vertex' : 'fragment'
|
|
||||||
} shader. Error info: ${gl.getShaderInfoLog(shader)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return shader;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WebGLCanvas extends WebGLBase {
|
|
||||||
canvas: HTMLCanvasElement;
|
|
||||||
gl: WebGLRenderingContext;
|
|
||||||
|
|
||||||
constructor(canvas?: HTMLCanvasElement) {
|
|
||||||
super();
|
|
||||||
this.canvas = canvas ?? document.createElement('canvas');
|
|
||||||
this.gl = this.canvas.getContext('webgl')!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WebGL2Canvas extends WebGLBase {
|
|
||||||
canvas: HTMLCanvasElement;
|
|
||||||
gl: WebGL2RenderingContext;
|
|
||||||
|
|
||||||
constructor(canvas?: HTMLCanvasElement) {
|
|
||||||
super();
|
|
||||||
this.canvas = canvas ?? document.createElement('canvas');
|
|
||||||
this.gl = this.canvas.getContext('webgl2')!;
|
|
||||||
}
|
|
||||||
|
|
||||||
vs(vs: string): void {
|
|
||||||
if (!vs.startsWith('#version 300 es')) {
|
|
||||||
this.vsSource = `#version 300 es\n` + vs;
|
|
||||||
} else {
|
|
||||||
this.vsSource = vs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs(fs: string): void {
|
|
||||||
if (!fs.startsWith('#version 300 es')) {
|
|
||||||
this.fsSource = `#version 300 es\n` + fs;
|
|
||||||
} else {
|
|
||||||
this.vsSource = fs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadShader(
|
|
||||||
gl: WebGLRenderingContext,
|
|
||||||
type: number,
|
|
||||||
source: string
|
|
||||||
) {
|
|
||||||
const shader = gl.createShader(type)!;
|
|
||||||
gl.shaderSource(shader, source);
|
|
||||||
gl.compileShader(shader);
|
|
||||||
|
|
||||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
||||||
logger.error(
|
|
||||||
10,
|
|
||||||
type === gl.VERTEX_SHADER ? 'vertex' : 'fragment',
|
|
||||||
gl.getShaderInfoLog(shader) ?? ''
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return shader;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createProgram(
|
|
||||||
gl: WebGLRenderingContext,
|
|
||||||
vsSource: string,
|
|
||||||
fsSource: string
|
|
||||||
) {
|
|
||||||
const vs = loadShader(gl, gl.VERTEX_SHADER, vsSource);
|
|
||||||
const fs = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
|
||||||
|
|
||||||
const program = gl.createProgram()!;
|
|
||||||
gl.attachShader(program, vs);
|
|
||||||
gl.attachShader(program, fs);
|
|
||||||
gl.linkProgram(program);
|
|
||||||
|
|
||||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
||||||
logger.error(9, gl.getProgramInfoLog(program) ?? '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return program;
|
|
||||||
}
|
|
@ -8,12 +8,8 @@ export * as UI from './ui';
|
|||||||
export * as Components from './components';
|
export * as Components from './components';
|
||||||
export * from './preset';
|
export * from './preset';
|
||||||
export * from './tools';
|
export * from './tools';
|
||||||
export * from './fx';
|
|
||||||
|
|
||||||
export * from './animateController';
|
export * from './animateController';
|
||||||
export * from './controller';
|
export * from './controller';
|
||||||
export * from './danmaku';
|
|
||||||
// export * from './mark';
|
|
||||||
export * from './setting';
|
export * from './setting';
|
||||||
export * from './use';
|
export * from './use';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
import { Danmaku } from '../danmaku';
|
|
||||||
import { Component, h } from 'vue';
|
|
||||||
import { mainSetting } from './settingIns';
|
|
||||||
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`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDanmaku() {
|
|
||||||
const { hook } = Mota.require('@user/data-base');
|
|
||||||
|
|
||||||
hook.once('reset', () => {
|
|
||||||
Danmaku.fetch();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 勇士移动后显示弹幕
|
|
||||||
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,69 +0,0 @@
|
|||||||
import { debounce } from 'lodash-es';
|
|
||||||
import { ref } from 'vue';
|
|
||||||
import { sleep } from 'mutate-animate';
|
|
||||||
|
|
||||||
const close = ref(false);
|
|
||||||
|
|
||||||
let cx = 0;
|
|
||||||
let cy = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示定点查看
|
|
||||||
*/
|
|
||||||
const showFixed = debounce((block: Block) => {
|
|
||||||
const e = core.material.enemys[block.event.id as EnemyIds];
|
|
||||||
if (!e) return;
|
|
||||||
const enemy = core.status.thisMap.enemy.get(block.x, block.y);
|
|
||||||
if (!enemy) return;
|
|
||||||
const { fixedUi } = Mota.require('@motajs/legacy-ui');
|
|
||||||
fixedUi.open(
|
|
||||||
'fixed',
|
|
||||||
{ enemy, close, loc: [cx, cy], hovered },
|
|
||||||
{ close: closeFixed }
|
|
||||||
);
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关闭定点查看
|
|
||||||
*/
|
|
||||||
const closeFixed = () => {
|
|
||||||
close.value = true;
|
|
||||||
sleep(200).then(() => {
|
|
||||||
const { fixedUi } = Mota.require('@motajs/legacy-ui');
|
|
||||||
fixedUi.closeByName('fixed');
|
|
||||||
close.value = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// todo: 应当在这里实现查看临界与特殊属性的功能
|
|
||||||
export let hovered: Block | null;
|
|
||||||
|
|
||||||
export function createFixed() {
|
|
||||||
const { hook, gameListener } = Mota.require('@user/data-base');
|
|
||||||
|
|
||||||
gameListener.on('hoverBlock', block => {
|
|
||||||
closeFixed();
|
|
||||||
hovered = block;
|
|
||||||
});
|
|
||||||
gameListener.on('leaveBlock', (_, __, leaveGame) => {
|
|
||||||
showFixed.cancel();
|
|
||||||
if (!leaveGame) closeFixed();
|
|
||||||
hovered = null;
|
|
||||||
});
|
|
||||||
gameListener.on('mouseMove', e => {
|
|
||||||
cx = e.clientX;
|
|
||||||
cy = e.clientY;
|
|
||||||
showFixed.cancel();
|
|
||||||
if (hovered) {
|
|
||||||
showFixed(hovered);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
hook.once('mounted', () => {
|
|
||||||
const { mainUi } = Mota.require('@motajs/legacy-ui');
|
|
||||||
mainUi.on('start', () => {
|
|
||||||
showFixed.cancel();
|
|
||||||
closeFixed();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,18 +1,12 @@
|
|||||||
import { createDanmaku } from './danmaku';
|
|
||||||
import { createFixed } from './fixed';
|
|
||||||
import { createSetting, createUI } from './ui';
|
import { createSetting, createUI } from './ui';
|
||||||
|
|
||||||
export function createPreset() {
|
export function createPreset() {
|
||||||
createDanmaku();
|
|
||||||
createFixed();
|
|
||||||
createUI();
|
createUI();
|
||||||
createSetting();
|
createSetting();
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from './ui';
|
export * from './ui';
|
||||||
export * from './settings';
|
export * from './settings';
|
||||||
export * from './danmaku';
|
|
||||||
export * from './fixed';
|
|
||||||
export * from './keyboard';
|
export * from './keyboard';
|
||||||
export * from './uiIns';
|
export * from './uiIns';
|
||||||
export * from './settingIns';
|
export * from './settingIns';
|
||||||
|
@ -8,26 +8,13 @@ mainUi.register(
|
|||||||
new GameUi('toolbox', UI.Toolbox),
|
new GameUi('toolbox', UI.Toolbox),
|
||||||
new GameUi('equipbox', UI.Equipbox),
|
new GameUi('equipbox', UI.Equipbox),
|
||||||
new GameUi('settings', UI.Settings),
|
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('fly', UI.Fly),
|
||||||
new GameUi('fixedDetail', UI.FixedDetail),
|
|
||||||
new GameUi('shop', UI.Shop),
|
new GameUi('shop', UI.Shop),
|
||||||
// new GameUi('achievement', UI.Achievement),
|
|
||||||
new GameUi('hotkey', UI.Hotkey),
|
new GameUi('hotkey', UI.Hotkey),
|
||||||
new GameUi('virtualKey', VirtualKey)
|
new GameUi('virtualKey', VirtualKey)
|
||||||
);
|
);
|
||||||
mainUi.showAll();
|
mainUi.showAll();
|
||||||
|
|
||||||
export const fixedUi = new UiController(true);
|
export const fixedUi = new UiController(true);
|
||||||
fixedUi.register(
|
fixedUi.register(new GameUi('load', UI.Load));
|
||||||
new GameUi('fixed', UI.Fixed),
|
|
||||||
new GameUi('chapter', UI.Chapter),
|
|
||||||
new GameUi('start', UI.Start),
|
|
||||||
new GameUi('load', UI.Load),
|
|
||||||
new GameUi('danmaku', UI.Danmaku),
|
|
||||||
new GameUi('danmakuEditor', UI.DanmakuEditor),
|
|
||||||
new GameUi('tips', UI.Tips)
|
|
||||||
);
|
|
||||||
fixedUi.showAll();
|
fixedUi.showAll();
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
// import { init } from './achievement';
|
|
||||||
|
|
||||||
// init();
|
|
||||||
|
|
||||||
// export * from './achievement';
|
|
||||||
export * from './book';
|
export * from './book';
|
||||||
export * from './common';
|
export * from './common';
|
||||||
export * from './equipbox';
|
export * from './equipbox';
|
||||||
|
@ -1,143 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="chapter">
|
|
||||||
<canvas id="chapter-back"></canvas>
|
|
||||||
<span id="chapter-text">{{ props.chapter }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Animation, hyper, sleep } from 'mutate-animate';
|
|
||||||
import { onMounted } from 'vue';
|
|
||||||
import { IMountedVBind } from '../interface';
|
|
||||||
import { isNil } from 'lodash-es';
|
|
||||||
|
|
||||||
const props = defineProps<IMountedVBind>();
|
|
||||||
|
|
||||||
let can: HTMLCanvasElement;
|
|
||||||
let ctx: CanvasRenderingContext2D;
|
|
||||||
let text: HTMLSpanElement;
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
can = document.getElementById('chapter-back') as HTMLCanvasElement;
|
|
||||||
ctx = can.getContext('2d')!;
|
|
||||||
text = document.getElementById('chapter-text') as HTMLSpanElement;
|
|
||||||
|
|
||||||
const ani = new Animation();
|
|
||||||
const w = window.innerWidth * devicePixelRatio;
|
|
||||||
const h = window.innerHeight * devicePixelRatio;
|
|
||||||
ctx.font = '5vh normal';
|
|
||||||
const textWidth = ctx.measureText(props.chapter).width;
|
|
||||||
const line = h * 0.05;
|
|
||||||
ani.register('rect', 0);
|
|
||||||
ani.register('line', -10);
|
|
||||||
ani.register('lineOpacity', 1);
|
|
||||||
ani.register('rect2', h / 2);
|
|
||||||
ani.register('text', window.innerWidth + 10 + textWidth);
|
|
||||||
|
|
||||||
can.width = w;
|
|
||||||
can.height = h;
|
|
||||||
can.style.width = `${window.innerWidth}px`;
|
|
||||||
can.style.height = `${window.innerHeight}px`;
|
|
||||||
text.style.left = `${w + 10}px`;
|
|
||||||
text.style.width = `${textWidth}px`;
|
|
||||||
|
|
||||||
let soundPlayed = false;
|
|
||||||
let started = false;
|
|
||||||
|
|
||||||
ani.ticker.add(time => {
|
|
||||||
if (isNil(time) || isNaN(time)) return;
|
|
||||||
if (!started) {
|
|
||||||
started = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (time >= 4050) {
|
|
||||||
props.controller.close(props.num);
|
|
||||||
ani.ticker.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!soundPlayed && time >= 1500) {
|
|
||||||
soundPlayed = true;
|
|
||||||
core.playSound('chapter.opus');
|
|
||||||
}
|
|
||||||
ctx.restore();
|
|
||||||
ctx.save();
|
|
||||||
text.style.left = `${ani.value.text}px`;
|
|
||||||
ctx.fillStyle = '#000';
|
|
||||||
ctx.clearRect(0, 0, w, h);
|
|
||||||
if (time <= 2000) {
|
|
||||||
ctx.fillRect(0, h / 2, w, -ani.value.rect);
|
|
||||||
ctx.fillRect(0, h / 2, w, ani.value.rect);
|
|
||||||
} else if (time >= 2000 && time <= 3050) {
|
|
||||||
ctx.fillRect(0, 0, w, ani.value.rect2);
|
|
||||||
ctx.fillRect(0, h, w, -ani.value.rect2);
|
|
||||||
}
|
|
||||||
ctx.shadowColor = '#fff';
|
|
||||||
ctx.shadowBlur = 3;
|
|
||||||
ctx.shadowOffsetX = 0;
|
|
||||||
ctx.shadowOffsetY = 0;
|
|
||||||
ctx.lineWidth = 3;
|
|
||||||
ctx.strokeStyle = '#fff';
|
|
||||||
ctx.fillStyle = '#fff';
|
|
||||||
ctx.globalAlpha = ani.value.lineOpacity;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(0, h / 2 - line);
|
|
||||||
ctx.lineTo(ani.value.line, h / 2 - line);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(w, h / 2 + line);
|
|
||||||
ctx.lineTo(w - ani.value.line, h / 2 + line);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.shadowBlur = 0;
|
|
||||||
ctx.filter = 'blur(5px)';
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(ani.value.line, h / 2 - line, 10, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(w - ani.value.line, h / 2 + line, 10, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
});
|
|
||||||
|
|
||||||
ani.mode(hyper('tan', 'center'))
|
|
||||||
.time(3000)
|
|
||||||
.absolute()
|
|
||||||
.apply('line', w + 10)
|
|
||||||
.mode(hyper('sin', 'out'))
|
|
||||||
.time(1000)
|
|
||||||
.apply('rect', h / 2)
|
|
||||||
.mode(hyper('tan', 'center'))
|
|
||||||
.time(3000)
|
|
||||||
.apply('text', -textWidth * 2 - 10);
|
|
||||||
|
|
||||||
await sleep(2000);
|
|
||||||
ani.mode(hyper('sin', 'in')).time(1000).apply('rect2', 0);
|
|
||||||
await sleep(1000);
|
|
||||||
ani.mode(hyper('sin', 'out')).time(1000).apply('lineOpacity', 0);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
#chapter {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
user-select: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#chapter-back {
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#chapter-text {
|
|
||||||
position: relative;
|
|
||||||
font-family: 'normal';
|
|
||||||
font-size: 5vh;
|
|
||||||
text-shadow: 0px 0px 5px #fff;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,225 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="danmaku-div">
|
|
||||||
<div
|
|
||||||
:id="`danmaku-${one.id}`"
|
|
||||||
class="danmaku-one"
|
|
||||||
v-for="one of Danmaku.showList"
|
|
||||||
:key="one.id"
|
|
||||||
@mouseenter="mousein(one.id)"
|
|
||||||
@mouseleave="mouseleave(one.id)"
|
|
||||||
@touchstart="touchStart(one.id)"
|
|
||||||
>
|
|
||||||
<span class="danmaku-info">
|
|
||||||
<span
|
|
||||||
class="danmaku-like-icon"
|
|
||||||
:liked="likedMap[one.id] < 0"
|
|
||||||
@click="postLike(one)"
|
|
||||||
>
|
|
||||||
<like-filled />
|
|
||||||
</span>
|
|
||||||
<span class="danmaku-like-num">{{
|
|
||||||
Math.abs(likedMap[one.id])
|
|
||||||
}}</span>
|
|
||||||
</span>
|
|
||||||
<component :is="one.getVNode()"></component>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { nextTick, onUnmounted, reactive, watch } from 'vue';
|
|
||||||
import { Danmaku } from '../danmaku';
|
|
||||||
import { LikeFilled } from '@ant-design/icons-vue';
|
|
||||||
import { mainSetting } from '../preset/settingIns';
|
|
||||||
import { debounce } from 'lodash-es';
|
|
||||||
|
|
||||||
interface ElementMap {
|
|
||||||
ele: HTMLDivElement;
|
|
||||||
danmaku: Danmaku;
|
|
||||||
hover: boolean;
|
|
||||||
top: number;
|
|
||||||
style: CSSStyleDeclaration;
|
|
||||||
}
|
|
||||||
|
|
||||||
const likedMap: Record<number, number> = reactive({});
|
|
||||||
|
|
||||||
const map = Danmaku.showMap;
|
|
||||||
const eleMap: Map<number, ElementMap> = new Map();
|
|
||||||
const liked = reactive<Record<number, boolean>>({});
|
|
||||||
|
|
||||||
const speed = mainSetting.getValue('ui.danmakuSpeed', 60);
|
|
||||||
|
|
||||||
const likeFn = (l: boolean, d: Danmaku) => {
|
|
||||||
liked[d.id] = l;
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(Danmaku.showList, list => {
|
|
||||||
list.forEach(v => {
|
|
||||||
if (!eleMap.has(v.id)) {
|
|
||||||
nextTick(() => {
|
|
||||||
liked[v.id] = v.liked;
|
|
||||||
v.on('like', likeFn);
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
addElement(v.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function addElement(id: number) {
|
|
||||||
const danmaku = map.get(id);
|
|
||||||
if (!danmaku) return;
|
|
||||||
const div = document.getElementById(`danmaku-${id}`);
|
|
||||||
if (!div) return;
|
|
||||||
|
|
||||||
const ele: ElementMap = {
|
|
||||||
danmaku,
|
|
||||||
ele: div as HTMLDivElement,
|
|
||||||
hover: false,
|
|
||||||
top: -1,
|
|
||||||
style: getComputedStyle(div)
|
|
||||||
};
|
|
||||||
|
|
||||||
div.style.setProperty('--end', `${-div.scrollWidth}px`);
|
|
||||||
div.style.setProperty(
|
|
||||||
'--duration',
|
|
||||||
`${Math.floor((window.innerWidth + div.scrollWidth + 100) / speed)}s`
|
|
||||||
);
|
|
||||||
div.style.setProperty('left', ele.style.left);
|
|
||||||
div.addEventListener('animationend', () => {
|
|
||||||
danmaku.showEnd();
|
|
||||||
eleMap.delete(id);
|
|
||||||
delete likedMap[id];
|
|
||||||
});
|
|
||||||
|
|
||||||
eleMap.set(id, ele);
|
|
||||||
likedMap[id] = danmaku.liked ? -danmaku.likedNum : danmaku.likedNum;
|
|
||||||
|
|
||||||
calTop(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mousein(id: number) {
|
|
||||||
const danmaku = eleMap.get(id)!;
|
|
||||||
danmaku.hover = true;
|
|
||||||
danmaku.ele.classList.add('danmaku-paused');
|
|
||||||
}
|
|
||||||
|
|
||||||
function mouseleave(id: number) {
|
|
||||||
const danmaku = eleMap.get(id)!;
|
|
||||||
danmaku.hover = false;
|
|
||||||
danmaku.ele.classList.remove('danmaku-paused');
|
|
||||||
}
|
|
||||||
|
|
||||||
const touchDebounce = debounce(mouseleave, 3000);
|
|
||||||
function touchStart(id: number) {
|
|
||||||
mousein(id);
|
|
||||||
touchDebounce(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function calTop(id: number) {
|
|
||||||
const danmaku = eleMap.get(id)!;
|
|
||||||
const fontSize = mainSetting.getValue('screen.fontSize', 16) * 1.25 + 15;
|
|
||||||
|
|
||||||
const used: Set<number> = new Set();
|
|
||||||
eleMap.forEach(v => {
|
|
||||||
const { ele, style } = v;
|
|
||||||
const pos = parseInt(style.transform.slice(19, -4));
|
|
||||||
const width = ele.scrollWidth;
|
|
||||||
if (
|
|
||||||
pos <= window.innerWidth + 200 &&
|
|
||||||
pos + width >= window.innerWidth
|
|
||||||
) {
|
|
||||||
used.add(v.top);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let i = -1;
|
|
||||||
danmaku.top = 0;
|
|
||||||
danmaku.ele.style.top = `20px`;
|
|
||||||
while (++i < 20) {
|
|
||||||
if (!used.has(i)) {
|
|
||||||
danmaku.top = i;
|
|
||||||
danmaku.ele.style.top = `${fontSize * i + 20}px`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function postLike(danmaku: Danmaku) {
|
|
||||||
const res = await danmaku.triggerLike();
|
|
||||||
|
|
||||||
const liked = res.data.liked;
|
|
||||||
likedMap[danmaku.id] = liked ? -danmaku.likedNum : danmaku.likedNum;
|
|
||||||
}
|
|
||||||
|
|
||||||
onUnmounted(() => {});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
#danmaku-div {
|
|
||||||
position: fixed;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
overflow: visible;
|
|
||||||
font-size: 150%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-one {
|
|
||||||
--end: 0;
|
|
||||||
--duration: 5s;
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
transform: translateX(100vw);
|
|
||||||
width: max-content;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-wrap: nowrap;
|
|
||||||
padding: 0 5px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
animation: danmaku-roll linear var(--duration) forwards;
|
|
||||||
animation-play-state: running;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-one:hover {
|
|
||||||
background-color: #0006;
|
|
||||||
border-radius: 5px;
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-one .danmaku-paused {
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes danmaku-roll {
|
|
||||||
0% {
|
|
||||||
transform: translateX(100vw);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(var(--end));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-info {
|
|
||||||
text-shadow: 1px 1px 1px black, 1px -1px 1px black, -1px 1px 1px black,
|
|
||||||
-1px -1px 1px black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-like-num {
|
|
||||||
font-size: 75%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-like-icon {
|
|
||||||
transition: color 0.1s linear;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-like-icon[liked='true'],
|
|
||||||
.danmaku-like-icon:hover {
|
|
||||||
color: aqua;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,671 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="danmaku-editor" @click.capture="clickInter">
|
|
||||||
<div id="danmaku-input">
|
|
||||||
<span
|
|
||||||
class="danmaku-tool"
|
|
||||||
:open="cssOpened"
|
|
||||||
@click="openTool('css')"
|
|
||||||
>CSS</span
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="danmaku-tool"
|
|
||||||
:open="fillOpened"
|
|
||||||
@click="openTool('fillColor')"
|
|
||||||
>
|
|
||||||
<font-colors-outlined />
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="danmaku-tool"
|
|
||||||
:open="strokeOpened"
|
|
||||||
@click="openTool('strokeColor')"
|
|
||||||
>
|
|
||||||
<highlight-outlined />
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="danmaku-tool"
|
|
||||||
:open="iconOpened"
|
|
||||||
@click="openTool('icon')"
|
|
||||||
>
|
|
||||||
<meh-outlined />
|
|
||||||
</span>
|
|
||||||
<div id="danmaku-input-div">
|
|
||||||
<Input
|
|
||||||
id="danmaku-input-input"
|
|
||||||
v-model:value="inputValue"
|
|
||||||
:max-length="200"
|
|
||||||
placeholder="请在此输入弹幕,显示中括号请使用\[或\]"
|
|
||||||
autocomplete="off"
|
|
||||||
@change="input(inputValue)"
|
|
||||||
@press-enter="inputEnter()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="danmaku-tool danmaku-post"
|
|
||||||
:posting="posting"
|
|
||||||
@click="send()"
|
|
||||||
>
|
|
||||||
<send-outlined />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Transition name="danmaku">
|
|
||||||
<div v-if="cssOpened" id="danmaku-css">
|
|
||||||
<span id="danmaku-css-hint">编辑弹幕的 CSS 样式</span>
|
|
||||||
<Input
|
|
||||||
id="danmaku-css-input"
|
|
||||||
v-model:value="cssInfo"
|
|
||||||
:max-length="300"
|
|
||||||
placeholder="请在此输入样式"
|
|
||||||
autocomplete="off"
|
|
||||||
@blur="inputCSS(cssInfo)"
|
|
||||||
@press-enter="inputCSS(cssInfo)"
|
|
||||||
/>
|
|
||||||
<span v-if="cssError" id="danmaku-css-error">{{
|
|
||||||
cssError
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="iconOpened" id="danmaku-icon">
|
|
||||||
<span id="danmaku-icon-hint">常用图标</span>
|
|
||||||
<Scroll
|
|
||||||
class="danmaku-icon-scroll"
|
|
||||||
:no-scroll="true"
|
|
||||||
type="horizontal"
|
|
||||||
>
|
|
||||||
<div id="danmaku-icon-div">
|
|
||||||
<span
|
|
||||||
class="danmaku-icon-one"
|
|
||||||
v-for="icon of frequentlyIcon"
|
|
||||||
@click="addIcon(icon as AllIds)"
|
|
||||||
>
|
|
||||||
<BoxAnimate
|
|
||||||
:id="icon as AllIds"
|
|
||||||
:noborder="true"
|
|
||||||
:no-animate="true"
|
|
||||||
:height="getIconHeight(icon as AllIds)"
|
|
||||||
></BoxAnimate>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Scroll>
|
|
||||||
<span
|
|
||||||
id="danmaku-icon-all"
|
|
||||||
class="button-text"
|
|
||||||
:active="iconAll"
|
|
||||||
@click="iconAll = !iconAll"
|
|
||||||
>
|
|
||||||
所有图标 <up-outlined />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="fillOpened || strokeOpened" id="danmaku-color">
|
|
||||||
<span id="danmaku-color-hint"
|
|
||||||
>设置{{ fillOpened ? '填充' : '描边' }}颜色</span
|
|
||||||
>
|
|
||||||
<Scroll
|
|
||||||
class="danmaku-color-scroll"
|
|
||||||
:no-scroll="true"
|
|
||||||
type="horizontal"
|
|
||||||
>
|
|
||||||
<div id="danmaku-color-container">
|
|
||||||
<span
|
|
||||||
v-for="color of frequentlyColor"
|
|
||||||
:style="{ backgroundColor: color }"
|
|
||||||
:selected="color === nowColor"
|
|
||||||
@click="inputColor(color)"
|
|
||||||
class="danmaku-color-one"
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
</Scroll>
|
|
||||||
<Input
|
|
||||||
id="danmaku-color-input"
|
|
||||||
:max-length="100"
|
|
||||||
v-model:value="nowColor"
|
|
||||||
placeholder="输入颜色"
|
|
||||||
autocomplete="off"
|
|
||||||
@blur="inputColor(nowColor)"
|
|
||||||
@pressEnter="inputColor(nowColor)"
|
|
||||||
></Input>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
<Transition name="danmaku-icon">
|
|
||||||
<div v-if="iconAll" id="danmaku-icon-all-div">
|
|
||||||
<span
|
|
||||||
>本列表不包含额外素材,如果需要额外素材请手动填写素材id</span
|
|
||||||
>
|
|
||||||
<Scroll class="danmaku-all-scroll">
|
|
||||||
<div id="danmaku-all-container">
|
|
||||||
<span
|
|
||||||
v-for="icon of getAllIcons()"
|
|
||||||
@click="addIcon(icon)"
|
|
||||||
>
|
|
||||||
<BoxAnimate
|
|
||||||
:id="icon"
|
|
||||||
:height="getIconHeight(icon)"
|
|
||||||
:no-animate="true"
|
|
||||||
:noborder="true"
|
|
||||||
></BoxAnimate>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Scroll>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Ref, onMounted, onUnmounted, ref } from 'vue';
|
|
||||||
import {
|
|
||||||
FontColorsOutlined,
|
|
||||||
HighlightOutlined,
|
|
||||||
MehOutlined,
|
|
||||||
SendOutlined,
|
|
||||||
UpOutlined
|
|
||||||
} from '@ant-design/icons-vue';
|
|
||||||
import { Danmaku } from '../danmaku';
|
|
||||||
import { sleep } from 'mutate-animate';
|
|
||||||
import { calStringSize, stringifyCSS, parseCss, getIconHeight } from '../utils';
|
|
||||||
import { gameKey } from '@motajs/system-action';
|
|
||||||
import { isNil } from 'lodash-es';
|
|
||||||
import { logger, LogLevel } from '@motajs/common';
|
|
||||||
import Scroll from '../components/scroll.vue';
|
|
||||||
import BoxAnimate from '../components/boxAnimate.vue';
|
|
||||||
import { IMountedVBind } from '../interface';
|
|
||||||
import { tip } from '../use';
|
|
||||||
import { Input } from 'ant-design-vue';
|
|
||||||
|
|
||||||
const props = defineProps<IMountedVBind>();
|
|
||||||
|
|
||||||
const frequentlyIcon: (AllIds | 'hero' | `X${number}`)[] = [
|
|
||||||
'hero',
|
|
||||||
'yellowKey',
|
|
||||||
'blueKey',
|
|
||||||
'redKey',
|
|
||||||
'A492',
|
|
||||||
'A494',
|
|
||||||
'A497',
|
|
||||||
'redPotion',
|
|
||||||
'redGem',
|
|
||||||
'blueGem',
|
|
||||||
'I559',
|
|
||||||
'X10194',
|
|
||||||
'downPortal',
|
|
||||||
'leftPortal',
|
|
||||||
'upPortal',
|
|
||||||
'rightPortal',
|
|
||||||
'upFloor',
|
|
||||||
'downFloor',
|
|
||||||
'greenSlime',
|
|
||||||
'yellowKnight',
|
|
||||||
'bat',
|
|
||||||
'slimelord'
|
|
||||||
];
|
|
||||||
const frequentlyColor: string[] = [
|
|
||||||
'#ffffff',
|
|
||||||
'#000000',
|
|
||||||
'#ff0000',
|
|
||||||
'#00ff00',
|
|
||||||
'#0000ff',
|
|
||||||
'#ffff00',
|
|
||||||
'#00ffff',
|
|
||||||
'#ff00ff',
|
|
||||||
'#c0c0c0',
|
|
||||||
'#808080',
|
|
||||||
'#800000',
|
|
||||||
'#800080',
|
|
||||||
'#008000',
|
|
||||||
'#808000',
|
|
||||||
'#000080',
|
|
||||||
'#008080'
|
|
||||||
];
|
|
||||||
|
|
||||||
let mainDiv: HTMLDivElement;
|
|
||||||
|
|
||||||
let danmaku = Danmaku.lastEditoredDanmaku ?? new Danmaku();
|
|
||||||
|
|
||||||
const cssOpened = ref(false);
|
|
||||||
const iconOpened = ref(false);
|
|
||||||
const fillOpened = ref(false);
|
|
||||||
const strokeOpened = ref(false);
|
|
||||||
const posting = ref(false);
|
|
||||||
const iconAll = ref(false);
|
|
||||||
const nowColor = ref('#ffffff');
|
|
||||||
|
|
||||||
const inputValue = ref(danmaku.text);
|
|
||||||
const cssInfo = ref(stringifyCSS(danmaku.style));
|
|
||||||
const cssError = ref('');
|
|
||||||
|
|
||||||
const map: Record<string, Ref<boolean>> = {
|
|
||||||
css: cssOpened,
|
|
||||||
icon: iconOpened,
|
|
||||||
fillColor: fillOpened,
|
|
||||||
strokeColor: strokeOpened
|
|
||||||
};
|
|
||||||
|
|
||||||
function openTool(tool: string) {
|
|
||||||
iconAll.value = false;
|
|
||||||
for (const [key, value] of Object.entries(map)) {
|
|
||||||
if (key === tool) {
|
|
||||||
value.value = !value.value;
|
|
||||||
} else {
|
|
||||||
value.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (tool === 'fillColor') {
|
|
||||||
nowColor.value = danmaku.textColor;
|
|
||||||
} else if (tool === 'strokeColor') {
|
|
||||||
nowColor.value = danmaku.strokeColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function send() {
|
|
||||||
if (posting.value) return;
|
|
||||||
if (danmaku.text === '') {
|
|
||||||
tip('warning', '请填写弹幕!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!core.isPlaying()) {
|
|
||||||
tip('warning', '请进入游戏后再发送弹幕');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (calStringSize(danmaku.text) > 200) {
|
|
||||||
tip('warning', '弹幕长度超限!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { x, y } = core.status.hero.loc;
|
|
||||||
const floor = core.status.floorId;
|
|
||||||
if (isNil(x) || isNil(y) || isNil(floor)) {
|
|
||||||
tip('warning', '当前无法发送弹幕');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
danmaku.x = x;
|
|
||||||
danmaku.y = y;
|
|
||||||
danmaku.floor = floor;
|
|
||||||
|
|
||||||
danmaku
|
|
||||||
.post()
|
|
||||||
.then(value => {
|
|
||||||
if (value.data.code === 0) {
|
|
||||||
danmaku.show();
|
|
||||||
danmaku = new Danmaku();
|
|
||||||
inputValue.value = '';
|
|
||||||
cssInfo.value = '';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
posting.value = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
mainDiv.classList.remove('danmaku-startup');
|
|
||||||
mainDiv.classList.add('danmaku-close');
|
|
||||||
sleep(200).then(() => {
|
|
||||||
props.controller.close(props.num);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function input(value: string) {
|
|
||||||
const size = calStringSize(value);
|
|
||||||
if (size > 200) {
|
|
||||||
tip('warning', '弹幕长度超限!');
|
|
||||||
}
|
|
||||||
|
|
||||||
const before = danmaku.text;
|
|
||||||
const { info, ret } = logger.catch(() => {
|
|
||||||
danmaku.text = value;
|
|
||||||
return danmaku.parse();
|
|
||||||
});
|
|
||||||
if (info.length > 0) {
|
|
||||||
if (info[0].code === 4) {
|
|
||||||
tip('error', '请检查中括号匹配');
|
|
||||||
danmaku.text = before;
|
|
||||||
} else {
|
|
||||||
danmaku.vNode = ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function inputEnter() {
|
|
||||||
input(inputValue.value);
|
|
||||||
inputCSS(cssInfo.value);
|
|
||||||
send();
|
|
||||||
}
|
|
||||||
|
|
||||||
function inputCSS(text: string) {
|
|
||||||
const { info, ret } = logger.catch(() => {
|
|
||||||
return parseCss(text);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (info.some(v => v.level > LogLevel.LOG)) {
|
|
||||||
cssError.value = '语法错误';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allow = Danmaku.checkCSSAllow(ret);
|
|
||||||
if (allow.length > 0) {
|
|
||||||
cssError.value = allow[0];
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
cssError.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
danmaku.css(ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
function inputColor(color: string) {
|
|
||||||
nowColor.value = color;
|
|
||||||
if (fillOpened.value) {
|
|
||||||
danmaku.textColor = color;
|
|
||||||
} else {
|
|
||||||
danmaku.strokeColor = color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addIcon(icon: AllIds | 'hero') {
|
|
||||||
const iconText = `[i:${icon}]`;
|
|
||||||
if (iconText.length + danmaku.text.length > 200) {
|
|
||||||
tip('warn', '弹幕长度超限!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
danmaku.text += iconText;
|
|
||||||
inputValue.value = danmaku.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAllIcons() {
|
|
||||||
return [
|
|
||||||
...new Set(
|
|
||||||
Object.values(core.maps.blocksInfo)
|
|
||||||
.filter(v => v.cls !== 'tileset')
|
|
||||||
.map(v => {
|
|
||||||
return v.id;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
let clickIn = false;
|
|
||||||
let closed = false;
|
|
||||||
function clickOuter() {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (!clickIn) {
|
|
||||||
if (!iconAll.value) {
|
|
||||||
if (!closed) {
|
|
||||||
closed = true;
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
} else iconAll.value = false;
|
|
||||||
}
|
|
||||||
clickIn = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function clickInter() {
|
|
||||||
clickIn = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let lockedBefore = false;
|
|
||||||
onMounted(() => {
|
|
||||||
mainDiv = document.getElementById('danmaku-editor') as HTMLDivElement;
|
|
||||||
mainDiv.classList.add('danmaku-startup');
|
|
||||||
gameKey.disable();
|
|
||||||
|
|
||||||
core.lockControl();
|
|
||||||
mainDiv.addEventListener('focus', () => {
|
|
||||||
lockedBefore = core.status.lockControl;
|
|
||||||
core.lockControl();
|
|
||||||
gameKey.disable();
|
|
||||||
});
|
|
||||||
mainDiv.addEventListener('blur', () => {
|
|
||||||
gameKey.enable();
|
|
||||||
if (!lockedBefore) core.unlockControl();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('click', clickOuter, { capture: true });
|
|
||||||
document.addEventListener('touchend', clickOuter, { capture: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (danmaku.text !== '' || Object.keys(danmaku.style).length > 0) {
|
|
||||||
Danmaku.lastEditoredDanmaku = danmaku;
|
|
||||||
} else {
|
|
||||||
delete Danmaku.lastEditoredDanmaku;
|
|
||||||
}
|
|
||||||
if (!lockedBefore) core.unlockControl();
|
|
||||||
gameKey.enable();
|
|
||||||
document.removeEventListener('click', clickOuter);
|
|
||||||
document.removeEventListener('touchend', clickOuter);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
#danmaku-editor {
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
bottom: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
justify-content: end;
|
|
||||||
font-size: 200%;
|
|
||||||
background-color: #000b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-startup {
|
|
||||||
animation: editor-startup 0.2s ease-out 0s 1 normal forwards running;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-close {
|
|
||||||
animation: editor-close 0.2s ease-in 0s 1 normal forwards running;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes editor-startup {
|
|
||||||
0% {
|
|
||||||
transform: translateY(70px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes editor-close {
|
|
||||||
0% {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateY(110px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#danmaku-input {
|
|
||||||
height: 40px;
|
|
||||||
width: 60%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 10px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#danmaku-input-div {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
#danmaku-input-input {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 80%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-tool {
|
|
||||||
cursor: pointer;
|
|
||||||
color: white;
|
|
||||||
transition: color 0.2s linear;
|
|
||||||
margin-right: 7px;
|
|
||||||
font-family: 'FiraCode', 'Arial';
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-tool[open='true'],
|
|
||||||
.danmaku-tool:hover {
|
|
||||||
color: aqua;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-post {
|
|
||||||
margin-left: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-post[posting='true'] {
|
|
||||||
color: gray;
|
|
||||||
cursor: wait;
|
|
||||||
}
|
|
||||||
|
|
||||||
#danmaku-css {
|
|
||||||
width: 60%;
|
|
||||||
font-size: 60%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-family: 'FiraCode', 'Arial';
|
|
||||||
|
|
||||||
#danmaku-css-input {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 80%;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#danmaku-css-error {
|
|
||||||
color: lightcoral;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#danmaku-icon {
|
|
||||||
width: 60%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 80%;
|
|
||||||
white-space: nowrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-family: 'FiraCode', 'Arial';
|
|
||||||
|
|
||||||
.danmaku-icon-scroll {
|
|
||||||
width: calc(90% - 200px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#danmaku-icon-div {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-icon-one {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#danmaku-icon-all-div {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 110px;
|
|
||||||
height: 50vh;
|
|
||||||
background-color: #000b;
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 1%;
|
|
||||||
font-size: 75%;
|
|
||||||
width: 60%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
color: #dddd;
|
|
||||||
|
|
||||||
#danmaku-all-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-all-scroll {
|
|
||||||
height: calc(100% - 70px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#danmaku-color {
|
|
||||||
width: 60%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-family: 'FiraCode', 'Arial';
|
|
||||||
font-size: 75%;
|
|
||||||
|
|
||||||
#danmaku-color-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 10px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-color-one {
|
|
||||||
width: 30px;
|
|
||||||
min-width: 30px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-right: 7px;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
transition: border 0.1s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-color-one[selected='true'],
|
|
||||||
.danmaku-color-one:hover {
|
|
||||||
border: 2px solid gold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-color-scroll {
|
|
||||||
width: calc(100% - 400px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#danmaku-color-input {
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-enter-active,
|
|
||||||
.danmaku-leave-active {
|
|
||||||
transition: all 0.4s ease-out;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-enter-from,
|
|
||||||
.danmaku-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(50px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-icon-enter-active,
|
|
||||||
.danmaku-icon-leave-active {
|
|
||||||
transition: all 0.3s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.danmaku-icon-enter-from,
|
|
||||||
.danmaku-icon-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(50px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
#danmaku-input {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#danmaku-css {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#danmaku-icon {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,75 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Colomn @close="exit" :width="80" :height="80" :left="30" :right="70"
|
|
||||||
><template #left
|
|
||||||
><div id="desc-list">
|
|
||||||
<div
|
|
||||||
v-for="(data, k) in desc"
|
|
||||||
class="selectable desc-item"
|
|
||||||
:selected="selected === k"
|
|
||||||
:show="show(data.condition)"
|
|
||||||
@click="click(k)"
|
|
||||||
>
|
|
||||||
<span v-if="show(data.condition)">{{ data.text }}</span>
|
|
||||||
</div>
|
|
||||||
</div></template
|
|
||||||
>
|
|
||||||
<template #right><span v-html="content"></span></template
|
|
||||||
></Colomn>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onUnmounted, ref } from 'vue';
|
|
||||||
import desc from '../data/desc.json';
|
|
||||||
import { splitText } from '../utils';
|
|
||||||
import Colomn from '../components/colomn.vue';
|
|
||||||
import { gameKey } from '@motajs/system-action';
|
|
||||||
import { IMountedVBind } from '../interface';
|
|
||||||
|
|
||||||
const props = defineProps<IMountedVBind>();
|
|
||||||
|
|
||||||
type DescKey = keyof typeof desc;
|
|
||||||
|
|
||||||
const selected = ref(Object.keys(desc)[0] as DescKey);
|
|
||||||
|
|
||||||
function exit() {
|
|
||||||
props.controller.close(props.num);
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = computed(() => {
|
|
||||||
return eval('`' + splitText(desc[selected.value].desc) + '`');
|
|
||||||
});
|
|
||||||
|
|
||||||
function click(key: DescKey) {
|
|
||||||
if (!eval(desc[key].condition)) return;
|
|
||||||
selected.value = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
function show(condition: string) {
|
|
||||||
return eval(condition);
|
|
||||||
}
|
|
||||||
|
|
||||||
gameKey.use(props.ui.symbol);
|
|
||||||
gameKey
|
|
||||||
.realize('exit', () => {
|
|
||||||
exit();
|
|
||||||
})
|
|
||||||
.realize('desc', () => {
|
|
||||||
exit();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
gameKey.dispose(props.ui.symbol);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
#desc-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desc-item[show='false'] {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,174 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="fixed">
|
|
||||||
<Box
|
|
||||||
v-model:height="height"
|
|
||||||
v-model:left="left"
|
|
||||||
v-model:top="top"
|
|
||||||
v-model:width="width"
|
|
||||||
>
|
|
||||||
<div id="enemy-fixed">
|
|
||||||
<span id="enemy-name">{{ enemy.enemy.name }}</span>
|
|
||||||
<div id="enemy-special">
|
|
||||||
<span
|
|
||||||
v-for="(text, i) of special"
|
|
||||||
:style="{ color: text[1] }"
|
|
||||||
>{{ text[0] }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="enemy-attr" v-for="(a, i) of detail">
|
|
||||||
<span class="attr-name" :style="{ color: a[2] }">{{
|
|
||||||
a[1]
|
|
||||||
}}</span>
|
|
||||||
<span class="attr-value" :style="{ color: a[2] }">{{
|
|
||||||
format(a[0])
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { onMounted, onUpdated, Ref, ref, watch } from 'vue';
|
|
||||||
import Box from '../components/box.vue';
|
|
||||||
import { nextFrame } from '../utils';
|
|
||||||
import { EnemyInfo, IDamageEnemy } from '@motajs/types';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
num: number;
|
|
||||||
enemy: IDamageEnemy;
|
|
||||||
close: Ref<boolean>;
|
|
||||||
loc: [x: number, y: number];
|
|
||||||
}>();
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(e: 'close'): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
watch(props.close, n => {
|
|
||||||
if (n) {
|
|
||||||
fixed.style.opacity = '0';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let main: HTMLDivElement;
|
|
||||||
let fixed: HTMLDivElement;
|
|
||||||
|
|
||||||
const format = core.formatBigNumber;
|
|
||||||
const enemy = props.enemy;
|
|
||||||
const detail = ((): [number, string, string][] => {
|
|
||||||
const info = enemy.info;
|
|
||||||
const data = enemy.calCritical()[0];
|
|
||||||
const ratio = core.status.thisMap?.ratio ?? 1;
|
|
||||||
return [
|
|
||||||
[info.hp, '生命', 'lightgreen'],
|
|
||||||
[info.atk, '攻击', 'lightcoral'],
|
|
||||||
[info.def, '防御', 'lightblue'],
|
|
||||||
[enemy.enemy.money, '金币', 'lightyellow'],
|
|
||||||
[enemy.enemy.exp, '经验', 'lawgreen'],
|
|
||||||
[data?.atkDelta ?? 0, '临界', 'lightsalmon'],
|
|
||||||
[data?.delta ?? 0, '临界减伤', 'lightpink'],
|
|
||||||
[enemy.calDefDamage(ratio).delta, `${ratio}防`, 'cyan']
|
|
||||||
];
|
|
||||||
})();
|
|
||||||
const special = (() => {
|
|
||||||
const s = [...enemy.info.special];
|
|
||||||
|
|
||||||
const fromFunc = (
|
|
||||||
func: string | ((enemy: EnemyInfo) => string),
|
|
||||||
enemy: EnemyInfo
|
|
||||||
) => {
|
|
||||||
return typeof func === 'string' ? func : func(enemy);
|
|
||||||
};
|
|
||||||
|
|
||||||
const show = s.slice(0, 2).map(v => {
|
|
||||||
const s = Mota.require('@user/data-state').specials[v];
|
|
||||||
return [fromFunc(s.name, enemy.info), s.color];
|
|
||||||
});
|
|
||||||
if (s.length > 2) show.push(['...', 'white']);
|
|
||||||
return show;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const left = ref(0);
|
|
||||||
const top = ref(0);
|
|
||||||
const width = ref(300);
|
|
||||||
const height = ref(400);
|
|
||||||
let vh = window.innerHeight;
|
|
||||||
let vw = window.innerWidth;
|
|
||||||
|
|
||||||
async function calHeight() {
|
|
||||||
vh = window.innerHeight;
|
|
||||||
vw = window.innerWidth;
|
|
||||||
width.value = vh * 0.28;
|
|
||||||
await new Promise(res => requestAnimationFrame(res));
|
|
||||||
updateMain();
|
|
||||||
if (!main) return;
|
|
||||||
const style = getComputedStyle(main);
|
|
||||||
const h = parseFloat(style.height);
|
|
||||||
const [cx, cy] = props.loc;
|
|
||||||
if (cy + h + 10 > vh - 10) top.value = vh - h - 10;
|
|
||||||
else top.value = cy + 10;
|
|
||||||
if (cx + width.value + 10 > vw - 10) left.value = vw - width.value - 10;
|
|
||||||
else left.value = cx + 10;
|
|
||||||
height.value = h;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateMain() {
|
|
||||||
main = document.getElementById('enemy-fixed') as HTMLDivElement;
|
|
||||||
fixed = document.getElementById('fixed') as HTMLDivElement;
|
|
||||||
if (main) {
|
|
||||||
main.addEventListener('mouseleave', () => emits('close'));
|
|
||||||
}
|
|
||||||
nextFrame(() => {
|
|
||||||
fixed.style.opacity = '1';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdated(calHeight);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
updateMain();
|
|
||||||
calHeight();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
#fixed {
|
|
||||||
font-family: 'normal';
|
|
||||||
font-size: 2.5vh;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
#enemy-fixed {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
background-color: #000c;
|
|
||||||
padding: 1vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#enemy-special {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-around;
|
|
||||||
}
|
|
||||||
|
|
||||||
.enemy-attr {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attr-name {
|
|
||||||
flex-basis: 50%;
|
|
||||||
width: 100%;
|
|
||||||
text-align: right;
|
|
||||||
padding-right: 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attr-value {
|
|
||||||
flex-basis: 50%;
|
|
||||||
padding-left: 5%;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,47 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="fixed-detail">
|
|
||||||
<BookDetail
|
|
||||||
:from-book="false"
|
|
||||||
:default-panel="panel"
|
|
||||||
@close="close"
|
|
||||||
></BookDetail>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { getDetailedEnemy } from '../tools/fixed';
|
|
||||||
import BookDetail from './bookDetail.vue';
|
|
||||||
import { detailInfo } from '../tools/book';
|
|
||||||
import { hovered } from '../preset/fixed';
|
|
||||||
import { IMountedVBind } from '../interface';
|
|
||||||
|
|
||||||
const props = defineProps<IMountedVBind>();
|
|
||||||
|
|
||||||
const panel = props.panel ?? 'special';
|
|
||||||
|
|
||||||
detailInfo.pos = 0;
|
|
||||||
|
|
||||||
if (hovered) {
|
|
||||||
const { x, y } = hovered;
|
|
||||||
const enemy = core.status.thisMap.enemy.get(x, y);
|
|
||||||
if (enemy) {
|
|
||||||
const detail = getDetailedEnemy(enemy);
|
|
||||||
detailInfo.enemy = detail;
|
|
||||||
} else {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
props.controller.close(props.num);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
#fixed-detail {
|
|
||||||
width: 80%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,19 +1,9 @@
|
|||||||
export { default as Book } from './book.vue';
|
export { default as Book } from './book.vue';
|
||||||
export { default as BookDetail } from './bookDetail.vue';
|
export { default as BookDetail } from './bookDetail.vue';
|
||||||
export { default as Chapter } from './chapter.vue';
|
|
||||||
export { default as Desc } from './desc.vue';
|
|
||||||
export { default as Equipbox } from './equipbox.vue';
|
export { default as Equipbox } from './equipbox.vue';
|
||||||
export { default as Fixed } from './fixed.vue';
|
|
||||||
export { default as FixedDetail } from './fixedDetail.vue';
|
|
||||||
export { default as Fly } from './fly.vue';
|
export { default as Fly } from './fly.vue';
|
||||||
export { default as Settings } from './settings.vue';
|
export { default as Settings } from './settings.vue';
|
||||||
export { default as Shop } from './shop.vue';
|
export { default as Shop } from './shop.vue';
|
||||||
export { default as Skill } from './skill.vue';
|
|
||||||
export { default as SkillTree } from './skillTree.vue';
|
|
||||||
export { default as Start } from './start.vue';
|
|
||||||
export { default as Toolbox } from './toolbox.vue';
|
export { default as Toolbox } from './toolbox.vue';
|
||||||
export { default as Hotkey } from './hotkey.vue';
|
export { default as Hotkey } from './hotkey.vue';
|
||||||
export { default as Load } from './load.vue';
|
export { default as Load } from './load.vue';
|
||||||
export { default as Danmaku } from './danmaku.vue';
|
|
||||||
export { default as DanmakuEditor } from './danmakuEditor.vue';
|
|
||||||
export { default as Tips } from './tips.vue';
|
|
||||||
|
@ -1,79 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Column @close="exit" :width="70" :height="70"
|
|
||||||
><template #left
|
|
||||||
><div id="skill-list">
|
|
||||||
<span
|
|
||||||
v-for="(v, k) in skills"
|
|
||||||
class="selectable skill-item"
|
|
||||||
:selected="k === selected"
|
|
||||||
:selectable="skillOpened(k)"
|
|
||||||
@click="select(k)"
|
|
||||||
>{{ v.text }}</span
|
|
||||||
>
|
|
||||||
</div></template
|
|
||||||
>
|
|
||||||
<template #right><span v-html="content"></span></template
|
|
||||||
></Column>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import skills from '../data/skill.json';
|
|
||||||
import Column from '../components/colomn.vue';
|
|
||||||
import { IMountedVBind } from '../interface';
|
|
||||||
import { isNil } from 'lodash-es';
|
|
||||||
|
|
||||||
const props = defineProps<IMountedVBind>();
|
|
||||||
|
|
||||||
type Skills = keyof typeof skills;
|
|
||||||
|
|
||||||
const selected = ref<Skills>('none');
|
|
||||||
|
|
||||||
function skillOpened(skill: Skills) {
|
|
||||||
return eval(skills[skill].opened) as boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function select(skill: Skills) {
|
|
||||||
if (!skillOpened(skill)) return;
|
|
||||||
selected.value = skill;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = computed(() => {
|
|
||||||
return eval(
|
|
||||||
'`' +
|
|
||||||
skills[selected.value].desc
|
|
||||||
.map((v, i, a) => {
|
|
||||||
if (/^\d+\./.test(v)) return `${' '.repeat(12)}${v}`;
|
|
||||||
else if (
|
|
||||||
(!isNil(a[i - 1]) &&
|
|
||||||
v !== '<br>' &&
|
|
||||||
a[i - 1] === '<br>') ||
|
|
||||||
i === 0
|
|
||||||
) {
|
|
||||||
return `${' '.repeat(8)}${v}`;
|
|
||||||
} else return v;
|
|
||||||
})
|
|
||||||
.join('')
|
|
||||||
.replace(
|
|
||||||
/level:(\d+)/g,
|
|
||||||
'Mota.require("@user/data-state").getSkillLevel($1)'
|
|
||||||
) +
|
|
||||||
'`'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
function exit() {
|
|
||||||
props.controller.close(props.num);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
#skill-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-item[selectable='false'] {
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,449 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="skill-tree">
|
|
||||||
<div id="tools">
|
|
||||||
<span id="back" class="button-text tools" @click="exit"
|
|
||||||
><left-outlined />返回游戏</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<span id="skill-title">{{ skill.title }}</span>
|
|
||||||
<Divider dashed style="border-color: #ddd4" id="divider"></Divider>
|
|
||||||
<div id="skill-info">
|
|
||||||
<Scroll id="skill-desc" :no-scroll="true">
|
|
||||||
<span v-html="desc"></span>
|
|
||||||
</Scroll>
|
|
||||||
<div id="skill-effect">
|
|
||||||
<span v-if="level > 0" v-html="effect[0]"></span>
|
|
||||||
<span v-if="level < skill.max" v-html="effect[1]"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Divider
|
|
||||||
dashed
|
|
||||||
style="border-color: #ddd4"
|
|
||||||
id="divider-split"
|
|
||||||
></Divider>
|
|
||||||
<div id="skill-bottom">
|
|
||||||
<canvas id="skill-canvas"></canvas>
|
|
||||||
<Divider
|
|
||||||
dashed
|
|
||||||
style="border-color: #ddd4"
|
|
||||||
:type="isMobile ? 'horizontal' : 'vertical'"
|
|
||||||
id="divider-vertical"
|
|
||||||
></Divider>
|
|
||||||
<div id="skill-upgrade-info">
|
|
||||||
<div id="skill-upgrade-up">
|
|
||||||
<span id="skill-level"
|
|
||||||
>当前等级:{{ level }} / {{ skill.max }}</span
|
|
||||||
>
|
|
||||||
<Divider dashed class="upgrade-divider"></Divider>
|
|
||||||
<span
|
|
||||||
v-if="level < skill.max"
|
|
||||||
id="skill-consume"
|
|
||||||
:style="{ color: consume <= mdef ? '#fff' : '#f44' }"
|
|
||||||
>升级花费:{{ consume }}</span
|
|
||||||
>
|
|
||||||
<span v-else id="skill-consume" style="color: gold"
|
|
||||||
>已满级</span
|
|
||||||
>
|
|
||||||
<Divider dashed class="upgrade-divider"></Divider>
|
|
||||||
<Scroll id="front-scroll" :no-scroll="true"
|
|
||||||
><div id="skill-front">
|
|
||||||
<span>前置技能</span>
|
|
||||||
<span
|
|
||||||
v-for="str of front"
|
|
||||||
:style="{
|
|
||||||
color: str.startsWith('a') ? '#fff' : '#f44'
|
|
||||||
}"
|
|
||||||
>{{ str.slice(1) }}</span
|
|
||||||
>
|
|
||||||
</div></Scroll
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div id="skill-upgrade-bottom">
|
|
||||||
<Divider dashed class="upgrade-divider"></Divider>
|
|
||||||
<div id="skill-chapter">
|
|
||||||
<span class="button-text" @click="selectChapter(-1)"
|
|
||||||
><LeftOutlined
|
|
||||||
/></span>
|
|
||||||
|
|
||||||
<span>{{ chapterDict[chapter] }}</span>
|
|
||||||
|
|
||||||
<span class="button-text" @click="selectChapter(1)"
|
|
||||||
><RightOutlined
|
|
||||||
/></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
||||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
|
|
||||||
import Scroll from '../components/scroll.vue';
|
|
||||||
import { splitText } from '../utils';
|
|
||||||
import { isMobile, tip } from '../use';
|
|
||||||
import { sleep } from 'mutate-animate';
|
|
||||||
import { gameKey } from '@motajs/system-action';
|
|
||||||
import { IMountedVBind } from '../interface';
|
|
||||||
import { isNil } from 'lodash-es';
|
|
||||||
import { Divider } from 'ant-design-vue';
|
|
||||||
|
|
||||||
const props = defineProps<IMountedVBind>();
|
|
||||||
|
|
||||||
const skillTree = Mota.require('@user/data-state');
|
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement;
|
|
||||||
let ctx: CanvasRenderingContext2D;
|
|
||||||
|
|
||||||
const chapterDict = {
|
|
||||||
chapter1: '第一章',
|
|
||||||
chapter2: '第二章'
|
|
||||||
};
|
|
||||||
|
|
||||||
const selected = ref(0);
|
|
||||||
const chapter = ref<keyof typeof chapterDict>('chapter1');
|
|
||||||
const update = ref(false);
|
|
||||||
|
|
||||||
flags.skillTree ??= 0;
|
|
||||||
|
|
||||||
const s = skillTree.skills;
|
|
||||||
|
|
||||||
const chapterList = Object.keys(s) as (keyof typeof chapterDict)[];
|
|
||||||
|
|
||||||
selected.value = s[chapterList[flags.skillTree]][0].index;
|
|
||||||
chapter.value = chapterList[flags.skillTree];
|
|
||||||
|
|
||||||
watch(selected, draw);
|
|
||||||
watch(update, () => (mdef.value = core.status.hero.mdef));
|
|
||||||
|
|
||||||
const mdef = ref(core.status.hero.mdef);
|
|
||||||
|
|
||||||
const skill = computed(() => {
|
|
||||||
update.value;
|
|
||||||
return skillTree.getSkillFromIndex(selected.value)!;
|
|
||||||
});
|
|
||||||
|
|
||||||
const skills = computed(() => {
|
|
||||||
return s[chapter.value];
|
|
||||||
});
|
|
||||||
|
|
||||||
const desc = computed(() => {
|
|
||||||
return eval(
|
|
||||||
'`' +
|
|
||||||
splitText(skill.value.desc).replace(/level(:\d+)?/g, (str, $1) => {
|
|
||||||
if ($1) return `skillTree.getSkillLevel(${$1})`;
|
|
||||||
else return `skillTree.getSkillLevel(${skill.value.index})`;
|
|
||||||
}) +
|
|
||||||
'`'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const effect = computed(() => {
|
|
||||||
return [0, 1].map(v => {
|
|
||||||
update.value = update.value;
|
|
||||||
const prefix = v === 0 ? '当前效果:' : '下一级效果:';
|
|
||||||
const level = skillTree.getSkillLevel(skill.value.index);
|
|
||||||
const content = skill.value.effect(level + v);
|
|
||||||
return prefix + content.join('');
|
|
||||||
}) as [string, string];
|
|
||||||
});
|
|
||||||
|
|
||||||
const dict = computed(() => {
|
|
||||||
const dict: Record<number, number> = {};
|
|
||||||
const all = skills.value;
|
|
||||||
all.forEach((v, i) => {
|
|
||||||
dict[v.index] = i;
|
|
||||||
});
|
|
||||||
return dict;
|
|
||||||
});
|
|
||||||
|
|
||||||
const front = computed(() => {
|
|
||||||
return skill.value.front.map(v => {
|
|
||||||
return `${skillTree.getSkillLevel(v[0]) >= v[1] ? 'a' : 'b'}${
|
|
||||||
v[1]
|
|
||||||
}级 ${skills.value[dict.value[v[0]]].title}`;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const consume = computed(() => {
|
|
||||||
update.value;
|
|
||||||
return skillTree.getSkillConsume(selected.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const level = computed(() => {
|
|
||||||
update.value;
|
|
||||||
return skillTree.getSkillLevel(selected.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
function exit() {
|
|
||||||
props.controller.close(props.num);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resize() {
|
|
||||||
canvas.width = canvas.scrollWidth * devicePixelRatio;
|
|
||||||
canvas.height = canvas.scrollHeight * devicePixelRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
const d = dict.value;
|
|
||||||
const w = canvas.width;
|
|
||||||
const per = w / 11;
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
skills.value.forEach(v => {
|
|
||||||
const [x, y] = v.loc.map(v => v * 2 - 1);
|
|
||||||
// 技能连线
|
|
||||||
v.front.forEach(([skill], i) => {
|
|
||||||
const s = skills.value[d[skill]];
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x * per + per / 2, y * per + per / 2);
|
|
||||||
ctx.lineTo(
|
|
||||||
...(s.loc.map(v => (v * 2 - 1) * per + per / 2) as LocArr)
|
|
||||||
);
|
|
||||||
if (skillTree.getSkillLevel(s.index) < v.front[i][1])
|
|
||||||
ctx.strokeStyle = '#aaa';
|
|
||||||
else if (skillTree.getSkillLevel(s.index) === s.max)
|
|
||||||
ctx.strokeStyle = '#ff0';
|
|
||||||
else ctx.strokeStyle = '#0f8';
|
|
||||||
ctx.lineWidth = devicePixelRatio;
|
|
||||||
ctx.stroke();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
skills.value.forEach(v => {
|
|
||||||
const [x, y] = v.loc.map(v => v * 2 - 1);
|
|
||||||
const level = skillTree.getSkillLevel(v.index);
|
|
||||||
// 技能图标
|
|
||||||
ctx.save();
|
|
||||||
ctx.lineWidth = per * 0.06;
|
|
||||||
if (selected.value === v.index) {
|
|
||||||
ctx.strokeStyle = '#ff0';
|
|
||||||
ctx.lineWidth *= 2;
|
|
||||||
} else if (level === 0) ctx.strokeStyle = '#888';
|
|
||||||
else if (level === v.max) ctx.strokeStyle = '#F7FF68';
|
|
||||||
else ctx.strokeStyle = '#00FF69';
|
|
||||||
ctx.strokeRect(x * per, y * per, per, per);
|
|
||||||
const img =
|
|
||||||
core.material.images.images[`skill${v.index}.png` as ImageIds];
|
|
||||||
ctx.drawImage(img, x * per, y * per, per, per);
|
|
||||||
if (selected.value === v.index) {
|
|
||||||
ctx.fillStyle = '#ff04';
|
|
||||||
ctx.fillRect(x * per, y * per, per, per);
|
|
||||||
}
|
|
||||||
ctx.restore();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function click(e: MouseEvent) {
|
|
||||||
const px = e.offsetX;
|
|
||||||
const py = e.offsetY;
|
|
||||||
const w = canvas.width / devicePixelRatio;
|
|
||||||
const per = w / 11;
|
|
||||||
const x = Math.floor(px / per);
|
|
||||||
const y = Math.floor(py / per);
|
|
||||||
if (x % 2 !== 1 || y % 2 !== 1) return;
|
|
||||||
const sx = Math.floor(x / 2) + 1;
|
|
||||||
const sy = Math.floor(y / 2) + 1;
|
|
||||||
const skill = skills.value.find(v => v.loc[0] === sx && v.loc[1] === sy);
|
|
||||||
if (!skill) return;
|
|
||||||
if (selected.value !== skill.index) selected.value = skill.index;
|
|
||||||
else {
|
|
||||||
upgrade(skill.index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function upgrade(index: number) {
|
|
||||||
const success = skillTree.upgradeSkill(index);
|
|
||||||
if (!success) tip('error', '升级失败!');
|
|
||||||
else {
|
|
||||||
tip('success', '升级成功!');
|
|
||||||
update.value = !update.value;
|
|
||||||
core.status.route.push(`skill:${selected.value}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gameKey.use(props.ui.symbol);
|
|
||||||
gameKey
|
|
||||||
.realize('exit', () => {
|
|
||||||
exit();
|
|
||||||
})
|
|
||||||
.realize('confirm', () => {
|
|
||||||
upgrade(selected.value);
|
|
||||||
})
|
|
||||||
.realize('skillTree', () => {
|
|
||||||
exit();
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
canvas = document.getElementById('skill-canvas') as HTMLCanvasElement;
|
|
||||||
ctx = canvas.getContext('2d')!;
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
resize();
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
draw();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await sleep(50);
|
|
||||||
canvas.addEventListener('click', click);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
gameKey.dispose(props.ui.symbol);
|
|
||||||
});
|
|
||||||
|
|
||||||
function selectChapter(delta: number) {
|
|
||||||
const now = chapterList.indexOf(chapter.value);
|
|
||||||
const to = now + delta;
|
|
||||||
|
|
||||||
if (!isNil(chapterList[to]) && flags.chapter > to) {
|
|
||||||
selected.value = s[chapterList[to]][0].index;
|
|
||||||
chapter.value = chapterList[to];
|
|
||||||
update.value = !update.value;
|
|
||||||
flags.skillTree = to;
|
|
||||||
draw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
#skill-tree {
|
|
||||||
width: 90vh;
|
|
||||||
height: 90vh;
|
|
||||||
font-family: 'normal';
|
|
||||||
font-size: 150%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skill-title {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 130%;
|
|
||||||
height: 5vh;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tools {
|
|
||||||
height: 5vh;
|
|
||||||
font-size: 3.2vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skill-info {
|
|
||||||
height: 24vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
#divider {
|
|
||||||
width: 100%;
|
|
||||||
margin: 1vh 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#divider-split {
|
|
||||||
margin: 1vh 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#divider-vertical {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skill-bottom {
|
|
||||||
height: 53vh;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skill-canvas {
|
|
||||||
height: 53vh;
|
|
||||||
width: 53vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skill-effect {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skill-consume {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
height: 4vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skill-upgrade-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
padding-top: 1vh;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
#skill-upgrade-up {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skill-upgrade-bottom {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.upgrade-divider {
|
|
||||||
margin: 1vh 0;
|
|
||||||
border-color: #ddd4;
|
|
||||||
}
|
|
||||||
|
|
||||||
#front-scroll {
|
|
||||||
width: 100%;
|
|
||||||
max-height: 30vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skill-front {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
#skill-tree {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 100%;
|
|
||||||
padding: 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skill-title {
|
|
||||||
width: 100%;
|
|
||||||
font-size: 130%;
|
|
||||||
height: 5vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
#divider-vertical {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skill-bottom {
|
|
||||||
height: auto;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#skill-canvas {
|
|
||||||
height: 35vh;
|
|
||||||
width: 35vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#front-scroll {
|
|
||||||
height: 18vh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,670 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="start">
|
|
||||||
<div id="start-div">
|
|
||||||
<img id="background" :src="bg.src" />
|
|
||||||
<div id="start-main">
|
|
||||||
<div id="title">人类:开天辟地</div>
|
|
||||||
<div id="settings">
|
|
||||||
<div
|
|
||||||
id="sound"
|
|
||||||
class="setting-buttons"
|
|
||||||
:checked="soundChecked"
|
|
||||||
@click="bgm"
|
|
||||||
>
|
|
||||||
<sound-outlined />
|
|
||||||
<span v-if="!soundChecked" id="sound-del"></span>
|
|
||||||
</div>
|
|
||||||
<fullscreen-outlined
|
|
||||||
v-if="!fullscreen"
|
|
||||||
class="button-text setting-buttons2"
|
|
||||||
@click="setFullscreen"
|
|
||||||
/>
|
|
||||||
<fullscreen-exit-outlined
|
|
||||||
v-else
|
|
||||||
class="button-text setting-buttons2"
|
|
||||||
@click="setFullscreen"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div id="background-gradient"></div>
|
|
||||||
<div id="buttons">
|
|
||||||
<right-outlined id="cursor" />
|
|
||||||
<TransitionGroup name="start">
|
|
||||||
<span
|
|
||||||
class="start-button"
|
|
||||||
v-for="(v, i) of toshow"
|
|
||||||
:id="v"
|
|
||||||
:key="v"
|
|
||||||
:selected="selected === v"
|
|
||||||
:showed="showed"
|
|
||||||
:index="i"
|
|
||||||
:length="text[i].length"
|
|
||||||
@click="clickStartButton(v)"
|
|
||||||
@mouseenter="
|
|
||||||
movein(
|
|
||||||
$event.target as HTMLElement,
|
|
||||||
toshow.length - i - 1
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>{{ text[i] }}</span
|
|
||||||
>
|
|
||||||
</TransitionGroup>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="listen" @mousemove="onmove"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
|
|
||||||
import {
|
|
||||||
RightOutlined,
|
|
||||||
SoundOutlined,
|
|
||||||
FullscreenOutlined,
|
|
||||||
FullscreenExitOutlined
|
|
||||||
} from '@ant-design/icons-vue';
|
|
||||||
import { sleep } from 'mutate-animate';
|
|
||||||
import { doByInterval, triggerFullscreen } from '../utils';
|
|
||||||
import { isMobile } from '../use';
|
|
||||||
import { gameKey } from '@motajs/system-action';
|
|
||||||
import { mainSetting } from '../preset/settingIns';
|
|
||||||
import { mat4 } from 'gl-matrix';
|
|
||||||
import { IMountedVBind } from '../interface';
|
|
||||||
|
|
||||||
const props = defineProps<IMountedVBind>();
|
|
||||||
|
|
||||||
const bg = core.material.images.images['bg.webp'];
|
|
||||||
|
|
||||||
let startdiv: HTMLDivElement;
|
|
||||||
let start: HTMLDivElement;
|
|
||||||
let main: HTMLDivElement;
|
|
||||||
let cursor: HTMLElement;
|
|
||||||
let background: HTMLImageElement;
|
|
||||||
|
|
||||||
let buttons: HTMLSpanElement[] = [];
|
|
||||||
|
|
||||||
let played: boolean = false;
|
|
||||||
const soundChecked = ref(false);
|
|
||||||
const fullscreen = ref(!!document.fullscreenElement);
|
|
||||||
|
|
||||||
const showed = ref(false);
|
|
||||||
|
|
||||||
const text1 = ['开始游戏', '读取存档', '录像回放', '查看成就'].reverse();
|
|
||||||
const text2 = ['轮回', '分支', '观测', '回忆'].reverse();
|
|
||||||
|
|
||||||
const ids = ['start-game', 'load-game', 'replay', 'achievement'].reverse();
|
|
||||||
const hardIds = ['easy', 'hard-hard', 'back'].reverse();
|
|
||||||
const hard = ['简单', '困难', '返回'].reverse();
|
|
||||||
const text = ref(text1);
|
|
||||||
const toshow = reactive<string[]>([]);
|
|
||||||
|
|
||||||
const selected = ref('start-game');
|
|
||||||
|
|
||||||
const perspective = mat4.create();
|
|
||||||
|
|
||||||
function resize() {
|
|
||||||
if (!window.core) return;
|
|
||||||
const scale = core.domStyle.scale;
|
|
||||||
const h = core._PY_;
|
|
||||||
const height = h * scale;
|
|
||||||
const width = height * 1.5;
|
|
||||||
if (!isMobile) {
|
|
||||||
startdiv.style.width = `${width}px`;
|
|
||||||
startdiv.style.height = `${height}px`;
|
|
||||||
main.style.fontSize = `${scale * 16}px`;
|
|
||||||
} else {
|
|
||||||
startdiv.style.width = `${window.innerWidth}px`;
|
|
||||||
startdiv.style.height = `${(window.innerHeight * 2) / 3}px`;
|
|
||||||
main.style.fontSize = `${scale * 8}px`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showCursor() {
|
|
||||||
cursor.style.opacity = '1';
|
|
||||||
setCursor(buttons[0], 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置光标位置
|
|
||||||
*/
|
|
||||||
function setCursor(ele: HTMLSpanElement, i: number) {
|
|
||||||
if (!ele) return;
|
|
||||||
const style = getComputedStyle(ele);
|
|
||||||
cursor.style.top = `${
|
|
||||||
parseFloat(style.height) * (i + 0.5) -
|
|
||||||
parseFloat(style.marginBottom) * (1 - i)
|
|
||||||
}px`;
|
|
||||||
cursor.style.left = `${
|
|
||||||
parseFloat(style.left) - 20 * core.domStyle.scale
|
|
||||||
}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clickStartButton(id: string) {
|
|
||||||
if (id === 'start-game') showHard();
|
|
||||||
if (id === 'back') setButtonAnimate();
|
|
||||||
if (id === 'easy' || id === 'hard-hard') {
|
|
||||||
start.style.opacity = '0';
|
|
||||||
await sleep(600);
|
|
||||||
core.startGame(id === 'easy' ? 'easy' : 'hard');
|
|
||||||
}
|
|
||||||
if (id === 'load-game') {
|
|
||||||
start.style.top = '200vh';
|
|
||||||
core.load();
|
|
||||||
}
|
|
||||||
if (id === 'replay') core.chooseReplayFile();
|
|
||||||
if (id === 'achievement') {
|
|
||||||
props.controller.open('achievement');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onmove(e: MouseEvent) {
|
|
||||||
if (!window.core) return;
|
|
||||||
const { offsetX, offsetY } = e;
|
|
||||||
const ele = e.target as HTMLDivElement;
|
|
||||||
const style = getComputedStyle(ele);
|
|
||||||
const width = parseFloat(style.width);
|
|
||||||
const height = parseFloat(style.height);
|
|
||||||
const cx = width / 2;
|
|
||||||
const cy = height / 2;
|
|
||||||
const dx = (offsetX - cx) / cx;
|
|
||||||
const dy = (offsetY - cy) / cy;
|
|
||||||
|
|
||||||
const matrix = mat4.identity(perspective);
|
|
||||||
mat4.scale(matrix, matrix, [1.2, 1.2, 1]);
|
|
||||||
mat4.rotateX(matrix, matrix, -(dy * 10 * Math.PI) / 180);
|
|
||||||
mat4.rotateY(matrix, matrix, (dx * 10 * Math.PI) / 180);
|
|
||||||
|
|
||||||
const end = Array.from(matrix).join(',');
|
|
||||||
background.style.transform = `perspective(${
|
|
||||||
1000 * core.domStyle.scale
|
|
||||||
}px)matrix3d(${end})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function movein(button: HTMLElement, i: number) {
|
|
||||||
setCursor(button, i);
|
|
||||||
selected.value = button.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
gameKey.use(props.ui.symbol);
|
|
||||||
gameKey
|
|
||||||
.realize(
|
|
||||||
'@start_up',
|
|
||||||
() => {
|
|
||||||
const i = toshow.indexOf(selected.value);
|
|
||||||
const next = toshow[i + 1];
|
|
||||||
if (!next) return;
|
|
||||||
selected.value = next;
|
|
||||||
setCursor(buttons[toshow.length - i - 2], toshow.length - i - 2);
|
|
||||||
},
|
|
||||||
{ type: 'down-repeat' }
|
|
||||||
)
|
|
||||||
.realize(
|
|
||||||
'@start_down',
|
|
||||||
() => {
|
|
||||||
const i = toshow.indexOf(selected.value);
|
|
||||||
const next = toshow[i - 1];
|
|
||||||
if (!next) return;
|
|
||||||
selected.value = next;
|
|
||||||
setCursor(buttons[toshow.length - i], toshow.length - i);
|
|
||||||
},
|
|
||||||
{ type: 'down-repeat' }
|
|
||||||
)
|
|
||||||
.realize('confirm', () => {
|
|
||||||
clickStartButton(selected.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
function bgm() {
|
|
||||||
// core.triggerBgm();
|
|
||||||
soundChecked.value = !soundChecked.value;
|
|
||||||
mainSetting.setValue('audio.bgmEnabled', soundChecked.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setFullscreen() {
|
|
||||||
const index = toshow.length - toshow.indexOf(selected.value) - 1;
|
|
||||||
await triggerFullscreen(!fullscreen.value);
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
fullscreen.value = !!document.fullscreenElement;
|
|
||||||
setCursor(buttons[index], index);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始 -> 难度
|
|
||||||
*/
|
|
||||||
async function showHard() {
|
|
||||||
cursor.style.transition =
|
|
||||||
'left 0.4s ease-out, top 0.4s ease-out, opacity 0.4s linear';
|
|
||||||
cursor.style.opacity = '0';
|
|
||||||
buttons.forEach(v => (v.style.transition = ''));
|
|
||||||
|
|
||||||
await doByInterval(
|
|
||||||
Array(4).fill(() => ids.unshift(toshow.pop()!)),
|
|
||||||
150
|
|
||||||
);
|
|
||||||
await sleep(250);
|
|
||||||
text.value = hard;
|
|
||||||
|
|
||||||
await doByInterval(
|
|
||||||
Array(3).fill(() => toshow.push(hardIds.shift()!)),
|
|
||||||
150
|
|
||||||
);
|
|
||||||
selected.value = 'easy';
|
|
||||||
nextTick(() => {
|
|
||||||
buttons = toshow
|
|
||||||
.map(v => document.getElementById(v) as HTMLSpanElement)
|
|
||||||
.reverse();
|
|
||||||
cursor.style.opacity = '1';
|
|
||||||
setCursor(buttons[0], 0);
|
|
||||||
});
|
|
||||||
await sleep(600);
|
|
||||||
buttons.forEach(
|
|
||||||
v =>
|
|
||||||
(v.style.transition =
|
|
||||||
'transform 0.3s ease-out, color 0.3s ease-out')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 难度 | 无 -> 初始
|
|
||||||
*/
|
|
||||||
async function setButtonAnimate() {
|
|
||||||
if (toshow.length > 0) {
|
|
||||||
cursor.style.transition =
|
|
||||||
'left 0.4s ease-out, top 0.4s ease-out, opacity 0.4s linear';
|
|
||||||
cursor.style.opacity = '0';
|
|
||||||
buttons.forEach(v => (v.style.transition = ''));
|
|
||||||
await doByInterval(
|
|
||||||
Array(3).fill(() => hardIds.unshift(toshow.pop()!)),
|
|
||||||
150
|
|
||||||
);
|
|
||||||
}
|
|
||||||
text.value = text1;
|
|
||||||
if (played) {
|
|
||||||
text.value = text2;
|
|
||||||
}
|
|
||||||
await sleep(250);
|
|
||||||
|
|
||||||
await doByInterval(
|
|
||||||
Array(4).fill(() => toshow.push(ids.shift()!)),
|
|
||||||
150
|
|
||||||
);
|
|
||||||
|
|
||||||
selected.value = 'start-game';
|
|
||||||
nextTick(() => {
|
|
||||||
buttons = toshow
|
|
||||||
.map(v => document.getElementById(v) as HTMLSpanElement)
|
|
||||||
.reverse();
|
|
||||||
cursor.style.opacity = '1';
|
|
||||||
setCursor(buttons[0], 0);
|
|
||||||
buttons.forEach((v, i) => {});
|
|
||||||
});
|
|
||||||
if (!showed.value) await sleep(1200);
|
|
||||||
else await sleep(600);
|
|
||||||
|
|
||||||
buttons.forEach(
|
|
||||||
v =>
|
|
||||||
v &&
|
|
||||||
(v.style.transition =
|
|
||||||
'transform 0.3s ease-out, color 0.3s ease-out')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
cursor = document.getElementById('cursor')!;
|
|
||||||
startdiv = document.getElementById('start-div') as HTMLDivElement;
|
|
||||||
main = document.getElementById('start-main') as HTMLDivElement;
|
|
||||||
start = document.getElementById('start') as HTMLDivElement;
|
|
||||||
background = document.getElementById('background') as HTMLImageElement;
|
|
||||||
|
|
||||||
window.addEventListener('resize', resize);
|
|
||||||
resize();
|
|
||||||
|
|
||||||
soundChecked.value = mainSetting.getValue('audio.bgmEnabled', true);
|
|
||||||
const { bgmController } = Mota.require('@user/client-modules');
|
|
||||||
bgmController.play('title.opus');
|
|
||||||
|
|
||||||
start.style.opacity = '1';
|
|
||||||
if (played) {
|
|
||||||
text.value = text2;
|
|
||||||
hard.splice(1, 0, '挑战');
|
|
||||||
}
|
|
||||||
setButtonAnimate().then(() => (showed.value = true));
|
|
||||||
await sleep(1000);
|
|
||||||
showCursor();
|
|
||||||
await sleep(1200);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', resize);
|
|
||||||
gameKey.dispose(props.ui.symbol);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
#start {
|
|
||||||
position: relative;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.6s ease-out;
|
|
||||||
background-color: black;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
#start-div {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
object-fit: contain;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#background {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
filter: sepia(30%) contrast(115%);
|
|
||||||
transform: scale(120%);
|
|
||||||
}
|
|
||||||
|
|
||||||
#background-gradient {
|
|
||||||
z-index: 2;
|
|
||||||
position: absolute;
|
|
||||||
width: 200%;
|
|
||||||
height: 100%;
|
|
||||||
left: -100%;
|
|
||||||
background-image: linear-gradient(
|
|
||||||
45deg,
|
|
||||||
transparent 0%,
|
|
||||||
transparent 30%,
|
|
||||||
#000 60%,
|
|
||||||
#000 100%
|
|
||||||
);
|
|
||||||
animation: gradient 4s ease-out 0.5s 1 normal forwards;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#listen {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#start-main {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 16px;
|
|
||||||
|
|
||||||
#title {
|
|
||||||
margin-top: 7%;
|
|
||||||
text-align: center;
|
|
||||||
font: 4em 'normal';
|
|
||||||
font-weight: 200;
|
|
||||||
background-image: linear-gradient(
|
|
||||||
to right,
|
|
||||||
rgb(0, 0, 0),
|
|
||||||
rgb(44, 44, 44),
|
|
||||||
rgb(136, 0, 214),
|
|
||||||
rgb(0, 2, 97),
|
|
||||||
rgb(0, 2, 97)
|
|
||||||
);
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
text-shadow:
|
|
||||||
1px 1px 4px rgba(0, 0, 0, 0.5),
|
|
||||||
-1px -1px 3px rgba(255, 255, 255, 0.3),
|
|
||||||
5px 5px 5px rgba(0, 0, 0, 0.4);
|
|
||||||
filter: brightness(1.8);
|
|
||||||
user-select: none;
|
|
||||||
animation: opacity 3s ease-out 0.5s 1 normal forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
#buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
justify-content: center;
|
|
||||||
position: absolute;
|
|
||||||
left: 18%;
|
|
||||||
bottom: 10%;
|
|
||||||
filter: brightness(120%) contrast(110%);
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
#cursor {
|
|
||||||
text-shadow: 2px 2px 3px black;
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
animation: cursor 2.5s linear 0s infinite normal running;
|
|
||||||
transition:
|
|
||||||
left 0.4s ease-out,
|
|
||||||
top 0.4s ease-out,
|
|
||||||
opacity 1.5s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-button {
|
|
||||||
position: relative;
|
|
||||||
font: bold 1.5em 'normal';
|
|
||||||
text-shadow:
|
|
||||||
1px 1px 2px rgba(0, 0, 0, 0.4),
|
|
||||||
0px 0px 1px rgba(255, 255, 255, 0.3);
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-button[index='1'][length='4'] {
|
|
||||||
left: 7.5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-button[index='2'][length='4'] {
|
|
||||||
left: 15%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-button[index='3'][length='4'] {
|
|
||||||
left: 22.5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-button[index='1'][length='2'] {
|
|
||||||
left: 15%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-button[index='2'][length='2'] {
|
|
||||||
left: 30%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-button[index='3'][length='2'] {
|
|
||||||
left: 45%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#start-game {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgb(255, 255, 255),
|
|
||||||
rgb(0, 255, 255)
|
|
||||||
);
|
|
||||||
margin-bottom: 8%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#load-game {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgb(255, 255, 255),
|
|
||||||
rgb(0, 255, 55)
|
|
||||||
);
|
|
||||||
margin-bottom: 8%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#replay {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgb(255, 255, 255),
|
|
||||||
rgb(255, 251, 0)
|
|
||||||
);
|
|
||||||
margin-bottom: 8%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#achievement {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgb(255, 255, 255),
|
|
||||||
rgb(0, 208, 255)
|
|
||||||
);
|
|
||||||
margin-bottom: 8%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#easy {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgb(255, 255, 255),
|
|
||||||
rgb(87, 255, 72)
|
|
||||||
);
|
|
||||||
margin-bottom: 16%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#hard-hard {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgb(255, 255, 255),
|
|
||||||
rgb(255, 0, 0)
|
|
||||||
);
|
|
||||||
margin-bottom: 16%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#back {
|
|
||||||
background-image: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgb(255, 255, 255),
|
|
||||||
rgb(132, 132, 132)
|
|
||||||
);
|
|
||||||
margin-bottom: 16%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#settings {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
justify-content: flex-start;
|
|
||||||
right: 5%;
|
|
||||||
bottom: 10%;
|
|
||||||
font-size: 1.3em;
|
|
||||||
z-index: 1;
|
|
||||||
width: 50%;
|
|
||||||
|
|
||||||
.setting-buttons {
|
|
||||||
margin-left: 4%;
|
|
||||||
color: white;
|
|
||||||
transition: color 0.2s linear;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-buttons2 {
|
|
||||||
margin-left: 4%;
|
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sound {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sound[checked='false'] {
|
|
||||||
color: rgb(255, 43, 43);
|
|
||||||
}
|
|
||||||
|
|
||||||
#sound:hover {
|
|
||||||
color: aqua;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sound[checked='false']:hover {
|
|
||||||
color: rgb(253, 139, 139);
|
|
||||||
}
|
|
||||||
|
|
||||||
#sound-del {
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-bottom: 2px solid #aaa;
|
|
||||||
transform: translate(-85%, -50%) rotate(-45deg) scale(1.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-button {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-button[selected='true'] {
|
|
||||||
color: transparent;
|
|
||||||
transform: scale(115%) translate(7.5%);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes cursor {
|
|
||||||
from {
|
|
||||||
transform: rotateX(0deg) scaleY(0.7);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotateX(360deg) scaleY(0.7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gradient {
|
|
||||||
from {
|
|
||||||
left: -100%;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes opacity {
|
|
||||||
from {
|
|
||||||
color: #bbb;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-enter-active {
|
|
||||||
transition: all 1.2s ease-out;
|
|
||||||
}
|
|
||||||
.start-enter-active[showed='true'] {
|
|
||||||
transition: all 0.6s ease-out;
|
|
||||||
}
|
|
||||||
.start-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(20px);
|
|
||||||
}
|
|
||||||
.start-leave-active {
|
|
||||||
transition: all 0.4s ease-out;
|
|
||||||
}
|
|
||||||
.start-leave-to {
|
|
||||||
transform: translateX(-20px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
|
||||||
#buttons {
|
|
||||||
font-size: 250%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#start-main {
|
|
||||||
#title {
|
|
||||||
font-size: 700%;
|
|
||||||
}
|
|
||||||
#settings {
|
|
||||||
font-size: 400%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,46 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span ref="span" class="tip">{{ nowTip }}</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
|
||||||
import tips from '../data/tips.json';
|
|
||||||
|
|
||||||
const span = ref<HTMLSpanElement>();
|
|
||||||
const nowTip = ref<string>();
|
|
||||||
|
|
||||||
function changeTip() {
|
|
||||||
const tip = tips[Math.floor(Math.random() * tips.length)];
|
|
||||||
if (!tip) return;
|
|
||||||
nowTip.value = '小贴士:' + tip;
|
|
||||||
}
|
|
||||||
|
|
||||||
const interval = window.setInterval(changeTip, 30_000);
|
|
||||||
|
|
||||||
function resize() {
|
|
||||||
const game = core.dom.gameDraw;
|
|
||||||
const right = window.innerWidth - (game.offsetLeft + game.offsetWidth);
|
|
||||||
const bottom = window.innerHeight - game.offsetTop;
|
|
||||||
if (!span.value) return;
|
|
||||||
span.value.style.right = `${right}px`;
|
|
||||||
span.value.style.bottom = `${bottom + 6}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('resize', resize);
|
|
||||||
resize();
|
|
||||||
changeTip();
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', resize);
|
|
||||||
window.clearInterval(interval);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.tip {
|
|
||||||
position: fixed;
|
|
||||||
font: 150% 'normal';
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||||||
import { KeyCode } from '@motajs/client-base';
|
import { KeyCode } from '@motajs/client-base';
|
||||||
import { KeyboardEmits, Keyboard, isAssist } from '@motajs/system-action';
|
import { KeyboardEmits, Keyboard, isAssist } from '@motajs/system-action';
|
||||||
import { mainUi, fixedUi } from './preset/uiIns';
|
import { mainUi } from './preset/uiIns';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 唤起虚拟键盘,并获取到一次按键操作
|
* 唤起虚拟键盘,并获取到一次按键操作
|
||||||
@ -39,9 +39,3 @@ export function getVitualKeyOnce(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openDanmakuPoster() {
|
|
||||||
if (!fixedUi.hasName('danmakuEditor')) {
|
|
||||||
fixedUi.open('danmakuEditor');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -11,7 +11,7 @@ main.floors.empty=
|
|||||||
"images": [],
|
"images": [],
|
||||||
"ratio": 1,
|
"ratio": 1,
|
||||||
"defaultGround": "ground",
|
"defaultGround": "ground",
|
||||||
"bgm": "bgm.mp3",
|
"bgm": "bgm.opus",
|
||||||
"firstArrive": [],
|
"firstArrive": [],
|
||||||
"eachArrive": [],
|
"eachArrive": [],
|
||||||
"parallelDo": "",
|
"parallelDo": "",
|
||||||
|
@ -8,7 +8,7 @@ main.floors.sample0=
|
|||||||
"canUseQuickShop": true,
|
"canUseQuickShop": true,
|
||||||
"defaultGround": "ground",
|
"defaultGround": "ground",
|
||||||
"images": [],
|
"images": [],
|
||||||
"bgm": "bgm.mp3",
|
"bgm": "bgm.opus",
|
||||||
"ratio": 1,
|
"ratio": 1,
|
||||||
"map": [
|
"map": [
|
||||||
[ 0, 0,220, 0, 0, 20, 87, 3, 58, 59, 60, 61, 64],
|
[ 0, 0,220, 0, 0, 20, 87, 3, 58, 59, 60, 61, 64],
|
||||||
|
Loading…
Reference in New Issue
Block a user