HumanBreak/src/core/main/custom/danmaku.ts

528 lines
14 KiB
TypeScript
Raw Normal View History

2024-04-22 23:08:46 +08:00
import BoxAnimate from '@/components/boxAnimate.vue';
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';
2024-04-23 21:43:26 +08:00
import axios, { AxiosResponse, toFormData } from 'axios';
import { Component, VNode, h, shallowReactive } from 'vue';
2024-04-22 23:08:46 +08:00
/* @__PURE__ */ import { id, password } from '../../../../user';
2024-04-22 23:27:23 +08:00
import { mainSetting } from '../setting';
2024-04-22 23:08:46 +08:00
type CSSObj = Partial<Record<CanParseCss, string>>;
interface DanmakuResponse extends ResponseBase {
total: number;
list: DanmakuInfo[];
}
interface DanmakuInfo {
id: number;
comment: string;
tags: string;
love: number;
}
interface DanmakuPostInfo extends Partial<DanmakuContentInfo> {
type: 1 | 2 | 3;
towername: 'HumanBreak';
id?: number;
}
interface DanmakuContentInfo {
comment: string;
tags: string;
}
interface PostDanmakuResponse extends ResponseBase {
id: number;
}
interface PostLikeResponse extends ResponseBase {
liked: boolean;
}
interface DanmakuEvent extends EmitableEvent {
showStart: (danmaku: Danmaku) => void;
showEnd: (danmaku: Danmaku) => void;
like: (liked: boolean, danmaku: Danmaku) => void;
}
type SpecContentFn = (content: string, type: string) => VNode;
2024-04-23 21:43:26 +08:00
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;
}
}
};
2024-04-22 23:08:46 +08:00
export class Danmaku extends EventEmitter<DanmakuEvent> {
static backend: string = `/backend/tower/barrage.php`;
static all: Set<Danmaku> = new Set();
2024-04-23 21:43:26 +08:00
static allInPos: Partial<Record<FloorIds, Record<LocString, Danmaku[]>>> =
{};
2024-04-22 23:08:46 +08:00
static showList: Danmaku[] = shallowReactive([]);
static showMap: Map<number, Danmaku> = new Map();
static specList: Record<string, SpecContentFn> = {};
2024-04-23 21:43:26 +08:00
static lastEditoredDanmaku?: Danmaku;
2024-04-22 23:08:46 +08:00
id: number = -1;
text: string = '';
x: number = 0;
y: number = 0;
floor?: FloorIds;
showing: boolean = false;
likedNum: number = 0;
liked: boolean = false;
style: CSSObj = {};
textColor: string = 'white';
strokeColor: string = 'black';
private posted: boolean = false;
private vNode?: VNode;
private posting: boolean = false;
/**
*
* @returns Axios Post Promise
*/
2024-04-23 21:43:26 +08:00
async post(): Promise<AxiosResponse<PostDanmakuResponse>> {
2024-04-22 23:08:46 +08:00
if (this.posted || this.posting) {
logger.warn(5, `Repeat post danmaku.`);
2024-04-23 21:43:26 +08:00
return Promise.reject();
2024-04-22 23:08:46 +08:00
}
const data: DanmakuPostInfo = {
type: 2,
towername: 'HumanBreak',
...this.encode()
};
this.posting = true;
const form = toFormData(data);
/* @__PURE__ */ form.append('userid', id);
/* @__PURE__ */ form.append('password', password);
2024-04-22 23:27:23 +08:00
try {
const res = await axios.post<PostDanmakuResponse>(
Danmaku.backend,
form
);
2024-04-22 23:08:46 +08:00
2024-04-22 23:27:23 +08:00
this.id = res.data.id;
this.posting = false;
2024-04-23 21:43:26 +08:00
if (res.data.code === 0) {
this.posted = true;
tip('success', '发送成功');
this.addToList();
} else {
tip('error', res.data.message);
}
2024-04-22 23:27:23 +08:00
return res;
} catch (e) {
this.posted = false;
this.posting = false;
logger.error(
3,
`Unexpected error when posting danmaku. Error info: ${e}`
);
2024-04-23 21:43:26 +08:00
return Promise.reject();
2024-04-22 23:27:23 +08:00
}
2024-04-22 23:08:46 +08:00
}
/**
*
*/
encode(): DanmakuContentInfo {
const css = this.getEncodedCSS();
return {
comment: this.text,
tags: JSON.stringify([
`!css:${JSON.stringify(css)}`,
`!pos:${this.x},${this.y},${this.floor}`
])
};
}
/**
*
* @param info
*/
decode(info: DanmakuContentInfo) {
this.text = info.comment;
ensureArray(JSON.parse(info.tags) as string[]).forEach(v => {
if (v.startsWith('!css:')) {
this.style = JSON.parse(v.slice(5));
} else if (v.startsWith('!pos:')) {
const [x, y, f] = v.slice(5).split(',');
this.x = parseInt(x);
this.y = parseInt(y);
this.floor = f as FloorIds;
} else {
logger.warn(3, `Unknown danmaku tag: ${v}`);
}
});
}
getEncodedCSS() {
const css = JSON.parse(JSON.stringify(this.style)) as CSSObj;
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}`;
2024-04-23 21:43:26 +08:00
return { ...css, ...this.style };
2024-04-22 23:08:46 +08:00
}
/**
*
* @param fill
* @param stroke
*/
color(fill?: string, stroke?: string) {
fill && (this.textColor = fill);
stroke && (this.strokeColor = stroke);
}
/**
*
* @param icon id
*/
addIcon(icon: AllIds) {
this.text += `[i:${icon}]`;
}
/**
* css信息
* @param str css字符串
* @param overwrite css
*/
css(str: string, overwrite?: boolean): void;
/**
* css信息
* @param str css对象 CSSStyleDeclaration
* @param overwrite css
*/
css(obj: CSSObj, overwrite?: boolean): void;
css(obj: string | CSSObj, overwrite: boolean = false) {
const res = typeof obj === 'string' ? parseCss(obj) : obj;
2024-04-23 21:43:26 +08:00
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(',')}`
);
2024-04-22 23:08:46 +08:00
}
}
/**
*
*/
addToList() {
Danmaku.all.add(this);
if (!this.floor) return;
Danmaku.allInPos[this.floor] ??= {};
2024-04-23 21:43:26 +08:00
Danmaku.allInPos[this.floor]![`${this.x},${this.y}`] ??= [];
Danmaku.allInPos[this.floor]![`${this.x},${this.y}`].push(this);
2024-04-22 23:08:46 +08:00
}
/**
* VNode
*/
parse() {
let pointer = -1;
let ignore = false;
let str = '';
let spec = false;
let specType = '';
let specTypeEnd = false;
let specContent = '';
const children: VNode[] = [];
while (++pointer < this.text.length) {
const char = this.text[pointer];
if (char === '\\' && !ignore) {
ignore = true;
continue;
}
if (ignore) {
str += char;
continue;
}
if (char === '[') {
spec = true;
children.push(h('span', str));
str = '';
continue;
}
if (char === ']') {
if (!spec) {
logger.warn(4, `Ignored a mismatched ']' in danmaku.`);
str += char;
} else {
spec = false;
specTypeEnd = false;
children.push(this.createSpecVNode(specType, specContent));
specType = '';
specContent = '';
}
continue;
}
if (spec) {
if (!specTypeEnd) {
if (char !== ':') {
specType += char;
} else {
specTypeEnd = true;
}
} else {
specContent += char;
}
continue;
}
str += char;
}
if (str.length > 0) {
children.push(h('span', str));
}
return h(
'span',
{ class: 'danmaku', style: this.getEncodedCSS() },
children
);
}
/**
* VNode
*/
getVNode(nocache: boolean = false) {
if (nocache) return (this.vNode = this.parse());
return this.vNode ?? (this.vNode = this.parse());
}
/**
*
*/
show() {
this.showing = true;
Danmaku.showList.push(this);
Danmaku.showMap.set(this.id, this);
this.emit('showStart', this);
}
/**
*
*/
showEnd() {
this.showing = false;
deleteWith(Danmaku.showList, this);
Danmaku.showMap.delete(this.id);
this.emit('showEnd', this);
}
/**
*
*/
async triggerLike() {
const post: DanmakuPostInfo = {
type: 3,
towername: 'HumanBreak',
id: this.id
};
const res = await axios.post<PostLikeResponse>(Danmaku.backend, post);
if (res.data.code !== 0) {
logger.severe(
2,
`Uncaught error in posting like info for danmaku. Danmaku id: ${this.id}.`
);
tip('error', `Error ${res.data.code}. ${res.data.message}`);
} else {
tip('success', res.data.message);
if (res.data.liked) {
this.liked = true;
this.likedNum++;
} else {
this.liked = false;
this.likedNum--;
}
this.emit('like', this.liked, this);
}
return res;
}
/**
*
*/
destroy() {
this.showEnd();
Danmaku.all.delete(this);
if (this.floor) {
const floor = Danmaku.allInPos[this.floor];
if (floor) {
delete floor[`${this.x},${this.y}`];
}
}
}
private createSpecVNode(type: string, content: string): VNode {
if (Danmaku.specList[type]) {
return Danmaku.specList[type](content, type);
} else {
logger.severe(1, `Unknown special danmaku element: ${type}.`);
}
return h('span');
}
2024-04-23 21:43:26 +08:00
/**
* 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;
}
2024-04-22 23:08:46 +08:00
/**
*
*/
static async fetch() {
const form = toFormData({
type: 1,
towername: 'HumanBreak'
});
/* @__PURE__ */ form.append('userid', id);
/* @__PURE__ */ form.append('password', password);
const data = await axios.post<DanmakuResponse>(Danmaku.backend, form);
data.data.list.forEach(v => {
const dan = new Danmaku();
dan.id = v.id;
dan.likedNum = v.love;
dan.decode(v);
dan.posted = true;
dan.addToList();
});
}
/**
*
* @param dan
*/
static show(dan: Danmaku) {
dan.show();
}
/**
*
* @param type
* @param fn VNode
*/
static registerSpecContent(type: string, fn: SpecContentFn) {
if (this.specList[type]) {
logger.warn(6, `Registered special danmaku element: ${type}`);
}
this.specList[type] = fn;
}
}
// 图标类型
Danmaku.registerSpecContent('i', content => {
const iconInfo = core.getBlockInfo(content as AllIds);
if (!iconInfo) {
return h(BoxAnimate as Component, {
id: 'greenSlime',
noborder: true,
width: 32,
height: 32
});
}
return h(BoxAnimate as Component, {
id: content,
noborder: true,
2024-04-23 21:43:26 +08:00
noAnimate: true,
2024-04-22 23:08:46 +08:00
width: 32,
height: iconInfo.height
});
});
/* @__PURE__ */ Danmaku.backend = `/danmaku`;
2024-04-22 23:27:23 +08:00
2024-04-22 23:08:46 +08:00
Mota.require('var', 'hook').once('reset', () => {
Danmaku.fetch();
});
2024-04-22 23:27:23 +08:00
// 勇士移动后显示弹幕
Mota.require('var', 'hook').on('moveOneStep', (x, y, floor) => {
const enabled = mainSetting.getValue('ui.danmaku', true);
if (!enabled) return;
const f = Danmaku.allInPos[floor];
if (f) {
const danmaku = f[`${x},${y}`];
if (danmaku) {
2024-04-23 21:43:26 +08:00
danmaku.forEach(v => {
setTimeout(() => {
v.show();
}, Math.random() * 1000);
});
2024-04-22 23:27:23 +08:00
}
}
});