mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-02-28 09:27:07 +08:00
feat: 弹幕编辑器
This commit is contained in:
parent
36f0495b8e
commit
ed3ef3c16a
@ -40,7 +40,7 @@ main.floors.MT22=
|
|||||||
"第二章的加点已开启,可以在技能树的前置技能下方选择",
|
"第二章的加点已开启,可以在技能树的前置技能下方选择",
|
||||||
"如果你玩过上个版本,直接跳到了本章,记得查看背包里面的各种道具,尤其是百科全书,同时注意左边是你来的方向,那里还有些怪物",
|
"如果你玩过上个版本,直接跳到了本章,记得查看背包里面的各种道具,尤其是百科全书,同时注意左边是你来的方向,那里还有些怪物",
|
||||||
"从现在开始,跳跃技能不再消耗生命值,别忘了你还有跳跃技能",
|
"从现在开始,跳跃技能不再消耗生命值,别忘了你还有跳跃技能",
|
||||||
"为了确保平衡与可玩性,从现在开始,无上之盾(第一章终极技能)效果变为1/10,同时每个怪物的最大吸血量限制为攻防和"
|
"为了确保平衡与可玩性,从现在开始,无上之盾(第一章终极技能)效果变为1/10,同时每个怪物的最大吸血量限制为怪物生命值的四分之一"
|
||||||
],
|
],
|
||||||
"7,9": [
|
"7,9": [
|
||||||
"百科全书中已解锁第二章需要特别说明的怪物属性,你可以在百科全书中查看",
|
"百科全书中已解锁第二章需要特别说明的怪物属性,你可以在百科全书中查看",
|
||||||
|
@ -33,6 +33,8 @@ interface RollupInfo {
|
|||||||
const rollupMap = new Map<string, RollupInfo>();
|
const rollupMap = new Map<string, RollupInfo>();
|
||||||
let bundleIndex = 0;
|
let bundleIndex = 0;
|
||||||
let ws: WebSocket;
|
let ws: WebSocket;
|
||||||
|
let h: Server;
|
||||||
|
let wt: chokidar.FSWatcher;
|
||||||
|
|
||||||
class RefValue<T> extends EventEmitter {
|
class RefValue<T> extends EventEmitter {
|
||||||
private _value: T;
|
private _value: T;
|
||||||
@ -448,6 +450,7 @@ ${names}
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startHttpServer(port: number = 3000) {
|
async function startHttpServer(port: number = 3000) {
|
||||||
|
if (h) return h;
|
||||||
const server = http();
|
const server = http();
|
||||||
|
|
||||||
const tryNext = () => {
|
const tryNext = () => {
|
||||||
@ -516,6 +519,7 @@ function setupHttp(server: Server) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function watchProject() {
|
function watchProject() {
|
||||||
|
if (wt) return;
|
||||||
const watcher = chokidar.watch('public/', {
|
const watcher = chokidar.watch('public/', {
|
||||||
persistent: true,
|
persistent: true,
|
||||||
ignored: [
|
ignored: [
|
||||||
@ -530,6 +534,7 @@ function watchProject() {
|
|||||||
/_.*/
|
/_.*/
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
wt = watcher;
|
||||||
watcher.removeAllListeners();
|
watcher.removeAllListeners();
|
||||||
watcher.on('change', async path => {
|
watcher.on('change', async path => {
|
||||||
// 楼层热重载
|
// 楼层热重载
|
||||||
@ -574,6 +579,7 @@ function setupSocket(socket: WebSocket) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startWsServer(http: Server) {
|
async function startWsServer(http: Server) {
|
||||||
|
if (ws) return;
|
||||||
return new Promise<WebSocketServer>(res => {
|
return new Promise<WebSocketServer>(res => {
|
||||||
const server = new WebSocketServer({
|
const server = new WebSocketServer({
|
||||||
server: http
|
server: http
|
||||||
@ -603,6 +609,7 @@ async function ensureConfig() {
|
|||||||
// 2. 启动样板http服务
|
// 2. 启动样板http服务
|
||||||
await ensureConfig();
|
await ensureConfig();
|
||||||
const server = await startHttpServer(3000);
|
const server = await startHttpServer(3000);
|
||||||
|
h = server;
|
||||||
|
|
||||||
// 3. 启动样板ws热重载服务
|
// 3. 启动样板ws热重载服务
|
||||||
await startWsServer(server);
|
await startWsServer(server);
|
||||||
|
@ -18,6 +18,7 @@ const props = defineProps<{
|
|||||||
noborder?: boolean;
|
noborder?: boolean;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
noAnimate?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let c: HTMLCanvasElement;
|
let c: HTMLCanvasElement;
|
||||||
@ -26,7 +27,6 @@ let ctx: CanvasRenderingContext2D;
|
|||||||
let drawFn: () => void;
|
let drawFn: () => void;
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
if (id === 'none') return;
|
|
||||||
if (has(drawFn)) removeAnimate(drawFn);
|
if (has(drawFn)) removeAnimate(drawFn);
|
||||||
|
|
||||||
const cls = core.getClsFromId(props.id as AllIds);
|
const cls = core.getClsFromId(props.id as AllIds);
|
||||||
@ -58,12 +58,16 @@ function draw() {
|
|||||||
} else {
|
} else {
|
||||||
drawFn = () => {
|
drawFn = () => {
|
||||||
core.clearMap(ctx);
|
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);
|
core.drawIcon(ctx, props.id as AllIds, 0, 0, w, h, frame);
|
||||||
};
|
};
|
||||||
|
|
||||||
drawFn();
|
drawFn();
|
||||||
addAnimate(drawFn);
|
if (!props.noAnimate) {
|
||||||
|
addAnimate(drawFn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,11 +108,13 @@ async function calHeight(first: boolean = false) {
|
|||||||
canvas.height = width * scale;
|
canvas.height = width * scale;
|
||||||
if (props.noScroll) canvas.style.height = `0px`;
|
if (props.noScroll) canvas.style.height = `0px`;
|
||||||
}
|
}
|
||||||
await new Promise(res => {
|
await new Promise<void>(res => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const style = getComputedStyle(content);
|
total =
|
||||||
total = parseFloat(style[canvasAttr]);
|
props.type === 'horizontal'
|
||||||
res('');
|
? content.scrollWidth
|
||||||
|
: content.scrollHeight;
|
||||||
|
res();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,17 @@ export const enum LogLevel {
|
|||||||
ERROR
|
ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LoggerCatchInfo {
|
||||||
|
code?: number;
|
||||||
|
level: LogLevel;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoggerCatchReturns<T> {
|
||||||
|
ret: T;
|
||||||
|
info: LoggerCatchInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
let logTip: HTMLSpanElement;
|
let logTip: HTMLSpanElement;
|
||||||
if (!main.replayChecking) {
|
if (!main.replayChecking) {
|
||||||
const tip = document.createElement('span');
|
const tip = document.createElement('span');
|
||||||
@ -38,6 +49,9 @@ export class Logger {
|
|||||||
level: LogLevel = LogLevel.LOG;
|
level: LogLevel = LogLevel.LOG;
|
||||||
enabled: boolean = true;
|
enabled: boolean = true;
|
||||||
|
|
||||||
|
private catching: boolean = false;
|
||||||
|
private catchedInfo: LoggerCatchInfo[] = [];
|
||||||
|
|
||||||
constructor(logLevel: LogLevel) {
|
constructor(logLevel: LogLevel) {
|
||||||
this.level = logLevel;
|
this.level = logLevel;
|
||||||
}
|
}
|
||||||
@ -56,6 +70,13 @@ export class Logger {
|
|||||||
* @param text 错误信息
|
* @param text 错误信息
|
||||||
*/
|
*/
|
||||||
error(code: number, text: string) {
|
error(code: number, text: string) {
|
||||||
|
if (this.catching) {
|
||||||
|
this.catchedInfo.push({
|
||||||
|
level: LogLevel.ERROR,
|
||||||
|
message: text,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
}
|
||||||
if (this.level <= LogLevel.ERROR && this.enabled) {
|
if (this.level <= LogLevel.ERROR && this.enabled) {
|
||||||
console.error(`[ERROR Code ${code}] ${text}`);
|
console.error(`[ERROR Code ${code}] ${text}`);
|
||||||
if (!main.replayChecking) {
|
if (!main.replayChecking) {
|
||||||
@ -73,6 +94,13 @@ export class Logger {
|
|||||||
* @param text 警告信息
|
* @param text 警告信息
|
||||||
*/
|
*/
|
||||||
severe(code: number, text: string) {
|
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) {
|
if (this.level <= LogLevel.SEVERE_WARNING && this.enabled) {
|
||||||
console.warn(`[SEVERE WARNING Code ${code}] ${text}`);
|
console.warn(`[SEVERE WARNING Code ${code}] ${text}`);
|
||||||
if (!main.replayChecking) {
|
if (!main.replayChecking) {
|
||||||
@ -90,6 +118,13 @@ export class Logger {
|
|||||||
* @param text 警告信息
|
* @param text 警告信息
|
||||||
*/
|
*/
|
||||||
warn(code: number, text: string) {
|
warn(code: number, text: string) {
|
||||||
|
if (this.catching) {
|
||||||
|
this.catchedInfo.push({
|
||||||
|
level: LogLevel.ERROR,
|
||||||
|
message: text,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
}
|
||||||
if (this.level <= LogLevel.WARNING && this.enabled) {
|
if (this.level <= LogLevel.WARNING && this.enabled) {
|
||||||
console.warn(`[WARNING Code ${code}] ${text}`);
|
console.warn(`[WARNING Code ${code}] ${text}`);
|
||||||
if (!main.replayChecking) {
|
if (!main.replayChecking) {
|
||||||
@ -106,11 +141,29 @@ export class Logger {
|
|||||||
* @param text 日志信息
|
* @param text 日志信息
|
||||||
*/
|
*/
|
||||||
log(text: string) {
|
log(text: string) {
|
||||||
|
if (this.catching) {
|
||||||
|
this.catchedInfo.push({
|
||||||
|
level: LogLevel.ERROR,
|
||||||
|
message: text
|
||||||
|
});
|
||||||
|
}
|
||||||
if (this.level <= LogLevel.LOG && this.enabled) {
|
if (this.level <= LogLevel.LOG && this.enabled) {
|
||||||
console.log(`[LOG] ${text}`);
|
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() {
|
disable() {
|
||||||
this.enabled = false;
|
this.enabled = false;
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@ import { EmitableEvent, EventEmitter } from '@/core/common/eventEmitter';
|
|||||||
import { logger } from '@/core/common/logger';
|
import { logger } from '@/core/common/logger';
|
||||||
import { ResponseBase } from '@/core/interface';
|
import { ResponseBase } from '@/core/interface';
|
||||||
import { deleteWith, ensureArray, parseCss, tip } from '@/plugin/utils';
|
import { deleteWith, ensureArray, parseCss, tip } from '@/plugin/utils';
|
||||||
import axios, { toFormData } from 'axios';
|
import axios, { AxiosResponse, toFormData } from 'axios';
|
||||||
import { Component, VNode, h, ref, shallowReactive } from 'vue';
|
import { Component, VNode, h, shallowReactive } from 'vue';
|
||||||
/* @__PURE__ */ import { id, password } from '../../../../user';
|
/* @__PURE__ */ import { id, password } from '../../../../user';
|
||||||
import { mainSetting } from '../setting';
|
import { mainSetting } from '../setting';
|
||||||
|
|
||||||
@ -49,17 +49,45 @@ interface DanmakuEvent extends EmitableEvent {
|
|||||||
|
|
||||||
type SpecContentFn = (content: string, type: string) => VNode;
|
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> {
|
export class Danmaku extends EventEmitter<DanmakuEvent> {
|
||||||
static num: number = 0;
|
|
||||||
static backend: string = `/backend/tower/barrage.php`;
|
static backend: string = `/backend/tower/barrage.php`;
|
||||||
static all: Set<Danmaku> = new Set();
|
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 showList: Danmaku[] = shallowReactive([]);
|
||||||
static showMap: Map<number, Danmaku> = new Map();
|
static showMap: Map<number, Danmaku> = new Map();
|
||||||
static specList: Record<string, SpecContentFn> = {};
|
static specList: Record<string, SpecContentFn> = {};
|
||||||
|
|
||||||
num: number = Danmaku.num++;
|
static lastEditoredDanmaku?: Danmaku;
|
||||||
|
|
||||||
id: number = -1;
|
id: number = -1;
|
||||||
text: string = '';
|
text: string = '';
|
||||||
@ -82,10 +110,10 @@ export class Danmaku extends EventEmitter<DanmakuEvent> {
|
|||||||
* 发送弹幕
|
* 发送弹幕
|
||||||
* @returns 弹幕发送的 Axios Post 信息,为 Promise
|
* @returns 弹幕发送的 Axios Post 信息,为 Promise
|
||||||
*/
|
*/
|
||||||
async post() {
|
async post(): Promise<AxiosResponse<PostDanmakuResponse>> {
|
||||||
if (this.posted || this.posting) {
|
if (this.posted || this.posting) {
|
||||||
logger.warn(5, `Repeat post danmaku.`);
|
logger.warn(5, `Repeat post danmaku.`);
|
||||||
return Promise.resolve();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: DanmakuPostInfo = {
|
const data: DanmakuPostInfo = {
|
||||||
@ -108,6 +136,14 @@ export class Danmaku extends EventEmitter<DanmakuEvent> {
|
|||||||
this.id = res.data.id;
|
this.id = res.data.id;
|
||||||
this.posting = false;
|
this.posting = false;
|
||||||
|
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
this.posted = true;
|
||||||
|
tip('success', '发送成功');
|
||||||
|
this.addToList();
|
||||||
|
} else {
|
||||||
|
tip('error', res.data.message);
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.posted = false;
|
this.posted = false;
|
||||||
@ -116,7 +152,7 @@ export class Danmaku extends EventEmitter<DanmakuEvent> {
|
|||||||
3,
|
3,
|
||||||
`Unexpected error when posting danmaku. Error info: ${e}`
|
`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.color) css.color = this.textColor;
|
||||||
if (!css.textShadow)
|
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}`;
|
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,9 +231,17 @@ export class Danmaku extends EventEmitter<DanmakuEvent> {
|
|||||||
css(obj: CSSObj, overwrite?: boolean): void;
|
css(obj: CSSObj, overwrite?: boolean): void;
|
||||||
css(obj: string | CSSObj, overwrite: boolean = false) {
|
css(obj: string | CSSObj, overwrite: boolean = false) {
|
||||||
const res = typeof obj === 'string' ? parseCss(obj) : obj;
|
const res = typeof obj === 'string' ? parseCss(obj) : obj;
|
||||||
if (overwrite) this.style = res;
|
const allow = Danmaku.checkCSSAllow(res);
|
||||||
else {
|
if (allow.length === 0) {
|
||||||
this.style = { ...this.style, ...res };
|
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);
|
Danmaku.all.add(this);
|
||||||
if (!this.floor) return;
|
if (!this.floor) return;
|
||||||
Danmaku.allInPos[this.floor] ??= {};
|
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');
|
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, {
|
return h(BoxAnimate as Component, {
|
||||||
id: content,
|
id: content,
|
||||||
noborder: true,
|
noborder: true,
|
||||||
|
noAnimate: true,
|
||||||
width: 32,
|
width: 32,
|
||||||
height: iconInfo.height
|
height: iconInfo.height
|
||||||
});
|
});
|
||||||
@ -450,7 +517,11 @@ Mota.require('var', 'hook').on('moveOneStep', (x, y, floor) => {
|
|||||||
if (f) {
|
if (f) {
|
||||||
const danmaku = f[`${x},${y}`];
|
const danmaku = f[`${x},${y}`];
|
||||||
if (danmaku) {
|
if (danmaku) {
|
||||||
danmaku.show();
|
danmaku.forEach(v => {
|
||||||
|
setTimeout(() => {
|
||||||
|
v.show();
|
||||||
|
}, Math.random() * 1000);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { KeyCode } from '@/plugin/keyCodes';
|
import { KeyCode } from '@/plugin/keyCodes';
|
||||||
import { Hotkey, HotkeyJSON } from '../custom/hotkey';
|
import { Hotkey, HotkeyJSON } from '../custom/hotkey';
|
||||||
import { generateBinary, keycode } from '@/plugin/utils';
|
import { generateBinary, keycode, openDanmakuPoster } from '@/plugin/utils';
|
||||||
import { hovered } from './fixed';
|
import { hovered } from './fixed';
|
||||||
import { hasMarkedEnemy, markEnemy, unmarkEnemy } from '@/plugin/mark';
|
import { hasMarkedEnemy, markEnemy, unmarkEnemy } from '@/plugin/mark';
|
||||||
import { mainUi } from './ui';
|
import { mainUi } from './ui';
|
||||||
@ -136,6 +136,12 @@ gameKey
|
|||||||
name: '鼠标位置怪物临界',
|
name: '鼠标位置怪物临界',
|
||||||
defaults: KeyCode.KeyC
|
defaults: KeyCode.KeyC
|
||||||
})
|
})
|
||||||
|
.register({
|
||||||
|
id: 'danmaku',
|
||||||
|
name: '发送弹幕',
|
||||||
|
defaults: KeyCode.KeyA,
|
||||||
|
ctrl: true
|
||||||
|
})
|
||||||
.register({
|
.register({
|
||||||
id: 'quickEquip_1',
|
id: 'quickEquip_1',
|
||||||
name: '切换/保存套装_1',
|
name: '切换/保存套装_1',
|
||||||
@ -486,6 +492,9 @@ gameKey
|
|||||||
if (enemy) mainUi.open('fixedDetail', { panel: 'critical' });
|
if (enemy) mainUi.open('fixedDetail', { panel: 'critical' });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.realize('danmaku', () => {
|
||||||
|
openDanmakuPoster();
|
||||||
|
})
|
||||||
.realize('restart', () => {
|
.realize('restart', () => {
|
||||||
core.confirmRestart();
|
core.confirmRestart();
|
||||||
})
|
})
|
||||||
|
@ -34,13 +34,11 @@ fixedUi.register(
|
|||||||
new GameUi('start', UI.Start),
|
new GameUi('start', UI.Start),
|
||||||
new GameUi('toolbar', UI.Toolbar),
|
new GameUi('toolbar', UI.Toolbar),
|
||||||
new GameUi('load', UI.Load),
|
new GameUi('load', UI.Load),
|
||||||
new GameUi('danmaku', UI.Danmaku)
|
new GameUi('danmaku', UI.Danmaku),
|
||||||
|
new GameUi('danmakuEditor', UI.DanmakuEditor)
|
||||||
);
|
);
|
||||||
fixedUi.showAll();
|
fixedUi.showAll();
|
||||||
|
|
||||||
let loaded = false;
|
|
||||||
let mounted = false;
|
|
||||||
|
|
||||||
const hook = Mota.require('var', 'hook');
|
const hook = Mota.require('var', 'hook');
|
||||||
hook.once('mounted', () => {
|
hook.once('mounted', () => {
|
||||||
const ui = document.getElementById('ui-main')!;
|
const ui = document.getElementById('ui-main')!;
|
||||||
@ -73,6 +71,4 @@ hook.once('mounted', () => {
|
|||||||
fixedUi.on('end', () => {
|
fixedUi.on('end', () => {
|
||||||
fixed.style.display = 'none';
|
fixed.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
mounted = true;
|
|
||||||
});
|
});
|
||||||
|
@ -523,7 +523,7 @@ loading.once('coreInit', () => {
|
|||||||
'ui.danmaku': storage.getValue('ui.danmaku', true),
|
'ui.danmaku': storage.getValue('ui.danmaku', true),
|
||||||
'ui.danmakuSpeed': storage.getValue(
|
'ui.danmakuSpeed': storage.getValue(
|
||||||
'ui.danmakuSpeed',
|
'ui.danmakuSpeed',
|
||||||
Math.floor(window.innerWidth / 25) * 5
|
Math.floor(window.innerWidth / 30) * 5
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -27,6 +27,7 @@ import * as frag from '@/plugin/fx/frag';
|
|||||||
import * as use from '@/plugin/use';
|
import * as use from '@/plugin/use';
|
||||||
import * as gameCanvas from '@/plugin/fx/gameCanvas';
|
import * as gameCanvas from '@/plugin/fx/gameCanvas';
|
||||||
import * as smooth from '@/plugin/fx/smoothView';
|
import * as smooth from '@/plugin/fx/smoothView';
|
||||||
|
import * as animateController from '@/plugin/animateController';
|
||||||
|
|
||||||
Mota.Plugin.register('shadow_r', shadow, shadow.init);
|
Mota.Plugin.register('shadow_r', shadow, shadow.init);
|
||||||
Mota.Plugin.register('gameShadow_r', gameShadow, gameShadow.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('use_r', use);
|
||||||
Mota.Plugin.register('gameCanvas_r', gameCanvas);
|
Mota.Plugin.register('gameCanvas_r', gameCanvas);
|
||||||
Mota.Plugin.register('smooth_r', smooth, smooth.init);
|
Mota.Plugin.register('smooth_r', smooth, smooth.init);
|
||||||
|
Mota.Plugin.register(
|
||||||
|
'animateController_r',
|
||||||
|
animateController,
|
||||||
|
animateController.default
|
||||||
|
);
|
||||||
|
@ -8,8 +8,9 @@ import axios from 'axios';
|
|||||||
import { decompressFromBase64 } from 'lz-string';
|
import { decompressFromBase64 } from 'lz-string';
|
||||||
import { parseColor } from './webgl/utils';
|
import { parseColor } from './webgl/utils';
|
||||||
import { Keyboard, KeyboardEmits } from '@/core/main/custom/keyboard';
|
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 { isAssist } from '@/core/main/custom/hotkey';
|
||||||
|
import { logger } from '@/core/common/logger';
|
||||||
|
|
||||||
type CanParseCss = keyof {
|
type CanParseCss = keyof {
|
||||||
[P in keyof CSSStyleDeclaration as CSSStyleDeclaration[P] extends string
|
[P in keyof CSSStyleDeclaration as CSSStyleDeclaration[P] extends string
|
||||||
@ -82,20 +83,110 @@ export function keycode(key: number) {
|
|||||||
* @param css 要解析的css字符串
|
* @param css 要解析的css字符串
|
||||||
*/
|
*/
|
||||||
export function parseCss(css: string): Partial<Record<CanParseCss, string>> {
|
export function parseCss(css: string): Partial<Record<CanParseCss, string>> {
|
||||||
const str = css.replace(/[\n\s\t]*/g, '').replace(/;*/g, ';');
|
if (css.length === 0) return {};
|
||||||
const styles = str.split(';');
|
|
||||||
|
let pointer = -1;
|
||||||
|
let inProp = true;
|
||||||
|
let prop = '';
|
||||||
|
let value = '';
|
||||||
|
let upper = false;
|
||||||
const res: Partial<Record<CanParseCss, string>> = {};
|
const res: Partial<Record<CanParseCss, string>> = {};
|
||||||
|
|
||||||
for (const one of styles) {
|
while (++pointer < css.length) {
|
||||||
const [key, data] = one.split(':');
|
const char = css[pointer];
|
||||||
const cssKey = key.replace(/\-([a-z])/g, (str, $1) =>
|
|
||||||
$1.toUpperCase()
|
if ((char === ' ' || char === '\n' || char === '\r') && inProp) {
|
||||||
) as CanParseCss;
|
continue;
|
||||||
res[cssKey] = data;
|
}
|
||||||
|
|
||||||
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function stringifyCSS(css: Partial<Record<CanParseCss, string>>) {
|
||||||
|
let str = '';
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(css)) {
|
||||||
|
let pointer = -1;
|
||||||
|
let prop = '';
|
||||||
|
while (++pointer < key.length) {
|
||||||
|
const char = key[pointer];
|
||||||
|
if (char.toLowerCase() === char) {
|
||||||
|
prop += char;
|
||||||
|
} else {
|
||||||
|
prop += `-${char.toLowerCase()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
str += `${prop}:${value};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用打字机效果显示一段文字
|
* 使用打字机效果显示一段文字
|
||||||
* @param str 要打出的字符串
|
* @param str 要打出的字符串
|
||||||
@ -401,3 +492,22 @@ let num = 0;
|
|||||||
export function requireUniqueSymbol() {
|
export function requireUniqueSymbol() {
|
||||||
return num++;
|
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
624
src/ui/danmakuEditor.vue
Normal 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>
|
@ -24,3 +24,4 @@ export { default as Toolbar } from './toolbar.vue';
|
|||||||
export { default as ToolEditor } from './toolEditor.vue';
|
export { default as ToolEditor } from './toolEditor.vue';
|
||||||
export { default as Load } from './load.vue';
|
export { default as Load } from './load.vue';
|
||||||
export { default as Danmaku } from './danmaku.vue';
|
export { default as Danmaku } from './danmaku.vue';
|
||||||
|
export { default as DanmakuEditor } from './danmakuEditor.vue';
|
||||||
|
Loading…
Reference in New Issue
Block a user