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.
726 lines
27 KiB
726 lines
27 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;
|
|
/** 本局 numberN 对应上限 */
|
|
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 = this._readTextNumberN(row, state.numberN);
|
|
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;
|
|
}
|
|
|
|
/** 读取 Text.numberN */
|
|
private _readTextNumberN(row: ITableText, n: number): number {
|
|
const key = `number${n}` as keyof ITableText;
|
|
const v = Number(row[key]);
|
|
return Number.isFinite(v) ? v : 0;
|
|
}
|
|
|
|
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:隐藏多余子节点,切 Text.icon 皮肤 */
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|