refactor: UI 移入 @motajs/legacy-ui

This commit is contained in:
AncTe committed 2025-03-05 11:25:12 +08:00
1 parent 4aa894a204
commit 693e531639
85 files changed
+940 -1252

No files matched your search

-413
View File
@@ -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>
-90
View File
@@ -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>
-144
View File
@@ -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>
-238
View File
@@ -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] }"
>&nbsp;{{ text[0] }}&nbsp;</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"
>生命&nbsp;&nbsp;&nbsp;&nbsp;{{
core.formatBigNumber(enemy.enemy.info.hp)
}}</span
>
</div>
<div class="detail-info">
<span style="color: lightcoral"
>攻击&nbsp;&nbsp;&nbsp;&nbsp;{{
core.formatBigNumber(enemy.enemy.info.atk)
}}</span
>
</div>
<div class="detail-info">
<span style="color: lightblue"
>防御&nbsp;&nbsp;&nbsp;&nbsp;{{
core.formatBigNumber(enemy.enemy.info.def)
}}</span
>
</div>
<div class="detail-info">
<span style="color: lightyellow"
>金币&nbsp;&nbsp;&nbsp;&nbsp;{{
core.formatBigNumber(enemy.enemy.enemy.money)
}}</span
>
</div>
<div class="detail-info">
<span style="color: lawngreen"
>经验&nbsp;&nbsp;&nbsp;&nbsp;{{
core.formatBigNumber(enemy.enemy.enemy.exp)
}}</span
>
</div>
<div class="detail-info">
<span :style="{ color: enemy.damageColor }"
>伤害&nbsp;&nbsp;&nbsp;&nbsp;{{
enemy.damage
}}</span
>
</div>
<div class="detail-info">
<span style="color: lightsalmon"
>临界&nbsp;&nbsp;&nbsp;&nbsp;{{
enemy.critical
}}</span
>
</div>
<div class="detail-info">
<span style="color: lightpink"
>减伤&nbsp;&nbsp;&nbsp;&nbsp;<span
:style="{
color: 'lightpink'
}"
>{{ enemy.criticalDam }}</span
></span
>
</div>
<div class="detail-info">
<span style="color: cyan"
>{{
core.formatBigNumber(core.status.thisMap.ratio)
}}防&nbsp;&nbsp;&nbsp;&nbsp;{{
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>
-176
View File
@@ -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>
-263
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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';
+1 -1
View File
@@ -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';
+7 -2
View File
@@ -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';
+2 -2
View File
@@ -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';
+6 -48
View File
@@ -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);
}
}
+8 -3
View File
@@ -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 -1
View File
@@ -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
+4 -5
View File
@@ -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 -2
View File
@@ -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),
-509
View File
@@ -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;
}
}
+2 -4
View File
@@ -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;
}
}
+3 -4
View File
@@ -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
View File
@@ -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');
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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
View File
@@ -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:*"
}
}
-307
View File
@@ -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>加攻次数&nbsp;&nbsp;&nbsp;&nbsp;{{ addAtk }}</span>
<a-slider
class="slider"
v-model:value="addAtk"
:max="ceil((originCri.at(-1)?.[0] ?? 2) / ratio) - 1"
></a-slider>
<span
>最大值&nbsp;&nbsp;&nbsp;&nbsp;{{
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>加防次数&nbsp;&nbsp;&nbsp;&nbsp;{{ addDef }}</span>
<a-slider
class="slider"
v-model:value="addDef"
:max="ceil((originDef.at(-1)?.[0] ?? 2) / ratio) - 1"
></a-slider>
<span
>最大值&nbsp;&nbsp;&nbsp;&nbsp;{{
ceil((originDef.at(-1)?.[0] ?? 2) / ratio) - 1
}}</span
>
</div>
</div>
<div id="now-damage">
<div>
<span
>当前加攻{{
isMobile ? '' : '&nbsp;&nbsp;&nbsp;&nbsp;'
}}</span
>
<span class="changable" :change="addAtkChangable">{{
format(addAtk * ratio)
}}</span>
</div>
<div>
<span
>当前加防{{
isMobile ? '' : '&nbsp;&nbsp;&nbsp;&nbsp;'
}}</span
>
<span class="changable" :change="addDefChangable">{{
format(addDef * ratio)
}}</span>
</div>
<div>
<span
>当前减伤{{
isMobile ? '' : '&nbsp;&nbsp;&nbsp;&nbsp;'
}}</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 ? '' : '&nbsp;&nbsp;&nbsp;&nbsp;'
}}</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>
-97
View File
@@ -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>
-85
View File
@@ -1,85 +0,0 @@
<template>
<div id="enemy-target">
<div id="enemy-desc">
<span>怪物描述</span>
<Scroll id="enemy-desc-scroll">
<span
>&nbsp;&nbsp;&nbsp;&nbsp;{{
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>
-108
View File
@@ -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>
-32
View File
@@ -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);
}
+1 -1
View File
@@ -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 -1
View File
@@ -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,
-113
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
import { Animation, linear, sleep } from 'mutate-animate';
import { has } from '../utils';
import { has } from '@motajs/legacy-ui';
// todo: 移植到渲染树
-15
View File
@@ -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);
-825
View File
@@ -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;
}
-164
View File
@@ -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();
});
-120
View File
@@ -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);
}
}
-123
View File
@@ -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] }}>
&nbsp;&nbsp;&nbsp;&nbsp;{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;
}
-36
View File
@@ -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
};
}
-110
View File
@@ -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>
&nbsp;&nbsp;&nbsp;&nbsp;
<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>
);
}
-61
View File
@@ -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;
}
-565
View File
@@ -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();
}
}
}
-4
View File
@@ -1,4 +0,0 @@
import { ref } from 'vue';
export const status = ref(false);
export const fontSize = ref(100);
-48
View File
@@ -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;
}
-178
View File
@@ -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);
});
}
-528
View File
@@ -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 `${'&nbsp;'.repeat(12)}${v}`;
else if (
(has(a[i - 1]) && v !== '<br>' && a[i - 1] === '<br>') ||
i === 0
) {
return `${'&nbsp;'.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;
}
-339
View File
@@ -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>
-90
View File
@@ -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"
>出自&nbsp;&nbsp;&nbsp;&nbsp;{{
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
View File
@@ -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>
-240
View File
@@ -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>
-148
View File
@@ -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>
-117
View File
@@ -1,117 +0,0 @@
<template>
<Box id="complete-box">
<div id="complete">
<span>完成成就&nbsp;&nbsp;&nbsp;&nbsp;{{ 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>
-225
View File
@@ -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>
-674
View File
@@ -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>
-79
View File
@@ -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>
-730
View File
@@ -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>×&nbsp;{{ 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>
-176
View File
@@ -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>
-52
View File
@@ -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
View File
@@ -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>
-261
View File
@@ -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>
-26
View File
@@ -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
View File
@@ -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>
-157
View File
@@ -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>
-370
View File
@@ -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
View File
@@ -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
>×&nbsp;{{
(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>
-81
View File
@@ -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 `${'&nbsp;'.repeat(12)}${v}`;
else if (
(has(a[i - 1]) &&
v !== '<br>' &&
a[i - 1] === '<br>') ||
i === 0
) {
return `${'&nbsp;'.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>
-452
View File
@@ -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>
&nbsp;&nbsp;
<span>{{ chapterDict[chapter] }}</span>
&nbsp;&nbsp;
<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>
-677
View File
@@ -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>
-393
View File
@@ -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>
-46
View File
@@ -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>
-586
View File
@@ -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>&nbsp;&nbsp;
<span>新增工具栏</span>
</div>
<div v-else>
<a-input
style="
height: 100%;
font-size: 80%;
width: 100%;
"
v-model:value="addingToolId"
@blur="addTool"
></a-input>
</div>
</div>
</div>
</Scroll>
<a-divider
type="vertical"
dashed
v-if="isMobile"
style="height: 100%; border-color: #ddd4"
></a-divider>
<div id="tool-preview" v-if="!!bar && isMobile">
<div id="tool-preview-container">
<div class="tool-preview-item" v-for="item of bar.items">
<component
:is="CustomToolbar.info[item.type].show as any"
:item="item"
:toolbar="bar"
></component>
</div>
</div>
</div>
</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>&nbsp;&nbsp;
<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>
-138
View File
@@ -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>
-440
View File
@@ -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>×&nbsp;{{ 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>