mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-01-31 23:29:27 +08:00
feat: 弹幕系统
This commit is contained in:
parent
5b74a22494
commit
27230b3d47
3
.gitignore
vendored
3
.gitignore
vendored
@ -43,6 +43,7 @@ dam3.png
|
|||||||
dam4.png
|
dam4.png
|
||||||
meeting.md
|
meeting.md
|
||||||
|
|
||||||
special.csv
|
*.csv
|
||||||
script/special.ts
|
script/special.ts
|
||||||
|
script/people.ts
|
||||||
docs/
|
docs/
|
||||||
|
@ -46,6 +46,7 @@ function draw() {
|
|||||||
c.width = scale * w;
|
c.width = scale * w;
|
||||||
c.height = scale * h;
|
c.height = scale * h;
|
||||||
ctx.scale(scale, scale);
|
ctx.scale(scale, scale);
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
if (props.id === 'none') return;
|
if (props.id === 'none') return;
|
||||||
|
|
||||||
@ -73,6 +74,7 @@ onUnmounted(() => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
c = document.getElementById(`box-animate-${id}`) as HTMLCanvasElement;
|
c = document.getElementById(`box-animate-${id}`) as HTMLCanvasElement;
|
||||||
ctx = c.getContext('2d')!;
|
ctx = c.getContext('2d')!;
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
draw();
|
draw();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ const hideTipText = debounce(() => {
|
|||||||
|
|
||||||
export class Logger {
|
export class Logger {
|
||||||
level: LogLevel = LogLevel.LOG;
|
level: LogLevel = LogLevel.LOG;
|
||||||
|
enabled: boolean = true;
|
||||||
|
|
||||||
constructor(logLevel: LogLevel) {
|
constructor(logLevel: LogLevel) {
|
||||||
this.level = logLevel;
|
this.level = logLevel;
|
||||||
@ -55,7 +56,7 @@ export class Logger {
|
|||||||
* @param text 错误信息
|
* @param text 错误信息
|
||||||
*/
|
*/
|
||||||
error(code: number, text: string) {
|
error(code: number, text: string) {
|
||||||
if (this.level <= LogLevel.ERROR) {
|
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) {
|
||||||
logTip.style.color = 'lightcoral';
|
logTip.style.color = 'lightcoral';
|
||||||
@ -72,7 +73,7 @@ export class Logger {
|
|||||||
* @param text 警告信息
|
* @param text 警告信息
|
||||||
*/
|
*/
|
||||||
severe(code: number, text: string) {
|
severe(code: number, text: string) {
|
||||||
if (this.level <= LogLevel.SEVERE_WARNING) {
|
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) {
|
||||||
logTip.style.color = 'goldenrod';
|
logTip.style.color = 'goldenrod';
|
||||||
@ -89,7 +90,7 @@ export class Logger {
|
|||||||
* @param text 警告信息
|
* @param text 警告信息
|
||||||
*/
|
*/
|
||||||
warn(code: number, text: string) {
|
warn(code: number, text: string) {
|
||||||
if (this.level <= LogLevel.WARNING) {
|
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) {
|
||||||
logTip.style.color = 'gold';
|
logTip.style.color = 'gold';
|
||||||
@ -105,10 +106,18 @@ export class Logger {
|
|||||||
* @param text 日志信息
|
* @param text 日志信息
|
||||||
*/
|
*/
|
||||||
log(text: string) {
|
log(text: string) {
|
||||||
if (this.level <= LogLevel.LOG) {
|
if (this.level <= LogLevel.LOG && this.enabled) {
|
||||||
console.log(`[LOG] ${text}`);
|
console.log(`[LOG] ${text}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
this.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
this.enabled = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const logger = new Logger(LogLevel.LOG);
|
export const logger = new Logger(LogLevel.LOG);
|
||||||
|
@ -58,6 +58,7 @@ import KeyboardPanel from '@/panel/keyboard.vue';
|
|||||||
import { MCGenerator } from './main/layout';
|
import { MCGenerator } from './main/layout';
|
||||||
import { ResourceController } from './loader/controller';
|
import { ResourceController } from './loader/controller';
|
||||||
import { logger } from './common/logger';
|
import { logger } from './common/logger';
|
||||||
|
import { Danmaku } from './main/custom/danmaku';
|
||||||
|
|
||||||
// ----- 类注册
|
// ----- 类注册
|
||||||
Mota.register('class', 'AudioPlayer', AudioPlayer);
|
Mota.register('class', 'AudioPlayer', AudioPlayer);
|
||||||
@ -75,6 +76,7 @@ Mota.register('class', 'SoundEffect', SoundEffect);
|
|||||||
Mota.register('class', 'UiController', UiController);
|
Mota.register('class', 'UiController', UiController);
|
||||||
Mota.register('class', 'MComponent', MComponent);
|
Mota.register('class', 'MComponent', MComponent);
|
||||||
Mota.register('class', 'ResourceController', ResourceController);
|
Mota.register('class', 'ResourceController', ResourceController);
|
||||||
|
Mota.register('class', 'Danmaku', Danmaku);
|
||||||
// ----- 函数注册
|
// ----- 函数注册
|
||||||
Mota.register('fn', 'm', m);
|
Mota.register('fn', 'm', m);
|
||||||
Mota.register('fn', 'unwrapBinary', unwarpBinary);
|
Mota.register('fn', 'unwrapBinary', unwarpBinary);
|
||||||
|
@ -12,3 +12,8 @@ export interface Undoable<T> {
|
|||||||
*/
|
*/
|
||||||
redo(): T | undefined;
|
redo(): T | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResponseBase {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
430
src/core/main/custom/danmaku.ts
Normal file
430
src/core/main/custom/danmaku.ts
Normal file
@ -0,0 +1,430 @@
|
|||||||
|
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';
|
||||||
|
import axios, { toFormData } from 'axios';
|
||||||
|
import { Component, VNode, h, ref, shallowReactive } from 'vue';
|
||||||
|
/* @__PURE__ */ import { id, password } from '../../../../user';
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 showList: Danmaku[] = shallowReactive([]);
|
||||||
|
static showMap: Map<number, Danmaku> = new Map();
|
||||||
|
static specList: Record<string, SpecContentFn> = {};
|
||||||
|
|
||||||
|
num: number = Danmaku.num++;
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
async post() {
|
||||||
|
if (this.posted || this.posting) {
|
||||||
|
logger.warn(5, `Repeat post danmaku.`);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
const res = await axios.post<PostDanmakuResponse>(
|
||||||
|
Danmaku.backend,
|
||||||
|
form
|
||||||
|
);
|
||||||
|
|
||||||
|
this.id = res.data.id;
|
||||||
|
this.posting = false;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将弹幕整合为可以发送的格式
|
||||||
|
*/
|
||||||
|
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}`;
|
||||||
|
return css;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置文字的颜色
|
||||||
|
* @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;
|
||||||
|
if (overwrite) this.style = res;
|
||||||
|
else {
|
||||||
|
this.style = { ...this.style, ...res };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将这个弹幕添加至弹幕列表
|
||||||
|
*/
|
||||||
|
addToList() {
|
||||||
|
Danmaku.all.add(this);
|
||||||
|
if (!this.floor) return;
|
||||||
|
Danmaku.allInPos[this.floor] ??= {};
|
||||||
|
Danmaku.allInPos[this.floor]![`${this.x},${this.y}`] = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析这个弹幕为 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉取本塔所有弹幕
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
width: 32,
|
||||||
|
height: iconInfo.height
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* @__PURE__ */ Danmaku.backend = `/danmaku`;
|
||||||
|
Mota.require('var', 'hook').once('reset', () => {
|
||||||
|
Danmaku.fetch();
|
||||||
|
});
|
@ -33,7 +33,8 @@ fixedUi.register(
|
|||||||
new GameUi('completeAchi', UI.CompleteAchi),
|
new GameUi('completeAchi', UI.CompleteAchi),
|
||||||
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)
|
||||||
);
|
);
|
||||||
fixedUi.showAll();
|
fixedUi.showAll();
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import settingsText from '@/data/settings.json';
|
|||||||
import { isMobile } from '@/plugin/use';
|
import { isMobile } from '@/plugin/use';
|
||||||
import { fontSize } from '@/plugin/ui/statusBar';
|
import { fontSize } from '@/plugin/ui/statusBar';
|
||||||
import { CustomToolbar } from './custom/toolbar';
|
import { CustomToolbar } from './custom/toolbar';
|
||||||
|
import { fixedUi } from './init/ui';
|
||||||
|
|
||||||
export interface SettingComponentProps {
|
export interface SettingComponentProps {
|
||||||
item: MotaSettingItem;
|
item: MotaSettingItem;
|
||||||
@ -410,6 +411,12 @@ function handleUiSetting<T extends number | boolean>(key: string, n: T, o: T) {
|
|||||||
v.setSize(v.width * scale, v.height * scale);
|
v.setSize(v.width * scale, v.height * scale);
|
||||||
});
|
});
|
||||||
CustomToolbar.refreshAll(true);
|
CustomToolbar.refreshAll(true);
|
||||||
|
} else if (key === 'danmaku') {
|
||||||
|
if (n) {
|
||||||
|
fixedUi.open('danmaku');
|
||||||
|
} else {
|
||||||
|
fixedUi.closeByName('danmaku');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -478,6 +485,8 @@ mainSetting
|
|||||||
.setDisplayFunc('toolbarScale', value => `${value}%`)
|
.setDisplayFunc('toolbarScale', value => `${value}%`)
|
||||||
.register('bookScale', '怪物手册缩放', 100, COM.Number, [10, 500, 10])
|
.register('bookScale', '怪物手册缩放', 100, COM.Number, [10, 500, 10])
|
||||||
.setDisplayFunc('bookScale', value => `${value}%`)
|
.setDisplayFunc('bookScale', value => `${value}%`)
|
||||||
|
.register('danmaku', '显示弹幕', true, COM.Boolean)
|
||||||
|
.register('danmakuSpeed', '弹幕速度', 60, COM.Number, [10, 200, 5])
|
||||||
);
|
);
|
||||||
|
|
||||||
const loading = Mota.require('var', 'loading');
|
const loading = Mota.require('var', 'loading');
|
||||||
@ -511,6 +520,8 @@ loading.once('coreInit', () => {
|
|||||||
isMobile ? 50 : Math.floor((window.innerWidth / 1700) * 10) * 10
|
isMobile ? 50 : Math.floor((window.innerWidth / 1700) * 10) * 10
|
||||||
),
|
),
|
||||||
'ui.bookScale': storage.getValue('ui.bookScale', isMobile ? 100 : 80),
|
'ui.bookScale': storage.getValue('ui.bookScale', isMobile ? 100 : 80),
|
||||||
|
'ui.danmaku': storage.getValue('ui.danmaku', true),
|
||||||
|
'ui.danmakuSpeed': storage.getValue('ui.danmakuSpeed', 60),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -545,6 +556,8 @@ mainSetting
|
|||||||
.setDescription('ui.mapScale', `楼传小地图的缩放,百分比格式`)
|
.setDescription('ui.mapScale', `楼传小地图的缩放,百分比格式`)
|
||||||
.setDescription('ui.toolbarScale', `自定义工具栏的缩放比例`)
|
.setDescription('ui.toolbarScale', `自定义工具栏的缩放比例`)
|
||||||
.setDescription('ui.bookScale', `怪物手册界面中每个怪物框体的高度缩放,最小值限定为 20% 屏幕高度`)
|
.setDescription('ui.bookScale', `怪物手册界面中每个怪物框体的高度缩放,最小值限定为 20% 屏幕高度`)
|
||||||
|
.setDescription('ui.danmaku', '是否显示弹幕')
|
||||||
|
.setDescription('ui.danmakuSpeed', '弹幕速度,刷新或开关弹幕显示后起效')
|
||||||
.setDescription('screen.fontSizeStatus', `修改状态栏的字体大小`)
|
.setDescription('screen.fontSizeStatus', `修改状态栏的字体大小`)
|
||||||
.setDescription('screen.blur', '打开任意ui界面时是否有背景虚化效果,移动端打开后可能会有掉帧或者发热现象。关闭ui后生效');
|
.setDescription('screen.blur', '打开任意ui界面时是否有背景虚化效果,移动端打开后可能会有掉帧或者发热现象。关闭ui后生效');
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import type * as battle from './enemy/battle';
|
|||||||
import type * as hero from './hero';
|
import type * as hero from './hero';
|
||||||
import type * as damage from './enemy/damage';
|
import type * as damage from './enemy/damage';
|
||||||
import type { Logger } from '@/core/common/logger';
|
import type { Logger } from '@/core/common/logger';
|
||||||
|
import type { Danmaku } from '@/core/main/custom/danmaku';
|
||||||
|
|
||||||
interface ClassInterface {
|
interface ClassInterface {
|
||||||
// 渲染进程与游戏进程通用
|
// 渲染进程与游戏进程通用
|
||||||
@ -48,6 +49,7 @@ interface ClassInterface {
|
|||||||
Range: typeof Range;
|
Range: typeof Range;
|
||||||
EnemyCollection: typeof EnemyCollection;
|
EnemyCollection: typeof EnemyCollection;
|
||||||
DamageEnemy: typeof DamageEnemy;
|
DamageEnemy: typeof DamageEnemy;
|
||||||
|
Danmaku: typeof Danmaku;
|
||||||
}
|
}
|
||||||
|
|
||||||
type _IBattle = typeof battle;
|
type _IBattle = typeof battle;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { tip } from './utils';
|
||||||
|
|
||||||
export default function init() {
|
export default function init() {
|
||||||
return { useDrag, useWheel, useUp, isMobile };
|
return { useDrag, useWheel, useUp, isMobile };
|
||||||
}
|
}
|
||||||
@ -29,7 +31,8 @@ checkMobile();
|
|||||||
|
|
||||||
function checkMobile() {
|
function checkMobile() {
|
||||||
if (isMobile && !alerted) {
|
if (isMobile && !alerted) {
|
||||||
alert(
|
tip(
|
||||||
|
'info',
|
||||||
'手机端建议使用新版APP或者自带的浏览器进行游玩,并在进入游戏后开启游戏内的全屏设置游玩'
|
'手机端建议使用新版APP或者自带的浏览器进行游玩,并在进入游戏后开启游戏内的全屏设置游玩'
|
||||||
);
|
);
|
||||||
alerted = true;
|
alerted = true;
|
||||||
|
@ -119,3 +119,9 @@ div.toolbar-editor-item {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.danmaku {
|
||||||
|
margin-left: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
210
src/ui/danmaku.vue
Normal file
210
src/ui/danmaku.vue
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
<template>
|
||||||
|
<div id="danmaku-div">
|
||||||
|
<div
|
||||||
|
:id="`danmaku-${one.id}`"
|
||||||
|
class="danmaku-one"
|
||||||
|
v-for="one of Danmaku.showList"
|
||||||
|
:key="one.id"
|
||||||
|
@mouseenter="mousein(one.id)"
|
||||||
|
@mouseleave="mouseleave(one.id)"
|
||||||
|
@touchstart="touchStart(one.id)"
|
||||||
|
>
|
||||||
|
<span class="danmaku-info">
|
||||||
|
<like-filled
|
||||||
|
class="danmaku-like-icon"
|
||||||
|
:liked="one.liked"
|
||||||
|
@click="one.triggerLike()"
|
||||||
|
/>
|
||||||
|
<span class="danmaku-like-num">{{ one.likedNum }}</span>
|
||||||
|
</span>
|
||||||
|
<component :is="one.getVNode()"></component>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { nextTick, onUnmounted, reactive, watch } from 'vue';
|
||||||
|
import { Danmaku } from '../core/main/custom/danmaku';
|
||||||
|
import { LikeFilled } from '@ant-design/icons-vue';
|
||||||
|
import { Ticker } from 'mutate-animate';
|
||||||
|
import { mainSetting } from '@/core/main/setting';
|
||||||
|
import { debounce } from 'lodash-es';
|
||||||
|
|
||||||
|
interface ElementMap {
|
||||||
|
pos: number;
|
||||||
|
ele: HTMLDivElement;
|
||||||
|
danmaku: Danmaku;
|
||||||
|
style: CSSStyleDeclaration;
|
||||||
|
width: number;
|
||||||
|
hover: boolean;
|
||||||
|
top: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = Danmaku.showMap;
|
||||||
|
const eleMap: Map<number, ElementMap> = new Map();
|
||||||
|
const liked = reactive<Record<number, boolean>>({});
|
||||||
|
const ticker = new Ticker();
|
||||||
|
|
||||||
|
const speed = mainSetting.getValue('ui.danmakuSpeed', 60);
|
||||||
|
|
||||||
|
const likeFn = (l: boolean, d: Danmaku) => {
|
||||||
|
liked[d.id] = l;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(Danmaku.showList, list => {
|
||||||
|
list.forEach(v => {
|
||||||
|
liked[v.id] = v.liked;
|
||||||
|
v.on('like', likeFn);
|
||||||
|
if (!eleMap.has(v.id)) {
|
||||||
|
nextTick(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
addElement(v.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function addElement(id: number) {
|
||||||
|
const danmaku = map.get(id);
|
||||||
|
if (!danmaku) return;
|
||||||
|
const div = document.getElementById(`danmaku-${id}`);
|
||||||
|
if (!div) return;
|
||||||
|
|
||||||
|
const style = getComputedStyle(div);
|
||||||
|
|
||||||
|
const ele: ElementMap = {
|
||||||
|
danmaku,
|
||||||
|
pos: window.innerWidth + 100,
|
||||||
|
ele: div as HTMLDivElement,
|
||||||
|
style,
|
||||||
|
width: parseInt(style.width),
|
||||||
|
hover: false,
|
||||||
|
top: -1
|
||||||
|
};
|
||||||
|
|
||||||
|
eleMap.set(id, ele);
|
||||||
|
calTop(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastTime = 0;
|
||||||
|
ticker.add(time => {
|
||||||
|
const dt = (time - lastTime) / 1000;
|
||||||
|
const toDelete: number[] = [];
|
||||||
|
|
||||||
|
eleMap.forEach((value, id) => {
|
||||||
|
const { danmaku, ele, style, width, hover } = value;
|
||||||
|
if (!hover) {
|
||||||
|
const dx = dt * speed;
|
||||||
|
value.pos -= dx;
|
||||||
|
ele.style.left = `${value.pos.toFixed(2)}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.pos + width < 0) {
|
||||||
|
toDelete.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
lastTime = time;
|
||||||
|
|
||||||
|
toDelete.forEach(v => {
|
||||||
|
eleMap.delete(v);
|
||||||
|
const danmaku = map.get(v);
|
||||||
|
if (danmaku) {
|
||||||
|
danmaku.showEnd();
|
||||||
|
}
|
||||||
|
map.delete(v);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function mousein(id: number) {
|
||||||
|
const danmaku = eleMap.get(id)!;
|
||||||
|
danmaku.hover = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mouseleave(id: number) {
|
||||||
|
const danmaku = eleMap.get(id)!;
|
||||||
|
danmaku.hover = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const touchDebounce = debounce(mouseleave, 3000);
|
||||||
|
function touchStart(id: number) {
|
||||||
|
mousein(id);
|
||||||
|
touchDebounce(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calTop(id: number) {
|
||||||
|
const danmaku = eleMap.get(id)!;
|
||||||
|
const fontSize = mainSetting.getValue('screen.fontSize', 16) * 1.25 + 10;
|
||||||
|
|
||||||
|
const used: Set<number> = new Set();
|
||||||
|
eleMap.forEach(v => {
|
||||||
|
const { pos, width } = v;
|
||||||
|
if (
|
||||||
|
pos <= window.innerWidth + 125 &&
|
||||||
|
pos + width >= window.innerWidth + 75
|
||||||
|
) {
|
||||||
|
used.add(v.top);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let i = -1;
|
||||||
|
while (++i < 20) {
|
||||||
|
if (!used.has(i)) {
|
||||||
|
danmaku.top = i;
|
||||||
|
danmaku.ele.style.top = `${fontSize * i + 20}px`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
ticker.destroy();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
#danmaku-div {
|
||||||
|
position: fixed;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
overflow: visible;
|
||||||
|
font-size: 150%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-one {
|
||||||
|
position: fixed;
|
||||||
|
left: 100vw;
|
||||||
|
width: max-content;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
padding: 0 5px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-one:hover {
|
||||||
|
background-color: #0006;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-info {
|
||||||
|
text-shadow: 1px 1px 1px black, 1px -1px 1px black, -1px 1px 1px black,
|
||||||
|
-1px -1px 1px black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-like-num {
|
||||||
|
font-size: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-like-icon {
|
||||||
|
transition: color 0.1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-like-icon[liked='true'],
|
||||||
|
.danmaku-like-icon:hover {
|
||||||
|
color: aqua;
|
||||||
|
}
|
||||||
|
</style>
|
@ -23,3 +23,4 @@ export { default as Hotkey } from './hotkey.vue';
|
|||||||
export { default as Toolbar } from './toolbar.vue';
|
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';
|
||||||
|
2
user.ts
Normal file
2
user.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const id = 2691;
|
||||||
|
export const password = '26e631510147c1d0b71a368a3729df5a';
|
@ -75,7 +75,8 @@ export default defineConfig({
|
|||||||
rewrite(path) {
|
rewrite(path) {
|
||||||
return path.replace(/^\/forceTem/, '');
|
return path.replace(/^\/forceTem/, '');
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
'/danmaku': 'https://h5mota.com/backend/tower/barrage.php'
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
ignored: ['**/public/**']
|
ignored: ['**/public/**']
|
||||||
|
Loading…
Reference in New Issue
Block a user