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.
152 lines
5.0 KiB
152 lines
5.0 KiB
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 });
|
|
}
|
|
}
|
|
|