import { DMessage, DTips } from '../models/index.js';
import { FHelp } from '../utils/index.js';
import { UBaseController } from './base.js';
/**
* @class
* @summary tips 控制器类
* @classdesc 用于控制 tips 相关的的控制器, 例如控制提示框淡入淡出, 以及消息的显示等等
* @extends UBaseController
* @memberof module:controller
* @alias UTipsController
*/
export class UTipsController extends UBaseController {
/**
* 提示数据集合, 用于存储提示数据, 以及消息数据
* @summary tips 数据
* @protected
* @type {DTips}
*/
_data;
/**
* stage 中的消息提示框元素
* @summary 提示框元素
* @protected
* @type {HTMLElement}
*/
_tips;
/**
* 消息数据集合, 用于存储消息数据, 包括但不限于 DMessage
* @summary 消息数据集
* @protected
* @type {DMessage[]}
* @default []
*/
_messages;
/**
* 在提示框显示期间的定时器 id
* @summary 显示定时器 id
* @protected
* @type {?number}
* @default null
*/
_showId;
/**
* 在提示框隐藏期间的定时器 id
* @summary 隐藏定时器 id
* @protected
* @type {?number}
* @default null
*/
_hiddenId;
/**
* 在提示框显示时期需要显示的文本
* @summary 消息文本
* @protected
* @type {string}
* @default ''
*/
_text;
/**
* 是否停止 tips 的淡入淡出循环, 如果需要停止的话则需要等待淡出之后才会生效
* @summary 停止提示框循环
* @type {boolean}
* @protected
* @default false
*/
_stop;
/**
* 创建 live2d tips 控制器
* @summary tips 控制器构造
* @constructor
* @param {ULive2dController} live2d live2d 上下文
* @param {DTips | null} [data=null] tips 数据
*/
constructor(live2d, data = null) {
super(live2d);
this._data = FHelp.mergeAll(new DTips(), data ?? {});
this._messages = this._data.message.map(t => new DMessage(t));
this._tips = this.live2d.stage.tips;
this._stop = false;
this._text = '';
this._showId = null;
this._hiddenId = null;
// 提示框
const tips = this.live2d.stage.tips;
const { minWidth, minHeight, offsetX, offsetY } = this.data;
tips.style.minWidth = `${ minWidth }px`;
tips.style.minHeight = `${ minHeight }px`;
tips.style.setProperty('--tips-offset-x', `${ offsetX }px`);
tips.style.setProperty('--tips-offset-y', `${ offsetY }px`);
}
/**
* getter: 消息提示数据
* @summary tips 数据
* @type {DTips}
* @readonly
*/
get data() {
return this._data;
}
/**
* getter: 提示框显示时的持续时间, 单位 ms
* @summary 显示时的持续时间
* @type {number}
* @readonly
*/
get duration() {
return this._data.duration;
}
/**
* getter: 提示框隐藏时的持续时间, 单位 ms
* @summary 隐藏时的持续时间
* @type {number}
* @readonly
*/
get interval() {
return this._data.interval;
}
/**
* getter: 所有消息提示数据集合
* @summary 消息数据集
* @type {DMessage[]}
* @readonly
*/
get messages() {
return this._messages;
}
/**
* getter: 提示框显示时期的文本值
* @summary 提示框文本
* @type {string}
* @readonly
*/
get text() {
return this._text;
}
/**
* getter: 是否停止 tips 的淡入淡出循环, 如果需要停止的话则需要等待淡出之后才会生效
* @summary 停止提示框循环
* @type {boolean}
* @readonly
*/
get stop() {
return this._stop;
}
/**
* 初始化 tips 控制器, 并开始提示框的淡入淡出
* @summary 初始化tips控制器
* @override
*/
init() {
this._hiddenId = setTimeout(this.startFade.bind(this), this.interval);
}
/**
* 移除绑定事件, 停止淡入淡出, 以及销毁消息集合
* @summary 销毁控制器
* @override
*/
destroy() {
super.destroy();
this.stopFade();
this._messages = [];
}
/**
* 开始淡入提示框, 淡入完成后等待一段时间执行淡出
*
* 如果 `inherit = true`, 则继承原有的时间, 否则重新开始计时
*
* 如果消息集合为空, 则会一直循环等待消息的添加, 而循环时长为隐藏时长
* @summary 提示框淡入
* @param {boolean} [inherit=false] 继承原有时间
* @return {Promise<void>}
* @async
*/
async fadeIn(inherit = false) {
// 已经停止则返回
if (this._stop) return;
// 需要检测且句柄有效则返回
if (inherit && this._showId > 0) return;
// 清除之前的定时
this.#clearTime();
// 检测消息数量
if (this._messages.length <= 0) {
// 循环等待
this._showId = setTimeout(() => {
// 清除句柄
this._showId = null;
this.fadeIn().catch(FHelp.F);
}, this.interval);
return;
}
// 设置显示的文本
this._tips.innerText = this._text = this.getRandomMessage();
await this.live2d.stage.fadeIn(this._tips);
// 淡入完成后计时
this._showId = setTimeout(() => {
// 清除句柄
this._showId = null;
// 淡出失败不负责
this.fadeOut().catch(FHelp.F);
}, this.duration);
}
/**
* 开始淡出提示框, 淡出完成后等待一段时间执行淡入
*
* 如果 `inherit = true`, 则继承原有的时间, 否则重新开始计时
* @summary 提示框淡出
* @param {boolean} [inherit=fale] 继承原有时间
* @return {Promise<void>}
* @async
*/
async fadeOut(inherit = false) {
// 需要检测且句柄有效则返回
if (inherit && this._hiddenId > 0) return;
// 清除之前的定时
this.#clearTime();
await this.live2d.stage.fadeOut(this._tips);
// 淡出完成后计时
this._hiddenId = setTimeout(() => {
// 清除句柄
this._hiddenId = null;
// 淡出失败不负责
this.fadeIn().catch(FHelp.F);
}, this.interval);
}
/**
* 开始进行淡入, 并将 `stop = false`, 之后恢复淡入淡出循环
* @summary 结束淡入淡出
*/
startFade() {
this._stop = false;
this.fadeIn().catch(FHelp.F);
}
/**
* 立即进行淡出, 并将 `stop = true`, 淡出完成后停止淡入淡出
* @summary 结束淡入淡出
*/
stopFade() {
this.fadeOut().catch(FHelp.F);
this._stop = true;
}
/**
* 立即淡入提示框显示对应的消息, 并且重置提示框显示时长, 完成后从消息集合中移除对应的消息
* @summary 通知消息
* @param {string} text 需要显示的文本
* @async
*/
async notify(text) {
const mes = new DMessage();
mes.text = this._text = text;
mes.priority = 99999;
// 添加到消息队列中
this.addMessage(mes);
await this.fadeIn().catch(FHelp.F).finally(() => this.removeMessage(mes));
}
/**
* 将消息集添加到消息列表中
*
* 通常这并不会立即显示, 而是等待下一轮提示框显示时根据一定概率随机抽取消息进行显示
* @summary 添加消息
* @param {...DMessage} messages 消息集
* @return {UTipsController} 自身引用
*/
addMessage(...messages) {
this._messages.push(...messages.filter(mes => FHelp.is(DMessage, mes)));
return this;
}
/**
* 从消息列表中移除对应的消息
* @summary 移除消息
* @param {...DMessage} message 消息集
* @return {UTipsController} 自身引用
*/
removeMessage(...message) {
for (const mes of message) {
const index = this._messages.indexOf(mes);
if (index >= 0) {
this._messages.splice(index, 1);
}
}
return this;
}
/**
* 从消息列表中安装一定概率随机获取消息
* @summary 随机获取消息
* @return {string} 消息文本
*/
getRandomMessage() {
// 通过 condition 筛选出一部分, 并以优先级精选分组
const list = this._messages.filter(m => m.condition == null || m.condition());
/** @type {Record<number, DMessage[]> } */
const group = FHelp.groupBy((m) => m.priority, list);
// 获取数量占比的函数, 以优先级为主, 数量其次
const getLength = (key) => {
const length = Math.ceil(group[key].length / 5) + Math.max(key, 0);
return { length };
};
// 优先级列表, 优先级小的在前面
const priority = Object.keys(group)
.map(m => parseInt(m))
.sort()
.map(p => Array.from(getLength(p), () => p))
.flat();
if (priority.length <= 0) {
// 没有消息则返回 text
return this._text;
}
// 从优先级列表随机选取一个值
const key = priority[FHelp.random(0, priority.length, 'floor')];
// 对应的目标数组
const targets = group[key];
// 随机获取需要显示的消息
const target = targets[FHelp.random(0, targets.length, 'floor')];
if (FHelp.is(Array, target.text)) {
return target.text[FHelp.random(0, target.text.length, 'floor')];
}
return target.text;
}
/**
* 清除显示定时器 id, 以及隐藏定时器 id
* @summary 清除时间句柄
* @private
*/
#clearTime() {
clearInterval(this._showId);
clearInterval(this._hiddenId);
this._showId = null;
this._hiddenId = null;
}
}