mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-01-19 04:19:30 +08:00
楼传
This commit is contained in:
parent
71a4551004
commit
b3602d3f21
1
components.d.ts
vendored
1
components.d.ts
vendored
@ -11,6 +11,7 @@ declare module '@vue/runtime-core' {
|
||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||
ASlider: typeof import('ant-design-vue/es')['Slider']
|
||||
ASwitch: typeof import('ant-design-vue/es')['Switch']
|
||||
Box: typeof import('./src/components/box.vue')['default']
|
||||
BoxAnimate: typeof import('./src/components/boxAnimate.vue')['default']
|
||||
Colomn: typeof import('./src/components/colomn.vue')['default']
|
||||
|
@ -2685,8 +2685,8 @@ maps.prototype._drawThumbnail_drawToTarget = function (floorId, options) {
|
||||
y = options.y || 0,
|
||||
size = options.size || 1;
|
||||
// size的含义改为(0,1]范围的系数以适配长方形,默认为1,楼传为3/4,SL界面为0.3
|
||||
var w = Math.ceil(size * core._PX_),
|
||||
h = Math.ceil(size * core._PY_);
|
||||
var w = size * core._PX_,
|
||||
h = size * core._PY_;
|
||||
// 特判是否为编辑器,编辑器中长宽均采用core.js的遗留正方形像素边长,以保证下面的绘制正常
|
||||
if (main.mode == 'editor') w = h = size * core.__PIXELS__;
|
||||
var width = core.floors[floorId].width,
|
||||
@ -2697,6 +2697,21 @@ maps.prototype._drawThumbnail_drawToTarget = function (floorId, options) {
|
||||
if (centerY == null) centerY = Math.floor(height / 2);
|
||||
var tempCanvas = core.bigmap.tempCanvas;
|
||||
|
||||
if (options.inFlyMap) {
|
||||
ctx.drawImage(
|
||||
tempCanvas.canvas,
|
||||
0,
|
||||
0,
|
||||
tempCanvas.canvas.width,
|
||||
tempCanvas.canvas.height,
|
||||
options.x,
|
||||
options.y,
|
||||
options.w,
|
||||
options.h
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const scale = core.domStyle.scale * devicePixelRatio;
|
||||
if (options.all) {
|
||||
var tempWidth = tempCanvas.canvas.width,
|
||||
|
@ -52,6 +52,7 @@ var functions_d6ad677b_427a_4623_b50f_a445a3b0ef8a = {
|
||||
// 隐藏右下角的音乐按钮
|
||||
core.dom.musicBtn.style.display = 'none';
|
||||
core.dom.enlargeBtn.style.display = 'none';
|
||||
core.splitArea();
|
||||
},
|
||||
win: function (reason, norank, noexit) {
|
||||
// 游戏获胜事件
|
||||
|
@ -325,7 +325,7 @@ var items_296f5d02_12fd_4166_a7c1_b5e830c9ee3a =
|
||||
"text": "可以自由往来去过的楼层",
|
||||
"hideInReplay": true,
|
||||
"hideInToolbox": true,
|
||||
"useItemEffect": "core.ui.drawFly(core.floorIds.indexOf(core.status.floorId));",
|
||||
"useItemEffect": "core.ui.drawFly();",
|
||||
"canUseItemEffect": "(function () {\n\treturn core.status.maps[core.status.floorId].canFlyFrom;\n})();"
|
||||
},
|
||||
"coin": {
|
||||
|
@ -5445,6 +5445,11 @@ var plugins_bb40132b_638b_4a9f_b028_d3fe47acc8d1 = {
|
||||
return (core.plugin.equipOpened.value = true);
|
||||
};
|
||||
|
||||
ui.prototype.drawFly = function () {
|
||||
if (!core.isReplaying())
|
||||
return (core.plugin.flyOpened.value = true);
|
||||
};
|
||||
|
||||
control.prototype.updateStatusBar_update = function () {
|
||||
if (!core.isPlaying() || core.hasFlag('__statistics__')) return;
|
||||
core.control.controldata.updateStatusBar();
|
||||
|
@ -44,12 +44,12 @@ body {
|
||||
}
|
||||
|
||||
#startPanel {
|
||||
width: 100%;
|
||||
width: 150%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #fff;
|
||||
left: -25%;
|
||||
background-color: #000;
|
||||
overflow: hidden;
|
||||
z-index: 300;
|
||||
}
|
||||
@ -106,6 +106,7 @@ body {
|
||||
width: auto;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 260;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#startLogo {
|
||||
@ -143,7 +144,7 @@ body {
|
||||
}
|
||||
|
||||
#startButtonGroup {
|
||||
width: 33%;
|
||||
width: 25%;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-size: 1.4em;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { has } from '../utils';
|
||||
|
||||
export default function init() {
|
||||
return { splitArea, getMapData };
|
||||
return { splitArea, getMapDrawData };
|
||||
}
|
||||
|
||||
type BFSFromString = `${FloorIds},${number},${number},${Dir}`;
|
||||
@ -12,7 +12,20 @@ interface MapBFSResult {
|
||||
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<AllIds, Dir>> = {
|
||||
leftPortal: 'left',
|
||||
@ -21,9 +34,125 @@ const arrow: Partial<Record<AllIds, Dir>> = {
|
||||
downPortal: 'down'
|
||||
};
|
||||
|
||||
export function splitArea() {}
|
||||
/**
|
||||
* 切分地图区域
|
||||
*/
|
||||
export function splitArea() {
|
||||
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 > 0) {
|
||||
const title = core.status.maps[id].title;
|
||||
area[title] = data.maps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getMapDrawData(floorId: FloorIds) {}
|
||||
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 (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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 广度优先搜索地图信息
|
||||
@ -53,12 +182,12 @@ export function getMapData(
|
||||
const block = blocks[loc as LocString];
|
||||
const id = block.event.id;
|
||||
if (id in arrow) {
|
||||
const from = `${now},${loc},${arrow[id]}` as BFSFromString;
|
||||
const to = `${target},${ev.loc![0]},${
|
||||
ev.loc![1]
|
||||
}` as BFSToString;
|
||||
link[from] = to;
|
||||
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);
|
||||
}
|
||||
@ -67,8 +196,74 @@ export function getMapData(
|
||||
used[now] = true;
|
||||
}
|
||||
|
||||
return {
|
||||
const res = {
|
||||
maps: floors,
|
||||
link
|
||||
};
|
||||
|
||||
return (bfsCache[floorId] = res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制小地图
|
||||
* @param ctx 画布
|
||||
* @param floorId 中心楼层
|
||||
* @param interval 楼层间距
|
||||
* @param border 边框粗细
|
||||
* @param noCache 是否不使用缓存
|
||||
*/
|
||||
export function drawFlyMap(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
floorId: FloorIds,
|
||||
offset: [number, number],
|
||||
size: [number, number],
|
||||
scale: number = 3,
|
||||
interval: number = 5,
|
||||
border: number = 1,
|
||||
noCache: boolean = false
|
||||
) {
|
||||
const data = getMapDrawData(floorId, interval, border, noCache);
|
||||
const [ox, oy] = offset;
|
||||
const [width, height] = size;
|
||||
const canvas = ctx.canvas;
|
||||
canvas.width = data.width * devicePixelRatio * scale;
|
||||
canvas.height = data.height * devicePixelRatio * scale;
|
||||
ctx.lineWidth = border * devicePixelRatio * scale;
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.scale(scale, scale);
|
||||
// 绘制连线
|
||||
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
|
||||
][]) {
|
||||
drawThumbnail(ctx, id, scale, [x, y], offset, size);
|
||||
}
|
||||
}
|
||||
|
||||
function drawThumbnail(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
floorId: FloorIds,
|
||||
scale: number,
|
||||
pos: [number, number],
|
||||
offset: [number, number],
|
||||
size: [number, number]
|
||||
) {
|
||||
const [x, y] = pos;
|
||||
const [ox, oy] = offset;
|
||||
const [width, height] = size;
|
||||
const map = core.status.maps[floorId];
|
||||
if (
|
||||
ox + x * scale + (map.width * scale) / 2 < 0 ||
|
||||
ox + x * scale - (map.width * scale) / 2 > width ||
|
||||
oy + y * scale + (map.height * scale) / 2 < 0 ||
|
||||
oy + y * scale - (map.height * scale) / 2 > height
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import Settings from '../ui/settings.vue';
|
||||
import Desc from '../ui/desc.vue';
|
||||
import Skill from '../ui/skill.vue';
|
||||
import SkillTree from '../ui/skillTree.vue';
|
||||
import Fly from '../ui/fly.vue';
|
||||
|
||||
export const bookOpened = ref(false);
|
||||
export const toolOpened = ref(false);
|
||||
@ -16,6 +17,7 @@ export const settingsOpened = ref(false);
|
||||
export const descOpened = ref(false);
|
||||
export const skillOpened = ref(false);
|
||||
export const skillTreeOpened = ref(false);
|
||||
export const flyOpened = ref(false);
|
||||
|
||||
export const transition = ref(true);
|
||||
export const noClosePanel = ref(false);
|
||||
@ -30,7 +32,8 @@ const UI_LIST: [Ref<boolean>, Component][] = [
|
||||
[settingsOpened, Settings],
|
||||
[descOpened, Desc],
|
||||
[skillOpened, Skill],
|
||||
[skillTreeOpened, SkillTree]
|
||||
[skillTreeOpened, SkillTree],
|
||||
[flyOpened, Fly]
|
||||
];
|
||||
|
||||
/** ui栈 */
|
||||
@ -63,7 +66,8 @@ export default function init() {
|
||||
settingsOpened,
|
||||
descOpened,
|
||||
skillOpened,
|
||||
skillTreeOpened
|
||||
skillTreeOpened,
|
||||
flyOpened
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,8 @@
|
||||
|
||||
.selectable {
|
||||
border: #0000 0.5px solid;
|
||||
padding: 1% 3% 1% 3%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selectable[selected='true'] {
|
||||
|
2
src/types/action.d.ts
vendored
2
src/types/action.d.ts
vendored
@ -132,6 +132,8 @@ interface Actions extends VoidedActionFuncs {
|
||||
* @param x 要判断的横坐标
|
||||
*/
|
||||
_out(x: number): boolean;
|
||||
|
||||
_getNextFlyFloor(delta: number, index: number): number;
|
||||
}
|
||||
|
||||
declare const actions: new () => Actions;
|
||||
|
2
src/types/function.d.ts
vendored
2
src/types/function.d.ts
vendored
@ -196,7 +196,7 @@ interface EventData {
|
||||
* @param toId 目标楼层
|
||||
* @param callback 飞到后的回调函数
|
||||
*/
|
||||
flyTo(toId: FloorIds, callback: () => void): void;
|
||||
flyTo(toId: FloorIds, callback?: () => void): boolean;
|
||||
|
||||
/**
|
||||
* 与怪物战斗前
|
||||
|
30
src/types/map.d.ts
vendored
30
src/types/map.d.ts
vendored
@ -187,6 +187,11 @@ interface Floor<T extends FloorIds = FloorIds> extends FloorBase<T> {
|
||||
* 图块信息
|
||||
*/
|
||||
blocks: Block[];
|
||||
|
||||
/**
|
||||
* 是否被砍层
|
||||
*/
|
||||
deleted?: boolean;
|
||||
}
|
||||
|
||||
interface ResolvedFloor<T extends FloorIds = FloorIds> extends FloorBase<T> {
|
||||
@ -400,6 +405,31 @@ interface DrawThumbnailConfig {
|
||||
* 是否使用v2优化
|
||||
*/
|
||||
v2: boolean;
|
||||
|
||||
/**
|
||||
* 是否在小地图中出现
|
||||
*/
|
||||
inFlyMap: boolean;
|
||||
|
||||
/**
|
||||
* 小地图模式下的横坐标
|
||||
*/
|
||||
x: number;
|
||||
|
||||
/**
|
||||
* 小地图模式下的纵坐标
|
||||
*/
|
||||
y: number;
|
||||
|
||||
/**
|
||||
* 小地图模式下的宽度
|
||||
*/
|
||||
w: number;
|
||||
|
||||
/**
|
||||
* 小地图模式下的高度
|
||||
*/
|
||||
h: number;
|
||||
}
|
||||
|
||||
interface BlockFilter {
|
||||
|
13
src/types/plugin.d.ts
vendored
13
src/types/plugin.d.ts
vendored
@ -18,7 +18,8 @@ interface PluginDeclaration
|
||||
extends PluginUtils,
|
||||
PluginUis,
|
||||
PluginUse,
|
||||
SkillTree {
|
||||
SkillTree,
|
||||
MiniMap {
|
||||
/**
|
||||
* 添加函数 例:添加弹出文字,像这个就可以使用core.addPop或core.plugin.addPop调用
|
||||
* @param px 弹出的横坐标
|
||||
@ -149,6 +150,9 @@ interface PluginUis {
|
||||
/** 技能树界面是否打开 */
|
||||
readonly skillTreeOpened: Ref<boolean>;
|
||||
|
||||
/** 楼传界面是否打开 */
|
||||
readonly flyOpened: Ref<boolean>;
|
||||
|
||||
/** ui栈 */
|
||||
readonly uiStack: Ref<Component[]>;
|
||||
|
||||
@ -246,6 +250,13 @@ interface SkillTree {
|
||||
loadSkillTree(data: number[]): void;
|
||||
}
|
||||
|
||||
interface MiniMap {
|
||||
/**
|
||||
* 切分区域
|
||||
*/
|
||||
splitArea(): void;
|
||||
}
|
||||
|
||||
type Chapter = 'chapter1';
|
||||
|
||||
interface Skill {
|
||||
|
@ -201,7 +201,6 @@ function keydown(e: KeyboardEvent) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const div = document.getElementById('book') as HTMLDivElement;
|
||||
if (core.plugin.transition.value) await sleep(600);
|
||||
else await sleep(50);
|
||||
document.addEventListener('keyup', keyup);
|
||||
|
@ -4,7 +4,7 @@
|
||||
><div id="desc-list">
|
||||
<div
|
||||
v-for="(data, k) in desc"
|
||||
class="selectable desc-item"
|
||||
class="selectable"
|
||||
:selected="selected === k"
|
||||
@click="click(k)"
|
||||
>
|
||||
@ -53,9 +53,4 @@ function show(condition: string) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.desc-item {
|
||||
padding: 1% 3% 1% 3%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
748
src/ui/fly.vue
Normal file
748
src/ui/fly.vue
Normal file
@ -0,0 +1,748 @@
|
||||
<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>
|
||||
</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';
|
||||
import { keycode, tip } from '../plugin/utils';
|
||||
import { sleep } from 'mutate-animate';
|
||||
import { KeyCode } from '../plugin/keyCodes';
|
||||
|
||||
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(false);
|
||||
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>> = {};
|
||||
|
||||
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].includes(nowFloor.value))
|
||||
nowFloor.value =
|
||||
area[n].find(v => v === core.status.floorId) ?? area[n][0];
|
||||
});
|
||||
watch(noBorder, n => {
|
||||
drawedThumbnail = {};
|
||||
drawMap();
|
||||
});
|
||||
|
||||
const temp = document.createElement('canvas');
|
||||
const tempCtx = temp.getContext('2d')!;
|
||||
let map: HTMLCanvasElement;
|
||||
let mapCtx: CanvasRenderingContext2D;
|
||||
let thumb: HTMLCanvasElement;
|
||||
let thumbCtx: CanvasRenderingContext2D;
|
||||
|
||||
function exit() {
|
||||
core.plugin.flyOpened.value = false;
|
||||
}
|
||||
|
||||
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 (!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: true
|
||||
});
|
||||
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 = {};
|
||||
drawMap();
|
||||
drawRight();
|
||||
}
|
||||
|
||||
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) {
|
||||
while (to !== now) {
|
||||
to -= Math.sign(delta);
|
||||
const floor = core.status.maps[core.floorIds[to]];
|
||||
if (!floor.deleted) 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 ? 5 : 0,
|
||||
noBorder.value ? 0.5 : 1
|
||||
);
|
||||
const [x, y] = data.locs[id]!;
|
||||
ox = (-x + data.width / 2) * scale;
|
||||
oy = (-y + data.height / 2) * scale;
|
||||
}
|
||||
|
||||
// -------------------- 键盘事件
|
||||
|
||||
function keyup(e: KeyboardEvent) {
|
||||
const c = keycode(e.keyCode);
|
||||
if (c === KeyCode.Enter || c === KeyCode.Space || c === KeyCode.KeyC) fly();
|
||||
if (c === KeyCode.Escape || c === KeyCode.KeyX || c === KeyCode.KeyG) {
|
||||
exit();
|
||||
}
|
||||
if (!tradition.value) {
|
||||
if (c === KeyCode.LeftArrow) changeFloorByDir('left');
|
||||
if (c === KeyCode.RightArrow) changeFloorByDir('right');
|
||||
if (c === KeyCode.UpArrow) changeFloorByDir('up');
|
||||
if (c === KeyCode.DownArrow) changeFloorByDir('down');
|
||||
if (c === KeyCode.PageUp) changeFloorByDelta(1);
|
||||
if (c === KeyCode.PageDown) changeFloorByDelta(-1);
|
||||
} else {
|
||||
if (c === KeyCode.UpArrow) changeFloorByDelta(1);
|
||||
if (c === KeyCode.DownArrow) changeFloorByDelta(-1);
|
||||
if (c === KeyCode.LeftArrow) changeFloorByDelta(-10);
|
||||
if (c === KeyCode.RightArrow) changeFloorByDelta(10);
|
||||
if (c === KeyCode.PageUp) changeFloorByDelta(10);
|
||||
if (c === KeyCode.PageDown) changeFloorByDelta(-10);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- 触摸事件
|
||||
|
||||
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());
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
await sleep(50);
|
||||
if (core.plugin.transition.value) await sleep(600);
|
||||
|
||||
document.addEventListener('keyup', keyup);
|
||||
map.addEventListener('touchstart', touchdown);
|
||||
map.addEventListener('touchend', touchup);
|
||||
map.addEventListener('touchend', touchmove);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelGlobalDrag(drag);
|
||||
document.removeEventListener('keyup', keyup);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
#fly {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 2.7vh;
|
||||
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: 3.8vw;
|
||||
}
|
||||
|
||||
#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>
|
@ -3,7 +3,7 @@
|
||||
><template #left
|
||||
><div id="setting-list">
|
||||
<span
|
||||
class="selectable setting-item"
|
||||
class="selectable"
|
||||
:selected="selected === 'transition'"
|
||||
@click="click('transition')"
|
||||
>界面动画: {{
|
||||
@ -11,7 +11,7 @@
|
||||
}}</span
|
||||
>
|
||||
<span
|
||||
class="selectable setting-item"
|
||||
class="selectable"
|
||||
:selected="selected === 'itemDetail'"
|
||||
@click="click('itemDetail')"
|
||||
>宝石血瓶显伤: {{
|
||||
@ -19,7 +19,7 @@
|
||||
}}</span
|
||||
>
|
||||
<span
|
||||
class="selectable setting-item"
|
||||
class="selectable"
|
||||
:selected="selected === 'autoSkill'"
|
||||
@click="click('autoSkill')"
|
||||
>自动切换技能: {{
|
||||
@ -27,7 +27,7 @@
|
||||
}}</span
|
||||
>
|
||||
<span
|
||||
class="selectable setting-item"
|
||||
class="selectable"
|
||||
:selected="selected === 'autoScale'"
|
||||
@click="click('autoScale')"
|
||||
>自动放缩: {{
|
||||
|
@ -67,11 +67,6 @@ function exit() {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.skill-item {
|
||||
width: 100%;
|
||||
padding: 1% 3% 1% 3%;
|
||||
}
|
||||
|
||||
.skill-item[selectable='false'] {
|
||||
color: gray;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user