feat: 弹幕编辑器

This commit is contained in:
unanmed 2024-04-23 21:43:26 +08:00
parent 36f0495b8e
commit ed3ef3c16a
13 changed files with 922 additions and 39 deletions

View File

@ -40,7 +40,7 @@ main.floors.MT22=
"第二章的加点已开启,可以在技能树的前置技能下方选择",
"如果你玩过上个版本,直接跳到了本章,记得查看背包里面的各种道具,尤其是百科全书,同时注意左边是你来的方向,那里还有些怪物",
"从现在开始,跳跃技能不再消耗生命值,别忘了你还有跳跃技能",
"为了确保平衡与可玩性从现在开始无上之盾第一章终极技能效果变为1/10同时每个怪物的最大吸血量限制为攻防和"
"为了确保平衡与可玩性从现在开始无上之盾第一章终极技能效果变为1/10同时每个怪物的最大吸血量限制为怪物生命值的四分之一"
],
"7,9": [
"百科全书中已解锁第二章需要特别说明的怪物属性,你可以在百科全书中查看",

View File

@ -33,6 +33,8 @@ interface RollupInfo {
const rollupMap = new Map<string, RollupInfo>();
let bundleIndex = 0;
let ws: WebSocket;
let h: Server;
let wt: chokidar.FSWatcher;
class RefValue<T> extends EventEmitter {
private _value: T;
@ -448,6 +450,7 @@ ${names}
}
async function startHttpServer(port: number = 3000) {
if (h) return h;
const server = http();
const tryNext = () => {
@ -516,6 +519,7 @@ function setupHttp(server: Server) {
}
function watchProject() {
if (wt) return;
const watcher = chokidar.watch('public/', {
persistent: true,
ignored: [
@ -530,6 +534,7 @@ function watchProject() {
/_.*/
]
});
wt = watcher;
watcher.removeAllListeners();
watcher.on('change', async path => {
// 楼层热重载
@ -574,6 +579,7 @@ function setupSocket(socket: WebSocket) {
}
async function startWsServer(http: Server) {
if (ws) return;
return new Promise<WebSocketServer>(res => {
const server = new WebSocketServer({
server: http
@ -603,6 +609,7 @@ async function ensureConfig() {
// 2. 启动样板http服务
await ensureConfig();
const server = await startHttpServer(3000);
h = server;
// 3. 启动样板ws热重载服务
await startWsServer(server);

View File

@ -18,6 +18,7 @@ const props = defineProps<{
noborder?: boolean;
width?: number;
height?: number;
noAnimate?: boolean;
}>();
let c: HTMLCanvasElement;
@ -26,7 +27,6 @@ let ctx: CanvasRenderingContext2D;
let drawFn: () => void;
function draw() {
if (id === 'none') return;
if (has(drawFn)) removeAnimate(drawFn);
const cls = core.getClsFromId(props.id as AllIds);
@ -58,13 +58,17 @@ function draw() {
} else {
drawFn = () => {
core.clearMap(ctx);
const frame = core.status.globalAnimateStatus % frames;
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(() => {

View File

@ -108,11 +108,13 @@ async function calHeight(first: boolean = false) {
canvas.height = width * scale;
if (props.noScroll) canvas.style.height = `0px`;
}
await new Promise(res => {
await new Promise<void>(res => {
requestAnimationFrame(() => {
const style = getComputedStyle(content);
total = parseFloat(style[canvasAttr]);
res('');
total =
props.type === 'horizontal'
? content.scrollWidth
: content.scrollHeight;
res();
});
});
}

View File

@ -11,6 +11,17 @@ export const enum LogLevel {
ERROR
}
interface LoggerCatchInfo {
code?: number;
level: LogLevel;
message: string;
}
interface LoggerCatchReturns<T> {
ret: T;
info: LoggerCatchInfo[];
}
let logTip: HTMLSpanElement;
if (!main.replayChecking) {
const tip = document.createElement('span');
@ -38,6 +49,9 @@ export class Logger {
level: LogLevel = LogLevel.LOG;
enabled: boolean = true;
private catching: boolean = false;
private catchedInfo: LoggerCatchInfo[] = [];
constructor(logLevel: LogLevel) {
this.level = logLevel;
}
@ -56,6 +70,13 @@ export class Logger {
* @param text
*/
error(code: number, text: string) {
if (this.catching) {
this.catchedInfo.push({
level: LogLevel.ERROR,
message: text,
code
});
}
if (this.level <= LogLevel.ERROR && this.enabled) {
console.error(`[ERROR Code ${code}] ${text}`);
if (!main.replayChecking) {
@ -73,6 +94,13 @@ export class Logger {
* @param text
*/
severe(code: number, text: string) {
if (this.catching) {
this.catchedInfo.push({
level: LogLevel.ERROR,
message: text,
code
});
}
if (this.level <= LogLevel.SEVERE_WARNING && this.enabled) {
console.warn(`[SEVERE WARNING Code ${code}] ${text}`);
if (!main.replayChecking) {
@ -90,6 +118,13 @@ export class Logger {
* @param text
*/
warn(code: number, text: string) {
if (this.catching) {
this.catchedInfo.push({
level: LogLevel.ERROR,
message: text,
code
});
}
if (this.level <= LogLevel.WARNING && this.enabled) {
console.warn(`[WARNING Code ${code}] ${text}`);
if (!main.replayChecking) {
@ -106,11 +141,29 @@ export class Logger {
* @param text
*/
log(text: string) {
if (this.catching) {
this.catchedInfo.push({
level: LogLevel.ERROR,
message: text
});
}
if (this.level <= LogLevel.LOG && this.enabled) {
console.log(`[LOG] ${text}`);
}
}
catch<T>(fn: () => T): LoggerCatchReturns<T> {
const before = this.enabled;
this.catchedInfo = [];
this.disable();
this.catching = true;
const ret = fn();
this.catching = false;
if (before) this.enable();
return { ret, info: this.catchedInfo };
}
disable() {
this.enabled = false;
}

View File

@ -3,8 +3,8 @@ import { EmitableEvent, EventEmitter } from '@/core/common/eventEmitter';
import { logger } from '@/core/common/logger';
import { ResponseBase } from '@/core/interface';
import { deleteWith, ensureArray, parseCss, tip } from '@/plugin/utils';
import axios, { toFormData } from 'axios';
import { Component, VNode, h, ref, shallowReactive } from 'vue';
import axios, { AxiosResponse, toFormData } from 'axios';
import { Component, VNode, h, shallowReactive } from 'vue';
/* @__PURE__ */ import { id, password } from '../../../../user';
import { mainSetting } from '../setting';
@ -49,17 +49,45 @@ interface DanmakuEvent extends EmitableEvent {
type SpecContentFn = (content: string, type: string) => VNode;
interface AllowedCSS {
property: string;
check: (value: string, prop: string) => true | string;
}
const allowedCSS: Partial<Record<CanParseCss, AllowedCSS>> = {
color: {
property: 'color',
check: () => true
},
backgroundColor: {
property: 'backgroundColor',
check: () => true
},
fontSize: {
property: 'fontSize',
check: value => {
if (!/^\d+%$/.test(value)) {
return '字体大小只能设置为百分格式';
}
if (parseInt(value) > 200) {
return '字体最大只能为200%';
}
return true;
}
}
};
export class Danmaku extends EventEmitter<DanmakuEvent> {
static num: number = 0;
static backend: string = `/backend/tower/barrage.php`;
static all: Set<Danmaku> = new Set();
static allInPos: Partial<Record<FloorIds, Record<LocString, Danmaku>>> = {};
static allInPos: Partial<Record<FloorIds, Record<LocString, Danmaku[]>>> =
{};
static showList: Danmaku[] = shallowReactive([]);
static showMap: Map<number, Danmaku> = new Map();
static specList: Record<string, SpecContentFn> = {};
num: number = Danmaku.num++;
static lastEditoredDanmaku?: Danmaku;
id: number = -1;
text: string = '';
@ -82,10 +110,10 @@ export class Danmaku extends EventEmitter<DanmakuEvent> {
*
* @returns Axios Post Promise
*/
async post() {
async post(): Promise<AxiosResponse<PostDanmakuResponse>> {
if (this.posted || this.posting) {
logger.warn(5, `Repeat post danmaku.`);
return Promise.resolve();
return Promise.reject();
}
const data: DanmakuPostInfo = {
@ -108,6 +136,14 @@ export class Danmaku extends EventEmitter<DanmakuEvent> {
this.id = res.data.id;
this.posting = false;
if (res.data.code === 0) {
this.posted = true;
tip('success', '发送成功');
this.addToList();
} else {
tip('error', res.data.message);
}
return res;
} catch (e) {
this.posted = false;
@ -116,7 +152,7 @@ export class Danmaku extends EventEmitter<DanmakuEvent> {
3,
`Unexpected error when posting danmaku. Error info: ${e}`
);
return Promise.resolve();
return Promise.reject();
}
}
@ -160,7 +196,7 @@ export class Danmaku extends EventEmitter<DanmakuEvent> {
if (!css.color) css.color = this.textColor;
if (!css.textShadow)
css.textShadow = `1px 1px 1px ${this.strokeColor}, 1px -1px 1px ${this.strokeColor}, -1px 1px 1px ${this.strokeColor}, -1px -1px 1px ${this.strokeColor}`;
return css;
return { ...css, ...this.style };
}
/**
@ -195,10 +231,18 @@ export class Danmaku extends EventEmitter<DanmakuEvent> {
css(obj: CSSObj, overwrite?: boolean): void;
css(obj: string | CSSObj, overwrite: boolean = false) {
const res = typeof obj === 'string' ? parseCss(obj) : obj;
const allow = Danmaku.checkCSSAllow(res);
if (allow.length === 0) {
if (overwrite) this.style = res;
else {
this.style = { ...this.style, ...res };
}
} else {
logger.error(
8,
`Post not allowed css danmaku. Allow info: ${allow.join(',')}`
);
}
}
/**
@ -208,7 +252,8 @@ export class Danmaku extends EventEmitter<DanmakuEvent> {
Danmaku.all.add(this);
if (!this.floor) return;
Danmaku.allInPos[this.floor] ??= {};
Danmaku.allInPos[this.floor]![`${this.x},${this.y}`] = this;
Danmaku.allInPos[this.floor]![`${this.x},${this.y}`] ??= [];
Danmaku.allInPos[this.floor]![`${this.x},${this.y}`].push(this);
}
/**
@ -373,6 +418,27 @@ export class Danmaku extends EventEmitter<DanmakuEvent> {
return h('span');
}
/**
* CSS内容是否符合发弹幕要求
* @param css CSS内容
*/
static checkCSSAllow(css: CSSObj) {
const problem: string[] = [];
for (const [key, value] of Object.entries(css)) {
if (!allowedCSS[key as CanParseCss]) {
problem.push(`不允许的CSS:${key}`);
continue;
} else {
const res = allowedCSS[key as CanParseCss]!.check(value, key);
if (res !== true) {
problem.push(res);
}
}
}
return problem;
}
/**
*
*/
@ -431,6 +497,7 @@ Danmaku.registerSpecContent('i', content => {
return h(BoxAnimate as Component, {
id: content,
noborder: true,
noAnimate: true,
width: 32,
height: iconInfo.height
});
@ -450,7 +517,11 @@ Mota.require('var', 'hook').on('moveOneStep', (x, y, floor) => {
if (f) {
const danmaku = f[`${x},${y}`];
if (danmaku) {
danmaku.show();
danmaku.forEach(v => {
setTimeout(() => {
v.show();
}, Math.random() * 1000);
});
}
}
});

View File

@ -1,6 +1,6 @@
import { KeyCode } from '@/plugin/keyCodes';
import { Hotkey, HotkeyJSON } from '../custom/hotkey';
import { generateBinary, keycode } from '@/plugin/utils';
import { generateBinary, keycode, openDanmakuPoster } from '@/plugin/utils';
import { hovered } from './fixed';
import { hasMarkedEnemy, markEnemy, unmarkEnemy } from '@/plugin/mark';
import { mainUi } from './ui';
@ -136,6 +136,12 @@ gameKey
name: '鼠标位置怪物临界',
defaults: KeyCode.KeyC
})
.register({
id: 'danmaku',
name: '发送弹幕',
defaults: KeyCode.KeyA,
ctrl: true
})
.register({
id: 'quickEquip_1',
name: '切换/保存套装_1',
@ -486,6 +492,9 @@ gameKey
if (enemy) mainUi.open('fixedDetail', { panel: 'critical' });
}
})
.realize('danmaku', () => {
openDanmakuPoster();
})
.realize('restart', () => {
core.confirmRestart();
})

View File

@ -34,13 +34,11 @@ fixedUi.register(
new GameUi('start', UI.Start),
new GameUi('toolbar', UI.Toolbar),
new GameUi('load', UI.Load),
new GameUi('danmaku', UI.Danmaku)
new GameUi('danmaku', UI.Danmaku),
new GameUi('danmakuEditor', UI.DanmakuEditor)
);
fixedUi.showAll();
let loaded = false;
let mounted = false;
const hook = Mota.require('var', 'hook');
hook.once('mounted', () => {
const ui = document.getElementById('ui-main')!;
@ -73,6 +71,4 @@ hook.once('mounted', () => {
fixedUi.on('end', () => {
fixed.style.display = 'none';
});
mounted = true;
});

View File

@ -523,7 +523,7 @@ loading.once('coreInit', () => {
'ui.danmaku': storage.getValue('ui.danmaku', true),
'ui.danmakuSpeed': storage.getValue(
'ui.danmakuSpeed',
Math.floor(window.innerWidth / 25) * 5
Math.floor(window.innerWidth / 30) * 5
),
});
});

View File

@ -27,6 +27,7 @@ import * as frag from '@/plugin/fx/frag';
import * as use from '@/plugin/use';
import * as gameCanvas from '@/plugin/fx/gameCanvas';
import * as smooth from '@/plugin/fx/smoothView';
import * as animateController from '@/plugin/animateController';
Mota.Plugin.register('shadow_r', shadow, shadow.init);
Mota.Plugin.register('gameShadow_r', gameShadow, gameShadow.init);
@ -38,3 +39,8 @@ Mota.Plugin.register('frag_r', frag, frag.init);
Mota.Plugin.register('use_r', use);
Mota.Plugin.register('gameCanvas_r', gameCanvas);
Mota.Plugin.register('smooth_r', smooth, smooth.init);
Mota.Plugin.register(
'animateController_r',
animateController,
animateController.default
);

View File

@ -8,8 +8,9 @@ import axios from 'axios';
import { decompressFromBase64 } from 'lz-string';
import { parseColor } from './webgl/utils';
import { Keyboard, KeyboardEmits } from '@/core/main/custom/keyboard';
import { mainUi } from '@/core/main/init/ui';
import { fixedUi, mainUi } from '@/core/main/init/ui';
import { isAssist } from '@/core/main/custom/hotkey';
import { logger } from '@/core/common/logger';
type CanParseCss = keyof {
[P in keyof CSSStyleDeclaration as CSSStyleDeclaration[P] extends string
@ -82,18 +83,108 @@ export function keycode(key: number) {
* @param css css字符串
*/
export function parseCss(css: string): Partial<Record<CanParseCss, string>> {
const str = css.replace(/[\n\s\t]*/g, '').replace(/;*/g, ';');
const styles = str.split(';');
if (css.length === 0) return {};
let pointer = -1;
let inProp = true;
let prop = '';
let value = '';
let upper = false;
const res: Partial<Record<CanParseCss, string>> = {};
for (const one of styles) {
const [key, data] = one.split(':');
const cssKey = key.replace(/\-([a-z])/g, (str, $1) =>
$1.toUpperCase()
) as CanParseCss;
res[cssKey] = data;
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,
`Syntax error in parsing CSS: Unexpected ':'. Col: ${pointer}. CSS string: '${css}'`
);
return res;
}
inProp = false;
continue;
}
if (char === ';') {
if (prop.length === 0) continue;
if (inProp) {
logger.error(
4,
`Syntax error in parsing CSS: Unexpected ';'. Col: ${pointer}. CSS string: '${css}'`
);
return res;
}
res[prop as CanParseCss] = value.trim();
inProp = true;
prop = '';
value = '';
continue;
}
if (upper) {
if (!inProp) {
logger.error(
5,
`Syntax error in parsing CSS: Missing property name after '-'. Col: ${pointer}. CSS string: '${css}'`
);
}
prop += char.toUpperCase();
upper = false;
} else {
if (inProp) prop += char;
else value += char;
}
}
if (inProp && prop.length > 0) {
logger.error(
6,
`Syntax error in parsing CSS: Unexpected end of css, expecting ':'. Col: ${pointer}. CSS string: '${css}'`
);
return res;
}
if (!inProp && value.trim().length === 0) {
logger.error(
7,
`Syntax error in parsing CSS: Unexpected end of css, expecting property value. Col: ${pointer}. CSS string: '${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;
}
/**
@ -401,3 +492,22 @@ 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;
}

624
src/ui/danmakuEditor.vue Normal file
View File

@ -0,0 +1,624 @@
<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>

View File

@ -24,3 +24,4 @@ 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';