import { DModel } from '../models/index.js';
import { EEvent, FHelp } from '../utils/index.js';
import { UBaseController } from './base.js';
/**
* @class
* @summary model 控制器类
* @classdesc 用于控制模型相关的的控制器, 例如加载模型, 切换模型等等
* @extends UBaseController
* @memberof module:controller
* @alias UModelController
*/
export class UModelController extends UBaseController {
/**
* 所有的模型数据, 用于存储对应的模型数据
* @summary 模型数据集
* @protected
* @type {TModels}
*/
_data;
/**
* 当前模型在模型集中的位置索引
* @summary 模型索引
* @protected
* @type {number}
*/
_modelId;
/**
* 当前模型贴图在模型集中的位置索引
* @summary 贴图索引
* @protected
* @type {number}
*/
_textureId;
/**
* 模型加载完成后的模型实例
* @summary 模型实例
* @protected
* @type {?TLive2DModel}
* @default null
*/
_model = null;
/**
* 当前模型正在执行的 motion
* @summary 模型 motion
* @protected
* @type {?string}
* @default null
*/
_currentMotion = null;
/**
* 创建 live2d model 控制器
* @summary model 控制器构造
* @constructor
* @param {ULive2dController} live2d live2d 上下文
* @param {TModels} [data=[]] 模型数据集
*/
constructor(live2d, data = []) {
super(live2d);
const fun = (data) => FHelp.is(Array, data) ? data.map(fun) : new DModel(data);
this._data = FHelp.is(Array, data) ? data.map(fun) : [];
// 设置索引
this.modelId = FHelp.defaultTo(0, parseInt(localStorage.getItem('model-id')));
this.textureId = FHelp.defaultTo(0, parseInt(localStorage.getItem('texture-id')));
}
/**
* getter: 所有的模型数据
* @summary 模型数据
* @type {TModels}
* @readonly
*/
get data() {
return this._data;
}
/**
* 当前模型在模型集中的位置索引
* @summary 模型索引
* @type {number}
*/
get modelId() {
return this._modelId;
}
/**
* @param {number} index 模型索引
*/
set modelId(index) {
this._modelId = FHelp.clamp(0, Math.max(0, this._data.length - 1), index);
localStorage.setItem('model-id', `${ this._modelId }`);
}
/**
* 当前模型贴图在模型集中的位置索引
* @summary 贴图索引
* @type {number}
*/
get textureId() {
return this._textureId;
}
/**
* @param {number} index 贴图索引
*/
set textureId(index) {
this._textureId = FHelp.clamp(0, this.#getOutfitMaxIndex(), index);
localStorage.setItem('texture-id', `${ this._textureId }`);
}
/**
* 当前模型索引对应的模型数据项目
* @summary 模型数据项目
* @type {TModelItem}
* @readonly
*/
get modelData() {
return this._data[this.modelId];
}
/**
* 获取当前正在展示的 live2d 模型实例, 只有在模型加载完成后才不为 null
* @summary 模型实例
* @type {?TLive2DModel}
* @readonly
*/
get model() {
return this._model;
}
/**
* 当前模型正在执行的 motion, 未执行时为 null
* @summary 模型 motion
* @type {?string}
* @readonly
*/
get currentMotion() {
return this._currentMotion;
}
/**
* 当前正在展示的模型数据中定义的背景颜色, 默认为 transparent
* @summary 模型背景色
* @type {string}
* @readonly
*/
get backgroundColor() {
let data = this.modelData;
if (FHelp.is(Array, data)) {
data = data[this.textureId];
}
return data?.backgroundColor ?? 'transparent';
}
/**
* 在 model 控制器初始化时开始加载与 modelId 及 textureId 对应的模型
* @summary 初始化 model 控制器
* @override
*/
init() {
this.loadModel(this.modelId, this.textureId).catch(FHelp.F);
}
/**
* 销毁模型实例, 移除模型数据, 以及移除绑定的事件
* @summary 销毁 model 控制器
* @override
*/
destroy() {
super.destroy();
this._data = null;
this._model?.destroy();
this._model = null;
this._currentMotion = null;
}
/**
* 加载与 modelId 及 textureId 对应的模型
* @summary 加载模型
* @param {number} modelId 模型索引
* @param {number} [textureId=0] 模型服装索引
* @return {Promise<void>}
* @fires EEvent#modelStart 模型开始加载事件
* @fires EEvent#modelError 模型加载错误事件
* @fires EEvent#modelLoad 模型加载成功事件
* @async
*/
async loadModel(modelId, textureId = 0) {
let current = this._data[modelId];
const stage = this.app.stage;
const event = this.event;
current = FHelp.is(Array, current) ? current[textureId] : current;
if (FHelp.isNotValid(current)) {
event.emit(EEvent.modelError, Error('没有找到模型哦'));
return;
}
// 开始加载事件
event.emit(EEvent.modelStart);
// 获取URL
let url = current.path;
if (FHelp.isNotValid(url)) {
url = 'https://fastly.jsdelivr.net/gh/Eikanya/Live2d-model/%E5%B0%91%E5%A5%B3%E5%89%8D%E7%BA%BF%20girls%20Frontline/live2dold/old/kp31/normal/model.json';
}
// 移除上一个模型
this._model = null;
stage.removeChildren(0, stage.children.length);
/** @type {TLive2DModel | any} */
const model = await ILive2DModel.from(url, {
onError: (e) => {
event.emit(EEvent.modelError, e);
}
});
this._model = model;
// 加载完成后更新索引
this.modelId = modelId;
this.textureId = textureId;
// 设置模型数据
model.x = current.position?.x ?? 0;
model.y = current.position?.y ?? 0;
model.scale.set(0.15 * (current.scale ?? 1));
// 添加到舞台
stage.addChild(model);
// 如果模型数据中有定义宽高, 则直接设置模型的宽高, 否则使用模型加载后自己的宽高
current.width && (model.width = current.width);
current.height && (model.height = current.height);
// 绑定动作
this.motion();
// 发出事件
event.emit(EEvent.modelLoad, { width: model.width, height: model.height });
}
/**
* 切换与 modelId 及 textureId 对应的模型
* @summary 切换模型
* @param {number} id 模型索引
* @param {number} [textureId=0] 模型服装索引
* @return {Promise<void>}
* @async
*/
async switchModel(id, textureId = 0) {
const stage = this.live2d.stage;
// 此时 wrapper 不为 display:none , 可以获取宽高数据, canvas 渲染不会出错
await stage.fadeOut(stage.canvas);
await this.loadModel(id, textureId);
await stage.fadeIn(stage.canvas);
}
/**
* 喀什切换模型数据集中的下一个模型
* @summary 下一个模型
* @return {Promise<void>}
* @async
*/
async nextModel() {
const max = this._data.length - 1;
if (max <= 0) {
// 通知: 没有其它模型哦
await this.live2d.tips.notify('没有其它模型哦');
return;
}
await this.switchModel(this.modelId >= max ? 0 : this.modelId + 1, 0);
}
/**
* 开始切换模型的下一个服装
* @summary 下一个服装
* @return {Promise<void>}
* @async
*/
async nextTexture() {
const max = this.#getOutfitMaxIndex();
if (max <= 0) {
// 通知: 没有其它服装哦
await this.live2d.tips.notify('没有其它服装哦');
return;
}
await this.switchModel(this.modelId, this.textureId >= max ? 0 : this.textureId + 1);
}
/**
* 重新加载当前模型
* @summary 重置模型
* @return {Promise<void>}
* @async
*/
async resetModel() {
await this.loadModel(this.modelId, this.textureId);
}
/**
* 在模型加载完成后绑定模型的 `hit` 事件, 并在点击时触发对应的 motion, 同时绑定 motionStart 与 motionFinish
* @summary 绑定 motion
* @return {UModelController} 自身引用
* @fires EEvent#motionStart 模型运动开始事件
* @fires EEvent#motionFinish 模型运动完成事件
*/
motion() {
this.model.on('hit', /**@param {string[]} hitAreas*/async (hitAreas) => {
// 无法自动加载 motion, 不知是什么原因
await this.model.motion(`tap_${ hitAreas[0] }`) || await this.model.motion(hitAreas[0]);
});
this.model.internalModel.motionManager.on('motionStart', (group, index, audio) => {
// 设置音量
audio && (audio.volume = this.modelData.volume ?? 0.5);
this._currentMotion = group;
this.event.emit(EEvent.motionStart, group, index, audio);
});
this.model.internalModel.motionManager.on('motionFinish', () => {
this._currentMotion = null;
this.event.emit(EEvent.motionFinish);
});
return this;
}
/**
* 判断当前模型是否有其他服装
* @summary 是否有其他服装
* @return {boolean} true: 有其他服装, false: 没有其他服装
*/
hasOutfit() {
let current = this.modelData;
return FHelp.is(Array, current) && current.length > 1;
}
/**
* 获取当前模型服装的最大索引
* @summary 服装最大索引
* @return {number} 最大索引
* @private
*/
#getOutfitMaxIndex() {
let current = this.modelData;
return FHelp.is(Array, current) ? Math.max(0, current.length - 1) : 0;
}
}