mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-03-13 10:27:06 +08:00
625 lines
15 KiB
Vue
625 lines
15 KiB
Vue
<template>
|
||
<div id="danmaku-editor" @click.stop>
|
||
<div id="danmaku-input">
|
||
<span
|
||
class="danmaku-tool"
|
||
:open="cssOpened"
|
||
@click="openTool('css')"
|
||
>css</span
|
||
>
|
||
<font-colors-outlined
|
||
class="danmaku-tool"
|
||
:open="fillOpened"
|
||
@click="openTool('fillColor')"
|
||
/>
|
||
<highlight-outlined
|
||
class="danmaku-tool"
|
||
:open="strokeOpened"
|
||
@click="openTool('strokeColor')"
|
||
/>
|
||
<meh-outlined
|
||
class="danmaku-tool"
|
||
:open="iconOpened"
|
||
@click="openTool('icon')"
|
||
/>
|
||
<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>
|
||
<send-outlined
|
||
class="danmaku-tool danmaku-post"
|
||
:posting="posting"
|
||
@click="send()"
|
||
/>
|
||
</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">设置颜色</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 { tip } from '@/plugin/utils';
|
||
import { gameKey } from '@/core/main/init/hotkey';
|
||
import { isNil } from 'lodash-es';
|
||
import { stringifyCSS, parseCss, getIconHeight } from '@/plugin/utils';
|
||
import { logger, LogLevel } from '@/core/common/logger';
|
||
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;
|
||
}
|
||
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(400).then(() => {
|
||
fixedUi.close(props.num);
|
||
});
|
||
}
|
||
|
||
function input(value: string) {
|
||
danmaku.text = value;
|
||
}
|
||
|
||
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;
|
||
})
|
||
)
|
||
];
|
||
}
|
||
|
||
function clickOuter() {
|
||
if (!iconAll.value) close();
|
||
else iconAll.value = false;
|
||
}
|
||
|
||
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);
|
||
});
|
||
|
||
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);
|
||
});
|
||
</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.4s ease-out 0s 1 normal forwards running;
|
||
}
|
||
|
||
.danmaku-close {
|
||
animation: editor-close 0.4s 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;
|
||
}
|
||
|
||
.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: 'Fira Code';
|
||
|
||
#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;
|
||
|
||
.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: 'Fira Code';
|
||
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>
|