import { _decorator, Component, Node, sp } from 'cc'; import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch'; import { BattleSpinePlayback } from '../presentation/BattleSpinePlayback'; import { applyGlyphSpineSkin, GLYPH_ACTIVE_SKIN_BUCKETS, GLYPH_SKIN_IDLE, GLYPH_SKIN_QUALITY, resolveActiveGlyphSkin, } from './GlyphSpineSkin'; const { ccclass } = _decorator; const SKIN_RETRY_MAX = 30; @ccclass('TextSkin') export class TextSkin extends Component { anim: sp.Skeleton = null; Data: ITableText = null; private _pendingSkinPreferred = ''; private _pendingSkinExtras: string[] = []; private _skinRetryCount = 0; protected onLoad(): void { if (this.Data) this._bindGlyphSkeleton(); else this.anim = this.node.getComponentInChildren(sp.Skeleton); } setData(data: ITableText) { this.Data = data; this._bindGlyphSkeleton(); this.setDefaultSkin(); } /** 绑定字形 Spine(anim 子节点优先) */ private _bindGlyphSkeleton(): void { const animNode = this.node.getChildByName('anim'); if (animNode?.isValid) { this.anim = animNode.getComponent(sp.Skeleton) ?? animNode.getComponentInChildren(sp.Skeleton); return; } this.anim = this.node.getComponentInChildren(sp.Skeleton); } /** 设置默认皮肤 */ setDefaultSkin() { this._applySkin(GLYPH_SKIN_IDLE, []); } /** 设置皮肤(等级和品质) */ setSkinByLevelAndQuality(level: number, quality: number = GLYPH_SKIN_QUALITY) { if (!this.Data) return; const lv = Math.max(1, Math.floor(level)); const preferred = lv >= 3 ? `皮肤字${quality}_${lv}` : resolveActiveGlyphSkin(lv, quality); const extras = GLYPH_ACTIVE_SKIN_BUCKETS .map((b) => `皮肤字${quality}_${b}`) .filter((name) => name !== preferred); this._applySkin(preferred, extras); } /**播放待机动画 */ playIdleAnim(timescale: number = 1) { BattleSpinePlayback.play(this.anim, '待机', { loop: true, timeScale: timescale, optional: true }); } /**播放攻击动画 */ playAttackAnim(timescale: number = 1, callback: Function = null) { if (!this.anim?.isValid) { if (callback) callback(); return; } BattleSpinePlayback.clearPendingComplete(this.anim); if (!BattleSpinePlayback.play(this.anim, '攻击', { loop: false, timeScale: timescale, onComplete: callback ? () => callback() : undefined, })) { if (callback) callback(); } } private _applySkin(preferred: string, extras: readonly string[]): void { this._pendingSkinPreferred = preferred; this._pendingSkinExtras = [...extras]; this._tryApplyPendingSkin(); } private _tryApplyPendingSkin(): void { const preferred = this._pendingSkinPreferred; if (!preferred) return; const anim = this.anim; if (!anim?.isValid) return; const sd = anim.skeletonData; if (!sd?.isValid) { this._scheduleSkinRetry(); return; } if (!this._skeletonTexturesReady(sd)) { this._scheduleSkinRetry(); return; } ensureSpineSkeletonDataRuntime(sd); if (applyGlyphSpineSkin(anim, preferred, this._pendingSkinExtras)) { this._skinRetryCount = 0; this._ensureIdleIfNoTrack(anim); return; } // 纹理/runtime 尚未就绪:下一帧再试,避免误报「皮肤不存在」 if (this._scheduleSkinRetry()) return; console.log( '[TextSkin] Spine 皮肤未能应用', preferred, anim.node?.name, this.Data?.id, ); } private _scheduleSkinRetry(): boolean { if (this._skinRetryCount >= SKIN_RETRY_MAX) return false; this._skinRetryCount++; this.scheduleOnce(() => this._tryApplyPendingSkin(), 0.05); return true; } private _skeletonTexturesReady(sd: sp.SkeletonData): boolean { const textures = (sd as sp.SkeletonData & { textures?: unknown[] }).textures; return Array.isArray(textures) && textures.length > 0; } /** 切肤后若无有效 track,播待机,避免 动画报错 */ private _ensureIdleIfNoTrack(anim: sp.Skeleton): void { const cur = anim.getCurrent?.(0); const rawName = cur?.animation?.name; if (typeof rawName === 'string' && rawName.length > 0 && rawName !== '') return; BattleSpinePlayback.play(anim, '待机', { loop: true, optional: true }); } private _playNamed(name: string, loop: boolean, timescale: number): boolean { const anim = this.anim; if (!anim?.isValid) return false; return BattleSpinePlayback.play(anim, name, { loop, timeScale: timescale }); } }