HumanBreak/src/ui/fly.vue

808 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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"></canvas>
<div id="fly-tools">
<double-left-outlined
@click="changeFloorByDelta(-10)"
class="button-text"
/>
<left-outlined
@click="changeFloorByDelta(-1)"
class="button-text"
/>
<span id="fly-now">{{ title }}</span>
<right-outlined
@click="changeFloorByDelta(1)"
class="button-text"
/>
<double-right-outlined
@click="changeFloorByDelta(10)"
class="button-text"
/>
</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, getMapDrawData, getMapData } 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 { downloadCanvasImage, tip } from '../plugin/utils';
import { GameUi } from '@/core/main/custom/ui';
import { gameKey } from '@/core/main/init/hotkey';
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);
let scale = isMobile ? 1.5 : 3;
let ox = 0;
let oy = 0;
let drawedThumbnail: Partial<Record<FloorIds, boolean>> = {};
let thumbnailLoc: Partial<Record<FloorIds, Loc2>> = {};
noBorder.value = core.getLocalStorage('noBorder', true);
tradition.value = core.getLocalStorage('flyTradition', false);
const floor = computed(() => {
return core.status.maps[nowFloor.value];
});
watch(nowFloor, draw);
watch(nowArea, n => {
ox = 0;
oy = 0;
scale = 3;
lastScale = 3;
if (area[n] && !area[n].includes(nowFloor.value))
nowFloor.value =
area[n].find(v => v === core.status.floorId) ?? area[n][0];
});
watch(noBorder, n => {
core.setLocalStorage('noBorder', n);
drawedThumbnail = {};
drawMap();
});
watch(tradition, n => {
core.setLocalStorage('flyTradition', n);
});
const temp = document.createElement('canvas');
const tempCtx = temp.getContext('2d')!;
let map: HTMLCanvasElement;
let mapCtx: CanvasRenderingContext2D;
let thumb: HTMLCanvasElement;
let thumbCtx: CanvasRenderingContext2D;
let downloadMode = false;
function exit() {
mota.ui.main.close(props.num);
}
const title = computed(() => {
return core.status.maps[nowFloor.value].title;
});
/**
* 绘制小地图
* @param noCache 是否不使用缓存
*/
function drawMap(noCache: boolean = false) {
const border = noBorder.value ? 0.5 : 1;
const data = getMapDrawData(
nowFloor.value,
noBorder.value ? 0 : 5,
border,
noCache
);
const ctx = tempCtx;
const s = 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 (!noBorder.value) {
// 绘制连线
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 (!noBorder.value) drawBorder(id, x, y);
drawThumbnail(id, x, y);
}
drawToTarget();
}
function drawBorder(id: FloorIds, x: number, y: number) {
const border = noBorder.value ? 0.5 : 1;
const ctx = tempCtx;
ctx.lineWidth = border * devicePixelRatio;
const map = core.status.maps[id];
if (!core.hasVisitedFloor(id)) {
ctx.fillStyle = '#d0d';
} else {
ctx.fillStyle = '#000';
}
if (id === nowFloor.value) {
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 === nowFloor.value) {
ctx.fillStyle = '#ff04';
ctx.fillRect(
x - map.width / 2,
y - map.height / 2,
map.width,
map.height
);
}
}
/**
* 绘制小地图至目标画布
*/
function drawToTarget(s: number = 1) {
mapCtx.clearRect(0, 0, map.width, map.height);
mapCtx.drawImage(
temp,
0,
0,
temp.width,
temp.height,
ox * devicePixelRatio + (map.width - temp.width) / 2,
oy * devicePixelRatio + (map.height - temp.height) / 2,
temp.width,
temp.height
);
}
/**
* 检查是否应该绘制缩略图
*/
function checkThumbnail(floorId: FloorIds, x: number, y: number) {
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;
thumbnailLoc[floorId] = [left, top, right, bottom];
if (
drawedThumbnail[floorId] ||
(!noBorder.value && scale <= 4) ||
right < 0 ||
bottom < 0 ||
left > map.width ||
top > map.height
)
return false;
return true;
}
/**
* 绘制缩略图
*/
function drawThumbnail(
floorId: FloorIds,
x: number,
y: number,
noCheck: boolean = false
) {
if (!downloadMode && !noCheck && !checkThumbnail(floorId, x, y)) return;
const floor = core.status.maps[floorId];
drawedThumbnail[floorId] = true;
// 绘制缩略图
const ctx = 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: scale > 7
});
if (!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 (nowFloor.value === floorId) {
ctx.fillStyle = '#ff04';
ctx.fillRect(
x - floor.width / 2,
y - floor.height / 2,
floor.width,
floor.height
);
ctx.fillStyle = '#000';
}
}
}
/**
* 当移动时检查是否应该绘制缩略图
*/
function checkMoveThumbnail() {
const border = noBorder.value ? 0.5 : 1;
const data = getMapDrawData(nowFloor.value, noBorder.value ? 0 : 5, border);
for (const [id, [x, y]] of Object.entries(data.locs) as [
FloorIds,
LocArr
][]) {
if (checkThumbnail(id, x, y)) drawThumbnail(id, x, y, true);
}
}
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() {
drawedThumbnail = {};
thumbnailLoc = {};
drawMap();
drawRight();
}
function download() {
if (nowArea.value === '') {
tip('error', '当前地图不在任意一个区域内!');
return;
}
downloadMode = true;
const before = scale;
scale = 32;
drawMap();
downloadCanvasImage(temp, nowArea.value);
scale = before;
downloadMode = false;
draw();
tip('success', '图片下载成功!');
}
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 = '';
drawedThumbnail = {};
drawMap();
lastScale = s;
}, 200);
function resize(delta: number) {
ox *= delta;
oy *= delta;
scale = delta * scale;
changeScale(scale);
map.style.transform = `scale(${scale / lastScale})`;
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;
ox += dx;
oy += dy;
lastX = x;
lastY = y;
checkMoveThumbnail();
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(
thumbnailLoc
) as [FloorIds, Loc2][]) {
if (x >= left && x <= right && y >= top && y <= bottom) {
if (id === nowFloor.value) {
fly();
} else {
nowFloor.value = id;
}
}
}
}
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);
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;
locateMap(target);
nowFloor.value = target;
return;
}
}
}
/**
* 居中地图
* @param id 楼层id
*/
function locateMap(id: FloorIds) {
const data = getMapDrawData(
id,
noBorder.value ? 0 : 5, // 可恶的0和5写反了找一个多小时
noBorder.value ? 0.5 : 1
);
if (!data.locs[id]) return;
const [x, y] = data.locs[id]!;
ox = (-x + data.width / 2 - 5) * scale;
oy = (-y + data.height / 2 - 5) * scale;
}
// -------------------- 键盘事件
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;
mapCtx = map.getContext('2d')!;
thumb = document.getElementById('fly-thumbnail') as HTMLCanvasElement;
thumbCtx = thumb.getContext('2d')!;
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());
});
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;
}
#fly-tools {
margin: 0;
width: 80%;
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
}
#fly-thumbnail {
width: 35vw;
height: 35vw;
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;
}
@media screen and (max-width: 600px) {
#fly {
padding: 5%;
font-size: 100%;
}
#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>