mirror of
https://github.com/unanmed/HumanBreak.git
synced 2026-11-15 03:04:30 +08:00
refactor: UI 移入 @motajs/legacy-ui
This commit is contained in:
1 parent
4aa894a204
commit
693e531639
85 files changed
+940
-1252
No files matched your search
@@ -1,413 +0,0 @@
|
||||
<template>
|
||||
<div :id="`box-${id}`" class="box">
|
||||
<div :id="`box-main-${id}`" class="box-main" @click="click">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div
|
||||
v-if="dragable"
|
||||
:id="`box-move-${id}`"
|
||||
class="box-move"
|
||||
:selected="moveSelected"
|
||||
>
|
||||
<drag-outlined :id="`box-drag-${id}`" class="box-drag" />
|
||||
</div>
|
||||
<div
|
||||
v-if="resizable"
|
||||
:id="`box-size-${id}`"
|
||||
class="box-size"
|
||||
:selected="moveSelected"
|
||||
>
|
||||
<ArrowsAltOutlined :id="`box-resize-${id}`" class="box-resize" />
|
||||
</div>
|
||||
<div
|
||||
class="border border-vertical border-left"
|
||||
:id="`border-left-${id}`"
|
||||
:selected="moveSelected && resizable"
|
||||
:selectable="resizable"
|
||||
></div>
|
||||
<div
|
||||
class="border border-horizontal border-top"
|
||||
:id="`border-top-${id}`"
|
||||
:selected="moveSelected && resizable"
|
||||
:selectable="resizable"
|
||||
></div>
|
||||
<div
|
||||
class="border border-vertical border-right"
|
||||
:id="`border-right-${id}`"
|
||||
:selected="moveSelected && resizable"
|
||||
:selectable="resizable"
|
||||
></div>
|
||||
<div
|
||||
class="border border-horizontal border-bottom"
|
||||
:id="`border-bottom-${id}`"
|
||||
:selected="moveSelected && resizable"
|
||||
:selectable="resizable"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, onUpdated, ref, watch } from 'vue';
|
||||
import { ArrowsAltOutlined, DragOutlined } from '@ant-design/icons-vue';
|
||||
import { isMobile, useDrag, cancelGlobalDrag } from '../plugin/use';
|
||||
import { has, requireUniqueSymbol } from '../plugin/utils';
|
||||
|
||||
// todo: 重写
|
||||
|
||||
const props = defineProps<{
|
||||
dragable?: boolean;
|
||||
resizable?: boolean;
|
||||
left?: number;
|
||||
top?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:left', data: number): void;
|
||||
(e: 'update:top', data: number): void;
|
||||
(e: 'update:width', data: number): void;
|
||||
(e: 'update:height', data: number): void;
|
||||
}>();
|
||||
|
||||
const id = requireUniqueSymbol().toFixed(0);
|
||||
|
||||
const moveSelected = ref(false);
|
||||
let moveTimeout = 0;
|
||||
let main: HTMLDivElement;
|
||||
let leftB: HTMLDivElement;
|
||||
let rightB: HTMLDivElement;
|
||||
let topB: HTMLDivElement;
|
||||
let bottomB: HTMLDivElement;
|
||||
let drag: HTMLElement;
|
||||
let size: HTMLElement;
|
||||
|
||||
const width = ref(
|
||||
isMobile ? window.innerWidth - 100 : window.innerHeight * 0.175
|
||||
);
|
||||
const height = ref(isMobile ? 250 : window.innerHeight - 100);
|
||||
const left = ref(isMobile ? 30 : 50);
|
||||
const top = ref(isMobile ? 30 : 50);
|
||||
|
||||
watch(left, n => emits('update:left', n));
|
||||
watch(top, n => emits('update:top', n));
|
||||
watch(width, n => emits('update:width', n));
|
||||
watch(height, n => emits('update:height', n));
|
||||
|
||||
async function click() {
|
||||
moveSelected.value = true;
|
||||
moveTimeout = window.setTimeout(() => {
|
||||
moveSelected.value = false;
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
|
||||
function clampX(x: number) {
|
||||
if (x < 16) x = 16;
|
||||
const mx = window.innerWidth - 16 - width.value;
|
||||
if (x > mx) x = mx;
|
||||
return x;
|
||||
}
|
||||
|
||||
function clampY(y: number) {
|
||||
if (y < 16) y = 16;
|
||||
const my = window.innerHeight - 16 - height.value;
|
||||
if (y > my) y = my;
|
||||
return y;
|
||||
}
|
||||
|
||||
function clampPos(x: number, y: number) {
|
||||
return { x: clampX(x), y: clampY(y) };
|
||||
}
|
||||
|
||||
function clampWidth(w: number) {
|
||||
if (w < 16) w = 16;
|
||||
const mw = window.innerWidth - 16 - left.value;
|
||||
if (w > mw) w = mw;
|
||||
return w;
|
||||
}
|
||||
|
||||
function clampHeight(h: number) {
|
||||
if (h < 16) h = 16;
|
||||
const mh = window.innerHeight - 16 - top.value;
|
||||
if (h > mh) h = mh;
|
||||
return h;
|
||||
}
|
||||
|
||||
function clampSize(w: number, h: number) {
|
||||
return { w: clampWidth(w), h: clampHeight(h) };
|
||||
}
|
||||
|
||||
function dragFn(x: number, y: number) {
|
||||
const { x: tx, y: ty } = clampPos(x + 8, y + 8);
|
||||
left.value = tx;
|
||||
top.value = ty;
|
||||
main.style.left = `${tx}px`;
|
||||
main.style.top = `${ty}px`;
|
||||
clearTimeout(moveTimeout);
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
}
|
||||
|
||||
let right = left.value + width.value;
|
||||
function leftDrag(x: number, y: number) {
|
||||
const tx = clampX(x);
|
||||
main.style.left = `${tx}px`;
|
||||
width.value = right - tx;
|
||||
left.value = tx;
|
||||
main.style.width = `${width.value}px`;
|
||||
}
|
||||
|
||||
let bottom = top.value + height.value;
|
||||
function topDrag(x: number, y: number) {
|
||||
const ty = clampY(y);
|
||||
main.style.top = `${ty}px`;
|
||||
height.value = bottom - ty;
|
||||
top.value = ty;
|
||||
main.style.height = `${height.value}px`;
|
||||
}
|
||||
|
||||
function rightDrag(x: number, y: number) {
|
||||
const w = clampWidth(x - main.offsetLeft);
|
||||
width.value = w;
|
||||
main.style.width = `${w}px`;
|
||||
}
|
||||
|
||||
function bottomDrag(x: number, y: number) {
|
||||
const h = clampHeight(y - main.offsetTop);
|
||||
height.value = h;
|
||||
main.style.height = `${h}px`;
|
||||
}
|
||||
|
||||
function resizeBox(x: number, y: number) {
|
||||
const { w, h } = clampSize(x - main.offsetLeft - 8, y - main.offsetTop - 8);
|
||||
width.value = w;
|
||||
height.value = h;
|
||||
main.style.width = `${w}px`;
|
||||
main.style.height = `${h}px`;
|
||||
}
|
||||
|
||||
function resize() {
|
||||
main = document.getElementById(`box-${id}`) as HTMLDivElement;
|
||||
leftB = document.getElementById(`border-left-${id}`) as HTMLDivElement;
|
||||
topB = document.getElementById(`border-top-${id}`) as HTMLDivElement;
|
||||
rightB = document.getElementById(`border-right-${id}`) as HTMLDivElement;
|
||||
bottomB = document.getElementById(`border-bottom-${id}`) as HTMLDivElement;
|
||||
drag = document.getElementById(`box-drag-${id}`) as HTMLElement;
|
||||
size = document.getElementById(`box-resize-${id}`) as HTMLElement;
|
||||
|
||||
if (!main) return;
|
||||
|
||||
if (has(props.width)) width.value = props.width;
|
||||
if (has(props.height)) height.value = props.height;
|
||||
if (has(props.left)) left.value = props.left;
|
||||
if (has(props.top)) top.value = props.top;
|
||||
|
||||
const beforeWidth = width.value;
|
||||
const beforeHeight = height.value;
|
||||
width.value = 16;
|
||||
height.value = 16;
|
||||
|
||||
left.value = clampX(left.value);
|
||||
top.value = clampY(top.value);
|
||||
main.style.left = `${left.value}px`;
|
||||
main.style.top = `${top.value}px`;
|
||||
|
||||
width.value = clampWidth(beforeWidth);
|
||||
height.value = clampHeight(beforeHeight);
|
||||
main.style.width = `${width.value}px`;
|
||||
main.style.height = `${height.value}px`;
|
||||
}
|
||||
|
||||
onUpdated(resize);
|
||||
|
||||
onMounted(async () => {
|
||||
resize();
|
||||
|
||||
if (!main) return;
|
||||
|
||||
if (props.dragable) {
|
||||
useDrag(
|
||||
drag,
|
||||
dragFn,
|
||||
(x, y) => {
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
},
|
||||
() => {
|
||||
moveSelected.value = false;
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if (props.resizable) {
|
||||
useDrag(
|
||||
leftB,
|
||||
leftDrag,
|
||||
(x, y) => {
|
||||
right = left.value + width.value;
|
||||
},
|
||||
void 0,
|
||||
true
|
||||
);
|
||||
|
||||
useDrag(
|
||||
topB,
|
||||
topDrag,
|
||||
(x, y) => {
|
||||
bottom = top.value + height.value;
|
||||
},
|
||||
void 0,
|
||||
true
|
||||
);
|
||||
|
||||
useDrag(rightB, rightDrag, void 0, void 0, true);
|
||||
useDrag(bottomB, bottomDrag, void 0, void 0, true);
|
||||
useDrag(size, resizeBox, void 0, void 0, true);
|
||||
}
|
||||
window.addEventListener('resize', resize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', resize);
|
||||
if (props.dragable) cancelGlobalDrag(dragFn);
|
||||
if (props.resizable) {
|
||||
cancelGlobalDrag(leftDrag);
|
||||
cancelGlobalDrag(topDrag);
|
||||
cancelGlobalDrag(rightDrag);
|
||||
cancelGlobalDrag(bottomDrag);
|
||||
cancelGlobalDrag(resizeBox);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.box {
|
||||
width: 300px;
|
||||
height: calc(100vh - 100px);
|
||||
position: fixed;
|
||||
left: 50px;
|
||||
top: 50px;
|
||||
display: flex;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.box-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.box-move {
|
||||
transition: font-size 0.3s ease-out;
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
top: -32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.box-size {
|
||||
transition: font-size 0.3s ease-out;
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.box-drag {
|
||||
cursor: all-scroll;
|
||||
user-select: none;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.box-resize {
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
transform: rotateX(180deg);
|
||||
user-select: none;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.box-move[selected='false'],
|
||||
.box-size[selected='false'] {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.box-move[selected='true'],
|
||||
.box-size[selected='true'] {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.border {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
|
||||
.border-horizontal {
|
||||
width: 100%;
|
||||
height: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.border-horizontal[selected='true'][selectable='true'] {
|
||||
transform: scaleY(300%);
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.border-horizontal:hover[selectable='true'],
|
||||
.border-horizontal:active[selectable='true'] {
|
||||
transform: scaleY(500%);
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.border-vertical {
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.border-vertical[selected='true'][selectable='true'] {
|
||||
transform: scaleX(300%);
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.border-vertical:hover[selectable='true'],
|
||||
.border-vertical:active[selectable='true'] {
|
||||
transform: scaleX(500%);
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.border-left {
|
||||
left: 0;
|
||||
border-left: 2px solid #ddd9;
|
||||
}
|
||||
|
||||
.border-right {
|
||||
right: 0;
|
||||
border-right: 2px solid #ddd9;
|
||||
}
|
||||
|
||||
.border-top {
|
||||
top: 0;
|
||||
border-top: 2px solid #ddd9;
|
||||
}
|
||||
|
||||
.border-bottom {
|
||||
bottom: 0;
|
||||
border-bottom: 2px solid #ddd9;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.box {
|
||||
width: calc(100vw - 100px);
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,90 +0,0 @@
|
||||
<template>
|
||||
<canvas
|
||||
:width="width ?? 32"
|
||||
:height="height ?? 32"
|
||||
:id="`box-animate-${id}`"
|
||||
></canvas>
|
||||
</template>
|
||||
|
||||
<script lang="tsx" setup>
|
||||
import { onMounted, onUnmounted, onUpdated } from 'vue';
|
||||
import { addAnimate, removeAnimate } from '../plugin/animateController';
|
||||
import { has, requireUniqueSymbol } from '../plugin/utils';
|
||||
|
||||
const id = requireUniqueSymbol().toFixed(0);
|
||||
|
||||
const props = defineProps<{
|
||||
id: AllIds | 'hero' | 'none';
|
||||
noborder?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
noAnimate?: boolean;
|
||||
}>();
|
||||
|
||||
let c: HTMLCanvasElement;
|
||||
let ctx: CanvasRenderingContext2D;
|
||||
|
||||
let drawFn: () => void;
|
||||
|
||||
function draw() {
|
||||
if (has(drawFn)) removeAnimate(drawFn);
|
||||
|
||||
const cls = core.getClsFromId(props.id as AllIds);
|
||||
const frames = core.getAnimateFrames(cls);
|
||||
const w = props.width ?? 32;
|
||||
const h = props.height ?? 32;
|
||||
|
||||
if (!props.noborder) {
|
||||
c.style.border = '1.5px solid #ddd';
|
||||
c.style.backgroundColor = '#222';
|
||||
}
|
||||
|
||||
const scale = window.devicePixelRatio;
|
||||
|
||||
c.style.width = `${w}px`;
|
||||
c.style.height = `${h}px`;
|
||||
c.width = scale * w;
|
||||
c.height = scale * h;
|
||||
ctx.scale(scale, scale);
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
|
||||
if (props.id === 'none') return;
|
||||
|
||||
if (props.id === 'hero') {
|
||||
const img = core.material.images.hero;
|
||||
ctx.drawImage(img, 0, 0, img.width / 4, img.height / 4, 0, 0, w, h);
|
||||
} else if (frames === 1) {
|
||||
core.drawIcon(ctx, props.id, 0, 0, props.width, props.height);
|
||||
} else {
|
||||
drawFn = () => {
|
||||
core.clearMap(ctx);
|
||||
const frame = props.noAnimate
|
||||
? 0
|
||||
: core.status.globalAnimateStatus % frames;
|
||||
core.drawIcon(ctx, props.id as AllIds, 0, 0, w, h, frame);
|
||||
};
|
||||
|
||||
drawFn();
|
||||
if (!props.noAnimate) {
|
||||
addAnimate(drawFn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
removeAnimate(drawFn);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
c = document.getElementById(`box-animate-${id}`) as HTMLCanvasElement;
|
||||
ctx = c.getContext('2d')!;
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
draw();
|
||||
});
|
||||
|
||||
onUpdated(() => {
|
||||
draw();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -1,144 +0,0 @@
|
||||
<template>
|
||||
<div class="colomn">
|
||||
<div class="tools">
|
||||
<span class="button-text" @click="emits('close')"
|
||||
><left-outlined /> 返回</span
|
||||
>
|
||||
</div>
|
||||
<div class="column-main" :id="`column-${id}`">
|
||||
<Scroll class="column-left" :id="`column-left-${id}`">
|
||||
<slot name="left"></slot>
|
||||
</Scroll>
|
||||
<a-divider
|
||||
class="divider"
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
:type="isMobile ? 'horizontal' : 'vertical'"
|
||||
></a-divider>
|
||||
<Scroll class="column-right" :id="`column-right-${id}`"
|
||||
><slot name="right"></slot
|
||||
></Scroll>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, onUpdated, ref } from 'vue';
|
||||
import { LeftOutlined } from '@ant-design/icons-vue';
|
||||
import Scroll from './scroll.vue';
|
||||
import { isMobile } from '../plugin/use';
|
||||
import { has, requireUniqueSymbol } from '../plugin/utils';
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
width?: number;
|
||||
height?: number;
|
||||
left?: number;
|
||||
right?: number;
|
||||
}>();
|
||||
|
||||
const id = requireUniqueSymbol().toFixed(0);
|
||||
|
||||
let main: HTMLDivElement;
|
||||
let left: HTMLDivElement;
|
||||
let right: HTMLDivElement;
|
||||
|
||||
function resize() {
|
||||
main = document.getElementById(`column-${id}`) as HTMLDivElement;
|
||||
left = document.getElementById(`column-left-${id}`) as HTMLDivElement;
|
||||
right = document.getElementById(`column-right-${id}`) as HTMLDivElement;
|
||||
|
||||
if (has(props.width) && !isMobile) main.style.width = `${props.width}%`;
|
||||
if (has(props.height)) main.style.height = `${props.height}%`;
|
||||
if (has(props.left)) left.style.flexBasis = `${props.left}%`;
|
||||
if (has(props.right)) right.style.flexBasis = `${props.right}%`;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
resize();
|
||||
});
|
||||
onUpdated(resize);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.colomn {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'normal';
|
||||
font-size: 150%;
|
||||
user-select: none;
|
||||
}
|
||||
.column-main {
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.column-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.column-item {
|
||||
width: 100%;
|
||||
padding: 1% 3% 1% 3%;
|
||||
}
|
||||
|
||||
.column-item[selectable='false'] {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.column-left {
|
||||
flex-basis: 40%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.column-right {
|
||||
flex-basis: 60%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tools {
|
||||
width: 100%;
|
||||
font-family: 'normal';
|
||||
font-size: 3.2vh;
|
||||
height: 5vh;
|
||||
position: fixed;
|
||||
left: 10vw;
|
||||
top: 5vh;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.column-main {
|
||||
flex-direction: column;
|
||||
width: 90%;
|
||||
height: 75%;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
margin: 5% 0 5% 0;
|
||||
}
|
||||
|
||||
.column-left {
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
.column-right {
|
||||
height: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,238 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="enemy-container"
|
||||
@click="select"
|
||||
@mousemove="enter"
|
||||
:selected="selected"
|
||||
:style="style"
|
||||
>
|
||||
<div class="info">
|
||||
<div class="leftbar">
|
||||
<span class="name">{{ enemy.enemy.enemy.name }}</span>
|
||||
<BoxAnimate
|
||||
:id="enemy.enemy.id"
|
||||
:width="isMobile ? 32 : w"
|
||||
:height="isMobile ? 32 : w"
|
||||
style="margin: 5%"
|
||||
></BoxAnimate>
|
||||
<div
|
||||
class="special-text"
|
||||
v-if="has(enemy.special) && enemy.special.length > 0"
|
||||
>
|
||||
<template v-for="(text, i) in enemy.showSpecial">
|
||||
<span
|
||||
v-if="i < (isMobile ? 1 : 2)"
|
||||
:style="{ color: text[2] }"
|
||||
> {{ text[0] }} </span
|
||||
>
|
||||
<span v-if="i === (isMobile ? 1 : 2)">...</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="special-text" v-else>无属性</div>
|
||||
</div>
|
||||
<a-divider
|
||||
type="vertical"
|
||||
dashed
|
||||
style="height: 100%; margin: 0 3% 0 1%; border-color: #ddd4"
|
||||
></a-divider>
|
||||
<div class="rightbar">
|
||||
<div class="detail">
|
||||
<div class="detail-info">
|
||||
<span style="color: lightgreen"
|
||||
>生命 {{
|
||||
core.formatBigNumber(enemy.enemy.info.hp)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<span style="color: lightcoral"
|
||||
>攻击 {{
|
||||
core.formatBigNumber(enemy.enemy.info.atk)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<span style="color: lightblue"
|
||||
>防御 {{
|
||||
core.formatBigNumber(enemy.enemy.info.def)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<span style="color: lightyellow"
|
||||
>金币 {{
|
||||
core.formatBigNumber(enemy.enemy.enemy.money)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<span style="color: lawngreen"
|
||||
>经验 {{
|
||||
core.formatBigNumber(enemy.enemy.enemy.exp)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<span :style="{ color: enemy.damageColor }"
|
||||
>伤害 {{
|
||||
enemy.damage
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="detail-info">
|
||||
<span style="color: lightsalmon"
|
||||
>临界 {{
|
||||
enemy.critical
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<span style="color: lightpink"
|
||||
>减伤 <span
|
||||
:style="{
|
||||
color: 'lightpink'
|
||||
}"
|
||||
>{{ enemy.criticalDam }}</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div class="detail-info">
|
||||
<span style="color: cyan"
|
||||
>{{
|
||||
core.formatBigNumber(core.status.thisMap.ratio)
|
||||
}}防 {{
|
||||
core.formatBigNumber(enemy.defDam)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { has } from '../plugin/utils';
|
||||
import BoxAnimate from '../components/boxAnimate.vue';
|
||||
import { isMobile } from '../plugin/use';
|
||||
import { ToShowEnemy } from '../plugin/ui/book';
|
||||
import border from '@/data/enemyBorder.json';
|
||||
|
||||
const props = defineProps<{
|
||||
enemy: ToShowEnemy;
|
||||
selected?: boolean;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'select'): void;
|
||||
(e: 'hover'): void;
|
||||
}>();
|
||||
|
||||
const core = window.core;
|
||||
|
||||
const w = window.innerWidth * 0.032;
|
||||
|
||||
const style = border[props.enemy.enemy.enemy.id as keyof typeof border] ?? {};
|
||||
|
||||
/**
|
||||
* 选择这个怪物时
|
||||
*/
|
||||
function select(e: MouseEvent) {
|
||||
emits('select');
|
||||
}
|
||||
|
||||
function enter() {
|
||||
emits('hover');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.enemy-container {
|
||||
border: 1.5px solid transparent;
|
||||
border-radius: 15px;
|
||||
transition: all 0.2s linear;
|
||||
height: 100%;
|
||||
|
||||
.info {
|
||||
flex-basis: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.enemy-container[selected='true'] {
|
||||
border: 1.5px solid gold;
|
||||
}
|
||||
|
||||
.leftbar {
|
||||
width: 15%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 120%;
|
||||
padding-left: 1%;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.special-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-items: space-between;
|
||||
}
|
||||
|
||||
.rightbar {
|
||||
font-size: 140%;
|
||||
width: 85%;
|
||||
height: 100%;
|
||||
padding: 1.5vh 0 1.5vh 0;
|
||||
|
||||
.detail {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
height: 100%;
|
||||
|
||||
.detail-info {
|
||||
flex-basis: 33.3%;
|
||||
line-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.rightbar {
|
||||
width: 80%;
|
||||
font-size: 110%;
|
||||
}
|
||||
|
||||
.leftbar {
|
||||
width: 20%;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.enemy-container {
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,176 +0,0 @@
|
||||
<template>
|
||||
<canvas :id="`minimap-${id}`"></canvas>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { requireUniqueSymbol } from '../plugin/utils';
|
||||
import { MinimapDrawer, getArea } from '../plugin/ui/fly';
|
||||
import { hook } from '@/game/game';
|
||||
import { useDrag, useWheel } from '@/plugin/use';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
|
||||
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();
|
||||
|
||||
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(() => {
|
||||
hook.off('afterChangeFloor', onChange);
|
||||
hook.off('afterBattle', afterBattle);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -1,263 +0,0 @@
|
||||
<template>
|
||||
<div :id="`scroll-div-${id}`" class="scroll-main">
|
||||
<div class="main-div">
|
||||
<div :id="`content-${id}`" class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
<canvas :id="`scroll-${id}`" class="scroll"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { onMounted, onUnmounted, onUpdated } from 'vue';
|
||||
import { cancelGlobalDrag, useDrag, useWheel } from '../plugin/use';
|
||||
import { requireUniqueSymbol } from '@/plugin/utils';
|
||||
|
||||
let main: HTMLDivElement;
|
||||
|
||||
const props = defineProps<{
|
||||
now?: number;
|
||||
type?: 'vertical' | 'horizontal';
|
||||
drag?: boolean;
|
||||
width?: number;
|
||||
update?: boolean;
|
||||
noScroll?: boolean;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'update:now', value: number): void;
|
||||
(e: 'update:drag', value: boolean): void;
|
||||
(e: 'update:update'): void;
|
||||
}>();
|
||||
|
||||
let now = 0;
|
||||
let total = 0;
|
||||
|
||||
const id = requireUniqueSymbol().toFixed(0);
|
||||
const scale = window.devicePixelRatio;
|
||||
const width = props.width ?? 20;
|
||||
|
||||
const cssTarget = props.type === 'horizontal' ? 'left' : 'top';
|
||||
const canvasAttr = props.type === 'horizontal' ? 'width' : 'height';
|
||||
|
||||
let ctx: CanvasRenderingContext2D;
|
||||
let content: HTMLDivElement;
|
||||
let fromSelf = false;
|
||||
|
||||
const resize = async () => {
|
||||
await calHeight(false);
|
||||
draw();
|
||||
};
|
||||
|
||||
/** 绘制 */
|
||||
function draw() {
|
||||
if (total === 0) return;
|
||||
if (total < ctx.canvas[canvasAttr] / scale) {
|
||||
now = 0;
|
||||
} else if (now > total - ctx.canvas[canvasAttr] / scale) {
|
||||
now = total - ctx.canvas[canvasAttr] / scale;
|
||||
} else if (now < 0) {
|
||||
now = 0;
|
||||
}
|
||||
if (props.noScroll) return;
|
||||
const w = ctx.canvas.width;
|
||||
const h = ctx.canvas.height;
|
||||
emits('update:now', now);
|
||||
const length =
|
||||
Math.min(ctx.canvas[canvasAttr] / total / scale, 1) *
|
||||
ctx.canvas[canvasAttr];
|
||||
const py = (now / total) * ctx.canvas[canvasAttr];
|
||||
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
ctx.beginPath();
|
||||
if (props.type === 'horizontal') {
|
||||
ctx.moveTo(Math.max(py + 5, 5), h / 2);
|
||||
ctx.lineTo(Math.min(py + length - 5, ctx.canvas.width - 5), h / 2);
|
||||
} else {
|
||||
ctx.moveTo(w / 2, Math.max(py + 5, 5));
|
||||
ctx.lineTo(w / 2, Math.min(py + length - 5, ctx.canvas.height - 5));
|
||||
}
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineWidth = 6;
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算元素总长度
|
||||
*/
|
||||
async function calHeight(first: boolean = false) {
|
||||
if (!first) {
|
||||
await sleep(20);
|
||||
}
|
||||
|
||||
const canvas = ctx.canvas;
|
||||
const style2 = getComputedStyle(canvas);
|
||||
if (!props.type || props.type === 'vertical') {
|
||||
if (props.noScroll) {
|
||||
canvas.style.width = `0px`;
|
||||
} else {
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.width = width * scale;
|
||||
canvas.height = parseFloat(style2.height) * scale;
|
||||
}
|
||||
} else {
|
||||
main.style.flexDirection = 'column';
|
||||
if (props.noScroll) {
|
||||
canvas.style.height = `0px`;
|
||||
} else {
|
||||
canvas.style.height = `${width}px`;
|
||||
canvas.style.width = '98%';
|
||||
canvas.style.margin = '0 1% 0 1%';
|
||||
canvas.width = parseFloat(style2.width) * scale;
|
||||
canvas.height = width * scale;
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise<void>(res => {
|
||||
requestAnimationFrame(() => {
|
||||
total =
|
||||
props.type === 'horizontal'
|
||||
? content.scrollWidth
|
||||
: content.scrollHeight;
|
||||
res();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function scroll() {
|
||||
draw();
|
||||
content.style[cssTarget] = `${-now}px`;
|
||||
}
|
||||
|
||||
onUpdated(async () => {
|
||||
if (fromSelf) return;
|
||||
now = props.now ?? now;
|
||||
content.style.transition = `${cssTarget} 0.2s ease-out`;
|
||||
await calHeight(false);
|
||||
scroll();
|
||||
});
|
||||
|
||||
let last: number;
|
||||
let contentLast: number;
|
||||
|
||||
function canvasDrag(x: number, y: number) {
|
||||
emits('update:drag', true);
|
||||
const d = props.type === 'horizontal' ? x : y;
|
||||
const dy = d - last;
|
||||
last = d;
|
||||
if (ctx.canvas[canvasAttr] < total * scale)
|
||||
now += ((dy * total) / ctx.canvas[canvasAttr]) * scale;
|
||||
scroll();
|
||||
}
|
||||
|
||||
function contentDrag(x: number, y: number) {
|
||||
emits('update:drag', true);
|
||||
const d = props.type === 'horizontal' ? x : y;
|
||||
const dy = d - contentLast;
|
||||
contentLast = d;
|
||||
if (ctx.canvas[canvasAttr] < total * scale) now -= dy;
|
||||
scroll();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
main = document.getElementById(`scroll-div-${id}`) as HTMLDivElement;
|
||||
const d = document.getElementById(`content-${id}`) as HTMLDivElement;
|
||||
content = d;
|
||||
const canvas = document.getElementById(`scroll-${id}`) as HTMLCanvasElement;
|
||||
ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
|
||||
if (!props.noScroll) {
|
||||
// 绑定滚动条拖拽事件
|
||||
useDrag(
|
||||
canvas,
|
||||
canvasDrag,
|
||||
(x, y) => {
|
||||
fromSelf = true;
|
||||
last = props.type === 'horizontal' ? x : y;
|
||||
content.style.transition = '';
|
||||
},
|
||||
() => {
|
||||
setTimeout(() => emits('update:drag', false));
|
||||
fromSelf = false;
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// 绑定文本拖拽事件
|
||||
useDrag(
|
||||
content,
|
||||
contentDrag,
|
||||
(x, y) => {
|
||||
fromSelf = true;
|
||||
contentLast = props.type === 'horizontal' ? x : y;
|
||||
content.style.transition = '';
|
||||
},
|
||||
() => {
|
||||
setTimeout(() => emits('update:drag', false));
|
||||
fromSelf = false;
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
useWheel(content, (x, y) => {
|
||||
fromSelf = true;
|
||||
const d = x !== 0 ? x : y;
|
||||
if (Math.abs(d) > 30) {
|
||||
content.style.transition = `${cssTarget} 0.2s ease-out`;
|
||||
} else {
|
||||
content.style.transition = '';
|
||||
}
|
||||
now += d;
|
||||
scroll();
|
||||
fromSelf = false;
|
||||
});
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
await calHeight(true);
|
||||
await calHeight();
|
||||
draw();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', resize);
|
||||
cancelGlobalDrag(canvasDrag);
|
||||
cancelGlobalDrag(contentDrag);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.scroll {
|
||||
opacity: 0.2;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
|
||||
.scroll:hover {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.scroll:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.scroll-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-div {
|
||||
flex-basis: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ensureArray, tip } from '@/plugin/utils';
|
||||
import { ensureArray, tip } from '@motajs/legacy-ui';
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
|
||||
+2
-34
@@ -11,8 +11,7 @@ import {
|
||||
mainSetting,
|
||||
settingStorage
|
||||
} from './main/setting';
|
||||
import { KeyCode } from '@/plugin/keyCodes';
|
||||
import { status } from '@/plugin/ui/statusBar';
|
||||
import { KeyCode } from '@motajs/client';
|
||||
import '@/plugin';
|
||||
import './package';
|
||||
import { CustomToolbar } from './main/custom/toolbar';
|
||||
@@ -25,26 +24,13 @@ import {
|
||||
} from './main/custom/hotkey';
|
||||
import { Keyboard, generateKeyboardEvent } from './main/custom/keyboard';
|
||||
import './main/layout';
|
||||
import { MComponent, m } from './main/layout';
|
||||
import { createSettingComponents } from './main/init/settings';
|
||||
import {
|
||||
createToolbarComponents,
|
||||
createToolbarEditorComponents
|
||||
} from './main/init/toolbar';
|
||||
import { VirtualKey } from './main/init/misc';
|
||||
import * as utils from '@/plugin/utils';
|
||||
import * as use from '@/plugin/use';
|
||||
import * as mark from '@/plugin/mark';
|
||||
import * as keyCodes from '@/plugin/keyCodes';
|
||||
import { addAnimate, removeAnimate } from '@/plugin/animateController';
|
||||
import * as bookTools from '@/plugin/ui/book';
|
||||
import * as commonTools from '@/plugin/ui/common';
|
||||
import * as equipboxTools from '@/plugin/ui/equipbox';
|
||||
import * as fixedTools from '@/plugin/ui/fixed';
|
||||
import * as flyTools from '@/plugin/ui/fly';
|
||||
import * as statusBarTools from '@/plugin/ui/statusBar';
|
||||
import * as toolboxTools from '@/plugin/ui/toolbox';
|
||||
import * as UI from '@ui/index';
|
||||
import { UI } from '@motajs/legacy-ui';
|
||||
import Box from '@/components/box.vue';
|
||||
import BoxAnimate from '@/components/boxAnimate.vue';
|
||||
import Colomn from '@/components/colomn.vue';
|
||||
@@ -54,7 +40,6 @@ import EnemyCritical from '@/panel/enemyCritical.vue';
|
||||
import EnemySpecial from '@/panel/enemySpecial.vue';
|
||||
import EnemyTarget from '@/panel/enemyTarget.vue';
|
||||
import KeyboardPanel from '@/panel/keyboard.vue';
|
||||
import { MCGenerator } from './main/layout';
|
||||
import { logger } from '@motajs/common';
|
||||
import { Danmaku } from './main/custom/danmaku';
|
||||
import * as Shadow from './fx/shadow';
|
||||
@@ -73,16 +58,12 @@ Mota.register('class', 'Keyboard', Keyboard);
|
||||
Mota.register('class', 'MotaSetting', MotaSetting);
|
||||
Mota.register('class', 'SettingDisplayer', SettingDisplayer);
|
||||
Mota.register('class', 'UiController', UiController);
|
||||
Mota.register('class', 'MComponent', MComponent);
|
||||
Mota.register('class', 'Danmaku', Danmaku);
|
||||
// ----- 函数注册
|
||||
Mota.register('fn', 'm', m);
|
||||
Mota.register('fn', 'unwrapBinary', unwarpBinary);
|
||||
Mota.register('fn', 'checkAssist', checkAssist);
|
||||
Mota.register('fn', 'isAssist', isAssist);
|
||||
Mota.register('fn', 'generateKeyboardEvent', generateKeyboardEvent);
|
||||
Mota.register('fn', 'addAnimate', addAnimate);
|
||||
Mota.register('fn', 'removeAnimate', removeAnimate);
|
||||
// ----- 变量注册
|
||||
Mota.register('var', 'mainUi', mainUi);
|
||||
Mota.register('var', 'fixedUi', fixedUi);
|
||||
@@ -102,18 +83,6 @@ Mota.register('module', 'MiscComponents', {
|
||||
VirtualKey
|
||||
});
|
||||
Mota.register('module', 'RenderUtils', utils);
|
||||
Mota.register('module', 'Use', use);
|
||||
Mota.register('module', 'Mark', mark);
|
||||
Mota.register('module', 'KeyCodes', keyCodes);
|
||||
Mota.register('module', 'UITools', {
|
||||
book: bookTools,
|
||||
common: commonTools,
|
||||
equipbox: equipboxTools,
|
||||
fixed: fixedTools,
|
||||
fly: flyTools,
|
||||
statusBar: statusBarTools,
|
||||
toolbox: toolboxTools
|
||||
});
|
||||
Mota.register('module', 'UI', UI);
|
||||
Mota.register('module', 'UIComponents', {
|
||||
Box,
|
||||
@@ -126,7 +95,6 @@ Mota.register('module', 'UIComponents', {
|
||||
EnemyTarget,
|
||||
Keyboard: KeyboardPanel
|
||||
});
|
||||
Mota.register('module', 'MCGenerator', MCGenerator);
|
||||
Mota.register('module', 'Shadow', Shadow);
|
||||
Mota.register('module', 'Effect', {});
|
||||
Mota.register('module', 'Render', Render);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { KeyCode } from '@/plugin/keyCodes';
|
||||
import { KeyCode } from '@motajs/client';
|
||||
import { Hotkey, HotkeyData } from '../custom/hotkey';
|
||||
import type { HeroMover, IMoveController } from '@/game/state/move';
|
||||
import { Ticker } from 'mutate-animate';
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
getIconHeight,
|
||||
parseCss,
|
||||
tip
|
||||
} from '@/plugin/utils';
|
||||
} from '@motajs/legacy-ui';
|
||||
import axios, { AxiosResponse, toFormData } from 'axios';
|
||||
import { Component, VNode, h, shallowReactive } from 'vue';
|
||||
// /* @__PURE__ */ import { id, password } from '../../../../user';
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { KeyCode } from '@/plugin/keyCodes';
|
||||
import { deleteWith, generateBinary, keycode, spliceBy } from '@/plugin/utils';
|
||||
import { KeyCode } from '@motajs/client';
|
||||
import {
|
||||
deleteWith,
|
||||
generateBinary,
|
||||
keycode,
|
||||
spliceBy
|
||||
} from '@motajs/legacy-ui';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { EventEmitter, Listener } from '@motajs/legacy-common';
|
||||
import { KeyCode } from '@/plugin/keyCodes';
|
||||
import { KeyCode } from '@motajs/client';
|
||||
import { gameKey } from './hotkey';
|
||||
import { unwarpBinary } from './hotkey';
|
||||
import { deleteWith, flipBinary } from '@/plugin/utils';
|
||||
import { deleteWith, flipBinary } from '@motajs/legacy-ui';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { shallowReactive } from 'vue';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EventEmitter } from '@motajs/legacy-common';
|
||||
import { deleteWith, has } from '@/plugin/utils';
|
||||
import { deleteWith, has } from '@motajs/legacy-ui';
|
||||
import { Component, nextTick, reactive, shallowReactive } from 'vue';
|
||||
import { fixedUi } from '../init/ui';
|
||||
import { GameStorage } from '../storage';
|
||||
@@ -126,48 +126,6 @@ const misc: Misc = {
|
||||
}
|
||||
};
|
||||
|
||||
const playingDefaultTool: MiscToolbar = {
|
||||
id: '@defaults_misc',
|
||||
type: 'misc',
|
||||
folded: false,
|
||||
noDefaultAction: true,
|
||||
items: [
|
||||
'book',
|
||||
'fly',
|
||||
'save',
|
||||
'load',
|
||||
'toolbox',
|
||||
'equipbox',
|
||||
'shop',
|
||||
'virtualKey',
|
||||
'setting',
|
||||
'undo',
|
||||
'redo',
|
||||
'viewMap',
|
||||
'danmaku',
|
||||
'minimap'
|
||||
]
|
||||
};
|
||||
const replayingDefaultTool: MiscToolbar = {
|
||||
id: '@defaults_misc',
|
||||
type: 'misc',
|
||||
folded: false,
|
||||
noDefaultAction: true,
|
||||
items: [
|
||||
'startReplay',
|
||||
'stopReplay',
|
||||
'rewindReplay',
|
||||
'stepReplay',
|
||||
'book',
|
||||
'speedDownReplay',
|
||||
'speedUpReplay',
|
||||
'save',
|
||||
'viewMap',
|
||||
'danmaku',
|
||||
'minimap'
|
||||
]
|
||||
};
|
||||
|
||||
export class CustomToolbar extends EventEmitter<CustomToolbarEvent> {
|
||||
static num: number = 0;
|
||||
static list: CustomToolbar[] = shallowReactive([]);
|
||||
@@ -389,11 +347,11 @@ export class CustomToolbar extends EventEmitter<CustomToolbarEvent> {
|
||||
toolbarStorage.read();
|
||||
for (const [key, value] of Object.entries(toolbarStorage.data)) {
|
||||
const bar = this.get(key) ?? new CustomToolbar(key);
|
||||
bar.x = value.x;
|
||||
bar.y = value.y;
|
||||
bar.width = value.w;
|
||||
bar.height = value.h;
|
||||
for (const item of value.items) {
|
||||
bar.x = value!.x;
|
||||
bar.y = value!.y;
|
||||
bar.width = value!.w;
|
||||
bar.height = value!.h;
|
||||
for (const item of value!.items) {
|
||||
bar.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { KeyCode } from '@/plugin/keyCodes';
|
||||
import { KeyCode } from '@motajs/client';
|
||||
import { gameKey, HotkeyJSON } from '../custom/hotkey';
|
||||
import { openDanmakuPoster, tip } from '@/plugin/utils';
|
||||
import {
|
||||
openDanmakuPoster,
|
||||
tip,
|
||||
hasMarkedEnemy,
|
||||
markEnemy,
|
||||
unmarkEnemy
|
||||
} from '@motajs/legacy-ui';
|
||||
import { hovered } from './fixed';
|
||||
import { hasMarkedEnemy, markEnemy, unmarkEnemy } from '@/plugin/mark';
|
||||
import { mainUi } from './ui';
|
||||
import { GameStorage } from '../storage';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { KeyCode } from '@/plugin/keyCodes';
|
||||
import { KeyCode } from '@motajs/client';
|
||||
import { Keyboard } from '../custom/keyboard';
|
||||
|
||||
const qweKey = new Keyboard('qwe'); // 字母键盘,A-Z
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { KeyCode, KeyCodeUtils } from '@/plugin/keyCodes';
|
||||
import { KeyCode, KeyCodeUtils } from '@motajs/client';
|
||||
import { CustomToolbar } from '../custom/toolbar';
|
||||
import BoxAnimate from '@/components/boxAnimate.vue';
|
||||
import { checkAssist, unwarpBinary } from '../custom/hotkey';
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
flipBinary,
|
||||
getVitualKeyOnce,
|
||||
openDanmakuPoster
|
||||
} from '@/plugin/utils';
|
||||
} from '@motajs/legacy-ui';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import {
|
||||
Button,
|
||||
@@ -18,11 +18,10 @@ import {
|
||||
import { MotaSettingItem, mainSetting } from '../setting';
|
||||
import Minimap from '@/components/minimap.vue';
|
||||
import { gameKey } from '../custom/hotkey';
|
||||
import { FunctionalComponent, StyleValue, h, ref } from 'vue';
|
||||
import { FunctionalComponent, h, ref } from 'vue';
|
||||
import { mainUi } from './ui';
|
||||
import { isMobile } from '@/plugin/use';
|
||||
import { isMobile } from '@motajs/legacy-ui';
|
||||
import {
|
||||
BackwardFilled,
|
||||
BackwardOutlined,
|
||||
BorderOuterOutlined,
|
||||
CaretRightOutlined,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as UI from '@ui/.';
|
||||
import { UI } from '@motajs/legacy-ui';
|
||||
import * as MiscUI from './misc';
|
||||
import { GameUi, UiController } from '../custom/ui';
|
||||
import { mainSetting } from '../setting';
|
||||
@@ -26,7 +26,6 @@ mainUi.showAll();
|
||||
|
||||
export const fixedUi = new UiController(true);
|
||||
fixedUi.register(
|
||||
new GameUi('statusBar', UI.StatusBar),
|
||||
new GameUi('markedEnemy', UI.Marked),
|
||||
new GameUi('fixed', UI.Fixed),
|
||||
new GameUi('chapter', UI.Chapter),
|
||||
|
||||
@@ -1,509 +0,0 @@
|
||||
import {
|
||||
Component,
|
||||
RenderFunction,
|
||||
SetupContext,
|
||||
VNode,
|
||||
VNodeChild,
|
||||
defineComponent,
|
||||
h as hVue,
|
||||
isVNode,
|
||||
onMounted
|
||||
} from 'vue';
|
||||
import BoxAnimate from '@/components/boxAnimate.vue';
|
||||
import { ensureArray } from '@/plugin/utils';
|
||||
|
||||
interface VForRenderer {
|
||||
type: '@v-for';
|
||||
items: any[] | (() => any[]);
|
||||
map: (value: any, index: number) => VNode;
|
||||
}
|
||||
|
||||
interface MotaComponent extends MotaComponentConfig {
|
||||
type: string;
|
||||
children: (MComponent | MotaComponent | VNode)[];
|
||||
}
|
||||
|
||||
interface MotaComponentConfig {
|
||||
innerText?: string | (() => string);
|
||||
props?: Record<string, () => any>;
|
||||
component?: Component | MComponent;
|
||||
dComponent?: () => Component;
|
||||
/** 传递插槽 */
|
||||
slots?: Record<string, (props: Record<string, any>) => VNode | VNode[]>;
|
||||
vif?: () => boolean;
|
||||
velse?: boolean;
|
||||
}
|
||||
|
||||
type OnSetupFunction = (props: Record<string, any>, ctx: SetupContext) => void;
|
||||
type SetupFunction = (
|
||||
props: Record<string, any>,
|
||||
ctx: SetupContext
|
||||
) => RenderFunction | Promise<RenderFunction>;
|
||||
type RetFunction = (
|
||||
props: Record<string, any>,
|
||||
ctx: SetupContext
|
||||
) => VNodeChild | VNodeChild[];
|
||||
type OnMountedFunction = (
|
||||
props: Record<string, any>,
|
||||
ctx: SetupContext,
|
||||
canvas: HTMLCanvasElement[]
|
||||
) => void;
|
||||
|
||||
type NonComponentConfig = Omit<
|
||||
MotaComponentConfig,
|
||||
'innerText' | 'component' | 'slots' | 'dComponent'
|
||||
>;
|
||||
|
||||
type MComponentChildren =
|
||||
| (MComponent | MotaComponent | VNode)[]
|
||||
| MComponent
|
||||
| MotaComponent
|
||||
| VNode;
|
||||
|
||||
export class MComponent {
|
||||
static mountNum: number = 0;
|
||||
|
||||
content: (MotaComponent | VForRenderer)[] = [];
|
||||
|
||||
private onSetupFn?: OnSetupFunction;
|
||||
private setupFn?: SetupFunction;
|
||||
private onMountedFn?: OnMountedFunction;
|
||||
private retFn?: RetFunction;
|
||||
|
||||
private propsDef: Record<string, any> = {};
|
||||
private emitsDef: string[] = [];
|
||||
|
||||
/**
|
||||
* 定义一个props,是一个对象,键表示props名称,值表示类型,例如num: Number
|
||||
* 对于直接通过`UiController.open`方法打开的ui,应当包含以下两项
|
||||
* - num: ui的唯一标识符,类型为Number
|
||||
* - ui: 对于的GameUi实例,类型为GameUi
|
||||
* @param props 被定义的props
|
||||
*/
|
||||
defineProps(props: Record<string, any>) {
|
||||
this.propsDef = props;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义这个组件的emits,是一个字符串数组,表示emits的名称
|
||||
* @param emits 被定义的emits
|
||||
*/
|
||||
defineEmits(emits: string[]) {
|
||||
this.emitsDef = emits;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个div渲染内容
|
||||
* @param children 渲染内容的子内容
|
||||
* @param config 渲染内容的配置信息,参考 {@link MComponent.h}
|
||||
*/
|
||||
div(children?: MComponentChildren, config?: NonComponentConfig) {
|
||||
return this.h('div', children, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个span渲染内容
|
||||
* @param children 渲染内容的子内容
|
||||
* @param config 渲染内容的配置信息,参考 {@link MComponent.h}
|
||||
*/
|
||||
span(children?: MComponentChildren, config?: NonComponentConfig) {
|
||||
return this.h('span', children, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个canvas渲染内容
|
||||
* @param config 渲染内容的配置信息,参考 {@link MComponent.h}
|
||||
*/
|
||||
canvas(config?: NonComponentConfig) {
|
||||
return this.h('canvas', [], config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个文字渲染内容
|
||||
* @param text 要渲染的文字内容
|
||||
*/
|
||||
text(text: string | (() => string), config: NonComponentConfig = {}) {
|
||||
return this.h('text', [], { ...config, innerText: text });
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个组件渲染内容
|
||||
* @param component 要添加的组件
|
||||
* @param config 渲染内容的配置信息,参考 {@link MComponent.h}
|
||||
*/
|
||||
com(
|
||||
component: Component | MComponent,
|
||||
config?: Omit<MotaComponentConfig, 'innerText' | 'component'>
|
||||
) {
|
||||
return this.h(component, [], config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表渲染一系列内容
|
||||
* @param items 要遍历的列表,可以是一个数组,也可以是一个返回数组的函数
|
||||
* @param map 遍历函数,接收列表的每一项的值和索引,并返回一个VNode,VNode可以通过Vue.h函数,
|
||||
* 或者MComponent.vNode函数生成。
|
||||
*/
|
||||
vfor<T>(items: T[] | (() => T[]), map: (value: T, index: number) => VNode) {
|
||||
this.content.push(MCGenerator.vfor(items, map));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加渲染内容,注意区分组件和`MComponent`的区别,组件是经由`MComponent`的`export`函数输出的内容。
|
||||
* 该函数是对`Vue`的`h`函数的高度包装,将`h`函数抽象成为了一个模板,然后经由`export`函数导出后直接输出成为一个组件。
|
||||
* 而因此,几乎所有内容都要求传入一个函数,一般这个函数会在真正渲染的时候执行,并将返回值作为真正值传入。
|
||||
* 不过对于部分内容,例如`slots`和`vfor.map`,并不是这样的。具体用法请参考参数注释。
|
||||
* 注意如果使用了该包装,那么是无法实现响应式布局的,如果想要使用响应式布局,就必须调用`setup`方法,
|
||||
* 手写全部的`setup`函数。
|
||||
* @param type 要添加的渲染内容。
|
||||
* - 可以是一个字符串,表示dom元素,例如`div` `span`等,
|
||||
* - 可以是一个组件,也可以是一个`MComponent`,表示将其的导出作为组件。
|
||||
* - 除此之外,还可以填`text`,表示这个渲染内容是一个单独的文字,同时`children`会无效,
|
||||
* 必须填写`config`的`innerText`参数。
|
||||
* - 该值还可以是字符串`component`,表示动态组件,同时必须填入`config`的`component`参数,
|
||||
* 同时`children`会无效
|
||||
* - 该值不能填`@v-for`
|
||||
* @param children 该渲染内容的子内容。
|
||||
* - 可以是一个`MComponent`数组,数组内容即是子内容
|
||||
* - 也可以是一个`MComponent`,表示这个组件内容为子内容
|
||||
* @param config 渲染内容的配置内容,包含下列内容,均为可选。
|
||||
* - `innerText`: 当渲染内容为字符串时显示的内容,可以是字符串,或是返回字符串的函数
|
||||
* - `props`: 传入渲染内容的`props`,是一个对象,每个值都是一个函数,其返回值是真正传入的`props`
|
||||
* 对象的键是`prop`名称,如果是如`class` `id`这样的html属性,那么会视为其`attribute`,
|
||||
* 会符合`Vue`的`attribute`透传。对于以on开头,然后紧接着大写字母的属性,会被视为事件监听,
|
||||
* 即v-on
|
||||
* - `component`: 当为动态组件时,该项与`dComponent`必填其中之一,该项表示动态组件的内容
|
||||
* - `dComponent`: 当为动态组件时,该项与`component`必填其中之一,该项是一个函数,返回值表示动态组件的内容
|
||||
* 当`component`也填时,优先使用该项
|
||||
* - `slots`: 传递插槽,将内容传入渲染内容的插槽,是一个对象,每个对象都是一个函数,
|
||||
* 要求函数返回一个渲染VNode或数组,可以通过`MComponent.vNode`函数将组件转换成VNode数组,
|
||||
* 返回值直接作为插槽内容
|
||||
* - `vif`: 条件渲染,是一个函数,返回一个布尔值,表示条件是否满足,当`velse`为`true`时,
|
||||
* 条件渲染将会变成 `else-if`
|
||||
* - `velse`: 条件渲染,当前一个条件不满足时渲染该内容
|
||||
*/
|
||||
h(
|
||||
type: string | Component | MComponent,
|
||||
children?: MComponentChildren,
|
||||
config: MotaComponentConfig = {}
|
||||
): this {
|
||||
this.content.push(MCGenerator.h(type, children, config));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当setup被执行时,要执行的函数,接受props,没有返回值,可以不设置
|
||||
*/
|
||||
onSetup(fn: OnSetupFunction) {
|
||||
this.onSetupFn = fn;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当组件被挂载完毕后执行函数
|
||||
* @param fn 当组件被挂载完毕后执行的函数,接收props和当前级组件(不包含子组件)的所有画布作为参数
|
||||
* 当前级组件表示直接在当前组件中渲染的内容,不包括子组件,子组件的画布需要在其对应的函数中获取
|
||||
* 例如我在A组件中调用了B组件,那么我只能获取A组件的画布,而不能获取B组件的画布
|
||||
*/
|
||||
onMounted(fn: OnMountedFunction) {
|
||||
this.onMountedFn = fn;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完全设置setup执行函数,接收props, slots,并返回一个函数,函数返回VNode,可以不设置
|
||||
*/
|
||||
setup(fn: SetupFunction) {
|
||||
this.setupFn = fn;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完全设置setup返回的函数,可以不设置
|
||||
* @param fn setup返回的函数
|
||||
*/
|
||||
ret(fn: RetFunction) {
|
||||
this.retFn = fn;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将这个MComponent实例导出成为一个组件
|
||||
*/
|
||||
export() {
|
||||
if (!this.setupFn) {
|
||||
return defineComponent(
|
||||
(props, ctx) => {
|
||||
const mountNum = MComponent.mountNum++;
|
||||
this.onSetupFn?.(props, ctx);
|
||||
|
||||
onMounted(() => {
|
||||
this.onMountedFn?.(
|
||||
props,
|
||||
ctx,
|
||||
Array.from(
|
||||
document.getElementsByClassName(
|
||||
`--mota-component-canvas-${mountNum}`
|
||||
) as HTMLCollectionOf<HTMLCanvasElement>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
if (this.retFn) return () => this.retFn!(props, ctx);
|
||||
else {
|
||||
return () => {
|
||||
const vNodes = MComponent.vNode(
|
||||
this.content,
|
||||
mountNum
|
||||
);
|
||||
return vNodes;
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
emits: this.emitsDef,
|
||||
props: this.propsDef
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return defineComponent((props, ctx) => this.setupFn!(props, ctx));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将单个渲染内容生成为一个单个的VNode
|
||||
* @param child 要生成的单个渲染内容
|
||||
* @param mount 组件生成时的挂载id,一般不需要填,用于画布获取
|
||||
*/
|
||||
static vNodeS(child: MotaComponent, mount?: number) {
|
||||
return this.vNode([child], mount)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一个MComponent实例生成为一个VNode列表
|
||||
* @param mc 要生成VNode的组件
|
||||
* @param mount 组件生成时的挂载id,一般不需要填,用于画布获取
|
||||
*/
|
||||
static vNodeM(mc: MComponent, mount?: number) {
|
||||
return this.vNode(mc.content, mount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一系列MComponent内容生成为一个VNode列表
|
||||
* @param children 要生成VNode的内容列表
|
||||
* @param mount 组件生成时的挂载id,一般不需要填,用于画布获取
|
||||
*/
|
||||
static vNode(
|
||||
children: (MotaComponent | VForRenderer | VNode)[],
|
||||
mount?: number
|
||||
) {
|
||||
const mountNum = mount ?? this.mountNum++;
|
||||
|
||||
const res: VNode[] = [];
|
||||
const vifRes: Map<number, boolean> = new Map();
|
||||
children.forEach((v, i) => {
|
||||
if (isVNode(v)) {
|
||||
res.push(v);
|
||||
return;
|
||||
}
|
||||
if (v.type === '@v-for') {
|
||||
const node = v as VForRenderer;
|
||||
const items =
|
||||
typeof node.items === 'function'
|
||||
? node.items()
|
||||
: node.items;
|
||||
items.forEach((v, i) => {
|
||||
res.push(node.map(v, i));
|
||||
});
|
||||
} else {
|
||||
const node = v as MotaComponent;
|
||||
if (node.velse && vifRes.get(i - 1)) {
|
||||
vifRes.set(i, true);
|
||||
return;
|
||||
}
|
||||
let vif = true;
|
||||
if (node.vif) {
|
||||
vifRes.set(i, (vif = node.vif()));
|
||||
}
|
||||
if (!vif) return;
|
||||
const props = this.unwrapProps(node.props);
|
||||
if (v.type === 'component') {
|
||||
if (!v.component && !v.dComponent) {
|
||||
throw new Error(
|
||||
`Using dynamic component must provide component property.`
|
||||
);
|
||||
}
|
||||
if (v.dComponent) {
|
||||
res.push(hVue(v.dComponent(), props, v.slots));
|
||||
} else {
|
||||
if (v.component instanceof MComponent) {
|
||||
res.push(
|
||||
...MComponent.vNode(
|
||||
v.component.content,
|
||||
mountNum
|
||||
)
|
||||
);
|
||||
} else {
|
||||
res.push(hVue(v.component!, props, v.slots));
|
||||
}
|
||||
}
|
||||
} else if (v.type === 'text') {
|
||||
res.push(
|
||||
hVue(
|
||||
'span',
|
||||
typeof v.innerText === 'function'
|
||||
? v.innerText()
|
||||
: v.innerText
|
||||
)
|
||||
);
|
||||
} else if (v.type === 'canvas') {
|
||||
const cls = `--mota-component-canvas-${mountNum}`;
|
||||
const mix = !!props.class ? cls + ' ' + props.class : cls;
|
||||
props.class = mix;
|
||||
res.push(hVue('canvas', props, node.slots));
|
||||
} else {
|
||||
// 这个时候不可能会有插槽,只会有子内容,因此直接渲染子内容
|
||||
const content = node.children;
|
||||
const vn = this.vNode(
|
||||
content
|
||||
.map(v => (v instanceof MComponent ? v.content : v))
|
||||
.flat(),
|
||||
mountNum
|
||||
);
|
||||
res.push(hVue(v.type, props, vn));
|
||||
}
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取props的真实值。因为传入渲染内容的props是一个函数,因此需要一层调用
|
||||
* @param props 要获取的props
|
||||
*/
|
||||
static unwrapProps(props?: Record<string, () => any>): Record<string, any> {
|
||||
if (!props) return {};
|
||||
const res: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
res[key] = value();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在渲染时给一个组件传递props。实际效果为在调用后并不会传递,当被传递的组件被渲染时,将会传递props。
|
||||
* @param component 要传递props的组件
|
||||
* @param props 要传递的props
|
||||
*/
|
||||
static prop(component: Component, props: Record<string, any>) {
|
||||
return hVue(component, props);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个MComponent实例,由于该函数在创建ui时会频繁使用,因此使用m这个简单的名字作为函数名
|
||||
* @returns 一个新的MComponent实例
|
||||
*/
|
||||
export function m() {
|
||||
return new MComponent();
|
||||
}
|
||||
|
||||
export namespace MCGenerator {
|
||||
export function h(
|
||||
type: string | Component | MComponent,
|
||||
children?: MComponentChildren,
|
||||
config: MotaComponentConfig = {}
|
||||
): MotaComponent {
|
||||
if (typeof type === 'string') {
|
||||
return {
|
||||
type,
|
||||
children: ensureArray(children ?? []),
|
||||
props: config.props,
|
||||
innerText: config.innerText,
|
||||
slots: config.slots,
|
||||
vif: config.vif,
|
||||
velse: config.velse,
|
||||
component: config.component
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'component',
|
||||
children: ensureArray(children ?? []),
|
||||
props: config.props,
|
||||
innerText: config.innerText,
|
||||
slots: config.slots,
|
||||
vif: config.vif,
|
||||
velse: config.velse,
|
||||
component: type
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function div(
|
||||
children?: MComponentChildren,
|
||||
config?: NonComponentConfig
|
||||
): MotaComponent {
|
||||
return h('div', children, config);
|
||||
}
|
||||
|
||||
export function span(
|
||||
children?: MComponentChildren,
|
||||
config?: NonComponentConfig
|
||||
): MotaComponent {
|
||||
return h('span', children, config);
|
||||
}
|
||||
|
||||
export function canvas(config?: NonComponentConfig): MotaComponent {
|
||||
return h('canvas', [], config);
|
||||
}
|
||||
|
||||
export function text(
|
||||
text: string | (() => string),
|
||||
config: NonComponentConfig = {}
|
||||
): MotaComponent {
|
||||
return h('text', [], { ...config, innerText: text });
|
||||
}
|
||||
|
||||
export function com(
|
||||
component: Component | MComponent,
|
||||
config: Omit<MotaComponentConfig, 'innerText' | 'component'>
|
||||
): MotaComponent {
|
||||
return h(component, [], config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成一个图标的VNode
|
||||
* @param id 图标的id
|
||||
* @param width 显示宽度,单位像素
|
||||
* @param height 显示高度,单位像素
|
||||
* @param noBoarder 显示的时候是否没有边框和背景
|
||||
*/
|
||||
export function icon(
|
||||
id: AllIds,
|
||||
width?: number,
|
||||
height?: number,
|
||||
noBoarder?: number
|
||||
): VNode {
|
||||
return hVue(BoxAnimate, { id, width, height, noBoarder });
|
||||
}
|
||||
|
||||
export function vfor<T>(
|
||||
items: T[] | (() => T[]),
|
||||
map: (value: T, index: number) => VNode
|
||||
): VForRenderer {
|
||||
return {
|
||||
type: '@v-for',
|
||||
items,
|
||||
map
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 为一个常量创建为一个函数
|
||||
* @param value 要创建成函数的值
|
||||
*/
|
||||
export function f<T>(value: T): () => T {
|
||||
return () => value;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import { FunctionalComponent, reactive } from 'vue';
|
||||
import { EventEmitter } from '@motajs/legacy-common';
|
||||
import { GameStorage } from './storage';
|
||||
import { has, triggerFullscreen } from '@/plugin/utils';
|
||||
import { has, triggerFullscreen, isMobile } from '@motajs/legacy-ui';
|
||||
import { createSettingComponents } from './init/settings';
|
||||
import settingsText from '@/data/settings.json';
|
||||
import { isMobile } from '@/plugin/use';
|
||||
import { fontSize } from '@/plugin/ui/statusBar';
|
||||
import { CustomToolbar } from './custom/toolbar';
|
||||
import { fixedUi } from './init/ui';
|
||||
import { bgmController, soundPlayer } from '@/module';
|
||||
@@ -361,7 +359,7 @@ function handleScreenSetting<T extends number | boolean>(
|
||||
storage.setValue('@@absoluteFontSize', absoluteSize);
|
||||
storage.write();
|
||||
} else if (key === 'fontSizeStatus') {
|
||||
fontSize.value = n as number;
|
||||
// fontSize.value = n as number;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { has } from '@/plugin/utils';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
export class GameStorage<T extends object = any> {
|
||||
static list: GameStorage<any>[] = [];
|
||||
|
||||
key: string;
|
||||
data!: T;
|
||||
data!: Partial<T>;
|
||||
|
||||
constructor(key: string) {
|
||||
this.key = key;
|
||||
@@ -42,7 +42,7 @@ export class GameStorage<T extends object = any> {
|
||||
getValue<K extends keyof T>(key: K, defaults: T[K]): T[K];
|
||||
getValue<T>(key: string, defaults?: T): T;
|
||||
getValue<K extends keyof T>(key: K, defaults?: T[K]) {
|
||||
if (has(this.data[key])) return this.data[key];
|
||||
if (!isNil(this.data[key])) return this.data[key];
|
||||
else {
|
||||
if (defaults !== void 0) {
|
||||
this.data[key] = defaults;
|
||||
@@ -57,7 +57,6 @@ export class GameStorage<T extends object = any> {
|
||||
}
|
||||
|
||||
clear() {
|
||||
// @ts-ignore
|
||||
this.data = {};
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -13,7 +13,7 @@ import type { MotaSetting, SettingDisplayer } from '@/core/main/setting';
|
||||
import type { GameStorage } from '@/core/main/storage';
|
||||
import type { DamageEnemy, EnemyCollection } from './enemy/damage';
|
||||
import type { specials } from './enemy/special';
|
||||
import type { KeyCode } from '@/plugin/keyCodes';
|
||||
import type { KeyCode } from '@motajs/client';
|
||||
import type { Ref } from 'vue';
|
||||
import type * as battle from './enemy/battle';
|
||||
import type * as hero from './state/hero';
|
||||
@@ -121,10 +121,7 @@ type InterfaceType = keyof SystemInterfaceMap;
|
||||
|
||||
interface PluginInterface {
|
||||
// 渲染进程定义的插件
|
||||
use_r: typeof import('../plugin/use');
|
||||
fly_r: typeof import('../plugin/ui/fly');
|
||||
chase_r: typeof import('../plugin/chase');
|
||||
completion_r: typeof import('../plugin/completion');
|
||||
gameCanvas_r: typeof import('../plugin/fx/gameCanvas');
|
||||
frag_r: typeof import('../plugin/fx/frag');
|
||||
boss_r: typeof import('../plugin/boss');
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { generateKeyboardEvent } from '@/core/main/custom/keyboard';
|
||||
import { getVitualKeyOnce } from '@/plugin/utils';
|
||||
import { getVitualKeyOnce } from '@motajs/legacy-ui';
|
||||
import { getAllSavesData, getSaveData } from '@/module/utils';
|
||||
|
||||
export interface SettingsProps extends Partial<ChoicesProps>, UIComponentProps {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { transitionedColor } from '../use';
|
||||
import { linear } from 'mutate-animate';
|
||||
import { Scroll } from '../components/scroll';
|
||||
import { getArea, MinimapDrawer } from '@/plugin/ui/fly';
|
||||
import { getArea, MinimapDrawer } from '@motajs/legacy-ui';
|
||||
import {
|
||||
NumpadToolbar,
|
||||
PlayingToolbar,
|
||||
|
||||
@@ -17,12 +17,12 @@ import {
|
||||
generateBinary,
|
||||
getVitualKeyOnce,
|
||||
openDanmakuPoster
|
||||
} from '@/plugin/utils';
|
||||
} from '@motajs/legacy-ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { generateKeyboardEvent } from '@/core/main/custom/keyboard';
|
||||
import { transitioned } from '../use';
|
||||
import { linear } from 'mutate-animate';
|
||||
import { KeyCode } from '@/plugin/keyCodes';
|
||||
import { KeyCode } from '@motajs/client';
|
||||
import { Progress } from '../components/misc';
|
||||
|
||||
interface ToolbarProps extends DefaultProps {
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@
|
||||
"@motajs/system-ui": "workspace:*",
|
||||
"@motajs/legacy-common": "workspace:*",
|
||||
"@motajs/legacy-client": "workspace:*",
|
||||
"@motajs/legacy-data": "workspace:*"
|
||||
"@motajs/legacy-data": "workspace:*",
|
||||
"@motajs/legacy-ui": "workspacee:*"
|
||||
}
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
<template>
|
||||
<div id="critical-main">
|
||||
<div id="critical">
|
||||
<div class="des">加攻伤害</div>
|
||||
<canvas ref="critical" class="chart"></canvas>
|
||||
<div class="slider-div">
|
||||
<span>加攻次数 {{ addAtk }}</span>
|
||||
<a-slider
|
||||
class="slider"
|
||||
v-model:value="addAtk"
|
||||
:max="ceil((originCri.at(-1)?.[0] ?? 2) / ratio) - 1"
|
||||
></a-slider>
|
||||
<span
|
||||
>最大值 {{
|
||||
ceil((originCri.at(-1)?.[0] ?? 2) / ratio) - 1
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider
|
||||
dashed
|
||||
style="width: 100%; border-color: #ddd4; margin: 1vh 0 1vh 0"
|
||||
></a-divider>
|
||||
<div id="def">
|
||||
<div class="des">加防伤害</div>
|
||||
<canvas ref="def" class="chart"></canvas>
|
||||
<div class="slider-div">
|
||||
<span>加防次数 {{ addDef }}</span>
|
||||
<a-slider
|
||||
class="slider"
|
||||
v-model:value="addDef"
|
||||
:max="ceil((originDef.at(-1)?.[0] ?? 2) / ratio) - 1"
|
||||
></a-slider>
|
||||
<span
|
||||
>最大值 {{
|
||||
ceil((originDef.at(-1)?.[0] ?? 2) / ratio) - 1
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div id="now-damage">
|
||||
<div>
|
||||
<span
|
||||
>当前加攻{{
|
||||
isMobile ? '' : ' '
|
||||
}}</span
|
||||
>
|
||||
<span class="changable" :change="addAtkChangable">{{
|
||||
format(addAtk * ratio)
|
||||
}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
>当前加防{{
|
||||
isMobile ? '' : ' '
|
||||
}}</span
|
||||
>
|
||||
<span class="changable" :change="addDefChangable">{{
|
||||
format(addDef * ratio)
|
||||
}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
>当前减伤{{
|
||||
isMobile ? '' : ' '
|
||||
}}</span
|
||||
>
|
||||
<span class="changable" :change="nowDamageChangable"
|
||||
><span style="font-family: 'FiraCode'">{{
|
||||
(nowDamage[0] as number) < 0 && !has(enemy.damage)
|
||||
? '=>'
|
||||
: ''
|
||||
}}</span
|
||||
>{{
|
||||
(nowDamage[0] as number) < 0 && !has(enemy.damage)
|
||||
? format(-nowDamage[0])
|
||||
: format(nowDamage[0])
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
>当前伤害{{
|
||||
isMobile ? '' : ' '
|
||||
}}</span
|
||||
>
|
||||
<span class="changable" :change="nowDamageChangable">{{
|
||||
format(nowDamage[1])
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { detailInfo, getCriticalDamage, getDefDamage } from '../plugin/ui/book';
|
||||
import Chart, { ChartConfiguration } from 'chart.js/auto';
|
||||
import { has, setCanvasSize } from '../plugin/utils';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { isMobile } from '../plugin/use';
|
||||
import { createChangable } from '@/plugin/ui/common';
|
||||
const props = defineProps<{
|
||||
fromBook?: boolean;
|
||||
}>();
|
||||
|
||||
const critical = ref<HTMLCanvasElement>();
|
||||
const def = ref<HTMLCanvasElement>();
|
||||
|
||||
const enemy = detailInfo.enemy!;
|
||||
const ceil = Math.ceil;
|
||||
|
||||
const x = ref<number>();
|
||||
const y = ref<number>();
|
||||
x.value = has(x.value)
|
||||
? Math.round(x.value + core.bigmap.offsetX / 32)
|
||||
: void 0;
|
||||
y.value = has(y.value)
|
||||
? Math.round(y.value + core.bigmap.offsetY / 32)
|
||||
: void 0;
|
||||
|
||||
let originCri = getCriticalDamage(enemy, 0, 0);
|
||||
let originDef = getDefDamage(enemy, 0, 0);
|
||||
|
||||
// 当前数据
|
||||
const allCri = ref(originCri);
|
||||
const allDef = ref(originDef);
|
||||
|
||||
// 加攻加防数量
|
||||
const addAtk = ref(0);
|
||||
const addDef = ref(0);
|
||||
const addAtkChangable = createChangable(addAtk).change;
|
||||
const addDefChangable = createChangable(addDef).change;
|
||||
|
||||
const originDamage = enemy.enemy.calDamage(core.status.hero).damage;
|
||||
|
||||
// 转发core上的内容至当前作用域
|
||||
const format = core.formatBigNumber;
|
||||
const ratio = core.status.thisMap.ratio;
|
||||
|
||||
const nowDamage = computed(() => {
|
||||
const dam = enemy.enemy.calDamage({
|
||||
atk: core.status.hero.atk + addAtk.value * ratio,
|
||||
def: core.status.hero.def + addDef.value * ratio
|
||||
}).damage;
|
||||
if (!isFinite(dam)) return ['???', '???'];
|
||||
if (!isFinite(originDamage)) return [-dam, dam];
|
||||
return [originDamage - dam, dam];
|
||||
});
|
||||
const nowDamageChangable = createChangable(nowDamage, 1).change;
|
||||
|
||||
function generateChart(ele: HTMLCanvasElement, data: [number, number][]) {
|
||||
Chart.defaults.color = '#aaa';
|
||||
const config: ChartConfiguration = {
|
||||
type: 'line',
|
||||
data: generateData(data),
|
||||
options: {
|
||||
elements: {
|
||||
point: {
|
||||
radius: 5,
|
||||
hoverRadius: 7
|
||||
},
|
||||
line: {
|
||||
borderJoinStyle: 'round'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
grid: {
|
||||
color: '#ddd3'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return new Chart(ele, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图表数据
|
||||
* @param data 数据
|
||||
*/
|
||||
function generateData(data: [number, number][]): ChartConfiguration['data'] {
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
data: data.map(v => v[1]),
|
||||
label: '怪物伤害'
|
||||
}
|
||||
],
|
||||
labels: data.map(v => Math.round(v[0] / ratio))
|
||||
};
|
||||
}
|
||||
|
||||
const update = debounce((atk: Chart, def: Chart) => {
|
||||
allCri.value = getCriticalDamage(
|
||||
enemy,
|
||||
addAtk.value * ratio,
|
||||
addDef.value * ratio
|
||||
);
|
||||
allDef.value = getDefDamage(
|
||||
enemy,
|
||||
addDef.value * ratio,
|
||||
addAtk.value * ratio
|
||||
);
|
||||
if (allCri.value.length > originCri.length) originCri = allCri.value;
|
||||
if (allDef.value.length > originDef.length) originDef = allDef.value;
|
||||
|
||||
atk.data = generateData(allCri.value);
|
||||
def.data = generateData(allDef.value);
|
||||
atk.update('resize');
|
||||
def.update('resize');
|
||||
}, 200);
|
||||
|
||||
onMounted(() => {
|
||||
const div = document.getElementById('critical-main') as HTMLDivElement;
|
||||
const style = getComputedStyle(div);
|
||||
|
||||
const width = parseFloat(style.width);
|
||||
const height = window.innerHeight / 5;
|
||||
const c = critical.value!;
|
||||
const d = def.value!;
|
||||
|
||||
setCanvasSize(c, width, height);
|
||||
setCanvasSize(d, width, height);
|
||||
|
||||
const criChart = generateChart(c, allCri.value);
|
||||
const defChart = generateChart(d, allDef.value);
|
||||
|
||||
watch(addAtk, n => {
|
||||
update(criChart, defChart);
|
||||
});
|
||||
|
||||
watch(addDef, n => {
|
||||
update(criChart, defChart);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#critical-main {
|
||||
width: 100%;
|
||||
height: 50vh;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
top: 20vh;
|
||||
}
|
||||
|
||||
.des {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 140%;
|
||||
}
|
||||
|
||||
.slider-div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 120%;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
#now-damage {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
font-size: 160%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#now-damage {
|
||||
font-size: 100%;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
#critical-main {
|
||||
position: absolute;
|
||||
top: 25vh;
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
.slider-div span {
|
||||
font-size: 70%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,97 +0,0 @@
|
||||
<template>
|
||||
<div id="special-main">
|
||||
<Scroll id="special-scroll">
|
||||
<div id="special">
|
||||
<component :is="info"></component>
|
||||
</div>
|
||||
</Scroll>
|
||||
<a-divider
|
||||
dashed
|
||||
style="margin: 2vh 0 2vh 0; border-color: #ddd4"
|
||||
></a-divider>
|
||||
<div id="critical">
|
||||
<div style="font-size: 2.5vh; width: 100%; text-align: center">
|
||||
临界表
|
||||
</div>
|
||||
<div id="critical-main">
|
||||
<div id="critical-des">
|
||||
<span>加攻</span>
|
||||
<span>减伤</span>
|
||||
</div>
|
||||
<div v-for="cri of criticals" class="critical">
|
||||
<span class="critical-atk">{{ format(cri.atkDelta) }}</span>
|
||||
<span>{{ format(cri.delta) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { isMobile } from '../plugin/use';
|
||||
import { detailInfo, getSpecialHint } from '../plugin/ui/book';
|
||||
|
||||
const props = defineProps<{
|
||||
fromBook?: boolean;
|
||||
}>();
|
||||
|
||||
const enemy = detailInfo.enemy!;
|
||||
|
||||
const info = getSpecialHint(enemy);
|
||||
|
||||
const criticals = enemy.enemy.calCritical(isMobile ? 4 : 8);
|
||||
|
||||
const format = core.formatBigNumber;
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#special-main {
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
font-size: 160%;
|
||||
position: absolute;
|
||||
top: 20vh;
|
||||
}
|
||||
|
||||
#critical-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#critical-des,
|
||||
.critical {
|
||||
font-size: 110%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.critical-atk {
|
||||
border-bottom: 1px solid #ddd4;
|
||||
}
|
||||
|
||||
.critical {
|
||||
border-left: 1px solid #ddd4;
|
||||
padding-left: 1%;
|
||||
}
|
||||
|
||||
#special-scroll {
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#critical-des,
|
||||
.critical {
|
||||
font-size: 70%;
|
||||
}
|
||||
|
||||
#special {
|
||||
font-size: 70%;
|
||||
}
|
||||
|
||||
#special-main {
|
||||
top: 25vh;
|
||||
width: 90vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,85 +0,0 @@
|
||||
<template>
|
||||
<div id="enemy-target">
|
||||
<div id="enemy-desc">
|
||||
<span>怪物描述</span>
|
||||
<Scroll id="enemy-desc-scroll">
|
||||
<span
|
||||
> {{
|
||||
enemy.enemy.enemy.description
|
||||
}}</span
|
||||
>
|
||||
</Scroll>
|
||||
</div>
|
||||
<a-divider dashed style="border-color: #ddd4"></a-divider>
|
||||
<div>
|
||||
<div id="mark-target">
|
||||
<span
|
||||
id="mark-info"
|
||||
:style="{ color: marked ? 'lightgreen' : 'lightcoral' }"
|
||||
>{{ marked ? '已标记该怪物' : '未标记该怪物' }}</span
|
||||
>
|
||||
<span class="button-text" @click.stop="mark">{{
|
||||
marked ? '取消标记该怪物' : '标记该怪物为目标'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import { detailInfo } from '../plugin/ui/book';
|
||||
import { hasMarkedEnemy, markEnemy } from '@/plugin/mark';
|
||||
|
||||
const enemy = detailInfo.enemy!;
|
||||
const marked = ref(hasMarkedEnemy(enemy.enemy.id));
|
||||
|
||||
function mark() {
|
||||
markEnemy(enemy.enemy.id);
|
||||
marked.value = hasMarkedEnemy(enemy.enemy.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#enemy-target {
|
||||
width: 100%;
|
||||
font-size: 160%;
|
||||
}
|
||||
|
||||
#enemy-desc {
|
||||
width: 100%;
|
||||
height: 30vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#enemy-desc-scroll {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#mark-target {
|
||||
margin-top: 10%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
font-size: 3.3vh;
|
||||
}
|
||||
|
||||
#mark-info {
|
||||
transition: color 0.2s linear;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#enemy-desc {
|
||||
font-size: 70%;
|
||||
}
|
||||
|
||||
#mark-target {
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,108 +0,0 @@
|
||||
<template>
|
||||
<div class="keyboard-container">
|
||||
<div
|
||||
v-for="(key, i) of keyboard.keys"
|
||||
class="keyboard-item"
|
||||
@click="keyboard.emitKey(key, i)"
|
||||
:active="checkAssist(assist, key.key)"
|
||||
:style="{
|
||||
left: `${key.x}px`,
|
||||
top: `${key.y}px`,
|
||||
width: `${key.width}px`,
|
||||
height: `${key.height}px`
|
||||
}"
|
||||
>
|
||||
<span
|
||||
class="keyboard-key button-text"
|
||||
v-html="key.text ?? KeyCodeUtils.toString(key.key)"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { checkAssist } from '@/core/main/custom/hotkey';
|
||||
import { Keyboard } from '@/core/main/custom/keyboard';
|
||||
import { KeyboardEmits } from '@/core/main/custom/keyboard';
|
||||
import { KeyCodeUtils } from '@/plugin/keyCodes';
|
||||
import { nextTick, onUnmounted, ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
keyboard: Keyboard;
|
||||
}>();
|
||||
|
||||
const assist = ref(props.keyboard.assist);
|
||||
const fontSize = `${props.keyboard.fontSize}px`;
|
||||
|
||||
const [width, height] = (() => {
|
||||
const key = props.keyboard;
|
||||
let mw = 0;
|
||||
let mh = 0;
|
||||
for (const k of key.keys) {
|
||||
if (k.x + k.width > mw) mw = k.x + k.width;
|
||||
if (k.y + k.height > mh) mh = k.y + k.height;
|
||||
}
|
||||
|
||||
return [`${mw}px`, `${mh}px`];
|
||||
})();
|
||||
|
||||
function onAssist() {
|
||||
nextTick(() => {
|
||||
assist.value = props.keyboard.assist;
|
||||
});
|
||||
}
|
||||
props.keyboard.on('emit', onAssist);
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'keyup', data: KeyboardEmits): void;
|
||||
}>();
|
||||
|
||||
onUnmounted(() => {
|
||||
props.keyboard.off('emit', onAssist);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.keyboard-container {
|
||||
width: v-bind(width);
|
||||
height: v-bind(height);
|
||||
display: block;
|
||||
font-size: v-bind(fontSize);
|
||||
position: relative;
|
||||
font-family: Arial;
|
||||
}
|
||||
|
||||
.keyboard-item {
|
||||
position: absolute;
|
||||
background-color: #222;
|
||||
border: 1.5px solid #ddd8;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.1s linear;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.keyboard-item:hover,
|
||||
.keyboard-item[active='true'] {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.keyboard-item[active='true']::v-deep(> *) {
|
||||
color: gold;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.keyboard-key {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
text-overflow: clip;
|
||||
text-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,32 +0,0 @@
|
||||
const animation: ((time: number) => void)[] = [];
|
||||
|
||||
let animateTime = 0;
|
||||
|
||||
export default function init() {
|
||||
core.registerAnimationFrame('animateController', true, time => {
|
||||
if (time - animateTime <= core.values.animateSpeed) return;
|
||||
for (const fn of animation) {
|
||||
fn(time);
|
||||
}
|
||||
animateTime = core.animateFrame.animateTime;
|
||||
});
|
||||
return { addAnimate, removeAnimate };
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个动画
|
||||
* @param fn 要添加的函数
|
||||
*/
|
||||
export function addAnimate(fn: (time: number) => void) {
|
||||
animation.push(fn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除一个动画
|
||||
* @param fn 要移除的函数
|
||||
*/
|
||||
export function removeAnimate(fn: (time: number) => void) {
|
||||
const index = animation.findIndex(v => v === fn);
|
||||
if (index === -1) return;
|
||||
animation.splice(index, 1);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Transform, MotaOffscreenCanvas2D } from '@motajs/render';
|
||||
import { IStateDamageable } from '@/game/state/interface';
|
||||
import { Hitbox, Projectile } from './barrage';
|
||||
import type { PalaceBoss } from './palaceBoss';
|
||||
import { clamp } from '../utils';
|
||||
import { clamp } from '@motajs/legacy-ui';
|
||||
|
||||
function popDamage(damage: number, boss: PalaceBoss, color: string) {
|
||||
const { x, y } = core.status.hero.loc;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Animation, hyper, linear, power, sleep } from 'mutate-animate';
|
||||
import { Chase, ChaseData, IChaseController } from './chase';
|
||||
import { completeAchievement } from '../ui/achievement';
|
||||
import { completeAchievement } from '@motajs/legacy-ui';
|
||||
import {
|
||||
Camera,
|
||||
CameraAnimation,
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import {
|
||||
AchievementType,
|
||||
completeAchievement,
|
||||
hasCompletedAchievement
|
||||
} from './ui/achievement';
|
||||
import { changeLocalStorage } from './utils';
|
||||
import list from '../data/achievement.json';
|
||||
|
||||
export const floors: Record<number, FloorIds[]> = {
|
||||
1: ['MT0', 'tower7']
|
||||
};
|
||||
const achis: Record<number, Record<AchievementType, number[]>> = {
|
||||
1: {
|
||||
normal: [0, 1],
|
||||
challenge: [0],
|
||||
explore: [1]
|
||||
}
|
||||
};
|
||||
|
||||
export const achiDict: Record<number, number> = {
|
||||
1: 0
|
||||
};
|
||||
|
||||
export function init() {
|
||||
Object.values(floors).forEach((v, i) => {
|
||||
const from = core.floorIds.indexOf(v[0]);
|
||||
const to = core.floorIds.indexOf(v[1]);
|
||||
const all = core.floorIds.slice(from, to + 1);
|
||||
floors[i + 1] = all;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查所有到达过的楼层,用于成就的计算
|
||||
*/
|
||||
export function checkVisitedFloor() {
|
||||
changeLocalStorage<Partial<Record<FloorIds, boolean>>>(
|
||||
'visitedFloor',
|
||||
data => {
|
||||
let needUpdate = false;
|
||||
core.floorIds.forEach(v => {
|
||||
if (core.hasVisitedFloor(v)) {
|
||||
data[v] = true;
|
||||
needUpdate = true;
|
||||
}
|
||||
});
|
||||
if (needUpdate) {
|
||||
checkCompletionAchievement();
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个章节的完成度
|
||||
* @param num 章节
|
||||
*/
|
||||
export function getChapterCompletion(num: number) {
|
||||
if (!achis[num]) return 0;
|
||||
let res = 0;
|
||||
const all = floors[num];
|
||||
const achiNum = Object.values(achis[num]).reduce(
|
||||
(pre, cur) => pre + cur.length,
|
||||
0
|
||||
);
|
||||
|
||||
// 计算到达过的楼层
|
||||
let visitedFloor = 0;
|
||||
const visited = core.getLocalStorage<Partial<Record<FloorIds, boolean>>>(
|
||||
'visitedFloor',
|
||||
{}
|
||||
);
|
||||
all.forEach(v => {
|
||||
if (visited[v]) visitedFloor++;
|
||||
});
|
||||
const floorRatio = all.length / (all.length + achiNum);
|
||||
const floorPoint = (floorRatio * visitedFloor) / all.length;
|
||||
|
||||
let completedPoint = 0;
|
||||
let totalPoint = 0;
|
||||
|
||||
// 计算成就,占比按成就点走
|
||||
for (const [type, achi] of Object.entries(achis[num]) as [
|
||||
AchievementType,
|
||||
number[]
|
||||
][]) {
|
||||
achi.forEach(v => {
|
||||
totalPoint += list[type][v].point;
|
||||
if (hasCompletedAchievement(type, v)) {
|
||||
completedPoint += list[type][v].point;
|
||||
}
|
||||
});
|
||||
}
|
||||
const achiPoint = (completedPoint / totalPoint) * (1 - floorRatio);
|
||||
|
||||
res = floorPoint + achiPoint;
|
||||
|
||||
return Math.floor(res * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查完成度成就是否完成
|
||||
*/
|
||||
export function checkCompletionAchievement() {
|
||||
[1].forEach(v => {
|
||||
if (getChapterCompletion(v) >= 100) {
|
||||
completeAchievement('explore', achiDict[v]);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Animation, linear, sleep } from 'mutate-animate';
|
||||
import { has } from '../utils';
|
||||
import { has } from '@motajs/legacy-ui';
|
||||
|
||||
// todo: 移植到渲染树
|
||||
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
import * as fly from './ui/fly';
|
||||
import * as chase from './chase';
|
||||
import * as completion from './completion';
|
||||
import * as use from './use';
|
||||
import * as gameCanvas from './fx/gameCanvas';
|
||||
import * as animateController from './animateController';
|
||||
import * as achievement from './ui/achievement';
|
||||
import * as boss from './boss';
|
||||
import * as utils from './utils';
|
||||
import './loopMap';
|
||||
|
||||
Mota.Plugin.register('fly_r', fly);
|
||||
Mota.Plugin.register('chase_r', chase);
|
||||
Mota.Plugin.register('completion_r', completion, completion.init);
|
||||
Mota.Plugin.register('use_r', use);
|
||||
Mota.Plugin.register('gameCanvas_r', gameCanvas);
|
||||
Mota.Plugin.register(
|
||||
'animateController_r',
|
||||
animateController,
|
||||
animateController.default
|
||||
);
|
||||
Mota.Plugin.register('chase_r', chase);
|
||||
Mota.Plugin.register('achievement_r', achievement);
|
||||
Mota.Plugin.register('boss_r', boss);
|
||||
Mota.Plugin.register('utils_r', utils);
|
||||
@@ -1,825 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Virtual Key Codes, the value does not hold any inherent meaning.
|
||||
* Inspired somewhat from https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||
* But these are "more general", as they should work across browsers & OS`s.
|
||||
*/
|
||||
export enum KeyCode {
|
||||
DependsOnKbLayout = -1,
|
||||
|
||||
/**
|
||||
* Placed first to cover the 0 value of the enum.
|
||||
*/
|
||||
Unknown = 0,
|
||||
|
||||
Backspace,
|
||||
Tab,
|
||||
Enter,
|
||||
Shift,
|
||||
Ctrl,
|
||||
Alt,
|
||||
PauseBreak,
|
||||
CapsLock,
|
||||
Escape,
|
||||
Space,
|
||||
PageUp,
|
||||
PageDown,
|
||||
End,
|
||||
Home,
|
||||
LeftArrow,
|
||||
UpArrow,
|
||||
RightArrow,
|
||||
DownArrow,
|
||||
Insert,
|
||||
Delete,
|
||||
|
||||
Digit0,
|
||||
Digit1,
|
||||
Digit2,
|
||||
Digit3,
|
||||
Digit4,
|
||||
Digit5,
|
||||
Digit6,
|
||||
Digit7,
|
||||
Digit8,
|
||||
Digit9,
|
||||
|
||||
KeyA,
|
||||
KeyB,
|
||||
KeyC,
|
||||
KeyD,
|
||||
KeyE,
|
||||
KeyF,
|
||||
KeyG,
|
||||
KeyH,
|
||||
KeyI,
|
||||
KeyJ,
|
||||
KeyK,
|
||||
KeyL,
|
||||
KeyM,
|
||||
KeyN,
|
||||
KeyO,
|
||||
KeyP,
|
||||
KeyQ,
|
||||
KeyR,
|
||||
KeyS,
|
||||
KeyT,
|
||||
KeyU,
|
||||
KeyV,
|
||||
KeyW,
|
||||
KeyX,
|
||||
KeyY,
|
||||
KeyZ,
|
||||
|
||||
Meta,
|
||||
ContextMenu,
|
||||
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12,
|
||||
F13,
|
||||
F14,
|
||||
F15,
|
||||
F16,
|
||||
F17,
|
||||
F18,
|
||||
F19,
|
||||
|
||||
NumLock,
|
||||
ScrollLock,
|
||||
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
* For the US standard keyboard, the ';:' key
|
||||
*/
|
||||
Semicolon,
|
||||
/**
|
||||
* For any country/region, the '+' key
|
||||
* For the US standard keyboard, the '=+' key
|
||||
*/
|
||||
Equal,
|
||||
/**
|
||||
* For any country/region, the ',' key
|
||||
* For the US standard keyboard, the ',<' key
|
||||
*/
|
||||
Comma,
|
||||
/**
|
||||
* For any country/region, the '-' key
|
||||
* For the US standard keyboard, the '-_' key
|
||||
*/
|
||||
Minus,
|
||||
/**
|
||||
* For any country/region, the '.' key
|
||||
* For the US standard keyboard, the '.>' key
|
||||
*/
|
||||
Period,
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
* For the US standard keyboard, the '/?' key
|
||||
*/
|
||||
Slash,
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
* For the US standard keyboard, the '`~' key
|
||||
*/
|
||||
Backquote,
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
* For the US standard keyboard, the '[{' key
|
||||
*/
|
||||
BracketLeft,
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
* For the US standard keyboard, the '\|' key
|
||||
*/
|
||||
Backslash,
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
* For the US standard keyboard, the ']}' key
|
||||
*/
|
||||
BracketRight,
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
* For the US standard keyboard, the ''"' key
|
||||
*/
|
||||
Quote,
|
||||
/**
|
||||
* Used for miscellaneous characters; it can vary by keyboard.
|
||||
*/
|
||||
OEM_8,
|
||||
/**
|
||||
* Either the angle bracket key or the backslash key on the RT 102-key keyboard.
|
||||
*/
|
||||
IntlBackslash,
|
||||
|
||||
Numpad0, // VK_NUMPAD0, 0x60, Numeric keypad 0 key
|
||||
Numpad1, // VK_NUMPAD1, 0x61, Numeric keypad 1 key
|
||||
Numpad2, // VK_NUMPAD2, 0x62, Numeric keypad 2 key
|
||||
Numpad3, // VK_NUMPAD3, 0x63, Numeric keypad 3 key
|
||||
Numpad4, // VK_NUMPAD4, 0x64, Numeric keypad 4 key
|
||||
Numpad5, // VK_NUMPAD5, 0x65, Numeric keypad 5 key
|
||||
Numpad6, // VK_NUMPAD6, 0x66, Numeric keypad 6 key
|
||||
Numpad7, // VK_NUMPAD7, 0x67, Numeric keypad 7 key
|
||||
Numpad8, // VK_NUMPAD8, 0x68, Numeric keypad 8 key
|
||||
Numpad9, // VK_NUMPAD9, 0x69, Numeric keypad 9 key
|
||||
|
||||
NumpadMultiply, // VK_MULTIPLY, 0x6A, Multiply key
|
||||
NumpadAdd, // VK_ADD, 0x6B, Add key
|
||||
NUMPAD_SEPARATOR, // VK_SEPARATOR, 0x6C, Separator key
|
||||
NumpadSubtract, // VK_SUBTRACT, 0x6D, Subtract key
|
||||
NumpadDecimal, // VK_DECIMAL, 0x6E, Decimal key
|
||||
NumpadDivide, // VK_DIVIDE, 0x6F,
|
||||
|
||||
/**
|
||||
* Cover all key codes when IME is processing input.
|
||||
*/
|
||||
KEY_IN_COMPOSITION,
|
||||
|
||||
ABNT_C1, // Brazilian (ABNT) Keyboard
|
||||
ABNT_C2, // Brazilian (ABNT) Keyboard
|
||||
|
||||
AudioVolumeMute,
|
||||
AudioVolumeUp,
|
||||
AudioVolumeDown,
|
||||
|
||||
BrowserSearch,
|
||||
BrowserHome,
|
||||
BrowserBack,
|
||||
BrowserForward,
|
||||
|
||||
MediaTrackNext,
|
||||
MediaTrackPrevious,
|
||||
MediaStop,
|
||||
MediaPlayPause,
|
||||
LaunchMediaPlayer,
|
||||
LaunchMail,
|
||||
LaunchApp2,
|
||||
|
||||
/**
|
||||
* VK_CLEAR, 0x0C, CLEAR key
|
||||
*/
|
||||
Clear,
|
||||
|
||||
/**
|
||||
* Placed last to cover the length of the enum.
|
||||
* Please do not depend on this value!
|
||||
*/
|
||||
MAX_VALUE
|
||||
}
|
||||
|
||||
/**
|
||||
* keyboardEvent.code
|
||||
*/
|
||||
export const enum ScanCode {
|
||||
DependsOnKbLayout = -1,
|
||||
None,
|
||||
Hyper,
|
||||
Super,
|
||||
Fn,
|
||||
FnLock,
|
||||
Suspend,
|
||||
Resume,
|
||||
Turbo,
|
||||
Sleep,
|
||||
WakeUp,
|
||||
KeyA,
|
||||
KeyB,
|
||||
KeyC,
|
||||
KeyD,
|
||||
KeyE,
|
||||
KeyF,
|
||||
KeyG,
|
||||
KeyH,
|
||||
KeyI,
|
||||
KeyJ,
|
||||
KeyK,
|
||||
KeyL,
|
||||
KeyM,
|
||||
KeyN,
|
||||
KeyO,
|
||||
KeyP,
|
||||
KeyQ,
|
||||
KeyR,
|
||||
KeyS,
|
||||
KeyT,
|
||||
KeyU,
|
||||
KeyV,
|
||||
KeyW,
|
||||
KeyX,
|
||||
KeyY,
|
||||
KeyZ,
|
||||
Digit1,
|
||||
Digit2,
|
||||
Digit3,
|
||||
Digit4,
|
||||
Digit5,
|
||||
Digit6,
|
||||
Digit7,
|
||||
Digit8,
|
||||
Digit9,
|
||||
Digit0,
|
||||
Enter,
|
||||
Escape,
|
||||
Backspace,
|
||||
Tab,
|
||||
Space,
|
||||
Minus,
|
||||
Equal,
|
||||
BracketLeft,
|
||||
BracketRight,
|
||||
Backslash,
|
||||
IntlHash,
|
||||
Semicolon,
|
||||
Quote,
|
||||
Backquote,
|
||||
Comma,
|
||||
Period,
|
||||
Slash,
|
||||
CapsLock,
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12,
|
||||
PrintScreen,
|
||||
ScrollLock,
|
||||
Pause,
|
||||
Insert,
|
||||
Home,
|
||||
PageUp,
|
||||
Delete,
|
||||
End,
|
||||
PageDown,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
NumLock,
|
||||
NumpadDivide,
|
||||
NumpadMultiply,
|
||||
NumpadSubtract,
|
||||
NumpadAdd,
|
||||
NumpadEnter,
|
||||
Numpad1,
|
||||
Numpad2,
|
||||
Numpad3,
|
||||
Numpad4,
|
||||
Numpad5,
|
||||
Numpad6,
|
||||
Numpad7,
|
||||
Numpad8,
|
||||
Numpad9,
|
||||
Numpad0,
|
||||
NumpadDecimal,
|
||||
IntlBackslash,
|
||||
ContextMenu,
|
||||
Power,
|
||||
NumpadEqual,
|
||||
F13,
|
||||
F14,
|
||||
F15,
|
||||
F16,
|
||||
F17,
|
||||
F18,
|
||||
F19,
|
||||
F20,
|
||||
F21,
|
||||
F22,
|
||||
F23,
|
||||
F24,
|
||||
Open,
|
||||
Help,
|
||||
Select,
|
||||
Again,
|
||||
Undo,
|
||||
Cut,
|
||||
Copy,
|
||||
Paste,
|
||||
Find,
|
||||
AudioVolumeMute,
|
||||
AudioVolumeUp,
|
||||
AudioVolumeDown,
|
||||
NumpadComma,
|
||||
IntlRo,
|
||||
KanaMode,
|
||||
IntlYen,
|
||||
Convert,
|
||||
NonConvert,
|
||||
Lang1,
|
||||
Lang2,
|
||||
Lang3,
|
||||
Lang4,
|
||||
Lang5,
|
||||
Abort,
|
||||
Props,
|
||||
NumpadParenLeft,
|
||||
NumpadParenRight,
|
||||
NumpadBackspace,
|
||||
NumpadMemoryStore,
|
||||
NumpadMemoryRecall,
|
||||
NumpadMemoryClear,
|
||||
NumpadMemoryAdd,
|
||||
NumpadMemorySubtract,
|
||||
NumpadClear,
|
||||
NumpadClearEntry,
|
||||
ControlLeft,
|
||||
ShiftLeft,
|
||||
AltLeft,
|
||||
MetaLeft,
|
||||
ControlRight,
|
||||
ShiftRight,
|
||||
AltRight,
|
||||
MetaRight,
|
||||
BrightnessUp,
|
||||
BrightnessDown,
|
||||
MediaPlay,
|
||||
MediaRecord,
|
||||
MediaFastForward,
|
||||
MediaRewind,
|
||||
MediaTrackNext,
|
||||
MediaTrackPrevious,
|
||||
MediaStop,
|
||||
Eject,
|
||||
MediaPlayPause,
|
||||
MediaSelect,
|
||||
LaunchMail,
|
||||
LaunchApp2,
|
||||
LaunchApp1,
|
||||
SelectTask,
|
||||
LaunchScreenSaver,
|
||||
BrowserSearch,
|
||||
BrowserHome,
|
||||
BrowserBack,
|
||||
BrowserForward,
|
||||
BrowserStop,
|
||||
BrowserRefresh,
|
||||
BrowserFavorites,
|
||||
ZoomToggle,
|
||||
MailReply,
|
||||
MailForward,
|
||||
MailSend,
|
||||
|
||||
MAX_VALUE
|
||||
}
|
||||
|
||||
class KeyCodeStrMap {
|
||||
|
||||
public _keyCodeToStr: string[];
|
||||
public _strToKeyCode: { [str: string]: KeyCode };
|
||||
|
||||
constructor() {
|
||||
this._keyCodeToStr = [];
|
||||
this._strToKeyCode = Object.create(null);
|
||||
}
|
||||
|
||||
define(keyCode: KeyCode, str: string): void {
|
||||
this._keyCodeToStr[keyCode] = str;
|
||||
this._strToKeyCode[str.toLowerCase()] = keyCode;
|
||||
}
|
||||
|
||||
keyCodeToStr(keyCode: KeyCode): string {
|
||||
return this._keyCodeToStr[keyCode];
|
||||
}
|
||||
|
||||
strToKeyCode(str: string): KeyCode {
|
||||
return this._strToKeyCode[str.toLowerCase()] || KeyCode.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
const uiMap = new KeyCodeStrMap();
|
||||
const userSettingsUSMap = new KeyCodeStrMap();
|
||||
const userSettingsGeneralMap = new KeyCodeStrMap();
|
||||
export const EVENT_KEY_CODE_MAP: { [keyCode: number]: KeyCode } = new Array(230);
|
||||
export const NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE: { [nativeKeyCode: string]: KeyCode } = {};
|
||||
const scanCodeIntToStr: string[] = [];
|
||||
const scanCodeStrToInt: { [code: string]: number } = Object.create(null);
|
||||
const scanCodeLowerCaseStrToInt: { [code: string]: number } = Object.create(null);
|
||||
|
||||
export const ScanCodeUtils = {
|
||||
lowerCaseToEnum: (scanCode: string) => scanCodeLowerCaseStrToInt[scanCode] || ScanCode.None,
|
||||
toEnum: (scanCode: string) => scanCodeStrToInt[scanCode] || ScanCode.None,
|
||||
toString: (scanCode: ScanCode) => scanCodeIntToStr[scanCode] || 'None'
|
||||
};
|
||||
|
||||
/**
|
||||
* -1 if a ScanCode => KeyCode mapping depends on kb layout.
|
||||
*/
|
||||
export const IMMUTABLE_CODE_TO_KEY_CODE: KeyCode[] = [];
|
||||
|
||||
/**
|
||||
* -1 if a KeyCode => ScanCode mapping depends on kb layout.
|
||||
*/
|
||||
export const IMMUTABLE_KEY_CODE_TO_CODE: ScanCode[] = [];
|
||||
|
||||
for (let i = 0; i <= ScanCode.MAX_VALUE; i++) {
|
||||
IMMUTABLE_CODE_TO_KEY_CODE[i] = KeyCode.DependsOnKbLayout;
|
||||
}
|
||||
|
||||
for (let i = 0; i <= KeyCode.MAX_VALUE; i++) {
|
||||
IMMUTABLE_KEY_CODE_TO_CODE[i] = ScanCode.DependsOnKbLayout;
|
||||
}
|
||||
|
||||
(function () {
|
||||
|
||||
// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
||||
// See https://github.com/microsoft/node-native-keymap/blob/master/deps/chromium/keyboard_codes_win.h
|
||||
|
||||
const empty = '';
|
||||
type IMappingEntry = [number, 0 | 1, ScanCode, string, KeyCode, string, number, string, string, string];
|
||||
const mappings: IMappingEntry[] = [
|
||||
// keyCodeOrd, immutable, scanCode, scanCodeStr, keyCode, keyCodeStr, eventKeyCode, vkey, usUserSettingsLabel, generalUserSettingsLabel
|
||||
[0, 1, ScanCode.None, 'None', KeyCode.Unknown, 'unknown', 0, 'VK_UNKNOWN', empty, empty],
|
||||
[0, 1, ScanCode.Hyper, 'Hyper', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Super, 'Super', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Fn, 'Fn', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.FnLock, 'FnLock', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Suspend, 'Suspend', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Resume, 'Resume', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Turbo, 'Turbo', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Sleep, 'Sleep', KeyCode.Unknown, empty, 0, 'VK_SLEEP', empty, empty],
|
||||
[0, 1, ScanCode.WakeUp, 'WakeUp', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[31, 0, ScanCode.KeyA, 'KeyA', KeyCode.KeyA, 'A', 65, 'VK_A', empty, empty],
|
||||
[32, 0, ScanCode.KeyB, 'KeyB', KeyCode.KeyB, 'B', 66, 'VK_B', empty, empty],
|
||||
[33, 0, ScanCode.KeyC, 'KeyC', KeyCode.KeyC, 'C', 67, 'VK_C', empty, empty],
|
||||
[34, 0, ScanCode.KeyD, 'KeyD', KeyCode.KeyD, 'D', 68, 'VK_D', empty, empty],
|
||||
[35, 0, ScanCode.KeyE, 'KeyE', KeyCode.KeyE, 'E', 69, 'VK_E', empty, empty],
|
||||
[36, 0, ScanCode.KeyF, 'KeyF', KeyCode.KeyF, 'F', 70, 'VK_F', empty, empty],
|
||||
[37, 0, ScanCode.KeyG, 'KeyG', KeyCode.KeyG, 'G', 71, 'VK_G', empty, empty],
|
||||
[38, 0, ScanCode.KeyH, 'KeyH', KeyCode.KeyH, 'H', 72, 'VK_H', empty, empty],
|
||||
[39, 0, ScanCode.KeyI, 'KeyI', KeyCode.KeyI, 'I', 73, 'VK_I', empty, empty],
|
||||
[40, 0, ScanCode.KeyJ, 'KeyJ', KeyCode.KeyJ, 'J', 74, 'VK_J', empty, empty],
|
||||
[41, 0, ScanCode.KeyK, 'KeyK', KeyCode.KeyK, 'K', 75, 'VK_K', empty, empty],
|
||||
[42, 0, ScanCode.KeyL, 'KeyL', KeyCode.KeyL, 'L', 76, 'VK_L', empty, empty],
|
||||
[43, 0, ScanCode.KeyM, 'KeyM', KeyCode.KeyM, 'M', 77, 'VK_M', empty, empty],
|
||||
[44, 0, ScanCode.KeyN, 'KeyN', KeyCode.KeyN, 'N', 78, 'VK_N', empty, empty],
|
||||
[45, 0, ScanCode.KeyO, 'KeyO', KeyCode.KeyO, 'O', 79, 'VK_O', empty, empty],
|
||||
[46, 0, ScanCode.KeyP, 'KeyP', KeyCode.KeyP, 'P', 80, 'VK_P', empty, empty],
|
||||
[47, 0, ScanCode.KeyQ, 'KeyQ', KeyCode.KeyQ, 'Q', 81, 'VK_Q', empty, empty],
|
||||
[48, 0, ScanCode.KeyR, 'KeyR', KeyCode.KeyR, 'R', 82, 'VK_R', empty, empty],
|
||||
[49, 0, ScanCode.KeyS, 'KeyS', KeyCode.KeyS, 'S', 83, 'VK_S', empty, empty],
|
||||
[50, 0, ScanCode.KeyT, 'KeyT', KeyCode.KeyT, 'T', 84, 'VK_T', empty, empty],
|
||||
[51, 0, ScanCode.KeyU, 'KeyU', KeyCode.KeyU, 'U', 85, 'VK_U', empty, empty],
|
||||
[52, 0, ScanCode.KeyV, 'KeyV', KeyCode.KeyV, 'V', 86, 'VK_V', empty, empty],
|
||||
[53, 0, ScanCode.KeyW, 'KeyW', KeyCode.KeyW, 'W', 87, 'VK_W', empty, empty],
|
||||
[54, 0, ScanCode.KeyX, 'KeyX', KeyCode.KeyX, 'X', 88, 'VK_X', empty, empty],
|
||||
[55, 0, ScanCode.KeyY, 'KeyY', KeyCode.KeyY, 'Y', 89, 'VK_Y', empty, empty],
|
||||
[56, 0, ScanCode.KeyZ, 'KeyZ', KeyCode.KeyZ, 'Z', 90, 'VK_Z', empty, empty],
|
||||
[22, 0, ScanCode.Digit1, 'Digit1', KeyCode.Digit1, '1', 49, 'VK_1', empty, empty],
|
||||
[23, 0, ScanCode.Digit2, 'Digit2', KeyCode.Digit2, '2', 50, 'VK_2', empty, empty],
|
||||
[24, 0, ScanCode.Digit3, 'Digit3', KeyCode.Digit3, '3', 51, 'VK_3', empty, empty],
|
||||
[25, 0, ScanCode.Digit4, 'Digit4', KeyCode.Digit4, '4', 52, 'VK_4', empty, empty],
|
||||
[26, 0, ScanCode.Digit5, 'Digit5', KeyCode.Digit5, '5', 53, 'VK_5', empty, empty],
|
||||
[27, 0, ScanCode.Digit6, 'Digit6', KeyCode.Digit6, '6', 54, 'VK_6', empty, empty],
|
||||
[28, 0, ScanCode.Digit7, 'Digit7', KeyCode.Digit7, '7', 55, 'VK_7', empty, empty],
|
||||
[29, 0, ScanCode.Digit8, 'Digit8', KeyCode.Digit8, '8', 56, 'VK_8', empty, empty],
|
||||
[30, 0, ScanCode.Digit9, 'Digit9', KeyCode.Digit9, '9', 57, 'VK_9', empty, empty],
|
||||
[21, 0, ScanCode.Digit0, 'Digit0', KeyCode.Digit0, '0', 48, 'VK_0', empty, empty],
|
||||
[3, 1, ScanCode.Enter, 'Enter', KeyCode.Enter, 'Enter', 13, 'VK_RETURN', empty, empty],
|
||||
[9, 1, ScanCode.Escape, 'Escape', KeyCode.Escape, 'Escape', 27, 'VK_ESCAPE', empty, empty],
|
||||
[1, 1, ScanCode.Backspace, 'Backspace', KeyCode.Backspace, 'Backspace', 8, 'VK_BACK', empty, empty],
|
||||
[2, 1, ScanCode.Tab, 'Tab', KeyCode.Tab, 'Tab', 9, 'VK_TAB', empty, empty],
|
||||
[10, 1, ScanCode.Space, 'Space', KeyCode.Space, 'Space', 32, 'VK_SPACE', empty, empty],
|
||||
[83, 0, ScanCode.Minus, 'Minus', KeyCode.Minus, '-', 189, 'VK_OEM_MINUS', '-', 'OEM_MINUS'],
|
||||
[81, 0, ScanCode.Equal, 'Equal', KeyCode.Equal, '=', 187, 'VK_OEM_PLUS', '=', 'OEM_PLUS'],
|
||||
[87, 0, ScanCode.BracketLeft, 'BracketLeft', KeyCode.BracketLeft, '[', 219, 'VK_OEM_4', '[', 'OEM_4'],
|
||||
[89, 0, ScanCode.BracketRight, 'BracketRight', KeyCode.BracketRight, ']', 221, 'VK_OEM_6', ']', 'OEM_6'],
|
||||
[88, 0, ScanCode.Backslash, 'Backslash', KeyCode.Backslash, '\\', 220, 'VK_OEM_5', '\\', 'OEM_5'],
|
||||
[0, 0, ScanCode.IntlHash, 'IntlHash', KeyCode.Unknown, empty, 0, empty, empty, empty], // has been dropped from the w3c spec
|
||||
[80, 0, ScanCode.Semicolon, 'Semicolon', KeyCode.Semicolon, ';', 186, 'VK_OEM_1', ';', 'OEM_1'],
|
||||
[90, 0, ScanCode.Quote, 'Quote', KeyCode.Quote, '\'', 222, 'VK_OEM_7', '\'', 'OEM_7'],
|
||||
[86, 0, ScanCode.Backquote, 'Backquote', KeyCode.Backquote, '`', 192, 'VK_OEM_3', '`', 'OEM_3'],
|
||||
[82, 0, ScanCode.Comma, 'Comma', KeyCode.Comma, ',', 188, 'VK_OEM_COMMA', ',', 'OEM_COMMA'],
|
||||
[84, 0, ScanCode.Period, 'Period', KeyCode.Period, '.', 190, 'VK_OEM_PERIOD', '.', 'OEM_PERIOD'],
|
||||
[85, 0, ScanCode.Slash, 'Slash', KeyCode.Slash, '/', 191, 'VK_OEM_2', '/', 'OEM_2'],
|
||||
[8, 1, ScanCode.CapsLock, 'CapsLock', KeyCode.CapsLock, 'CapsLock', 20, 'VK_CAPITAL', empty, empty],
|
||||
[59, 1, ScanCode.F1, 'F1', KeyCode.F1, 'F1', 112, 'VK_F1', empty, empty],
|
||||
[60, 1, ScanCode.F2, 'F2', KeyCode.F2, 'F2', 113, 'VK_F2', empty, empty],
|
||||
[61, 1, ScanCode.F3, 'F3', KeyCode.F3, 'F3', 114, 'VK_F3', empty, empty],
|
||||
[62, 1, ScanCode.F4, 'F4', KeyCode.F4, 'F4', 115, 'VK_F4', empty, empty],
|
||||
[63, 1, ScanCode.F5, 'F5', KeyCode.F5, 'F5', 116, 'VK_F5', empty, empty],
|
||||
[64, 1, ScanCode.F6, 'F6', KeyCode.F6, 'F6', 117, 'VK_F6', empty, empty],
|
||||
[65, 1, ScanCode.F7, 'F7', KeyCode.F7, 'F7', 118, 'VK_F7', empty, empty],
|
||||
[66, 1, ScanCode.F8, 'F8', KeyCode.F8, 'F8', 119, 'VK_F8', empty, empty],
|
||||
[67, 1, ScanCode.F9, 'F9', KeyCode.F9, 'F9', 120, 'VK_F9', empty, empty],
|
||||
[68, 1, ScanCode.F10, 'F10', KeyCode.F10, 'F10', 121, 'VK_F10', empty, empty],
|
||||
[69, 1, ScanCode.F11, 'F11', KeyCode.F11, 'F11', 122, 'VK_F11', empty, empty],
|
||||
[70, 1, ScanCode.F12, 'F12', KeyCode.F12, 'F12', 123, 'VK_F12', empty, empty],
|
||||
[0, 1, ScanCode.PrintScreen, 'PrintScreen', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[79, 1, ScanCode.ScrollLock, 'ScrollLock', KeyCode.ScrollLock, 'ScrollLock', 145, 'VK_SCROLL', empty, empty],
|
||||
[7, 1, ScanCode.Pause, 'Pause', KeyCode.PauseBreak, 'PauseBreak', 19, 'VK_PAUSE', empty, empty],
|
||||
[19, 1, ScanCode.Insert, 'Insert', KeyCode.Insert, 'Insert', 45, 'VK_INSERT', empty, empty],
|
||||
[14, 1, ScanCode.Home, 'Home', KeyCode.Home, 'Home', 36, 'VK_HOME', empty, empty],
|
||||
[11, 1, ScanCode.PageUp, 'PageUp', KeyCode.PageUp, 'PageUp', 33, 'VK_PRIOR', empty, empty],
|
||||
[20, 1, ScanCode.Delete, 'Delete', KeyCode.Delete, 'Delete', 46, 'VK_DELETE', empty, empty],
|
||||
[13, 1, ScanCode.End, 'End', KeyCode.End, 'End', 35, 'VK_END', empty, empty],
|
||||
[12, 1, ScanCode.PageDown, 'PageDown', KeyCode.PageDown, 'PageDown', 34, 'VK_NEXT', empty, empty],
|
||||
[17, 1, ScanCode.ArrowRight, 'ArrowRight', KeyCode.RightArrow, 'RightArrow', 39, 'VK_RIGHT', 'Right', empty],
|
||||
[15, 1, ScanCode.ArrowLeft, 'ArrowLeft', KeyCode.LeftArrow, 'LeftArrow', 37, 'VK_LEFT', 'Left', empty],
|
||||
[18, 1, ScanCode.ArrowDown, 'ArrowDown', KeyCode.DownArrow, 'DownArrow', 40, 'VK_DOWN', 'Down', empty],
|
||||
[16, 1, ScanCode.ArrowUp, 'ArrowUp', KeyCode.UpArrow, 'UpArrow', 38, 'VK_UP', 'Up', empty],
|
||||
[78, 1, ScanCode.NumLock, 'NumLock', KeyCode.NumLock, 'NumLock', 144, 'VK_NUMLOCK', empty, empty],
|
||||
[108, 1, ScanCode.NumpadDivide, 'NumpadDivide', KeyCode.NumpadDivide, 'NumPad_Divide', 111, 'VK_DIVIDE', empty, empty],
|
||||
[103, 1, ScanCode.NumpadMultiply, 'NumpadMultiply', KeyCode.NumpadMultiply, 'NumPad_Multiply', 106, 'VK_MULTIPLY', empty, empty],
|
||||
[106, 1, ScanCode.NumpadSubtract, 'NumpadSubtract', KeyCode.NumpadSubtract, 'NumPad_Subtract', 109, 'VK_SUBTRACT', empty, empty],
|
||||
[104, 1, ScanCode.NumpadAdd, 'NumpadAdd', KeyCode.NumpadAdd, 'NumPad_Add', 107, 'VK_ADD', empty, empty],
|
||||
[3, 1, ScanCode.NumpadEnter, 'NumpadEnter', KeyCode.Enter, empty, 0, empty, empty, empty],
|
||||
[94, 1, ScanCode.Numpad1, 'Numpad1', KeyCode.Numpad1, 'NumPad1', 97, 'VK_NUMPAD1', empty, empty],
|
||||
[95, 1, ScanCode.Numpad2, 'Numpad2', KeyCode.Numpad2, 'NumPad2', 98, 'VK_NUMPAD2', empty, empty],
|
||||
[96, 1, ScanCode.Numpad3, 'Numpad3', KeyCode.Numpad3, 'NumPad3', 99, 'VK_NUMPAD3', empty, empty],
|
||||
[97, 1, ScanCode.Numpad4, 'Numpad4', KeyCode.Numpad4, 'NumPad4', 100, 'VK_NUMPAD4', empty, empty],
|
||||
[98, 1, ScanCode.Numpad5, 'Numpad5', KeyCode.Numpad5, 'NumPad5', 101, 'VK_NUMPAD5', empty, empty],
|
||||
[99, 1, ScanCode.Numpad6, 'Numpad6', KeyCode.Numpad6, 'NumPad6', 102, 'VK_NUMPAD6', empty, empty],
|
||||
[100, 1, ScanCode.Numpad7, 'Numpad7', KeyCode.Numpad7, 'NumPad7', 103, 'VK_NUMPAD7', empty, empty],
|
||||
[101, 1, ScanCode.Numpad8, 'Numpad8', KeyCode.Numpad8, 'NumPad8', 104, 'VK_NUMPAD8', empty, empty],
|
||||
[102, 1, ScanCode.Numpad9, 'Numpad9', KeyCode.Numpad9, 'NumPad9', 105, 'VK_NUMPAD9', empty, empty],
|
||||
[93, 1, ScanCode.Numpad0, 'Numpad0', KeyCode.Numpad0, 'NumPad0', 96, 'VK_NUMPAD0', empty, empty],
|
||||
[107, 1, ScanCode.NumpadDecimal, 'NumpadDecimal', KeyCode.NumpadDecimal, 'NumPad_Decimal', 110, 'VK_DECIMAL', empty, empty],
|
||||
[92, 0, ScanCode.IntlBackslash, 'IntlBackslash', KeyCode.IntlBackslash, 'OEM_102', 226, 'VK_OEM_102', empty, empty],
|
||||
[58, 1, ScanCode.ContextMenu, 'ContextMenu', KeyCode.ContextMenu, 'ContextMenu', 93, empty, empty, empty],
|
||||
[0, 1, ScanCode.Power, 'Power', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.NumpadEqual, 'NumpadEqual', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[71, 1, ScanCode.F13, 'F13', KeyCode.F13, 'F13', 124, 'VK_F13', empty, empty],
|
||||
[72, 1, ScanCode.F14, 'F14', KeyCode.F14, 'F14', 125, 'VK_F14', empty, empty],
|
||||
[73, 1, ScanCode.F15, 'F15', KeyCode.F15, 'F15', 126, 'VK_F15', empty, empty],
|
||||
[74, 1, ScanCode.F16, 'F16', KeyCode.F16, 'F16', 127, 'VK_F16', empty, empty],
|
||||
[75, 1, ScanCode.F17, 'F17', KeyCode.F17, 'F17', 128, 'VK_F17', empty, empty],
|
||||
[76, 1, ScanCode.F18, 'F18', KeyCode.F18, 'F18', 129, 'VK_F18', empty, empty],
|
||||
[77, 1, ScanCode.F19, 'F19', KeyCode.F19, 'F19', 130, 'VK_F19', empty, empty],
|
||||
[0, 1, ScanCode.F20, 'F20', KeyCode.Unknown, empty, 0, 'VK_F20', empty, empty],
|
||||
[0, 1, ScanCode.F21, 'F21', KeyCode.Unknown, empty, 0, 'VK_F21', empty, empty],
|
||||
[0, 1, ScanCode.F22, 'F22', KeyCode.Unknown, empty, 0, 'VK_F22', empty, empty],
|
||||
[0, 1, ScanCode.F23, 'F23', KeyCode.Unknown, empty, 0, 'VK_F23', empty, empty],
|
||||
[0, 1, ScanCode.F24, 'F24', KeyCode.Unknown, empty, 0, 'VK_F24', empty, empty],
|
||||
[0, 1, ScanCode.Open, 'Open', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Help, 'Help', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Select, 'Select', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Again, 'Again', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Undo, 'Undo', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Cut, 'Cut', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Copy, 'Copy', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Paste, 'Paste', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Find, 'Find', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.AudioVolumeMute, 'AudioVolumeMute', KeyCode.AudioVolumeMute, 'AudioVolumeMute', 173, 'VK_VOLUME_MUTE', empty, empty],
|
||||
[0, 1, ScanCode.AudioVolumeUp, 'AudioVolumeUp', KeyCode.AudioVolumeUp, 'AudioVolumeUp', 175, 'VK_VOLUME_UP', empty, empty],
|
||||
[0, 1, ScanCode.AudioVolumeDown, 'AudioVolumeDown', KeyCode.AudioVolumeDown, 'AudioVolumeDown', 174, 'VK_VOLUME_DOWN', empty, empty],
|
||||
[105, 1, ScanCode.NumpadComma, 'NumpadComma', KeyCode.NUMPAD_SEPARATOR, 'NumPad_Separator', 108, 'VK_SEPARATOR', empty, empty],
|
||||
[110, 0, ScanCode.IntlRo, 'IntlRo', KeyCode.ABNT_C1, 'ABNT_C1', 193, 'VK_ABNT_C1', empty, empty],
|
||||
[0, 1, ScanCode.KanaMode, 'KanaMode', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 0, ScanCode.IntlYen, 'IntlYen', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Convert, 'Convert', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.NonConvert, 'NonConvert', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Lang1, 'Lang1', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Lang2, 'Lang2', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Lang3, 'Lang3', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Lang4, 'Lang4', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Lang5, 'Lang5', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Abort, 'Abort', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.Props, 'Props', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.NumpadParenLeft, 'NumpadParenLeft', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.NumpadParenRight, 'NumpadParenRight', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.NumpadBackspace, 'NumpadBackspace', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.NumpadMemoryStore, 'NumpadMemoryStore', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.NumpadMemoryRecall, 'NumpadMemoryRecall', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.NumpadMemoryClear, 'NumpadMemoryClear', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.NumpadMemoryAdd, 'NumpadMemoryAdd', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.NumpadMemorySubtract, 'NumpadMemorySubtract', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.NumpadClear, 'NumpadClear', KeyCode.Clear, 'Clear', 12, 'VK_CLEAR', empty, empty],
|
||||
[0, 1, ScanCode.NumpadClearEntry, 'NumpadClearEntry', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[5, 1, ScanCode.None, empty, KeyCode.Ctrl, 'Ctrl', 17, 'VK_CONTROL', empty, empty],
|
||||
[4, 1, ScanCode.None, empty, KeyCode.Shift, 'Shift', 16, 'VK_SHIFT', empty, empty],
|
||||
[6, 1, ScanCode.None, empty, KeyCode.Alt, 'Alt', 18, 'VK_MENU', empty, empty],
|
||||
[57, 1, ScanCode.None, empty, KeyCode.Meta, 'Meta', 0, 'VK_COMMAND', empty, empty],
|
||||
[5, 1, ScanCode.ControlLeft, 'ControlLeft', KeyCode.Ctrl, empty, 0, 'VK_LCONTROL', empty, empty],
|
||||
[4, 1, ScanCode.ShiftLeft, 'ShiftLeft', KeyCode.Shift, empty, 0, 'VK_LSHIFT', empty, empty],
|
||||
[6, 1, ScanCode.AltLeft, 'AltLeft', KeyCode.Alt, empty, 0, 'VK_LMENU', empty, empty],
|
||||
[57, 1, ScanCode.MetaLeft, 'MetaLeft', KeyCode.Meta, empty, 0, 'VK_LWIN', empty, empty],
|
||||
[5, 1, ScanCode.ControlRight, 'ControlRight', KeyCode.Ctrl, empty, 0, 'VK_RCONTROL', empty, empty],
|
||||
[4, 1, ScanCode.ShiftRight, 'ShiftRight', KeyCode.Shift, empty, 0, 'VK_RSHIFT', empty, empty],
|
||||
[6, 1, ScanCode.AltRight, 'AltRight', KeyCode.Alt, empty, 0, 'VK_RMENU', empty, empty],
|
||||
[57, 1, ScanCode.MetaRight, 'MetaRight', KeyCode.Meta, empty, 0, 'VK_RWIN', empty, empty],
|
||||
[0, 1, ScanCode.BrightnessUp, 'BrightnessUp', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.BrightnessDown, 'BrightnessDown', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.MediaPlay, 'MediaPlay', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.MediaRecord, 'MediaRecord', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.MediaFastForward, 'MediaFastForward', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.MediaRewind, 'MediaRewind', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[114, 1, ScanCode.MediaTrackNext, 'MediaTrackNext', KeyCode.MediaTrackNext, 'MediaTrackNext', 176, 'VK_MEDIA_NEXT_TRACK', empty, empty],
|
||||
[115, 1, ScanCode.MediaTrackPrevious, 'MediaTrackPrevious', KeyCode.MediaTrackPrevious, 'MediaTrackPrevious', 177, 'VK_MEDIA_PREV_TRACK', empty, empty],
|
||||
[116, 1, ScanCode.MediaStop, 'MediaStop', KeyCode.MediaStop, 'MediaStop', 178, 'VK_MEDIA_STOP', empty, empty],
|
||||
[0, 1, ScanCode.Eject, 'Eject', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[117, 1, ScanCode.MediaPlayPause, 'MediaPlayPause', KeyCode.MediaPlayPause, 'MediaPlayPause', 179, 'VK_MEDIA_PLAY_PAUSE', empty, empty],
|
||||
[0, 1, ScanCode.MediaSelect, 'MediaSelect', KeyCode.LaunchMediaPlayer, 'LaunchMediaPlayer', 181, 'VK_MEDIA_LAUNCH_MEDIA_SELECT', empty, empty],
|
||||
[0, 1, ScanCode.LaunchMail, 'LaunchMail', KeyCode.LaunchMail, 'LaunchMail', 180, 'VK_MEDIA_LAUNCH_MAIL', empty, empty],
|
||||
[0, 1, ScanCode.LaunchApp2, 'LaunchApp2', KeyCode.LaunchApp2, 'LaunchApp2', 183, 'VK_MEDIA_LAUNCH_APP2', empty, empty],
|
||||
[0, 1, ScanCode.LaunchApp1, 'LaunchApp1', KeyCode.Unknown, empty, 0, 'VK_MEDIA_LAUNCH_APP1', empty, empty],
|
||||
[0, 1, ScanCode.SelectTask, 'SelectTask', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.LaunchScreenSaver, 'LaunchScreenSaver', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.BrowserSearch, 'BrowserSearch', KeyCode.BrowserSearch, 'BrowserSearch', 170, 'VK_BROWSER_SEARCH', empty, empty],
|
||||
[0, 1, ScanCode.BrowserHome, 'BrowserHome', KeyCode.BrowserHome, 'BrowserHome', 172, 'VK_BROWSER_HOME', empty, empty],
|
||||
[112, 1, ScanCode.BrowserBack, 'BrowserBack', KeyCode.BrowserBack, 'BrowserBack', 166, 'VK_BROWSER_BACK', empty, empty],
|
||||
[113, 1, ScanCode.BrowserForward, 'BrowserForward', KeyCode.BrowserForward, 'BrowserForward', 167, 'VK_BROWSER_FORWARD', empty, empty],
|
||||
[0, 1, ScanCode.BrowserStop, 'BrowserStop', KeyCode.Unknown, empty, 0, 'VK_BROWSER_STOP', empty, empty],
|
||||
[0, 1, ScanCode.BrowserRefresh, 'BrowserRefresh', KeyCode.Unknown, empty, 0, 'VK_BROWSER_REFRESH', empty, empty],
|
||||
[0, 1, ScanCode.BrowserFavorites, 'BrowserFavorites', KeyCode.Unknown, empty, 0, 'VK_BROWSER_FAVORITES', empty, empty],
|
||||
[0, 1, ScanCode.ZoomToggle, 'ZoomToggle', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.MailReply, 'MailReply', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.MailForward, 'MailForward', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
[0, 1, ScanCode.MailSend, 'MailSend', KeyCode.Unknown, empty, 0, empty, empty, empty],
|
||||
|
||||
// See https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
|
||||
// If an Input Method Editor is processing key input and the event is keydown, return 229.
|
||||
[109, 1, ScanCode.None, empty, KeyCode.KEY_IN_COMPOSITION, 'KeyInComposition', 229, empty, empty, empty],
|
||||
[111, 1, ScanCode.None, empty, KeyCode.ABNT_C2, 'ABNT_C2', 194, 'VK_ABNT_C2', empty, empty],
|
||||
[91, 1, ScanCode.None, empty, KeyCode.OEM_8, 'OEM_8', 223, 'VK_OEM_8', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_KANA', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_HANGUL', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_JUNJA', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_FINAL', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_HANJA', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_KANJI', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_CONVERT', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_NONCONVERT', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_ACCEPT', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_MODECHANGE', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_SELECT', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_PRINT', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_EXECUTE', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_SNAPSHOT', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_HELP', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_APPS', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_PROCESSKEY', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_PACKET', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_DBE_SBCSCHAR', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_DBE_DBCSCHAR', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_ATTN', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_CRSEL', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_EXSEL', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_EREOF', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_PLAY', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_ZOOM', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_NONAME', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_PA1', empty, empty],
|
||||
[0, 1, ScanCode.None, empty, KeyCode.Unknown, empty, 0, 'VK_OEM_CLEAR', empty, empty],
|
||||
];
|
||||
|
||||
const seenKeyCode: boolean[] = [];
|
||||
const seenScanCode: boolean[] = [];
|
||||
for (const mapping of mappings) {
|
||||
const [_keyCodeOrd, immutable, scanCode, scanCodeStr, keyCode, keyCodeStr, eventKeyCode, vkey, usUserSettingsLabel, generalUserSettingsLabel] = mapping;
|
||||
if (!seenScanCode[scanCode]) {
|
||||
seenScanCode[scanCode] = true;
|
||||
scanCodeIntToStr[scanCode] = scanCodeStr;
|
||||
scanCodeStrToInt[scanCodeStr] = scanCode;
|
||||
scanCodeLowerCaseStrToInt[scanCodeStr.toLowerCase()] = scanCode;
|
||||
if (immutable) {
|
||||
IMMUTABLE_CODE_TO_KEY_CODE[scanCode] = keyCode;
|
||||
if (
|
||||
(keyCode !== KeyCode.Unknown)
|
||||
&& (keyCode !== KeyCode.Enter)
|
||||
&& (keyCode !== KeyCode.Ctrl)
|
||||
&& (keyCode !== KeyCode.Shift)
|
||||
&& (keyCode !== KeyCode.Alt)
|
||||
&& (keyCode !== KeyCode.Meta)
|
||||
) {
|
||||
IMMUTABLE_KEY_CODE_TO_CODE[keyCode] = scanCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!seenKeyCode[keyCode]) {
|
||||
seenKeyCode[keyCode] = true;
|
||||
if (!keyCodeStr) {
|
||||
throw new Error(`String representation missing for key code ${keyCode} around scan code ${scanCodeStr}`);
|
||||
}
|
||||
uiMap.define(keyCode, keyCodeStr);
|
||||
userSettingsUSMap.define(keyCode, usUserSettingsLabel || keyCodeStr);
|
||||
userSettingsGeneralMap.define(keyCode, generalUserSettingsLabel || usUserSettingsLabel || keyCodeStr);
|
||||
}
|
||||
if (eventKeyCode) {
|
||||
EVENT_KEY_CODE_MAP[eventKeyCode] = keyCode;
|
||||
}
|
||||
if (vkey) {
|
||||
NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE[vkey] = keyCode;
|
||||
}
|
||||
}
|
||||
// Manually added due to the exclusion above (due to duplication with NumpadEnter)
|
||||
IMMUTABLE_KEY_CODE_TO_CODE[KeyCode.Enter] = ScanCode.Enter;
|
||||
|
||||
})();
|
||||
|
||||
export namespace KeyCodeUtils {
|
||||
export function toString(keyCode: KeyCode): string {
|
||||
return uiMap.keyCodeToStr(keyCode);
|
||||
}
|
||||
export function fromString(key: string): KeyCode {
|
||||
return uiMap.strToKeyCode(key);
|
||||
}
|
||||
|
||||
export function toUserSettingsUS(keyCode: KeyCode): string {
|
||||
return userSettingsUSMap.keyCodeToStr(keyCode);
|
||||
}
|
||||
export function toUserSettingsGeneral(keyCode: KeyCode): string {
|
||||
return userSettingsGeneralMap.keyCodeToStr(keyCode);
|
||||
}
|
||||
export function fromUserSettings(key: string): KeyCode {
|
||||
return userSettingsUSMap.strToKeyCode(key) || userSettingsGeneralMap.strToKeyCode(key);
|
||||
}
|
||||
|
||||
export function toElectronAccelerator(keyCode: KeyCode): string | null {
|
||||
if (keyCode >= KeyCode.Numpad0 && keyCode <= KeyCode.NumpadDivide) {
|
||||
// [Electron Accelerators] Electron is able to parse numpad keys, but unfortunately it
|
||||
// renders them just as regular keys in menus. For example, num0 is rendered as "0",
|
||||
// numdiv is rendered as "/", numsub is rendered as "-".
|
||||
//
|
||||
// This can lead to incredible confusion, as it makes numpad based keybindings indistinguishable
|
||||
// from keybindings based on regular keys.
|
||||
//
|
||||
// We therefore need to fall back to custom rendering for numpad keys.
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (keyCode) {
|
||||
case KeyCode.UpArrow:
|
||||
return 'Up';
|
||||
case KeyCode.DownArrow:
|
||||
return 'Down';
|
||||
case KeyCode.LeftArrow:
|
||||
return 'Left';
|
||||
case KeyCode.RightArrow:
|
||||
return 'Right';
|
||||
}
|
||||
|
||||
return uiMap.keyCodeToStr(keyCode);
|
||||
}
|
||||
}
|
||||
|
||||
export const enum KeyMod {
|
||||
CtrlCmd = (1 << 11) >>> 0,
|
||||
Shift = (1 << 10) >>> 0,
|
||||
Alt = (1 << 9) >>> 0,
|
||||
WinCtrl = (1 << 8) >>> 0,
|
||||
}
|
||||
|
||||
export function KeyChord(firstPart: number, secondPart: number): number {
|
||||
const chordPart = ((secondPart & 0x0000FFFF) << 16) >>> 0;
|
||||
return (firstPart | chordPart) >>> 0;
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
import { fixedUi } from '@/core/main/init/ui';
|
||||
import type { DamageEnemy } from '../game/enemy/damage';
|
||||
import { tip } from './utils';
|
||||
import { ref, Ref } from 'vue';
|
||||
|
||||
export interface MarkInfo<T extends EnemyIds> {
|
||||
id: T;
|
||||
enemy: DamageEnemy<T>;
|
||||
/**
|
||||
* 提示模式,从低到高位数分别为:
|
||||
* 1. 踩临界时
|
||||
* 2. 能打过怪物时
|
||||
* 3. 小于勇士生命值的2/3时
|
||||
* 4. 小于勇士生命值的1/3时
|
||||
* 5. 零伤时
|
||||
* 6. 小于指定伤害时
|
||||
*/
|
||||
mode: number;
|
||||
/** 当前提示状态,提示模式的 2-6 */
|
||||
status: number;
|
||||
lastAtk: number;
|
||||
lastDamage: number;
|
||||
markDamage?: number;
|
||||
/** 数据更新用,取反更新标记信息 */
|
||||
update: Ref<boolean>;
|
||||
}
|
||||
|
||||
const uiMap = new Map<EnemyIds, number>();
|
||||
const marked: MarkInfo<EnemyIds>[] = [];
|
||||
|
||||
/**
|
||||
* 标记一个怪物,标记后的怪物会在勇士刚好能打过怪物时、伤害刚好小于勇士生命值的2/3和1/3时、踩到临界时提示
|
||||
* @param id 标记的怪物id
|
||||
*/
|
||||
export function markEnemy(id: EnemyIds) {
|
||||
if (hasMarkedEnemy(id)) return;
|
||||
const DamageEnemy = Mota.require('class', 'DamageEnemy');
|
||||
const enemy = new DamageEnemy(core.material.enemys[id]);
|
||||
enemy.calAttribute();
|
||||
enemy.getRealInfo();
|
||||
|
||||
const info: MarkInfo<EnemyIds> = {
|
||||
id,
|
||||
enemy,
|
||||
mode: 0b011111,
|
||||
lastAtk: Mota.requireAll('fn').getHeroStatusOn('atk', 'empty'),
|
||||
lastDamage: enemy.calDamage().damage,
|
||||
status: 0b0,
|
||||
update: ref(true)
|
||||
};
|
||||
marked.push(info);
|
||||
|
||||
uiMap.set(id, fixedUi.open('markedEnemy', { enemy: info }));
|
||||
|
||||
tip('success', `已标记 ${enemy.enemy.name}!`);
|
||||
}
|
||||
|
||||
export function unmarkEnemy(id: EnemyIds) {
|
||||
fixedUi.close(uiMap.get(id) ?? -1);
|
||||
uiMap.delete(id);
|
||||
const index = marked.findIndex(v => v.id === id);
|
||||
if (index === -1) return;
|
||||
tip('success', `已取消标记 ${marked[index].enemy.enemy.name}!`);
|
||||
marked.splice(index, 1);
|
||||
}
|
||||
|
||||
export function checkMarkedEnemy() {
|
||||
const { getHeroStatusOn } = Mota.requireAll('fn');
|
||||
marked.forEach(v => {
|
||||
const { id, enemy, mode, lastAtk, lastDamage, markDamage } = v;
|
||||
const atk = getHeroStatusOn('atk', 'empty');
|
||||
let tip = 0;
|
||||
if (mode & 0b11110) {
|
||||
const damage = enemy.calDamage().damage;
|
||||
const hp = core.status.hero.hp;
|
||||
v.lastDamage = damage;
|
||||
if (damage > lastDamage) return;
|
||||
// 重置标记状态
|
||||
if (damage > hp) {
|
||||
v.status &= 0b100001;
|
||||
}
|
||||
if (damage > (markDamage ?? Infinity)) {
|
||||
v.status &= 0b1;
|
||||
}
|
||||
// 能打过怪物提示、2/3提示、1/3提示、零伤提示、指定伤害提示
|
||||
if (mode & (1 << 1) && damage < hp && damage > (hp * 2) / 3) {
|
||||
if (!(v.status & (1 << 1))) {
|
||||
v.status &= 0b100001;
|
||||
v.status |= 1 << 1;
|
||||
tip |= 1 << 1;
|
||||
}
|
||||
} else if (mode & (1 << 2) && damage > hp / 3) {
|
||||
if (!(v.status & (1 << 2))) {
|
||||
v.status &= 0b100011;
|
||||
v.status |= 1 << 2;
|
||||
tip |= 1 << 2;
|
||||
}
|
||||
} else if (mode & (1 << 3) && damage > 0) {
|
||||
if (!(v.status & (1 << 3))) {
|
||||
v.status &= 0b100111;
|
||||
v.status |= 1 << 3;
|
||||
tip |= 1 << 3;
|
||||
}
|
||||
} else if (mode & (1 << 4)) {
|
||||
if (!(v.status & (1 << 4))) {
|
||||
v.status &= 0b101111;
|
||||
v.status |= 1 << 4;
|
||||
tip |= 1 << 4;
|
||||
}
|
||||
}
|
||||
if (mode & (1 << 5) && damage < (markDamage ?? Infinity)) {
|
||||
if (!(v.status & (1 << 5))) {
|
||||
if (damage < (markDamage ?? Infinity)) {
|
||||
v.status |= 1 << 5;
|
||||
} else {
|
||||
v.status &= 0b011111;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 临界提示
|
||||
if (mode & (1 << 0)) {
|
||||
const critical = enemy.calCritical(1)[0]?.atkDelta ?? Infinity;
|
||||
v.lastAtk = atk + critical;
|
||||
if (critical + atk > lastAtk) {
|
||||
tip |= 1 << 0;
|
||||
}
|
||||
}
|
||||
makeTip(id, tip, v);
|
||||
v.update.value = !v.update.value;
|
||||
});
|
||||
}
|
||||
|
||||
function makeTip(enemy: EnemyIds, mode: number, info: MarkInfo<EnemyIds>) {
|
||||
const name = core.material.enemys[enemy].name;
|
||||
if (mode & (1 << 0)) {
|
||||
tip('success', `已踩到 ${name} 的临界!`);
|
||||
}
|
||||
if (mode & (1 << 1)) {
|
||||
tip('success', `已能打过 ${name}!`);
|
||||
}
|
||||
if (mode & (1 << 2)) {
|
||||
tip('success', `${name} 的伤害已降至 2/3!`);
|
||||
}
|
||||
if (mode & (1 << 3)) {
|
||||
tip('success', `${name} 的伤害已降至 1/3!`);
|
||||
}
|
||||
if (mode & (1 << 4)) {
|
||||
tip('success', `${name} 已零伤!`);
|
||||
}
|
||||
if (mode & (1 << 5)) {
|
||||
const damage = core.formatBigNumber(info.markDamage ?? Infinity);
|
||||
tip('success', `${name} 的伤害已降至 ${damage}!`);
|
||||
}
|
||||
}
|
||||
|
||||
export function hasMarkedEnemy(id: EnemyIds) {
|
||||
return marked.some(v => v.id === id);
|
||||
}
|
||||
|
||||
const hook = Mota.require('var', 'hook');
|
||||
hook.on('statusBarUpdate', () => {
|
||||
checkMarkedEnemy();
|
||||
});
|
||||
@@ -1,120 +0,0 @@
|
||||
import { ref } from 'vue';
|
||||
import list from '../../data/achievement.json';
|
||||
import { achiDict, checkCompletionAchievement } from '../completion';
|
||||
import { changeLocalStorage, has } from '../utils';
|
||||
|
||||
type AchievementList = typeof list;
|
||||
export type AchievementType = keyof AchievementList;
|
||||
|
||||
type AchievementData = Record<AchievementType, boolean[]>;
|
||||
|
||||
export interface Achievement {
|
||||
name: string;
|
||||
text: string[];
|
||||
point: number;
|
||||
hide?: string;
|
||||
progress?: string;
|
||||
percent?: boolean;
|
||||
}
|
||||
|
||||
export default function init() {
|
||||
return { completeAchievement, hasCompletedAchievement, addMountSign };
|
||||
}
|
||||
|
||||
export const totalPoint = Object.values(list)
|
||||
.map((v: Achievement[]) =>
|
||||
v.reduce((prev, curr) => {
|
||||
return curr.point + prev;
|
||||
}, 0)
|
||||
)
|
||||
.reduce((prev, curr) => prev + curr);
|
||||
|
||||
/**
|
||||
* 完成一个成就
|
||||
* @param type 成就类型
|
||||
* @param index 成就索引
|
||||
*/
|
||||
export function completeAchievement(type: AchievementType, index: number) {
|
||||
if (flags.debug || hasCompletedAchievement(type, index)) return;
|
||||
changeLocalStorage<AchievementData>(
|
||||
'achievement',
|
||||
data => {
|
||||
data[type][index] = true;
|
||||
return data;
|
||||
},
|
||||
{
|
||||
normal: [],
|
||||
challenge: [],
|
||||
explore: []
|
||||
}
|
||||
);
|
||||
if (type === 'explore' && !Object.values(achiDict).includes(index)) {
|
||||
checkCompletionAchievement();
|
||||
}
|
||||
Mota.require('var', 'fixedUi').open('completeAchi', {
|
||||
complete: `${type},${index}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否完成了某个成就
|
||||
* @param type 成就类型
|
||||
* @param index 成就索引
|
||||
*/
|
||||
export function hasCompletedAchievement(type: AchievementType, index: number) {
|
||||
let data = core.getLocalStorage<AchievementData>('achievement');
|
||||
if (!has(data)) {
|
||||
const d = {
|
||||
normal: [],
|
||||
challenge: [],
|
||||
explore: []
|
||||
};
|
||||
data = d;
|
||||
core.setLocalStorage('achievement', d);
|
||||
}
|
||||
return data[type][index] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前成就点数
|
||||
*/
|
||||
export function getNowPoint() {
|
||||
let res = 0;
|
||||
for (const [type, achi] of Object.entries(list)) {
|
||||
achi.forEach((v, i) => {
|
||||
if (hasCompletedAchievement(type as AchievementType, i)) {
|
||||
res += v.point;
|
||||
}
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// ----- 各个成就相关的函数
|
||||
|
||||
/**
|
||||
* 山路木牌
|
||||
* @param id 木牌id
|
||||
*/
|
||||
export function addMountSign(id: number) {
|
||||
if (flags.debug) return;
|
||||
if (
|
||||
!core.getLocalStorage(`mountSign_${id}`, false) &&
|
||||
!hasCompletedAchievement('explore', 1)
|
||||
) {
|
||||
changeLocalStorage(
|
||||
'mountSign',
|
||||
n => {
|
||||
if (n + 1 >= 5) {
|
||||
completeAchievement('explore', 1);
|
||||
for (const i of [1, 2, 3, 4, 5]) {
|
||||
core.removeLocalStorage(`mountSign_${i}`);
|
||||
}
|
||||
}
|
||||
return n + 1;
|
||||
},
|
||||
0
|
||||
);
|
||||
core.setLocalStorage(`mountSign_${id}`, true);
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import type { CurrentEnemy } from '../../game/enemy/battle';
|
||||
import { has } from '../utils';
|
||||
|
||||
export interface ToShowEnemy extends CurrentEnemy {
|
||||
critical: string;
|
||||
criticalDam: string;
|
||||
defDam: string;
|
||||
/** [名称, 描述, 颜色] */
|
||||
special: [string, string, string][];
|
||||
damageColor: string;
|
||||
showSpecial: [string, string, string][];
|
||||
damage: string;
|
||||
}
|
||||
|
||||
interface BookDetailInfo {
|
||||
/** 怪物手册详细信息展示的怪物 */
|
||||
enemy?: ToShowEnemy;
|
||||
/** 怪物手册的怪物详细信息的初始位置 */
|
||||
pos?: number;
|
||||
}
|
||||
|
||||
export const detailInfo: BookDetailInfo = {};
|
||||
|
||||
/**
|
||||
* 获取怪物的特殊技能描述
|
||||
* @param enemy 怪物实例
|
||||
*/
|
||||
export function getSpecialHint(enemy: ToShowEnemy) {
|
||||
return (
|
||||
<div>
|
||||
{enemy.special.map((v, i) => {
|
||||
return (
|
||||
<div class="special">
|
||||
<span style={{ color: v[2] }}>
|
||||
{v[0]}:
|
||||
</span>
|
||||
<span innerHTML={v[1]}></span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得怪物的最近100个加防减伤
|
||||
* @param enemy 怪物实例
|
||||
*/
|
||||
export function getDefDamage(
|
||||
enemy: ToShowEnemy,
|
||||
addDef: number = 0,
|
||||
addAtk: number = 0
|
||||
) {
|
||||
const ratio = core.status.thisMap.ratio;
|
||||
const res: [number, number][] = [];
|
||||
|
||||
let origin: number | undefined;
|
||||
let last = 0;
|
||||
|
||||
const max = 100 - Math.floor(addDef / ratio);
|
||||
|
||||
for (let i = 0; i <= max; i++) {
|
||||
const dam = enemy.enemy.calDamage({
|
||||
atk: core.status.hero.atk + addAtk,
|
||||
def: core.status.hero.def + addDef + i * ratio
|
||||
});
|
||||
|
||||
if (res.length === 0) {
|
||||
origin = dam.damage;
|
||||
if (has(origin)) {
|
||||
res.push([addDef + i * ratio, origin]);
|
||||
last = origin;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!isFinite(dam.damage)) continue;
|
||||
if (dam.damage === res.at(-1)?.[1]) continue;
|
||||
last = dam.damage;
|
||||
res.push([ratio * i + addDef, dam.damage]);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取怪物的临界信息
|
||||
* @param enemy 怪物实例
|
||||
*/
|
||||
export function getCriticalDamage(
|
||||
enemy: ToShowEnemy,
|
||||
addAtk: number = 0,
|
||||
addDef: number = 0
|
||||
): [number, number][] {
|
||||
const ratio = core.status.thisMap.ratio;
|
||||
const res: [number, number][] = [];
|
||||
|
||||
let origin: number | undefined;
|
||||
let last = 0;
|
||||
|
||||
const max = 100 - Math.floor(addAtk / ratio);
|
||||
|
||||
for (let i = 0; i <= max; i++) {
|
||||
const dam = enemy.enemy.calDamage({
|
||||
atk: core.status.hero.atk + addAtk + i * ratio,
|
||||
def: core.status.hero.def + addDef
|
||||
});
|
||||
|
||||
if (res.length === 0) {
|
||||
origin = dam.damage;
|
||||
if (has(origin)) {
|
||||
res.push([addAtk + i * ratio, origin]);
|
||||
last = origin;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!isFinite(dam.damage)) continue;
|
||||
if (dam.damage === res.at(-1)?.[1]) continue;
|
||||
last = dam.damage;
|
||||
res.push([ratio * i + addAtk, dam.damage]);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Ref, ref, watch } from 'vue';
|
||||
import { nextFrame } from '../utils';
|
||||
|
||||
interface ChangableValue<T> {
|
||||
change: Ref<boolean>;
|
||||
value: Ref<T>;
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个监听响应式变量更改的,可以用于Changable的监听器
|
||||
* @param value 要监听的值
|
||||
*/
|
||||
export function createChangable<T>(
|
||||
value: Ref<T>,
|
||||
key?: keyof T
|
||||
): ChangableValue<T> {
|
||||
const change = ref(false);
|
||||
const stop = watch(value, (n, o) => {
|
||||
if (key) {
|
||||
if (n[key] === o[key]) return;
|
||||
}
|
||||
if (change.value) {
|
||||
change.value = false;
|
||||
nextFrame(() => (change.value = true));
|
||||
} else {
|
||||
change.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
change,
|
||||
value,
|
||||
stop
|
||||
};
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import { getStatusLabel, has } from '../utils';
|
||||
|
||||
/**
|
||||
* 获取所有装备
|
||||
*/
|
||||
export function getEquips(): [ItemIdOf<'equips'>, number][] {
|
||||
return Object.entries(core.status.hero.items.equips) as [
|
||||
ItemIdOf<'equips'>,
|
||||
number
|
||||
][];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取装备增加的属性
|
||||
* @param equip 装备
|
||||
*/
|
||||
export function getAddStatus(equip: Equip) {
|
||||
const toGet = Object.assign({}, equip.value, equip.percentage);
|
||||
const keys = Object.keys(toGet) as (keyof typeof toGet)[];
|
||||
|
||||
return (
|
||||
<div class="equip-add-detail">
|
||||
{keys.map(v => {
|
||||
const value = Math.floor(
|
||||
(equip.value[v] ?? 0) * core.getBuff(v) +
|
||||
(core.status.hero[v as keyof HeroStatus] *
|
||||
(equip.percentage[v] ?? 0)) /
|
||||
100
|
||||
);
|
||||
|
||||
return (
|
||||
<span style="display: flex">
|
||||
<span style="flex-basis: 50%">{getStatusLabel(v)}</span>
|
||||
|
||||
<span
|
||||
style={{
|
||||
color: value > 0 ? 'lightgreen' : 'lightsalmon'
|
||||
}}
|
||||
>
|
||||
{value > 0 ? `+${value}` : value}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前勇士属性,如果有选中的装备,会在后面追加显示增加量
|
||||
* @param nowEquip 当前选中的装备
|
||||
*/
|
||||
export function getNowStatus(nowEquip?: Equip, onCol: boolean = false) {
|
||||
const toShow = [
|
||||
'hp',
|
||||
'lv',
|
||||
'atk',
|
||||
'def',
|
||||
'mdef',
|
||||
'mana',
|
||||
'hpmax',
|
||||
'money'
|
||||
] as (keyof SelectType<HeroStatus, number>)[];
|
||||
const { getHeroStatusOn } = Mota.requireAll('fn');
|
||||
|
||||
return (
|
||||
<div id="hero-status">
|
||||
{toShow.map(v => {
|
||||
let status: string;
|
||||
if (v === 'lv') status = core.getLvName() ?? '';
|
||||
else status = getHeroStatusOn(v)?.toString();
|
||||
|
||||
let add = 0;
|
||||
if (has(nowEquip)) {
|
||||
add += Math.floor(
|
||||
(nowEquip.value[v] ?? 0) * core.getBuff(v)
|
||||
);
|
||||
const per = Math.floor(
|
||||
(nowEquip.percentage[v] * core.getStatus(v)) / 100
|
||||
);
|
||||
add += isNaN(per) ? 0 : per;
|
||||
}
|
||||
if (onCol) add = -add;
|
||||
|
||||
return (
|
||||
<div class="hero-status-one">
|
||||
<span class="hero-status-label">
|
||||
{getStatusLabel(v)}
|
||||
</span>
|
||||
<div class="hero-status-value">
|
||||
<span style="margin-right: 20%">{status}</span>
|
||||
{add !== 0 && (
|
||||
<span
|
||||
style={{
|
||||
color:
|
||||
add > 0
|
||||
? 'lightgreen'
|
||||
: 'lightsalmon'
|
||||
}}
|
||||
>
|
||||
{add > 0 ? '+' + add : '-' + -add}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { getDamageColor } from '../utils';
|
||||
import { ToShowEnemy } from './book';
|
||||
import type { DamageEnemy, EnemyInfo } from '../../game/enemy/damage';
|
||||
import { isMobile } from '../use';
|
||||
|
||||
export function getLocFromMouseLoc(x: number, y: number): LocArr {
|
||||
const mx = Math.round(x + core.bigmap.offsetX / 32);
|
||||
const my = Math.round(y + core.bigmap.offsetY / 32);
|
||||
return [mx, my];
|
||||
}
|
||||
|
||||
export function getDetailedEnemy(
|
||||
enemy: DamageEnemy,
|
||||
floorId: FloorIds = core.status.floorId
|
||||
): ToShowEnemy {
|
||||
const ratio = core.status.maps[floorId].ratio;
|
||||
|
||||
const dam = enemy.calDamage().damage;
|
||||
const cri = enemy.calCritical(1)[0];
|
||||
const critical = core.formatBigNumber(cri?.atkDelta);
|
||||
const criticalDam = core.formatBigNumber(cri?.delta);
|
||||
const defDam = core.formatBigNumber(enemy.calDefDamage(ratio).delta);
|
||||
const damage = core.formatBigNumber(dam);
|
||||
|
||||
const fromFunc = (
|
||||
func: string | ((enemy: EnemyInfo) => string),
|
||||
enemy: EnemyInfo
|
||||
) => {
|
||||
return typeof func === 'string' ? func : func(enemy);
|
||||
};
|
||||
const special: [string, string, string][] = [...enemy.info.special]
|
||||
.filter(v => !enemy.info.specialHalo?.includes(v))
|
||||
.map(vv => {
|
||||
const s = Mota.require('var', 'enemySpecials')[vv];
|
||||
return [
|
||||
fromFunc(s.name, enemy.info),
|
||||
fromFunc(s.desc, enemy.info),
|
||||
s.color as string
|
||||
];
|
||||
});
|
||||
const l = isMobile ? 1 : 2;
|
||||
const showSpecial =
|
||||
special.length > l
|
||||
? special.slice(0, l).concat([['...', '', '#fff']])
|
||||
: special.slice();
|
||||
|
||||
const damageColor = getDamageColor(dam) as string;
|
||||
|
||||
const detail: ToShowEnemy = {
|
||||
enemy,
|
||||
onMapEnemy: [enemy],
|
||||
critical,
|
||||
criticalDam,
|
||||
defDam,
|
||||
damageColor,
|
||||
special,
|
||||
showSpecial,
|
||||
damage
|
||||
};
|
||||
return detail;
|
||||
}
|
||||
@@ -1,565 +0,0 @@
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { downloadCanvasImage, has, tip } from '../utils';
|
||||
|
||||
type BFSFromString = `${FloorIds},${number},${number},${Dir}`;
|
||||
type BFSToString = `${FloorIds},${number},${number}`;
|
||||
|
||||
interface MapBFSResult {
|
||||
maps: FloorIds[];
|
||||
link: Record<BFSFromString, BFSToString>;
|
||||
}
|
||||
|
||||
interface MapDrawData {
|
||||
locs: Partial<Record<FloorIds, LocArr>>;
|
||||
line: [number, number, number, number][];
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
let area: Record<string, FloorIds[]> = {};
|
||||
|
||||
const bfsCache: Partial<Record<FloorIds, MapBFSResult>> = {};
|
||||
/**
|
||||
* 键的格式:FloorIds,interval,border
|
||||
*/
|
||||
const drawCache: Record<string, MapDrawData> = {};
|
||||
|
||||
const arrow: Partial<Record<AllNumbers, Dir>> = {
|
||||
92: 'left',
|
||||
94: 'right',
|
||||
91: 'up',
|
||||
93: 'down'
|
||||
};
|
||||
|
||||
/**
|
||||
* 切分地图区域
|
||||
*/
|
||||
export function splitArea() {
|
||||
area = {};
|
||||
const used: FloorIds[] = [];
|
||||
for (const id of core.floorIds) {
|
||||
if (used.includes(id) || core.status.maps[id].deleted) continue;
|
||||
const data = getMapData(id, true);
|
||||
used.push(...data.maps);
|
||||
if (data.maps.length > 1) {
|
||||
const title = core.status.maps[id].title;
|
||||
area[title] = data.maps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getArea() {
|
||||
return area;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取地图绘制信息
|
||||
* @param floorId 中心楼层
|
||||
* @param interval 地图间距
|
||||
* @param border 边框宽度
|
||||
* @param noCache 是否不使用缓存
|
||||
*/
|
||||
export function getMapDrawData(
|
||||
floorId: FloorIds,
|
||||
interval: number = 5,
|
||||
border: number = 1,
|
||||
noCache: boolean = false
|
||||
): MapDrawData {
|
||||
const id = `${floorId},${interval},${border}`;
|
||||
if (!floorId) return { locs: {}, line: [], width: 1, height: 1 };
|
||||
if (drawCache[id] && !noCache) return drawCache[id];
|
||||
const { link, maps } = getMapData(floorId, noCache);
|
||||
const locs: Partial<Record<FloorIds, LocArr>> = {};
|
||||
const line: [number, number, number, number][] = [];
|
||||
const center = core.status.maps[floorId];
|
||||
let left = -center.width / 2,
|
||||
right = center.width / 2,
|
||||
top = -center.height / 2,
|
||||
bottom = center.height / 2;
|
||||
for (const [from, to] of Object.entries(link)) {
|
||||
const [fromId, fxs, fys, dir] = from.split(',') as [
|
||||
FloorIds,
|
||||
string,
|
||||
string,
|
||||
Dir
|
||||
];
|
||||
const [toId, txs, tys] = to.split(',') as [FloorIds, string, string];
|
||||
const fromMap = core.status.maps[fromId];
|
||||
const toMap = core.status.maps[toId];
|
||||
const fx = parseInt(fxs),
|
||||
fy = parseInt(fys),
|
||||
tx = parseInt(txs),
|
||||
ty = parseInt(tys);
|
||||
const fw = fromMap.width,
|
||||
fh = fromMap.height;
|
||||
const tw = toMap.width,
|
||||
th = toMap.height;
|
||||
locs[fromId] ??= [0, 0];
|
||||
const [fromX, fromY] = locs[fromId]!;
|
||||
if (!locs[toId]) {
|
||||
const dx = core.utils.scan[dir].x,
|
||||
dy = core.utils.scan[dir].y;
|
||||
const toX =
|
||||
fromX +
|
||||
(fx - fw / 2) -
|
||||
(tx - tw / 2) +
|
||||
(border * 2 + interval) * dx,
|
||||
toY =
|
||||
fromY +
|
||||
(fy - fh / 2) -
|
||||
(ty - th / 2) +
|
||||
(border * 2 + interval) * dy;
|
||||
// 地图位置和连线位置
|
||||
locs[toId] = [toX, toY];
|
||||
}
|
||||
const [toX, toY] = locs[toId]!;
|
||||
line.push([
|
||||
fromX + (fx - fw / 2 + 0.5),
|
||||
fromY + (fy - fh / 2 + 0.5),
|
||||
toX + (tx - tw / 2 + 0.5),
|
||||
toY + (ty - th / 2 + 0.5)
|
||||
]);
|
||||
|
||||
// 计算地图总长宽
|
||||
const l = toX - tw / 2,
|
||||
r = toX + tw / 2,
|
||||
t = toY - th / 2,
|
||||
b = toY + th / 2;
|
||||
if (l < left) left = l;
|
||||
if (r > right) right = r;
|
||||
if (t < top) top = t;
|
||||
if (b > bottom) bottom = b;
|
||||
}
|
||||
|
||||
// 移动位置,居中
|
||||
Object.values(locs).forEach(v => {
|
||||
v[0] -= left;
|
||||
v[1] -= top;
|
||||
});
|
||||
line.forEach(v => {
|
||||
v[0] -= left;
|
||||
v[2] -= left;
|
||||
v[1] -= top;
|
||||
v[3] -= top;
|
||||
});
|
||||
|
||||
left -= 5;
|
||||
right += 5;
|
||||
top -= 5;
|
||||
bottom += 5;
|
||||
|
||||
const res = { locs, line, width: right - left, height: bottom - top };
|
||||
|
||||
return (drawCache[id] = res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 广度优先搜索地图信息
|
||||
* @param floorId 中心楼层id
|
||||
* @param noCache 是否不使用缓存
|
||||
*/
|
||||
export function getMapData(
|
||||
floorId: FloorIds,
|
||||
noCache: boolean = false
|
||||
): MapBFSResult {
|
||||
if (!floorId) return { maps: [], link: {} };
|
||||
if (has(bfsCache[floorId]) && !noCache) return bfsCache[floorId]!;
|
||||
|
||||
const queue = [floorId];
|
||||
const used: Partial<Record<FloorIds, boolean>> = {
|
||||
[floorId]: true
|
||||
};
|
||||
const floors = [floorId];
|
||||
const link: Record<BFSFromString, BFSToString> = {};
|
||||
|
||||
while (queue.length > 0) {
|
||||
const now = queue.shift()!;
|
||||
const floor = core.floors[now];
|
||||
const change = floor.changeFloor;
|
||||
for (const [loc, ev] of Object.entries(change)) {
|
||||
const target = ev.floorId as FloorIds;
|
||||
if (target.startsWith(':')) continue;
|
||||
const [x, y] = loc.split(',').map(v => parseInt(v));
|
||||
const id = floor.map[y][x] as AllNumbers;
|
||||
if (id in arrow) {
|
||||
if (!used[target]) {
|
||||
const from = `${now},${loc},${arrow[id]}` as BFSFromString;
|
||||
const to = `${target},${ev.loc![0]},${
|
||||
ev.loc![1]
|
||||
}` as BFSToString;
|
||||
link[from] = to;
|
||||
queue.push(target);
|
||||
floors.push(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
used[now] = true;
|
||||
}
|
||||
|
||||
const res = {
|
||||
maps: floors,
|
||||
link
|
||||
};
|
||||
|
||||
return (bfsCache[floorId] = res);
|
||||
}
|
||||
|
||||
type Loc2 = [number, number, number, number];
|
||||
|
||||
export class MinimapDrawer {
|
||||
ctx: CanvasRenderingContext2D;
|
||||
canvas: HTMLCanvasElement;
|
||||
scale: number = 1;
|
||||
nowFloor: FloorIds = core.status.floorId;
|
||||
nowArea: string = '';
|
||||
|
||||
// position & config
|
||||
ox: number = 0;
|
||||
oy: number = 0;
|
||||
noBorder: boolean = false;
|
||||
showInfo: boolean = false;
|
||||
|
||||
// cache
|
||||
drawedThumbnail: Partial<Record<FloorIds, boolean>> = {};
|
||||
thumbnailLoc: Partial<Record<FloorIds, Loc2>> = {};
|
||||
|
||||
// temp
|
||||
private tempCanvas: HTMLCanvasElement = document.createElement('canvas');
|
||||
private tempCtx: CanvasRenderingContext2D;
|
||||
|
||||
private downloadMode: boolean = false;
|
||||
private innerRatio: number = 1;
|
||||
|
||||
constructor(canvas: HTMLCanvasElement) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
this.tempCtx = this.tempCanvas.getContext('2d')!;
|
||||
this.innerRatio = mainSetting.getValue('ui.mapScale', 100) / 100;
|
||||
this.scale *= this.innerRatio;
|
||||
}
|
||||
|
||||
link(canvas: HTMLCanvasElement) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
this.drawedThumbnail = {};
|
||||
this.thumbnailLoc = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制小地图
|
||||
* @param noCache 是否不使用缓存
|
||||
*/
|
||||
drawMap(noCache: boolean = false) {
|
||||
const border = this.noBorder ? 0.5 : 1;
|
||||
const data = getMapDrawData(
|
||||
this.nowFloor,
|
||||
this.noBorder ? 0 : 5,
|
||||
border,
|
||||
noCache
|
||||
);
|
||||
const temp = this.tempCanvas;
|
||||
const ctx = this.tempCtx;
|
||||
const s = this.scale * devicePixelRatio;
|
||||
temp.width = data.width * s;
|
||||
temp.height = data.height * s;
|
||||
ctx.lineWidth = (border * devicePixelRatio) / 2;
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.scale(s, s);
|
||||
ctx.translate(5, 5);
|
||||
|
||||
if (!this.noBorder) {
|
||||
// 绘制连线
|
||||
data.line.forEach(([x1, y1, x2, y2]) => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
// 绘制地图及缩略图
|
||||
for (const [id, [x, y]] of Object.entries(data.locs) as [
|
||||
FloorIds,
|
||||
LocArr
|
||||
][]) {
|
||||
if (!this.noBorder) this.drawBorder(id, x, y);
|
||||
this.drawThumbnail(id, x, y);
|
||||
}
|
||||
this.drawToTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制一个楼层的边框
|
||||
*/
|
||||
drawBorder(id: FloorIds, x: number, y: number) {
|
||||
const border = this.noBorder ? 0.5 : 1;
|
||||
const ctx = this.tempCtx;
|
||||
ctx.lineWidth = border * devicePixelRatio;
|
||||
const map = core.status.maps[id];
|
||||
|
||||
if (!core.hasVisitedFloor(id)) {
|
||||
ctx.fillStyle = '#d0d';
|
||||
} else {
|
||||
ctx.fillStyle = '#000';
|
||||
}
|
||||
if (id === this.nowFloor) {
|
||||
ctx.strokeStyle = 'gold';
|
||||
} else {
|
||||
ctx.strokeStyle = '#fff';
|
||||
}
|
||||
|
||||
ctx.strokeRect(
|
||||
x - map.width / 2,
|
||||
y - map.height / 2,
|
||||
map.width,
|
||||
map.height
|
||||
);
|
||||
|
||||
ctx.fillRect(
|
||||
x - map.width / 2,
|
||||
y - map.height / 2,
|
||||
map.width,
|
||||
map.height
|
||||
);
|
||||
if (id === this.nowFloor) {
|
||||
ctx.fillStyle = '#ff04';
|
||||
ctx.fillRect(
|
||||
x - map.width / 2,
|
||||
y - map.height / 2,
|
||||
map.width,
|
||||
map.height
|
||||
);
|
||||
}
|
||||
if (!this.noBorder && this.scale * this.innerRatio <= 7)
|
||||
this.drawEnemy(ctx, id, x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将临时画布的内容绘制到目标画布上
|
||||
*/
|
||||
drawToTarget() {
|
||||
const mapCtx = this.ctx;
|
||||
const map = this.canvas;
|
||||
const temp = this.tempCanvas;
|
||||
|
||||
mapCtx.clearRect(0, 0, map.width, map.height);
|
||||
mapCtx.drawImage(
|
||||
temp,
|
||||
0,
|
||||
0,
|
||||
temp.width,
|
||||
temp.height,
|
||||
this.ox * devicePixelRatio + (map.width - temp.width) / 2,
|
||||
this.oy * devicePixelRatio + (map.height - temp.height) / 2,
|
||||
temp.width,
|
||||
temp.height
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该绘制缩略图
|
||||
*/
|
||||
checkThumbnail(floorId: FloorIds, x: number, y: number) {
|
||||
const scale = this.scale;
|
||||
const ox = this.ox;
|
||||
const oy = this.oy;
|
||||
const map = this.canvas;
|
||||
const temp = this.tempCanvas;
|
||||
|
||||
const floor = core.status.maps[floorId];
|
||||
const s = scale * devicePixelRatio;
|
||||
const px = ox * devicePixelRatio + (map.width - temp.width) / 2 + 5 * s;
|
||||
const py =
|
||||
oy * devicePixelRatio + (map.height - temp.height) / 2 + 5 * s;
|
||||
const left = px + (x - floor.width / 2) * s;
|
||||
const top = py + (y - floor.height / 2) * s;
|
||||
const right = left + floor.width * s;
|
||||
const bottom = top + floor.height * s;
|
||||
|
||||
this.thumbnailLoc[floorId] = [left, top, right, bottom];
|
||||
|
||||
if (
|
||||
this.drawedThumbnail[floorId] ||
|
||||
(!this.noBorder && scale * this.innerRatio <= 7) ||
|
||||
right < 0 ||
|
||||
bottom < 0 ||
|
||||
left > map.width ||
|
||||
top > map.height
|
||||
)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制缩略图
|
||||
*/
|
||||
drawThumbnail(
|
||||
floorId: FloorIds,
|
||||
x: number,
|
||||
y: number,
|
||||
noCheck: boolean = false
|
||||
) {
|
||||
if (
|
||||
!this.downloadMode &&
|
||||
!noCheck &&
|
||||
!this.checkThumbnail(floorId, x, y)
|
||||
)
|
||||
return;
|
||||
const floor = core.status.maps[floorId];
|
||||
this.drawedThumbnail[floorId] = true;
|
||||
|
||||
// 绘制缩略图
|
||||
const ctx = this.tempCtx;
|
||||
core.drawThumbnail(floorId, void 0, {
|
||||
all: true,
|
||||
inFlyMap: true,
|
||||
x: x - floor.width / 2,
|
||||
y: y - floor.height / 2,
|
||||
w: floor.width,
|
||||
h: floor.height,
|
||||
ctx,
|
||||
damage: this.scale * this.innerRatio > 15
|
||||
});
|
||||
if (!this.downloadMode) {
|
||||
if (!core.hasVisitedFloor(floorId)) {
|
||||
ctx.fillStyle = '#d0d6';
|
||||
ctx.fillRect(
|
||||
x - floor.width / 2,
|
||||
y - floor.height / 2,
|
||||
floor.width,
|
||||
floor.height
|
||||
);
|
||||
ctx.fillStyle = '#000';
|
||||
}
|
||||
if (this.nowFloor === floorId) {
|
||||
ctx.fillStyle = '#ff04';
|
||||
ctx.fillRect(
|
||||
x - floor.width / 2,
|
||||
y - floor.height / 2,
|
||||
floor.width,
|
||||
floor.height
|
||||
);
|
||||
ctx.fillStyle = '#000';
|
||||
}
|
||||
}
|
||||
this.drawEnemy(ctx, floorId, x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当移动时检查是否应该绘制缩略图
|
||||
*/
|
||||
checkMoveThumbnail() {
|
||||
const border = this.noBorder ? 0.5 : 1;
|
||||
const data = getMapDrawData(
|
||||
this.nowFloor,
|
||||
this.noBorder ? 0 : 5,
|
||||
border
|
||||
);
|
||||
for (const [id, [x, y]] of Object.entries(data.locs) as [
|
||||
FloorIds,
|
||||
LocArr
|
||||
][]) {
|
||||
if (this.checkThumbnail(id, x, y)) {
|
||||
this.drawThumbnail(id, x, y, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
download() {
|
||||
if (this.nowArea === '') {
|
||||
tip('error', '当前地图不在任意一个区域内!');
|
||||
return;
|
||||
}
|
||||
this.downloadMode = true;
|
||||
const before = this.scale;
|
||||
this.scale = 32;
|
||||
this.drawMap();
|
||||
downloadCanvasImage(this.tempCanvas, this.nowArea);
|
||||
this.scale = before;
|
||||
this.downloadMode = false;
|
||||
tip('success', '图片下载成功!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 居中地图
|
||||
* @param id 楼层id
|
||||
*/
|
||||
locateMap(id: FloorIds) {
|
||||
const data = getMapDrawData(
|
||||
id,
|
||||
this.noBorder ? 0 : 5, // 可恶的0和5,写反了找一个多小时
|
||||
this.noBorder ? 0.5 : 1
|
||||
);
|
||||
if (!data.locs[id]) return;
|
||||
|
||||
const [x, y] = data.locs[id]!;
|
||||
this.ox = (-x + data.width / 2 - 5) * this.scale;
|
||||
this.oy = (-y + data.height / 2 - 5) * this.scale;
|
||||
}
|
||||
|
||||
private drawEnemy(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
floorId: FloorIds,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
if (
|
||||
this.scale * this.innerRatio > 2 &&
|
||||
this.scale * this.innerRatio < 40 &&
|
||||
!this.downloadMode &&
|
||||
this.showInfo
|
||||
) {
|
||||
ctx.save();
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = `3px "normal"`;
|
||||
ctx.strokeStyle = 'black';
|
||||
Mota.require('fn', 'ensureFloorDamage')(floorId);
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
|
||||
ctx.fillRect(x - 6, y - 2, 12, 4);
|
||||
ctx.fillStyle = 'white';
|
||||
const enemy = core.status.maps[floorId].enemy.list;
|
||||
if (enemy.size === 0) {
|
||||
ctx.strokeStyle = 'lightgreen';
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x - 1.5, y);
|
||||
ctx.lineTo(x - 0.5, y + 1);
|
||||
ctx.lineTo(x + 1.5, y - 1);
|
||||
ctx.stroke();
|
||||
} else if (enemy.size <= 2) {
|
||||
const idSet = new Set<EnemyIds>();
|
||||
enemy.forEach(v => {
|
||||
idSet.add(v.id);
|
||||
});
|
||||
const ids: EnemyIds[] = [...idSet];
|
||||
if (ids.length === 1) {
|
||||
core.drawIcon(ctx, ids[0], x - 2, y - 2, 4, 4);
|
||||
} else if (ids.length === 2) {
|
||||
core.drawIcon(ctx, ids[0], x - 4, y - 2, 4, 4);
|
||||
core.drawIcon(ctx, ids[1], x, y - 2, 4, 4);
|
||||
} else {
|
||||
core.drawIcon(ctx, ids[0], x - 5, y - 2, 4, 4);
|
||||
core.drawIcon(ctx, ids[1], x - 1, y - 2, 4, 4);
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.strokeStyle = 'black';
|
||||
ctx.strokeText('…', x + 4, y);
|
||||
ctx.fillText('…', x + 4, y);
|
||||
}
|
||||
} else {
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(`+${enemy.size}`, x, y);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const status = ref(false);
|
||||
export const fontSize = ref(100);
|
||||
@@ -1,48 +0,0 @@
|
||||
import { has } from '../utils';
|
||||
|
||||
interface ClsMap {
|
||||
tools: '消耗道具';
|
||||
constants: '永久道具';
|
||||
all: '全部道具';
|
||||
}
|
||||
|
||||
interface AllItem {
|
||||
tools: [ItemIdOf<'tools'>, number][];
|
||||
constants: [ItemIdOf<'constants'>, number][];
|
||||
}
|
||||
|
||||
const clsMap: ClsMap = {
|
||||
tools: '消耗道具',
|
||||
constants: '永久道具',
|
||||
all: '全部道具'
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据道具的cls获取中文类型
|
||||
* @param cls 道具的cls
|
||||
*/
|
||||
export function getClsName<T extends keyof ClsMap | 'all'>(cls: T): ClsMap[T] {
|
||||
return clsMap[cls];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某个类型的所有道具
|
||||
* @param cls 道具类型
|
||||
*/
|
||||
export function getItems(cls: 'all'): AllItem;
|
||||
export function getItems(cls: keyof ClsMap): [string, number][];
|
||||
export function getItems(cls: keyof ClsMap | 'all') {
|
||||
const i = core.status.hero.items;
|
||||
const ignore = ['book'];
|
||||
if (cls === 'all') {
|
||||
return {
|
||||
tools: Object.entries(i.tools).filter(v => !ignore.includes(v[0])),
|
||||
constants: Object.entries(i.constants).filter(
|
||||
v => !ignore.includes(v[0])
|
||||
)
|
||||
};
|
||||
}
|
||||
const items = i[cls];
|
||||
const data = Object.entries(items);
|
||||
return data;
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { tip } from './utils';
|
||||
|
||||
export default function init() {
|
||||
return { useDrag, useWheel, useUp, isMobile };
|
||||
}
|
||||
|
||||
type DragFn = (x: number, y: number, e: MouseEvent | TouchEvent) => void;
|
||||
|
||||
type DragMap = [
|
||||
(e: MouseEvent) => void,
|
||||
(e: TouchEvent) => void,
|
||||
((e: MouseEvent) => void)?,
|
||||
((e: TouchEvent) => void)?
|
||||
];
|
||||
|
||||
const dragFnMap = new Map<DragFn, DragMap>();
|
||||
|
||||
/**
|
||||
* 是否是移动设备
|
||||
*/
|
||||
export let isMobile = matchMedia('(max-width: 600px)').matches;
|
||||
|
||||
let alerted = false;
|
||||
window.addEventListener('resize', () => {
|
||||
requestAnimationFrame(() => {
|
||||
isMobile = matchMedia('(max-width: 600px)').matches;
|
||||
checkMobile();
|
||||
});
|
||||
});
|
||||
checkMobile();
|
||||
|
||||
sleep(2000).then(() => {
|
||||
if (!isMobile) {
|
||||
tip('info', `注意,不推荐使用浏览器的缩放功能,使用游戏内的缩放即可`);
|
||||
}
|
||||
});
|
||||
|
||||
function checkMobile() {
|
||||
if (isMobile && !alerted) {
|
||||
tip(
|
||||
'info',
|
||||
'手机端建议使用新版APP或者自带的浏览器进行游玩,并在进入游戏后开启游戏内的全屏设置游玩'
|
||||
);
|
||||
alerted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向一个元素添加拖拽事件
|
||||
* @param ele 目标元素,当为全局拖拽时,传入数组表示所有元素共用一个全局拖拽函数
|
||||
* @param fn 拖拽时触发的函数,传入x y和鼠标事件或点击事件
|
||||
* @param ondown 鼠标按下时执行的函数
|
||||
* @param global 是否全局拖拽,即拖拽后鼠标或手指离开元素后是否依然视为正在拖拽
|
||||
*/
|
||||
export function useDrag(
|
||||
ele: HTMLElement | HTMLElement[],
|
||||
fn: DragFn,
|
||||
ondown?: DragFn,
|
||||
onup?: (e: MouseEvent | TouchEvent) => void,
|
||||
global: boolean = false
|
||||
) {
|
||||
const touchFn = (e: TouchEvent) => {
|
||||
fn(e.touches[0].clientX, e.touches[0].clientY, e);
|
||||
};
|
||||
|
||||
const mouseFn = (e: MouseEvent) => {
|
||||
fn(e.clientX, e.clientY, e);
|
||||
};
|
||||
|
||||
const mouseUp = (e: MouseEvent) => {
|
||||
const ele = global ? document : e.target;
|
||||
if (ele) {
|
||||
(ele as HTMLElement).removeEventListener('mousemove', mouseFn);
|
||||
}
|
||||
onup && onup(e);
|
||||
};
|
||||
|
||||
const touchUp = (e: TouchEvent) => {
|
||||
const ele = global ? document : e.target;
|
||||
if (ele) {
|
||||
(ele as HTMLElement).removeEventListener('touchmove', touchFn);
|
||||
}
|
||||
onup && onup(e);
|
||||
};
|
||||
|
||||
const md = (e: MouseEvent) => {
|
||||
const ele = global ? document : e.target;
|
||||
if (ele) {
|
||||
(ele as HTMLElement).addEventListener('mousemove', mouseFn);
|
||||
}
|
||||
if (ondown) ondown(e.clientX, e.clientY, e);
|
||||
};
|
||||
const td = (e: TouchEvent) => {
|
||||
const ele = global ? document : e.target;
|
||||
if (ele) {
|
||||
(ele as HTMLElement).addEventListener('touchmove', touchFn);
|
||||
}
|
||||
if (ondown) ondown(e.touches[0].clientX, e.touches[0].clientY, e);
|
||||
};
|
||||
|
||||
if (ele instanceof Array) {
|
||||
ele.forEach(v => {
|
||||
v.addEventListener('mousedown', md);
|
||||
v.addEventListener('touchstart', td);
|
||||
});
|
||||
} else {
|
||||
ele.addEventListener('mousedown', md);
|
||||
ele.addEventListener('touchstart', td);
|
||||
}
|
||||
|
||||
const target = global ? document : ele;
|
||||
|
||||
if (target instanceof Array) {
|
||||
target.forEach(v => {
|
||||
v.addEventListener('mouseup', mouseUp);
|
||||
v.addEventListener('touchend', touchUp);
|
||||
});
|
||||
} else {
|
||||
target.addEventListener('mouseup', mouseUp as EventListener);
|
||||
target.addEventListener('touchend', touchUp as EventListener);
|
||||
}
|
||||
dragFnMap.set(fn, [mouseUp, touchUp]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 去除一个全局拖拽函数
|
||||
* @param fn 要去除的函数
|
||||
*/
|
||||
export function cancelGlobalDrag(fn: DragFn): void {
|
||||
const fns = dragFnMap.get(fn);
|
||||
dragFnMap.delete(fn);
|
||||
if (!fns) return;
|
||||
document.removeEventListener('mouseup', fns[0]);
|
||||
document.removeEventListener('touchend', fns[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当触发滚轮时执行函数
|
||||
* @param ele 目标元素
|
||||
* @param fn 当滚轮触发时执行的函数
|
||||
*/
|
||||
export function useWheel(
|
||||
ele: HTMLElement,
|
||||
fn: (x: number, y: number, z: number, e: WheelEvent) => void
|
||||
): void {
|
||||
ele.addEventListener('wheel', e => {
|
||||
fn(e.deltaX, e.deltaY, e.deltaZ, e);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 当鼠标或手指松开时执行函数
|
||||
* @param ele 目标元素
|
||||
* @param fn 当鼠标或手指松开时执行的函数
|
||||
*/
|
||||
export function useUp(ele: HTMLElement, fn: DragFn): void {
|
||||
ele.addEventListener('mouseup', e => {
|
||||
fn(e.clientX, e.clientY, e);
|
||||
});
|
||||
ele.addEventListener('touchend', e => {
|
||||
fn(e.touches[0].clientX, e.touches[0].clientY, e);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 当鼠标或手指按下时执行函数
|
||||
* @param ele 目标元素
|
||||
* @param fn 当鼠标或手指按下时执行的函数
|
||||
*/
|
||||
export function useDown(ele: HTMLElement, fn: DragFn): void {
|
||||
ele.addEventListener('mousedown', e => {
|
||||
fn(e.clientX, e.clientY, e);
|
||||
});
|
||||
ele.addEventListener('touchstart', e => {
|
||||
fn(e.touches[0].clientX, e.touches[0].clientY, e);
|
||||
});
|
||||
}
|
||||
@@ -1,528 +0,0 @@
|
||||
import { message } from 'ant-design-vue';
|
||||
import { MessageApi } from 'ant-design-vue/lib/message';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { Animation, sleep, TimingFn } from 'mutate-animate';
|
||||
import { Ref, ref } from 'vue';
|
||||
import { EVENT_KEY_CODE_MAP, KeyCode } from './keyCodes';
|
||||
import axios from 'axios';
|
||||
import { decompressFromBase64 } from 'lz-string';
|
||||
import { Keyboard, KeyboardEmits } from '@/core/main/custom/keyboard';
|
||||
import { fixedUi, mainUi } from '@/core/main/init/ui';
|
||||
import { isAssist } from '@/core/main/custom/hotkey';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
type CanParseCss = keyof {
|
||||
[P in keyof CSSStyleDeclaration as CSSStyleDeclaration[P] extends string
|
||||
? P extends string
|
||||
? P
|
||||
: never
|
||||
: never]: CSSStyleDeclaration[P];
|
||||
};
|
||||
|
||||
export default function init() {
|
||||
return {
|
||||
has,
|
||||
getDamageColor,
|
||||
parseCss,
|
||||
tip,
|
||||
changeLocalStorage,
|
||||
swapChapter
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判定一个值是否不是undefined或null
|
||||
* @param value 要判断的值
|
||||
*/
|
||||
export function has<T>(value: T): value is NonNullable<T> {
|
||||
return !isNil(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据伤害大小获取颜色
|
||||
* @param damage 伤害大小
|
||||
*/
|
||||
export function getDamageColor(damage: number): string {
|
||||
if (typeof damage !== 'number') return '#f00';
|
||||
if (damage === 0) return '#2f2';
|
||||
if (damage < 0) return '#7f7';
|
||||
if (damage < core.status.hero.hp / 3) return '#fff';
|
||||
if (damage < (core.status.hero.hp * 2) / 3) return '#ff4';
|
||||
if (damage < core.status.hero.hp) return '#f93';
|
||||
return '#f22';
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置画布的长宽
|
||||
* @param canvas 画布
|
||||
* @param w 宽度
|
||||
* @param h 高度
|
||||
*/
|
||||
export function setCanvasSize(
|
||||
canvas: HTMLCanvasElement,
|
||||
w: number,
|
||||
h: number
|
||||
): void {
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
canvas.style.width = `${w}px`;
|
||||
canvas.style.height = `${h}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件中的keycode对应的键
|
||||
* @param key 要获取的键
|
||||
*/
|
||||
export function keycode(key: number) {
|
||||
return EVENT_KEY_CODE_MAP[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析css字符串为CSSStyleDeclaration对象
|
||||
* @param css 要解析的css字符串
|
||||
*/
|
||||
export function parseCss(css: string): Partial<Record<CanParseCss, string>> {
|
||||
if (css.length === 0) return {};
|
||||
|
||||
let pointer = -1;
|
||||
let inProp = true;
|
||||
let prop = '';
|
||||
let value = '';
|
||||
let upper = false;
|
||||
const res: Partial<Record<CanParseCss, string>> = {};
|
||||
|
||||
while (++pointer < css.length) {
|
||||
const char = css[pointer];
|
||||
|
||||
if ((char === ' ' || char === '\n' || char === '\r') && inProp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '-' && inProp) {
|
||||
if (prop.length !== 0) {
|
||||
upper = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === ':') {
|
||||
if (!inProp) {
|
||||
logger.error(3, pointer.toString(), css);
|
||||
return res;
|
||||
}
|
||||
inProp = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === ';') {
|
||||
if (prop.length === 0) continue;
|
||||
if (inProp) {
|
||||
logger.error(4, pointer.toString(), css);
|
||||
return res;
|
||||
}
|
||||
res[prop as CanParseCss] = value.trim();
|
||||
inProp = true;
|
||||
prop = '';
|
||||
value = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (upper) {
|
||||
if (!inProp) {
|
||||
logger.error(5, pointer.toString(), css);
|
||||
}
|
||||
prop += char.toUpperCase();
|
||||
upper = false;
|
||||
} else {
|
||||
if (inProp) prop += char;
|
||||
else value += char;
|
||||
}
|
||||
}
|
||||
if (inProp && prop.length > 0) {
|
||||
logger.error(6, pointer.toString(), css);
|
||||
return res;
|
||||
}
|
||||
if (!inProp && value.trim().length === 0) {
|
||||
logger.error(7, pointer.toString(), css);
|
||||
return res;
|
||||
}
|
||||
if (prop.length > 0) res[prop as CanParseCss] = value.trim();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function stringifyCSS(css: Partial<Record<CanParseCss, string>>) {
|
||||
let str = '';
|
||||
|
||||
for (const [key, value] of Object.entries(css)) {
|
||||
let pointer = -1;
|
||||
let prop = '';
|
||||
while (++pointer < key.length) {
|
||||
const char = key[pointer];
|
||||
if (char.toLowerCase() === char) {
|
||||
prop += char;
|
||||
} else {
|
||||
prop += `-${char.toLowerCase()}`;
|
||||
}
|
||||
}
|
||||
str += `${prop}:${value};`;
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用打字机效果显示一段文字
|
||||
* @param str 要打出的字符串
|
||||
* @param time 打出总用时,默认1秒,当第四个参数是true时,该项为每个字的平均时间
|
||||
* @param timing 打出时的速率曲线,默认为线性变化
|
||||
* @param avr 是否将第二个参数视为每个字的平均时间
|
||||
* @returns 文字的响应式变量
|
||||
*/
|
||||
export function type(
|
||||
str: string,
|
||||
time: number = 1000,
|
||||
timing: TimingFn = n => n,
|
||||
avr: boolean = false
|
||||
): Ref<string> {
|
||||
const toShow = eval('`' + str + '`') as string;
|
||||
if (typeof toShow !== 'string') {
|
||||
throw new TypeError('Error str type in typing!');
|
||||
}
|
||||
if (toShow.startsWith('!!html')) return ref(toShow);
|
||||
if (avr) time *= toShow.length;
|
||||
const ani = new Animation();
|
||||
const content = ref('');
|
||||
const all = toShow.length;
|
||||
|
||||
const fn = (time: number) => {
|
||||
if (!has(time)) return;
|
||||
const now = ani.x;
|
||||
content.value = toShow.slice(0, Math.floor(now));
|
||||
if (Math.floor(now) === all) {
|
||||
ani.ticker.destroy();
|
||||
content.value = toShow;
|
||||
}
|
||||
};
|
||||
|
||||
ani.ticker.add(fn);
|
||||
|
||||
ani.mode(timing).time(time).move(all, 0);
|
||||
|
||||
setTimeout(() => ani.ticker.destroy(), time + 100);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
message.config({
|
||||
maxCount: 3
|
||||
});
|
||||
export function tip(
|
||||
type: Exclude<keyof MessageApi, 'open' | 'config' | 'destroy'>,
|
||||
text: string
|
||||
) {
|
||||
message[type]({
|
||||
content: text,
|
||||
class: 'antdv-message'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置文字分段换行等
|
||||
* @param str 文字
|
||||
*/
|
||||
export function splitText(str: string[]) {
|
||||
return str
|
||||
.map((v, i, a) => {
|
||||
if (/^\d+\./.test(v)) return `${' '.repeat(12)}${v}`;
|
||||
else if (
|
||||
(has(a[i - 1]) && v !== '<br>' && a[i - 1] === '<br>') ||
|
||||
i === 0
|
||||
) {
|
||||
return `${' '.repeat(8)}${v}`;
|
||||
} else return v;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* 在下一帧执行某个函数
|
||||
* @param cb 执行的函数
|
||||
*/
|
||||
export function nextFrame(cb: (time: number) => void) {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(cb);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载一个画布对应的图片
|
||||
* @param canvas 画布
|
||||
* @param name 图片名称
|
||||
*/
|
||||
export function downloadCanvasImage(
|
||||
canvas: HTMLCanvasElement,
|
||||
name: string
|
||||
): void {
|
||||
const data = canvas.toDataURL('image/png');
|
||||
download(data, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载一个文件
|
||||
* @param content 下载的内容
|
||||
* @param name 文件名称
|
||||
*/
|
||||
export function download(content: string, name: string) {
|
||||
const a = document.createElement('a');
|
||||
a.download = `${name}.png`;
|
||||
a.href = content;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
/**
|
||||
* 间隔一段时间调用一个函数
|
||||
* @param funcs 函数列表
|
||||
* @param interval 调用间隔
|
||||
*/
|
||||
export async function doByInterval(
|
||||
funcs: (() => void)[],
|
||||
interval: number,
|
||||
awaitFirst: boolean = false
|
||||
) {
|
||||
if (awaitFirst) {
|
||||
await sleep(interval);
|
||||
}
|
||||
for await (const fn of funcs) {
|
||||
fn();
|
||||
await sleep(interval);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更改一个本地存储
|
||||
* @deprecated
|
||||
* @param name 要更改的信息
|
||||
* @param fn 更改时执行的函数
|
||||
* @param defaultValue 如果不存在时获取的默认值
|
||||
*/
|
||||
export function changeLocalStorage<T>(
|
||||
name: string,
|
||||
fn: (data: T) => T,
|
||||
defaultValue?: T
|
||||
) {
|
||||
const now = core.getLocalStorage(name, defaultValue);
|
||||
const to = fn(now);
|
||||
core.setLocalStorage(name, to);
|
||||
}
|
||||
|
||||
export async function swapChapter(chapter: number, hard: number) {
|
||||
const h = hard === 2 ? 'hard' : 'easy';
|
||||
const save = await axios.get(
|
||||
`${import.meta.env.BASE_URL}swap/${chapter}.${h}.h5save`,
|
||||
{
|
||||
responseType: 'text',
|
||||
responseEncoding: 'utf-8'
|
||||
}
|
||||
);
|
||||
const data = JSON.parse(decompressFromBase64(save.data));
|
||||
|
||||
core.loadData(data.data, () => {
|
||||
core.removeFlag('__fromLoad__');
|
||||
core.drawTip('读档成功');
|
||||
});
|
||||
}
|
||||
|
||||
export function ensureArray<T>(arr: T): T extends any[] ? T : T[] {
|
||||
// @ts-ignore
|
||||
return arr instanceof Array ? arr : [arr];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数组内的某个项,返回删除后的数组
|
||||
* @param arr 要操作的数组
|
||||
* @param ele 要删除的项
|
||||
*/
|
||||
export function deleteWith<T>(arr: T[], ele: T): T[] {
|
||||
const index = arr.indexOf(ele);
|
||||
if (index === -1) return arr;
|
||||
arr.splice(index, 1);
|
||||
return arr;
|
||||
}
|
||||
|
||||
export function spliceBy<T>(arr: T[], from: T): T[] {
|
||||
const index = arr.indexOf(from);
|
||||
if (index === -1) return arr;
|
||||
arr.splice(index);
|
||||
return arr;
|
||||
}
|
||||
|
||||
export async function triggerFullscreen(full: boolean) {
|
||||
if (!Mota.Plugin.inited) return;
|
||||
const { maxGameScale } = Mota.Plugin.require('utils_g');
|
||||
if (!!document.fullscreenElement && !full) {
|
||||
if (window.jsinterface) {
|
||||
window.jsinterface.requestPortrait();
|
||||
return;
|
||||
}
|
||||
await document.exitFullscreen();
|
||||
requestAnimationFrame(() => {
|
||||
maxGameScale(1);
|
||||
});
|
||||
}
|
||||
if (full && !document.fullscreenElement) {
|
||||
if (window.jsinterface) {
|
||||
window.jsinterface.requestLandscape();
|
||||
return;
|
||||
}
|
||||
await document.body.requestFullscreen();
|
||||
requestAnimationFrame(() => {
|
||||
maxGameScale();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据布尔值数组转换成一个二进制数
|
||||
* @param arr 要转换的布尔值数组
|
||||
*/
|
||||
export function generateBinary(arr: boolean[]) {
|
||||
let num = 0;
|
||||
arr.forEach((v, i) => {
|
||||
if (v) {
|
||||
num |= 1 << i;
|
||||
}
|
||||
});
|
||||
return num;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得某个状态的中文名
|
||||
* @param name 要获取的属性名
|
||||
*/
|
||||
export function getStatusLabel(name: string) {
|
||||
return (
|
||||
{
|
||||
name: '名称',
|
||||
lv: '等级',
|
||||
hpmax: '生命回复',
|
||||
hp: '生命',
|
||||
manamax: '魔力上限',
|
||||
mana: '额外攻击',
|
||||
atk: '攻击',
|
||||
def: '防御',
|
||||
mdef: '智慧',
|
||||
money: '金币',
|
||||
exp: '经验',
|
||||
point: '加点',
|
||||
steps: '步数',
|
||||
up: '升级',
|
||||
magicDef: '魔法防御',
|
||||
none: '无'
|
||||
}[name] || name
|
||||
);
|
||||
}
|
||||
|
||||
export function flipBinary(num: number, col: number) {
|
||||
const n = 1 << col;
|
||||
if (num & n) return num & ~n;
|
||||
else return num | n;
|
||||
}
|
||||
|
||||
/**
|
||||
* 唤起虚拟键盘,并获取到一次按键操作
|
||||
* @param emitAssist 是否可以获取辅助按键,为true时,如果按下辅助按键,那么会立刻返回该按键,
|
||||
* 否则会视为开关辅助按键
|
||||
* @param assist 初始化的辅助按键
|
||||
*/
|
||||
export function getVitualKeyOnce(
|
||||
emitAssist: boolean = false,
|
||||
assist: number = 0,
|
||||
emittable: KeyCode[] = []
|
||||
): Promise<KeyboardEmits> {
|
||||
// todo: 正确触发后删除监听器
|
||||
return new Promise(res => {
|
||||
const key = Keyboard.get('full')!;
|
||||
key.withAssist(assist);
|
||||
const id = mainUi.open('virtualKey', { keyboard: key });
|
||||
key.on('emit', (item, assist, index, ev) => {
|
||||
ev.preventDefault();
|
||||
if (emitAssist) {
|
||||
if (emittable.length === 0 || emittable.includes(item.key)) {
|
||||
res({ key: item.key, assist: 0 });
|
||||
key.disposeScope();
|
||||
mainUi.close(id);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
!isAssist(item.key) &&
|
||||
(emittable.length === 0 || emittable.includes(item.key))
|
||||
) {
|
||||
res({ key: item.key, assist });
|
||||
key.disposeScope();
|
||||
mainUi.close(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function formatSize(size: number) {
|
||||
return size < 1 << 10
|
||||
? `${size.toFixed(2)}B`
|
||||
: size < 1 << 20
|
||||
? `${(size / (1 << 10)).toFixed(2)}KB`
|
||||
: size < 1 << 30
|
||||
? `${(size / (1 << 20)).toFixed(2)}MB`
|
||||
: `${(size / (1 << 30)).toFixed(2)}GB`;
|
||||
}
|
||||
|
||||
let num = 0;
|
||||
export function requireUniqueSymbol() {
|
||||
return num++;
|
||||
}
|
||||
|
||||
export function openDanmakuPoster() {
|
||||
if (!fixedUi.hasName('danmakuEditor')) {
|
||||
fixedUi.open('danmakuEditor');
|
||||
}
|
||||
}
|
||||
|
||||
export function getIconHeight(icon: AllIds | 'hero') {
|
||||
if (icon === 'hero') {
|
||||
if (core.isPlaying()) {
|
||||
return (
|
||||
core.material.images.images[core.status.hero.image].height / 4
|
||||
);
|
||||
} else {
|
||||
return 48;
|
||||
}
|
||||
}
|
||||
return core.getBlockInfo(icon)?.height ?? 32;
|
||||
}
|
||||
|
||||
const ascii = 16 ** 2;
|
||||
const other = 16 ** 4;
|
||||
const emoji = 16 ** 5;
|
||||
export function calStringSize(str: string) {
|
||||
let size = 0;
|
||||
|
||||
for (const char of str) {
|
||||
const num = char.charCodeAt(0);
|
||||
if (num < ascii) size += 1;
|
||||
else if (num < other) size += 2;
|
||||
else if (num < emoji) size += 2.5;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
export function clamp(num: number, start: number, end: number) {
|
||||
const s = Math.min(start, end);
|
||||
const e = Math.max(start, end);
|
||||
if (num < s) return s;
|
||||
else if (num > e) return e;
|
||||
return num;
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
<template>
|
||||
<div id="achievement">
|
||||
<div id="tools">
|
||||
<span id="back" class="button-text tools" @click="exit"
|
||||
><left-outlined />返回游戏</span
|
||||
>
|
||||
</div>
|
||||
<div id="column">
|
||||
<div class="achievement-column" v-for="c of column">
|
||||
<span
|
||||
class="column-text button-text"
|
||||
:active="selectedColumn === c"
|
||||
@click="selectedColumn = c"
|
||||
>{{ columnName[c] }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider dashed id="divider"></a-divider>
|
||||
<div id="list">
|
||||
<div id="achievement-list" :style="{ left: `-${offset}%` }">
|
||||
<div v-for="t of column" class="achievement-one">
|
||||
<Scroll class="list-scroll" :width="isMobile ? 10 : 20">
|
||||
<div class="list-div">
|
||||
<div
|
||||
v-for="a of getAllAchievements(t)"
|
||||
class="list-one"
|
||||
>
|
||||
<div
|
||||
class="list-content"
|
||||
:complete="a.complete"
|
||||
>
|
||||
<span class="list-name">{{ a.name }}</span>
|
||||
<span
|
||||
class="list-text"
|
||||
v-html="a.text"
|
||||
></span>
|
||||
<div class="list-end">
|
||||
<div class="end-info">
|
||||
<span
|
||||
class="complete"
|
||||
:complete="a.complete"
|
||||
>完成情况:
|
||||
{{
|
||||
a.complete
|
||||
? '已完成'
|
||||
: '未完成'
|
||||
}}</span
|
||||
>
|
||||
<span class="point"
|
||||
>成就点数: {{ a.point }}</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="a.progress"
|
||||
class="list-progress"
|
||||
>
|
||||
<a-progress
|
||||
:percent="a.percent"
|
||||
:strokeColor="{
|
||||
'0%': '#108ee9',
|
||||
'100%': '#87d068'
|
||||
}"
|
||||
:strokeWidth="height / 150"
|
||||
:format="
|
||||
() =>
|
||||
a.usePercent
|
||||
? `${a.percent}%`
|
||||
: a.progress
|
||||
"
|
||||
></a-progress>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider id="divider" dashed></a-divider>
|
||||
</div>
|
||||
</div>
|
||||
</Scroll>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="total-progress">
|
||||
<a-progress
|
||||
id="point-progress"
|
||||
:percent="(nowPoint / totalPoint) * 100"
|
||||
:strokeColor="{
|
||||
'0%': '#108ee9',
|
||||
'100%': '#87d068'
|
||||
}"
|
||||
:strokeWidth="height / 150"
|
||||
:showInfo="false"
|
||||
></a-progress>
|
||||
<span id="point-number"
|
||||
>成就点: {{ nowPoint }} / {{ totalPoint }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { LeftOutlined } from '@ant-design/icons-vue';
|
||||
import list from '../data/achievement.json';
|
||||
import {
|
||||
Achievement,
|
||||
getNowPoint,
|
||||
hasCompletedAchievement
|
||||
} from '../plugin/ui/achievement';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import { isMobile } from '../plugin/use';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
}>();
|
||||
|
||||
type AchievementList = typeof list;
|
||||
type AchievementType = keyof AchievementList;
|
||||
|
||||
interface ResolvedAchievement {
|
||||
name: string;
|
||||
text: string;
|
||||
complete: boolean;
|
||||
point: number;
|
||||
/** number / number */
|
||||
progress?: string;
|
||||
percent?: number;
|
||||
usePercent?: boolean;
|
||||
}
|
||||
|
||||
const column: AchievementType[] = ['normal', 'challenge', 'explore'];
|
||||
const columnName = {
|
||||
normal: '普通成就',
|
||||
challenge: '挑战成就',
|
||||
explore: '探索成就'
|
||||
};
|
||||
|
||||
const selectedColumn = ref<AchievementType>('normal');
|
||||
|
||||
const offset = computed(() => {
|
||||
return column.indexOf(selectedColumn.value) * 100;
|
||||
});
|
||||
|
||||
const height = window.innerHeight;
|
||||
const width = window.innerWidth;
|
||||
|
||||
const totalPoint = Object.values(list)
|
||||
.map((v: Achievement[]) =>
|
||||
v.reduce((prev, curr) => {
|
||||
return curr.point + prev;
|
||||
}, 0)
|
||||
)
|
||||
.reduce((prev, curr) => prev + curr);
|
||||
const nowPoint = getNowPoint();
|
||||
|
||||
/**
|
||||
* 获取一个类型的所有成就
|
||||
* @param type 成就类型
|
||||
*/
|
||||
function getAllAchievements(type: AchievementType): ResolvedAchievement[] {
|
||||
return list[type].map<ResolvedAchievement>((v: Achievement, i) => {
|
||||
const complete = hasCompletedAchievement(type, i);
|
||||
const text = v.hide && !complete ? v.hide : v.text.join('');
|
||||
const res: ResolvedAchievement = {
|
||||
text,
|
||||
name: v.name,
|
||||
point: v.point,
|
||||
complete
|
||||
};
|
||||
if (v.progress) {
|
||||
const p = eval('`' + v.progress + '`') as string;
|
||||
res.progress = p;
|
||||
res.percent = Math.floor(eval(p) * 100);
|
||||
if (v.percent) res.usePercent = true;
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
function exit() {
|
||||
Mota.require('var', 'mainUi').close(props.num);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#achievement {
|
||||
width: 90vh;
|
||||
height: 90vh;
|
||||
font-family: 'normal';
|
||||
font-size: 150%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#divider {
|
||||
margin: 1% 0;
|
||||
border-color: #ddd4;
|
||||
}
|
||||
|
||||
#tools {
|
||||
height: 5vh;
|
||||
font-size: 3.2vh;
|
||||
}
|
||||
|
||||
#column {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
margin-top: 3%;
|
||||
font-size: 130%;
|
||||
}
|
||||
|
||||
.list-scroll {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#list {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
}
|
||||
|
||||
#achievement-list {
|
||||
position: relative;
|
||||
width: 300%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
transition: left 0.4s ease;
|
||||
}
|
||||
|
||||
.achievement-one {
|
||||
width: 90vh;
|
||||
}
|
||||
|
||||
.list-div {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-one {
|
||||
width: 70%;
|
||||
|
||||
.list-content {
|
||||
height: 18vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 2px double rgba(132, 132, 132, 0.17);
|
||||
border-radius: 10px;
|
||||
margin: 2% 0 2.5% 0;
|
||||
background-color: rgba(59, 59, 59, 0.281);
|
||||
}
|
||||
|
||||
.list-content[complete='true'] {
|
||||
background-color: rgba(239, 255, 63, 0.205);
|
||||
}
|
||||
|
||||
.list-name {
|
||||
border-bottom: 1px solid #ddd4;
|
||||
}
|
||||
|
||||
.list-text {
|
||||
font-size: 100%;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.list-end {
|
||||
width: 90%;
|
||||
height: 95%;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
font-size: 90%;
|
||||
|
||||
.end-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.complete {
|
||||
color: lightcoral;
|
||||
}
|
||||
|
||||
.complete[complete='true'] {
|
||||
color: lightgreen;
|
||||
}
|
||||
}
|
||||
|
||||
.list-progress {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.progress {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#total-progress {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
#point-progress {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#point-number {
|
||||
font-size: 70%;
|
||||
margin-left: 2%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#achievement {
|
||||
width: 90vw;
|
||||
height: 90vh;
|
||||
}
|
||||
|
||||
.list-one {
|
||||
width: 90%;
|
||||
|
||||
.list-content {
|
||||
height: 15vh;
|
||||
}
|
||||
|
||||
.list-end {
|
||||
margin-bottom: 0.8vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,90 +0,0 @@
|
||||
<template>
|
||||
<Colomn @close="exit" :width="60" :height="80" :left="30" :right="70"
|
||||
><template #left>
|
||||
<div id="bgm-list">
|
||||
<span
|
||||
v-for="(bgm, i) of list"
|
||||
class="selectable"
|
||||
:selected="selected === i"
|
||||
@click="select(i)"
|
||||
>
|
||||
{{ bgm!.area }}
|
||||
</span>
|
||||
</div></template
|
||||
>
|
||||
<template #right
|
||||
><div>
|
||||
<div id="bgm-name">
|
||||
<img id="bgm-image" :src="list[selected]!.img" />
|
||||
<span>{{ name }}</span>
|
||||
<span v-if="list[selected]!.from"
|
||||
>出自 {{
|
||||
list[selected]!.from
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<span v-html="content"></span></div></template
|
||||
></Colomn>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import Colomn from '../components/colomn.vue';
|
||||
import bgm from '../data/bgm.json';
|
||||
import { splitText } from '../plugin/utils';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
}>();
|
||||
|
||||
interface Bgm {
|
||||
img: string;
|
||||
area: string;
|
||||
name: string;
|
||||
desc: string[];
|
||||
from?: string;
|
||||
}
|
||||
|
||||
const list = bgm as Partial<Record<BgmIds, Bgm>>;
|
||||
|
||||
const selected = ref<BgmIds>('title.opus');
|
||||
|
||||
const content = computed(() => {
|
||||
return eval('`' + splitText(list[selected.value]!.desc) + '`');
|
||||
});
|
||||
const name = computed(() => list[selected.value]!.name);
|
||||
|
||||
function exit() {
|
||||
mainUi.close(props.num);
|
||||
}
|
||||
|
||||
function select(id: BgmIds) {
|
||||
selected.value = id;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#bgm-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#bgm-name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#bgm-image {
|
||||
margin-top: 5%;
|
||||
border: 1px solid #fff;
|
||||
width: 33vw;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#bgm-image {
|
||||
width: 70vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-258
@@ -1,258 +0,0 @@
|
||||
<!-- 怪物手册ui -->
|
||||
<template>
|
||||
<div id="book">
|
||||
<div id="tools">
|
||||
<span id="back" class="button-text tools" @click="exit"
|
||||
><left-outlined />返回游戏</span
|
||||
>
|
||||
</div>
|
||||
<div v-if="enemy.length === 0" id="none">
|
||||
<div>本层无怪物</div>
|
||||
</div>
|
||||
<Scroll
|
||||
v-else
|
||||
style="width: 100%; height: 94%; font-family: normal"
|
||||
v-model:now="scroll"
|
||||
v-model:drag="drag"
|
||||
>
|
||||
<div v-for="(e, i) of toShow" class="enemy">
|
||||
<EnemyOne
|
||||
:selected="i === selected"
|
||||
:enemy="e"
|
||||
:key="i"
|
||||
@select="select(e, i)"
|
||||
@hover="selected = i"
|
||||
></EnemyOne>
|
||||
<a-divider
|
||||
dashed
|
||||
style="width: 100%; border-color: #ddd4"
|
||||
></a-divider>
|
||||
</div>
|
||||
</Scroll>
|
||||
</div>
|
||||
<BookDetail
|
||||
v-if="detail"
|
||||
:from-book="true"
|
||||
@close="closeDetail()"
|
||||
></BookDetail>
|
||||
</template>
|
||||
|
||||
<script setup lang="tsx">
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import EnemyOne from '../components/enemyOne.vue';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import { getDamageColor, has, keycode } from '../plugin/utils';
|
||||
import BookDetail from './bookDetail.vue';
|
||||
import { LeftOutlined } from '@ant-design/icons-vue';
|
||||
import { ToShowEnemy, detailInfo } from '../plugin/ui/book';
|
||||
import { getDetailedEnemy } from '../plugin/ui/fixed';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { isMobile } from '@/plugin/use';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
}>();
|
||||
|
||||
const floorId =
|
||||
// @ts-ignore
|
||||
core.floorIds[core.status.event?.ui?.index] ?? core.status.floorId;
|
||||
|
||||
const enemy = core.getCurrentEnemys(floorId);
|
||||
const toShow: ToShowEnemy[] = enemy.map(v =>
|
||||
getDetailedEnemy(v.enemy, floorId)
|
||||
);
|
||||
|
||||
const scroll = ref(0);
|
||||
const drag = ref(false);
|
||||
const detail = ref(false);
|
||||
const selected = ref(0);
|
||||
|
||||
const settingScale = mainSetting.getValue('ui.bookScale', 100) / 100;
|
||||
const scale = isMobile
|
||||
? Math.max(settingScale * 15, 20)
|
||||
: Math.max(
|
||||
(window.innerWidth / window.innerHeight) * 15 * settingScale,
|
||||
20
|
||||
);
|
||||
|
||||
/**
|
||||
* 选择怪物,展示详细信息
|
||||
* @param enemy 选择的怪物
|
||||
* @param index 选择的怪物索引
|
||||
*/
|
||||
function select(enemy: ToShowEnemy, index: number) {
|
||||
if (drag.value) return;
|
||||
const h = window.innerHeight;
|
||||
const y = index * h * 0.2 - scroll.value;
|
||||
detailInfo.enemy = enemy;
|
||||
detailInfo.pos = y;
|
||||
detail.value = true;
|
||||
hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏怪物手册
|
||||
*/
|
||||
async function hide() {
|
||||
const div = document.getElementById('book') as HTMLDivElement;
|
||||
div.style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭详细信息
|
||||
*/
|
||||
async function closeDetail() {
|
||||
show();
|
||||
detail.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示怪物手册
|
||||
*/
|
||||
async function show() {
|
||||
const div = document.getElementById('book') as HTMLDivElement;
|
||||
div.style.display = 'flex';
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出怪物手册
|
||||
*/
|
||||
async function exit() {
|
||||
const hold = mainUi.holdOn();
|
||||
mainUi.close(props.num);
|
||||
if (core.events.recoverEvents(core.status.event.interval)) {
|
||||
hold.end(true);
|
||||
return;
|
||||
} else if (has(core.status.event.ui)) {
|
||||
core.status.boxAnimateObjs = [];
|
||||
// @ts-ignore
|
||||
core.ui._drawViewMaps(core.status.event.ui);
|
||||
hold.end(true);
|
||||
} else hold.end();
|
||||
}
|
||||
|
||||
function checkScroll() {
|
||||
const h = window.innerHeight;
|
||||
const y = (selected.value * h * scale) / 100 - scroll.value;
|
||||
if (y < 0) {
|
||||
scroll.value += y - 20;
|
||||
}
|
||||
if (y > h * 0.655) {
|
||||
scroll.value += y - h * 0.655 + 20;
|
||||
}
|
||||
}
|
||||
|
||||
// 按键控制
|
||||
setTimeout(() => {
|
||||
gameKey.use(props.ui.symbol);
|
||||
gameKey
|
||||
.realize(
|
||||
'@book_up',
|
||||
() => {
|
||||
if (selected.value > 0) {
|
||||
selected.value--;
|
||||
}
|
||||
checkScroll();
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@book_down',
|
||||
() => {
|
||||
if (selected.value < enemy.length - 1) {
|
||||
selected.value++;
|
||||
}
|
||||
checkScroll();
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@book_pageDown',
|
||||
() => {
|
||||
if (selected.value >= enemy.length - 5) {
|
||||
selected.value = enemy.length - 1;
|
||||
} else {
|
||||
selected.value += 5;
|
||||
}
|
||||
checkScroll();
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@book_pageUp',
|
||||
() => {
|
||||
if (selected.value <= 4) {
|
||||
selected.value = 0;
|
||||
} else {
|
||||
selected.value -= 5;
|
||||
}
|
||||
checkScroll();
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize('exit', () => {
|
||||
exit();
|
||||
})
|
||||
.realize('confirm', () => {
|
||||
select(toShow[selected.value], selected.value);
|
||||
});
|
||||
}, 0);
|
||||
|
||||
onUnmounted(() => {
|
||||
gameKey.dispose(props.ui.symbol);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#book {
|
||||
user-select: none;
|
||||
width: 80%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#tools {
|
||||
height: 6%;
|
||||
font-size: 3.2vh;
|
||||
}
|
||||
|
||||
#none {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 6vw;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.enemy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: v-bind('scale + "vh"');
|
||||
width: 100%;
|
||||
padding: 0 1% 0 1%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#tools {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
#book {
|
||||
width: 100%;
|
||||
padding: 5%;
|
||||
}
|
||||
|
||||
.enemy {
|
||||
height: v-bind('scale * 2 / 3 + "vh"');
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,240 +0,0 @@
|
||||
<!-- 怪物详细信息 -->
|
||||
<template>
|
||||
<div id="detail">
|
||||
<div id="tools">
|
||||
<span id="back" class="button-text tools" @click="close"
|
||||
><left-outlined />返回</span
|
||||
>
|
||||
</div>
|
||||
<div id="info" :style="{ top: `${top}px` }">
|
||||
<EnemyOne :enemy="enemy!"></EnemyOne>
|
||||
<a-divider
|
||||
dashed
|
||||
style="margin: 2vh 0 2vh 0; border-color: #ddd4; width: 100%"
|
||||
></a-divider>
|
||||
</div>
|
||||
<Transition name="detail">
|
||||
<EnemySpecial
|
||||
:from-book="fromBook"
|
||||
v-if="panel === 'special'"
|
||||
></EnemySpecial>
|
||||
<EnemyCritical
|
||||
:from-book="fromBook"
|
||||
v-else-if="panel === 'critical'"
|
||||
></EnemyCritical>
|
||||
<EnemyTarget v-else-if="panel === 'target'"></EnemyTarget>
|
||||
</Transition>
|
||||
<div id="detail-more">
|
||||
<Transition name="detail">
|
||||
<div
|
||||
id="special-more"
|
||||
class="detial-more"
|
||||
v-if="panel === 'special'"
|
||||
>
|
||||
<span
|
||||
id="enemy-target"
|
||||
class="button-text more"
|
||||
@click="changePanel($event, 'target')"
|
||||
><LeftOutlined /> 怪物更多信息</span
|
||||
>
|
||||
<span
|
||||
id="critical-more"
|
||||
class="button-text more"
|
||||
@click="changePanel($event, 'critical')"
|
||||
>详细临界信息 <right-outlined
|
||||
/></span>
|
||||
</div>
|
||||
<div
|
||||
id="special-more"
|
||||
class="detial-more"
|
||||
v-else-if="panel === 'critical'"
|
||||
>
|
||||
<span
|
||||
id="enemy-pos"
|
||||
class="button-text more"
|
||||
@click="changePanel($event, 'special')"
|
||||
><left-outlined /> 怪物特殊属性</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
id="special-more"
|
||||
class="detial-more"
|
||||
v-else-if="panel === 'target'"
|
||||
>
|
||||
<span></span>
|
||||
<span
|
||||
id="enemy-pos"
|
||||
class="button-text more"
|
||||
@click="changePanel($event, 'special')"
|
||||
>怪物特殊属性 <RightOutlined
|
||||
/></span>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import EnemyOne from '../components/enemyOne.vue';
|
||||
import { useDrag } from '../plugin/use';
|
||||
import EnemySpecial from '../panel/enemySpecial.vue';
|
||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
|
||||
import EnemyCritical from '../panel/enemyCritical.vue';
|
||||
import EnemyTarget from '../panel/enemyTarget.vue';
|
||||
import { detailInfo } from '../plugin/ui/book';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
|
||||
const props = defineProps<{
|
||||
fromBook?: boolean;
|
||||
defaultPanel?: 'special' | 'critical' | 'target';
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const enemy = detailInfo.enemy;
|
||||
const top = ref(detailInfo.pos);
|
||||
const panel = ref<string>(props.defaultPanel ?? 'special');
|
||||
const symbol = Symbol();
|
||||
|
||||
let detail: HTMLDivElement;
|
||||
|
||||
function changePanel(e: MouseEvent, to: string) {
|
||||
e.stopPropagation();
|
||||
panel.value = to;
|
||||
}
|
||||
|
||||
function close() {
|
||||
top.value = detailInfo.pos;
|
||||
detail.style.opacity = '0';
|
||||
emits('close');
|
||||
}
|
||||
|
||||
gameKey.use(symbol);
|
||||
gameKey
|
||||
.realize('exit', () => {
|
||||
close();
|
||||
})
|
||||
.realize('confirm', () => {
|
||||
close();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
top.value = 0;
|
||||
detail = document.getElementById('detail') as HTMLDivElement;
|
||||
detail.style.opacity = '1';
|
||||
|
||||
const style = getComputedStyle(detail);
|
||||
|
||||
let moved = false;
|
||||
let pos = [0, 0];
|
||||
|
||||
useDrag(
|
||||
detail,
|
||||
(x, y) => {
|
||||
if ((x - pos[0]) ** 2 + (y - pos[1]) ** 2 >= 100) moved = true;
|
||||
},
|
||||
(x, y) => {
|
||||
pos = [x, y];
|
||||
if (y > (parseFloat(style.height) * 4) / 5) moved = true;
|
||||
},
|
||||
() => {
|
||||
if (moved === false && panel.value === 'special') {
|
||||
close();
|
||||
}
|
||||
moved = false;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
gameKey.dispose(symbol);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#info {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
transition: all 0.6s ease;
|
||||
height: 20vh;
|
||||
padding: 0 1% 0 1%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#detail {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: 14%;
|
||||
font-family: 'normal';
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 72%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#detail-more {
|
||||
position: absolute;
|
||||
margin-top: 3%;
|
||||
width: 100%;
|
||||
font-size: 3vh;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.detial-more {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.more {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.detail-enter-active,
|
||||
.detail-leave-active {
|
||||
transition: all 0.6s ease;
|
||||
}
|
||||
|
||||
.detail-enter-from,
|
||||
.detail-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#tools {
|
||||
position: fixed;
|
||||
height: 6%;
|
||||
font-size: 3.2vh;
|
||||
width: 100%;
|
||||
left: 5%;
|
||||
top: 5%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#detail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 5%;
|
||||
left: 0%;
|
||||
}
|
||||
|
||||
#detail-more {
|
||||
font-size: 4vw;
|
||||
bottom: 5%;
|
||||
left: 5vw;
|
||||
width: 80vw;
|
||||
}
|
||||
|
||||
#info {
|
||||
transform: translateY(10%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,148 +0,0 @@
|
||||
<template>
|
||||
<div id="chapter">
|
||||
<canvas id="chapter-back"></canvas>
|
||||
<span id="chapter-text">{{ chapter }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Animation, hyper, sleep } from 'mutate-animate';
|
||||
import { onMounted } from 'vue';
|
||||
import { has } from '../plugin/utils';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { fixedUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
chapter: string;
|
||||
}>();
|
||||
|
||||
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 (!has(time) || isNaN(time)) return;
|
||||
if (!started) {
|
||||
started = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (time >= 4050) {
|
||||
fixedUi.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,117 +0,0 @@
|
||||
<template>
|
||||
<Box id="complete-box">
|
||||
<div id="complete">
|
||||
<span>完成成就 {{ achi.name }}</span>
|
||||
<a-progress
|
||||
id="progress"
|
||||
:percent="progress * 100"
|
||||
:strokeColor="{
|
||||
'0%': '#108ee9',
|
||||
'100%': '#87d068'
|
||||
}"
|
||||
:strokeWidth="height / 200"
|
||||
:showInfo="false"
|
||||
></a-progress>
|
||||
<span id="point-number">成就点: {{ now }} / {{ totalPoint }}</span>
|
||||
</div>
|
||||
</Box>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { sleep, Ticker } from 'mutate-animate';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import Box from '../components/box.vue';
|
||||
import list from '../data/achievement.json';
|
||||
import {
|
||||
AchievementType,
|
||||
getNowPoint,
|
||||
totalPoint
|
||||
} from '../plugin/ui/achievement';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { fixedUi } from '@/core/main/init/ui';
|
||||
|
||||
const height = window.innerHeight;
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
complete: string;
|
||||
}>();
|
||||
|
||||
const c = props.complete.split(',');
|
||||
const type = c[0] as AchievementType;
|
||||
const index = parseInt(c[1]);
|
||||
|
||||
const achi = list[type][index];
|
||||
const point = achi.point;
|
||||
|
||||
const nowPoint = getNowPoint() - point;
|
||||
const now = ref(nowPoint);
|
||||
const progress = computed(() => now.value / totalPoint);
|
||||
|
||||
onMounted(async () => {
|
||||
await sleep(500);
|
||||
const ticker = new Ticker();
|
||||
const time = Date.now();
|
||||
ticker.add(() => {
|
||||
const nowTime = Date.now();
|
||||
if (nowTime - time > 1000) {
|
||||
now.value = nowPoint + point;
|
||||
ticker.destroy();
|
||||
}
|
||||
const ratio = (nowTime - time) / 1000;
|
||||
now.value = Math.floor(nowPoint + point * ratio);
|
||||
});
|
||||
await sleep(4600);
|
||||
fixedUi.close(props.num);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#complete-box {
|
||||
width: 30vw;
|
||||
height: 13vh;
|
||||
left: 35vw;
|
||||
position: fixed;
|
||||
background-color: #000d;
|
||||
animation: ani 5s ease 0s 1 forwards running;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
#complete {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: 'normal';
|
||||
font-size: 2.2vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#progress {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
@keyframes ani {
|
||||
0% {
|
||||
top: -30vh;
|
||||
}
|
||||
20% {
|
||||
top: 4vh;
|
||||
}
|
||||
80% {
|
||||
top: 4vh;
|
||||
}
|
||||
100% {
|
||||
top: -30vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#complete-box {
|
||||
width: 90vw;
|
||||
left: 5%;
|
||||
}
|
||||
}
|
||||
</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 '../core/main/custom/danmaku';
|
||||
import { LikeFilled } from '@ant-design/icons-vue';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
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,674 +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">
|
||||
<a-input
|
||||
id="danmaku-input-input"
|
||||
:max-length="200"
|
||||
v-model:value="inputValue"
|
||||
placeholder="请在此输入弹幕,显示中括号请使用\[或\]"
|
||||
autocomplete="off"
|
||||
@change="input(inputValue)"
|
||||
@pressEnter="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>
|
||||
<a-input
|
||||
id="danmaku-css-input"
|
||||
:max-length="300"
|
||||
v-model:value="cssInfo"
|
||||
placeholder="请在此输入样式"
|
||||
autocomplete="off"
|
||||
@blur="inputCSS(cssInfo)"
|
||||
@pressEnter="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>
|
||||
<a-input
|
||||
id="danmaku-color-input"
|
||||
:max-length="100"
|
||||
v-model:value="nowColor"
|
||||
placeholder="输入颜色"
|
||||
autocomplete="off"
|
||||
@blur="inputColor(nowColor)"
|
||||
@pressEnter="inputColor(nowColor)"
|
||||
></a-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 '@/core/main/custom/danmaku';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { fixedUi } from '@/core/main/init/ui';
|
||||
import { calStringSize, tip } from '@/plugin/utils';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { stringifyCSS, parseCss, getIconHeight } from '@/plugin/utils';
|
||||
import { logger, LogLevel } from '@motajs/common';
|
||||
import Scroll from '@/components/scroll.vue';
|
||||
import BoxAnimate from '@/components/boxAnimate.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
}>();
|
||||
|
||||
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(() => {
|
||||
fixedUi.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,79 +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 '../plugin/utils';
|
||||
import Colomn from '../components/colomn.vue';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
}>();
|
||||
|
||||
type DescKey = keyof typeof desc;
|
||||
|
||||
const selected = ref(Object.keys(desc)[0] as DescKey);
|
||||
|
||||
function exit() {
|
||||
mainUi.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,730 +0,0 @@
|
||||
<template>
|
||||
<div id="equipbox">
|
||||
<div id="tools">
|
||||
<span class="button-text tools" @click="exit"
|
||||
><left-outlined /> 返回游戏</span
|
||||
>
|
||||
<span class="button-text tools" @click="toTool"
|
||||
>道具栏 <right-outlined
|
||||
/></span>
|
||||
</div>
|
||||
<div id="equipbox-main">
|
||||
<div id="equip-list">
|
||||
<div id="filter">
|
||||
<a-select v-model:value="norm" class="select">
|
||||
<a-select-option v-for="t of normList" :value="t">{{
|
||||
t === 'none' ? '所有' : getStatusLabel(t)
|
||||
}}</a-select-option>
|
||||
</a-select>
|
||||
<a-divider type="vertical" class="divider"></a-divider>
|
||||
<a-select v-model:value="sType" class="select">
|
||||
<a-select-option value="value">数值</a-select-option>
|
||||
<a-select-option value="percentage"
|
||||
>百分比</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
<a-divider type="vertical" class="divider"></a-divider>
|
||||
<span
|
||||
@click="changeSort()"
|
||||
class="button-text"
|
||||
id="sort-type"
|
||||
>
|
||||
<span v-if="sort === 'down'">
|
||||
<sort-ascending-outlined />
|
||||
</span>
|
||||
<span v-else><sort-descending-outlined /></span>
|
||||
</span>
|
||||
</div>
|
||||
<a-divider
|
||||
dashed
|
||||
style="border-color: #ddd4; margin: 1vh 0 1vh 0"
|
||||
></a-divider>
|
||||
<Scroll id="equip-scroll"
|
||||
><div
|
||||
class="equip selectable"
|
||||
v-for="([id, num], i) of toShow"
|
||||
:selected="selected === i && !isCol"
|
||||
@mousedown="select(i)"
|
||||
@touchstart="select(i)"
|
||||
@click="clickList(i)"
|
||||
>
|
||||
<div class="equip-icon">
|
||||
<BoxAnimate
|
||||
:id="id ?? 'none'"
|
||||
:width="32"
|
||||
:height="32"
|
||||
:noborder="true"
|
||||
></BoxAnimate>
|
||||
<span class="equip-name">{{ all[id].name }}</span>
|
||||
</div>
|
||||
<span>× {{ num }}</span>
|
||||
</div></Scroll
|
||||
>
|
||||
</div>
|
||||
<div id="equip-status">
|
||||
<a-divider
|
||||
class="divider"
|
||||
:type="isMobile ? 'horizontal' : 'vertical'"
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
></a-divider>
|
||||
<div id="equip-status-main">
|
||||
<div id="equip-now">
|
||||
<Scroll
|
||||
:style="{
|
||||
height: isMobile ? '10vh' : '30vh'
|
||||
}"
|
||||
>
|
||||
<div id="equip-now-div">
|
||||
<div
|
||||
v-for="(name, i) of equipCol"
|
||||
class="equip-now-one draginable selectable"
|
||||
:draged="draged"
|
||||
:access="canDragin(i)"
|
||||
:selected="isCol && selected === i"
|
||||
@mouseenter="dragin($event, i)"
|
||||
@mouseleave="dragout"
|
||||
@click="select(i, true)"
|
||||
>
|
||||
<BoxAnimate
|
||||
:id="equiped[i] ?? 'none'"
|
||||
></BoxAnimate>
|
||||
<span>{{ name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Scroll>
|
||||
</div>
|
||||
<a-divider
|
||||
dashed
|
||||
style="border-color: #ddd4; margin: 1vh 0 1vh 0"
|
||||
></a-divider>
|
||||
<div id="equip-hero" v-if="!isMobile">
|
||||
<div id="hero-icon">
|
||||
<BoxAnimate
|
||||
id="hero"
|
||||
:width="48"
|
||||
:height="64"
|
||||
noborder
|
||||
></BoxAnimate>
|
||||
</div>
|
||||
<div id="hero-status">
|
||||
<component :is="nowStatus"></component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider
|
||||
v-if="!isMobile"
|
||||
class="divider"
|
||||
type="vertical"
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
></a-divider>
|
||||
</div>
|
||||
<div id="equip-desc">
|
||||
<div id="equip-icon">
|
||||
<BoxAnimate
|
||||
:id="
|
||||
isCol
|
||||
? equiped[selected] ?? 'none'
|
||||
: toShow[selected]?.[0] ?? 'none'
|
||||
"
|
||||
></BoxAnimate>
|
||||
<span>{{ equip.name }}</span>
|
||||
</div>
|
||||
<div id="equip-type">
|
||||
<span>装备孔:{{ equip.equip?.type }}</span>
|
||||
</div>
|
||||
<a-divider
|
||||
dashed
|
||||
style="border-color: #ddd4; margin: 1vh 0 1vh 0"
|
||||
></a-divider>
|
||||
<div id="equip-add">
|
||||
<span id="title">增减属性</span>
|
||||
<Scroll style="width: 100%; height: 100%">
|
||||
<component :is="addStatus"></component>
|
||||
</Scroll>
|
||||
</div>
|
||||
<a-divider
|
||||
dashed
|
||||
style="border-color: #ddd4; margin: 1vh 0 1vh 0"
|
||||
></a-divider>
|
||||
<div id="equip-desc-text">
|
||||
<span id="title">装备介绍</span>
|
||||
<Scroll id="desc-text" style="height: 100%; width: 100%">
|
||||
<div v-if="!descText.value!.startsWith('!!html')">
|
||||
{{ descText.value }}
|
||||
</div>
|
||||
<div v-else v-html="descText.value!.slice(6)"></div>
|
||||
</Scroll>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="icon-drag">
|
||||
<BoxAnimate
|
||||
class="drag-icon"
|
||||
v-if="draged"
|
||||
:id="toShow[selected]?.[0] ?? 'none'"
|
||||
:width="48"
|
||||
:height="48"
|
||||
noborder
|
||||
></BoxAnimate>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
SortAscendingOutlined,
|
||||
SortDescendingOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
reactive,
|
||||
ref,
|
||||
watch
|
||||
} from 'vue';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import { getAddStatus, getEquips, getNowStatus } from '../plugin/ui/equipbox';
|
||||
import BoxAnimate from '../components/boxAnimate.vue';
|
||||
import { has, tip, type } from '../plugin/utils';
|
||||
import { cancelGlobalDrag, isMobile, useDrag } from '../plugin/use';
|
||||
import { hyper } from 'mutate-animate';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { getStatusLabel } from '../plugin/utils';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
}>();
|
||||
|
||||
const equips = ref(getEquips());
|
||||
const col = ref('all');
|
||||
const all = core.material.items;
|
||||
const selected = ref(0);
|
||||
const isCol = ref(false); // 是否是选中的已装备的装备
|
||||
const equipCol = core.status.globalAttribute.equipName;
|
||||
const equiped = ref(core.status.hero.equipment);
|
||||
const listClicked = ref(false);
|
||||
|
||||
const draged = ref(false);
|
||||
const toEquipType = ref(-1); // 要穿至的装备孔,-1表示不穿
|
||||
|
||||
/** 排序方式,down表示下面大上面小 */
|
||||
const sort = ref<'up' | 'down'>('down');
|
||||
const norm = ref<keyof SelectType<HeroStatus, number> | 'none'>('none');
|
||||
const sType = ref<'value' | 'percentage'>('value');
|
||||
|
||||
// 攻击 防御 回血 额外攻击
|
||||
const normList = ['none', 'atk', 'def', 'hpmax', 'mana'];
|
||||
|
||||
watch(sort, n => {
|
||||
selected.value = toShow.value.length - selected.value - 1;
|
||||
});
|
||||
|
||||
watch(norm, n => {
|
||||
selected.value = 0;
|
||||
});
|
||||
|
||||
watch(sType, n => {
|
||||
selected.value = 0;
|
||||
});
|
||||
|
||||
/**
|
||||
* 当前装备
|
||||
*/
|
||||
const equip = computed(() => {
|
||||
const index = toShow.value[selected.value];
|
||||
const none = {
|
||||
name: '没有选择装备',
|
||||
cls: 'equip',
|
||||
text: '没有选择装备',
|
||||
equip: { type: '无', value: {}, percentage: {}, animate: '' }
|
||||
};
|
||||
if (isCol.value) {
|
||||
const id = equiped.value[selected.value];
|
||||
const e = core.material.items[id];
|
||||
if (!has(e)) return none;
|
||||
return e;
|
||||
}
|
||||
if (!has(index)) return none;
|
||||
return all[index[0]];
|
||||
});
|
||||
|
||||
const addStatus = computed(() => {
|
||||
// @ts-ignore
|
||||
return getAddStatus(equip.value.equip!, isCol.value);
|
||||
});
|
||||
|
||||
const descText = computed(() => {
|
||||
if (equip.value.text!.startsWith('!!html')) return ref(equip.value.text);
|
||||
return type(equip.value.text!, 25, hyper('sin', 'out'), true);
|
||||
});
|
||||
|
||||
const nowStatus = computed(() => {
|
||||
// @ts-ignore
|
||||
return getNowStatus(equip.value.equip, isCol.value);
|
||||
});
|
||||
|
||||
/**
|
||||
* 需要展示的装备,已进行排序等操作
|
||||
*/
|
||||
const toShow = computed(() => {
|
||||
const sortType = sort.value;
|
||||
const sortNorm = norm.value;
|
||||
const sortBy = sType.value;
|
||||
|
||||
const res = equips.value.filter(v => {
|
||||
const e = all[v[0]].equip!;
|
||||
const t = e.type;
|
||||
if (sortNorm !== 'none') {
|
||||
if (!has(e[sortBy][sortNorm])) return false;
|
||||
}
|
||||
if (col.value === 'all') return true;
|
||||
if (typeof t === 'string') return t === col.value;
|
||||
else return core.status.globalAttribute.equipName[t] === col.value;
|
||||
});
|
||||
if (sortNorm === 'none') return res;
|
||||
else {
|
||||
if (sortType === 'down') {
|
||||
return res.sort((a, b) => {
|
||||
const ea = all[a[0]].equip!;
|
||||
const eb = all[b[0]].equip!;
|
||||
return ea[sortBy][sortNorm] - eb[sortBy][sortNorm];
|
||||
});
|
||||
} else {
|
||||
return res.sort((a, b) => {
|
||||
const ea = all[a[0]].equip!;
|
||||
const eb = all[b[0]].equip!;
|
||||
return eb[sortBy][sortNorm] - ea[sortBy][sortNorm];
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function changeSort() {
|
||||
if (sort.value === 'down') {
|
||||
sort.value = 'up';
|
||||
} else {
|
||||
sort.value = 'down';
|
||||
}
|
||||
}
|
||||
|
||||
function exit() {
|
||||
mainUi.close(props.num);
|
||||
}
|
||||
|
||||
function clickList(i: number) {
|
||||
if (i === selected.value && listClicked.value) {
|
||||
const id = toShow.value[selected.value]?.[0];
|
||||
if (!core.canEquip(id)) {
|
||||
tip('warn', '无法装备!');
|
||||
return;
|
||||
}
|
||||
core.loadEquip(id);
|
||||
update();
|
||||
listClicked.value = false;
|
||||
}
|
||||
listClicked.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择某个装备
|
||||
*/
|
||||
function select(i: number, col: boolean = false) {
|
||||
if (i !== selected.value && !col) {
|
||||
listClicked.value = false;
|
||||
}
|
||||
if (col) listClicked.value = false;
|
||||
if (col && isCol.value === col && selected.value === i) {
|
||||
core.unloadEquip(i);
|
||||
update();
|
||||
}
|
||||
isCol.value = col;
|
||||
selected.value = i;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可以将当前装备拖入该栏
|
||||
* @param type 装备栏
|
||||
*/
|
||||
function canDragin(type: number) {
|
||||
if (type < 0) return false;
|
||||
const et = equip.value.equip?.type;
|
||||
if (!core.canEquip(toShow.value[selected.value]?.[0])) return false;
|
||||
if (!has(et)) return false;
|
||||
if (typeof et === 'number') return type === et;
|
||||
return equipCol[type] === et;
|
||||
}
|
||||
|
||||
/**
|
||||
* 穿上某个装备
|
||||
*/
|
||||
function loadEquip() {
|
||||
const num = toEquipType.value;
|
||||
if (num < 0) return;
|
||||
if (!canDragin(num)) {
|
||||
tip('warn', '无法装备!');
|
||||
return;
|
||||
}
|
||||
const now = equiped.value[num];
|
||||
const to = toShow.value[selected.value]?.[0];
|
||||
core.items._realLoadEquip(num, to, now);
|
||||
update();
|
||||
}
|
||||
|
||||
function update() {
|
||||
equiped.value = core.status.hero.equipment;
|
||||
equips.value = getEquips();
|
||||
requestAnimationFrame(() => {
|
||||
bind();
|
||||
});
|
||||
}
|
||||
|
||||
// ----- 绑定函数
|
||||
function bind() {
|
||||
const equips = Array.from(
|
||||
document.querySelectorAll('.equip')
|
||||
) as HTMLDivElement[];
|
||||
|
||||
// 绑定武器拖拽事件,使武器可以拖拽穿脱
|
||||
useDrag(
|
||||
equips,
|
||||
dragEquip,
|
||||
(x, y) => {
|
||||
fx = x;
|
||||
fy = y;
|
||||
},
|
||||
() => {
|
||||
if (draged.value) {
|
||||
draged.value = false;
|
||||
loadEquip();
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// ----- 交互函数
|
||||
let [fx, fy] = [0, 0];
|
||||
function dragEquip(x: number, y: number, e: TouchEvent | MouseEvent) {
|
||||
if ((x - fx) ** 2 + (y - fy) ** 2 > 10 ** 2 && !draged.value) {
|
||||
draged.value = true;
|
||||
}
|
||||
if (draged.value) {
|
||||
const target = document.getElementById('icon-drag') as HTMLDivElement;
|
||||
target.style.left = `${x - 24}px`;
|
||||
target.style.top = `${y - 24}px`;
|
||||
}
|
||||
}
|
||||
|
||||
function dragin(e: Event, type: number) {
|
||||
e.stopPropagation();
|
||||
toEquipType.value = type;
|
||||
}
|
||||
|
||||
function dragout(e: Event) {
|
||||
e.stopPropagation();
|
||||
toEquipType.value = -1;
|
||||
}
|
||||
|
||||
function toTool() {
|
||||
mainUi.holdOn();
|
||||
exit();
|
||||
nextTick(() => {
|
||||
mainUi.open('toolbox');
|
||||
});
|
||||
}
|
||||
|
||||
gameKey.use(props.ui.symbol);
|
||||
gameKey
|
||||
.realize('exit', () => {
|
||||
exit();
|
||||
})
|
||||
.realize('equipbox', () => {
|
||||
exit();
|
||||
})
|
||||
.realize('quickEquip', id => {
|
||||
const num = parseInt(id.split('_').at(-1)!);
|
||||
core.quickSaveEquip(num);
|
||||
tip('success', `已保存至${num}号套装`);
|
||||
});
|
||||
|
||||
watch(toShow, n => {
|
||||
bind();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
bind();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelGlobalDrag(dragEquip);
|
||||
gameKey.dispose(props.ui.symbol);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#equipbox {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#tools {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 3.2vh;
|
||||
height: 5vh;
|
||||
justify-content: space-between;
|
||||
font-family: 'normal';
|
||||
|
||||
.tools {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
#equipbox-main {
|
||||
height: 85vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-family: 'normal';
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#equip-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-basis: 25%;
|
||||
|
||||
#filter {
|
||||
margin-top: 2vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
|
||||
.select {
|
||||
width: 100%;
|
||||
font-family: 'normal';
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
#sort-type {
|
||||
font-size: 75%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
#equip-scroll {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#equip-status {
|
||||
display: flex;
|
||||
flex-basis: 50%;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
#equip-status-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#equip-desc {
|
||||
display: flex;
|
||||
flex-basis: 25%;
|
||||
flex-direction: column;
|
||||
padding-top: 2vh;
|
||||
align-items: center;
|
||||
|
||||
#equip-icon {
|
||||
width: 80%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ddd4;
|
||||
padding-bottom: 1%;
|
||||
|
||||
span {
|
||||
margin-left: 5%;
|
||||
}
|
||||
}
|
||||
|
||||
#equip-add {
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
#title {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.equip-add-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#equip-desc-text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
#title {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#equip-now {
|
||||
width: 100%;
|
||||
|
||||
#equip-now-div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding-left: 5%;
|
||||
|
||||
.equip-now-one {
|
||||
flex-basis: 30%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 3% 3.3% 3% 0;
|
||||
padding-left: 0.5%;
|
||||
|
||||
span {
|
||||
margin-left: 10%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#equip-hero {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#hero-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 10% 0 10% 0;
|
||||
}
|
||||
|
||||
#hero-status {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
.hero-status-one {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-basis: 50%;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
font-size: 110%;
|
||||
white-space: nowrap;
|
||||
|
||||
.hero-status-label {
|
||||
width: 100%;
|
||||
margin-right: 5%;
|
||||
}
|
||||
|
||||
.hero-status-value {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.equip {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5vh 0.5vw;
|
||||
border: #0000 0.5px solid;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
.equip-icon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.equip-name {
|
||||
margin-left: 5%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#icon-drag {
|
||||
position: fixed;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#equipbox-main {
|
||||
flex-direction: column-reverse;
|
||||
font-size: 225%;
|
||||
}
|
||||
|
||||
#equip-now-div {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
#equip-status {
|
||||
flex-direction: column;
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
#equip-list {
|
||||
flex-basis: 45%;
|
||||
|
||||
#filter #sort-type {
|
||||
font-size: 150%;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 1% 0 1% 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,176 +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 { GameUi } from '@/core/main/custom/ui';
|
||||
import type { DamageEnemy, EnemyInfo } from '@/game/enemy/damage';
|
||||
import { nextFrame } from '@/plugin/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
enemy: DamageEnemy;
|
||||
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('var', 'enemySpecials')[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,52 +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 '../plugin/ui/fixed';
|
||||
import BookDetail from './bookDetail.vue';
|
||||
import { detailInfo } from '../plugin/ui/book';
|
||||
import { hovered } from '@/core/main/init/fixed';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
panel?: 'special' | 'critical' | 'target';
|
||||
}>();
|
||||
|
||||
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() {
|
||||
mainUi.close(props.num);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#fixed-detail {
|
||||
width: 80%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
-638
@@ -1,638 +0,0 @@
|
||||
<template>
|
||||
<div id="fly">
|
||||
<div id="tools">
|
||||
<span class="button-text" @click="exit"
|
||||
><left-outlined /> 返回游戏</span
|
||||
>
|
||||
</div>
|
||||
<div id="fly-settings">
|
||||
<div id="fly-border">
|
||||
<span>无边框模式</span>
|
||||
<a-switch
|
||||
class="fly-settings"
|
||||
v-model:checked="noBorder"
|
||||
checked-children="ON"
|
||||
un-checked-children="OFF"
|
||||
></a-switch>
|
||||
</div>
|
||||
<div v-if="!isMobile" id="fly-tradition">
|
||||
<span>传统按键模式</span>
|
||||
<a-switch
|
||||
class="fly-settings"
|
||||
v-model:checked="tradition"
|
||||
checked-children="ON"
|
||||
un-checked-children="OFF"
|
||||
></a-switch>
|
||||
</div>
|
||||
<span
|
||||
v-if="!isMobile"
|
||||
class="button-text"
|
||||
id="fly-download"
|
||||
@click="download"
|
||||
>下载地图图片</span
|
||||
>
|
||||
</div>
|
||||
<div id="fly-main">
|
||||
<div id="fly-left">
|
||||
<Scroll id="fly-area"
|
||||
><div id="area-list">
|
||||
<span
|
||||
v-for="(v, k) in area"
|
||||
:selected="nowArea === k"
|
||||
class="selectable"
|
||||
@click="nowArea = k"
|
||||
>{{ k }}</span
|
||||
>
|
||||
</div></Scroll
|
||||
>
|
||||
<a-divider type="vertical" dashed id="divider-left"></a-divider>
|
||||
<div id="fly-map-div">
|
||||
<canvas id="fly-map" @click="click"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider
|
||||
id="divider-right"
|
||||
dashed
|
||||
:type="isMobile ? 'horizontal' : 'vertical'"
|
||||
></a-divider>
|
||||
<div id="fly-right">
|
||||
<canvas id="fly-thumbnail" @click="fly" @wheel="wheel"></canvas>
|
||||
<div id="fly-tools">
|
||||
<double-left-outlined
|
||||
@click="changeFloorByDelta(-10)"
|
||||
class="button-text fly-button"
|
||||
/>
|
||||
<left-outlined
|
||||
@click="changeFloorByDelta(-1)"
|
||||
class="button-text fly-button"
|
||||
/>
|
||||
<span
|
||||
class="changable"
|
||||
id="fly-now"
|
||||
:change="titleChange"
|
||||
>{{ title }}</span
|
||||
>
|
||||
<right-outlined
|
||||
@click="changeFloorByDelta(1)"
|
||||
class="button-text fly-button"
|
||||
/>
|
||||
<double-right-outlined
|
||||
@click="changeFloorByDelta(10)"
|
||||
class="button-text fly-button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import { getArea, getMapData, MinimapDrawer } from '../plugin/ui/fly';
|
||||
import { cancelGlobalDrag, isMobile, useDrag, useWheel } from '../plugin/use';
|
||||
import {
|
||||
LeftOutlined,
|
||||
DoubleLeftOutlined,
|
||||
RightOutlined,
|
||||
DoubleRightOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { tip } from '../plugin/utils';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { createChangable } from '@/plugin/ui/common';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { GameStorage } from '@/core/main/storage';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
}>();
|
||||
|
||||
type Loc2 = [number, number, number, number];
|
||||
|
||||
const area = getArea();
|
||||
const nowArea = ref(
|
||||
Object.keys(area).find(v => area[v].includes(core.status.floorId)) ?? ''
|
||||
);
|
||||
const nowFloor = ref(core.status.floorId);
|
||||
const noBorder = ref(true);
|
||||
const tradition = ref(false);
|
||||
const settingScale = mainSetting.getValue('ui.mapScale', 100) / 100;
|
||||
let scale = (isMobile ? 1.5 : 3) * settingScale;
|
||||
|
||||
const storage = GameStorage.for(GameStorage.fromAuthor('AncTe', 'flyConfig'));
|
||||
|
||||
noBorder.value = storage.getValue('noBorder', true);
|
||||
tradition.value = storage.getValue('flyTradition', false);
|
||||
|
||||
const floor = computed(() => {
|
||||
return core.status.maps[nowFloor.value];
|
||||
});
|
||||
|
||||
watch(nowFloor, n => {
|
||||
drawer.nowFloor = n;
|
||||
draw();
|
||||
});
|
||||
watch(nowArea, n => {
|
||||
scale = 3 * settingScale;
|
||||
lastScale = 3 * settingScale;
|
||||
drawer.nowArea = n;
|
||||
drawer.scale = 3 * settingScale;
|
||||
drawer.ox = 0;
|
||||
drawer.oy = 0;
|
||||
if (area[n] && !area[n].includes(nowFloor.value))
|
||||
nowFloor.value =
|
||||
area[n].find(v => v === core.status.floorId) ?? area[n][0];
|
||||
});
|
||||
watch(noBorder, n => {
|
||||
storage.setValue('noBorder', n);
|
||||
drawer.noBorder = n;
|
||||
drawer.drawedThumbnail = {};
|
||||
drawer.drawMap();
|
||||
});
|
||||
watch(tradition, n => {
|
||||
storage.setValue('flyTradition', n);
|
||||
});
|
||||
|
||||
let map: HTMLCanvasElement;
|
||||
let thumb: HTMLCanvasElement;
|
||||
let thumbCtx: CanvasRenderingContext2D;
|
||||
|
||||
function exit() {
|
||||
mainUi.close(props.num);
|
||||
}
|
||||
|
||||
const title = computed(() => {
|
||||
return core.status.maps[nowFloor.value].title;
|
||||
});
|
||||
const titleChange = createChangable(title).change;
|
||||
|
||||
let drawer: MinimapDrawer;
|
||||
|
||||
function drawRight() {
|
||||
let w = thumb.width;
|
||||
let h = thumb.height;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
const ratio = floor.value.width / floor.value.height;
|
||||
if (ratio > 1) {
|
||||
h = w / ratio;
|
||||
y = thumb.height / 2 - h / 2;
|
||||
}
|
||||
if (ratio < 1) {
|
||||
w = h * ratio;
|
||||
x = thumb.width / 2 - w / 2;
|
||||
}
|
||||
thumbCtx.fillStyle = '#000';
|
||||
thumbCtx.fillRect(0, 0, thumb.width, thumb.height);
|
||||
core.drawThumbnail(nowFloor.value, void 0, {
|
||||
ctx: thumbCtx,
|
||||
all: true,
|
||||
damage: true,
|
||||
inFlyMap: true,
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制所有内容
|
||||
*/
|
||||
function draw() {
|
||||
drawer.clearCache();
|
||||
drawer.drawMap();
|
||||
drawRight();
|
||||
}
|
||||
|
||||
function download() {
|
||||
drawer.download();
|
||||
draw();
|
||||
}
|
||||
|
||||
function fly() {
|
||||
if (core.flyTo(nowFloor.value)) exit();
|
||||
else tip('error', `无法飞往${floor.value.title}`);
|
||||
}
|
||||
|
||||
let lastScale = scale;
|
||||
const changeScale = debounce((s: number) => {
|
||||
map.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);
|
||||
map.style.transform = `scale(${scale / lastScale})`;
|
||||
drawer.thumbnailLoc = {};
|
||||
}
|
||||
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
|
||||
let moved = false;
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
|
||||
// -------------------- 点击事件
|
||||
|
||||
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 click(e: MouseEvent) {
|
||||
if (moved) return;
|
||||
|
||||
const x = e.offsetX * devicePixelRatio;
|
||||
const y = e.offsetY * devicePixelRatio;
|
||||
for (const [id, [left, top, right, bottom]] of Object.entries(
|
||||
drawer.thumbnailLoc
|
||||
) as [FloorIds, Loc2][]) {
|
||||
if (x >= left && x <= right && y >= top && y <= bottom) {
|
||||
if (id === nowFloor.value) {
|
||||
fly();
|
||||
} else {
|
||||
nowFloor.value = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wheel(ev: WheelEvent) {
|
||||
changeFloorByDelta(-Math.sign(ev.deltaY));
|
||||
}
|
||||
|
||||
function changeAreaByFloor(id: FloorIds) {
|
||||
nowArea.value = Object.keys(area).find(v => area[v].includes(id))!;
|
||||
}
|
||||
|
||||
function changeFloorByDelta(delta: number) {
|
||||
const now = core.floorIds.indexOf(nowFloor.value);
|
||||
let to = now + delta;
|
||||
if (to < 0) to = 0;
|
||||
if (to >= core.floorIds.length) to = core.floorIds.length - 1;
|
||||
const floor = core.status.maps[core.floorIds[to]];
|
||||
if (floor.deleted || floor.forceDelete) {
|
||||
while (to !== now) {
|
||||
to += Math.sign(delta);
|
||||
const floor = core.status.maps[core.floorIds[to]];
|
||||
if (floor.cannotViewMap) continue;
|
||||
if (!floor.deleted && !floor.forceDelete) break;
|
||||
if (to < 0 || to >= core.floorIds.length) break;
|
||||
}
|
||||
}
|
||||
nowFloor.value = core.floorIds[to];
|
||||
changeAreaByFloor(nowFloor.value);
|
||||
drawer.locateMap(nowFloor.value);
|
||||
}
|
||||
|
||||
function changeFloorByDir(dir: Dir) {
|
||||
const data = getMapData(nowFloor.value);
|
||||
|
||||
for (const [from, to] of Object.entries(data.link)) {
|
||||
if (!from.startsWith(nowFloor.value)) continue;
|
||||
const d = from.split(',')[3] as Dir;
|
||||
|
||||
if (d === dir) {
|
||||
const target = to.split(',')[0] as FloorIds;
|
||||
drawer.locateMap(target);
|
||||
nowFloor.value = target;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- 键盘事件
|
||||
|
||||
gameKey.use(props.ui.symbol);
|
||||
gameKey
|
||||
.realize('@fly_left', () => {
|
||||
if (!tradition.value) changeFloorByDir('left');
|
||||
})
|
||||
.realize('@fly_right', () => {
|
||||
if (!tradition.value) changeFloorByDir('right');
|
||||
})
|
||||
.realize('@fly_up', () => {
|
||||
if (!tradition.value) changeFloorByDir('up');
|
||||
})
|
||||
.realize('@fly_down', () => {
|
||||
if (!tradition.value) changeFloorByDir('down');
|
||||
})
|
||||
.realize('@fly_last', () => {
|
||||
if (!tradition.value) changeFloorByDelta(-1);
|
||||
})
|
||||
.realize('@fly_next', () => {
|
||||
if (!tradition.value) changeFloorByDelta(1);
|
||||
})
|
||||
.realize('@fly_down_t', () => {
|
||||
if (tradition.value) changeFloorByDelta(-1);
|
||||
})
|
||||
.realize('@fly_up_t', () => {
|
||||
if (tradition.value) changeFloorByDelta(1);
|
||||
})
|
||||
.realize('@fly_left_t', () => {
|
||||
if (tradition.value) changeFloorByDelta(-10);
|
||||
})
|
||||
.realize('@fly_right_t', () => {
|
||||
if (tradition.value) changeFloorByDelta(10);
|
||||
})
|
||||
.realize('exit', () => {
|
||||
exit();
|
||||
})
|
||||
.realize('confirm', () => {
|
||||
fly();
|
||||
})
|
||||
.realize('fly', () => {
|
||||
exit();
|
||||
});
|
||||
|
||||
// -------------------- 触摸事件
|
||||
|
||||
let touchScale = false;
|
||||
let lastDis = 0;
|
||||
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;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
map = document.getElementById('fly-map') as HTMLCanvasElement;
|
||||
thumb = document.getElementById('fly-thumbnail') as HTMLCanvasElement;
|
||||
thumbCtx = thumb.getContext('2d')!;
|
||||
|
||||
drawer = new MinimapDrawer(map);
|
||||
drawer.scale = scale;
|
||||
drawer.noBorder = noBorder.value;
|
||||
drawer.showInfo = true;
|
||||
drawer.nowArea = nowArea.value;
|
||||
|
||||
const mapStyle = getComputedStyle(map);
|
||||
const thumbStyle = getComputedStyle(thumb);
|
||||
map.width = parseFloat(mapStyle.width) * devicePixelRatio;
|
||||
map.height = parseFloat(mapStyle.height) * devicePixelRatio;
|
||||
thumb.width = parseFloat(thumbStyle.width) * devicePixelRatio;
|
||||
thumb.height = parseFloat(thumbStyle.width) * devicePixelRatio;
|
||||
|
||||
Array.from(document.getElementsByClassName('fly-settings')).forEach(v => {
|
||||
v.addEventListener('click', e => (v as HTMLElement).blur());
|
||||
});
|
||||
|
||||
drawer.locateMap(nowFloor.value);
|
||||
draw();
|
||||
|
||||
useDrag(
|
||||
map,
|
||||
drag,
|
||||
(x, y) => {
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
startX = x;
|
||||
startY = y;
|
||||
},
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
moved = false;
|
||||
}, 50);
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
useWheel(map, (x, y) => {
|
||||
const delta = -Math.sign(y) * 0.1 + 1;
|
||||
resize(delta);
|
||||
});
|
||||
|
||||
map.addEventListener('touchstart', touchdown);
|
||||
map.addEventListener('touchend', touchup);
|
||||
map.addEventListener('touchmove', touchmove);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelGlobalDrag(drag);
|
||||
gameKey.dispose(props.ui.symbol);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#fly {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 150%;
|
||||
font-family: 'normal';
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#tools {
|
||||
width: 100%;
|
||||
font-family: 'normal';
|
||||
font-size: 3.2vh;
|
||||
height: 5vh;
|
||||
position: fixed;
|
||||
left: 5vw;
|
||||
top: 5vh;
|
||||
}
|
||||
|
||||
#fly-main {
|
||||
display: flex;
|
||||
height: 80%;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#fly-left {
|
||||
width: 50vw;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#fly-area {
|
||||
height: 100%;
|
||||
width: 15vw;
|
||||
}
|
||||
|
||||
#area-list {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#divider-left {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
border-color: #ddd4;
|
||||
}
|
||||
|
||||
#fly-map-div,
|
||||
#fly-map {
|
||||
width: 35vw;
|
||||
height: 72vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#divider-right {
|
||||
height: 100%;
|
||||
border-color: #ddd4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#fly-right {
|
||||
width: 40vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#fly-tools {
|
||||
margin: 0;
|
||||
width: 80%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background-color: #0004;
|
||||
}
|
||||
|
||||
#fly-thumbnail {
|
||||
width: 35vw;
|
||||
height: 35vw;
|
||||
max-height: 75vh;
|
||||
max-width: 75vh;
|
||||
border: 0.1vw solid #ddd4;
|
||||
}
|
||||
|
||||
#fly-settings {
|
||||
position: fixed;
|
||||
bottom: 5vh;
|
||||
left: 10vw;
|
||||
width: 80vw;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
margin-right: 5vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fly-settings[aria-checked='false'] {
|
||||
background-color: #ddd4;
|
||||
}
|
||||
|
||||
.fly-button {
|
||||
padding: 3%;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.301);
|
||||
border: 1px dashed #ddda;
|
||||
filter: drop-shadow(0px 0px 16px black);
|
||||
}
|
||||
|
||||
#fly-now {
|
||||
text-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
max-width: 50%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-shadow: 1px 1px 1px black, 1px -1px 1px black, -1px 1px 1px black,
|
||||
-1px -1px 1px black;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#fly {
|
||||
font-size: 225%;
|
||||
}
|
||||
|
||||
#fly-main {
|
||||
flex-direction: column;
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
#fly-map-div,
|
||||
#fly-map {
|
||||
width: 60vw;
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
#fly-area {
|
||||
width: 30vw;
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
#fly-left {
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
#divider-right {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
#fly-right {
|
||||
width: 90vw;
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
#fly-thumbnail {
|
||||
width: 80vw;
|
||||
height: 80vw;
|
||||
}
|
||||
|
||||
#tools {
|
||||
top: 2vh;
|
||||
}
|
||||
|
||||
#fly-settings {
|
||||
bottom: 2%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,261 +0,0 @@
|
||||
<template>
|
||||
<Column @close="close" :left="30">
|
||||
<template #left>
|
||||
<div id="hotkey-group">
|
||||
<span
|
||||
class="hotkey-group-one selectable"
|
||||
v-for="(name, group) of groupName"
|
||||
:selected="selectedGroup === group"
|
||||
@click="selectedGroup = group"
|
||||
>
|
||||
{{ name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #right>
|
||||
<div id="hotkey-data">
|
||||
<div class="hotkey-one" v-for="(data, id) of show">
|
||||
<span class="hotkey-one-name"> {{ data.name }} </span>
|
||||
<div class="hotkey-one-set">
|
||||
<span
|
||||
v-for="key of data.keys"
|
||||
class="hotkey-one-set-item"
|
||||
:selected="
|
||||
data.id === selectedKey.id &&
|
||||
key.index === selectedKey.index
|
||||
"
|
||||
@click="select(data.id, key.index)"
|
||||
>
|
||||
{{ getKeyShow(key.key, key.assist) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Hotkey } from '@/core/main/custom/hotkey';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import Column from '@/components/colomn.vue';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { KeyCode, KeyCodeUtils } from '@/plugin/keyCodes';
|
||||
import { generateBinary, keycode } from '@/plugin/utils';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
|
||||
interface HotkeyKeys {
|
||||
index: number;
|
||||
key: KeyCode;
|
||||
/** 从低到高依次为 `ctrl` `shift` `alt` */
|
||||
assist: number;
|
||||
}
|
||||
|
||||
interface HotkeySetting {
|
||||
id: string;
|
||||
name: string;
|
||||
keys: HotkeyKeys[];
|
||||
}
|
||||
|
||||
interface SelectedKey {
|
||||
id: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
hotkey: Hotkey;
|
||||
}>();
|
||||
|
||||
const hotkey = props.hotkey;
|
||||
|
||||
function close() {
|
||||
mainUi.close(props.num);
|
||||
}
|
||||
|
||||
const selectedGroup = ref('ui');
|
||||
const keyData = generateData();
|
||||
const show = computed(() => {
|
||||
return keyData[selectedGroup.value];
|
||||
});
|
||||
const groupName = cloneDeep(hotkey.groupName);
|
||||
delete groupName.none;
|
||||
|
||||
const selectedKey: SelectedKey = reactive({
|
||||
id: 'none',
|
||||
index: -1
|
||||
});
|
||||
|
||||
function generateData() {
|
||||
const res: Record<string, Record<string, HotkeySetting>> = {};
|
||||
|
||||
for (const [group, data] of Object.entries(hotkey.groups)) {
|
||||
if (group === 'none') continue;
|
||||
res[group] = {};
|
||||
const d = res[group];
|
||||
data.forEach(v => {
|
||||
const split = v.split('_');
|
||||
const isMulti = /^\d+$/.test(split.at(-1)!);
|
||||
const id = isMulti ? split.slice(0, -1).join('_') : v;
|
||||
const index = isMulti ? parseInt(split.at(-1)!) : -1;
|
||||
|
||||
const key = hotkey.data[v];
|
||||
d[id] ??= {
|
||||
id,
|
||||
name: isMulti
|
||||
? key.name.split('_').slice(0, -1).join('_')
|
||||
: key.name,
|
||||
keys: reactive([])
|
||||
};
|
||||
d[id].keys.push({
|
||||
index,
|
||||
key: key.key,
|
||||
assist: generateBinary([key.ctrl, key.shift, key.alt])
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function unwarpAssist(assist: number) {
|
||||
let res = '';
|
||||
if (assist & (1 << 0)) {
|
||||
res += 'Ctrl + ';
|
||||
}
|
||||
if (assist & (1 << 1)) {
|
||||
res += 'Shift + ';
|
||||
}
|
||||
if (assist & (1 << 2)) {
|
||||
res += 'Alt + ';
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function getKeyShow(key: KeyCode, assist: number) {
|
||||
return (
|
||||
unwarpAssist(assist) +
|
||||
(key === KeyCode.Unknown ? '' : KeyCodeUtils.toString(key))
|
||||
);
|
||||
}
|
||||
|
||||
function select(id: string, index: number) {
|
||||
selectedKey.id = id;
|
||||
selectedKey.index = index;
|
||||
}
|
||||
|
||||
function keyup(e: KeyboardEvent) {
|
||||
if (selectedKey.id === 'none') return;
|
||||
e.preventDefault();
|
||||
const code = keycode(e.keyCode);
|
||||
|
||||
const assist = generateBinary([e.ctrlKey, e.shiftKey, e.altKey]);
|
||||
const id =
|
||||
selectedKey.index === -1
|
||||
? selectedKey.id
|
||||
: `${selectedKey.id}_${selectedKey.index}`;
|
||||
|
||||
const key = keyData[selectedGroup.value][selectedKey.id].keys.find(
|
||||
v => v.index === selectedKey.index
|
||||
);
|
||||
|
||||
if (key) {
|
||||
if (
|
||||
code === KeyCode.Ctrl ||
|
||||
code === KeyCode.Shift ||
|
||||
code === KeyCode.Alt ||
|
||||
code === KeyCode.Unknown
|
||||
) {
|
||||
const data = hotkey.data[id];
|
||||
key.assist = generateBinary([data.ctrl, data.shift, data.alt]);
|
||||
key.key = data.key;
|
||||
} else {
|
||||
key.key = code;
|
||||
key.assist = assist;
|
||||
hotkey.set(id, code, assist);
|
||||
}
|
||||
}
|
||||
|
||||
selectedKey.id = 'none';
|
||||
selectedKey.index = -1;
|
||||
}
|
||||
|
||||
function keydown(e: KeyboardEvent) {
|
||||
if (selectedKey.id === 'none') return;
|
||||
e.preventDefault();
|
||||
const code = keycode(e.keyCode);
|
||||
const assist = generateBinary([e.ctrlKey, e.shiftKey, e.altKey]);
|
||||
const key = keyData[selectedGroup.value][selectedKey.id].keys.find(
|
||||
v => v.index === selectedKey.index
|
||||
);
|
||||
if (!key) return;
|
||||
key.assist = assist;
|
||||
key.key = KeyCode.Unknown;
|
||||
|
||||
if (
|
||||
code !== KeyCode.Ctrl &&
|
||||
code !== KeyCode.Shift &&
|
||||
code !== KeyCode.Alt
|
||||
) {
|
||||
key.key = code;
|
||||
}
|
||||
}
|
||||
|
||||
// ban other keys
|
||||
gameKey.disable();
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keyup', keyup);
|
||||
document.addEventListener('keydown', keydown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
gameKey.enable();
|
||||
document.removeEventListener('keyup', keyup);
|
||||
document.removeEventListener('keydown', keydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#hotkey-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hotkey-group-one {
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.hotkey-one {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin: 0 3% 2% 3%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hotkey-one-set {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 50%;
|
||||
text-overflow: clip;
|
||||
align-items: flex-end;
|
||||
text-align: end;
|
||||
|
||||
.hotkey-one-set-item {
|
||||
background-color: #4446;
|
||||
padding: 0 5%;
|
||||
margin: 1% 0;
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.hotkey-one-set-item[selected='true'] {
|
||||
color: gold;
|
||||
border: 1px solid gold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +0,0 @@
|
||||
export { default as Achievement } from './achievement.vue';
|
||||
export { default as BgmList } from './bgmList.vue';
|
||||
export { default as Book } from './book.vue';
|
||||
export { default as BookDetail } from './bookDetail.vue';
|
||||
export { default as Chapter } from './chapter.vue';
|
||||
export { default as CompleteAchi } from './completeAchievement.vue';
|
||||
export { default as Desc } from './desc.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 Marked } from './markedEnemy.vue';
|
||||
export { default as Settings } from './settings.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 StatusBar } from './statusBar.vue';
|
||||
export { default as Toolbox } from './toolbox.vue';
|
||||
export { default as Hotkey } from './hotkey.vue';
|
||||
export { default as Toolbar } from './toolbar.vue';
|
||||
export { default as ToolEditor } from './toolEditor.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';
|
||||
-110
@@ -1,110 +0,0 @@
|
||||
<template>
|
||||
<div id="load">
|
||||
<a-progress
|
||||
class="task-progress"
|
||||
type="circle"
|
||||
:percent="(loading / totalTask) * 100"
|
||||
:success="{ percent: (loaded / totalTask) * 100 }"
|
||||
>
|
||||
<template #format>
|
||||
<span>{{ loaded }} / {{ totalTask }}</span>
|
||||
</template>
|
||||
</a-progress>
|
||||
<div class="byte-div">
|
||||
<span class="byte-progress-tip"
|
||||
>{{ formatSize(loadedByte) }} /
|
||||
{{ formatSize(totalByte) }}</span
|
||||
>
|
||||
<a-progress
|
||||
class="byte-progress"
|
||||
type="line"
|
||||
:percent="loadedPercent"
|
||||
></a-progress>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import {
|
||||
loadCompressedResource,
|
||||
loadDefaultResource,
|
||||
LoadTask
|
||||
} from '@motajs/legacy-common';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { formatSize } from '@/plugin/utils';
|
||||
import { logger } from '@motajs/common';
|
||||
import { fixedUi } from '@/core/main/init/ui';
|
||||
import { sleep } from 'mutate-animate';
|
||||
|
||||
const props = defineProps<{
|
||||
ui: GameUi;
|
||||
num: number;
|
||||
callback?: () => void;
|
||||
}>();
|
||||
|
||||
const loading = ref(0);
|
||||
const loaded = ref(0);
|
||||
const loadedByte = ref(0);
|
||||
const loadedPercent = ref(0);
|
||||
const totalByte = ref(0);
|
||||
const totalTask = ref(0);
|
||||
|
||||
let loadDiv: HTMLDivElement;
|
||||
|
||||
onMounted(async () => {
|
||||
if (import.meta.env.DEV) loadDefaultResource();
|
||||
else await loadCompressedResource();
|
||||
|
||||
LoadTask.onProgress(() => {
|
||||
const loadingNum = [...LoadTask.taskList].filter(v => v.loading).length;
|
||||
|
||||
loadedByte.value = LoadTask.loadedByte;
|
||||
loadedPercent.value = parseFloat(
|
||||
((LoadTask.loadedByte / LoadTask.totalByte) * 100).toFixed(2)
|
||||
);
|
||||
loading.value = loadingNum;
|
||||
loaded.value = LoadTask.loadedTask;
|
||||
totalByte.value = LoadTask.totalByte;
|
||||
totalTask.value = LoadTask.totalTask;
|
||||
});
|
||||
|
||||
LoadTask.load().then(async () => {
|
||||
core.loader._loadMaterials_afterLoad();
|
||||
core._afterLoadResources(props.callback);
|
||||
logger.log(`Resource load end.`);
|
||||
loadDiv.style.opacity = '0';
|
||||
Mota.require('var', 'loading').emit('loaded');
|
||||
await sleep(1000);
|
||||
fixedUi.close(props.num);
|
||||
fixedUi.open('start');
|
||||
});
|
||||
loadDiv = document.getElementById('load') as HTMLDivElement;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#load {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
font-family: 'Arial';
|
||||
transition: opacity 1s linear;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.byte-div {
|
||||
width: 50%;
|
||||
margin-top: 10vh;
|
||||
}
|
||||
|
||||
.byte-progress {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,157 +0,0 @@
|
||||
<template>
|
||||
<div id="marked-enemy">
|
||||
<Box
|
||||
v-model:left="boxPos.left"
|
||||
v-model:top="boxPos.top"
|
||||
v-model:width="boxPos.width"
|
||||
v-model:height="boxPos.height"
|
||||
:resizable="true"
|
||||
:dragable="true"
|
||||
>
|
||||
<Scroll class="box-scroll" :no-scroll="true">
|
||||
<div class="marked-main">
|
||||
<div class="marked-info">
|
||||
<BoxAnimate
|
||||
:id="enemy.id"
|
||||
:width="24"
|
||||
:height="24"
|
||||
></BoxAnimate>
|
||||
<span class="marked-name marked-item">{{
|
||||
getName()
|
||||
}}</span>
|
||||
</div>
|
||||
<span class="marked-damage marked-item"
|
||||
>伤害:{{ format(info.damage) }}</span
|
||||
>
|
||||
<span class="marked-critical marked-item"
|
||||
>临界:{{ format(info.critical) }}</span
|
||||
>
|
||||
<span class="marked-critical-damage marked-item"
|
||||
>减伤:{{ format(info.criticalDam) }}</span
|
||||
>
|
||||
<span class="marked-def marked-item"
|
||||
>{{ ratio }}防:{{ format(info.defDamage) }}</span
|
||||
>
|
||||
<div class="marked-button">
|
||||
<span
|
||||
class="marked-hide button-text"
|
||||
@click.stop="hidden = true"
|
||||
>隐藏盒子</span
|
||||
>
|
||||
<span
|
||||
class="marked-cancel button-text"
|
||||
@click.stop="unmarkEnemy(enemy.id)"
|
||||
>取消标记</span
|
||||
>
|
||||
</div>
|
||||
</div></Scroll
|
||||
>
|
||||
</Box>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { MarkInfo, unmarkEnemy } from '../plugin/mark';
|
||||
import Box from '../components/box.vue';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import BoxAnimate from '../components/boxAnimate.vue';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { fixedUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
enemy: MarkInfo<EnemyIds>;
|
||||
}>();
|
||||
|
||||
interface BoxPos {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface MarkedEnemy {
|
||||
damage: number;
|
||||
critical: number;
|
||||
criticalDam: number;
|
||||
defDamage: number;
|
||||
}
|
||||
|
||||
const enemy = props.enemy;
|
||||
const ratio = core.status.thisMap?.ratio ?? 1;
|
||||
|
||||
const format = core.formatBigNumber;
|
||||
|
||||
const boxPos = reactive<BoxPos>({
|
||||
left: window.innerWidth - 300,
|
||||
top: 100,
|
||||
width: 200,
|
||||
height: 150
|
||||
});
|
||||
const info = reactive<MarkedEnemy>({
|
||||
damage: 0,
|
||||
critical: 0,
|
||||
criticalDam: 0,
|
||||
defDamage: 0
|
||||
});
|
||||
|
||||
const hidden = ref(false);
|
||||
|
||||
watch(hidden, n => {
|
||||
if (n) fixedUi.close(props.num);
|
||||
});
|
||||
watch(enemy.update, update);
|
||||
|
||||
function update() {
|
||||
info.damage = enemy.enemy.calDamage().damage;
|
||||
const critical = enemy.enemy.calCritical()[0];
|
||||
info.critical = critical?.atkDelta ?? 0;
|
||||
info.criticalDam = critical?.delta ?? 0;
|
||||
info.defDamage = enemy.enemy.calDefDamage(ratio).delta;
|
||||
}
|
||||
|
||||
function getName() {
|
||||
return enemy.enemy.enemy.name;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#marked-enemy {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.box-scroll {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.marked-main {
|
||||
padding: 1vh 0;
|
||||
background-color: #0009;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.marked-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.marked-item {
|
||||
margin-left: 10%;
|
||||
}
|
||||
|
||||
.marked-button {
|
||||
align-self: center;
|
||||
width: 80%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
</style>
|
||||
@@ -1,370 +0,0 @@
|
||||
<template>
|
||||
<div class="setting-main">
|
||||
<div id="tools">
|
||||
<span class="button-text" @click="exit"
|
||||
><left-outlined /> 返回游戏</span
|
||||
>
|
||||
</div>
|
||||
<div class="setting-container">
|
||||
<div class="setting-select">
|
||||
<TransitionGroup name="list">
|
||||
<div
|
||||
v-for="(info, i) of display"
|
||||
:key="i"
|
||||
class="setting-display"
|
||||
>
|
||||
<Scroll class="setting-scroll">
|
||||
<div class="setting-list">
|
||||
<div
|
||||
v-for="item of info.list"
|
||||
class="setting-item selectable"
|
||||
:selected="item === info.item"
|
||||
@click="click(item.key, i, item)"
|
||||
>
|
||||
<span class="setting-name">{{
|
||||
item.name
|
||||
}}</span>
|
||||
<span
|
||||
:selected="item === info.item"
|
||||
class="setting-cascade"
|
||||
v-if="isCascade(item)"
|
||||
>
|
||||
<RightOutlined />
|
||||
</span>
|
||||
<span v-else class="setting-value">
|
||||
{{ getItemValue(item) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Scroll>
|
||||
<a-divider
|
||||
class="display-divider"
|
||||
:type="isMobile ? 'horizontal' : 'vertical'"
|
||||
dashed
|
||||
></a-divider>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
<div class="setting-info">
|
||||
<Scroll class="info-text-scroll">
|
||||
<div
|
||||
class="info-text"
|
||||
v-html="
|
||||
splitText(display.at(-1)?.text ?? ['请选择设置'])
|
||||
"
|
||||
></div>
|
||||
</Scroll>
|
||||
<a-divider class="info-divider" dashed></a-divider>
|
||||
<div class="info-editor" v-if="!!selectedItem">
|
||||
<div class="editor-custom">
|
||||
<component
|
||||
:is="(selectedItem.controller as any)"
|
||||
:item="selectedItem"
|
||||
:displayer="displayer"
|
||||
:setting="setting"
|
||||
:update="update"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onUnmounted, ref, shallowRef } from 'vue';
|
||||
import {
|
||||
mainSetting,
|
||||
MotaSetting,
|
||||
MotaSettingItem,
|
||||
SettingDisplayer,
|
||||
SettingDisplayInfo
|
||||
} from '../core/main/setting';
|
||||
import { RightOutlined, LeftOutlined } from '@ant-design/icons-vue';
|
||||
import { splitText } from '../plugin/utils';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import { isMobile } from '../plugin/use';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
info?: MotaSetting;
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
}>();
|
||||
|
||||
const setting = props.info ?? mainSetting;
|
||||
const display = shallowRef<SettingDisplayInfo[]>([]);
|
||||
const selectedItem = computed(() => display.value.at(-1)?.item);
|
||||
const update = ref(false);
|
||||
|
||||
const displayer = new SettingDisplayer(setting);
|
||||
displayer.on('update', (stack, dis) => {
|
||||
display.value = dis;
|
||||
update.value = !update.value;
|
||||
});
|
||||
display.value = displayer.displayInfo;
|
||||
|
||||
function getItemValue(item: MotaSettingItem) {
|
||||
if (item.value instanceof MotaSetting) {
|
||||
return '';
|
||||
} else {
|
||||
if (item.display) {
|
||||
return item.display(item.value);
|
||||
} else {
|
||||
if (typeof item.value === 'number') {
|
||||
return item.value.toString();
|
||||
} else {
|
||||
return item.value ? 'ON' : 'OFF';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isCascade(item: MotaSettingItem) {
|
||||
return item.value instanceof MotaSetting;
|
||||
}
|
||||
|
||||
function click(key: string, index: number, item: MotaSettingItem) {
|
||||
if (item.value instanceof MotaSetting) {
|
||||
if (index === display.value.length - 1) {
|
||||
displayer.add(key);
|
||||
} else {
|
||||
if (displayer.selectStack.includes(key)) {
|
||||
displayer.cut(index);
|
||||
} else {
|
||||
displayer.cut(index, true);
|
||||
displayer.add(key);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
displayer.cut(index, true);
|
||||
displayer.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
function exit() {
|
||||
mainUi.close(props.num);
|
||||
}
|
||||
|
||||
gameKey.use(props.ui.symbol);
|
||||
gameKey.realize('exit', () => {
|
||||
exit();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
gameKey.dispose(props.ui.symbol);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#tools {
|
||||
width: 100%;
|
||||
font-family: 'normal';
|
||||
font-size: 3.2vh;
|
||||
height: 5vh;
|
||||
position: fixed;
|
||||
left: 10vw;
|
||||
top: 10vh;
|
||||
}
|
||||
|
||||
.setting-main {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
font-family: normal;
|
||||
font-size: 150%;
|
||||
|
||||
.setting-container {
|
||||
height: 60%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-select {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.list-move,
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
|
||||
.list-leave-active {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.setting-display {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
|
||||
.setting-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.display-divider {
|
||||
border-color: #fff4;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
padding: 2%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.setting-cascade {
|
||||
font-size: 75%;
|
||||
margin-right: 40px;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.setting-cascade[selected='true'] {
|
||||
font-size: 90%;
|
||||
margin-right: 10px;
|
||||
color: cyan;
|
||||
}
|
||||
|
||||
.setting-name {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 70%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.setting-value {
|
||||
margin-right: 10px;
|
||||
color: rgb(242, 255, 101);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-scroll {
|
||||
width: 20vw;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.setting-info::v-deep(.editor-boolean) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 0 10% 0 5%;
|
||||
|
||||
.boolean-button {
|
||||
font-size: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-info::v-deep(.editor-number) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 0 10% 0 5%;
|
||||
|
||||
.number-input,
|
||||
.number-button {
|
||||
font-size: 80%;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.number-button {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.editor-number-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
width: 25vw;
|
||||
|
||||
.info-divider {
|
||||
border-color: #fff4;
|
||||
margin: 2% 0;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
font-size: 85%;
|
||||
min-height: 30%;
|
||||
max-height: 50%;
|
||||
}
|
||||
|
||||
.info-text-scroll {
|
||||
max-height: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#tools {
|
||||
top: 2vh;
|
||||
}
|
||||
|
||||
.setting-main {
|
||||
font-size: 225%;
|
||||
|
||||
.setting-container {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.setting-select {
|
||||
flex-direction: column;
|
||||
width: 90vw;
|
||||
}
|
||||
}
|
||||
|
||||
.list-enter-from,
|
||||
.list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
|
||||
.setting-scroll {
|
||||
width: 90vw;
|
||||
height: 20vh;
|
||||
}
|
||||
|
||||
.setting-display {
|
||||
width: 90vw;
|
||||
flex-direction: column;
|
||||
height: 22vh;
|
||||
|
||||
.display-divider {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin: 1vh 0;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
width: 90vw;
|
||||
height: 30vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-488
@@ -1,488 +0,0 @@
|
||||
<template>
|
||||
<div id="shop">
|
||||
<div id="tools">
|
||||
<span class="button-text" @click="exit"
|
||||
><left-outlined /> 返回游戏</span
|
||||
>
|
||||
</div>
|
||||
<span id="item-name">{{ info.name }}</span>
|
||||
<a-divider dashed style="border-color: #ddd4" id="divider"></a-divider>
|
||||
<div id="item-info">
|
||||
<Scroll id="item-desc" :no-scroll="true">
|
||||
<span v-html="desc"></span>
|
||||
</Scroll>
|
||||
</div>
|
||||
<a-divider
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
id="divider-split"
|
||||
></a-divider>
|
||||
<div id="shop-bottom">
|
||||
<div id="item-list">
|
||||
<Scroll style="width: 100%; height: 100%">
|
||||
<div
|
||||
class="selectable item-one"
|
||||
v-for="(i, index) of choices"
|
||||
:selected="index === selected"
|
||||
@click="selected = index"
|
||||
>
|
||||
<div class="item-icon">
|
||||
<BoxAnimate
|
||||
:id="i.id"
|
||||
:noborder="true"
|
||||
></BoxAnimate>
|
||||
<span>{{ all[i.id].name }}</span>
|
||||
</div>
|
||||
<span
|
||||
>× {{
|
||||
(mode === 'buy'
|
||||
? i.number - (f.itemShop[id]?.[i.id] ?? 0)
|
||||
: cnt(i.id)) + (update ? 0 : 0)
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</Scroll>
|
||||
</div>
|
||||
<a-divider
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
type="vertical"
|
||||
id="divider-vertical"
|
||||
></a-divider>
|
||||
<div id="item-sell-info">
|
||||
<div id="shop-mode">
|
||||
<span
|
||||
class="button-text mode-button"
|
||||
:active="mode === 'buy'"
|
||||
@click="mode = 'buy'"
|
||||
>购买</span
|
||||
>
|
||||
<a-divider
|
||||
dashed
|
||||
type="vertical"
|
||||
id="divider-vertical"
|
||||
style="border-color: #ddd4"
|
||||
></a-divider>
|
||||
<span
|
||||
class="button-text mode-button"
|
||||
:active="mode === 'sell'"
|
||||
danger="true"
|
||||
@click="mode = 'sell'"
|
||||
>售出</span
|
||||
>
|
||||
</div>
|
||||
<a-divider
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
id="divider-mode"
|
||||
></a-divider>
|
||||
<div
|
||||
class="item-sell-info"
|
||||
:style="{
|
||||
color:
|
||||
mode === 'buy'
|
||||
? nowMoney >= parseInt(item.money)
|
||||
? 'lightgreen'
|
||||
: 'lightcoral'
|
||||
: 'white'
|
||||
}"
|
||||
>
|
||||
<span>买价</span>
|
||||
<span>{{ item.money }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="item-sell-info"
|
||||
:style="{ color: mode === 'sell' ? 'lightcoral' : 'white' }"
|
||||
>
|
||||
<span>卖价</span>
|
||||
<span>{{ item.sell }}</span>
|
||||
</div>
|
||||
<div class="item-sell-info">
|
||||
<span>存货</span>
|
||||
<span>{{ remain }}</span>
|
||||
</div>
|
||||
<div class="item-sell-info">
|
||||
<span>拥有</span>
|
||||
<span>{{ cnt(item.id) }}</span>
|
||||
</div>
|
||||
<a-divider
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
id="divider-mode"
|
||||
></a-divider>
|
||||
<div id="sell-count">
|
||||
<span>{{ mode === 'buy' ? '购买' : '售出' }}数量</span>
|
||||
<div id="sell-count-select">
|
||||
<double-left-outlined
|
||||
class="button-text"
|
||||
@click="count -= 10"
|
||||
/>
|
||||
<left-outlined class="button-text" @click="count--" />
|
||||
<span id="fly-now">{{ count }}</span>
|
||||
<right-outlined class="button-text" @click="count++" />
|
||||
<double-right-outlined
|
||||
class="button-text"
|
||||
@click="count += 10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider
|
||||
dashed
|
||||
style="border-color: #ddd4; margin: 2vh 0 2vh 0"
|
||||
></a-divider>
|
||||
<div id="sell-confirm">
|
||||
<span
|
||||
id="sell-total"
|
||||
:style="{
|
||||
color:
|
||||
mode === 'buy'
|
||||
? nowMoney > cost
|
||||
? 'lightgreen'
|
||||
: 'lightcoral'
|
||||
: 'lightcoral'
|
||||
}"
|
||||
>总价:{{
|
||||
count *
|
||||
parseInt(mode === 'buy' ? item.money : item.sell)
|
||||
}}</span
|
||||
>
|
||||
<span
|
||||
id="sell-button"
|
||||
class="button-text"
|
||||
:danger="mode === 'sell'"
|
||||
danger-display="true"
|
||||
active="true"
|
||||
@click="confirm"
|
||||
>确认{{ mode === 'buy' ? '购买' : '售出' }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import {
|
||||
LeftOutlined,
|
||||
DoubleLeftOutlined,
|
||||
RightOutlined,
|
||||
DoubleRightOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { splitText, tip } from '../plugin/utils';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import BoxAnimate from '../components/boxAnimate.vue';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
shopId: string;
|
||||
}>();
|
||||
|
||||
const id = props.shopId;
|
||||
const shop = core.status.shops[id] as ItemShopEvent;
|
||||
if (!shop.item) {
|
||||
throw new TypeError(
|
||||
`Wrong global shop type delivered in opening item shop.`
|
||||
);
|
||||
}
|
||||
|
||||
flags.itemShop ??= {};
|
||||
|
||||
const f = flags;
|
||||
|
||||
const choices = shop.choices;
|
||||
const selected = ref(0);
|
||||
const mode = ref<'sell' | 'buy'>('buy');
|
||||
const all = core.material.items;
|
||||
const cnt = core.itemCount;
|
||||
const count = ref(0);
|
||||
const nowMoney = ref(core.status.hero.money);
|
||||
const update = ref(false);
|
||||
|
||||
let bought = false;
|
||||
|
||||
watch(count, n => {
|
||||
if (n < 0) n = 0;
|
||||
if (mode.value === 'buy') {
|
||||
if (n > remain.value) n = remain.value;
|
||||
} else {
|
||||
const c = cnt(item.value.id);
|
||||
if (n > c) n = c;
|
||||
}
|
||||
count.value = n;
|
||||
});
|
||||
|
||||
const item = computed(() => {
|
||||
return choices[selected.value];
|
||||
});
|
||||
const remain = computed(() => {
|
||||
update.value;
|
||||
return item.value.number - (flags.itemShop[id]?.[item.value.id] ?? 0);
|
||||
});
|
||||
const info = computed(() => {
|
||||
return core.material.items[item.value.id];
|
||||
});
|
||||
const desc = computed(() => {
|
||||
const text = info.value.text!;
|
||||
const res = splitText([text.startsWith('!!html') ? text.slice(6) : text]);
|
||||
return res;
|
||||
});
|
||||
const cost = computed(() => {
|
||||
return (
|
||||
count.value *
|
||||
parseInt(mode.value === 'buy' ? item.value.money : item.value.sell)
|
||||
);
|
||||
});
|
||||
|
||||
watch(remain, n => {
|
||||
if (n < count.value) {
|
||||
count.value = n;
|
||||
}
|
||||
});
|
||||
|
||||
function confirm() {
|
||||
if (count.value === 0) {
|
||||
return;
|
||||
}
|
||||
const money = core.status.hero.money;
|
||||
bought = true;
|
||||
if (mode.value === 'buy') {
|
||||
if (cost.value <= money) {
|
||||
core.getItem(item.value.id, count.value);
|
||||
core.status.hero.money -= cost.value;
|
||||
nowMoney.value -= cost.value;
|
||||
flags.itemShop[id] ??= {};
|
||||
flags.itemShop[id][item.value.id] ??= 0;
|
||||
flags.itemShop[id][item.value.id] += count.value;
|
||||
tip('success', `成功购买${count.value}个${info.value.name}!`);
|
||||
core.status.route.push(`buy:${item.value.id}:${count.value}`);
|
||||
} else {
|
||||
tip('error', '你的金币不够!');
|
||||
}
|
||||
} else {
|
||||
core.addItem(item.value.id, -count.value);
|
||||
core.status.hero.money += cost.value;
|
||||
nowMoney.value += cost.value;
|
||||
flags.itemShop[id] ??= {};
|
||||
flags.itemShop[id][item.value.id] ??= 0;
|
||||
flags.itemShop[id][item.value.id] -= count.value;
|
||||
tip('success', `成功卖出${count.value}个${info.value.name}!`);
|
||||
core.status.route.push(`sell:${item.value.id}:${count.value}`);
|
||||
}
|
||||
count.value = 0;
|
||||
update.value = !update.value;
|
||||
}
|
||||
|
||||
gameKey.use(props.ui.symbol);
|
||||
gameKey
|
||||
.realize('@shop_up', () => {
|
||||
if (selected.value >= 1) {
|
||||
selected.value--;
|
||||
}
|
||||
})
|
||||
.realize('@shop_down', () => {
|
||||
if (selected.value <= choices.length - 2) {
|
||||
selected.value++;
|
||||
}
|
||||
})
|
||||
.realize(
|
||||
'@shop_add',
|
||||
() => {
|
||||
count.value++;
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@shop_min',
|
||||
() => {
|
||||
count.value--;
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize('exit', () => {
|
||||
exit();
|
||||
})
|
||||
.realize('confirm', () => {
|
||||
confirm();
|
||||
})
|
||||
.realize('shop', () => {
|
||||
exit();
|
||||
});
|
||||
|
||||
function exit() {
|
||||
if (bought) core.status.route.push('closeShop');
|
||||
mainUi.close(props.num);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
core.lockControl();
|
||||
core.status.route.push(`openShop:${id}`);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
core.unlockControl();
|
||||
gameKey.dispose(props.ui.symbol);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#shop {
|
||||
width: 90vh;
|
||||
height: 90vh;
|
||||
font-family: 'normal';
|
||||
font-size: 150%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#tools {
|
||||
height: 5vh;
|
||||
font-size: 3.2vh;
|
||||
}
|
||||
|
||||
#item-name {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 140%;
|
||||
height: 5vh;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
#item-info {
|
||||
height: 24vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#divider {
|
||||
width: 100%;
|
||||
margin: 1vh 0 1vh 0;
|
||||
}
|
||||
|
||||
#divider-split {
|
||||
margin: 1vh 0 0 0;
|
||||
}
|
||||
|
||||
#divider-vertical {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#shop-bottom {
|
||||
height: 53vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#item-list {
|
||||
height: 53vh;
|
||||
width: 53vh;
|
||||
padding: 1vh 0 1vh 0;
|
||||
}
|
||||
|
||||
#item-sell-info {
|
||||
width: 47vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
span {
|
||||
margin-left: 5%;
|
||||
}
|
||||
}
|
||||
|
||||
.item-one {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
|
||||
#shop-mode {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
height: 5vh;
|
||||
}
|
||||
|
||||
.mode-button {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#divider-mode {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.item-sell-info {
|
||||
width: 60%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin: 1vh 0;
|
||||
transition: color 0.2s linear;
|
||||
}
|
||||
|
||||
#sell-count {
|
||||
width: 100%;
|
||||
padding-top: 1vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#sell-count-select {
|
||||
margin-top: 1vh;
|
||||
width: 90%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
#sell-confirm {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
#sell-total {
|
||||
transition: color 0.2s linear;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#shop {
|
||||
width: 90vw;
|
||||
padding-top: 5vh;
|
||||
font-size: 225%;
|
||||
}
|
||||
|
||||
#item-list {
|
||||
width: 40vw;
|
||||
}
|
||||
|
||||
#shop-bottom {
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
#item-sell-info {
|
||||
width: 50vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,81 +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 { has } from '../plugin/utils';
|
||||
import Column from '../components/colomn.vue';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
}>();
|
||||
|
||||
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 (
|
||||
(has(a[i - 1]) &&
|
||||
v !== '<br>' &&
|
||||
a[i - 1] === '<br>') ||
|
||||
i === 0
|
||||
) {
|
||||
return `${' '.repeat(8)}${v}`;
|
||||
} else return v;
|
||||
})
|
||||
.join('')
|
||||
.replace(
|
||||
/level:(\d+)/g,
|
||||
'Mota.Plugin.require("skillTree_g").getSkillLevel($1)'
|
||||
) +
|
||||
'`'
|
||||
);
|
||||
});
|
||||
|
||||
function exit() {
|
||||
mainUi.close(props.num);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#skill-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.skill-item[selectable='false'] {
|
||||
color: gray;
|
||||
}
|
||||
</style>
|
||||
@@ -1,452 +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>
|
||||
<a-divider dashed style="border-color: #ddd4" id="divider"></a-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>
|
||||
<a-divider
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
id="divider-split"
|
||||
></a-divider>
|
||||
<div id="skill-bottom">
|
||||
<canvas id="skill-canvas"></canvas>
|
||||
<a-divider
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
:type="isMobile ? 'horizontal' : 'vertical'"
|
||||
id="divider-vertical"
|
||||
></a-divider>
|
||||
<div id="skill-upgrade-info">
|
||||
<div id="skill-upgrade-up">
|
||||
<span id="skill-level"
|
||||
>当前等级:{{ level }} / {{ skill.max }}</span
|
||||
>
|
||||
<a-divider dashed class="upgrade-divider"></a-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
|
||||
>
|
||||
<a-divider dashed class="upgrade-divider"></a-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">
|
||||
<a-divider dashed class="upgrade-divider"></a-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 { has, splitText, tip } from '../plugin/utils';
|
||||
import { isMobile } from '../plugin/use';
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import type { Chapter } from '@/plugin/game/skillTree';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
}>();
|
||||
|
||||
const skillTree = Mota.Plugin.require('skillTree_g');
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
let ctx: CanvasRenderingContext2D;
|
||||
|
||||
const selected = ref(0);
|
||||
const chapter = ref<Chapter>('chapter1');
|
||||
const update = ref(false);
|
||||
|
||||
const chapterDict = {
|
||||
chapter1: '第一章',
|
||||
chapter2: '第二章'
|
||||
};
|
||||
|
||||
flags.skillTree ??= 0;
|
||||
|
||||
const s = Mota.Plugin.require('skillTree_g').skills;
|
||||
|
||||
const chapterList = Object.keys(s) as Chapter[];
|
||||
|
||||
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() {
|
||||
mainUi.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 (has(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,677 +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, keycode } from '../plugin/utils';
|
||||
import { triggerFullscreen } from '../plugin/utils';
|
||||
import { isMobile } from '../plugin/use';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import { CustomToolbar } from '@/core/main/custom/toolbar';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { mat4 } from 'gl-matrix';
|
||||
import { bgmController } from '@/module';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
}>();
|
||||
|
||||
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;
|
||||
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') {
|
||||
mainUi.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 = 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;
|
||||
CustomToolbar.closeAll();
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
|
||||
soundChecked.value = mainSetting.getValue('audio.bgmEnabled', true);
|
||||
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,393 +0,0 @@
|
||||
<template>
|
||||
<div id="status-bar">
|
||||
<Box
|
||||
:resizable="true"
|
||||
:dragable="true"
|
||||
v-model:width="width"
|
||||
v-model:height="height"
|
||||
>
|
||||
<Scroll
|
||||
id="status-main"
|
||||
v-model:update="updateStatus"
|
||||
:no-scroll="true"
|
||||
>
|
||||
<div id="status-div">
|
||||
<span
|
||||
id="status-floor"
|
||||
@click.stop="viewMap"
|
||||
class="button-text"
|
||||
>{{ floor }}</span
|
||||
>
|
||||
<span id="status-lv">{{ lvName }}</span>
|
||||
<div id="status-skill" class="status-item">
|
||||
<img :src="imgs['skill.png'].src" class="status-icon" />
|
||||
<span>{{ skill }}</span>
|
||||
<span
|
||||
v-if="has(spring)"
|
||||
id="status-spring"
|
||||
class="status-extra"
|
||||
>剩余{{ spring }}</span
|
||||
>
|
||||
</div>
|
||||
<div id="status-hp" class="status-item">
|
||||
<img :src="imgs['hp.png'].src" class="status-icon" />
|
||||
<span class="status-item-bold">{{
|
||||
format(hero.hp!)
|
||||
}}</span>
|
||||
<span
|
||||
id="status-hpmax"
|
||||
class="status-extra status-item-bold"
|
||||
>+{{
|
||||
format(Math.min(hero.hpmax!, hero.def! / 10))
|
||||
}}/t</span
|
||||
>
|
||||
<span
|
||||
v-if="jumpCnt !== -1"
|
||||
id="status-jump"
|
||||
class="status-extra"
|
||||
>跳跃剩余{{ jumpCnt }}</span
|
||||
>
|
||||
</div>
|
||||
<div id="status-atk" class="status-item">
|
||||
<img :src="imgs['atk.png'].src" class="status-icon" />
|
||||
<span class="status-item-bold">{{
|
||||
format(hero.atk!)
|
||||
}}</span>
|
||||
<span
|
||||
id="status-mana"
|
||||
class="status-extra status-item-bold"
|
||||
>+{{ format(hero.mana!) }}</span
|
||||
>
|
||||
</div>
|
||||
<div id="status-def" class="status-item status-item-bold">
|
||||
<img :src="imgs['def.png'].src" class="status-icon" />
|
||||
<span>{{ format(hero.def!) }}</span>
|
||||
<span
|
||||
v-if="hero.magicDef! > 0"
|
||||
id="status-magic-def"
|
||||
class="status-extra status-item-bold"
|
||||
>+{{ format(hero.magicDef!) }}</span
|
||||
>
|
||||
</div>
|
||||
<div id="status-mdef" class="status-item status-item-bold">
|
||||
<img :src="imgs['IQ.png'].src" class="status-icon" />
|
||||
<span>{{ format(hero.mdef!) }}</span>
|
||||
</div>
|
||||
<div id="status-money" class="status-item status-item-bold">
|
||||
<img :src="imgs['money.png'].src" class="status-icon" />
|
||||
<span>{{ format(hero.money!) }}</span>
|
||||
</div>
|
||||
<div id="status-exp" class="status-item status-item-bold">
|
||||
<img :src="imgs['exp.png'].src" class="status-icon" />
|
||||
<span>{{ format(up) }}</span>
|
||||
</div>
|
||||
<div id="status-key" class="status-item status-item-bold">
|
||||
<span style="color: #fca; padding-left: 10%">{{
|
||||
keys[0]?.toString().padStart(2, '0')
|
||||
}}</span>
|
||||
<span style="color: #aad">{{
|
||||
keys[1]?.toString().padStart(2, '0')
|
||||
}}</span>
|
||||
<span style="color: #f88; padding-right: 10%">{{
|
||||
keys[2]?.toString().padStart(2, '0')
|
||||
}}</span>
|
||||
</div>
|
||||
<div v-if="skillOpened" class="status-item">
|
||||
<span
|
||||
id="skill-tree"
|
||||
class="button-text"
|
||||
@click.stop="openSkillTree"
|
||||
>技能树</span
|
||||
>
|
||||
</div>
|
||||
<div v-if="skillOpened" class="status-item">
|
||||
<span
|
||||
id="status-skill"
|
||||
class="button-text"
|
||||
@click.stop="openSkill"
|
||||
>查看技能</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, shallowReactive, watch } from 'vue';
|
||||
import Box from '../components/box.vue';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import { status } from '../plugin/ui/statusBar';
|
||||
import { isMobile } from '../plugin/use';
|
||||
import { fontSize } from '../plugin/ui/statusBar';
|
||||
import { has } from '@/plugin/utils';
|
||||
|
||||
let main: HTMLDivElement;
|
||||
|
||||
const imgs = core.material.images.images;
|
||||
|
||||
const width = ref(
|
||||
isMobile ? window.innerWidth - 60 : window.innerWidth * 0.175
|
||||
);
|
||||
const height = ref(isMobile ? 250 : window.innerHeight - 100);
|
||||
const updateStatus = ref(false);
|
||||
const format = core.formatBigNumber;
|
||||
|
||||
watch(width, n => (updateStatus.value = !updateStatus.value));
|
||||
watch(height, n => (updateStatus.value = !updateStatus.value));
|
||||
watch(fontSize, n => (main.style.fontSize = `${isMobile ? n * 1.5 : n}%`));
|
||||
|
||||
const HeroSkill = Mota.require('module', 'Mechanism').HeroSkill;
|
||||
|
||||
const hero = shallowReactive<Partial<HeroStatus>>({});
|
||||
const keys = shallowReactive<number[]>([]);
|
||||
const floor = ref<string>();
|
||||
const lvName = ref<string>();
|
||||
const skill = ref<string>(HeroSkill.getAutoSkill() ? '自动切换' : '无');
|
||||
const up = ref(0);
|
||||
const spring = ref<number>();
|
||||
const skillOpened = ref(core.getFlag('chapter', 0) > 0);
|
||||
const jumpCnt = ref<number>();
|
||||
/**
|
||||
* 要展示的勇士属性
|
||||
*/
|
||||
const toShow: (keyof HeroStatus)[] = [
|
||||
'hp', // 生命
|
||||
'atk', // 攻击
|
||||
'def', // 防御
|
||||
'mdef', // 智力
|
||||
'hpmax', // 生命回复
|
||||
'mana', // 额外攻击
|
||||
'money', // 金币
|
||||
'exp', // 经验
|
||||
'lv', // 等级
|
||||
'magicDef' // 魔法防御
|
||||
];
|
||||
|
||||
watch(status, update);
|
||||
|
||||
/**
|
||||
* 更新显示内容
|
||||
*/
|
||||
function update() {
|
||||
toShow.forEach(v => {
|
||||
hero[v] = Mota.requireAll('fn').getHeroStatusOn(v);
|
||||
});
|
||||
keys[0] = core.itemCount('yellowKey');
|
||||
keys[1] = core.itemCount('blueKey');
|
||||
keys[2] = core.itemCount('redKey');
|
||||
floor.value = core.status.thisMap?.title;
|
||||
lvName.value = core.getLvName(hero.lv);
|
||||
|
||||
if (HeroSkill.getAutoSkill()) {
|
||||
skill.value = '自动切换';
|
||||
} else {
|
||||
const enabled = HeroSkill.getEnabled();
|
||||
switch (enabled) {
|
||||
case HeroSkill.Blade:
|
||||
skill.value = '断灭之刃';
|
||||
break;
|
||||
case HeroSkill.Shield:
|
||||
skill.value = '铸剑为盾';
|
||||
break;
|
||||
default:
|
||||
skill.value = '无';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
up.value = core.getNextLvUpNeed() ?? 0;
|
||||
if (core.hasFlag('spring')) {
|
||||
spring.value = 50 - (flags.springCount ?? 0);
|
||||
} else {
|
||||
spring.value = void 0;
|
||||
}
|
||||
skillOpened.value = core.getFlag('chapter', 0) > 0;
|
||||
|
||||
jumpCnt.value =
|
||||
!HeroSkill.learnedSkill(HeroSkill.Jump) ||
|
||||
Mota.Plugin.require('skill_g').jumpIgnoreFloor.has(core.status.floorId)
|
||||
? -1
|
||||
: 3 - (flags[`jump_${core.status.floorId}`] ?? 0);
|
||||
}
|
||||
|
||||
function openSkillTree() {
|
||||
core.useItem('skill1');
|
||||
}
|
||||
|
||||
function openSkill() {
|
||||
core.useItem('cross');
|
||||
}
|
||||
|
||||
function viewMap() {
|
||||
core.ui._drawViewMaps();
|
||||
}
|
||||
|
||||
function openStudy() {}
|
||||
|
||||
function resize() {
|
||||
requestAnimationFrame(() => {
|
||||
main.style.fontSize = `${
|
||||
isMobile ? fontSize.value * 1.5 : fontSize.value
|
||||
}%`;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
update();
|
||||
main = document.getElementById('status-main') as HTMLDivElement;
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', resize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#status-main {
|
||||
background-color: #0009;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1vh 0;
|
||||
font-size: v-bind(fontSize);
|
||||
}
|
||||
|
||||
.status-item {
|
||||
position: relative;
|
||||
max-width: 17.5vw;
|
||||
font-size: 200%;
|
||||
width: 100%;
|
||||
margin-bottom: 14px;
|
||||
text-shadow:
|
||||
3px 2px 3px #000,
|
||||
0px 0px 3px #111;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-item-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 2.8vw;
|
||||
height: 2.8vw;
|
||||
margin-right: 10%;
|
||||
margin-left: 10%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
#status-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#status-div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#status-floor {
|
||||
max-width: 17.5vw;
|
||||
font-size: 200%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
text-shadow:
|
||||
3px 2px 3px #000,
|
||||
0px 0px 3px #111;
|
||||
}
|
||||
|
||||
#status-lv {
|
||||
max-width: 17.5vw;
|
||||
font-size: 200%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
text-shadow:
|
||||
3px 2px 3px #000,
|
||||
0px 0px 3px #111;
|
||||
}
|
||||
|
||||
.status-extra {
|
||||
position: absolute;
|
||||
right: 10%;
|
||||
bottom: 0;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
#status-mana {
|
||||
line-height: 0;
|
||||
color: rgb(255, 211, 211);
|
||||
}
|
||||
|
||||
#status-magic-def {
|
||||
line-height: 0;
|
||||
color: rgb(176, 189, 255);
|
||||
}
|
||||
|
||||
#status-hpmax {
|
||||
line-height: 0;
|
||||
color: rgb(167, 255, 167);
|
||||
}
|
||||
|
||||
#status-spring {
|
||||
line-height: 0;
|
||||
color: rgb(167, 255, 167);
|
||||
top: 0;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
#status-jump {
|
||||
line-height: 0;
|
||||
top: 0;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
#status-key {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
#skill-tree,
|
||||
#status-skill {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.status-item {
|
||||
max-width: 40vw;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
#status-floor {
|
||||
max-width: 40vw;
|
||||
font-size: 120%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#status-lv {
|
||||
max-width: 40vw;
|
||||
font-size: 120%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
</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,586 +0,0 @@
|
||||
<template>
|
||||
<div id="tools">
|
||||
<span class="button-text" @click="exit"><left-outlined /> 返回</span>
|
||||
</div>
|
||||
<div id="tool-editor">
|
||||
<div id="tool-left">
|
||||
<Scroll class="tool-list-scroll">
|
||||
<div id="tool-list">
|
||||
<div
|
||||
v-for="(item, i) of list"
|
||||
class="tool-list-item selectable"
|
||||
:selected="i === selected"
|
||||
@click="selected = i"
|
||||
>
|
||||
<span>{{ item.id }}</span>
|
||||
<a-button
|
||||
type="danger"
|
||||
class="tool-list-delete"
|
||||
@click.stop="deleteTool(item.id)"
|
||||
>删除</a-button
|
||||
>
|
||||
</div>
|
||||
<div id="tool-list-add">
|
||||
<div
|
||||
id="tool-add-div"
|
||||
@click="addingTool = true"
|
||||
v-if="!addingTool"
|
||||
>
|
||||
<PlusOutlined></PlusOutlined>
|
||||
<span>新增工具栏</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<a-input
|
||||
style="
|
||||
height: 100%;
|
||||
font-size: 80%;
|
||||
width: 100%;
|
||||
"
|
||||
v-model:value="addingToolId"
|
||||
@blur="addTool"
|
||||
></a-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Scroll>
|
||||
<a-divider
|
||||
type="vertical"
|
||||
dashed
|
||||
v-if="isMobile"
|
||||
style="height: 100%; border-color: #ddd4"
|
||||
></a-divider>
|
||||
<div id="tool-preview" v-if="!!bar && isMobile">
|
||||
<div id="tool-preview-container">
|
||||
<div class="tool-preview-item" v-for="item of bar.items">
|
||||
<component
|
||||
:is="CustomToolbar.info[item.type].show as any"
|
||||
:item="item"
|
||||
:toolbar="bar"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider
|
||||
class="divider"
|
||||
dashed
|
||||
style="border-color: #ddd4"
|
||||
:type="isMobile ? 'horizontal' : 'vertical'"
|
||||
></a-divider>
|
||||
<div id="tool-info">
|
||||
<div id="tool-detail" v-if="!!bar">
|
||||
<Scroll class="tool-item-list-scroll">
|
||||
<div class="tool-item-list">
|
||||
<div
|
||||
class="tool-item-list-item"
|
||||
v-for="item of bar.items"
|
||||
>
|
||||
<div
|
||||
class="tool-item-header"
|
||||
:folded="!unfolded[item.id]"
|
||||
@click="triggerFold(item.id)"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="tool-fold"
|
||||
:folded="!unfolded[item.id]"
|
||||
>
|
||||
<RightOutlined></RightOutlined>
|
||||
</span>
|
||||
<span class="tool-name">
|
||||
<span
|
||||
v-if="editId !== item.id"
|
||||
style="cursor: text"
|
||||
@click.stop="editName(item.id)"
|
||||
>
|
||||
{{ item.id }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<a-input
|
||||
@blur="editNameSuccess(item.id)"
|
||||
@click.stop=""
|
||||
v-model:value="editValue"
|
||||
class="tool-name-edit"
|
||||
></a-input>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<a-button
|
||||
type="danger"
|
||||
class="tool-item-delete"
|
||||
@click.stop="deleteItem(item)"
|
||||
>删除</a-button
|
||||
>
|
||||
</div>
|
||||
<div v-if="!!unfolded[item.id]">
|
||||
<component
|
||||
:is="CustomToolbar.info[item.type].editor"
|
||||
:item="item"
|
||||
:toolbar="bar"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tool-item-add">
|
||||
<div
|
||||
id="tool-item-add-div"
|
||||
v-if="!addingItem"
|
||||
@click="addingItem = true"
|
||||
>
|
||||
<PlusOutlined></PlusOutlined>
|
||||
<span>新增工具</span>
|
||||
</div>
|
||||
<div id="tool-item-add-type" v-else>
|
||||
<span>工具类型</span>
|
||||
<a-select
|
||||
v-model:value="addingType"
|
||||
style="
|
||||
width: 120px;
|
||||
height: 100%;
|
||||
font-size: 80%;
|
||||
background-color: #222;
|
||||
"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(
|
||||
info, type
|
||||
) of CustomToolbar.info"
|
||||
:value="type"
|
||||
>{{ info.name }}</a-select-option
|
||||
>
|
||||
</a-select>
|
||||
<a-button
|
||||
type="primary"
|
||||
style="font-size: 80%; height: 100%"
|
||||
@click="addItem(true)"
|
||||
>确定</a-button
|
||||
>
|
||||
<a-button
|
||||
@click="addItem(false)"
|
||||
style="
|
||||
background-color: #222;
|
||||
font-size: 80%;
|
||||
height: 100%;
|
||||
"
|
||||
>取消</a-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Scroll>
|
||||
</div>
|
||||
<a-divider dashed v-if="!isMobile"></a-divider>
|
||||
<div id="tool-preview" v-if="!!bar && !isMobile">
|
||||
<div id="tool-preview-container">
|
||||
<div class="tool-preview-item" v-for="item of bar.items">
|
||||
<component
|
||||
:is="CustomToolbar.info[item.type].show as any"
|
||||
:item="item"
|
||||
:toolbar="bar"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { CustomToolbar } from '@/core/main/custom/toolbar';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { computed, onUnmounted, reactive, ref } from 'vue';
|
||||
import {
|
||||
PlusOutlined,
|
||||
RightOutlined,
|
||||
LeftOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
import { isMobile } from '@/plugin/use';
|
||||
import Scroll from '@/components/scroll.vue';
|
||||
import { deleteWith, tip } from '@/plugin/utils';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { ToolbarItemType } from '@/core/main/init/toolbar';
|
||||
|
||||
const props = defineProps<{
|
||||
ui: GameUi;
|
||||
num: number;
|
||||
}>();
|
||||
|
||||
const scale = mainSetting.getValue('ui.toolbarScale', 100) / 100;
|
||||
|
||||
const list = CustomToolbar.list;
|
||||
|
||||
const selected = ref(0);
|
||||
const bar = computed(() => list[selected.value]);
|
||||
|
||||
const unfolded = reactive<Record<string, boolean>>({});
|
||||
const editId = ref<string>();
|
||||
const editValue = ref<string>();
|
||||
|
||||
// 添加自定义工具
|
||||
const addingItem = ref(false);
|
||||
const addingType = ref<ToolbarItemType>('item');
|
||||
|
||||
// 添加自定义工具栏
|
||||
const addingTool = ref(false);
|
||||
const addingToolId = ref('');
|
||||
|
||||
/**
|
||||
* 编辑名称
|
||||
*/
|
||||
function editName(id: string) {
|
||||
editId.value = id;
|
||||
editValue.value = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑名称完成
|
||||
*/
|
||||
function editNameSuccess(id: string) {
|
||||
if (bar.value.items.some(v => v.id === editId.value)) {
|
||||
tip('error', '名称重复!');
|
||||
} else {
|
||||
const item = bar.value.items.find(v => v.id === id)!;
|
||||
item.id = editValue.value!;
|
||||
}
|
||||
editId.value = void 0;
|
||||
editValue.value = void 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除自定义工具
|
||||
*/
|
||||
function deleteItem(item: any) {
|
||||
Modal.confirm({
|
||||
title: '确定要删除这个自定义工具吗?',
|
||||
onOk() {
|
||||
deleteWith(bar.value.items, item);
|
||||
},
|
||||
onCancel() {}
|
||||
});
|
||||
}
|
||||
|
||||
function addItem(add: boolean) {
|
||||
if (add) {
|
||||
bar.value.add(
|
||||
// @ts-ignore
|
||||
CustomToolbar.info[addingType.value].onCreate({
|
||||
id: `tool-item-${bar.value.items.length}`,
|
||||
type: addingType.value
|
||||
})
|
||||
);
|
||||
}
|
||||
addingItem.value = false;
|
||||
addingType.value = 'item';
|
||||
}
|
||||
|
||||
/**
|
||||
* 更改折叠
|
||||
*/
|
||||
function triggerFold(id: string) {
|
||||
unfolded[id] = !unfolded[id];
|
||||
}
|
||||
|
||||
function exit() {
|
||||
mainUi.close(props.num);
|
||||
}
|
||||
|
||||
function deleteTool(id: string) {
|
||||
Modal.confirm({
|
||||
title: '确定要删除这个自定义工具栏吗?',
|
||||
onOk() {
|
||||
const index = CustomToolbar.list.findIndex(v => v.id === id);
|
||||
if (index !== -1) {
|
||||
CustomToolbar.list[index].closeAll();
|
||||
CustomToolbar.list.splice(index, 1);
|
||||
}
|
||||
selected.value = 0;
|
||||
},
|
||||
onCancel() {}
|
||||
});
|
||||
}
|
||||
|
||||
function addTool() {
|
||||
if (addingToolId.value === '') {
|
||||
addingToolId.value = '';
|
||||
addingTool.value = false;
|
||||
return;
|
||||
}
|
||||
if (CustomToolbar.list.some(v => v.id === addingToolId.value)) {
|
||||
tip('error', '工具栏名称重复!');
|
||||
return;
|
||||
} else {
|
||||
const bar = new CustomToolbar(addingToolId.value);
|
||||
}
|
||||
addingToolId.value = '';
|
||||
addingTool.value = false;
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
CustomToolbar.save();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#tools {
|
||||
width: 100%;
|
||||
font-family: 'normal';
|
||||
font-size: 3.2vh;
|
||||
height: 5vh;
|
||||
position: fixed;
|
||||
left: 10vw;
|
||||
top: 5vh;
|
||||
}
|
||||
|
||||
#tool-editor {
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'normal';
|
||||
font-size: 150%;
|
||||
user-select: none;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.tool-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tool-list-scroll {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#tool-left {
|
||||
flex-basis: 30%;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#tool-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#tool-list-add {
|
||||
margin-top: 5%;
|
||||
padding: 1% 3% 1% 3%;
|
||||
|
||||
#tool-add-div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
padding: 1% 4%;
|
||||
transition: background-color linear 0.1s;
|
||||
border-radius: 5px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#tool-add-div:hover {
|
||||
background-color: rgba(39, 251, 209, 0.316);
|
||||
}
|
||||
|
||||
#tool-add-div:active {
|
||||
background-color: rgba(39, 251, 209, 0.202);
|
||||
}
|
||||
}
|
||||
|
||||
.tool-list-delete {
|
||||
font-size: 80%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#tool-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
#tool-detail {
|
||||
height: 60%;
|
||||
|
||||
.tool-item-header {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tool-item-header[folded='false'] {
|
||||
border-bottom: 0.5px solid #888;
|
||||
}
|
||||
|
||||
.tool-item-list-scroll {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tool-item-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tool-item-list-item {
|
||||
border: 1px solid #ddd8;
|
||||
margin-bottom: 3%;
|
||||
background-color: #222;
|
||||
padding-left: 2%;
|
||||
}
|
||||
|
||||
.tool-fold {
|
||||
::v-deep(span) {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-fold[folded='false'] {
|
||||
::v-deep(span) {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.tool-name {
|
||||
margin-left: 3%;
|
||||
width: 40%;
|
||||
display: flex;
|
||||
|
||||
.tool-name-edit {
|
||||
width: 100%;
|
||||
font-size: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-item-delete {
|
||||
height: 100%;
|
||||
justify-self: end;
|
||||
font-size: 80%;
|
||||
padding: 2px 15px;
|
||||
}
|
||||
|
||||
#tool-item-add-div {
|
||||
padding: 1% 3% 1% 3%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
padding: 1% 4%;
|
||||
transition: background-color linear 0.1s;
|
||||
border-radius: 5px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#tool-item-add-div:hover {
|
||||
background-color: rgba(39, 251, 209, 0.316);
|
||||
}
|
||||
|
||||
#tool-item-add-div:active {
|
||||
background-color: rgba(39, 251, 209, 0.202);
|
||||
}
|
||||
|
||||
#tool-item-add-type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
padding: 1% 4%;
|
||||
justify-content: space-between;
|
||||
border-radius: 5px;
|
||||
background-color: rgba(39, 251, 209, 0.316);
|
||||
}
|
||||
}
|
||||
|
||||
#tool-preview {
|
||||
height: 40%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
|
||||
#tool-preview-container {
|
||||
width: 90%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border: 2px solid #ddd9;
|
||||
background-color: #0009;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tool-preview-item {
|
||||
display: flex;
|
||||
margin: v-bind('5 * scale + "px"');
|
||||
min-width: v-bind('50 * scale + "px"');
|
||||
min-height: v-bind('50 * scale + "px"');
|
||||
background-color: #222;
|
||||
border: 1.5px solid #ddd8;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.1s linear;
|
||||
|
||||
::v-deep(*) {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#tool-editor {
|
||||
padding-top: 15%;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#tool-left {
|
||||
width: 100%;
|
||||
flex-basis: 40%;
|
||||
max-height: 40vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.tool-list-scroll {
|
||||
height: 100%;
|
||||
flex-basis: 50%;
|
||||
}
|
||||
|
||||
#tool-preview {
|
||||
flex-basis: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#tool-info {
|
||||
width: 100%;
|
||||
flex-basis: 60%;
|
||||
|
||||
#tool-detail {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,138 +0,0 @@
|
||||
<template>
|
||||
<Box
|
||||
class="toolbar-container"
|
||||
:dragable="true"
|
||||
:resizable="true"
|
||||
v-model:left="box.x"
|
||||
v-model:top="box.y"
|
||||
v-model:height="box.height"
|
||||
v-model:width="box.width"
|
||||
:key="num"
|
||||
>
|
||||
<div class="toolbar">
|
||||
<div
|
||||
class="toolbar-item"
|
||||
v-for="item of bar.items"
|
||||
@click.stop="click"
|
||||
:noaction="!!item.noDefaultAction"
|
||||
>
|
||||
<component
|
||||
:is="(CustomToolbar.info[item.type].show as any)"
|
||||
:item="item"
|
||||
:toolbar="bar"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Box from '@/components/box.vue';
|
||||
import { CustomToolbar } from '@/core/main/custom/toolbar';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { mainSetting } from '@/core/main/setting';
|
||||
import { onUnmounted, reactive, watch } from 'vue';
|
||||
|
||||
interface BoxData {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
bar: CustomToolbar;
|
||||
}>();
|
||||
|
||||
const bar = props.bar;
|
||||
const scale = mainSetting.getValue('ui.toolbarScale', 100) / 100;
|
||||
|
||||
const box = reactive<BoxData>({
|
||||
x: bar.x,
|
||||
y: bar.y,
|
||||
width: bar.width,
|
||||
height: bar.height
|
||||
});
|
||||
|
||||
watch(box, ({ x, y, width, height }) => {
|
||||
bar.x = x;
|
||||
bar.y = y;
|
||||
bar.width = width;
|
||||
bar.height = height;
|
||||
});
|
||||
|
||||
function click() {
|
||||
// pass
|
||||
}
|
||||
|
||||
function posChange() {
|
||||
box.x = bar.x;
|
||||
box.y = bar.y;
|
||||
box.width = bar.width;
|
||||
box.height = bar.height;
|
||||
}
|
||||
|
||||
bar.on('posChange', posChange);
|
||||
|
||||
onUnmounted(() => {
|
||||
bar.off('posChange', posChange);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.toolbar-container {
|
||||
background-color: #0009;
|
||||
padding: v-bind('scale * 5 + "px"');
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.toolbar-item {
|
||||
display: flex;
|
||||
margin: v-bind('scale * 5 + "px"');
|
||||
min-width: v-bind('scale * 50 + "px"');
|
||||
min-height: v-bind('scale * 50 + "px"');
|
||||
background-color: #222;
|
||||
border: 1.5px solid #ddd8;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: all 0.1s linear;
|
||||
}
|
||||
|
||||
.toolbar-item[noaction='false'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar-item::v-deep(> *) {
|
||||
height: 100%;
|
||||
min-width: v-bind('scale * 50 + "px"');
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
text-overflow: clip;
|
||||
text-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar-item::v-deep(.button-text)[active='true'] {
|
||||
color: gold;
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.toolbar-item:hover[noaction='false'] {
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
.toolbar-item:active[noaction='false'] {
|
||||
background-color: #333;
|
||||
}
|
||||
</style>
|
||||
@@ -1,440 +0,0 @@
|
||||
<template>
|
||||
<div id="toolbox">
|
||||
<div id="tools">
|
||||
<span class="button-text tools" @click="exit"
|
||||
><left-outlined /> 返回游戏</span
|
||||
>
|
||||
<span class="button-text tools" @click="toEquip"
|
||||
>装备栏 <right-outlined
|
||||
/></span>
|
||||
</div>
|
||||
<div id="toolbox-main">
|
||||
<div v-for="cls of toShow" class="item-main">
|
||||
<div class="item-info">
|
||||
<div class="item-type" v-if="!isMobile">
|
||||
{{ getClsName(cls) }}
|
||||
</div>
|
||||
<div v-else id="item-type-mobile">
|
||||
<span
|
||||
class="button-text"
|
||||
@click="mode = 'tools'"
|
||||
:selected="mode === 'tools'"
|
||||
>消耗道具</span
|
||||
>
|
||||
<a-divider
|
||||
dashed
|
||||
style="border-color: #ddd4; height: 100%"
|
||||
type="vertical"
|
||||
></a-divider>
|
||||
<span
|
||||
class="button-text"
|
||||
@click="mode = 'constants'"
|
||||
:selected="mode === 'constants'"
|
||||
>永久道具</span
|
||||
>
|
||||
</div>
|
||||
<a-divider
|
||||
dashed
|
||||
style="margin: 1vh 0 1vh 0; border-color: #ddd4"
|
||||
></a-divider>
|
||||
<Scroll class="item-list">
|
||||
<div
|
||||
class="item selectable"
|
||||
v-for="[id, num] of items[cls]"
|
||||
:selected="selected === id"
|
||||
@click="
|
||||
mode = cls;
|
||||
select(id);
|
||||
"
|
||||
>
|
||||
<div class="item-icon">
|
||||
<BoxAnimate
|
||||
:id="id"
|
||||
:width="32"
|
||||
:height="32"
|
||||
:noborder="true"
|
||||
></BoxAnimate>
|
||||
<span class="item-name">{{
|
||||
all[id].name
|
||||
}}</span>
|
||||
</div>
|
||||
<span>× {{ num }}</span>
|
||||
</div>
|
||||
</Scroll>
|
||||
</div>
|
||||
<a-divider
|
||||
dashed
|
||||
:type="isMobile ? 'horizontal' : 'vertical'"
|
||||
class="divider"
|
||||
style="border-color: #ddd4; margin: 1%"
|
||||
></a-divider>
|
||||
</div>
|
||||
<div id="detail">
|
||||
<div id="info">
|
||||
<BoxAnimate
|
||||
:id="selected"
|
||||
:width="32"
|
||||
:height="32"
|
||||
></BoxAnimate>
|
||||
<div id="basic-info">
|
||||
<span style="border-bottom: 1px solid #ddd4">{{
|
||||
selected === 'none'
|
||||
? '没有道具'
|
||||
: all[selected].name
|
||||
}}</span>
|
||||
<span>{{
|
||||
selected === 'none'
|
||||
? '永久道具'
|
||||
: getClsName(all[selected].cls as ItemMode) ??
|
||||
'永久道具'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span style="margin-top: 2vh">点击该物品以使用</span>
|
||||
<a-divider dashed style="border-color: #ddd4"></a-divider>
|
||||
<div id="desc">
|
||||
<span>道具描述</span>
|
||||
<Scroll id="desc-text">
|
||||
<div v-if="!descText.value!.startsWith('!!html')">
|
||||
{{ descText.value }}
|
||||
</div>
|
||||
<div v-else v-html="descText.value!.slice(6)"></div>
|
||||
</Scroll>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import Scroll from '../components/scroll.vue';
|
||||
import BoxAnimate from '../components/boxAnimate.vue';
|
||||
import { getClsName, getItems } from '../plugin/ui/toolbox';
|
||||
import { isMobile } from '../plugin/use';
|
||||
import { type, keycode, has } from '../plugin/utils';
|
||||
import { hyper, sleep } from 'mutate-animate';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { GameUi } from '@/core/main/custom/ui';
|
||||
import { gameKey } from '@/core/main/custom/hotkey';
|
||||
import { mainUi } from '@/core/main/init/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
num: number;
|
||||
ui: GameUi;
|
||||
}>();
|
||||
|
||||
type ItemMode = 'tools' | 'constants';
|
||||
type ShowItemIds = ItemIdOf<'constants' | 'tools'> | 'none';
|
||||
|
||||
const mode = ref<ItemMode>('tools');
|
||||
|
||||
const items = getItems('all');
|
||||
|
||||
const toShow = computed<ItemMode[]>(() => {
|
||||
return isMobile ? [mode.value] : ['tools', 'constants'];
|
||||
});
|
||||
|
||||
const all = core.material.items;
|
||||
|
||||
const selected = ref<ShowItemIds>(items[toShow.value[0]][0]?.[0] ?? 'none');
|
||||
const index = ref(0);
|
||||
|
||||
watch(index, n => {
|
||||
select(items[mode.value][n][0], true);
|
||||
});
|
||||
|
||||
watch(mode, n => {
|
||||
if (!has(items[n][index.value])) {
|
||||
selected.value = 'none';
|
||||
return;
|
||||
}
|
||||
select(items[n][index.value][0], true);
|
||||
});
|
||||
|
||||
const descText = computed(() => {
|
||||
const s = selected.value;
|
||||
if (s === 'none') return ref('没有选择道具');
|
||||
return type(all[s].text!, 25, hyper('sin', 'out'), true);
|
||||
});
|
||||
|
||||
/**
|
||||
* 选择一个道具时
|
||||
* @param id 道具id
|
||||
*/
|
||||
async function select(id: ShowItemIds, nouse: boolean = false) {
|
||||
if (selected.value === id && !nouse) {
|
||||
use(id);
|
||||
}
|
||||
index.value = items[mode.value].findIndex(v => v[0] === id);
|
||||
selected.value = id;
|
||||
}
|
||||
|
||||
function exit() {
|
||||
mainUi.close(props.num);
|
||||
}
|
||||
|
||||
function use(id: ShowItemIds) {
|
||||
if (id === 'none') return;
|
||||
if (core.canUseItem(id)) {
|
||||
const hold = mainUi.holdOn();
|
||||
exit();
|
||||
nextTick(() => {
|
||||
core.tryUseItem(id, false, () => {
|
||||
if (mainUi.stack.length === 0) {
|
||||
hold.end(core.status.event.id !== 'toolbox');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
message.warn({
|
||||
content: '当前无法使用该道具!',
|
||||
class: 'antdv-message'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function toEquip() {
|
||||
mainUi.holdOn();
|
||||
exit();
|
||||
nextTick(() => {
|
||||
mainUi.open('equipbox');
|
||||
});
|
||||
}
|
||||
|
||||
gameKey.use(props.ui.symbol);
|
||||
gameKey
|
||||
.realize(
|
||||
'@toolbox_right',
|
||||
() => {
|
||||
const constants = items.constants.length;
|
||||
if (mode.value === 'tools') {
|
||||
if (index.value >= constants) {
|
||||
index.value = constants - 1;
|
||||
}
|
||||
mode.value = 'constants';
|
||||
}
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@toolbox_left',
|
||||
() => {
|
||||
const constants = items.tools.length;
|
||||
if (mode.value === 'constants') {
|
||||
if (index.value >= constants) {
|
||||
index.value = constants - 1;
|
||||
}
|
||||
mode.value = 'tools';
|
||||
}
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@toolbox_up',
|
||||
() => {
|
||||
if (index.value > 0) {
|
||||
index.value--;
|
||||
}
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize(
|
||||
'@toolbox_down',
|
||||
() => {
|
||||
const total = items[mode.value].length;
|
||||
if (index.value < total - 1) {
|
||||
index.value++;
|
||||
}
|
||||
},
|
||||
{ type: 'down-repeat' }
|
||||
)
|
||||
.realize('exit', () => {
|
||||
exit();
|
||||
})
|
||||
.realize('confirm', () => {
|
||||
use(selected.value);
|
||||
})
|
||||
.realize('toolbox', () => {
|
||||
exit();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
gameKey.dispose(props.ui.symbol);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#toolbox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#toolbox-main {
|
||||
width: 100%;
|
||||
height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
font-family: 'normal';
|
||||
font-size: 150%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#tools {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-family: 'normal';
|
||||
font-size: 3.2vh;
|
||||
height: 5vh;
|
||||
justify-content: space-between;
|
||||
|
||||
.tools {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.item-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.item-info {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.item-list {
|
||||
width: 100%;
|
||||
height: 88vh;
|
||||
}
|
||||
|
||||
.item-type {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#item-type-mobile {
|
||||
width: 80%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-text[selected='true'] {
|
||||
color: aqua;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 1vh 0 0 0;
|
||||
padding: 0.5vh 0.5vw;
|
||||
border: #0000 0.5px solid;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
.item-icon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.item-name {
|
||||
margin-left: 5%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
#info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
#basic-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-left: 1px solid #ddd4;
|
||||
padding-left: 5%;
|
||||
margin-left: 10%;
|
||||
}
|
||||
}
|
||||
|
||||
#desc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-basis: 50%;
|
||||
|
||||
#desc-text {
|
||||
margin-top: 2vh;
|
||||
margin-left: 0.5vw;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
#toolbox-main {
|
||||
flex-direction: column-reverse;
|
||||
height: 90%;
|
||||
font-size: 225%;
|
||||
margin-top: 10%;
|
||||
}
|
||||
|
||||
.item-list {
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item-main {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
#detail {
|
||||
flex-basis: 30%;
|
||||
|
||||
#desc {
|
||||
#desc-text {
|
||||
max-height: 10vh;
|
||||
height: 10vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user