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.
 
 
 
 
AdLike2048/assets/script/ui/PopIllustrated/UIPopIllustrated.ts

719 lines
26 KiB

//*********************
// create by 流云
// time: Sat Aug 01 2026
// desc: 战斗图鉴(牌库概率 + 神将图鉴)
//*********************
import { _decorator, assetManager, Color, Component, instantiate, Label, Layout, Node, Prefab, RichText, sp, Sprite, UIOpacity, UITransform } from 'cc';
import { Auto_PopIllustrated } from './Auto_PopIllustrated';
import { heroUltSkillId, textIconName } from '../../battle/config/BattleHeroTable';
import type { BattleBagSession } from '../../battle/bag/BattleBagSession';
import type { PlayerBagTextPoolState } from '../../battle/bag/PlayerBagTextPool';
import type { ProjectBattleHandle } from '../../battle/runtime/BattleRuntime';
import { applyGlyphSpineSkin } from '../../battle/view/GlyphSpineSkin';
import { TextGrid } from '../../battle/view/TextGrid';
import { GBundle, GPath } from '../../game/ConfigRes';
import { TableNames } from '../../game/ConfigTableData';
import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch';
import { LangTxt } from 'db://assets/mx/module/lang/LangTxt';
import { MERGE_EVENT_ALL, MergeEventType, type MergeEvent } from 'db://assets/mx/module/merge';
const { ccclass } = _decorator;
/** Text.type:基础字 */
const TEXT_TYPE_BASIC = 1;
/** Text.type:武将组合字 */
const TEXT_TYPE_HERO = 2;
const BASE_TEXT_CHAR_SCALE = 1;
const LOG = '[PopIllustrated]';
/** Hero.quality → 技能名颜色 */
const HERO_QUALITY_NAME_HEX: Record<number, string> = {
1: '#2ECC71',
2: '#3498DB',
3: '#9B59B6',
4: '#F39C12',
5: '#E53935',
};
/** 字形展示预制体(与背包 TextGrid 同源) */
const GLYPH_GRID_PATH = GPath.Prefab;
const GLYPH_GRID_NAME = 'textGrid';
/** 图鉴内隐藏 textGrid 子节点 */
const GLYPH_HIDE_CHILDREN = ['select', 'redSelect', 'canUpEff', 'trail1', 'trail2', 'trail3'];
/** 页签:图鉴 */
const TAB_TUJIAN = 0;
/** 页签:牌库 */
const TAB_PAIKU = 1;
/** 牌库基础字槽位 */
const PAIKU_ZI_SLOTS = ['zi1', 'zi2', 'zi3', 'zi4'] as const;
/** 牌库基础字概率标签 */
const PAIKU_NUM_SLOTS = ['num1', 'num2', 'num3', 'num4'] as const;
/** 牌库武将字列表项:单字挂点 */
const HERO_LIST_GLYPH_SLOT = 'hero';
/** 字池条目快照 */
interface IPoolTextSnap {
/** Text.id */
textId: number;
/** 配置行 */
row: ITableText;
/** 基础权重 */
weight: number;
/** 剩余数量 */
remaining: number;
/** 展示用上限(字池不限量时为 0) */
max: number;
/** weight × remaining */
effectiveWeight: number;
}
@ccclass('UIPopIllustrated')
export class UIPopIllustrated extends Auto_PopIllustrated {
private _heroListTemplate: Node = null;
private _haveListTemplate: Node = null;
/** 当前页签 */
private _pageIndex = TAB_TUJIAN;
/** 图鉴列表数据:Hero 表 herotype === 2 */
private _comboHeroList: ITableHero[] = [];
/** 图鉴当前选中下标 */
private _selectIndex = 0;
/** 图鉴列表项节点(与 _comboHeroList 下标对齐) */
private _haveListItems: Node[] = [];
/** 异步填充代数,过期回调丢弃 */
private _fillToken = 0;
/** 背包刷新监听 */
private _bagRefreshListener: ((e: MergeEvent) => void) | null = null;
/** 已注册 BagRefreshed 的背包会话 */
private _bagRefreshBag: BattleBagSession | null = null;
onLoad(): void {
super.onLoad();
this._heroListTemplate = this._resolveHeroListTemplate();
this._haveListTemplate = this._findListTemplate(this.ui_haveList);
if (this._heroListTemplate) this._heroListTemplate.active = false;
if (this._haveListTemplate) this._haveListTemplate.active = false;
}
onInit(): void {
let curWaveNum = gg.game.CurentBattle.CurentWaveNum
gg.sdk.reportDY('inLevel', `${gg.game.getReportDYBattleCountLabel(curWaveNum)}-打开图鉴`);
this._comboHeroList = [];
this._selectIndex = 0;
this._haveListItems = [];
this.click_btnTuJian();
}
onEnable(): void {
super.onEnable();
this._bindBagRefreshListener();
if (this._pageIndex === TAB_PAIKU) {
this.showPaiKu();
}
}
onDisable(): void {
this._unbindBagRefreshListener();
super.onDisable();
}
onDestroy(): void {
this._fillToken++;
this._clearHeroSlot(this.ui_select);
super.onDestroy();
}
updateUI(): void {
this._applyTabButton(this.btnTuJian, this._pageIndex === TAB_TUJIAN);
this._applyTabButton(this.btnPaiKu, this._pageIndex === TAB_PAIKU);
this.ui_tab1.active = this._pageIndex === TAB_TUJIAN;
this.ui_tab2.active = this._pageIndex === TAB_PAIKU;
this.ui_tujian.active = this._pageIndex === TAB_TUJIAN;
this.ui_paiku.active = this._pageIndex === TAB_PAIKU;
}
/** 页签按钮:未选中 font,选中 fontSelect */
private _applyTabButton(btn: Node, selected: boolean): void {
const font = btn.getChildByName('font');
const fontSelect = btn.getChildByName('fontSelect');
if (font) font.active = !selected;
if (fontSelect) fontSelect.active = selected;
}
click_btnClose(): void {
this.close(2);
}
click_btnPaiKu(): void {
this._pageIndex = TAB_PAIKU;
this.updateUI();
this._bindBagRefreshListener();
this.showPaiKu();
}
click_btnTuJian(): void {
this._pageIndex = TAB_TUJIAN;
this.updateUI();
this.showTuJian();
}
showPaiKu(): void {
const bag = this._getBattleBagSession();
const poolState = bag?.exportCounters().textPool ?? null;
if (!bag) {
console.log(LOG, '背包会话未就绪');
this._clearPaiKuView();
return;
}
if (!poolState?.ready) {
console.log(LOG, '局内字池未就绪', `N=${poolState?.numberN ?? 0}`);
this._clearPaiKuView();
return;
}
const entries = this._buildPoolEntries(poolState);
const totalEffective = this._sumEffectiveWeight(entries);
this._fillBasicTextSlots(entries, totalEffective);
this._fillHeroTextList(entries, poolState.numberN);
this._refreshHeroTextTotalProb(entries, totalEffective);
}
showTuJian(): void {
this._fillComboHeroList();
}
/** 牌库 zi1~4 / num1~4:Text.type===1,概率 = 有效权重 / 全池有效权重 */
private _fillBasicTextSlots(entries: readonly IPoolTextSnap[], totalEffective: number): void {
const basicRows = this._getTextRowsByType(TEXT_TYPE_BASIC, true);
for (let i = 0; i < PAIKU_ZI_SLOTS.length; i++) {
const ziNode = this.ui_paiku.getChildByName(PAIKU_ZI_SLOTS[i]);
const numLabel = this.ui_paiku.getChildByName(PAIKU_NUM_SLOTS[i])?.getComponent(Label);
const row = basicRows[i];
if (!row) {
if (ziNode?.isValid) ziNode.active = false;
this._setDynamicLabel(numLabel, '0%');
continue;
}
const snap = this._findPoolEntry(entries, Number(row.id));
if (ziNode?.isValid) {
ziNode.active = true;
this._mountGlyphChar(ziNode, textIconName(row), BASE_TEXT_CHAR_SCALE);
}
this._setDynamicLabel(numLabel, this._formatPercent(snap?.effectiveWeight ?? 0, totalEffective));
}
}
/** 牌库 ui_heroList:Text.type===2 单字 + 剩余数量 */
private _fillHeroTextList(entries: readonly IPoolTextSnap[], numberN: number): void {
if (!this._heroListTemplate?.isValid) return;
this._clearListChildren(this.ui_heroList, this._heroListTemplate);
const heroRows = this._getTextRowsByType(TEXT_TYPE_HERO, true);
for (let i = 0; i < heroRows.length; i++) {
const row = heroRows[i];
const snap = this._findPoolEntry(entries, Number(row.id));
const remaining = snap?.remaining ?? 0;
const item = instantiate(this._heroListTemplate);
item.parent = this.ui_heroList;
item.active = true;
const heroSlot = item.getChildByName(HERO_LIST_GLYPH_SLOT);
if (!heroSlot?.isValid) {
console.log(LOG, 'template hero 挂点缺失');
} else {
this._mountGlyphChar(heroSlot, textIconName(row), BASE_TEXT_CHAR_SCALE);
}
const numNode = item.getChildByName('num');
const numLabel = numNode?.getComponent(Label);
const depleted = remaining <= 0;
this._applyHeroListDepletedVisual(item, depleted);
this._setDynamicLabel(numLabel, this._formatRemainCount(remaining));
if (!snap && remaining <= 0) {
console.log(LOG, '字池无该武将字', row.id, textIconName(row), `N=${numberN}`);
}
}
this.ui_heroList.getComponent(Layout)?.updateLayout();
}
/** lb_heroNum:武将组合字合计概率(仅百分比) */
private _refreshHeroTextTotalProb(entries: readonly IPoolTextSnap[], totalEffective: number): void {
if (!this.lb_heroNum) return;
let heroEffective = 0;
for (let i = 0; i < entries.length; i++) {
if (Number(entries[i].row.type) === TEXT_TYPE_HERO) {
heroEffective += entries[i].effectiveWeight;
}
}
this._setDynamicLabel(this.lb_heroNum, this._formatPercent(heroEffective, totalEffective));
}
/** 字池未就绪时清空牌库展示 */
private _clearPaiKuView(): void {
for (let i = 0; i < PAIKU_ZI_SLOTS.length; i++) {
const ziNode = this.ui_paiku.getChildByName(PAIKU_ZI_SLOTS[i]);
const numLabel = this.ui_paiku.getChildByName(PAIKU_NUM_SLOTS[i])?.getComponent(Label) ?? null;
if (ziNode?.isValid) ziNode.active = false;
this._setDynamicLabel(numLabel, '0%');
}
if (this._heroListTemplate?.isValid) {
this._clearListChildren(this.ui_heroList, this._heroListTemplate);
}
this._setDynamicLabel(this.lb_heroNum, '0%');
}
/** 当前战斗背包会话(PopIllustrated 走 gg.game.CurentBattle,不依赖 SkillPresentationEnv) */
private _getBattleBagSession(): BattleBagSession | null {
const battle = gg.game.CurentBattle as ProjectBattleHandle | null;
return battle?.bagSession ?? null;
}
/** 字池快照 → 展示条目 */
private _buildPoolEntries(state: PlayerBagTextPoolState): IPoolTextSnap[] {
const entries: IPoolTextSnap[] = [];
for (let i = 0; i < state.textIds.length; i++) {
const textId = Number(state.textIds[i]);
const row = gg.data.table.getTableData<ITableText>(TableNames.Text, textId);
if (!row) {
console.log(LOG, 'Text 缺失', textId);
continue;
}
const remaining = Number(state.remaining[i]) | 0;
const weight = Number(state.weights[i]);
const max = 0;
entries.push({
textId,
row,
weight,
remaining,
max,
effectiveWeight: remaining > 0 && weight > 0 ? weight * remaining : 0,
});
}
return entries;
}
/** Text 表按 type 筛选并 id 升序;excludeZeroWeight 时跳过 weight<=0 */
private _getTextRowsByType(type: number, excludeZeroWeight = false): ITableText[] {
return (gg.data.table.getTableList<ITableText>(TableNames.Text) ?? [])
.filter((row) => {
if (Number(row.type) !== type) return false;
if (excludeZeroWeight && !(Number(row.weight) > 0)) return false;
return true;
})
.sort((a, b) => a.id - b.id);
}
/** 按 Text.id 查字池条目 */
private _findPoolEntry(entries: readonly IPoolTextSnap[], textId: number): IPoolTextSnap | null {
for (let i = 0; i < entries.length; i++) {
if (entries[i].textId === textId) return entries[i];
}
return null;
}
/** 有效权重总和 */
private _sumEffectiveWeight(entries: readonly IPoolTextSnap[]): number {
let total = 0;
for (let i = 0; i < entries.length; i++) {
total += entries[i].effectiveWeight;
}
return total;
}
/** 剩余数量展示:x0 */
private _formatRemainCount(remaining: number): string {
return `x${Math.max(0, remaining | 0)}`;
}
/** 动态文案 Label:移除 LangTxt 后再赋值,避免被语言系统覆盖 */
private _setDynamicLabel(label: Label | null, text: string): void {
if (!label?.isValid) return;
label.node.getComponent(LangTxt)?.destroy();
label.string = text;
}
private _bindBagRefreshListener(): void {
this._unbindBagRefreshListener();
const bag = this._getBattleBagSession();
if (!bag) return;
this._bagRefreshBag = bag;
this._bagRefreshListener = (e: MergeEvent) => {
if (this._pageIndex !== TAB_PAIKU) return;
if (e.type === MergeEventType.BagRefreshed || e.type === MergeEventType.GlyphRewritten) {
this.showPaiKu();
}
};
bag.on(MERGE_EVENT_ALL, this._bagRefreshListener);
}
private _unbindBagRefreshListener(): void {
if (this._bagRefreshBag && this._bagRefreshListener) {
this._bagRefreshBag.off(MERGE_EVENT_ALL, this._bagRefreshListener);
}
this._bagRefreshBag = null;
this._bagRefreshListener = null;
}
/** 图鉴列表:Hero 表 herotype === 2 */
private _fillComboHeroList(): void {
if (!this._haveListTemplate?.isValid) return;
this._fillToken++;
this._clearHeroSlot(this.ui_select);
this._clearListChildren(this.ui_haveList, this._haveListTemplate);
this._haveListItems = [];
this._comboHeroList = gg.equip.getNormalHeroList().slice().sort((a, b) => a.id - b.id);
if (this._comboHeroList.length <= 0) {
this._clearHeroDetailPanel();
return;
}
if (this._selectIndex < 0 || this._selectIndex >= this._comboHeroList.length) {
this._selectIndex = 0;
}
const token = this._fillToken;
this._comboHeroList.forEach((hero, index) => {
const item = instantiate(this._haveListTemplate);
item.parent = this.ui_haveList;
item.active = true;
this._haveListItems.push(item);
this._applyHaveListSelectState(item, index === this._selectIndex);
item.off(Node.EventType.TOUCH_END);
item.on(Node.EventType.TOUCH_END, () => this._selectHeroByIndex(index), this);
void this._fillHeroSlot(item.getChildByName('hero'), hero, token);
});
this.ui_haveList.getComponent(Layout)?.updateLayout();
void this._showSelectedHero(this._selectIndex, token);
}
/** 列表项选中框 select */
private _applyHaveListSelectState(item: Node, selected: boolean): void {
const selectNode = item.getChildByName('select');
if (selectNode) selectNode.active = selected;
}
private _refreshHaveListSelectState(): void {
for (let i = 0; i < this._haveListItems.length; i++) {
const item = this._haveListItems[i];
if (item?.isValid) this._applyHaveListSelectState(item, i === this._selectIndex);
}
}
private _selectHeroByIndex(index: number): void {
if (index < 0 || index >= this._comboHeroList.length) return;
if (this._selectIndex === index) return;
this._selectIndex = index;
this._refreshHaveListSelectState();
void this._showSelectedHero(index, this._fillToken);
}
/** 大图区 + 技能文案 */
private async _showSelectedHero(index: number, token: number): Promise<void> {
const hero = this._comboHeroList[index];
if (!hero) {
this._clearHeroDetailPanel();
return;
}
await this._fillHeroSlot(this.ui_select, hero, token);
if (token !== this._fillToken || !this.isValid) return;
this._applyHeroDetailPanel(hero);
}
private _applyHeroDetailPanel(hero: ITableHero): void {
const skillId = heroUltSkillId(hero, 1);
const skill = skillId > 0
? gg.data.table.getTableData<ITableSkill>(TableNames.Skill, skillId)
: null;
if (this.lb_skillName) {
this.lb_skillName.string = skill ? gg.lang.get(skill.name) : gg.lang.get(hero.name);
const hex = HERO_QUALITY_NAME_HEX[Math.floor(Number(hero.quality) || 0)];
if (!hex) {
console.log(LOG, '未知英雄品质色', hero.id, hero.quality);
} else {
this.lb_skillName.color = new Color().fromHEX(hex);
}
}
if (this.lb_skillDesc) {
const desc = skill ? gg.lang.get(skill.desc) : '';
this.lb_skillDesc.string = desc;
}
}
private _clearHeroDetailPanel(): void {
if (this.lb_skillName) {
this.lb_skillName.string = '';
this.lb_skillName.color = Color.WHITE;
}
if (this.lb_skillDesc) this.lb_skillDesc.string = '';
}
private _formatPercent(weight: number, total: number): string {
const w = Number(weight);
const t = Number(total);
if (!Number.isFinite(w) || !Number.isFinite(t) || t <= 0) return '0%';
return `${(w / t * 100).toFixed(2)}%`;
}
/** 挂载字形展示(与背包 TextGrid 同源 prefab) */
private _mountGlyphChar(parent: Node, iconName: string, scale = BASE_TEXT_CHAR_SCALE, clearAll = true): void {
const name = String(iconName || '').trim();
if (!name) {
console.log(LOG, '字形名为空', parent.name);
return;
}
if (clearAll) parent.removeAllChildren();
const attach = (gridNode: Node): void => {
gridNode.parent = parent;
gridNode.setPosition(0, 0, 0);
gridNode.setScale(scale, scale, 1);
this._applyGlyphGridPresentation(gridNode, name);
};
const prefab = gg.res.getRes<Prefab>(GLYPH_GRID_NAME, GLYPH_GRID_PATH, GBundle.BattleGame, Prefab);
if (prefab) {
attach(instantiate(prefab));
return;
}
const bundle = assetManager.getBundle(GBundle.BattleGame);
if (!bundle) {
console.log(LOG, 'BattleGame bundle 未加载', name);
return;
}
bundle.load(`${GLYPH_GRID_PATH}${GLYPH_GRID_NAME}`, Prefab, (err, loaded) => {
if (err || !loaded || !parent.isValid) {
if (err) console.log(LOG, 'textGrid 加载失败', name, String(err));
return;
}
attach(instantiate(loaded));
parent.parent?.getComponent(Layout)?.updateLayout();
});
}
/** 图鉴用 textGrid:隐藏多余子节点,切 Hero.res 皮肤 */
private _applyGlyphGridPresentation(gridNode: Node, iconName: string): void {
for (let i = 0; i < GLYPH_HIDE_CHILDREN.length; i++) {
const child = gridNode.getChildByName(GLYPH_HIDE_CHILDREN[i]);
if (child) child.active = false;
}
const skinNode = gridNode.getChildByName('skin');
if (skinNode) skinNode.active = true;
const textGrid = gridNode.getComponent(TextGrid);
if (textGrid) textGrid.enabled = false;
const sk = skinNode?.getComponent(sp.Skeleton) ?? skinNode?.getComponentInChildren(sp.Skeleton);
if (!sk?.isValid) {
console.log(LOG, '字形 Spine 缺失', iconName);
return;
}
sk.enabled = true;
if (!applyGlyphSpineSkin(sk, iconName)) {
console.log(LOG, '字形皮肤未能应用', iconName);
}
if (sk.skeletonData) ensureSpineSkeletonDataRuntime(sk.skeletonData);
const runtime = sk.skeletonData?.getRuntimeData?.(true);
if (runtime?.findAnimation?.('待机')) {
sk.setAnimation(0, '待机', true);
}
}
private _clearListChildren(listNode: Node, template: Node): void {
for (const child of [...listNode.children]) {
if (child === template) continue;
child.destroy();
}
}
/** ui_heroList 模板节点 */
private _resolveHeroListTemplate(): Node | null {
return this._findListTemplate(this.ui_heroList);
}
/** 列表 template 节点(优先 name=template) */
private _findListTemplate(listRoot: Node | null): Node | null {
if (!listRoot?.isValid) return null;
const named = listRoot.getChildByName('template');
if (named?.isValid) return named;
if (listRoot.children.length > 0) return listRoot.children[0];
return null;
}
/**
* 武将字列表项:剩余为 0 时隐藏 num、显示 complete。
* @param item 列表项
* @param depleted 是否已抽空
*/
private _applyHeroListDepletedVisual(item: Node, depleted: boolean): void {
const numNode = item.getChildByName('num');
const completeNode = item.getChildByName('complete');
if (numNode?.isValid) numNode.active = !depleted;
if (!completeNode?.isValid) {
if (depleted) console.log(LOG, 'template complete 节点缺失', item.name);
return;
}
completeNode.active = depleted;
if (depleted) {
completeNode.setSiblingIndex(item.children.length - 1);
}
}
/** 挂载 shenjiang/prefab/{Hero.res} 展示预制体 */
private async _fillHeroSlot(slot: Node | null, hero: ITableHero, token: number): Promise<void> {
if (!slot?.isValid) {
console.log(LOG, 'hero 挂点缺失');
return;
}
this._clearHeroSlot(slot);
const resName = String(hero.res || hero.name || '').trim();
if (!resName) {
console.log(LOG, 'Hero.res 为空', hero.id);
return;
}
const body = await this._loadDisplayPrefab(GPath.UIPrefab, resName, GBundle.Shenjiang);
if (token !== this._fillToken || !this.isValid || !slot.isValid) {
this._freeDisplay(body);
return;
}
if (!body?.isValid) {
console.log(LOG, '神将展示预制体缺失', resName, hero.id);
return;
}
this._disableLogicScripts(body);
this._mountDisplay(slot, body);
this._applySpine(body, '', '待机');
}
private _mountDisplay(slot: Node, child: Node): void {
child.parent = slot;
this._setLayerRecursive(child, slot.layer);
child.setPosition(0, 0, 0);
child.setScale(1, 1, 1);
child.angle = 0;
child.active = true;
}
private _clearHeroSlot(slot: Node | null): void {
if (!slot?.isValid) return;
const kids = slot.children;
const copy: Node[] = [];
for (let i = 0; i < kids.length; i++) copy.push(kids[i]);
for (let i = 0; i < copy.length; i++) this._freeDisplay(copy[i]);
}
private _loadDisplayPrefab(dir: string, prefabName: string, bundleName: string): Promise<Node | null> {
const name = (prefabName || '').trim();
if (!name) return Promise.resolve(null);
const full = `${dir}${name}`;
return new Promise((resolve) => {
const existed = assetManager.getBundle(bundleName);
if (existed) {
this._instantiatePrefab(existed, full, resolve);
return;
}
assetManager.loadBundle(bundleName, (err, bundle) => {
if (err || !bundle) {
console.error(LOG, 'bundle 加载失败', bundleName, String(err));
resolve(null);
return;
}
this._instantiatePrefab(bundle, full, resolve);
});
});
}
private _instantiatePrefab(bundle: ReturnType<typeof assetManager.getBundle>, full: string, resolve: (n: Node | null) => void): void {
if (!bundle) {
resolve(null);
return;
}
const cached = bundle.get(full, Prefab);
if (cached) {
resolve(instantiate(cached));
return;
}
bundle.load(full, Prefab, (err, p) => {
if (err || !p) {
console.error(LOG, '预制体加载失败', full, String(err));
resolve(null);
return;
}
resolve(instantiate(p));
});
}
private _freeDisplay(node: Node | null | undefined): void {
if (!node?.isValid) return;
node.removeFromParent();
node.destroy();
}
private _disableLogicScripts(node: Node): void {
const comps = node.getComponents(Component);
for (let i = 0; i < comps.length; i++) {
const c = comps[i];
if (c instanceof UITransform || c instanceof Sprite || c instanceof sp.Skeleton) continue;
if (c instanceof UIOpacity) continue;
c.enabled = false;
}
for (let i = 0; i < node.children.length; i++) {
this._disableLogicScripts(node.children[i]);
}
}
private _findSkeleton(node: Node | null | undefined): sp.Skeleton | null {
if (!node?.isValid) return null;
return node.getComponent(sp.Skeleton) ?? node.getComponentInChildren(sp.Skeleton) ?? null;
}
private _applySpine(node: Node, skin: string, anim: string): void {
const skel = this._findSkeleton(node);
if (!skel) return;
if (skel.skeletonData) ensureSpineSkeletonDataRuntime(skel.skeletonData);
if (skin) {
try {
skel.setSkin(skin);
skel.setSlotsToSetupPose();
} catch (e) {
console.log(LOG, `setSkin failed "${skin}"`, String(e));
}
}
const clip = anim || '待机';
if (!clip) return;
try {
skel.setAnimation(0, clip, true);
} catch (e) {
console.log(LOG, `setAnimation failed "${clip}"`, String(e));
}
}
private _setLayerRecursive(node: Node, layer: number): void {
node.layer = layer;
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if (child?.isValid) this._setLayerRecursive(child, layer);
}
}
}