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 }>; } | 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)]; } }