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.
731 lines
27 KiB
731 lines
27 KiB
|
|
//*********************
|
|
// create by 流云
|
|
// time: Sat Sep 05 2026
|
|
// desc: 战斗内点击五行数字 — 详情弹窗(数字皮 / 普攻技能 / 镶嵌宝石)
|
|
//*********************
|
|
import {
|
|
_decorator,
|
|
EventTouch,
|
|
instantiate,
|
|
Label,
|
|
Layout,
|
|
Node,
|
|
Sprite,
|
|
Tween,
|
|
UITransform,
|
|
UIOpacity,
|
|
UIRenderer,
|
|
Vec3,
|
|
sp,
|
|
} from 'cc';
|
|
import { type IPopAnimConfig, type IPopAnimSession } from '../../../mx/module/ui/popAnim';
|
|
import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch';
|
|
import { SpriteLoad } from 'db://assets/mx/components/SpriteLoad';
|
|
import { BATTLE_EVENT_ALL, BattleEventType, type BattleEvent } from 'db://assets/mx/module/battle';
|
|
import type { EntityId } from 'db://assets/mx/module/ecs';
|
|
import { GEvent } from '../../../mx/module/event/GEvent';
|
|
import { Auto_BattleWeaponDescHero } from './Auto_BattleWeaponDescHero';
|
|
import type { TileEntitySnapshot, TowerRuntimeSnapshot } from '../../battle/combat/TowerQueryTypes';
|
|
import { TowerQueryService } from '../../battle/combat/TowerQueryService';
|
|
import { deriveTowerSetupStats, TowerStatDisplay } from '../../battle/combat/TowerStatResolver';
|
|
import { heroUltSkillId, isElementHero } from '../../battle/config/BattleHeroTable';
|
|
import { WeaponComposeConfigProvider } from '../../battle/bag/WeaponComposeConfigProvider';
|
|
import { BATTLE_HERO_NUM_PATH } from '../../battle/res/BattleHeroResCollector';
|
|
import { glyphSpineHasSkin } from '../../battle/view/GlyphSpineSkin';
|
|
import { GBundle } from '../../game/ConfigRes';
|
|
import { TableNames } from '../../game/ConfigTableData';
|
|
const { ccclass } = _decorator;
|
|
|
|
const LOG = '[UIBattleWeaponDescHero]';
|
|
/** 空镶嵌提示文案(与预制体 gtemplate/lb 默认一致) */
|
|
const GEM_EMPTY_TIP = '尚未镶嵌宝石';
|
|
const NUM_IDLE = '待机';
|
|
|
|
/** 五行数字详情弹窗入参 */
|
|
export interface BattleWeaponDescHeroPanelData {
|
|
entity: TileEntitySnapshot;
|
|
runtime?: TowerRuntimeSnapshot | null;
|
|
/** 敌方阵营(敌方不读本机镶嵌) */
|
|
enemyCamp?: boolean;
|
|
/** 已解析的展示武器/技能宝石 id(兼容旧入参) */
|
|
weaponId?: number;
|
|
weaponWorldPos?: { x: number; y: number; z?: number };
|
|
gridStep?: number;
|
|
trySwitchAtUiPos?: (uiPos: { x: number; y: number }) => boolean;
|
|
/** 监视中的地图/背包格 */
|
|
watchTarget?: { kind: 'map' | 'bag'; index: number } | null;
|
|
/** 已部署 ECS 实体 */
|
|
watchEntityId?: EntityId;
|
|
/** 从棋盘重新读取 entity/runtime */
|
|
resolveLiveData?: () => Pick<BattleWeaponDescHeroPanelData, 'entity' | 'runtime' | 'weaponId'> | null;
|
|
}
|
|
|
|
@ccclass('UIBattleWeaponDescHero')
|
|
export class UIBattleWeaponDescHero extends Auto_BattleWeaponDescHero {
|
|
private static readonly PANEL_GAP_GRIDS = 1.7;
|
|
|
|
private _popupBody: Node | null = null;
|
|
/** 递增时取消尚未完成的关窗销毁 */
|
|
private _panelLifeGen = 0;
|
|
/** ui_node 下挂的数字预制体节点 */
|
|
private _numNode: Node | null = null;
|
|
/** 宝石列表模板 */
|
|
private _gemTpl: Node | null = null;
|
|
/** 已克隆的宝石行 */
|
|
private readonly _gemRows: Node[] = [];
|
|
/** 静态区缓存键 */
|
|
private _staticViewKey = '';
|
|
/** 战斗事件监听是否已绑定 */
|
|
private _battleEventBound = false;
|
|
/** GEvent 监听是否已绑定 */
|
|
private _geventBound = false;
|
|
|
|
onLoad(): void {
|
|
super.onLoad();
|
|
this._popupBody = this.node.getChildByName('弹窗主体');
|
|
this._bindNodes();
|
|
this._cacheGemTemplate();
|
|
}
|
|
|
|
/**
|
|
* 按最新预制体路径补绑(Auto 路径失效时兜底)。
|
|
* 结构:弹窗主体/框体/框体/{ui_node,layout,数值};宝石列表在外层框体。
|
|
*/
|
|
private _bindNodes(): void {
|
|
const body = this._popupBody ?? this.node.getChildByName('弹窗主体');
|
|
if (!body?.isValid) return;
|
|
const outer = body.getChildByName('框体');
|
|
const inner = outer?.getChildByName('框体') ?? null;
|
|
const panel = inner?.isValid ? inner : outer;
|
|
if (!panel?.isValid) return;
|
|
|
|
if (!this.ui_node?.isValid) this.ui_node = panel.getChildByName('ui_node');
|
|
const layout = panel.getChildByName('layout');
|
|
if (!this.lb_name?.isValid) {
|
|
this.lb_name = layout?.getChildByName('lb_name')?.getComponent(Label) ?? null;
|
|
}
|
|
if (!this.lb_level?.isValid) {
|
|
this.lb_level = layout?.getChildByName('lb_level')?.getComponent(Label) ?? null;
|
|
}
|
|
if (!this.lb_lvMax?.isValid) {
|
|
this.lb_lvMax = panel.getChildByName('lb_lvMax')?.getComponent(Label) ?? null;
|
|
}
|
|
if (!this.lb_attck?.isValid) {
|
|
this.lb_attck = panel.getChildByName('lb_attck')?.getComponent(Label) ?? null;
|
|
}
|
|
if (!this.lb_attckSpeed?.isValid) {
|
|
this.lb_attckSpeed = panel.getChildByName('lb_attckSpeed')?.getComponent(Label) ?? null;
|
|
}
|
|
if (!this.lb_skillName?.isValid) {
|
|
this.lb_skillName = panel.getChildByName('lb_skillName')?.getComponent(Label) ?? null;
|
|
}
|
|
if (!this.lb_skillDesc?.isValid) {
|
|
this.lb_skillDesc = panel.getChildByName('lb_skillDesc')?.getComponent(Label) ?? null;
|
|
}
|
|
if (!this.lb_listDesc?.isValid && outer?.isValid) {
|
|
this.lb_listDesc = outer.getChildByName('lb_listDesc')?.getComponent(Label) ?? null;
|
|
}
|
|
if (!this.ui_gemList?.isValid && outer?.isValid) {
|
|
this.ui_gemList = outer.getChildByName('ui_gemList');
|
|
}
|
|
}
|
|
|
|
onEnable(): void {
|
|
super.onEnable();
|
|
this.ui_bgMask?.on(Node.EventType.TOUCH_END, this.click_ui_bgMask, this);
|
|
this._bindLiveRefresh();
|
|
}
|
|
|
|
onDisable(): void {
|
|
if (this.ui_bgMask?.isValid) {
|
|
this.ui_bgMask.off(Node.EventType.TOUCH_END, this.click_ui_bgMask, this);
|
|
}
|
|
this._unbindLiveRefresh();
|
|
this.unschedule(this._deferredRefreshDynamic);
|
|
this._staticViewKey = '';
|
|
this._clearNumNode();
|
|
this._clearGemRows();
|
|
super.onDisable();
|
|
}
|
|
|
|
/** 订阅击杀/棋盘变更,实时刷新数值区 */
|
|
private _bindLiveRefresh(): void {
|
|
if (!this._geventBound) {
|
|
GEvent.Ins.on(GEvent.UpdateBestPlace, this._onLiveDataDirty, this);
|
|
this._geventBound = true;
|
|
}
|
|
const battle = gg.game.CurentBattle;
|
|
if (battle && !this._battleEventBound) {
|
|
battle.on(BATTLE_EVENT_ALL, this._onBattleEvent);
|
|
this._battleEventBound = true;
|
|
}
|
|
}
|
|
|
|
/** 取消实时刷新订阅 */
|
|
private _unbindLiveRefresh(): void {
|
|
if (this._geventBound) {
|
|
GEvent.Ins.off(GEvent.UpdateBestPlace, this._onLiveDataDirty, this);
|
|
this._geventBound = false;
|
|
}
|
|
const battle = gg.game.CurentBattle;
|
|
if (battle && this._battleEventBound) {
|
|
battle.off(BATTLE_EVENT_ALL, this._onBattleEvent);
|
|
this._battleEventBound = false;
|
|
}
|
|
}
|
|
|
|
private _onBattleEvent = (event: BattleEvent): void => {
|
|
if (event.type === BattleEventType.EntityKilled) {
|
|
this._scheduleRefreshDynamicFromLive();
|
|
}
|
|
};
|
|
|
|
private _onLiveDataDirty = (): void => {
|
|
this._scheduleRefreshDynamicFromLive();
|
|
};
|
|
|
|
/** 帧末刷新,确保击杀经验/升级写入后再读 */
|
|
private _scheduleRefreshDynamicFromLive(): void {
|
|
this.unschedule(this._deferredRefreshDynamic);
|
|
this.scheduleOnce(this._deferredRefreshDynamic, 0);
|
|
}
|
|
|
|
private _deferredRefreshDynamic = (): void => {
|
|
this._refreshDynamicFromLive();
|
|
};
|
|
|
|
onInit(): void {
|
|
this._panelLifeGen++;
|
|
this._ensurePanelVisible();
|
|
this._bindLiveRefresh();
|
|
this._bindNodes();
|
|
this._cacheGemTemplate();
|
|
|
|
const ctx = this._resolveRefreshContext();
|
|
if (!ctx) {
|
|
this._staticViewKey = '';
|
|
this._clearLabels();
|
|
this._clearNumNode();
|
|
this._clearGemRows();
|
|
return;
|
|
}
|
|
|
|
const { data, w1, level, levelLimit, stats, enemyCamp } = ctx;
|
|
const element = Math.floor(Number(w1.herotype) || 0);
|
|
const staticViewKey = this._buildStaticViewKey(w1.id, level, element, enemyCamp);
|
|
const needStaticRefresh = staticViewKey !== this._staticViewKey || !this._numNode?.isValid;
|
|
|
|
if (needStaticRefresh) {
|
|
this._staticViewKey = staticViewKey;
|
|
this._refreshNumber(w1);
|
|
this._refreshStaticLabels(w1, level, levelLimit);
|
|
this._refreshGemList(element, enemyCamp);
|
|
}
|
|
|
|
this._refreshDynamicStats(w1, level, stats);
|
|
this._applyPanelPosition(data);
|
|
}
|
|
|
|
/** 合并 prema 与 resolveLiveData */
|
|
private _resolveRefreshContext(): {
|
|
data: BattleWeaponDescHeroPanelData;
|
|
w1: ITableHero;
|
|
level: number;
|
|
levelLimit: number;
|
|
stats: TowerRuntimeSnapshot['stats'] | undefined;
|
|
runtime: TowerRuntimeSnapshot | null;
|
|
enemyCamp: boolean;
|
|
} | null {
|
|
const data = this.prema as BattleWeaponDescHeroPanelData | null;
|
|
if (!data?.entity) return null;
|
|
|
|
const live = data.resolveLiveData?.() ?? null;
|
|
if (live) {
|
|
data.entity = live.entity;
|
|
data.runtime = live.runtime ?? null;
|
|
if (live.weaponId != null && live.weaponId > 0) {
|
|
data.weaponId = live.weaponId;
|
|
}
|
|
if (live.runtime?.entityId) {
|
|
data.watchEntityId = live.runtime.entityId;
|
|
}
|
|
}
|
|
|
|
const w1 = data.entity.tower?.config ?? null;
|
|
if (!w1) return null;
|
|
if (!isElementHero(w1)) {
|
|
console.warn(LOG, '非五行数字,详情仅支持五行', w1.id, w1.herotype);
|
|
}
|
|
|
|
let runtime = data.runtime ?? null;
|
|
const watchEntityId = data.watchEntityId ?? runtime?.entityId ?? 0;
|
|
const battle = gg.game.CurentBattle;
|
|
if (watchEntityId > 0 && battle?.session) {
|
|
runtime = TowerQueryService.instance.getRuntimeSnapshot(battle.session, watchEntityId) ?? runtime;
|
|
data.runtime = runtime;
|
|
}
|
|
|
|
const comp = data.entity.composition;
|
|
const level = runtime?.level ?? comp?.level ?? data.entity.level;
|
|
const ht = Math.floor(Number(w1.herotype) || 0);
|
|
const levelLimit = data.entity.tower?.levelLimit
|
|
?? (ht >= 1 && ht <= 5 ? WeaponComposeConfigProvider.instance.getElementMaxLevel(ht) : level);
|
|
const liveStats = watchEntityId > 0 && battle?.session
|
|
? TowerQueryService.instance.readLiveStats(battle.session, watchEntityId)
|
|
: null;
|
|
const stats = liveStats ?? runtime?.stats ?? data.entity.tower?.stats;
|
|
const enemyCamp = !!data.enemyCamp;
|
|
|
|
return { data, w1, level, levelLimit, stats, runtime, enemyCamp };
|
|
}
|
|
|
|
/** 击杀/升级后仅刷新数值区 */
|
|
private _refreshDynamicFromLive(): void {
|
|
if (!this.node?.isValid || !this.node.active) return;
|
|
const ctx = this._resolveRefreshContext();
|
|
if (!ctx) return;
|
|
const { w1, level, levelLimit, stats, enemyCamp } = ctx;
|
|
const element = Math.floor(Number(w1.herotype) || 0);
|
|
const staticViewKey = this._buildStaticViewKey(w1.id, level, element, enemyCamp);
|
|
if (staticViewKey !== this._staticViewKey) {
|
|
this.onInit();
|
|
return;
|
|
}
|
|
this._refreshDynamicStats(w1, level, stats);
|
|
if (this.lb_lvMax) this.lb_lvMax.string = `满级${levelLimit}级`;
|
|
}
|
|
|
|
destroyPanel(tweenType = 1) {
|
|
return new Promise((resolve) => {
|
|
const lifeGen = this._panelLifeGen;
|
|
const finish = () => {
|
|
if (lifeGen !== this._panelLifeGen) {
|
|
resolve(true);
|
|
return;
|
|
}
|
|
this.prepareDestroyForRender();
|
|
if (this.node.isValid) {
|
|
this.node.destroy();
|
|
}
|
|
resolve(true);
|
|
this.notifyPanelClose();
|
|
};
|
|
if (tweenType === 2 || tweenType === 3) {
|
|
gg.ui.popAnim.playClose(this.node, this._resolvePopSession(), tweenType, finish);
|
|
return;
|
|
}
|
|
finish();
|
|
});
|
|
}
|
|
|
|
click_ui_bgMask(event?: EventTouch): void {
|
|
const uiPos = event?.getUILocation();
|
|
const trySwitch = (this.prema as BattleWeaponDescHeroPanelData | null)?.trySwitchAtUiPos;
|
|
if (uiPos && trySwitch?.(uiPos)) {
|
|
return;
|
|
}
|
|
this.close(2);
|
|
}
|
|
|
|
/** 静态区缓存键 */
|
|
private _buildStaticViewKey(heroId: number, level: number, element: number, enemyCamp: boolean): string {
|
|
return `${Math.floor(Number(heroId) || 0)}|${Math.floor(Number(level) || 0)}|${element}|${enemyCamp ? 1 : 0}`;
|
|
}
|
|
|
|
/**
|
|
* ui_node:按 Hero.number 加载数字预制体,并切五行皮肤。
|
|
* @param w1 当前数字对应 Hero
|
|
*/
|
|
private _refreshNumber(w1: ITableHero): void {
|
|
if (!this.ui_node?.isValid) return;
|
|
this._clearNumNode();
|
|
|
|
const num = Math.floor(Number(w1.number) || 0);
|
|
if (!(num > 0)) {
|
|
console.warn(LOG, 'Hero.number 无效', w1.id, w1.number);
|
|
this.ui_node.active = false;
|
|
return;
|
|
}
|
|
const skinName = this._elementSkinName(Math.floor(Number(w1.herotype) || 0));
|
|
if (!skinName) {
|
|
console.warn(LOG, '五行皮肤未定义', w1.id, w1.herotype);
|
|
this.ui_node.active = false;
|
|
return;
|
|
}
|
|
|
|
let node: Node | null = null;
|
|
try {
|
|
node = gg.res.getNode(BATTLE_HERO_NUM_PATH, String(num), GBundle.BattleHero);
|
|
} catch (e) {
|
|
console.warn(LOG, '数字预制体加载失败', num, String(e));
|
|
this.ui_node.active = false;
|
|
return;
|
|
}
|
|
if (!node?.isValid) {
|
|
console.warn(LOG, '数字预制体缺失', num);
|
|
this.ui_node.active = false;
|
|
return;
|
|
}
|
|
|
|
node.parent = this.ui_node;
|
|
node.layer = this.ui_node.layer;
|
|
node.setPosition(0, 0, 0);
|
|
node.setScale(1, 1, 1);
|
|
node.active = true;
|
|
this._numNode = node;
|
|
this.ui_node.active = true;
|
|
this._playElementSkin(node, skinName, w1.id);
|
|
}
|
|
|
|
/** Hero.herotype → 数字 Spine 皮肤名 */
|
|
private _elementSkinName(herotype: number): string {
|
|
switch (herotype) {
|
|
case 1: return '金';
|
|
case 2: return '木';
|
|
case 3: return '水';
|
|
case 4: return '火';
|
|
case 5: return '土';
|
|
default: return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 数字节点切五行皮并播待机。
|
|
* @param node 数字预制体
|
|
* @param skinName 金木水火土
|
|
* @param heroId 日志用
|
|
*/
|
|
private _playElementSkin(node: Node, skinName: string, heroId: number): void {
|
|
const sk = node.getComponent(sp.Skeleton);
|
|
if (!sk?.isValid) {
|
|
console.warn(LOG, '数字节点缺少 Skeleton', node.name, heroId);
|
|
return;
|
|
}
|
|
const sd = sk.skeletonData;
|
|
if (!sd?.isValid) {
|
|
sk.enabled = false;
|
|
console.warn(LOG, '数字节点 skeletonData 无效', node.name, heroId);
|
|
return;
|
|
}
|
|
sk.enabled = true;
|
|
sk.setAnimationCacheMode(sp.SpineAnimationCacheMode.REALTIME);
|
|
ensureSpineSkeletonDataRuntime(sd);
|
|
if (!glyphSpineHasSkin(sk, skinName)) {
|
|
console.warn(LOG, '数字皮不存在', skinName, node.name, heroId);
|
|
return;
|
|
}
|
|
sk.setSkin(skinName);
|
|
sk.setSlotsToSetupPose();
|
|
const runtime = sd.getRuntimeData?.(true);
|
|
if (runtime?.findAnimation?.(NUM_IDLE)) {
|
|
sk.setAnimation(0, NUM_IDLE, true);
|
|
}
|
|
}
|
|
|
|
/** 归还数字预制体 */
|
|
private _clearNumNode(): void {
|
|
const held = this._numNode;
|
|
this._numNode = null;
|
|
if (!held?.isValid) {
|
|
if (this.ui_node?.isValid) this.ui_node.removeAllChildren();
|
|
return;
|
|
}
|
|
Tween.stopAllByTarget(held);
|
|
if (held['resKey']) gg.res.putNode(held);
|
|
else held.destroy();
|
|
if (this.ui_node?.isValid) this.ui_node.removeAllChildren();
|
|
}
|
|
|
|
/** 名称 / 满级 / 普攻技能文案 */
|
|
private _refreshStaticLabels(w1: ITableHero, level: number, levelLimit: number): void {
|
|
if (this.lb_name) {
|
|
this.lb_name.string = gg.lang.get(w1.name);
|
|
}
|
|
if (this.lb_lvMax) {
|
|
this.lb_lvMax.string = `满级${levelLimit}级`;
|
|
}
|
|
this._refreshSkillLabels(w1, level);
|
|
}
|
|
|
|
/**
|
|
* lb_skillName / lb_skillDesc:读 Hero.skill_id 对应 Skill.name / Skill.desc。
|
|
* 名称不回退 desc,避免与描述重复。
|
|
* @param w1 Hero
|
|
* @param _level 合成等级(skill_id 不按等级取下标)
|
|
*/
|
|
private _refreshSkillLabels(w1: ITableHero, _level: number): void {
|
|
const skillId = heroUltSkillId(w1);
|
|
const skill = skillId > 0
|
|
? gg.data.table.getTableData<ITableSkill>(TableNames.Skill, skillId)
|
|
: null;
|
|
if (skillId > 0 && !skill) {
|
|
console.warn(LOG, 'Skill 缺失', skillId, w1.id);
|
|
}
|
|
const nameKey = skill ? String(skill.name ?? '').trim() : '';
|
|
const descKey = skill ? String(skill.desc ?? '').trim() : '';
|
|
if (skill && !nameKey) {
|
|
console.warn(LOG, 'Skill.name 为空', skillId, w1.id);
|
|
}
|
|
if (this.lb_skillName) {
|
|
this.lb_skillName.string = nameKey ? gg.lang.get(nameKey) : '';
|
|
}
|
|
if (this.lb_skillDesc) {
|
|
this.lb_skillDesc.string = descKey ? gg.lang.get(descKey) : '';
|
|
}
|
|
}
|
|
|
|
/** 等级 / 攻击 / 攻速 */
|
|
private _refreshDynamicStats(
|
|
w1: ITableHero,
|
|
level: number,
|
|
stats: TowerRuntimeSnapshot['stats'] | undefined,
|
|
): void {
|
|
const setup = deriveTowerSetupStats(w1, level, 0);
|
|
const attack = stats?.attack ?? setup.attack;
|
|
const attackSpeed = stats?.attackSpeed ?? setup.attackSpeed;
|
|
|
|
if (this.lb_level) {
|
|
this.lb_level.string = `Lv.${level}`;
|
|
}
|
|
if (this.lb_attck) {
|
|
this.lb_attck.string = TowerStatDisplay.oneDecimal(attack);
|
|
}
|
|
if (this.lb_attckSpeed) {
|
|
this.lb_attckSpeed.string = TowerStatDisplay.attackSpeed(attackSpeed);
|
|
}
|
|
}
|
|
|
|
/** 缓存 gtemplate */
|
|
private _cacheGemTemplate(): void {
|
|
if (this._gemTpl?.isValid) return;
|
|
const list = this.ui_gemList;
|
|
if (!list?.isValid) return;
|
|
const tpl = list.getChildByName('gtemplate');
|
|
if (!tpl?.isValid) {
|
|
console.warn(LOG, 'ui_gemList/gtemplate 缺失');
|
|
return;
|
|
}
|
|
this._gemTpl = tpl;
|
|
tpl.active = false;
|
|
}
|
|
|
|
/**
|
|
* 刷新镶嵌列表:只画已镶嵌宝石;全空时只实例化 1 行「尚未镶嵌宝石」。
|
|
* @param element Hero.herotype 1~5
|
|
* @param enemyCamp 敌方不读本机镶嵌
|
|
*/
|
|
private _refreshGemList(element: number, enemyCamp: boolean): void {
|
|
this._cacheGemTemplate();
|
|
this._clearGemRows();
|
|
const list = this.ui_gemList;
|
|
const tpl = this._gemTpl;
|
|
if (!list?.isValid || !tpl?.isValid) return;
|
|
|
|
const gemIds: number[] = [];
|
|
if (!enemyCamp && element >= 1 && element <= 5) {
|
|
try {
|
|
const raw = gg.gem.getElementSlots(element);
|
|
for (let i = 0; i < 4; i++) {
|
|
const gemId = Math.floor(Number(raw[i]) || 0);
|
|
if (gemId > 0) gemIds.push(gemId);
|
|
}
|
|
} catch (err) {
|
|
console.warn(LOG, 'getElementSlots 失败', element, err);
|
|
}
|
|
}
|
|
|
|
if (gemIds.length <= 0) {
|
|
const row = instantiate(tpl);
|
|
row.parent = list;
|
|
row.active = true;
|
|
this._paintEmptyGemSlot(row);
|
|
this._gemRows.push(row);
|
|
this._updateGemLayout(list);
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < gemIds.length; i++) {
|
|
const gemId = gemIds[i];
|
|
const row = instantiate(tpl);
|
|
row.parent = list;
|
|
row.active = true;
|
|
const conf = gg.gem.getGem(gemId);
|
|
if (!conf) {
|
|
console.warn(LOG, '镶嵌宝石配表缺失', gemId, 'element', element);
|
|
this._paintEmptyGemSlot(row);
|
|
} else {
|
|
this._paintGemRow(row, conf);
|
|
}
|
|
this._gemRows.push(row);
|
|
}
|
|
this._updateGemLayout(list);
|
|
}
|
|
|
|
/** 填充一颗已镶嵌宝石行 */
|
|
private _paintGemRow(row: Node, conf: ITableWeapon): void {
|
|
const lbEmpty = row.getChildByName('lb');
|
|
if (lbEmpty?.isValid) lbEmpty.active = false;
|
|
|
|
const quality = Math.floor(Number(conf.quality) || 0);
|
|
const lblv = row.getChildByName('lblv')?.getComponent(Label);
|
|
if (lblv) {
|
|
lblv.node.active = true;
|
|
lblv.string = `Lv.${quality}`;
|
|
}
|
|
const icon = row.getChildByName('icon');
|
|
if (icon?.isValid) {
|
|
icon.active = true;
|
|
const iconName = String(conf.icon || '').trim();
|
|
if (!iconName) console.warn(LOG, '宝石 icon 缺失', conf.id);
|
|
this._setItemSprite(icon, iconName ? `weapon/${iconName}` : '');
|
|
}
|
|
const lbName = row.getChildByName('lbName')?.getComponent(Label);
|
|
if (lbName) {
|
|
lbName.node.active = true;
|
|
lbName.string = this._gemBaseName(conf);
|
|
}
|
|
const lbDesc = row.getChildByName('lbDesc')?.getComponent(Label);
|
|
if (lbDesc) {
|
|
lbDesc.node.active = true;
|
|
lbDesc.string = gg.lang.get(conf.desc);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 宝石展示名:去掉配表名尾部「N级」,等级改由 lblv 的 Lv.N 展示。
|
|
* @param conf Weapon 行
|
|
*/
|
|
private _gemBaseName(conf: ITableWeapon): string {
|
|
const raw = gg.lang.get(conf.name);
|
|
const stripped = String(raw).replace(/\d+级\s*$/, '').trim();
|
|
return stripped || raw;
|
|
}
|
|
|
|
/**
|
|
* 空槽:只显示 lb,文案固定「尚未镶嵌宝石」。
|
|
* @param row 槽位行
|
|
*/
|
|
private _paintEmptyGemSlot(row: Node): void {
|
|
const lbEmpty = row.getChildByName('lb')?.getComponent(Label);
|
|
if (lbEmpty) {
|
|
lbEmpty.node.active = true;
|
|
lbEmpty.string = GEM_EMPTY_TIP;
|
|
}
|
|
const lblv = row.getChildByName('lblv');
|
|
if (lblv?.isValid) lblv.active = false;
|
|
const icon = row.getChildByName('icon');
|
|
if (icon?.isValid) {
|
|
icon.active = false;
|
|
this._setItemSprite(icon, '');
|
|
}
|
|
const lbName = row.getChildByName('lbName');
|
|
if (lbName?.isValid) lbName.active = false;
|
|
const lbDesc = row.getChildByName('lbDesc');
|
|
if (lbDesc?.isValid) lbDesc.active = false;
|
|
}
|
|
|
|
/** Items bundle 图集 */
|
|
private _setItemSprite(node: Node | null, path: string): void {
|
|
if (!node?.isValid) return;
|
|
if (!path) {
|
|
node.getComponent(SpriteLoad)?.clear();
|
|
const spComp = node.getComponent(Sprite);
|
|
if (spComp) spComp.spriteFrame = null;
|
|
return;
|
|
}
|
|
if (!node.getComponent(Sprite)) node.addComponent(Sprite);
|
|
node.getOrAddComponent(SpriteLoad).setSprite(path, GBundle.Items);
|
|
}
|
|
|
|
/** 清理宝石克隆行(保留模板) */
|
|
private _clearGemRows(): void {
|
|
for (let i = 0; i < this._gemRows.length; i++) {
|
|
const n = this._gemRows[i];
|
|
if (n?.isValid) n.destroy();
|
|
}
|
|
this._gemRows.length = 0;
|
|
if (this._gemTpl?.isValid) this._gemTpl.active = false;
|
|
}
|
|
|
|
/** 触发布局 */
|
|
private _updateGemLayout(list: Node): void {
|
|
const lay = list.getComponent(Layout);
|
|
if (lay) lay.updateLayout();
|
|
}
|
|
|
|
private _clearLabels(): void {
|
|
if (this.lb_name) this.lb_name.string = '';
|
|
if (this.lb_level) this.lb_level.string = '';
|
|
if (this.lb_lvMax) this.lb_lvMax.string = '';
|
|
if (this.lb_attck) this.lb_attck.string = '';
|
|
if (this.lb_attckSpeed) this.lb_attckSpeed.string = '';
|
|
if (this.lb_skillName) this.lb_skillName.string = '';
|
|
if (this.lb_skillDesc) this.lb_skillDesc.string = '';
|
|
// lb_listDesc 为预制体静态文案「当前镶嵌宝石:」,不清空
|
|
}
|
|
|
|
private _applyPanelPosition(data: BattleWeaponDescHeroPanelData | null): void {
|
|
const weaponWorld = data?.weaponWorldPos;
|
|
if (!weaponWorld || !this._popupBody?.isValid) return;
|
|
|
|
const rootUt = this.node.getComponent(UITransform);
|
|
const bodyUt = this._popupBody.getComponent(UITransform);
|
|
if (!rootUt || !bodyUt) return;
|
|
|
|
const step = data?.gridStep ?? 93;
|
|
const halfW = bodyUt.contentSize.width * 0.5;
|
|
const halfH = bodyUt.contentSize.height * 0.5;
|
|
const gap = UIBattleWeaponDescHero.PANEL_GAP_GRIDS;
|
|
const weaponLocal = rootUt.convertToNodeSpaceAR(
|
|
new Vec3(weaponWorld.x, weaponWorld.y, weaponWorld.z ?? 0),
|
|
);
|
|
|
|
const x = 0;
|
|
const y = weaponLocal.y + step * (0.5 + gap) + halfH;
|
|
|
|
const rootHalfW = rootUt.contentSize.width * 0.5;
|
|
const rootHalfH = rootUt.contentSize.height * 0.5;
|
|
const clampedX = Math.max(-rootHalfW + halfW, Math.min(rootHalfW - halfW, x));
|
|
const clampedY = Math.max(-rootHalfH + halfH, Math.min(rootHalfH - halfH, y));
|
|
this._popupBody.setPosition(clampedX, clampedY, 0);
|
|
}
|
|
|
|
private _ensurePanelVisible(): void {
|
|
if (!this.node?.isValid) return;
|
|
|
|
this.node.active = true;
|
|
Tween.stopAllByTarget(this.node);
|
|
|
|
const body = this._popupBody;
|
|
if (body?.isValid) {
|
|
body.active = true;
|
|
const renderers = body.getComponentsInChildren(UIRenderer);
|
|
for (let i = 0; i < renderers.length; i++) {
|
|
const r = renderers[i];
|
|
if (r?.isValid) r.enabled = true;
|
|
}
|
|
}
|
|
|
|
this._keepMaskTouchOnly();
|
|
}
|
|
|
|
private _keepMaskTouchOnly(): void {
|
|
if (!this.ui_bgMask?.isValid) return;
|
|
|
|
Tween.stopAllByTarget(this.ui_bgMask);
|
|
const maskSpr = this.ui_bgMask.getComponent(Sprite);
|
|
if (maskSpr) maskSpr.enabled = false;
|
|
|
|
const op = this.ui_bgMask.getComponent(UIOpacity);
|
|
if (op) op.opacity = 0;
|
|
}
|
|
|
|
private _resolvePopSession(): IPopAnimSession {
|
|
const cfg = this.uiInfo?.uiconf?.popAnim as IPopAnimConfig | undefined;
|
|
return {
|
|
mask: cfg?.mask ?? 'none',
|
|
center: cfg?.center ?? 'none',
|
|
};
|
|
}
|
|
}
|
|
|