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.

153 lines
5.0 KiB

3 weeks ago
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,播待机,避免 <None> 动画报错 */
private _ensureIdleIfNoTrack(anim: sp.Skeleton): void {
const cur = anim.getCurrent?.(0);
const rawName = cur?.animation?.name;
if (typeof rawName === 'string' && rawName.length > 0 && rawName !== '<None>') 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 });
}
}