You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
211 lines
8.0 KiB
211 lines
8.0 KiB
|
1 week ago
|
import { Component, Node, sp } from 'cc';
|
||
|
|
import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch';
|
||
|
|
|
||
|
|
/** 首页场景节点名 */
|
||
|
|
const NODE_SCENE = '首页场景';
|
||
|
|
/** 首页数字节点名 */
|
||
|
|
const NODE_NUM = '首页数字';
|
||
|
|
/** 数字待机动画 */
|
||
|
|
const ANIM_IDLE = '待机';
|
||
|
|
/** 数字场景变换动画 */
|
||
|
|
const ANIM_SWITCH = '场景变换';
|
||
|
|
/** 变换动画内切场景的事件名 */
|
||
|
|
const EVENT_SWITCH = '开始渐隐变换场景';
|
||
|
|
/** 场景皮肤列表 */
|
||
|
|
const SCENE_SKINS = ['初始', '熔浆', '覆水', '雪地'] as const;
|
||
|
|
/** 切换间隔下限(秒) */
|
||
|
|
const INTERVAL_MIN = 3;
|
||
|
|
/** 切换间隔上限(秒) */
|
||
|
|
const INTERVAL_MAX = 5;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 首页场景皮肤随机切换:间隔 3~5 秒,播「场景变换」,在事件帧切皮肤,播完回待机。
|
||
|
|
*/
|
||
|
|
export class PanelHomeSceneSwitchPresenter {
|
||
|
|
private _host: Component | null = null;
|
||
|
|
private _levelSelect: Node | null = null;
|
||
|
|
private _sceneSkel: sp.Skeleton | null = null;
|
||
|
|
private _numSkel: sp.Skeleton | null = null;
|
||
|
|
private _skinIndex = 0;
|
||
|
|
private _switching = false;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 绑定宿主与 ui_levelSelect,解析 Spine 并开局待机。
|
||
|
|
* @param host 用于 schedule 的组件(PanelHome)
|
||
|
|
* @param levelSelect ui_levelSelect
|
||
|
|
*/
|
||
|
|
bind(host: Component | null | undefined, levelSelect: Node | null | undefined): void {
|
||
|
|
this.unbind();
|
||
|
|
if (!host?.isValid || !levelSelect?.isValid) {
|
||
|
|
console.log('[PanelHomeSceneSwitch] 绑定参数无效');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this._host = host;
|
||
|
|
this._levelSelect = levelSelect;
|
||
|
|
this._sceneSkel = this._resolveSkel(NODE_SCENE);
|
||
|
|
this._numSkel = this._resolveSkel(NODE_NUM);
|
||
|
|
if (!this._sceneSkel || !this._numSkel) return;
|
||
|
|
this._skinIndex = this._resolveCurrentSkinIndex(this._sceneSkel);
|
||
|
|
this._playIdle();
|
||
|
|
this._scheduleNext();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 停止定时与切换,解除引用 */
|
||
|
|
unbind(): void {
|
||
|
|
this._switching = false;
|
||
|
|
const host = this._host;
|
||
|
|
if (host?.isValid) {
|
||
|
|
host.unschedule(this._onInterval);
|
||
|
|
host.unschedule(this._onSwitchEvent);
|
||
|
|
host.unschedule(this._onSwitchAnimDone);
|
||
|
|
}
|
||
|
|
this._host = null;
|
||
|
|
this._levelSelect = null;
|
||
|
|
this._sceneSkel = null;
|
||
|
|
this._numSkel = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 解析子节点 Spine */
|
||
|
|
private _resolveSkel(name: string): sp.Skeleton | null {
|
||
|
|
const root = this._levelSelect;
|
||
|
|
if (!root?.isValid) return null;
|
||
|
|
const node = root.getChildByName(name);
|
||
|
|
if (!node?.isValid) {
|
||
|
|
console.log('[PanelHomeSceneSwitch] 节点缺失', name);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
const skel = node.getComponent(sp.Skeleton);
|
||
|
|
if (!skel) {
|
||
|
|
console.log('[PanelHomeSceneSwitch] 缺少 sp.Skeleton', name);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
if (skel.skeletonData) {
|
||
|
|
ensureSpineSkeletonDataRuntime(skel.skeletonData);
|
||
|
|
}
|
||
|
|
return skel;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 读 Inspector defaultSkin 在列表中的下标;未知则 0 */
|
||
|
|
private _resolveCurrentSkinIndex(skel: sp.Skeleton): number {
|
||
|
|
const cur = String((skel as sp.Skeleton & { defaultSkin?: string }).defaultSkin ?? '').trim();
|
||
|
|
for (let i = 0; i < SCENE_SKINS.length; i++) {
|
||
|
|
if (SCENE_SKINS[i] === cur) return i;
|
||
|
|
}
|
||
|
|
console.log('[PanelHomeSceneSwitch] 当前皮肤不在列表,按初始处理', cur);
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 数字播待机循环 */
|
||
|
|
private _playIdle(): void {
|
||
|
|
const num = this._numSkel;
|
||
|
|
if (!num?.isValid) return;
|
||
|
|
if (num.skeletonData) ensureSpineSkeletonDataRuntime(num.skeletonData);
|
||
|
|
if (!num.findAnimation(ANIM_IDLE)) {
|
||
|
|
console.log('[PanelHomeSceneSwitch] 缺少动画', ANIM_IDLE);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
num.setAnimation(0, ANIM_IDLE, true);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 随机 3~5 秒后再切 */
|
||
|
|
private _scheduleNext(): void {
|
||
|
|
const host = this._host;
|
||
|
|
if (!host?.isValid) return;
|
||
|
|
host.unschedule(this._onInterval);
|
||
|
|
const delay = INTERVAL_MIN + Math.random() * (INTERVAL_MAX - INTERVAL_MIN);
|
||
|
|
host.scheduleOnce(this._onInterval, delay);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 读动画事件时刻(秒);无则 null */
|
||
|
|
private _findAnimEventTime(skel: sp.Skeleton, animName: string, eventName: string): number | null {
|
||
|
|
if (!skel?.isValid || !animName || !eventName) return null;
|
||
|
|
const sd = skel.skeletonData as sp.SkeletonData & { skeletonJson?: unknown; _skeletonJson?: unknown };
|
||
|
|
if (!sd?.isValid) return null;
|
||
|
|
try {
|
||
|
|
ensureSpineSkeletonDataRuntime(sd);
|
||
|
|
const raw = (sd.skeletonJson ?? sd._skeletonJson) as {
|
||
|
|
animations?: Record<string, { events?: Array<{ time?: number; name?: string }> }>;
|
||
|
|
} | null;
|
||
|
|
const events = raw?.animations?.[animName]?.events;
|
||
|
|
if (!events || events.length === 0) return null;
|
||
|
|
for (let i = 0; i < events.length; i++) {
|
||
|
|
const e = events[i];
|
||
|
|
if (String(e?.name ?? '') !== eventName) continue;
|
||
|
|
const t = Number(e.time);
|
||
|
|
if (!Number.isFinite(t) || t < 0) continue;
|
||
|
|
return t;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 间隔到:播场景变换,事件帧切皮,播完回待机 */
|
||
|
|
private _onInterval = (): void => {
|
||
|
|
if (this._switching) return;
|
||
|
|
const num = this._numSkel;
|
||
|
|
const host = this._host;
|
||
|
|
if (!num?.isValid || !host?.isValid) return;
|
||
|
|
if (num.skeletonData) ensureSpineSkeletonDataRuntime(num.skeletonData);
|
||
|
|
const anim = num.findAnimation(ANIM_SWITCH);
|
||
|
|
if (!anim) {
|
||
|
|
console.log('[PanelHomeSceneSwitch] 缺少动画', ANIM_SWITCH);
|
||
|
|
this._scheduleNext();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const eventTime = this._findAnimEventTime(num, ANIM_SWITCH, EVENT_SWITCH);
|
||
|
|
if (eventTime == null) {
|
||
|
|
console.log('[PanelHomeSceneSwitch] 缺少事件帧', EVENT_SWITCH, ANIM_SWITCH);
|
||
|
|
this._scheduleNext();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const duration = Number(anim.duration);
|
||
|
|
const scale = Math.max(0.01, num.timeScale || 1);
|
||
|
|
const eventDelay = eventTime / scale;
|
||
|
|
const doneDelay = (Number.isFinite(duration) && duration > 0 ? duration : eventTime) / scale;
|
||
|
|
this._switching = true;
|
||
|
|
num.setAnimation(0, ANIM_SWITCH, false);
|
||
|
|
host.unschedule(this._onSwitchEvent);
|
||
|
|
host.unschedule(this._onSwitchAnimDone);
|
||
|
|
host.scheduleOnce(this._onSwitchEvent, eventDelay);
|
||
|
|
host.scheduleOnce(this._onSwitchAnimDone, Math.max(eventDelay, doneDelay));
|
||
|
|
};
|
||
|
|
|
||
|
|
/** 事件帧:随机其他皮肤 */
|
||
|
|
private _onSwitchEvent = (): void => {
|
||
|
|
if (!this._switching) return;
|
||
|
|
const scene = this._sceneSkel;
|
||
|
|
if (!scene?.isValid) return;
|
||
|
|
const next = this._pickOtherSkinIndex(this._skinIndex);
|
||
|
|
const skin = SCENE_SKINS[next];
|
||
|
|
if (scene.skeletonData) ensureSpineSkeletonDataRuntime(scene.skeletonData);
|
||
|
|
try {
|
||
|
|
(scene as sp.Skeleton & { defaultSkin: string }).defaultSkin = skin;
|
||
|
|
scene.setSkin(skin);
|
||
|
|
scene.setSlotsToSetupPose();
|
||
|
|
this._skinIndex = next;
|
||
|
|
} catch (err) {
|
||
|
|
console.log('[PanelHomeSceneSwitch] setSkin 失败', skin, err);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/** 变换播完:回待机并排下次 */
|
||
|
|
private _onSwitchAnimDone = (): void => {
|
||
|
|
if (!this._switching) return;
|
||
|
|
this._switching = false;
|
||
|
|
this._playIdle();
|
||
|
|
this._scheduleNext();
|
||
|
|
};
|
||
|
|
|
||
|
|
/** 从当前以外的 3 个皮肤中随机下标 */
|
||
|
|
private _pickOtherSkinIndex(cur: number): number {
|
||
|
|
const others: number[] = [];
|
||
|
|
for (let i = 0; i < SCENE_SKINS.length; i++) {
|
||
|
|
if (i !== cur) others.push(i);
|
||
|
|
}
|
||
|
|
if (others.length === 0) return cur;
|
||
|
|
return others[Math.floor(Math.random() * others.length)];
|
||
|
|
}
|
||
|
|
}
|